核心课程 / 机械臂应用与开发 / EXP-02
EXP-02 · 第 2 章 · 验证性 · 4 学时

关节电机底层控制与应用

本章不启动 ROS2 节点。学习目标:机械臂的关节原理是什么、关节如何转动起来、又如何被控制。
注:机械臂关节设备的节点名称两平台不同——实验箱 /dev/stm32、小车 /dev/car1_stm32。

难度 ⭐⭐ · 4 学时 · 验证性 1Mbps 8N1 CRC8 查表 PWM 500~2500μs interactive_mode
⚠️
机械臂通电后会自主、有力地运动。在 04 节完成三条铁律打卡前,先不要给真实舵机上电发指令。详见 04 实操安全须知。

01问题引入:关节是怎么动起来的

上一节提到了机械臂的 joint(关节):它是连杆的连接处,是一个可以旋转的关节;而这个关节实际上是一台可以旋转的电机(也就是我们实验里真正用的舵机),带着连杆一起转动。关节怎么动的、为什么能动、怎么控制它按照要求运动,正是本章要解释的原理和应用的基础。为了解释这一系列问题,我们先从下面的流程图里查看关节被控制运动的完整流程,然后打开关节的黑盒,看看它的内部世界是如何运转的。

仿真里模型会动,是因为渲染软件收到了角度。真机上没人"渲染",每一微秒的转角都是电机真实出的力。下图把两条链画在一起:上排是你已经会的仿真链,中间泳道那个问号,就是本章要拆开的黑盒。

图 2-1|从拖动 GUI 到舵机转动的完整链路(可缩放/搜索;紫虚线是回程:0x08 状态帧读回)

本实验要拿到的六样东西:

  • 能默写帧结构 0xAA 0x55 + func + len + data + CRC8,并解释每一段的作用
  • 完成串口权限授权,验证 /dev/stm32(小车 /dev/car1_stm32)可读写
  • 运行 python3 test_servo_direct.py,用 interactive_mode 控制 5 关节 + 夹爪
  • 用 s 1 1520 单点微调、work 一键回工作位 [1500,1100,2200,1850,1500]
  • 录制 pre_grasp → gripper_open → grasp_down → gripper_close → lift → back_work 六步动作,导出 pick_and_place.yaml 并 colcon build 安装
  • 用 cutecom 抓一条真实帧,逐字节对照协议结构

02舵机原理:一整套伺服系统

给舵机发一个数,过一会儿关节就停在对应角度;用手去掰,它还会顶回来(除非先释放力矩)。一个数就能换来"听话又倔强"的动作,它内部是怎么做到的?

拆开一台舵机,里面是四件套,各干一件事:

四件套 · 动力
电机 Motor

把电能变成转速,只会"转多快",不知道"转到哪"。

四件套 · 力矩
减速器 Gear

齿轮组降速增矩:电机转好几圈,输出轴才转一圈,换来能抬起手臂的力矩。

🔧 先认识"力矩"(就是口语里的"劲")让东西转起来靠的不是力气本身,而是力矩 τ = F × r(力 × 力臂,单位 N·m 牛·米)。同样大的力气 F,力臂 r 越长、力矩越大——拧螺丝时短扳手拧不动,套根长管就拧动了:不是力气变大,是力臂变长、力矩变大。舵机要抬起手臂,输出轴给的就是力矩。所以下面公式里的 T,指的都是力矩。
📐 齿轮公式(四步推导)每步都是三件套:字母公式 → 代入数字 → 结果,数字全部带单位;符号与单位对照见上面齿轮卡片底部的图注。
① 先数牙:i = z₂ ÷ z₁ = 40 颗 ÷ 16 颗 = 2.5。两轮啮合处过牙速度必须一样,大轮一圈过 40 颗牙,小轮一圈只有 16 颗,所以小轮转 2.5 圈才配大轮 1 圈。颗÷颗,单位约掉,i 是纯倍数,只由齿数定。
② 转速:ω出 = ω入 ÷ i = 90 rpm ÷ 2.5 = 36 rpm(rpm = 转/分钟)。电机 90 转/分,输出轴只有 36 转/分,慢下来了。
③ 前提:功率守恒,齿轮只搬功率不造功率,P入 = T入 × ω入,P出 = T出 × ω出。代入数字对账:1.0 N·m × 90 rpm = 2.5 N·m × 36 rpm = 90 N·m·rpm,两边始终相等(就像水管变粗流速变慢,每秒流过去的水总量不变)。
④ 结论:T出 = T入 × i = 1.0 N·m × 2.5 = 2.5 N·m,就是把 ② 代进 ③ 解出来的。
一句话:转速除以 i,力矩就乘以 i,降速增矩四个字全在这(记住结论即可,推导不必背)。
四件套 · 眼睛
磁编码器 Encoder

轴尾巴上装块磁铁,跟着轴一起转;静止的芯片像枚指南针,读出磁场方向、换算出角度——舵机"知道自己在哪"全靠它。

四件套 · 大脑
驱动板 Driver

收到目标(脉宽),减去编码器读数得误差,再调电机电流把误差往零推——闭环的大脑。

串起来就一个回路:目标 − 现状 = 误差 → 电机朝误差变小的方向转 → 误差归零就停。这叫闭环控制,"伺服(Servo)"这个词指的就是这种自动纠偏。拿空调打比方:你设 26℃ 是目标,室温计读数是现状,温差让压缩机启停。像的部分是"设定值对测量值、误差驱执行";不像的部分是空调量温度、舵机量角度,而且舵机的"室温计"是芯片从磁场里实时解出的角度,0.1° 的偏差它都看得见。

四件套拼在一起,才是那颗"听话又倔强"的舵机。上面四个卡片是一块块拆开看,下面的 3D 实验室把它们装回同一台真舵机——先看它们怎么协同转起来,再学怎么指挥它。

那"目标"从哪来?舵机只认一种表达:脉宽。高电平持续 1500μs 表示"立正"(中位),500~2500μs 是安全窗,脉冲越宽转得越远。脉宽和角度的换算只有一条公式,记住结论即可,推导不必背:

🔍 换算公式弧度 = (PWM − 1500) × 0.002356(CENTER_PWM=1500,PWM_TO_RAD=0.002356)。EXP-05 起 ROS2 发弧度,底层发 μs,两侧永远由这条公式对账。

观察任务(打开即有默认演示值 1500μs):① 拖 PWM 滑杆,盯住右上读数区——蓝线反馈总是追着红线目标,两条线的差就是误差;② 点"越界 3000"试试安全窗外会发生什么,再点"复位"回到中位;③ 点四个部件卡,3D 里对应零件会亮,对照左侧一句话说明。

互动 2-1|舵机实验室 · 原理模式:四件套的 3D 合体总结——剖面 + 脉宽闭环 + 目标/反馈曲线(鼠标可旋转缩放;mode=lab)

你刚才证明了:PWM 只是目标位置的"一句话表达",真正的转角是编码器一直盯着、误差一点点磨出来的。

还有一个问题没解决:机械臂上 6 颗舵机,总不能给每颗单独拉一根信号线。我们的舵机是总线舵机:多颗共享一条串口线,脉宽这个数要装进"信封"、写上收件人编号才发得出去。这就是下一节的协议。

03舵机控制协议

先把上一节的四件套合起来看:电机 + 减速器 + 磁编码器 + 驱动板,四样拼齐,才是一颗能自己纠偏的"伺服舵机"。要是每做一个关节,都得自己绕电机绕组、切减速齿轮、焊驱动板、再写一套闭环固件,那机械臂 6 个关节就是 6 倍的重复工作——又难又不划算。

好在市面上有现成的舵机:它已经把四件套全部装进了同一个小盒子里,出厂就是一套完整的伺服系统。我们不用拆开它、也不必碰它内部,只要学会用它对外的接口说话就行。

我们这套设备给舵机留的接口是串口,说话的内容叫协议:不碰电机和电路,只发几条约定好的字节指令,就能让舵机转到指定位置、或回报它现在的状态。下面要学的,就是这门"语言的语法"。

伺服舵机(黑盒 · 四件套都在里面) 驱动板 Driver(大脑) 目标−实际=误差 → PID → 电流 电机 Motor 减速器 Gear 输出轴 摇臂 磁编码器 Encoder (读出角度,反馈) 电流 反馈角度 串口 TX / RX 下位机主板 STM32 / RK3576 (跑固件,收发串口)
图 3-1|舵机 = 封装好的伺服系统:四件套 + PID 闭环都关在虚线框里,我们只从外面接一根串口线,用协议跟它对话(主板发指令、舵机回状态)。

一条线上挂 6 颗舵机,谁该动、动多少、怎么防止听错和收错?发帧用挂号信那一套:先验信封,再拆正文,章不对整封退回。跟挂号信不一样的地方在于,这封"信"比真信多一个字段——信的类别(功能码),下指令和查状态是两种信。

一帧的完整结构(本课程全系列通用):

字段取值/含义挂号信里的对应物
帧头0xAA 0x55,固定两字节信封暗印,见到它才开始拆信
功能码 func0x05 总线舵机指令(正文带子命令)信的类别:下指令、问状态、还是写配置
长度 len + 数据段子命令 + 舵机 ID + PWM 值,如 [0x01, 时长, 数量, id, pwm…]信的正文:几号舵机、转到几 μs
CRC8从 func 一直盖到 data 的 256 字节查表 _CRC8_TABLE,与 board_sdk.py 一致骑缝章,错一字节即整帧拒收

正文里的子命令(脚本 test_servo_direct.py 实际用到的全部六种):

子命令功能方向你会在哪儿用到
0x01写多舵机目标(带运动时长)PC → 舵机work / s / g 等一切"让它动"的命令
0x05读单舵机位置(一问一答)问 → 答恢复力矩前先读当前位置
0x08全舵机状态持续上报(位置/温度/电压)下位机 → PCread 看状态、save 录姿态的数据源
0x0B / 0x0C释放力矩(可手掰)/ 锁定力矩PC → 舵机free / load,摆初始姿势靠它
0x40 / 0x42写入 / 读取开机模式(写 EEPROM 后舵机重启)PC → 舵机csm / csr 配置开机行为
🔍 两个必须算一遍的数(源码可验)① CRC8 到底盖了哪些字节?看 board_sdk.py 与 test_servo_direct.py 的组包代码 buf = [0xAA, 0x55, func, len] + data 后跟 crc8(buf[2:])——帧头两字节 0xAA 0x55 不参与校验,校验范围是 func + len + data。接收端也是同样的子串重算,所以少算一段或把帧头算进去都会导致"帧永远不通过"。
② 1Mbps 到底够不够用?一帧约 7 字节(帧头 2 + func 1 + len 1 + 子命令/ID/PWM 约 3 + CRC 1),5 关节 50 Hz:5 × 7 × 8 bit × 50 Hz = 14000 bps,仅占 1 Mbps 的 1.4%。就算挂 10 颗舵机、提到 100 Hz,也只有 10 × 7 × 8 × 100 = 56000 bps = 5.6%——所以这套总线的瓶颈从来不是带宽,而是单线半双工的分时排队与应答等待。

观察任务(互动 2-2):① 表里点"写多舵机目标",按"发送演示帧",看右侧 3D 输出轴跟着转,同时左下字节日志亮出 AA 55 05 … 和随数据变化的 CRC;② 点"0x0B 释放力矩",用出现的手掰滑块直接掰输出轴;③ 什么都不点,盯住 0x08 缓存表自己在跳。

互动 2-2|舵机实验室 · 协议模式:还是 2-1 那台 3D,右侧换成协议面板——左表点接口 → 3D 同步执行 + 实时字节帧对照(mode=proto)

你刚才证明了:去程帧里装的就是脉宽这个数加上收件人 ID;回程 0x08 是下位机白送的状态播报,save 不发问就有数据,靠的是它。

04实操安全须知与环境准备

接下来三节要动真家伙。机械臂关节是带力矩的舵机,指令错了它不会自己知道错,只会执行到位或顶到限位为止。三条铁律,动手前逐条过:

  • 上电前:机械臂展开摆到安全姿态,Workspace 里没有障碍物和手指;先确认知道急停/断电开关在哪,再合电。
  • 运行中:第一次发指令只发近中位值(如 s 1 1520)或直接 work 回工作位,禁止一上来就大角度横甩。
  • 异常时:出现异响、抖动、顶死,立刻 Ctrl+C 退出脚本并断电;按"线与权限 → 波特率与帧 → 上层逻辑"的顺序查(排障路径见 09 节)。

能向同伴口头复述这三条,再往下走。环境侧三件事,缺一不可:

硬件连接
USB 转 TTL → /dev/stm32

1Mbps 8N1;设备名按平台:实验箱 /dev/stm32,小车 /dev/car1_stm32(udev 规则建的固定软链)。CH340/CP2102/FT232 都行。

权限授权
临时或永久二选一

临时:sudo chmod 666 /dev/stm32(小车换 /dev/car1_stm32);永久:sudo usermod -a -G dialout $USER 后重新登录。

清场动作
cleanup_ros.sh 先行

test_servo_direct.py 直接占用 /dev/stm32,与 bringup 的 base_driver 节点互斥——运行脚本前先停掉所有 ROS 节点,否则典型症状是 Permission denied 或应答乱码。

# ① 清理占用串口的应用(本实验全程不启动 bringup!)
bash cleanup_ros.sh

# ② 确认设备与权限
ls -l /dev/stm32        # 小车换成 /dev/car1_stm32
# 预期:软链存在且可读写;打不开 → sudo chmod 666 /dev/stm32
dmesg | grep tty     # 找不到设备时看内核插入日志

05实操全景:test_servo_direct.py

动手前先见森林。这个脚本干的事可以一句话说完:打开串口,拉起一个后台收线程,然后给你一个敲命令的菜单。下图四个泳道对应四件事,主路径从左到右就是程序的一生。

图 2-2|test_servo_direct.py 实操全景:主线程 / 串口线 / 后台线程 / 功能区

读图任务:① 沿主路径从"打开 /dev/stm32"走到"yaml 导出",每个格子念一遍它干啥;② 重点看中间两格的反向关系:你敲命令是下行,0x08 状态是上行,两条路同时在一条串口线上跑;③ 右下三张卡片回答了本节最常被问的两个问题——save 的数据哪来的、动作怎么才能不丢。

接下来三节把主路径按由易到难拆成三步走:先打开(06),再聊起来(07),最后录成果(08)。

一句话复述:这个脚本 = 一个开串口的主线程 + 一个永远在收帧的后台线程 + 一个你敲命令的菜单。

06功能①:打开串口,发出第一条指令

先走通最小的一条往返:程序打开 /dev/stm32,发一帧,舵机动一下。

1清场 + 授权:bash cleanup_ros.sh,再按 04 节确认 /dev/stm32 可读写
2启动裸机调试:python3 test_servo_direct.py,出现选择模式提示即进入
3输入 1 进入交互模式,先发 work 让 5 关节回工作位——这是你的第一条真实指令
# 启动与判定
python3 test_servo_direct.py
# 成功标志:打印 打开串口 /dev/stm32 @ 1000000... 后出现 串口已打开
# 失败会提示三种可能:bringup 没停 / 设备不存在 / 权限不够(对照 04 节逐条排除)

# 可选:串口助手抓一条真帧逐字节看(观察任务)
cutecom --device /dev/stm32 --baud 1000000
# 看点:发 work 时应出现 AA 55 开头的一串字节;1Mbps 下字节会瞬间刷过,
# 抓完暂停再翻,对照 03 节数字段
⚠️ 半双工提醒单线半双工:同一时刻只有一方讲话。发完一帧给舵机留应答时间(脚本 timeout=1),别像机关枪一样连发。

判定成功:日志出现 /dev/stm32 @ 1000000 且无 Permission denied;work 后 5 关节与夹爪同步摆到工作位姿态。你刚才证明了:不启动任何 ROS2 节点,一段 Python 也能直接跟舵机对话。

07功能②:interactive_mode 让关节听指挥

进到交互菜单后,常用命令就下面这些。规律很明显:每条命令最后都变成同一类帧,只有 data 段的 ID 和脉宽在换(对照 03 节子命令表)。

命令做什么判读要点
work5 关节齐回工作位 [1500,1100,2200,1850,1500]换算成弧度约 [0, −0.943, 1.65, 0.825, 0],与 EXP-05 的 /joint_command 预置值对账
home6 路全回 1500 中位最安全的姿态,异常恢复先打它
s 1 1520单颗舵机微调(ID 1 → 1520μs)只有 1 号动 = ID 寻址正确
g 1000 / g 2000夹爪张开 / 全闭裸机自定值;ROS2 侧的规范口径是 开 800 / 闭 1500(避开 ±135° 硬限位,EXP-05 沿用),别把两套数混着记
1500 1100 2200 1850 1500五个数连打 = 5 关节一次到位与 work 本质相同,数值自定
read打印 0x08 上报的全部状态PWM / 温度 / 电压一目了然
free / load释放力矩(可手掰)/ 恢复锁死摆初始姿势用 free,摆完记得 load
# 安全流程:先回家,再微调
work            # 第一条永远是它:回到已知安全位
s 1 1520        # 1 号舵机中位+20μs,微调神器(换算 ≈ +0.0471 rad)
g 800           # 张开夹爪;g 1500 全闭(与 EXP-05 的 servo 口径一致)
free            # 手掰摆位;load 锁回去
🔍 安全窗对账任何手打的脉宽先心算:落在 500~2500 才准发;1520 这类近中位值永远是试手首选。03 节互动面板里点"越界 3000"看到的报警,就是实机同样的值会引发的后果。

判定成功:s 1 1520 只动 1 号舵机;free 后能推动、load 后推不动。你刚才证明了:ID + 脉宽两个数,足够在一条线上精确指挥任何一颗舵机。

08功能③:录制动作,交棒 EXP-05

最后一步把刚才摆过的姿势存成序列,导出给下一章回放。先弄清 save 的数据从哪来:下位机每隔几十毫秒广播一帧 0x08 状态,后台线程收到就覆盖缓存;你按 save 那一刻,脚本只是给缓存拍了张快照,从头到尾没现问过舵机(03 节互动面板的 0x08 表自己在跳,就是这条通道)。

1摆一个姿势 → save → 命名,如 pre_grasp
2依次录满六步:pre_grasp → gripper_open → grasp_down → gripper_close → lift → back_work(夹爪步用 save -g)
3play 原地回放检查一遍序列对不对
4yaml → 动作组名 pick_front → 导出 pick_and_place.yaml → colcon build 安装
# 导出与安装(YAML 默认落在包的 config/actions/ 下)
yaml             # 按提示输入动作组名,如 pick_front
colcon build --packages-select car1_arm_controller
# 不 build 的后果:EXP-05 的 action_player 找不到文件
⚠️ 动作只活在内存里save 存的序列退出 interactive_mode 即丢,唯一持久化途径是 yaml 导出;导出后必须 colcon build,install/share 才会拿到新 YAML。

判定成功:六条 save 全部命名成功,yaml 导出后 install/share 下存在新文件。你刚才证明了:裸机录下的不过是一行行"ID: 脉宽"清单,EXP-05 的 action_player 读的就是这份清单。

09重点与易错点

💡 本实验最大重点安全窗 500~2500μs 是铁律:超出范围轻则撞机械限位,重则堵转发热打齿轮——堵转损失不在质保范围。任何手动输入前先心算它落在哪。

出问题时按自底向上的三层查,别一上来就怀疑上层逻辑。有分支的路径画出来是这样:

脚本打不开串口 第一层 · 线与权限 ls /dev/stm32 存在? 设备枚举检查 dmesg | grep tty 设备未枚举 → 换线 / 换接口 chmod 666 / 加 dialout 组 Permission denied 专药 否 是 判据:ls 看得见 却打不开 = 权限问题, 不是设备坏 设备能打开了,但应答全是乱码 第二层 · 波特率与帧 bash cleanup_ros.sh 停 bringup 脚本与 ROS 节点抢串口 = 头号原因 再发帧 cutecom 抓一帧真实字节 对照 AA 55 开头的帧结构 帧对得上,但舵机不动作 第三层 · 才轮到上层逻辑 查舵机 ID s 1 1520 单点试 查脉宽范围 500~2500 安全窗内? 查力矩开关 free 之后要 load 回来
图 2-3|串口打不开时的排查路径:先线与权限,再波特率与帧,最后才怀疑逻辑
⚠️ 常见坑现象 → 解法
Permission denied打不开 /dev/stm32(小车 /dev/car1_stm32) → sudo chmod 666 /dev/stm32 或加入 dialout 组重新登录
找不到设备ls 无 /dev/stm32 → dmesg | grep tty 看内核枚举与 udev 建链,或换线/换接口
串口被抢占脚本与 bringup 同开 → 应答乱码或直接失败 → 先 bash cleanup_ros.sh
PWM 越界输入 3000μs 手臂顶死 → 严格执行 500~2500 窗口,先试 1520 这种近中位值
yaml 后找不到文件action_player 报文件不存在 → 检查 install/ 是否生成,必须 colcon build 重装
退出后动作丢失重开脚本 save 列表空了 → 正常现象,持久化只能靠 yaml 命令

三层排障观就是本章的方法论:线与权限(04)→ 波特率与帧(06 的 cutecom)→ 上层逻辑,从便宜的查起,从贵的停手。这条思路 EXP-05 接 ROS2 之后照样用。

10验收与总结

  • 能向同伴复述 04 节三条安全铁律(上电前 / 首指令 / 异常时)
  • 串口打开日志显示 /dev/stm32 @ 1000000,无 Permission denied
  • work 后 5 关节与夹爪同步到达工作位姿态
  • s 1 1520 只动 1 号舵机,说明 ID 寻址正确
  • 六条 save 全部命名成功,yaml 导出后 install/share 下存在新 YAML
  • 能口头解释:为什么这条路线"不依赖 ROS2 启动任何节点",以及它与 EXP-05 ROS2 路线的分工
✅ 完成成果
裸机驱动 5 臂 1 爪

pyserial 直连总线,菜单调试、单点微调、序列录制、YAML 导出全通过。

🧠 原理收获
伺服闭环 + 协议三件套

目标−编码器=误差的闭环;0xAA55 定界、func/len/data 载荷、CRC8 查表;PWM↔弧度一条公式打通软硬。

🛠 技能收获
串口工程能力

权限授权、设备排查(dmesg)、串口互斥清场(cleanup_ros.sh)、cutecom 抓帧。

❓ 排障思路
自底向上三层

线与权限 → 波特率与帧 → 才轮到上层逻辑。

📝 思考题 1五个关节 50Hz 持续刷新只用掉约 1.4% 的 1Mbps 带宽——如果总线上挂 10 个舵机并把频率提到 100Hz,带宽还够吗?请估算,并说明超出带宽会先出现什么症状。(提示:单帧约 7 字节)
📝 思考题 2CRC8 能发现"传输错字节",却拦不住"命令本身写错"——比如你把 1520 误敲成 15200(越界值)。协议层和应用层各应该加什么防线?对照本页安全红线与下一章的限位表回答。
🔍 下一节的钩子EXP-05 把同一条链路搬进 ROS2:话题和控制器在上层接管"决定发什么",底层还是本章这套帧和脉宽——你手里那份 pick_and_place.yaml,就是交接棒。