
舵机狂抖、LED 频闪、频率差一倍这期把 TIM2 四路 PWM 一次讲透。从 APB1 时钟链的 72MHz 陷阱到 PSC/ARR 的 1 细节再到 SG90 舵机脉宽映射与呼吸灯指数曲线配合六条避坑清单让你看完就能直接跑通代码。为什么舵机、电机、LED 都离不开 PWM你肯定遇到过这种情况想用单片机控制舵机转到指定角度结果舵机要么不动要么疯狂抖动想做个呼吸灯效果LED 却闪烁得刺眼。问题大概率出在 PWM 配置上。PWM脉冲宽度调制的原理很简单通过调节高电平在一个周期内的占比占空比控制输出给负载的平均功率。舵机看的是脉宽对应的角度LED 看的是平均电流对应的亮度电机看的是等效电压对应的转速。可以说PWM 是嵌入式世界里最基础的「模拟输出」手段。今天以 STM32F103 的 TIM2 为例把四路 PWM 从时钟配置到实际应用完整走一遍。硬件与引脚TIM2 四路输出到 PA0~PA3TIM2 是 STM32F103 的通用定时器支持四路输出比较通道。默认映射CH1 → PA0CH2 → PA1CH3 → PA2CH4 → PA3注意PA0~PA3 同时也是 ADC1 的模拟输入通道ADC1_IN0~IN3。它们不冲突但别配混——配成复用推挽就是 PWM配成模拟输入就是 ADC改的是同一个引脚的复用功能表。GPIO 配成复用推挽输出GPIO_Mode_AF_PP速度 50MHz。默认映射不需要开 AFIO 时钟只有重映射才要。舵机接线要小心SG90 舵机堵转电流 500mA 以上绝不能从 MCU 的 3.3V 引脚取电。正确接法舵机电源接独立 5V 电源信号线接 PA0和 MCU 共地。共地漏了舵机要么不动要么乱转这是新手最容易忽视的一步。四路通道的用途可以各不相同PA0 接舵机PA1~PA3 各接一路 LED记得串限流电阻10mA 左右用 330Ω~1kΩ直接怼 3.3V 会烧灯珠。四路共享同一个时基但比较值各自独立所以「舵机转 三路 LED 呼吸」可以同时跑互不干扰。时钟与频率为什么你算的 36MHz 差了一倍这是 PWM 配置里最容易出错的环节。TIM2/3/4/5 挂在 APB1 总线上。默认 APB1 预分频为 2PCLK1 72MHz / 2 36MHz。但注意当 APB1 预分频不为 1 时定时器时钟会自动 ×2。TIM2 的实际时钟是 36MHz × 2 72MHz。很多人拿着 36MHz 去算频率结果实际输出差了一倍还以为是代码问题。频率公式fPWM TIM_CLK / ((PSC1) × (ARR1))占空比 CCR / (ARR1)高电平时间 CCR / (TIM_CLK/(PSC1))。三个寄存器都是 16 位PSC、ARR、CCR 最大只能填 65535。还有那个经典的 1 陷阱ARR 填 19999 而不是 20000PSC 填 71 而不是 72。公式里是 (PSC1)×(ARR1)不是 PSC×ARR漏一个 1 频率就不对。还有个容易绕晕的点改 PSC 和 ARR 会同时影响频率和占空比改 CCR 只影响占空比。比如想把频率提到 100HzARR 改成 9999PSC 不变周期就是 10ms但占空比 50% 对应的 CCR 从 10000 变成 5000 了。频率、脉宽、占空比三个量互相牵扯动一个另两个就得重新算调试时先拿计算器过一遍再改代码。初始化五步走从时钟到输出标准外设库SPL下TIM2 的 PWM 初始化分五步时钟 → GPIO → 时基 → 输出比较 → 预装载与启动。// 第一步时钟TIM2 挂 APB1GPIOA 挂 APB2RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// 第二步GPIO 复用推挽 50MHzGPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_PinGPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;GPIO_InitStructure.GPIO_ModeGPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStructure);// 第三步时基——PSC71计数频率 1MHz每计数 1µsARR1999950HzTIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM_TimeBaseStructure.TIM_Period19999;// ARRTIM_TimeBaseStructure.TIM_Prescaler71;// PSCTIM_TimeBaseStructure.TIM_ClockDivisionTIM_CKD_DIV1;TIM_TimeBaseStructure.TIM_CounterModeTIM_CounterMode_Up;TIM_TimeBaseInit(TIM2,TIM_TimeBaseStructure);// 第四步输出比较通道 1PWM 模式 1初始 CCR1500 对应 1.5msTIM_OCInitTypeDef TIM_OCInitStructure;TIM_OCInitStructure.TIM_OCModeTIM_OCMode_PWM1;TIM_OCInitStructure.TIM_OutputStateTIM_OutputState_Enable;TIM_OCInitStructure.TIM_Pulse1500;// 初始 CCRTIM_OCInitStructure.TIM_OCPolarityTIM_OCPolarity_High;TIM_OC1Init(TIM2,TIM_OCInitStructure);// OC2、OC3、OC4 用同样的结构体分别调 TIM_OCxInit// 第五步预装载 启动TIM_ARRPreloadConfig(TIM2,ENABLE);TIM_OC1PreloadConfig(TIM2,TIM_OCPreload_Enable);// OC2、OC3、OC4 同样开预装载TIM_Cmd(TIM2,ENABLE);这套流程跑通PA0~PA3 上应该就有 50Hz 的 PWM 波形了。PWM 模式 1 的行为计数器从 0 数到 ARRCNT CCR 时输出有效电平极性 High 就是高电平CNT ≥ CCR 翻转。改 CCR 就改高电平宽度即占空比。极性选 High 还是 Low决定「有效电平」是哪个——舵机信号线要的是高电平脉宽用TIM_OCPolarity_High。预装载Preload值得单独说开了之后CCR 和 ARR 的新值会先写进影子寄存器等更新事件才一起生效。不开的话运行中改 CCR 可能正好撞在输出中间波形上冒一个毛刺——舵机会「咔」抖一下。四路共享同一个时基PSC/ARRCCR 各自独立这是四路 PWM 的配置前提。PWM 波形一个周期里发生了什么把示波器探头挂到 PA0 上看 50Hz 波形每 20ms 一个周期前 1.5ms 是高电平后面 18.5ms 是低电平。这就是 CCR1500、ARR19999 的结果。背后的机制计数器像锯齿一样从 0 爬到 19999比较器拿 CNT 和 CCR 比——CNT 小于 1500 时输出高大于等于 1500 输出低爬到顶后归零再来一轮。锯齿的高度由 ARR 决定周期比较点由 CCR 决定脉宽两者解耦这正是 PWM 灵活的地方。换算一下CCR1500 时占空比 1500 / 20000 7.5%高电平 20ms × 7.5% 1.5msCCR2500 时占空比 12.5%高电平 2.5ms。上示波器之前先把这几个数算好波形一出来就能判断配置对不对。预装载的生效时机也在这里体现CCR 和 ARR 的新值在更新事件计数器归零那一刻才进入影子寄存器。运行中改了 CCR当前周期按旧值走完下一周期才用新脉宽——恰好一个周期完成切换波形无缝衔接不会出现半周期宽半周期窄的情况。舵机控制50Hz 与脉宽映射SG90 舵机需要 50Hz周期 20ms的 PWM 信号高电平 0.5ms~2.5ms 对应 0°~180°。上面的配置PSC71ARR19999恰好满足计数频率 1MHz每计数 1µs周期 20ms。脉宽映射0° → 0.5ms → CCR 50090° → 1.5ms → CCR 1500180° → 2.5ms → CCR 2500换算公式CCR 500 (角度 / 180) × 2000动态调整角度TIM_SetCompare1(TIM2,1500);// 转到 90°运行中随时调用预装载保证这次更新不会撕裂波形。舵机内部是一个微型电位器 比较电路 电机信号线输入脉宽电位器反馈当前角度对应的电压两者比较决定电机转不转。所以脉宽必须精确到微秒级、周期必须稳定在 50Hz 附近——周期一抖比较结果跟着抖舵机就晃。要强调频率必须是 50Hz 左右。配成 1kHz舵机会疯狂抖动甚至发热——舵机内部是按 20ms 周期解调脉宽的频率不对它根本没法干活。脉宽超出 0.5~2.5ms 范围舵机会打满行程并咔哒响长时间顶着会损坏齿轮和电机。精度方面1µs 的计数对应 0.09°对绝大多数应用完全够用。呼吸灯线性渐变与指数曲线呼吸灯的核心是动态修改 CCR。最简单的做法是线性渐变CCR 从 0 递增到 ARR再递减回来。// 线性呼吸CH2即 PA1 上的 LEDwhile(1){for(uint16_tcc0;cc19999;cc50){TIM_SetCompare2(TIM2,cc);delay_ms(1);}for(uint16_tcc19999;cc0;cc-50){TIM_SetCompare2(TIM2,cc);delay_ms(1);}}跑起来你会发现前半段亮得很快后半段几乎没变化。因为人眼对亮度的感知是非线性的——物理上线性变化的占空比感知上不是线性。更舒服的做法是指数曲线让 CCR 在小占空比区间多停留那里人眼敏感大占空比快速掠过// 指数呼吸需要 #include math.h从暗到亮亮部变化快for(uint16_ti1;i100;i){uint16_tcc(uint16_t)(19999.0f*(1.0f-powf(0.97f,i)));TIM_SetCompare2(TIM2,cc);delay_ms(10);}i1 时 cc≈600刚点亮i100 时 cc≈19000几乎全亮亮度感知接近线性。四路通道共享时基但 CCR 独立所以四路 LED 可以各做各的呼吸节奏互不干扰。顺带一提cc 用的是 uint16_t最大 65535正好装得下 19999要是图省事用了 uint8_t溢出之后灯会乱闪——这是不少人调试呼吸灯时遇到的怪问题。避坑清单六个最容易翻车的点定时器时钟算错APB1 是 36MHz但定时器时钟 ×2 72MHz少 ×2 频率差一倍。忘记 1ARR20000、PSC72 都是错的公式里是 (PSC1)(ARR1)。忘记预装载运行中改 CCR 出毛刺ARR 和 CCR 都要开预装载。舵机频率错50Hz 才稳定1kHz 狂抖脉宽超范围打满行程咔哒响。舵机供电不足从 3.3V 或板载 5V 取电必欠压抖动独立电源 共地。漏配输出比较只配了时基没配 OC 输出引脚没波形PWM 模式 1 是 CNT CCR 输出有效电平。写在最后到这里TIM2 四路 PWM 的核心就全讲完了。时钟链的 72MHz 陷阱、PSC/ARR 的 1 细节、预装载防撕裂、舵机脉宽映射、呼吸灯指数曲线——这几样掌握住PWM 应用最常见的坑你基本都见过了。这套能力能延伸到很多场景直流电机调速、蜂鸣器音调、LED 调光原理相通差别只在 CCR 的变化策略。你在实际项目中用过 PWM 控制什么外设踩过什么奇怪的坑评论区聊聊说不定你的经验能帮到正在调试的人。推荐阅读[STM32F103 ADC 实战三通道 DMA 循环采集与 Vrefint 校准全流程拆解]模拟量采集本系列上一篇[STM32F103 CAN 实战双机通信与 TJA1050 全流程拆解]总线通信关注本号嵌入式干货每周更新。