posts/stm32-servo-pwm-vs-delay.md

同一只舵机的两代驱动:延时法 vs 定时器 PWM,谁才是产品级答案

📅 发布日期2026-09-17
STM32 系列 复盘 31 分钟阅读 #舵机#PWM#标定#TIM2#引脚重映射

同一只舵机的两代驱动:延时法 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 与原版仅一处显示文案差异。按键代码没动过——排除了"我改坏"的可能。

#排查第二步:对照实验

把最早的代码原样烧回去复现——原版也乱跑。至此实锤:与代码无关,硬件问题。

#原因分析(三条叠加)

  1. TTP224 上电校准被手污染:这颗触摸芯片每次上电的最初约 0.5 秒会自动校准触摸基准。连续测试时手一直停在板子附近,某次复位时手在触摸区上,基准就校错了——之后芯片"坚信"一直有键被按(输出常低),表现为锁死。
  2. 供电跌落连锁:舵机大电流转动把 USB 供电拉出毛刺,TTP224 欠压输出乱跳 → 多键齐触发 → 舵机来回乱转 → 电流更大。恶性循环。
  3. 触摸板汗渍污染:一下午的连续触摸,手汗导电让电容感应阈值漂移。

#处置:绕过它

好在洋桃 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 一起关)——固件一跑起来,调试口物理失效。

两个教训:

  1. 官方例程不是免疫的。跨板移植的官方 hex,烧之前最好确认调试口配置;
  2. 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 CH3PB0部分重映射仍是 PB0❌
TIM2 CH1PA0部分重映射1:CH1=PA15(CH2=PB3、CH3=PB10、CH4=PB11)✅
TIM1 CH1PA8—❌

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µs18001500
45°1102µs33063000
90°1604µs48124500
135°2079µs62376000
180°2555µs76657500

官方教科书值和这只舵机的实测值系统性偏差约 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,先写"上电即执行"的最小固件分流,别在完整应用里大海捞针
  • 对照实验是硬件排障的胜负手:"原版代码也复现"六个字,比任何分析都更能终结"是不是代码改坏了"的争论

#十八、相关阅读

#十八、更新日志

📝共 1 条记录
日期类型更新内容
2026-09-17新增首次发布:延时法驱动与三点标定 → 静默失败与单帧断粮 → TTP224 锁死(对照实验定位)→ 官方例程禁 SWD 与 BOOT0 救援 → 定时器 PWM(TIM2 部分重映射送回 PA15、JTAG 释放)→ 标定数据迁移 → 两代驱动八维度对比与中断抖动分析