给机器人装「头」— 舵机、PWM 与云台¶
这一章讲什么: 轮子让机器人会走,舵机让机器人会「摆头」。 三段:① 搞懂舵机——它和普通电机的区别是「停在指定角度」,以及 PWM 脉冲宽度 怎么编码角度;② 把两个舵机(云台的水平 pan、俯仰 tilt)接进 MQTT 服务, 加平滑移动、异常兜底、机械与软件两层校准;③ 写一个画圆行为,让头动起来。 这一章的硬件,是第 14 章人脸跟踪的支架。
1. 先看现象:40 和 -60 之间,隔着一整套机构¶
本章的第一次上机,动作只有几行:
但书里的叮嘱比代码多得多:听到嗡嗡声或摸到发热,就是超出了舵机的物理极限, 要立刻把数值调回来1;测试前要停掉 Inventor HAT 服务(独占硬件的老规矩); 排错第一条:发热 = 接线错了,马上断电重查2。 一台会动的头,代价是一整套新的物理约束——这正是本章的暗线。
2. 顶层全景:从脉冲到圆圈¶
① 机制 舵机 = 直流电机 + 齿轮 + 位置传感器 + 控制器(自带反馈环)
② 语言 PWM:50 Hz 方波,脉宽 1.5 ms = 中位,1 ms / 2 ms = 两端
③ 总线 motors/servo/pan/position、tilt/position、pan/stop、tilt/stop
④ 动作 插值(LERP)把跳变抹平;sin/cos 让头画圆(10 秒一圈、幅度 50°)
⑤ 校准 机械层(齿盘错位 >5° 拆重对)+ 软件层(fine_tune 偏移 ±3°)
图说:①② 是「它怎么听懂角度」,③ 是「谁都能指挥它」,④⑤ 是「动得好看且准」。
3. 核心原理之一:舵机与直流电机——「转」和「停」的分工¶
第 01 章给过定义,这里书里把它讲透了: 直流/齿轮电机只会转;要精确位置得外加传感器。舵机活动范围受限,但你能给它一个角度, 它会(以某种精度)努力停在那儿并保持3。代码可以控制这些角度,或编排成序列。
两类的分工由此而定:轮子用直流电机(要的是连续旋转), 关节用舵机(要的是已知位置)——云台、机械臂、腿,全是舵机的地盘4。
舵机内部其实是一台完整的小机器:控制器 + 直流电机 + 齿轮箱(通常带止挡)+ 位置传感器, 组成一个内置反馈环5。工作逻辑一句话:
控制器拿「你要的位置」和电机轴上传感器报回的「实际位置」比, 两者的差叫误差(error);控制器驱动电机,直到误差归零。 信号不停,它就持续对照、保持或移动到目标位置。5
这就是第 09 章比例控制的近亲——比例控制你写,舵机的控制器出厂就焊在里边。 顺便认一个分类:舵机分 PWM 控制和串行总线控制两类;后者更强但更贵, 本章只用 PWM 型6。
4. 核心原理之二:云台——两个轴,一头一底¶
云台(pan-and-tilt) 是这台机器人的「头」:传感器装在上面,由两个舵机带动两个轴7:
- tilt(俯仰)轴:抬头低头;
- pan(水平)轴:左右转。
线的接法书里标得很细(接错会烧):舵机三根线——信号(橙/白/黄)、电源(红)、 地(棕/黑);HAT 的舵机区每个口标了 S/+/-,黑线对负号8。Tip 原话: 「仔细核对接线,这里的错误会损坏零件。」8
5. 核心原理之三:PWM 编码角度——脉宽就是语言¶
PWM 第 02 章讲过概念,本章落到舵机的具体方言上。固定 50 Hz(每秒 50 个周期, 每周期 20 ms);变的是每个周期里高电平的持续时间——脉宽9:
| 脉宽 | 含义(本书库) |
|---|---|
| 1 ms | +90°(逆时针到底) |
| 1.5 ms | 中位 = 0°10 |
| 2 ms | -90°(顺时针到底) |
三个细节值得带走:① 1.5→1→2 ms 的变化「远比直流电机调速用的脉宽变化细微」—— 舵机的语言是精细的;② 周期长度始终不变,信息全在宽度里;③ 有些库用 0–180°、 中位 90° 的标度——换库先看文档9。新型舵机还有串行总线双向款,能回传状态, ArmBot 和 NE-Five 用的就是这类11。
6. 核心原理之四:平滑移动——线性插值(LERP)¶
value(40) 直接跳过去,动作是「啪、啪」的。平滑化的手法叫线性插值
(linear interpolation,圈子里简称 LERP):把一次跳变拆成很多小步走12:
start_position = -70
end_position = 60
steps = 100
total_time = 1.0
time_per_step = total_time / steps # 每步停多久
movement_per_step = (end - start) / steps # 每步走多少
for n in range(steps):
servo.value(current_position)
current_position += movement_per_step
sleep(time_per_step)
机制拆开:平滑 = 「起点 + 终点 + 总时长」三要素定死后,把位移和时间都均分成 N 份;步数越多越平滑,但有上限(舵机响应速度、CPU 调度都拦着)13。 书里的 Tip 顺带点了一条代码品味:给计算变量起描述性名字、写注释—— 代码写出来是要被读很多遍的,这条适用于任何变量和函数名13。 这个手法后面会升级成行为动画的基础(画圆就是它的连续版)。
7. 核心原理之五:服务化与健壮性——topic 设计 + try/except¶
舵机控制照例进 Inventor HAT 服务(它和轮电机在同一块板上,库只认这一块板)14。 四个新 topic,命名有个设计意图:位置变了、甚至舵机换了控制器,客户端(使用这个服务的那一方)代码都不用改15:
| topic | 消息体 | 动作 |
|---|---|---|
motors/servo/pan/position |
角度数 | 水平轴转到该角度 |
motors/servo/tilt/position |
角度数 | 俯仰轴转到该角度 |
motors/servo/pan/stop |
空 | 停/禁用水平轴 |
motors/servo/tilt/stop |
空 | 停/禁用俯仰轴 |
all/stop 也扩展为连舵机一起停。测法照旧:mosquitto_pub -t motors/servo/pan/position -m "25"。
然后书里演示了一次蓄意搞崩服务:-m "-5\n0" 这种垃圾消息让 float() 抛
ValueError,整个服务直接死16。修法是给消息处理器穿甲:
def set_servo_position(servo, client, userdata, msg):
try:
position = float(msg.payload)
servo.value(position)
except OSError:
print("Error: Failed to set servo position")
except ValueError:
print("Error: Invalid position value")
服务要报错,不能死。 出错的打印出来、流程继续走——OSError(I²C 偶发错误)和 ValueError(消息垃圾)两类都接住17。这是给所有「听消息的长跑服务」的通用纪律: 异常处理不是锦上添花,是服务存续的命门。
网页侧,摇杆页复制改造:x → pan、y → tilt,各自夹在 ±90°;tilt 取负号, 让「上推 = 抬头」符合直觉18。松开摇杆,头回到 0°——但 0° 未必正前方,于是有校准。
8. 核心原理之六:两层校准——机械对齿,软件微调¶
云台装得再小心,舵机齿盘是有齿数的:错半齿,0° 就可能偏好几度。书里分两层:
机械层(误差 > 5° 才动):把云台座拆开,拔下输出臂(servo horn)与齿盘的咬合, 转一个齿再插回去,让舵机中位对上「头朝正前」;齿数固定,未必能 100% 对准—— 残差(剩下的那点误差)交给软件19。
软件层(残差 ≤ 5°):给 set_servo_position 加一个 fine_tune 参数,
所有位置统一加一个偏移:set_pan(..., fine_tune=3) 表示每条位置消息都 +3°20。
作者自己的 tilt 恰好是 0° 不用调——这种「运气」书里如实说。
先机械后软件的顺序是通用的校准哲学:能用物理消掉的误差别留给代码; 物理消不掉的,用常数兜。
9. 核心原理之七:云台装车与画圆行为¶
机械上车:底盘上钻四个孔(前 22 mm、后 54 mm、左右各进 49 mm—— (126-28)/2 = 49 居中),1 mm 钻头开孔再扩 2 mm,胶带当标记纸(黑底盘铅笔不显), M2 螺栓固定21。
行为压轴——画圆。用时间当参数,一圈 10 秒、幅度 ±50°:
self.seconds_per_circle = 10
self.radians_per_second = (2 * math.pi) / self.seconds_per_circle
self.radius = 50
while True:
frame_in_radians = time.time() * self.radians_per_second
pan_position = self.radius * math.sin(frame_in_radians)
tilt_position = self.radius * math.cos(frame_in_radians)
publish_json(client, "motors/servo/pan/position", pan_position)
publish_json(client, "motors/servo/tilt/position", tilt_position)
time.sleep(0.05)
机制:三角函数用弧度;一整圈 = 2π 弧度,除以 10 秒得角速度; 任意时刻的角度 = 当前时间 × 角速度;一个轴取 sin、一个轴取 cos、各乘半径, 笔尖就走出了一个圆——参数方程画圆22。 每 0.05 秒发一帧(20 帧/秒),行为只管发目标位置,LERP 式的平滑由舵机自己的 控制环和机械惯性兜底。这套「时间基动画」的手法,作者明说也用在 Spiderbot 那 18 条腿的 18 个舵机上23——本小章是大道之渐。
排错清单照例扎实:线序 G/V/S 对错(黑红信号)、插头插紧、电池低会抖、 接头线皮有没有破(作者退过一批线有问题的货)24。还有句大实话: 「这是入门级的便宜云台;加钱加时间都能更顺滑,顶级云台要几百美元。」24
10. 作者的判断与证据¶
- 判断: 舵机归电机服务管,而不是单开服务——因为库只认一块板,已有服务已握着它。 topic 命名按「部位」不按「编号」,为将来换布局留余地1415。
- 判断: 校准先机械后软件;fine_tune 放进服务层而不是每个行为里, 因为「偏移对所有行为都一样」20。
- 事实层: 50 Hz/20 ms、1/1.5/2 ms 脉宽、±70° 安全区(±90° 会热会伤)、 (126-28)/2 = 49 mm 钻孔尺寸——可核对 SG90 数据手册9121。
- 坦承: 画圆行为里 tilt 的物理极限比 pan 更紧;廉价云台的 judder(抖动) 设计如此,不是装错了24。
11. 边界与局限¶
- 角度精度的天花板是齿盘。 机械校准按齿走(一度几度地跳),软件微调只有 ±3° 量程; 要更高精度得上串行总线舵机(本章明确绕开)620。
- 没有位置反馈。 PWM 舵机是「说了不听回响」的——发了 25° 它到没到,不知道; 这也是它便宜的原因。
- 云台承重有上限。 本章只挂距离传感器;第 14 章挂摄像头已是这套塑料件的合理上限。
- 行为动画只教了时间基一路。 关键帧、缓动函数(easing)这些动画手法一概未提, 画圆是惟一样板。
12. 可带走的¶
- 选电机看任务:连续旋转用直流,停在指定角度用舵机;舵机 = 电机 + 齿轮 + 位置传感器
- 控制器,误差归零是它的本能35。
- 舵机的 PWM 方言:50 Hz 定频,脉宽定角(1.5 ms 中位,1/2 ms 两端)910。
- ±70° 是安全区;嗡嗡声、发热、到不了位 = 超限或电池低,先退回来124。
- LERP 三要素:起点、终点、总时长;步数均分位移与时间,越多越平滑1213。
- 服务健壮性:消息处理器必须 try/except——报错可以,退出不行17。
- topic 按部位命名(pan/tilt),不按编号——布局变了客户端不改15。
- 校准两层:机械对齿(>5°)+ 软件 fine_tune(残差);先物理后代码1920。
- 时间基动画:角度 = 时间 × 角速度;sin 配 cos 画圆;高频小步发布22。
- 排错顺序:线序 → 插紧 → 电池 → 线皮 → 服务日志24。
13. 原文地图¶
| 主题 | 原书位置 |
|---|---|
| 舵机与直流电机的区别 | text/89-fm-technical-requirements.txt:25(搜「restricted range」) |
| PWM 舵机 vs 串行总线舵机 | text/89-fm-technical-requirements.txt:36(搜「serial bus」) |
| 云台两轴 | text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:40(搜「tilt axis」) |
| 接线与损坏警告 | text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:68(搜「black wire」) · text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:127(搜「buzzing noises」) |
| LERP 平滑 | text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:134(搜「linear interpolation」) · text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:161(搜「more steps we take」) |
| 舵机内部反馈环 | text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:207(搜「reduces to zero」) |
| PWM 脉宽编码 | text/91-fm-controlling-servo-motors-via-mqtt.txt:1(搜「1.5 milliseconds」) · text/91-fm-controlling-servo-motors-via-mqtt.txt:8(搜「2 milliseconds」) |
| 四个舵机 topic 与健壮性 | text/91-fm-controlling-servo-motors-via-mqtt.txt:44(搜「motors/servo/pan/position」) · text/91-fm-controlling-servo-motors-via-mqtt.txt:169(搜「not exit the service」) |
| 物理极限 ±70° | text/91-fm-controlling-servo-motors-via-mqtt.txt:186(搜「physical limits beyond 70」) |
| 摇杆页与取负 | text/91-fm-controlling-servo-motors-via-mqtt.txt:201(搜「Math.min(90」) |
| 两层校准 | text/91-fm-controlling-servo-motors-via-mqtt.txt:248(搜「above 5 degrees」) · text/93-fm-creating-servo-motor-behaviors.txt:34(搜「the teeth on the connecting splines」) |
| 装车钻孔尺寸 | text/92-fm-attaching-the-pan-and-tilt-mechanism-to-the-robo.txt:30(搜「49 mm」) |
| 画圆行为 | text/93-fm-creating-servo-motor-behaviors.txt:85(搜「seconds_per_circle」) |
| 总结与扩展 | text/94-fm-summary.txt:30(搜「18 motors in Spiderbot」) |
-
出处:「Positioning a servo motor with Raspberry Pi」第 127 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:127,搜「buzzing noises」)。原文:value 可在 +70 到 -70 度间安全调整;听到嗡嗡声或感到发热 = 超限,调回来;后文会加代码保护。 ↩↩↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 180 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:180,搜「isn't correct」)。原文排错:发热 = 接线错,立即拔线重查;AttributeError ioe = 没停 HAT 服务(独占);全无动作查电源;嗡嗡/抖动先确认 ±70° 内、电源够;别家库可能用 0–180° 标度。 ↩ -
出处:「Technical requirements」第 25 段(
text/89-fm-technical-requirements.txt:25,搜「restricted range」)。原文:直流/齿轮电机连续旋转,要精确位置需外加传感器;舵机范围受限,但给定角度会努力保持;代码可控制角度或编排序列。尺寸从 20–30 mm 到能驱动重型机械。 ↩↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 1 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:1,搜「18 servo motors」)。原文:Spiderbot 18 个舵机(每腿 3:脚、膝、前后摆);NE-Five 双臂各 8 个、颈 3 个、直线执行器调体位——舵机让机器人会转头、摆臂、走路、抓取。 ↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 207 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:207,搜「reduces to zero」)。原文:舵机内部 = 控制器 + 直流电机 + 齿轮箱(常带止挡)+ 传感器;输入目标位置,控制器与轴上传感器比较,差即误差,驱动电机至误差归零;信号不停就持续保持/移动。 ↩↩↩ -
出处:「Technical requirements」第 36 段(
text/89-fm-technical-requirements.txt:36,搜「serial bus」)。原文:舵机分 PWM 控制与串行总线控制两类;后者更强更贵,本章只用 PWM 型。 ↩↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 40 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:40,搜「tilt axis」)。原文:云台让传感器(或其他东西)绕两轴转:tilt 抬头低头,pan 左右旋转;先临时把 pan 舵机接到舵机口 1 演示。 ↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 68 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:68,搜「black wire」)。原文:信号线橙/白/黄,电源红,地棕/黑;HAT 舵机区标 S/+/-;黑线对负号;Tip:仔细核线,错误会损坏零件。 ↩↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 229 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:229,搜「50 Hz」)。原文:PWM 方波频率恒定(50 Hz,周期 20 ms);脉宽编码位置:短脉冲低值、长脉冲高值;三格图演示 1 ms(+90°)/1.5 ms(0°)/2 ms(-90°);这些脉宽变化远小于直流电机调速的变化;周期长度不变。 ↩↩↩↩ -
出处:「Controlling servo motors via MQTT」第 1 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:1,搜「1.5 milliseconds」)。原文:左图脉宽 1.5 ms——高 1.5 ms、休到下一周期;舵机解读为中位,即本库的 0°;中图 1 ms = 逆时针 90°,右图 2 ms = 顺时针 -90°。 ↩↩ -
出处:「Controlling servo motors via MQTT」第 15 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:15,搜「two-way serial bus」)。原文:部分舵机的内部位置传感器有额外输出,一批新品有双向串行总线,可精确控制与反馈;ArmBot 用开环反馈舵机,NE-Five 用数据总线舵机。 ↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 134 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:134,搜「linear interpolation」)。原文:用线性插值(LERPing,linear interpolation 的圈內简称)可做平滑运动;代码给起点 -70、终点 60、100 步、总 1 秒,算出每步时长与步进量,循环走位。 ↩↩ -
出处:「Positioning a servo motor with Raspberry Pi」第 161 段(
text/90-fm-positioning-a-servo-motor-with-raspberry-pi.txt:161,搜「more steps we take」)。原文:计算机做不出无限小的步,要选步数;步越多越平滑但有限度;Tip:计算用描述性变量名和注释——代码写完要读很多遍,此原则适用于一切命名。 ↩↩↩ -
出处:「Controlling servo motors via MQTT」第 30 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:30,搜「single board」)。原文:Inventor HAT 同时管轮电机与舵机;库直连单块板,而 HAT 服务已在管 motors topic,舵机控制也应加在这里。 ↩↩ -
出处:「Controlling servo motors via MQTT」第 44 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:44,搜「motors/servo/pan/position」)。原文:四 topic——pan/tilt 的 position(度数)与 stop;按部位命名意味着舵机挪位置或换控制器,客户端代码都无需改;本章不动 launcher 和 web server。 ↩↩↩ -
出处:「Controlling servo motors via MQTT」第 168 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:168,搜「crash」)。原文:误发的 payload(如 -50 写成两行)会让 float() 抛 ValueError,整个服务死掉(journal 可见);期望它报错但别退出。 ↩ -
出处:「Controlling servo motors via MQTT」第 169 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:169,搜「not exit the service」)。原文:try/except 包住取值与设位;OSError(舵机/I²C 偶发)与 ValueError(消息非法)分别打印;服务遇错继续跑。 ↩↩ -
出处:「Controlling servo motors via MQTT」第 201 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:201,搜「Math.min(90」)。原文:摇杆 x→pan、y→tilt,各夹 ±90;tilt 发布取负,上推 = 抬头;JSON.stringify 打包;先浏览器控制台调、再上手机。 ↩ -
出处:「Controlling servo motors via MQTT」第 248 段(
text/91-fm-controlling-servo-motors-via-mqtt.txt:248,搜「above 5 degrees」)。原文:误差 >5° 拆云台座,拔 servo horn 对齿重插(齿有齿数,未必 100% 对准);<5° 跳过;tilt 同理,两套螺栓别混;装回后软件兜残差。 ↩↩ -
出处:「Creating servo motor behaviors」第 34 段(
text/93-fm-creating-servo-motor-behaviors.txt:34,搜「the teeth on the connecting splines」)。原文:齿盘齿数让 0° 未必 100% 朝前;给 set_servo_position 加 fine_tune=0 参数,位置统一加偏移;SSH 发消息试出 -3..3 的小值;作者 pan 用 fine_tune=3、tilt 恰好 0;偏移放服务层因为对所有行为一致。 ↩↩↩↩ -
出处:「Attaching the pan-and-tilt mechanism to the robot」第 30 段(
text/92-fm-attaching-the-pan-and-tilt-mechanism-to-the-robo.txt:30,搜「49 mm」)。原文:底盘孔位前 22 mm、后 54 mm(22+32)、横向孔距 28 mm;(126-28)/2 = 49 mm 定左右居中;黑底盘贴胶带作标记纸;1 mm 钻头开孔再扩 2 mm;M2 螺栓固定,底盘大孔方便上螺母。 ↩↩ -
出处:「Creating servo motor behaviors」第 85 段(
text/93-fm-creating-servo-motor-behaviors.txt:85,搜「seconds_per_circle」)。原文:10 秒一圈、半径 50;radians_per_second = 2π/10;每帧 角度 = time.time() × 角速度;pan = r·sin、tilt = r·cos(圆的参数方程);0.05 秒一帧;改数字可变速度与幅度。 ↩↩ -
出处:「Summary」第 30 段(
text/94-fm-summary.txt:30,搜「18 motors in Spiderbot」)。原文:这套代码可移植到其他舵机系统(如机械臂);动画式手法(平滑、圆周)作者用于 Spiderbot 的 18 腿 18 舵机。 ↩ -
出处:「Summary」第 16 段(
text/94-fm-summary.txt:16,搜「hundreds of dollars」)。原文排错:G/V/S 线序、插紧、电池低会抖动、线皮无破损(作者退过一批线有问题的舵机);入门级便宜云台,投入时间或金钱能更顺滑,顶级云台数百美元。 ↩↩↩↩↩