利用16位定时器输出比较功能实现高精度软件PWM

利用16位定时器输出比较功能实现高精度软件PWM 1. 项目概述与核心价值在嵌入式开发领域脉宽调制PWM几乎是每个工程师都会打交道的技术。无论是驱动一个微型直流电机还是调节LED的亮度甚至是在开关电源中控制输出电压PWM都扮演着核心角色。它的魅力在于仅用一根数字信号线就能通过调节“开”和“关”的时间比例来模拟出连续变化的模拟量这种数字到模拟的“魔法”既高效又可靠。然而并非所有的微控制器都内置了专用的PWM硬件模块。尤其是在一些成本敏感、引脚资源有限的老型号或入门级MCU中硬件PWM可能是一种奢侈。这时一个经典的解决方案就浮出水面利用几乎所有MCU都标配的16位自由运行定时器及其输出比较功能通过软件“模拟”出一个高精度的PWM通道。这不仅仅是技术上的“曲线救国”更是一种对MCU底层资源深刻理解的体现。它能让你在不增加硬件成本的前提下为项目增添关键的模拟控制能力尤其是在电机控制、简易DAC数模转换或蜂鸣器发声等场景下这种方法的价值尤为突出。今天我们就以经典的Motorola现NXPHC05系列微控制器为例深入拆解如何用软件“驯服”定时器让它乖乖地输出我们想要的PWM波形。我会结合一份经典的官方应用笔记AN1734中的思路但不止于复述我会加入大量在实际调试中积累的细节、避坑指南和参数计算逻辑让你不仅能看懂代码更能理解每一个寄存器操作背后的“为什么”最终能在你自己的项目中游刃有余地应用这项技术。2. 核心原理当定时器遇上输出比较要理解软件PWM必须先吃透两个核心硬件模块自由运行定时器和输出比较器。你可以把它们想象成一个不知疲倦的跑步者和一个拿着秒表的裁判。2.1 自由运行定时器永不停止的时钟16位自由运行定时器本质上是一个从0开始向上计数计到最大值0xFFFF后溢出归零然后继续计数的循环计数器。它的计数频率由系统主频经过一个预分频器得到。例如在HC05中如果系统主频f_op是2MHz预分频器通常固定为4分频那么定时器的计数频率f_tim就是500kHz对应的计数周期t_tim就是2微秒。这意味着这个“跑步者”每2微秒就跑出一步计数值加1。这个t_tim是我们所有时间计算的基础单位至关重要。2.2 输出比较器精准的“发令枪”输出比较器则像是一个设定好目标的裁判。我们软件可以预先向一个叫做“输出比较寄存器”通常分为高字节OCH和低字节OCL的地方写入一个目标值。定时器这个“跑步者”会不停地跑并将自己的当前计数值与裁判手中的目标值进行比较。当两者完全匹配时就发生了一次“比较匹配”事件。这个事件可以触发两件事硬件自动动作如果MCU有专用的TCMPTimer Compare输出引脚并且配置正确硬件会自动翻转该引脚的电平从高变低或从低变高。这是最高效、最精准的方式。软件中断无论是否有专用引脚比较匹配事件都可以触发一个中断。在中断服务程序ISR里我们可以手动去操作任意一个GPIO引脚的电平。软件PWM的核心思想就是利用这个“比较匹配”事件作为时间基准点。我们通过计算分别在需要输出电平翻转的时刻脉冲开启和关闭的时刻向输出比较寄存器写入新的目标值。这样定时器就会在精确的时刻触发动作从而“编织”出我们想要的脉冲波形。2.3 PWM参数与定时器周期的换算假设我们需要一个频率为f_pwm、占空比为DC的PWM信号。PWM周期T 1 / f_pwm。例如2kHz的PWM周期T 500us。高电平时间t_on DC * T。低电平时间t_off T - t_on。接下来需要把时间转换成定时器的“步数”计数次数高电平计数值D_on t_on / t_tim低电平计数值D_off t_off / t_tim这里的D_on和D_off必须是整数。它们决定了我们需要让定时器跑多少“步”后才改变输出电平。例如t_tim2us要产生250us的高电平就需要D_on 250 / 2 125。这个125就是我们要写入输出比较寄存器的“增量”。注意这里有一个关键细节。我们通常不是直接向输出比较寄存器写入一个绝对时间点而是写入“当前定时器值 延迟计数值”。因为定时器在自由运行直接写绝对时间点计算复杂且容易出错。写入一个增量让硬件自动去计算下一个匹配点是更可靠的做法。3. 实现方案设计与关键考量基于输出比较实现PWM主要有两种架构中断驱动型和自动翻转型。它们的选择取决于你的MCU是否具备带硬件自动翻转功能的TCMP引脚。3.1 方案一中断驱动型通用性强这是最通用的方法适用于任何带有定时器和输出比较中断功能的MCU即使没有专用的TCMP引脚也行。初始化配置定时器使能输出比较中断设置一个初始输出电平比如高电平并计算第一个延时D_off如果先输出高电平则第一个延时是低电平时间这里需要厘清我们设定的是当前电平的持续时间。将当前定时器值 D_on写入输出比较寄存器。中断服务程序ISR进入中断后首先翻转输出引脚的电平通过GPIO操作。判断当前要设置的电平。如果是刚翻到高电平则下一个延时是D_on如果是刚翻到低电平则下一个延时是D_off。计算新的比较值新的OC值 旧的OC值 下一个延时值。注意处理16位加法可能产生的向高字节进位。将新的比较值写入输出比较寄存器。清除中断标志退出。优点极其灵活可以在任何GPIO引脚上产生PWM。缺点中断延迟。从比较匹配发生到CPU响应中断、执行完翻转指令这中间有数十个甚至上百个时钟周期的延迟。这个延迟会直接导致PWM波形产生“抖动”和误差在高频或高精度场合下是致命的。此外CPU需要频繁进入中断占用资源。3.2 方案二硬件自动翻转型精度高资源占用低这是本文重点推荐的方法前提是MCU的TCMP引脚支持“输出比较时自动翻转电平”功能通常通过配置TCR寄存器中的OLVL位实现。初始化配置TCMP引脚为输出比较模式并设置其初始输出电平例如置OLVL1表示首次匹配时输出高电平。计算D_on和D_off。将当前定时器值 D_on写入输出比较寄存器并使能输出比较中断。中断服务程序ISR此时硬件已经在比较匹配的精确时刻自动翻转了TCMP引脚的电平我们无需在中断里操作引脚。中断程序只需要做一件事为下一次翻转准备新的时间点。我们需要判断如果硬件刚刚输出了高电平意味着当前是t_on阶段结束开始t_off阶段那么下一次匹配应该发生在t_off时间之后所以新的比较值 旧的比较值 D_off。反之如果硬件刚刚输出了低电平则新的比较值 旧的比较值 D_on。同时需要预先设置好下一次匹配时硬件要执行的动作即设置OLVL位。如果下次要输出高电平就置位OLVL要输出低电平就清除OLVL。计算并写入新的输出比较值清除中断标志退出。优点精度极高电平翻转由硬件在匹配发生的同一时钟周期内完成没有软件延迟波形非常干净。CPU占用率低中断服务程序只做计算和设置非常短小精悍。可靠性高避免了因中断响应不及时导致的脉冲宽度错误。缺点必须使用指定的TCMP引脚灵活性稍差。实操心得在资源允许的情况下永远优先选择硬件自动翻转方案。它的精度优势是软件无法比拟的。在电机控制等对波形稳定性要求高的场景这点尤其重要。你可以将宝贵的CPU时间节省下来处理其他任务如通讯、传感器采样或更复杂的控制算法。4. 实战演练基于HC05的电机调速器让我们把手弄脏通过一个完整的电机控制项目来实践硬件自动翻转方案。项目目标用一颗MC68HC705P9通过一个电位器调节PWM占空比从而控制一个小风扇电机的转速。4.1 系统框架与硬件连接核心器件MCU: MC68HC705P9 (系统频率假设为2MHz定时器频率500kHzt_tim2us)外设电位器接至A/D通道AN0电机驱动电路TCMP引脚通过一个三极管或MOSFET驱动电机软件设计思路主循环不断轮询A/D转换完成标志读取电位器电压对应的数字值0-255。这个值直接作为目标D_on高电平时间计数。占空比更新比较新的D_on与当前值。如果不同则调用子程序调整D_on和D_off同时确保它们不超出可行范围。定时器中断采用硬件自动翻转模式根据当前的D_on和D_off计算并预装下一次比较匹配的时间点。4.2 关键参数计算与边界处理这是项目的核心也是容易出错的地方。我们设定PWM频率为2kHz周期T500us。计算理论值周期计数值D_total T / t_tim 500us / 2us 250(0xFA)。若目标占空比为50%则D_on D_off 125(0x7D)。确定极限值避坑关键最小值限制 (D_min)这不是0它受限于中断服务程序的执行时间。在中断里我们需要读取、计算、写入寄存器。假设最坏情况下我们的中断服务程序需要52个CPU周期。在2MHz主频下这就是26us。换算成定时器计数D_min 26us / t_tim 26 / 2 13(0x0D)。这意味着无论是D_on还是D_off都不能小于13。否则下一次比较匹配会在中断程序还没执行完时就发生导致输出混乱。最大值限制 (D_max)由于我们使用8位变量存储D_on为了匹配8位ADC值最大值是255 (0xFF)。但为了满足周期固定D_off D_total - D_on。当D_on取最小值13时D_off最大为237当D_on取最大值237时D_off最小为13。因此D_on的有效范围是[13, 237]。在代码中我们需要在DutyUp和DutyDown子程序里严格进行边界检查。动态调整算法当电位器读数新D_on增大时我们希望增加占空比。这意味着D_on要增加Δ同时D_off要减少Δ。但必须检查新的D_off 旧的D_off - Δ是否仍大于等于D_min13如果不是则说明已经调到极限D_on接近D_max。当电位器读数减小时D_on减少ΔD_off增加Δ。需要检查新的D_off 旧的D_off Δ是否小于等于255同时新的D_on是否仍大于等于D_min4.3 代码实现精讲以下是基于AN1734代码清单的关键部分解读和增强说明。我们聚焦于定时器中断服务程序和占空比更新逻辑这两个最核心的模块。;****************************************************************** ;* 定时器中断服务程序 (TimerInt) ;* 模式硬件自动翻转TCMP引脚电平 ;* 核心思想中断不负责翻转电平只负责为“下一次”翻转设定时间和动作。 ;****************************************************************** TimerInt: BRSET OLVL, TCR, GoLow ; 检查OLVL位。如果已置位说明“当前”硬件设置是“下次匹配时输出高电平”。 ; 但这意味着什么这意味着“刚刚发生”的匹配事件执行的是“上一次”中断设置的动作。 ; 所以如果OLVL1说明“刚刚”硬件输出了低电平因为OLVL是预置的“下次”动作。 ; 因此我们刚结束了一个t_on周期开始了一个t_off周期。 GoHigh: ; 分支目标我们判断出“刚刚”输出了低电平现在要设置“下一次”输出高电平。 BSET OLVL, TCR ; 预置下一次匹配时TCMP引脚输出高电平。 LDA OCL ; 读取当前输出比较寄存器的低字节即刚刚发生匹配的那个值。 ADD OffDelay ; 加上低电平时间的计数值。因为接下来是t_off周期。 BCC NoCarry1 ; 检查加法是否有进位C标志位。 INC OCH ; 如果有进位需要增加高字节OCH。 NoCarry1: LDX TSR ; 读TSR寄存器这是一个清除OCF中断标志的巧妙方法。 STA OCL ; 将计算好的新低字节值存入OCL为下一次比较做准备。 RTI ; 中断返回。 GoLow: ; 分支目标我们判断出“刚刚”输出了高电平现在要设置“下一次”输出低电平。 BCLR OLVL, TCR ; 预置下一次匹配时TCMP引脚输出低电平。 LDA OCL ADD OnDelay ; 加上高电平时间的计数值。因为接下来是t_on周期。 BCC NoCarry2 INC OCH NoCarry2: LDX TSR ; 清除OCF标志。 STA OCL RTI关键点剖析中断的时机中断发生在本次匹配动作完成之后。中断里的任务是为下一次匹配做准备。OLVL位的意义它控制的是“下一次”匹配发生时引脚要执行的动作。因此通过检查当前的OLVL值可以反推出“刚刚”完成的是什么动作。这是理解整个逻辑的钥匙。清除中断标志LDX TSR然后丢弃X的值是HC05架构下清除定时器状态寄存器TSR中输出比较标志OCF的标准且高效的方法。直接写TSR可能无效。进位处理由于OnDelay和OffDelay是8位而OC是16位寄存器加法可能产生进位必须手动处理高字节OCH。;****************************************************************** ;* 占空比增加子程序 (DutyUp) ;* 输入ADCData (新的目标D_on值), OnDelay, OffDelay (当前值) ;* 输出更新后的OnDelay, OffDelay ;****************************************************************** DutyUp: STA Delta ; 保存差值 (新D_on - 旧D_on)假设差值已在A中。 LDA OffDelay CMP Delta ; 比较当前OffDelay和需要减少的量(Delta)。 BLO Dlimit2 ; 如果 OffDelay Delta说明减少后OffDelay会小于0触发下限保护。 SUB Delta ; OffDelay OffDelay - Delta CMP #DELAYMIN ; 检查新的OffDelay是否小于最小允许值(13)。 BLO Dlimit2 ; 如果小于也触发下限保护。 STA OffDelay ; 保存新的OffDelay。 LDA ADCData STA OnDelay ; 保存新的OnDelay (即ADC读数)。 BRA Ddone2 Dlimit2: ; 达到极限的处理将占空比推到最大或最小。 LDA #DELAYMAX ; 此时OnDelay应设为最大值。 STA OnDelay LDA #DELAYMIN ; OffDelay应设为最小值。 STA OffDelay Ddone2: RTS避坑指南边界检查的顺序先检查减法操作是否会导致下溢OffDelay Delta再检查结果是否低于最小值。这个顺序不能错。极限值的设定当达到极限时不能简单地保持原值或取边界值。代码中将其设置为(D_onDELAYMAX, D_offDELAYMIN)这实际上是将占空比推至理论最大值(250-13)/250 ≈ 94.8%确保了控制的单调性和响应性。这是一个很实用的工程处理。变量范围OnDelay和OffDelay是8位无符号数所有比较和运算都要在这个前提下理解。DELAYMIN是13DELAYMAX是250 - 13 2370xED。5. 性能极限与优化策略软件PWM的性能是有天花板的主要受限于两个因素定时器频率和中断延迟。5.1 理论极限分析从AN1734中的表格可以总结出规律最高PWM频率由中断服务时间 最小D_on 最小D_off决定。近似等于1 / (2 * 中断服务时间)。例如中断需52周期主频2MHz则中断时间26us理论最高PWM频率约1/(2*26us) ≈ 19.2kHz。但此时占空比调节范围极窄。占空比调节范围在给定PWM频率下占空比可调范围受限于最小延迟D_min。可用范围是[D_min / D_total, (D_total - D_min) / D_total]。频率越高D_total越小这个范围就越窄。结论软件PWM适合中低频通常10kHz以下、对精度要求高、对占空比调节范围要求不是0%-100%极致的应用。例如电机调速几十到几百Hz、LED调光几百Hz、简易DAC等。5.2 优化技巧精简中断服务程序ISR这是提升性能最有效的方法。使用汇编语言优化指令顺序避免在中断内进行复杂计算。可以将D_on和D_off的计算放在主循环中ISR只做简单的加载和加法。使用更快的时钟提高MCU的主频可以直接降低t_tim从而在相同的PWM频率下获得更精细的占空比调节分辨率。使用16位延迟值如果CPU能力允许使用16位变量存储D_on和D_off可以突破8位0-255的限制获得更宽的调节范围和更高的分辨率尤其是在低频时。利用多个输出比较通道一些高级定时器有多个输出比较寄存器。可以用一个寄存器控制上升沿另一个控制下降沿实现更灵活、更精确的单脉冲控制甚至可以产生非对称PWM。考虑使用定时器溢出中断如果对精度要求不高可以只在定时器溢出时即每个PWM周期开始时设置一个新的比较值用于控制脉冲宽度。这样中断频率降低到PWM频率但占空比分辨率会下降。6. 常见问题与调试实录在实际焊接和调试中你肯定会遇到波形不对劲的时候。下面是我踩过的一些坑和排查方法。问题一输出的PWM频率不对远高于或低于预期。检查时钟源确认MCU的系统时钟配置是否正确。外部晶振是否起振内部RC是否校准这是所有定时问题的根源。核对预分频器仔细查阅数据手册确认定时器的时钟输入路径。是系统时钟直接驱动还是经过了分频分频系数设置对了吗验证计算重新计算t_tim、D_total。用示波器测量一个已知时间的延时比如让一个引脚定时翻转来反推实际的定时器频率。问题二占空比变化不线性或者在某个区间突变。检查边界条件重点调试DutyUp和DutyDown函数中的边界判断逻辑。在临界值如D_on接近13或237附近单步调试观察变量变化。确认变量类型和运算确保D_on、D_off、Delta等变量在计算时没有发生意外的符号扩展或溢出。在汇编中要特别注意进位标志C和零标志Z的状态。ADC读数处理如果占空比由ADC控制检查ADC读数是否稳定。可以加入简单的软件滤波如取多次平均。问题三波形上有毛刺或偶尔的宽脉冲/窄脉冲。中断冲突确认没有更高优先级的中断长时间关闭总中断导致输出比较中断被延迟响应。调整中断优先级确保定时器中断的响应时间尽可能稳定。共享变量保护如果主循环和中断服务程序都访问OnDelay和OffDelay在8位或16位MCU上读写这些变量可能不是原子操作。在主循环修改它们时应暂时关闭定时器中断修改完成后再打开。电源噪声在电机驱动电路中电机启停会产生很大的电源噪声。确保MCU的电源有良好的退耦如靠近MCU的VCC和GND之间加104电容驱动级与MCU之间用光耦或电平转换器隔离。问题四电机启动或低速时抖动严重。死区时间在D_on或D_off非常小接近D_min时由于中断延迟的微小波动可能导致脉冲宽度不稳定。可以软件设置一个比理论D_min稍大的“安全值”避免工作在这个临界区域。电机特性直流电机在极低占空比下可能无法克服静摩擦力表现为抖动。可以设置一个最小启动占空比如5%。调试时一把好的示波器是必不可少的。测量TCMP引脚波形关注周期、高电平时间是否与计算值相符。特别是要开启示波器的单次触发或滚动模式捕捉那些偶尔出现的异常脉冲这往往是解决棘手问题的关键。最后我想说的是利用定时器输出比较实现PWM是一项融合了对硬件定时器、中断系统和外围控制深刻理解的综合技能。它可能没有直接调用硬件PWM库函数来得方便但它带给你的是对MCU运行机制最直接的掌控感。当你看到通过自己编写的代码那根引脚上输出稳定而精确的方波并驱动电机平稳旋转时这种成就感是无可替代的。希望这篇长文能成为你探索嵌入式底层世界的一块扎实的垫脚石。