AB
AiBoss
チュートリアル

从摇杆到车轮:移动机器人控制链路拆解教程

チュートリアル

从摇杆到车轮:移动机器人控制链路拆解教程

面向有 AI 或软件背景、但刚接触机器人的人,逐层拆解一次遥控操作在系统内部走过的完整链路:摇杆输入如何变成速度指令,速度指令如何穿过上位机与下位控制器的边界,四个轮子如何被算出各自转速,以及编码器与 IMU 的反馈为什么不可省略。同时给出链路排查顺序,解释「指令在软件里看得见」与「电机真的转了」之间的差别。

很多人对 AI 的印象停留在屏幕里:读图、听声音、回答问题、写代码、做规划。但一旦要求 AI 在真实世界里「做点什么」,问题就变了。比如对机器人说一句「去隔壁房间」,让大模型理解这句话并不难,难的是理解之后——内存里的一句话,怎么让真实的轮子转起来?轮子转完之后,机器人又怎么知道自己走对了?这正是机器人与纯软件 AI 差别最大的地方。

这篇教程不碰 VLA、人形机器人这类大工程,而是从一个最小的场景入手:手柄摇杆被推倒,到四个轮子转动、结果再回流到上位机,中间到底发生了什么。适合有 AI 或软件开发经验、但没怎么接触过机器人的人;也适合听过节点、话题这些词、但还没建立起整体图景的人。如果已经用 ROS 2 跑过移动机器人,这篇里的内容对你来说可能不算新鲜。

准备工作

先明确这套链路成立所依赖的环境。不同厂商、不同型号的机器人细节会有差异,下面列出的是一套典型配置,具体以你手上设备的实际说明为准。

项目内容
机器人本体四轮麦克纳姆轮底盘(部分型号另带多轴机械臂)
主控板NVIDIA Jetson 系列(负责感知、规划、AI 推理)
下位控制器STM32 单片机,固件内置 micro-ROS 客户端
ROS 版本ROS 2 Humble
输入设备手柄型摇杆,在系统中识别为 /dev/input/js0
上下位连接Jetson 与 STM32 之间通过串口通信

需要提前具备的条件:

  • Jetson 上已装好 ROS 2,并且能正常执行 ros2 topic list 等基础命令。
  • 手柄插上后能被系统识别为输入设备,通常表现为 /dev/input/js0。
  • STM32 已烧录带 micro-ROS 客户端的固件,且串口设备节点存在(例如 /dev/myserial,实际名称以你的设备为准)。
  • 底盘供电稳定。电池电量偏低时,电机实际转速会明显偏离指令值,后面排查问题时容易误判。

另外要建立两个最基础的概念。Jetson 可以理解成机器人身上的一台小型 Linux 计算机,带 CPU 和 GPU,能跑摄像头处理、计算机视觉、SLAM、导航、AI 模型这些重活。ROS 2 则可以粗略理解成机器人内部各个程序之间交换数据的底座:每个程序叫一个节点,数据流动的通道叫话题。机器人里同时跑着读摇杆的程序、读激光雷达的程序、处理图像的、估计自身位置的、规划路径的、往硬件发指令的程序,它们必须互相传数据,ROS 2 解决的就是这件事。

操作步骤

第一步:确认摇杆输入已经进入系统

摇杆接在 Jetson 上,插上之后 Linux 把它当成一个输入设备。推杆时这个设备的值会变化,但此刻它和电机没有任何关系,必须由 Jetson 上的程序读取并交给 ROS,链路才真正开始。

先看摇杆数据本身:

ros2 topic echo /joy

保持这个命令运行,然后推动摇杆,可以看到 axes 的数值在变化;按下按键,buttons 的值会从 0 变成 1。这里要注意,axes 和 buttons 的数量与排列顺序取决于你用的手柄型号,不同设备不一样,不要照抄别人的数值去判断对错。

看到数值变化,只能说明「Jetson 读到了摇杆」。此时机器人仍然不知道「该以多快的速度走」,它只知道摇杆现在处在什么位置。

第二步:把摇杆位置翻译成速度指令

接下来需要一个程序,把摇杆位置翻译成速度。在这类底盘上,通常由一个专门的遥控节点承担这个角色,例如 /joy_ctrl。转换关系大致是这样:

摇杆操作转换后的指令
向前轻推以 0.2 m/s 前进
向旋转方向推以 0.4 rad/s 旋转

转换结果发布到 /cmd_vel 这个话题上。cmd_vel 是 command velocity(速度指令)的缩写,在移动机器人里极其重要。它的消息类型是 geometry_msgs/msg/Twist,主要用到的字段有三个:

字段含义
linear.x前进 / 后退
linear.y左移 / 右移(横向移动)
angular.z旋转

这里有一个很值得注意的性质:/cmd_vel 以下的控制部分,完全不需要知道这条指令是谁发的。今天是人推摇杆,明天可能是导航栈,将来可能是某个 AI 智能体。上层「谁来做决定」可以换,/cmd_vel 以下的流程每次都一样。换句话说,摇杆扮演的只是「决策者」这个角色,机器人其余部分并不关心这个决定来自人还是来自 AI,它只需要知道「现在该怎么动」。

第三步:理解为什么指令有了、轮子还不转

原因很简单:/cmd_vel 此刻还只是存在于 Jetson 上的一条数据。Jetson 不会把 linear.x = 0.2 这个数值直接变成电流灌进电机。Jetson 的下游还有另一颗芯片——STM32。

如果 Jetson 干的是「大脑」的活,STM32 更接近「手脚」:读编码器、驱动电机、跑速度控制环。分工可以概括成两句话:

  • Jetson 决定机器人「该做什么」。
  • STM32 负责「怎么把它执行到电机上」。

这种分工在机器人里非常普遍。没有人愿意让一个思考要花好几秒的大模型,顺便去兼任电机转速的稳定控制。

那么 Jetson 怎么把指令发给 STM32?答案是 micro-ROS。STM32 上跑不了 Linux,也跑不了完整的 ROS 2,它的固件里嵌入了一个 micro-ROS 客户端。Jetson 这一侧需要启动 micro_ros_agent,由这个 Agent 在 Jetson 和 STM32 之间做桥接:把 ROS 2 的消息转换成适合串口传输的小数据包,STM32 侧的 micro-ROS 收到后驱动电机。反方向上,/odom_raw 之类的测量数据也沿同一条路径回传。

启动命令形如:

micro_ros_agent serial \
  --dev /dev/myserial \
  -b 2000000

其中 --dev 指定串口设备节点,-b 指定波特率,这两个值都要按你的实际硬件填写。走到这一步,指令已经从 Linux 上的一个程序,传到了直接控制硬件的芯片上,离「数值变成动作」已经很近了。

第四步:四个轮子各自该转多快

这类底盘通常使用四个麦克纳姆轮。轮子周围带斜向滚轮,除了前进、后退、旋转,还能横向平移。但软件必须多解一道题:假设 /cmd_vel 说的是「向左横移」,左前轮和右前轮并不知道「向左移动」是什么意思,每个电机只听得懂「往哪个方向、以多快转速转」。

于是系统必须计算:要让整个机器人向左移动,四个轮子分别该怎么转?这就是麦克纳姆轮的逆运动学。名字听起来唬人,思路其实很朴素——把整车期望的运动(vx、vy、ω)分解成四个轮子各自的速度。同样四个轮子,只改变转向与转速的组合,就能实现前进、横移、旋转、斜向移动。

第五步:指令发出去,不等于按指令执行

这是软件与物理世界差别最明显的地方。在代码里写 x = 100,x 就一定是 100。但对电机说「以 100 RPM 转」,它可能只转到 92 RPM。原因可能有很多:

  • 电池电量偏低
  • 机器人整体偏重
  • 地面材质和摩擦不同
  • 某个电机比另一个略有力
  • 轮子打滑

所以机器人不能相信「指令发出去了,就应该按那样动了」,它必须实际测量。这类底盘的轮子上装有编码器,可以知道轮子转了多少、当前转速是多少。如果实测是 92 RPM,控制器就加大给电机的信号;过一会儿变成 99 RPM,就更接近目标了。这个调整过程常用 PID 控制:观察误差(目标与实际的差),逐步修正信号,让实际速度逼近目标,如此循环往复。

最终形成的链条是:软件指令 → 电信号 → 电机转动 → 轮子转动 → 机器人移动。到这一步,推杆这个动作才真正抵达了物理世界。

第六步:让结果回流,形成闭环

机器人跑起来之后,事情还没完。假设下的是直行指令,四个轮子都在转,但实际可能越走越偏,因为前面那些小误差会一点点累积。如果只看发出的指令,Jetson 会认为「我让它直行,它就在直行」——但这只是「希望如此」,机器人需要知道「实际发生了什么」。

在这套结构里,STM32 会通过话题把运动数据回传给 Jetson:

话题内容
/odom_raw根据轮子转动推算的位移量
/imu/data_raw角速度、加速度等 IMU 数据

这样数据流就构成了一个循环。举例来说,如果右侧电机比左侧快 6%,即使 /cmd_vel 一直是直行,机器人也会慢慢向左偏。能发现这个偏差的,只有回传的测量值。机器人不只是发指令,还要确认结果——这就是反馈,也是机器人学里最基本的思路之一。

不过,这还不能叫 Physical AI。在这个例子里最「聪明」的仍然是人:观察环境的是人,决定去哪的是人,操作摇杆的也是人,机器人只是照做。这种方式叫遥操作(teleoperation)。但有一点很关键:/cmd_vel 以下的部分,将来即使把人换成 AI,也几乎原样需要。比如不用摇杆,只说一句「去看看 3 号会议室」,完整系统可能需要:AI 理解这句话;确定 3 号会议室的位置和机器人当前位置;计算路径;生成并发布 /cmd_vel;一边用传感器观测一边前进,到达后结束任务。前三步可能很「AI」,但从第四步往后,和这套链路几乎一样。

一个完整示例

把上面的步骤串起来,一次完整的遥控操作大致是这样跑通的。

1. 启动下位机桥接。在 Jetson 上启动 micro-ROS Agent,让 ROS 2 与 STM32 建立串口通道:

micro_ros_agent serial \
  --dev /dev/myserial \
  -b 2000000

2. 确认下位机的数据已经上来了。另开一个终端,检查来自 STM32 的话题是否出现:

ros2 topic list | grep -E "odom_raw|imu"

如果这两个话题都不在列表里,说明桥接没建立成功,后面的步骤不用继续。

3. 确认反馈数据在持续更新。话题存在不等于数据在流动,要看频率:

ros2 topic hz /odom_raw

能看到稳定的频率输出,才说明下位机确实在回传测量值。

4. 观察摇杆输入。启动遥控节点后,查看摇杆话题:

ros2 topic echo /joy

推杆时 axes 变化,按键时 buttons 在 0 和 1 之间切换。

5. 观察速度指令。再开一个终端,看摇杆动作是否被翻译成了速度:

ros2 topic echo /cmd_vel

把 /joy 和 /cmd_vel 两个终端并排放在一起推杆,可以看到数值从「位置」变成「速度」的瞬间。

6. 手动发一条速度指令做验证。不依赖摇杆,直接向底盘下发一条前进指令:

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

如果机器人动了,说明从 /cmd_vel 到电机的整条链路是通的;如果没动,就按下一节的顺序逐层排查。

注意事项

「ROS 里看得见指令」不等于「电机收到了指令」。这是最容易踩的坑。有时从 ROS 侧看一切正常:话题存在,指令也在发布,但机器人就是不动。要让电机真正转起来,中间每一环都必须接通:micro-ROS Agent 已启动并能转发指令;STM32 连接正常;固件创建了正确的订阅者;电机控制器在工作;供电稳定。任何一环断开,Jetson 侧看起来都「很干净」,而机器人纹丝不动。

遇到这种情况,建议按下面的顺序逐层排查:

# 1. 有没有节点在订阅 /cmd_vel(Subscription count 为 0 说明没传到下游)
ros2 topic info /cmd_vel --verbose

# 2. STM32 侧的话题有没有上来(确认 micro-ROS 连接)
ros2 topic list | grep -E "odom_raw|imu"

# 3. 反馈数据是否真的在更新
ros2 topic hz /odom_raw

比如 micro-ROS Agent 根本没启动时,顺着这个顺序就能定位到原因。对刚接触机器人的人来说,这是一个不小的思维转变:普通软件里,确认「函数被调用了吗」「API 返回响应了吗」往往就够了;在机器人里还要多问一句——真实世界是否按软件指示在动?

其他几点也值得留意:

  • axes 与 buttons 的数量和顺序因手柄而异,不要用别人的数值判断自己的设备是否正常。
  • 串口设备节点名与波特率必须与实际硬件一致,写错会导致 Agent 启动失败或通信异常。
  • 电池电量偏低会直接表现为「指令发了但转速不够」,排查前先确认供电状态,避免误判成软件问题。
  • 轮子打滑、地面材质变化都会让实际运动偏离指令,这类偏差只能靠编码器和 IMU 的反馈发现,不能靠看指令发现。
  • 涉及具体型号、固件版本、参数默认值、价格与配额等信息,请以厂商官网当前公布的内容为准,不同批次和版本可能存在差异。

把这条链路记成一句话:判断与执行是分开的,/cmd_vel 是那条分界线;分界线以上是人、导航栈还是 AI 都可以换,分界线以下的机制基本不变;指令发出去不等于按指令执行,所以要用编码器和 IMU 测量、用 PID 之类的办法持续修正;ROS 里看得见不等于真实世界在动,排查时要一层一层往下追。归根结底,机器人学的基本循环就是:感知 → 判断 → 行动 → 观察结果 → 下一次判断。SLAM、导航、VLA 这些名词,都可以看成这个循环里某个位置上的零件。