同一只舵机的两代驱动:延时法 vs 定时器 PWM,谁才是产品级答案
同一只舵机的两代驱动:延时法 vs 定时器 PWM,谁才是产品级答案
双看门狗那篇复盘里提过,知识库里那篇 PWM 与电机控制是"理论篇",舵机实证一直欠着。这次 SG90 到货,洋桃 1 号配套的 demo37_SERVO_DELAY 上板,本来预估半小时收工,结果从中午折腾到深夜,抓出四只代码层的虫和两起硬件疑案。第二天接着把 demo38 的定时器 PWM 也跑通——同一个洋桃 1 号、同一只 SG90、同一条排线,把两代驱动放在同一张考卷上,顺便解决一个官方例程没打算帮你解决的问题:官方 PWM 例程输出在 PB0,而我的舵机排线插在 PA15 座上,三针一体的线,不想拆。
最终舵机是听话了,但比"听话"更值钱的是这一路的排查方法——这篇把两天的尸体全摆出来。
#一、延时法驱动舵机:最笨但最诚实的 PWM
SG90 舵机的协议一句话就能说完:50Hz 的方波(周期 20ms),高电平宽度 0.5~2.5ms,对应 0~180°。理论篇讲过 PWM 靠定时器硬件出波,而 demo37 用的是最笨的"延时法"——CPU 亲手把电平拉高、数微秒、拉低、再补齐周期:
cvoid SG90_angle(u8 a){ //舵机角度控制(官方原始版)
u8 b=100;//角度校正偏移量
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(1)); //拉高
delay_us(500+a*10+b); //高电平:600us(0°)~2400us(180°)
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(0)); //拉低
delay_us(19500-a*10-b); //低电平补齐 20ms 周期
}
信号脚 PA15,纯 GPIO 推挽输出。SG90_angle(0) 调用一次,CPU 空转整整 20ms,期间主循环什么都没干。这个写法的缺陷一眼可见——但恰恰因为它"笨",每一微秒都是 CPU 自己数出来的,标定时改什么、为什么改,全都透明。学 PWM,先从延时法学起,再看定时器硬件出波,顺序不能反——不知道 20ms 里每一微秒意味着什么,就直接上寄存器,等于背公式不做题。
#二、踩坑一:180° 只转 170°——比例失真
烧录官方原版,触摸按键切换 0/45/90/180 四个角度。实测:180° 只转到约 170°。按"比例放大"直觉,把每度系数从 10µs 放大到 10.6µs(180/170 倍):
cpw = 600 + a*106/10; //每度10.6us:按实测误差(180°只转170°)放大 180/170 倍
烧上去,180° 还差 3°——但注意,90° 也差了 3°。如果误差是纯比例的,90° 应该只差 1.5°。误差不随角度等比增长,说明这只舵机的行程响应是非线性的。
#三、踩坑二:放弃万能系数,改三点标定
非线性就意味着"一个系数修所有角度"这条路走不通。工程上的标准做法是:实测若干个锚点,段内线性插值。我用三个锚点(0°/90°/180°)做了分段线性:
cvoid SG90_angle(u8 a){
u16 pw; //高电平脉宽(微秒)
if(a <= 90){
pw = 600 + a*1004/90; //0°=600us, 90°=1604us(实测标定)
}else{
pw = 1604 + (a-90)*951/90; //90°=1604us, 180°=2555us(实测标定)
}
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(1));
delay_us(pw);
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(0));
delay_us(20000-pw);
}
90° 锚点是两段的共享点,保证分段处连续。实测 0°/45°/90°/135° 全部到位,180° 只剩 1~2° 残差。每个 SG90 的非线性程度都不一样(塑料齿轮间隙 + 电位器线性度),出厂参数只能当起点,"实测标定"才是产品级做法——工业伺服出厂要做全行程标定表,玩具舵机标三个点就够了。
#四、踩坑三:贪那 1~2°,撞上"静默失败"
我不死心,把 180° 的脉宽从 2555µs 推到 2571µs(+1.5°)。结果:OLED 显示 180°,舵机纹丝不动。回退 2555µs,立刻恢复。
这就是两天里最有价值的发现:脉宽超出舵机的容限上限(SG90 标称最大约 2500µs)后,舵机静默拒收——不报错、不抖动、不提示,就是不执行。2555µs 在容限边缘还被勉强接受,2571µs 直接越线。
"静默失败"是嵌入式里最阴险的失败模式:参数超界 → 系统拒收 → 表面一切正常。对比软件世界,API 参数超界至少抛个异常;硬件世界连异常都没有。教训写进肌肉记忆:标定要在器件容限内找最优,行程到不了的地方,是机械极限不是代码 bug。SG90 标称 180°,实际能到 ~178° 就该知足。
#五、踩坑四:单帧断粮,舵机半路停摆
角度标定完成后换按键逻辑,又撞一坑:从 180° 按键回 0°,OLED 显示 Angle 0,舵机却停在 90° 不动了。
原因:我的按键处理每次只调用一次 SG90_angle(0)——只发出一帧 20ms 的脉冲。从 180° 转 0° 是最长行程,舵机转到一半,脉冲流断了,SG90 内部失去指令就原地停住。之前"按住不放"的原版逻辑之所以正常,是因为按住期间主循环每圈都在发脉冲,相当于持续供粮。
修复:触发后连发 30 帧(600ms),足够走完全程:
cfor(i=0;i<30;i++) SG90_angle(180); //连发30帧(600ms):单帧20ms不够长行程
舵机要的不是"一条指令",而是"持续的指令流"——这也是后面定时器 PWM 价值的最好铺垫:硬件出波永不断粮,CPU 一微秒都不用陪跑。
#六、硬件疑案一:TTP224 触摸芯片全亮锁死
代码的虫抓完了,但测试中途出现过一个更诡异的现象:4 个触摸键指示灯全亮、松手不释放、舵机乱转,且越测越频繁,最后"一按就锁死"。
#排查第一步:证明不是代码
对比工程的最终手段是把原始发布包里的文件解压出来逐字节比对:touch_key.c 与原版完全一致;main.c 与原版仅一处显示文案差异。按键代码没动过——排除了"我改坏"的可能。
#排查第二步:对照实验
把最早的代码原样烧回去复现——原版也乱跑。至此实锤:与代码无关,硬件问题。
#原因分析(三条叠加)
- TTP224 上电校准被手污染:这颗触摸芯片每次上电的最初约 0.5 秒会自动校准触摸基准。连续测试时手一直停在板子附近,某次复位时手在触摸区上,基准就校错了——之后芯片"坚信"一直有键被按(输出常低),表现为锁死。
- 供电跌落连锁:舵机大电流转动把 USB 供电拉出毛刺,TTP224 欠压输出乱跳 → 多键齐触发 → 舵机来回乱转 → 电流更大。恶性循环。
- 触摸板汗渍污染:一下午的连续触摸,手汗导电让电容感应阈值漂移。
#处置:绕过它
好在洋桃 1 号的设计里藏了后手:板载 KEY1/KEY2 微动开关与触摸键 A/B 共用同一组引脚(PA0/PA1)。于是最终固件直接改用 KEY1/KEY2 控制(KEY1 每按 +45° 步进、KEY2 回零),彻底绕开 TTP224。芯片本身值几毛钱,坏了可以换,但"板载按键与触摸共用引脚"这个发现让维修方案变成零成本。
顺带的加固经验(写进了最终代码):按键处理加 20ms 消抖 + 二次确认 + 等待释放(带超时保护),一次触摸只响应一个键——就算信号源乱跳,软件上也只认一个键。
#七、硬件疑案二:官方例程烧完,SWD 失联
排障途中为了"用官方固件做基准",烧了一颗从配套资料里找的官方 hex(PWM 驱动舵机 demo)。烧完后 SWD 直接失联——Unable to get core ID,连"复位模式下连接"都失败。
最后发现规律:BOOT0 拉高进 ROM bootloader 就能连,正常启动跑这个官方固件就失联。结论:这颗为另一块板(YT32B1)编写的官方 demo,为了释放 PA15 做舵机信号脚,代码里有 SWJ 完全禁用(JTAG+SWD 一起关)——固件一跑起来,调试口物理失效。
两个教训:
- 官方例程不是免疫的。跨板移植的官方 hex,烧之前最好确认调试口配置;
- BOOT0 是最后一根救命稻草。SWD 被固件锁死时,BOOT0=1 让芯片进 ROM 引导(此时调试口必开),重新烧录即可复活。本次实验 BOOT0 救援成功多次,全程没动过一颗焊点。
#八、供电:乱转与失联的共同元凶
复盘整个下午,"舵机乱转"和"两次 core 失联"背后都有同一个推手:USB 供电带不动舵机。SG90 堵转电流可达 700mA, USB 口 + 板载稳压器的组合在转动瞬间电压塌陷,轻则触摸芯片乱跳、重则 MCU 内核失联。
产品级做法:舵机独立 5V 供电(与 MCU 共地),或至少转动瞬间减小负载。学习阶段实在要用 USB,那就记得:转不动别硬转,堵转是舵机电流最大的状态。
#九、延时法的最终代码:标定 + 按键 + 连发
c//SG90.c —— 三点标定 + 分段线性
void SG90_angle(u8 a){
u16 pw;
if(a <= 90){
pw = 600 + a*1004/90; //0°=600us, 90°=1604us
}else{
pw = 1604 + (a-90)*951/90; //90°=1604us, 180°=2555us(容限上限)
}
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(1));
delay_us(pw);
GPIO_WriteBit(SE_PORT,SE_OUT,(BitAction)(0));
delay_us(20000-pw);
}
//main.c —— KEY1 步进 / KEY2 回零,30 帧连发
u8 cur=0, i;
while(1){
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_A)){ //KEY1(=PA0)
delay_ms(20); //消抖
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_A)){
cur+=45; if(cur>180) cur=0;
if(cur==0) { OLED_DISPLAY_8x16_BUFFER(6," Angle 0 "); for(i=0;i<30;i++)SG90_angle(0); }
else if(cur==45){ OLED_DISPLAY_8x16_BUFFER(6," Angle 45 "); for(i=0;i<30;i++)SG90_angle(45); }
else if(cur==90){ OLED_DISPLAY_8x16_BUFFER(6," Angle 90 "); for(i=0;i<30;i++)SG90_angle(90); }
else if(cur==135){OLED_DISPLAY_8x16_BUFFER(6," Angle 135 "); for(i=0;i<30;i++)SG90_angle(135); }
else { OLED_DISPLAY_8x16_BUFFER(6," Angle 180 "); for(i=0;i<30;i++)SG90_angle(180); }
while(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_A)); //等待释放
delay_ms(50);
}
}
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_B)){ //KEY2(=PA1) 回零
delay_ms(20);
if(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_B)){
cur=0;
OLED_DISPLAY_8x16_BUFFER(6," Angle 0 ");
for(i=0;i<30;i++)SG90_angle(0);
while(!GPIO_ReadInputDataBit(TOUCH_KEYPORT,TOUCH_KEY_B));
delay_ms(50);
}
}
}
#十、PWM 版官方例程:寄存器里的 20ms
demo38 的核心是 TIM3_PWM_Init(arr, psc),逐行读懂它,PWM 就通了:
cTIM3_PWM_Init(59999, 23); // 溢出时间 = (59999+1)×(23+1)/72MHz = 20ms → 50Hz
- 72MHz 主时钟,预分频 23 分完是 3MHz——计数器加 1 = 1/3 微秒
- 自动重装载 59999,数满 60000 个计数正好 20ms 一个周期
- 改角度 = 改比较值 CCR:
TIM_SetCompare3(TIM3, 4500)→ 高电平 4500 × 1/3µs = 1500µs → 90°
一句话总结:脉宽(µs) = CCR ÷ 3。CNT 从 0 数到 59999 循环,CNT < CCR 时输出高、之后输出低——比较值在周期里的位置,就是高电平的宽度。这就是 PWM1 模式的全部秘密。
官方例程用四个触摸键切换四个角度(CCR=1500/3000/4500/7500,对应教科书脉宽 500/1000/1500/2500µs)。但有两件事官方没管:一是触摸键芯片已经被我们判定损坏(上文硬件疑案一),二是输出引脚。
#十一、引脚打架:官方要 PB0,我的排线在 PA15
延时法(demo37)的信号脚是 PA15,舵机排线三针一体插在板上的舵机座,用的就是 PA15。而 PWM 例程的 TIM3 通道 3 无重映射时固定输出在 PB0。
第一反应是查"能不能把 TIM3 挪到 PA15"——答案是不能。把 F103 的定时器引脚映射表翻一遍:
| 定时器 | 无重映射 | 部分重映射 | 可到 PA15? |
|---|---|---|---|
| TIM3 CH3 | PB0 | 部分重映射仍是 PB0 | ❌ |
| TIM2 CH1 | PA0 | 部分重映射1:CH1=PA15(CH2=PB3、CH3=PB10、CH4=PB11) | ✅ |
| TIM1 CH1 | PA8 | — | ❌ |
TIM2 部分重映射1(GPIO_PartialRemap1_TIM2)正好把 CH1 送到 PA15——排线不用动,改用 TIM2 就行。这个案例说明:引脚冲突时,"换引脚"和"换定时器"之间还有第三条路——查重映射表。STM32 的重映射机制就是官方预留的引脚冲突解药,可惜大多数人只背"TIM3 通道对应哪个脚",从不翻映射表。
#十二、JTAG 释放:PA15 不只是个 GPIO
PA15 有个特殊身份:JTDI——JTAG 调试口的数据输入脚。F103 复位后 PA15/PB3/PB4 默认归 JTAG 所有,想把它当 PWM 输出脚,必须先"赶走"JTAG:
cGPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 释放JTAG,保留SWD
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE); // TIM2_CH1 → PA15
注意这两行的先后与区别:
- JTAGDisable 只关 JTAG(释放 PA15/PB3/PB4),SWD 调试口(PA13/PA14)安然无恙——烧录调试一切正常
- 上文那颗官方 hex 用的是"SWJ 完全禁用",SWD 一起陪葬,烧完调试口直接失联,只能 BOOT0 救援
同一个 API 家族,一个参数之差,一个是常规操作,一个是给自己埋雷。动调试引脚前,先想清楚 SWD 还要不要。
#十三、标定移植:延时法的数据今天直接用
延时法时代实测的三点标定(0°=600µs、90°=1604µs、180°=2555µs),换算成 CCR 只需乘 3:
| 角度 | 脉宽(实测标定) | CCR(×3) | 官方教科书值 |
|---|---|---|---|
| 0° | 600µs | 1800 | 1500 |
| 45° | 1102µs | 3306 | 3000 |
| 90° | 1604µs | 4812 | 4500 |
| 135° | 2079µs | 6237 | 6000 |
| 180° | 2555µs | 7665 | 7500 |
官方教科书值和这只舵机的实测值系统性偏差约 100µs(≈10° 量级的起点差)——个体差异是舵机的宿命,标定数据才是这只舵机的真实身份。PWM 版烧上去,四个角度一次到位,延时法标定的成果零损耗迁移。这也验证了标定数据的存在价值:它不属于某一种驱动方式,它属于这只舵机本身。
#十四、诊断固件:一招分流"代码还是接线"
调试 PWM 版时还顺手用了个小技巧。烧录后舵机纹丝不动,是代码问题还是接线问题?与其反复检查,不如写个诊断固件:跳过所有按键逻辑,初始化完直接输出 90° 中位脉宽——
cTIM2_PWM_Init_PA15(59999, 23);
TIM_SetCompare1(TIM2, 4500); // 诊断:上电即输出90度,无需任何按键
上电转 90° = 波形链路全通,问题在按键;纹丝不动 = 接线/引脚错误。二选一,30 秒出结论。这个"上电即执行关键动作"的诊断模式,比在主循环里加 printf 高效得多——让系统自己把最大可疑变量先跑一遍。
#十五、两代驱动八维度对比(本文核心)
同一只舵机跑完两种方案,把体感差异量化成一张表:
| 维度 | 延时法(demo37) | 定时器 PWM(demo38) |
|---|---|---|
| 波形生成 | CPU 手动翻转 GPIO + delay_us 数微秒 | TIM 硬件计数器自动出波 |
| CPU 占用 | 每次调用阻塞 20ms;连发 30 帧 = 600ms 全程陪跑 | 设一次 CCR,之后 0 占用 |
| 断粮风险 | 有——单帧 20ms 不够长行程,实测停在 90° | 无——硬件永不停发,直到下次改 CCR |
| 精度 | delay_us 软件延时,含循环开销误差 | 计数器精度 1/3µs(晶振级) |
| 多路扩展 | 每加一路 CPU 负担线性涨,四路几乎吃满主循环 | 同一定时器四通道,加路几乎零成本 |
| 引脚灵活性 | 任意 GPIO 想用就用 | 受通道映射约束(重映射可解,但不自由) |
| 中断抗扰 | 致命伤(见下节) | 硬件波形雷打不动 |
| 代码与调试 | 十行代码,每微秒可见,教学极佳 | 寄存器抽象,需理解 CNT/CCR/ARR/重映射 |
#十六、延时法的隐藏致命伤:中断抖动
对比表里最值得展开的是"中断抗扰"。延时法的 20ms 周期靠 CPU 空转数出来的——任何一个中断(串口来了数据、定时器溢出、外部按键)都会把这段"数数时间"拉长。延时法驱动时如果开着串口打印,你会看到舵机微微抖动:中断把 600µs 的高电平拉成了 700µs,舵机以为你要它多转 10°。
这在"main 循环 + 一个按键"的 demo 里永远暴露不出来,但在真实产品里——主循环要跑通信、采样、显示,中断此起彼伏——延时法的舵机波形就是永不停歇的抖动源。这就是产品代码永远用硬件 PWM 的根本原因:CNT 是硬件数的,中断爱来不来,波形一个微秒都不差。
反过来说,教学阶段先跑延时法又是完全正确的顺序:你亲手数过 20ms 里的每一微秒,才知道 PWM 外设替你干了什么。先延时后 PWM,不是绕远路,是打地基。
#十七、应用价值
- 选型决策:一颗舵机 + 裸主循环,延时法够用;两颗以上舵机、或主循环还有别的活、或系统里有中断——直接 PWM,不用犹豫
- 标定即产品流程:凡是带执行器的产品(舵机、步进、温控),出厂前都要做行程标定。本文的三点标定是最小可行版本,工业设备做全行程查表
- 标定数据是资产:属于执行器本身,不绑定驱动方式。换驱动方案,标定值一个公式就迁移(本文:CCR = 脉宽µs × 3)
- 容限意识:驱动任何执行器前先查数据手册的标称范围(SG90:500~2500µs),标定只能在容限内做文章,超限就是静默失败
- 重映射思维:引脚冲突不是死局,先查映射表再动烙铁。STM32 的重映射能解决大半布局尴尬
- 调试引脚红线:PA13/PA14/PA15/PB3/PB4 这五个 JTAG/SWJ 引脚,动之前先想清楚 SWJ_CFG 的三种模式(全开 / JTAGDisable / 全禁),全禁 = 调试口自杀;真锁死了还有 BOOT0 这根救命稻草
- 诊断固件习惯:硬件行为类 bug,先写"上电即执行"的最小固件分流,别在完整应用里大海捞针
- 对照实验是硬件排障的胜负手:"原版代码也复现"六个字,比任何分析都更能终结"是不是代码改坏了"的争论
#十八、相关阅读
- PWM 与电机控制原理——本文的理论底子(CNT/ARR/CCR、H 桥、步进)
- 定时器与中断:从 1 秒心跳到五阶段状态机——TIM2/TIM3 出波之外,定时器还能干什么
- 触摸按键 TTP223(PA0–PA3)——本次"全亮锁死"的当事人与排查思路
- 洋桃1号 MODE 键 / BOOT 状态速查——BOOT0 救援 SWD 失联的完整流程
#十八、更新日志
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-17 | 新增 | 首次发布:延时法驱动与三点标定 → 静默失败与单帧断粮 → TTP224 锁死(对照实验定位)→ 官方例程禁 SWD 与 BOOT0 救援 → 定时器 PWM(TIM2 部分重映射送回 PA15、JTAG 释放)→ 标定数据迁移 → 两代驱动八维度对比与中断抖动分析 |