ROS 移动机器人调试:31 个现场故障,归并成 5 类根因
比赛前一周,车在场上跑三分钟就撞了。接下来七天,我们记下了 31 个问题。
修完最后一个,我把它们排成一张表——真正的原因只有 5 类,没有一个是「ROS 不会用」。
先说清楚边界:这是一台麦轮底盘 + 5 自由度机械臂 + 激光雷达 + 深度相机的竞赛车,跑 ROS 1 Melodic。我们在厂商例程上做二次开发,所以下面不放源码,只放诊断路径和那些能复现的数字。
如果你也在调移动机器人,这篇可以帮你少走一半弯路。
一、先看结论
| 根因 | 次数 | 典型表现 | 一句话判断依据 |
|---|---|---|---|
| ① 用时间代替距离 | 6 | 车冲过头 / 差十几厘米 | 代码里有没有「距离」这个变量?没有就是开环 |
| ② 改了没生效 | 7 | 改完行为完全不变 | rospack find + grep + rosparam get 三连 |
| ③ 一份逻辑散落多处 | 4 | 改了一处,别处还是旧的 | 同一个常量在几个文件里出现? |
| ④ 假反馈 | 7 | 硬件坏了日志还一片正常 | 这个「正常」是测量的,还是编出来的? |
| ⑤ 一次改多个变量 | 贯穿 | 好了不知道好在哪 | 上一次只改了一处是什么时候? |
下面逐个说,每个都给判断方法。
二、根因①:用时间代替距离
代码长这样:
# 示意
cmd.linear.x = 0.2
publish(cmd)
sleep(2) # 以为走了 40cm,其实只是"发了 2 秒指令"
这行代码里没有「距离」这个量。真实走多远,取决于打滑、启动延迟、地面摩擦——不是 0.2×2。
最惨的一次是两段开环叠加:前面盲冲 40cm,后面推进 17.5cm,一共 57.5cm。而那轮任务的停点本该离平台 26.5cm。车冲出去 31cm,撞翻了对面的道具。
这个 31cm 能算得严丝合缝:
40 + 17.5 = 57.5cm
57.5 - 26.5 = 31cm
判断方法:搜代码里所有 sleep,凡是后面跟着速度指令的,都是开环。
改法:换成里程计闭环,20 行左右。
# 示意
def move_by_odom(distance, speed, timeout):
x0, y0 = odom_xy()
while not timeout_expired():
if hypot(odom_xy(), (x0, y0)) >= distance:
break
if stalled(0.3s): # 0.3 秒几乎没动 = 顶住了
break
cmd.linear.x = speed
publish(cmd)
stop()
多写 20 行,换来「走多少就是多少」。
三、根因②:改了,但没生效
这是最耗时间的一类。三个晚上,最后都发现是「没生效」而不是「改错了」。
中间隔着五道关:
| 我以为 | 实际 | 怎么发现 |
|---|---|---|
| 改了本地副本 | 车上跑的是另一个路径 | rospack find <包名> |
| 改了 config | 没重启 launch,参数只在启动时加载一次 | 改完必须重启 |
| 加了新参数 | 代码压根没读这个键 | grep -rn "键名" scripts/ |
| 改了某个处理函数 | 那个服务没有任何调用者 | grep 调用点 |
| 改了一个参数 | 全包只有 config 里出现过一次 | 没人读它 |
三个命令,10 秒:
rospack find <包名> # 我在改哪一份?
grep -rn "改动点" <包>/scripts/ # 这段代码真的会被执行吗?
rosparam get /参数名 # ROS 里真的是新值吗?
第三行特别注意:加了新参数不等于有人读它。我们有两个参数,改了整整一个月,从没生效过——因为代码里根本没有 get_param 去取它。
四、根因③:你读的代码,不是跑的代码
同一个「待机姿态」,在我们包里写了四遍,分布在四个文件。改一处,另外三处还是旧的。
于是出现了那个经典现象:
对准时机械臂抬起来了,一进入夹取,它又掉了回去。
当时以为是舵机坏了。真相是:「对准」和「夹取」是两段代码,各写了一套姿态值,我们只改了其中一段。
更隐蔽的是死代码:有三个服务注册得整整齐齐,主控里却从来没有调用过。你改它们,效果恒等于零。
判断方法:
grep -rn "服务名\|函数名" <包>/scripts/ # 看调用点,而不是看定义
定义存在、调用为 0,就是死代码。
真正的教训:参数不是数字,是物理量。同一个物理量只能有一个来源。
五、根因④:没报错,就等于没问题?
在纯软件世界里基本成立。在机器人里完全不成立。
我们排查出四个把故障藏起来的开关:
| 开关 | 它做了什么 | 后果 |
|---|---|---|
| 读位置直接返回目标值 | 不问总线,把「你让它去的位置」当反馈 | 舵机烧了,位置依然完美 |
| 电压写死成常量 | 不测量,直接返回 9.0 | 电池快没电也看不出来 |
| 在线检查被注释 | 掉线不告警 | 一台舵机没了,日志一片安静 |
| IK 无解时静默返回 | 空列表就跳过整段动作 | 机械臂没动,系统认为它动了 |
最典型的是一条启动日志:
Pinging motor IDs 1 through 10...
Found 7 motors, initialization complete.
看着像体检报告。实际上:配置里给了设备列表,代码就把列表长度打印出来,一次总线都没问过。
那句「Found 7」的真实含义是:配置里写了 7 个。
判断方法:对任何一条「看起来正常」的信息,问一句——这是测量的,还是编的?
六、根因⑤:一次改三个地方
这条不像技术问题,但它是前面所有的放大器。
我们干过:把 sleep(2) 改 2.75,同时把视觉对齐注释掉——整个流程变成纯开环,总盲冲 72.5cm。跑完更糟,但已经分不清是哪个改动导致的。
还有一次:push_time 从 3.3 改成 0.2,同时动了别的地方。车确实不冲了,但两天后才明白——它不冲是因为根本走不到。
规矩:一次只改一个数,改完立刻跑一趟。赛前尤其重要,改多处等于放弃归因能力。
七、一页检查表(可以直接抄)
上车前
- 所有
sleep后面跟速度的,标记为疑似开环 -
rospack find确认改的是车上那份 - 备份一份能跑的版本
改完之后
-
grep -n "新值" 文件路径 -
rosparam get /参数名 -
python -m py_compile 改动的文件(Python 项目) - 只改了这一个数(对照上面写的计划)
卡住时,按层问
任务层 流程走到哪一步? → 决策层 状态机/目标对不对?
规划层 逆运动学有解吗? → 控制层 PID 输出饱和了吗?
通信层 topic 有数据吗? → 驱动层 串口有应答吗?
硬件层 手动能扳动吗?摸一下温度
每往下切一层,怀疑范围减半。
最后
31 个问题里没有一个是因为「不会 ROS」。全是方法问题:
改了一处,但生效的地方在另一处;改完没确认生效,就以为改错了。
我们带着纯软件世界的三个假设进了机器人世界——改了就会生效、没报错就是没问题、设定了就会执行。三个假设,全是错的。
基于某教育机器人平台的竞赛例程做二次开发;文中代码片段为示意重写,不涉及原始实现。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)