比赛前一周,车在场上跑三分钟就撞了。接下来七天,我们记下了 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」。全是方法问题:

改了一处,但生效的地方在另一处;改完没确认生效,就以为改错了。

我们带着纯软件世界的三个假设进了机器人世界——改了就会生效、没报错就是没问题、设定了就会执行。三个假设,全是错的。

基于某教育机器人平台的竞赛例程做二次开发;文中代码片段为示意重写,不涉及原始实现。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐