关节电机底层控制与应用
本章不启动 ROS2 节点。学习目标:机械臂的关节原理是什么、关节如何转动起来、又如何被控制。
注:机械臂关节设备的节点名称两平台不同——实验箱 /dev/stm32、小车 /dev/car1_stm32。
01问题引入:关节是怎么动起来的
上一节提到了机械臂的 joint(关节):它是连杆的连接处,是一个可以旋转的关节;而这个关节实际上是一台可以旋转的电机(也就是我们实验里真正用的舵机),带着连杆一起转动。关节怎么动的、为什么能动、怎么控制它按照要求运动,正是本章要解释的原理和应用的基础。为了解释这一系列问题,我们先从下面的流程图里查看关节被控制运动的完整流程,然后打开关节的黑盒,看看它的内部世界是如何运转的。
仿真里模型会动,是因为渲染软件收到了角度。真机上没人"渲染",每一微秒的转角都是电机真实出的力。下图把两条链画在一起:上排是你已经会的仿真链,中间泳道那个问号,就是本章要拆开的黑盒。
本实验要拿到的六样东西:
- 能默写帧结构
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舵机原理:一整套伺服系统
给舵机发一个数,过一会儿关节就停在对应角度;用手去掰,它还会顶回来(除非先释放力矩)。一个数就能换来"听话又倔强"的动作,它内部是怎么做到的?
拆开一台舵机,里面是四件套,各干一件事:
把电能变成转速,只会"转多快",不知道"转到哪"。
齿轮组降速增矩:电机转好几圈,输出轴才转一圈,换来能抬起手臂的力矩。
① 先数牙: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,降速增矩四个字全在这(记住结论即可,推导不必背)。
轴尾巴上装块磁铁,跟着轴一起转;静止的芯片像枚指南针,读出磁场方向、换算出角度——舵机"知道自己在哪"全靠它。
收到目标(脉宽),减去编码器读数得误差,再调电机电流把误差往零推——闭环的大脑。
串起来就一个回路:目标 − 现状 = 误差 → 电机朝误差变小的方向转 → 误差归零就停。这叫闭环控制,"伺服(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 里对应零件会亮,对照左侧一句话说明。
你刚才证明了:PWM 只是目标位置的"一句话表达",真正的转角是编码器一直盯着、误差一点点磨出来的。
还有一个问题没解决:机械臂上 6 颗舵机,总不能给每颗单独拉一根信号线。我们的舵机是总线舵机:多颗共享一条串口线,脉宽这个数要装进"信封"、写上收件人编号才发得出去。这就是下一节的协议。
03舵机控制协议
先把上一节的四件套合起来看:电机 + 减速器 + 磁编码器 + 驱动板,四样拼齐,才是一颗能自己纠偏的"伺服舵机"。要是每做一个关节,都得自己绕电机绕组、切减速齿轮、焊驱动板、再写一套闭环固件,那机械臂 6 个关节就是 6 倍的重复工作——又难又不划算。
好在市面上有现成的舵机:它已经把四件套全部装进了同一个小盒子里,出厂就是一套完整的伺服系统。我们不用拆开它、也不必碰它内部,只要学会用它对外的接口说话就行。
我们这套设备给舵机留的接口是串口,说话的内容叫协议:不碰电机和电路,只发几条约定好的字节指令,就能让舵机转到指定位置、或回报它现在的状态。下面要学的,就是这门"语言的语法"。
一条线上挂 6 颗舵机,谁该动、动多少、怎么防止听错和收错?发帧用挂号信那一套:先验信封,再拆正文,章不对整封退回。跟挂号信不一样的地方在于,这封"信"比真信多一个字段——信的类别(功能码),下指令和查状态是两种信。
一帧的完整结构(本课程全系列通用):
| 字段 | 取值/含义 | 挂号信里的对应物 |
|---|---|---|
| 帧头 | 0xAA 0x55,固定两字节 | 信封暗印,见到它才开始拆信 |
| 功能码 func | 0x05 总线舵机指令(正文带子命令) | 信的类别:下指令、问状态、还是写配置 |
| 长度 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 | 全舵机状态持续上报(位置/温度/电压) | 下位机 → PC | read 看状态、save 录姿态的数据源 |
0x0B / 0x0C | 释放力矩(可手掰)/ 锁定力矩 | PC → 舵机 | free / load,摆初始姿势靠它 |
0x40 / 0x42 | 写入 / 读取开机模式(写 EEPROM 后舵机重启) | PC → 舵机 | csm / csr 配置开机行为 |
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 缓存表自己在跳。
你刚才证明了:去程帧里装的就是脉宽这个数加上收件人 ID;回程 0x08 是下位机白送的状态播报,save 不发问就有数据,靠的是它。
04实操安全须知与环境准备
接下来三节要动真家伙。机械臂关节是带力矩的舵机,指令错了它不会自己知道错,只会执行到位或顶到限位为止。三条铁律,动手前逐条过:
- 上电前:机械臂展开摆到安全姿态,Workspace 里没有障碍物和手指;先确认知道急停/断电开关在哪,再合电。
- 运行中:第一次发指令只发近中位值(如
s 1 1520)或直接work回工作位,禁止一上来就大角度横甩。 - 异常时:出现异响、抖动、顶死,立刻 Ctrl+C 退出脚本并断电;按"线与权限 → 波特率与帧 → 上层逻辑"的顺序查(排障路径见 09 节)。
能向同伴口头复述这三条,再往下走。环境侧三件事,缺一不可:
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 后重新登录。
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
动手前先见森林。这个脚本干的事可以一句话说完:打开串口,拉起一个后台收线程,然后给你一个敲命令的菜单。下图四个泳道对应四件事,主路径从左到右就是程序的一生。
读图任务:① 沿主路径从"打开 /dev/stm32"走到"yaml 导出",每个格子念一遍它干啥;② 重点看中间两格的反向关系:你敲命令是下行,0x08 状态是上行,两条路同时在一条串口线上跑;③ 右下三张卡片回答了本节最常被问的两个问题——save 的数据哪来的、动作怎么才能不丢。
接下来三节把主路径按由易到难拆成三步走:先打开(06),再聊起来(07),最后录成果(08)。
一句话复述:这个脚本 = 一个开串口的主线程 + 一个永远在收帧的后台线程 + 一个你敲命令的菜单。
06功能①:打开串口,发出第一条指令
先走通最小的一条往返:程序打开 /dev/stm32,发一帧,舵机动一下。
bash cleanup_ros.sh,再按 04 节确认 /dev/stm32 可读写python3 test_servo_direct.py,出现选择模式提示即进入1 进入交互模式,先发 work 让 5 关节回工作位——这是你的第一条真实指令# 启动与判定 python3 test_servo_direct.py # 成功标志:打印 打开串口 /dev/stm32 @ 1000000... 后出现 串口已打开 # 失败会提示三种可能:bringup 没停 / 设备不存在 / 权限不够(对照 04 节逐条排除) # 可选:串口助手抓一条真帧逐字节看(观察任务) cutecom --device /dev/stm32 --baud 1000000 # 看点:发 work 时应出现 AA 55 开头的一串字节;1Mbps 下字节会瞬间刷过, # 抓完暂停再翻,对照 03 节数字段
判定成功:日志出现 /dev/stm32 @ 1000000 且无 Permission denied;work 后 5 关节与夹爪同步摆到工作位姿态。你刚才证明了:不启动任何 ROS2 节点,一段 Python 也能直接跟舵机对话。
07功能②:interactive_mode 让关节听指挥
进到交互菜单后,常用命令就下面这些。规律很明显:每条命令最后都变成同一类帧,只有 data 段的 ID 和脉宽在换(对照 03 节子命令表)。
| 命令 | 做什么 | 判读要点 |
|---|---|---|
work | 5 关节齐回工作位 [1500,1100,2200,1850,1500] | 换算成弧度约 [0, −0.943, 1.65, 0.825, 0],与 EXP-05 的 /joint_command 预置值对账 |
home | 6 路全回 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 锁回去
判定成功:s 1 1520 只动 1 号舵机;free 后能推动、load 后推不动。你刚才证明了:ID + 脉宽两个数,足够在一条线上精确指挥任何一颗舵机。
08功能③:录制动作,交棒 EXP-05
最后一步把刚才摆过的姿势存成序列,导出给下一章回放。先弄清 save 的数据从哪来:下位机每隔几十毫秒广播一帧 0x08 状态,后台线程收到就覆盖缓存;你按 save 那一刻,脚本只是给缓存拍了张快照,从头到尾没现问过舵机(03 节互动面板的 0x08 表自己在跳,就是这条通道)。
save → 命名,如 pre_graspsave -g)play 原地回放检查一遍序列对不对yaml → 动作组名 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 全部命名成功,yaml 导出后 install/share 下存在新文件。你刚才证明了:裸机录下的不过是一行行"ID: 脉宽"清单,EXP-05 的 action_player 读的就是这份清单。
09重点与易错点
出问题时按自底向上的三层查,别一上来就怀疑上层逻辑。有分支的路径画出来是这样:
| ⚠️ 常见坑 | 现象 → 解法 |
|---|---|
| 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 路线的分工
pyserial 直连总线,菜单调试、单点微调、序列录制、YAML 导出全通过。
目标−编码器=误差的闭环;0xAA55 定界、func/len/data 载荷、CRC8 查表;PWM↔弧度一条公式打通软硬。
权限授权、设备排查(dmesg)、串口互斥清场(cleanup_ros.sh)、cutecom 抓帧。
线与权限 → 波特率与帧 → 才轮到上层逻辑。