1. Tool I/O 到底控制的是什么:从法兰上那几根线说起
第一次拆开末端法兰盖板的时候,我是有点意外的——线没几根,一个七八针的航空插头,接出去的就是两路数字输入、两路数字输出,外加一路模拟输入和一组工具端供电。这就是 Aubo 协作臂的 Tool I/O。别看它规模小,凡是"机器人要动手"的场景,几乎都绕不开它:夹爪的张开闭合、吸盘的真空阀、焊枪的气阀、气爪的到位检测、末端力传感器的触发信号,最后都要落到这几根线的电平上。
很多人刚接触 Aubo 的时候会把 Tool I/O 和控制柜背后的 Board I/O 搞混。这两者逻辑上是一套东西——都是数字量输入输出——但物理位置、用途和接线方式完全不同。Board I/O 在控制柜上,走的是柜内端子,适合接外围设备:安全门、光幕、外部启动按钮、输送线信号。Tool I/O 在机械臂末端法兰上,是跟着手臂一起动的,专门伺候装在末端的执行器。这个区别决定了你在选型时的第一原则:跟着末端跑的信号走 Tool I/O,固定不动的信号走 Board I/O。
为什么这个区分重要?因为 Tool I/O 的线要经过手臂的每一个关节,走线长度、弯折次数、电磁环境都比柜内恶劣得多。你把一个不需要跟着动的输送线信号接到末端去,等于给自己埋了一颗线缆疲劳断裂的雷。我见过一个现场,把气缸的电磁阀装在底座、控制线却拉到了末端 Tool I/O 上,运行三个月后线在第三轴处断了,排查花了大半天。这不是 Tool I/O 的问题,是用法的问题。
从"学习"的角度看,Tool I/O 是理解协作臂软硬件分层的一个极好的切口。它同时牵涉三件事:电气层(电平、负载、隔离)、通信层(SDK 怎么把一条指令送到控制柜)、控制层(什么时候发这条指令,发了之后怎么确认生效)。三层里任何一层没想清楚,现象都是同一个——"我设了输出,但夹爪没动"。所以这篇东西我不打算只贴几行 API,而是把这三层拆开讲,顺便把我踩过的坑一次性摆出来。
需要先说明的是,不同型号、不同固件版本的末端 I/O 配置是有差异的,有的型号末端是两进两出,有的还带模拟量,工具端供电支持 12V/24V 切换的也见过。下面提到的路数、电流、阈值都是常见配置下的典型值,具体参数一定以你手上那台机器随附的接口手册为准,我这里给的是让你能建立判断框架的量级,不是让你照抄的绝对值。
2. 接线之前先搞清楚的电气边界:PNP/NPN、Tool Power 与负载电流
我在教别人的时候有个习惯:先不教怎么写代码,先让人拿万用表把末端插头的每一根线量一遍。这一步不做,后面所有的调试都是在猜。Tool I/O 的坑有八成出在接线,而不是出在程序。
2.1 输出到底是"给高电平"还是"拉低电平"
数字输出的核心问题是极性。工业上常见的两种形态:源型输出(输出高电平,俗称 PNP 型)和漏型输出(输出拉低到地,俗称 NPN 型)。你接一个中间继电器或者电磁阀,如果极性和输出类型不匹配,表现就是"程序里置 1 了,但负载纹丝不动",或者更隐蔽的——负载动了但逻辑是反的。
判断方法很直接:在示教器或者调试工具里把某一路 DO 强制置 1,用万用表直流电压档,红表笔接 DO 信号线,黑表笔接 Tool I/O 的公共端(GND)。读数接近 24V,说明是源型输出;读数接近 0V 而负载端到电源正极之间有电压,那大概率是漏型。这一步量完,你后面选继电器、选电磁阀、甚至选固态继电器的型号,心里就有底了。
还有一个容易忽略的点:工具端的供电(Tool Power)本身可能是可配置的。有些机器人在软件里能选 12V 或 24V 输出给末端设备用,如果你的夹爪控制器标称 24V,而工具端电源被配成了 12V,那夹爪可能"能动但没劲",或者干脆不启动。这种情况最容易误判成"IO 没通",实际上是供电电压不对。所以在怀疑程序之前,先把工具端供电的电压和电流能力核实清楚。
2.2 感性负载必须加续流二极管,这不是可选项
我在一个现场吃过这个亏。末端接的是个小型电磁阀,程序跑得好好的,用了两周之后某一路 DO 突然永久失效——置 1 也没输出。拆开一看,输出级的光耦击穿了。原因就是电磁阀线圈断电瞬间产生的反向电动势,几十伏甚至上百伏的尖峰反复冲击输出端,迟早击穿。
所有感性负载——继电器线圈、电磁阀、直流电机——在 Tool I/O 的输出端都必须并联续流二极管,方向是反向并联(阴极接正、阳极接地),让断电瞬间的感应电流有回路泄放。这个二极管成本几毛钱,但能救一路输出。我现在的习惯是:只要末端接了任何线圈类负载,一律先加二极管和 RC 吸收,再谈程序。顺便说一句,负载电流也要核,常见 Tool DO 单路持续电流在几百毫安量级,超了要么加中间继电器扩流,要么换用外部驱动板,不要让末端 I/O 直接驱动大电流负载。
2.3 独立供电时必须共地,否则电平会"飘"
末端夹爪如果自带电源适配器独立供电,那么夹爪的信号地和机器人的 Tool I/O 地必须是同一个参考点。不共地的后果是:机器人的"高电平"在夹爪看来可能只有十几伏,甚至因为浮地而随机跳变,表现出来就是输入信号时有时无、偶发误触发。这种故障最难查,因为它不规律。
我的做法是:末端设备统一从机器人的 Tool Power 取电(在功率允许的前提下),省掉共地的麻烦;如果必须独立供电,一定把两边的 GND 用一根线连起来,并且这根线要和信号线分开走,别绑在一起。
2.4 线缆走线是被严重低估的一环
末端线缆跟着手臂运动,这是它和控制柜内接线最大的不同。走线要留出各轴的行程余量,用扎带或线夹固定在法兰附近,避免在关节处形成锐角弯折。屏蔽层建议单端接地(一般接控制柜侧),不要两端都接,否则容易形成地环路引入干扰。
有个现场案例值得说:客户反映夹爪到位信号偶发丢失,只在高速运动时出现。查了三天程序没结果,最后发现是末端线缆在第四轴转动时被反复拉扯,内部一根信号线处于半断状态,静止时通、运动时断。这种问题万用表静态测量是测不出来的,得在运动状态下测,或者直接换线验证。所以我的经验是:只要故障和"运动"有相关性,第一怀疑对象是线,不是程序。
3. 用代码把第一路输出点亮:SDK、脚本与示教器三条路径
电气层确认没问题之后,才开始碰软件。Aubo 这边控制 Tool I/O 大致有三条路径,用途不一样,我按使用场景分开讲。
3.1 Python SDK:做上位机集成时最顺手
Python 这条路适合把机械臂接进一个更大的系统,比如视觉引导、产线调度、上位机监控。典型的调用链路是:建立 RPC 连接 → 登录 → 拿到机器人接口 → 拿到 IO 控制接口 → 读写。核心代码大概长这样:
import time import pyaubo_sdk ROBOT_IP = "192.168.0.100" ROBOT_PORT = 30004 client = pyaubo_sdk.RpcClient() client.connect(ROBOT_IP, ROBOT_PORT) client.login("aubo", "123456") robot = client.getRobotInterface() io = robot.getIoControl() # 置位第 0 路工具端数字输出 io.setToolDigitalOutput(0, True) time.sleep(0.05) # 立刻回读,确认状态真的变了 print("DO0 =", io.getToolDigitalOutput(0)) # 读第 0 路工具端数字输入(比如夹爪到位信号) print("DI0 =", io.getToolDigitalInput(0))几点实测心得。第一,login的默认账号密码在很多出厂配置下就是示例里那组,但上线前必须改,这是台联网设备,不是玩具。第二,setToolDigitalOutput的索引从 0 开始,不是 1,第一次用容易搞错,置了 0 号以为在控制 1 号。第三,也是最重要的一点:设置之后一定要回读。回读能区分两种情况——指令没发出去,和指令发出去了但硬件没响应。这个区别决定了你后面往哪个方向查。
C++ SDK 的逻辑一模一样,只是接口形态不同,适合对实时性要求更高或者要嵌入现有 C++ 框架的场景:
auto io = robot_interface->getIoControl(); io->setToolDigitalOutput(0, true); bool state = io->getToolDigitalOutput(0); bool di = io->getToolDigitalInput(0);工具端供电类型如果需要在软件里切换,不同版本的接口命名会有差异,有的叫setToolPowerType,有的放在别的命名空间下,这个别照抄网上的代码,直接翻你本地 SDK 头文件或者接口文档确认,几分钟的事,比试错快。
3.2 示教器脚本:让 IO 动作和轨迹咬在一起
如果 IO 是为了配合一段固定的轨迹动作(比如"移动到位 → 夹紧 → 抬起"),那放在示教器脚本里比放在上位机更合适。原因是脚本和运动指令在同一个执行上下文里,时序天然同步,不会出现"上位机发了夹紧指令,但机械臂还在走"的错位。
脚本里控制 Tool I/O 的写法通常是这种形式:
-- 到位后夹紧 set_tool_do(0, 1) sleep(0.1) local closed = get_tool_di(0) if closed == 1 then -- 确认夹紧后再抬起 movej(pose_above, a, v, 0, 0) else -- 夹爪没到位,报错停机 halt() end不同固件版本里函数名可能有细微差别,敲之前在脚本编辑器的函数列表里搜一下关键词,比凭记忆写靠谱。这段代码里最有价值的不是set_tool_do,而是那个if——用输入信号确认执行结果,而不是假设它一定成功。夹爪没夹住就抬起来,工件掉在半空,这种事故我见过不止一次。
3.3 示教器的 IO 面板:调试阶段的主力工具
真正排查问题时,示教器上的 IO 状态面板是最快的窗口。它能实时显示每一路 DI/DO 的电平状态,也能手动强制输出。我的排查流程一般是:先在这块面板上强制某一路 DO,看负载动不动。动了,说明电气和接线没问题,问题在程序;不动,直接回去查线。这一步能把排查范围砍掉一半,比在代码里加一堆打印高效得多。
4. 输出设了却没动作:一次完整的排查链路复盘
我遇到过最典型的一次,值得完整复盘。现象是:程序里setToolDigitalOutput(0, True)执行了,日志也打出来了,但夹爪完全没反应。示教器面板上那一路 DO 的指示灯也是亮的。
4.1 第一层:指令层是否真的生效
先别急着拆线。第一步是回读——getToolDigitalOutput(0)返回 True,说明控制柜内部的状态寄存器确实置位了。这一步排除了"指令没发出去"和"发到了错误的路"这两种可能。如果回读是 False,那问题在通信或索引上,跟硬件无关。
同时要确认一件事:这台机器上的 IO 是不是被别的进程或者后台脚本占用了。我碰到过一次,示教器里跑着一个循环脚本在不停地把那一路 DO 拉低,上位机设了 True,几十毫秒后又被拉回 False,表现出来就是"设了没用"。这种冲突不看代码是发现不了的,得把所有可能操作 IO 的地方都过一遍。
4.2 第二层:物理输出端是否有电压
回读正常,下一步上万用表。量 DO 信号线对公共端的电压。这里有三种结果,对应三条完全不同的排查方向:
| 万用表读数 | 说明 | 下一步 |
|---|---|---|
| 约 24V(源型) | 输出级正常,负载侧问题 | 查继电器、查线、查负载供电 |
| 约 0V 但负载端有电压 | 可能是漏型输出,极性接反 | 核对负载接线极性 |
| 完全没有电压 | 输出级损坏、工具电源未开、或处于禁止输出状态 | 查工具供电、换一路 IO 验证 |
我那次是第三种,完全没有电压。但示教器面板显示是亮的,这就产生了矛盾——面板显示的是软件状态,不是物理电平。这一点很多人会混淆:软件状态为 1 不等于端子有 24V。
4.3 第三层:换一路 IO 做交叉验证
怀疑某一路输出损坏时,最快的验证方法是把负载换到另一路 DO 上,程序里改个索引。如果另一路正常,基本可以确认原来那一路的输出级有问题。我那次就是这么确认的——换到 1 号 DO,夹爪正常工作。
回头查原因,就是我前面说的感性负载没加续流二极管,电磁阀的反向尖峰把 0 号路的输出光耦打穿了。这个教训之后,我所有的末端接线方案里,续流二极管是强制项,写在接线图上的。
4.4 第四层:容易被忽略的"隐性禁止"
还有一种情况更隐蔽:IO 本身没问题,但机器人处于某种状态导致输出被禁止。比如急停按下、安全门打开、或者处于某种特殊运行模式。不同型号的处理策略不一样,有的会直接切断工具端输出,有的保持。这个必须查手册确认,不能想当然。
我的建议是:调试阶段一旦遇到"IO 完全不响应",先看一眼机器人当前的运动状态和安全状态,再看 IO。这个顺序能省很多时间。
4.5 第五层:输入侧的"假信号"
输出侧排查完了,输入侧同样有坑。Tool DI 接的是夹爪的到位信号,如果这个信号是 NPN 型(拉低有效)而机器人端按源型理解,那读到的逻辑就是反的——夹爪明明夹紧了,程序读到的是 0。这种情况在混用不同品牌末端设备时特别常见。
判断方法还是万用表:夹爪动作到位时,量 DI 端子的电压。如果到位时是 0V、未到位时是 24V,那就是低电平有效,程序里的判断逻辑要反过来写。别嫌麻烦,这一步不做,后面所有的闭环判断都是错的。
5. 让夹爪动作和运动轨迹咬合:时序、回读与闭环等待
IO 通了只是开始,真正难的是让它和运动配合得刚刚好。我见过太多程序是这么写的:发个夹紧指令,sleep(0.5),然后直接抬起来。这种写法在现场跑一天可能没事,跑一周就会出一次工件掉落。
5.1 为什么 sleep 是靠不住的
sleep的问题在于它是固定值,而实际执行时间是可变的。夹爪的气路压力会波动、密封件会磨损、环境温度会影响动作速度。你按新夹爪调出来的 0.5 秒,三个月后可能就不够了。更麻烦的是,这种失败是概率性的,不出事的时候你根本不知道它有问题。
正确的做法是用输入信号做闭环等待:发指令之后,循环读 DI,直到读到"已夹紧",或者超时。
def wait_tool_di(io, index, expect, timeout=2.0, period=0.01): """等待某一路 Tool DI 达到期望电平,超时返回 False""" t0 = time.time() while time.time() - t0 < timeout: if bool(io.getToolDigitalInput(index)) == expect: return True time.sleep(period) return False # 使用:关夹爪,等待到位信号 io.setToolDigitalOutput(0, True) if not wait_tool_di(io, 0, True, timeout=2.0): raise RuntimeError("夹爪未在超时时间内到位")这段代码的价值在于:它把"动作完成"的定义从"过了多少时间"改成了"信号到了没有"。超时机制又保证了不会无限等下去——夹爪真的坏了,程序要报错停机,而不是挂在那里。
period这个参数也别随便给。给太小(比如 1ms),RPC 调用本身的开销会让你白白占用资源;给太大(比如 200ms),响应就迟钝。我一般用 10ms 到 20ms,实测下来响应和开销都合适。
5.2 顺序上的两个经典错误
第一个错误是"先动后夹"。程序里运动指令和 IO 指令写反了,机械臂已经抬起来了,夹爪才闭合。这种错误在纯文本代码里很显眼,但在图形化编程或者多线程架构里就不那么明顯了。多线程场景下尤其要注意:如果 IO 控制在一个线程、运动控制在另一个线程,两个指令的先后顺序是没有保证的,必须加同步机制。
第二个错误是"夹了不等"。指令发出去了,程序继续往下走,但夹爪还没到位。这个问题在低速、轻载时看不出,一旦节拍提上来就暴露。解决办法就是上面那个闭环等待。
5.3 松开的时候同样需要确认
很多人只对"夹紧"做到位检测,松开就不管了。但如果下一动作是伸进一个狭窄空间取件,夹爪没完全张开就伸进去,会撞。所以如果夹爪有"完全张开"的到位信号,同样要做闭环等待。没有这个信号的话,那就退而求其次,用一个稍保守的固定延时,但要清楚这是妥协,不是最佳实践。
5.4 时序精度能到什么程度
有人会问,这套东西的响应延迟有多大。实测量级上:从 SDK 调用到端子电平变化,通常在毫秒到十几毫秒之间,取决于控制周期和通信链路。加上夹爪本身的机械动作时间(气爪一般几十到几百毫秒),整体在百毫秒量级。
对于绝大多数抓取场景,这个精度是够的。但如果你要做的是高速飞拍、动态跟踪这类应用,就不能依赖软件层的 IO 时序了,得考虑硬件触发或者更底层的同步机制。这个边界要心里有数,别拿通用 IO 去做硬实时的事。
6. 两进两出不够用时的扩展思路与工程化封装
Tool I/O 的路数天生就少,两进两出在很多场景下是不够的:一个夹爪就吃掉一出一进,再加个真空检测、再加个吹气阀,立刻见底。这时候有几条路可以走。
6.1 扩展路数的三种方案对比
| 方案 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| 用控制柜 Board I/O | 信号不需要跟着末端动 | 路数多、接线方便 | 线要拉到柜子,末端设备不方便 |
| 末端加 IO 扩展模块(走总线) | 末端设备多、需要跟着动 | 扩展灵活、可级联 | 增加成本和末端重量 |
| 末端设备自带通信(如夹爪走串口/总线) | 夹爪、力传感器等智能末端 | 省 IO、能读更多状态 | 需要额外的通信集成工作 |
我的选择逻辑是:先看信号要不要跟着动。不动的,一律走 Board I/O,别浪费末端的宝贵路数。要动的,再看末端设备是不是智能的——现在很多电动夹爪支持总线控制,开合度、夹持力、到位状态全都能读回来,比用两路 IO 表达的信息丰富得多,这种情况用通信比用 IO 更划算。只有当末端是纯气动或者简单电磁阀时,才老老实实用 Tool I/O。
6.2 封装一层,别让业务代码碰裸接口
项目里如果到处散落着setToolDigitalOutput,维护起来会很痛苦。我的习惯是封一个薄薄的一层,把索引、极性、超时这些细节收进去:
class ToolIO: def __init__(self, io_control, do_map, di_map): self.io = io_control self.do_map = do_map # 名称 -> 索引 self.di_map = di_map def set(self, name, value): idx = self.do_map[name] self.io.setToolDigitalOutput(idx, bool(value)) # 回读校验,不一致直接抛异常 if bool(self.io.getToolDigitalOutput(idx)) != bool(value): raise IOError(f"Tool DO[{name}] 设置后回读不一致") def get(self, name): return bool(self.io.getToolDigitalInput(self.di_map[name])) def wait(self, name, expect, timeout=2.0): ... # 同前面的闭环等待这层封装带来三个好处。一是业务代码里出现的是gripper.close()而不是setToolDigitalOutput(0, True),可读性天差地别。二是索引变更时只改一处。三是把"设置后回读校验"这个动作固化了,任何人调用都不会忘记验证。第三条是最有价值的——把经验固化成代码约束,比写在文档里靠人记住靠谱得多。
6.3 上下电和异常恢复的顺序
还有一个容易被忽略的工程细节:上电初始化的顺序。我的做法是,机器人上电、连接建立之后,先把所有 Tool DO 显式置为安全状态(一般是全断),再开始业务逻辑。原因是控制柜重启后 IO 的初始状态不一定是你期望的,如果上一次异常掉电时某一路是导通的,重启后可能还是导通的,这时候如果末端接着气缸,可能会突然动作。
异常恢复同理。程序捕获到异常、准备停机之前,应该主动把执行器置于安全状态——夹爪松开或者保持(看工艺要求),气阀关闭。这个动作要写在异常处理里,不能指望"反正停电了都会断"。
6.4 日志要记到什么粒度
调试期和量产期的日志需求不一样。调试期我会把每一次 IO 操作和回读结果都打出来,包括时间戳,这样复盘时序问题时有据可查。量产期则收敛到只记录状态变化和异常,否则日志量太大。但有一条我会一直保留:每次闭环等待的超时事件必须记录,哪怕最后成功了。因为超时次数是设备劣化的早期信号——夹爪密封件老化、气路压力下降,最先表现出来的就是"偶尔要等更久才到位"。盯着这个指标,能在故障发生前做预防性维护。
这个思路是我从一次连续三天的偶发故障里总结出来的。当时工件偶尔掉落,查不出原因,最后翻历史日志发现有几次夹紧等待接近超时但没超时,说明夹爪已经在劣化边缘。换了密封件之后问题消失。如果当初没记这些数据,可能还要再查一周。
6.5 一点个人体会
Tool I/O 这东西,API 就那几个,看文档十分钟就能写出来。真正花时间的是那些文档里不会写的部分:二极管要不要加、线怎么走、等待用信号还是用延时、索引改了哪里要同步改。这些东西没有捷径,都是一次次现场趟出来的。我现在拿到一台新机器,第一件事不是写程序,是拿万用表把末端每一根线量一遍,把极性、电压、有效电平记在本子上。这个习惯让我少走了很多弯路,也建议你这么干。