第4章 软件PWM模拟:IO口模拟PWM原理、定时器中断实现、软件PWM精度分析

各位同学,咱们今天聊点硬核的——软件PWM。说实话,很多新手一上来就怼硬件PWM,觉得软件模拟是“野路子”。但我得说,在实际项目中,软件PWM往往能救你于水火之中。我自己就遇到过MCU硬件PWM通道不够用的情况,那时候真是抓耳挠腮,最后靠软件模拟硬是撑了下来。

4.1 IO口模拟PWM的原理

先说说最基础的东西。PWM的本质是什么?说白了就是让一个IO口在高低电平之间来回切换,通过控制高电平持续的时间(占空比)来模拟不同的电压值。

你想想看,一个LED灯,如果给它1ms的高电平、9ms的低电平,它看起来就是10%的亮度。如果反过来,9ms高、1ms低,那就是90%的亮度。这就是人眼的视觉暂留效应在帮我们“作弊”。

IO口模拟PWM的核心就两个参数:

  • 周期(Period):一个完整的PWM波形的时间长度
  • 占空比(Duty Cycle):高电平时间占整个周期的比例

举个例子,假设周期是10ms,占空比30%,那高电平就是3ms,低电平就是7ms。代码实现起来其实很直接:

// 伪代码示例
while(1) {
    GPIO_SetHigh();      // 拉高IO
    delay_us(3000);      // 保持3ms
    GPIO_SetLow();       // 拉低IO
    delay_us(7000);      // 保持7ms
}

嗯,这里要注意——这种纯延时的方式有个致命问题:CPU被完全占死了。你啥也干不了,只能在那傻等。所以实际项目中,我们很少这么干。

警告:纯延时阻塞式PWM只适合极简单的场景。但凡你的MCU还要干点别的活(比如按键扫描、串口通信),这种方式就直接废了。

4.2 定时器中断实现——这才是正经做法

我个人习惯用定时器中断来做软件PWM。为什么?因为中断是“不打扰”的。CPU该干嘛干嘛,到了时间点,中断来了,我切换一下IO状态,然后继续干我的活。

实现思路其实很简单:

  1. 设置一个定时器,让它以固定的频率产生中断
  2. 在中断服务函数里,维护一个计数器
  3. 计数器到了某个阈值,就翻转IO口

我给大家画个图(用文字描述):

  • 定时器每100us中断一次
  • 一个PWM周期需要100次中断,也就是10ms
  • 如果占空比是30%,那前30次中断IO为高,后70次为低

代码实现大概是这样的:

// 全局变量
volatile uint16_t pwm_counter = 0;
volatile uint16_t pwm_duty = 30;  // 占空比 0-100

// 定时器中断服务函数(假设每100us触发一次)
void TIMER_IRQHandler(void) {
    pwm_counter++;
    
    if(pwm_counter <= pwm_duty) {
        GPIO_SetHigh();   // 占空比内,输出高
    } else {
        GPIO_SetLow();    // 占空比外,输出低
    }
    
    if(pwm_counter >= 100) {  // 一个周期结束
        pwm_counter = 0;
    }
}

你看,这样CPU就解放了。主循环里可以跑别的逻辑,中断里只做最轻量的事情——比较和赋值。

小技巧:我曾经在项目中用这种方案同时驱动了8路LED,每路独立调光。秘诀就是每个通道维护自己的计数器和占空比,中断里轮询一遍就行。当然,中断服务函数要尽量短,别在里面做复杂运算。

4.3 软件PWM精度分析——别被数字骗了

好,现在咱们来聊聊精度。很多同学看到“软件PWM”就觉得精度不行。其实不然,精度取决于你的定时器分辨率和中断频率。

咱们算一笔账:

  • 假设MCU主频是72MHz
  • 定时器预分频后,得到1MHz的计数频率(每1us计一次)
  • 如果PWM周期是10ms(100Hz),那一个周期就是10000个计数
  • 理论上占空比分辨率可以达到1/10000,也就是0.01%

听起来很牛对吧?但别高兴太早。实际中你会遇到几个坑:

影响因素 影响程度 说明
中断响应延迟 中等 中断嵌套、其他中断优先级更高时,会延迟PWM切换
定时器分辨率 决定了你能达到的最小步进
CPU负载 中断频率越高,CPU被占用的时间越多
代码执行时间 中断服务函数里的指令数直接影响最小周期

我曾经在一个项目中,把PWM频率设到了20kHz,结果发现LED亮度不均匀,有肉眼可见的闪烁。查了半天,原来是中断响应延迟导致的。因为那个MCU同时还在处理USB通信,USB中断优先级更高,时不时就把PWM中断给挤掉了。

核心结论:软件PWM的实际精度 = 定时器分辨率 - 中断延迟抖动。一般来说,对于LED调光,8位分辨率(256级)就足够了,人眼根本分辨不出更细的差别。别为了追求理论上的高精度,把系统搞得不稳定。

4.4 避坑指南——我踩过的那些雷

做软件PWM这些年,我踩过的坑真不少。给大家列几个典型的:

  • 中断频率过高导致系统卡死:我曾经把PWM中断频率设到100kHz,结果CPU99%的时间都在处理中断,主循环根本跑不动。后来降到1kHz,一切正常。
  • 占空比突变导致LED闪烁:如果你在中断里直接修改占空比变量,可能会在周期中间切换,导致一个不完整的脉冲。解决方案是加一个“影子寄存器”,在周期结束时刻才更新。
  • 多路PWM相位不同步:多路软件PWM如果各自独立计数,它们的相位会随机漂移。我习惯用一个全局计数器,所有通道共用,这样相位就一致了。
特别注意:软件PWM不适合驱动感性负载(比如电机、电磁阀)。因为感性负载需要精确的续流时间,软件模拟的抖动会导致电流纹波增大,甚至烧毁驱动管。LED这种纯阻性负载就没问题。

4.5 实战建议——什么时候用软件PWM

说了这么多,总结一下我的经验:

  1. 通道数多但精度要求不高:比如你要驱动16路LED做氛围灯,硬件PWM只有4路,那就用软件模拟,每路8位分辨率足够了
  2. 频率要求不高:LED调光一般100Hz-1kHz就够,这个频率范围内软件PWM完全胜任
  3. 临时调试用:我经常在调试阶段先用软件PWM验证算法,等稳定了再切换到硬件PWM
  4. 成本敏感项目:有些低端MCU根本没有硬件PWM,软件模拟是唯一选择

嗯,今天就聊到这儿。下一章咱们会讲硬件PWM的实现,到时候你会看到,硬件方案虽然好,但软件方案也有它不可替代的灵活性。记住一句话:没有最好的方案,只有最合适的方案。