做电机驱动和电源控制的朋友,应该都撞见过这样一个场景:程序里明明已经把目标占空比写进了定时器的比较寄存器,示波器上 PWM 输出的变化却总要迟那么一拍才出现;偏偏在最需要“立刻刹车”的瞬间,输出还在按照旧值稳稳地执行。我第一次被这个问题坑到,是在调一台直流无刷电机的高速换相时,电流波形上的毛刺怎么都消不掉,参数试了一圈都不对,最后才意识到不是电流环的问题,而是传统 PWM 更新模式在背后拖后腿。
这里说的“传统 PWM 更新模式的固有局限”,简单概括就是:绝大多数单片机内置的 PWM 外设,出于波形完整性和硬件安全考虑,并不会让你写入寄存器的值立刻生效,而是把新值“扣”到某个特定的更新事件(通常是计数周期边界)才真正加载。这种机制在低速、低动态的场合没有任何问题,但在电机控制、数字电源、精密调光这类对响应时间敏感的应用里,它带来的延迟、阶跃跳变和采样同步问题,会直接变成系统性能的上限。
这篇文章会把这类局限从底层原理到实际表现逐个拆开,再给出一套不换主控也能用的优化思路。适合正在调试 PWM 驱动、电机闭环或者数字电源的朋友参考。
1. 更新事件与影子寄存器:传统 PWM 模式为什么非要“等一下”
1.1 影子寄存器机制:用“延迟”换“波形完整性”
其实任何一个合格的 PWM 外设,在比较寄存器前面都会再加一层影子寄存器(Shadow Register),也就是常说的预装载寄存器。你软件里写入 TIMx_CCRx,写的其实是影子寄存器;计数器在运行过程中真正拿来和计数值做比较的,是另一套活动寄存器。把影子寄存器的值搬运到活动寄存器的那一下,就是更新事件(Update Event)。
为什么要多这一层中转?设想一下如果不这么做会发生什么:PWM 波形正处在高电平中间,你突然把比较值从 100 改到 80,计数器在下一个瞬间就匹配了,当前周期会突然提前翻转,输出端出现一个极窄的高电平脉冲。对 LED 灯来说也许只是闪一下,对电机而言就是一次额外的电流冲击,对数字电源来说甚至可能直接触发过流保护。所以硬件工程师宁可在更新时刻牺牲一拍延迟,也不愿意让输出波形在周期中间被“打断”。
这个设计本质上是在拿“响应延迟”换取“波形完整性”,本身是合理的选择。问题在于,很多工程师在写应用层代码时根本没有意识到这一层存在——库函数封装得太好,你调一个pwm_set_duty(),里面做了什么都看不见,于是“写寄存器立即生效”这个错误假设就埋下了隐患。等你在控制环里发现相位滞后、系统不稳定时,往往已经排查了很久别的方向。
1.2 更新事件的位置选择:周期边界、立即更新、事件触发
从更新的触发位置来分,常见的有三类:
| 更新方式 | 加载时机 | 典型场景 | 代价 |
|---|---|---|---|
| 传统周期更新 | 计数器溢出/下溢产生更新事件时加载 | 常规电机控制、调光、加热 | 至少一个计数周期的延迟 |
| 立即更新 | 软件写入后直接修改活动寄存器 | 初始化、停机、一次性配置 | 周期中段可能产生畸形脉冲 |
| 事件触发更新 | 外部输入、另一定时器或比较事件触发加载 | 多外设同步、固定相位控制 | 需要额外硬件资源和严格时序设计 |
绝大多数 PWM 外设默认走的是第一种。STM32 里 ARPE 位控制自动重载寄存器是否预装载,OCxPE 位控制比较寄存器是否预装载;AURIX TC3xx 的 CCU6 模块里,T13 周期值、CC60/CC61/CC62 比较通道的装载逻辑也类似。不同芯片对“更新事件在哪个时刻产生”的定义还有差异,这就是很多移植代码跑起来行为不一致的根本原因。
1.3 中心对齐模式下更新点的额外复杂性
边沿对齐模式(向上计数)里,更新事件比较简单,基本就是计数器溢出那一下。但中心对齐模式(向上-向下计数)下,计数器从 0 递增到 ARR 再递减回 0,一个完整周期存在两个特殊时刻:计数值为 0 的谷值(下溢)和计数值为 ARR 的峰值(上溢)。
不同外设在中心对齐模式下产生更新事件的策略不完全一样,有的在上溢和下溢都触发,有的允许软件选择其一。这意味着你写进去的新占空比,可能在本周期的上半段生效,也可能要等下半段才生效。很多 STM32 工程师在中心对齐模式下踩的坑就在这里——发现 PWM 输出和预期值差了一个相位,怎么查都查不出问题,其实是更新的时间点和自己以为的不一样。后面第 3 章我会专门展开这个和 ADC 采样联动的问题。
2. 一拍延迟与阶跃跳变:高动态场景下两个绕不过去的代价
2.1 量化“一拍延迟”:从写寄存器到边沿变化到底有多久
先算一笔账。以 20kHz 的 PWM 为例,周期是 50µs。控制指令随机落在某个时刻,更新要到下一个周期边界才发生,所以延迟在 0~50µs 之间分布,平均 25µs,最坏情况接近 50µs。
如果电流环或者速度环的控制周期恰好也是 50µs,那整条链路就是:采样当前电流 → 运算出目标电压 → 写入比较寄存器 → 下一周期才真正生效。这等于在闭环里硬生生多了一个载波周期的纯滞后。自动控制里,纯滞后是最伤稳定性的一类因素,相位裕度会直接掉一截,系统容易振荡或者动态响应变差。
低频场景更夸张。舵机接收机的 PWM 信号常用 50Hz,周期 20ms,你在任意时刻写入目标角度,输出实际要到下一个周期才开始变化,平均 10ms、最坏 20ms 的延迟,人手都能直接试出来。这也是为什么舵机控制里“写入占空比”和“舵机开始动”之间总有一段肉眼可见的等待。
不同类型 PWM 对应的延迟量级可以参考这张表:
| PWM 频率 | 周期 | 平均更新延迟 | 最坏更新延迟 |
|---|---|---|---|
| 20kHz(常见电机控制) | 50µs | 25µs | 50µs |
| 10kHz(部分工业驱动) | 100µs | 50µs | 100µs |
| 1kHz(部分运动控制) | 1ms | 0.5ms | 1ms |
| 50Hz(舵机/伺服) | 20ms | 10ms | 20ms |
2.2 阶跃跳变:整周期替换带来的电流和机械冲击
除了延迟,还有一个更隐蔽的问题:传统更新模式对目标值的处理是“整周期替换”,占空比从 10% 直接切到 90%,中间没有任何过渡。这在软件逻辑上很简单,但在物理世界是一次标准的阶跃输入。电机上表现为电流尖峰和转矩突变,加热器上表现为功率骤变,LED 高速调光时会看到亮度跳变。
有一次我在调一个小型加热器,目标是快速升温到设定值再保温。温度环输出变化其实不大,但 PWM 满占空比和低占空比之间切换时,加热丝的电流冲击把同一个电源轨上的传感器信号干扰得乱七八糟。后来在软件里加了一个斜坡限幅,每次更新事件只允许占空比变化几个百分点,冲击才压下去。
斜坡限幅看着简单,但要注意它本身也依赖更新事件——如果 PWM 外设不支持周期内多次更新,斜坡就只能一步一个周期地走,系统响应速度又变慢了。所以这一步是用动态性能换冲击抑制,项目里要权衡好。
2.3 被延迟放大的“电机飞车”风险
“PWM 电机飞车”这类事故,严格讲通常由驱动失效、控制逻辑错误、传感器断线等根本性故障引起。但传统更新模式有一个逃不开的缺点:故障状态下软件试图把占空比紧急降到 0,这个“紧急”指令同样要被扣到下一个更新事件才生效。
20kHz 下最坏 50µs 的延迟,在很多人看来不算长,但在低速重载、大惯量的系统里,这个控制窗口的延长可能让电机在危险转速上多转不少角度。更麻烦的情况是,如果主循环响应慢,或者中断被长时间屏蔽,指令写进影子寄存器之后迟迟等不到加载,危险状态持续的时间就不是几微秒而是几十毫秒了。
所以设计安全逻辑的时候,不能假设“写入比较寄存器”等于“输出立刻变化”。PWM 输出真正发生变化是在下一个更新事件,这个认知必须刻在脑子里。真正常规的安全设计,会单独拉一路 GPIO 作为功率级使能,或者用独立硬件看门狗、故障比较器去强制关断,而不是指望依靠 PWM 更新本身来救命。
3. 中心对齐模式下更新点与 ADC 采样时刻的三角博弈
3.1 为什么电机控制偏爱中心对齐模式
边沿对齐模式实现简单,每个周期开始时开关动作比较集中,电流纹波大,谐波噪声也集中在固定频点。中心对齐模式下,开关切换分散在周期的不同位置,等效开关频率翻倍,电流纹波更小,噪声频谱更扁平,方便 EMI 滤波设计。
这就解释了为什么做电机控制、数字电源的工程师都偏好在中心对齐模式下工作。但中心对齐模式也把前面说的“更新事件位置”问题放大了——因为一个周期里有两个候选时刻:上溢(峰值)和下溢(谷值)。
3.2 更新点在峰值和谷值的行为差异:一个容易被忽略的坑
以很多 MCU 的典型实现为例,如果你在中心对齐模式下把影子寄存器写入了新占空比,更新事件发生在谷值,那么新值会在之后的整个上半周期生效;如果更新事件发生在峰值,新值则会从下半周期开始生效;如果上下溢都会产生更新事件,那一整个周期里可能发生两次加载,输出波形会有一次不预期的二次调整。
我见过最典型的翻车案例:某工程师从网上拷了一段霍尔传感器驱动的 PWM 配置,跑起来电机转是能转,就是噪音大、电流波形怪。查了两天,最后发现芯片手册里写得很清楚:该型号在这种中心对齐模式下,更新事件只在上溢产生,而他所有的占空比写入逻辑是按“下溢更新”来设计的,等于他的控制输出永远晚了半个周期。他以为是换相逻辑写错了,其实是更新点没对齐。
这里给一个实操提示:移植或者参考别人的 PWM 代码时,拿到新芯片的第一件事不是跑 demo,而是翻参考手册,确认三件事——更新事件在哪个计数时刻产生;比较寄存器是否默认带预装载;中心对齐模式下上溢和下溢分别对更新、比较中断和 ADC 触发各有什么影响。
3.3 ADC 采样和更新时刻的相互挤压
中心对齐模式下,最佳 ADC 采样点一般选在计数值为 ARR 的峰值附近,因为所有开关动作在比较匹配点附近完成,峰值和谷值时刻刚好是开关动作的空窗期,开关噪声最小,采到的电流最干净。
问题来了:如果更新事件也在峰值附近,那么“新值刚生效,采样立刻开始”,采到的是包含新占空比初始响应的电流,还不是稳态值;如果更新事件在谷值而采样在峰值,那这一周期采到的电流是上一周期设置的占空比产生的,控制环路就固定存在一个周期的相位滞后。
这个矛盾没有绝对正确的解,全看控制策略怎么设计。常见的做法有两种:一种是在 PWM 周期开始时刻采样并计算,在周期起始点更新,让新占空比在一个干净的时间基准上生效;另一种是采样放在峰值,更新放在谷值,两个动作错开半个周期,用相位固定的延迟换取采样窗口的干净。哪种更好取决于你控制的是电流还是位置、环路带宽是多少,但有一点是共通的:采样点和更新点的相位关系必须固定,不能在运行中漂移。
3.4 STM32 高级定时器实操:把采样时刻精确卡在中心点
STM32 的高级定时器(TIM1/TIM8)在中心对齐模式下的典型配置思路是这样的:
- 中心对齐模式选择 CMS=01 或 10;
- 开启自动重载预装载(ARPE=1),比较寄存器预装载(OCxPE=1);
- 用重复计数器 RCR 控制更新事件的发生频率,不让每次计数满都触发更新;
- 配置 TRGO 为更新事件,触发 ADC 注入组采样;
- ADC 采样完成后通过中断或 DMA 启动控制算法。
下面是一段简化示意,具体位域编码请以对应型号参考手册为准:
// 以STM32高级定时器TIM1为例,中心对齐、重复计数、TRGO触发ADC TIM1->CR1 |= TIM_CR1_CMS_0 | TIM_CR1_ARPE; // 中心对齐模式1,自动重载预装载 TIM1->RCR = 1; // 每2次计数翻转才产生一次更新事件 TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // 通道1比较寄存器预装载 TIM1->CR2 |= TIM_CR2_MMS_1; // TRGO选择更新事件,按手册确认编码 TIM1->EGR |= TIM_EGR_UG; // 先手动产生一次更新,初始化影子寄存器配置完定时器,再把 ADC 的触发源设为定时器 TRGO,注入组转换序列里放电流采样对应的通道。这样采样时刻就严格卡在 PWM 周期的指定相位上,不会随软件执行时间漂移。
这里要特别注意:如果 ADC 由更新事件触发,而更新又发生在计数器峰值附近,你采到的电流其实是“新占空比刚刚生效瞬间”的电流。它和理论上“占空比稳定之后的平均电流”之间有一个差值,控制算法里如果没考虑这一拍,环路可能会表现出奇怪的振荡。
4. 死区、互补输出与更新粒度的硬约束
4.1 动态死区为何难以实现
全桥和半桥驱动里,死区时间是防止上下管直通的必要设置。传统更新模式下,死区时间一般作为定时器外设的全局静态寄存器,运行中修改需要等待更新事件加载。问题在于,死区本身并不是一个“逐周期动态调节”的常规参数,硬件设计也不鼓励你在运行中频繁改它。
有动态死区需求的项目,比如轻载时希望减小死区来降低输出波形失真,重载时希望加大死区来提高可靠性,传统 PWM 更新模式直接帮你把这扇门关上了。替代做法只能是在换相逻辑内部用不同的比较值去做等效补偿,或者干脆用支持动态死区的专用电机驱动芯片、FPGA 方案。这个决策要在项目初期做,等到 PCB 打样回来再想改就晚了。
4.2 互补输出 CH1/CH1N 与更新粒度的分辨率墙
STM32 的高级定时器通道 CH1 和 CH1N 是一对互补输出,死区时间由定时器自动插入。使用 CH1N 时要注意几个开关:主输出 MOE 要打开,刹车输入 BKIN 不能处于锁定状态,还要把 CCxNE 位置位。很多人第一次用 CH1N 发现没有波形,八成是 MOE 没开或者刹车被触发了。
但真正卡住系统性能的,是更新粒度。假设定时器时钟 100MHz,PWM 频率做到 1MHz,ARR 就只有 100,边沿对齐模式下占空比分辨率是 1/100,也就是 1%;中心对齐模式等效超过 1MHz 也才 0.5%。如果死区需要 50ns,那就是 5 个计数周期,死区时间的调节步进已经不小了。
这还不是最难受的。传统更新模式下,任何比较值的跳变都会在输出端出现至少一个计数步的抖动,频率越高,这个抖动占周期比例越大。MHz 级 PWM 配合高分辨率现场总线,比如 EtherCAT 的直流母线同步,更新粒度不够会让整个系统的控制精度打折。
4.3 TC3xx CCU6 死区设置里的更新点选择
英飞凌 TC3xx 的 CCU6 模块里,T13 负责定时周期,CC60/CC61/CC62 是三个比较通道,死区由 DTM 模块产生,插入在 CCx 输出和最终输出之间。配置死区时有一个和前面类似的坑:DTM 相关配置寄存器的更新与 T13 周期同步,不能想什么时候写就什么时候写。
我在实际项目中的做法是:初始化阶段把所有死区参数一次性写入,并且等 T13 走到更新点之后再确认一次寄存器值;运行中如果要调整死区,先暂停 PWM 输出、更新配置、再恢复。这个流程不是最优雅的,但安全性最高。如果非要动态调节,也只能在小范围、经过充分测试的条件下做,而且修改点要严格放在更新事件发生的同一时刻,否则可能出现一个周期里上下管死区时间长短不一,极端情况下有直通风险。
5. 软件模拟 PWM 时,更新延迟问题被进一步放大
5.1 8051 模拟 16 位 PWM:用中断换分辨率,也在用分辨率换确定性
很多老工程师用 8051 这类没有硬件 PWM 的 MCU 做过模拟 PWM,典型方案是用定时器中断 + GPIO 翻转,配合一个 16 位累加器:每次中断累加一次,溢出就翻转引脚,占空比由比较值控制。想要 16 位分辨率,累加频率就不可能高,因为中断里能执行的指令有限,频率做得越高,周期抖动越明显。
这种纯软件 PWM 的“更新模式”完全在软件层:你更新一个全局变量,下一次中断才生效,而中断本身存在不确定性——其他中断抢占、临界区保护、指令执行时间抖动,都会让每次中断到达的时间不完全一致。于是整个 PWM 周期不均匀,在示波器上能看到边沿在抖动。16 位分辨率在这种实现下只是一个“理论精度”,实际的边沿抖动早就把精度吃掉了。
如果非要用这种方式驱动 WS2811 这类对时序敏感的芯片,建议直接用硬件 SPI 或者专门的单线协议外设,别拿模拟 PWM 去硬扛,时序抖动会让你怀疑人生。
5.2 RT-Thread 软件定时器 PWM:调度延迟让更新时刻变成“随机事件”
RT-Thread 环境下用软件定时器或者线程去做 GPIO 翻转,本质上也是一种软件 PWM。RTOS 调度有最小时间单位——tick,常见配置是 1ms 或者 10ms。要做 1kHz 的 PWM 输出,一个周期 1ms,用软件定时器做翻转已经逼近极限,每个 tick 的误差都会直接叠加到脉宽上。
实测数据是我在调一个小型舵机时发现的:用 RT-Thread 的软件定时器生成 50Hz、双极性脉宽控制信号,舵机在静止位置附近持续微抖,用逻辑分析仪抓数据,脉宽在 1.47ms 到 1.53ms 之间随机漂移。对舵机来说,这已经是一个不可接受的控制误差了。
所以要说清楚:RTOS 下软件 PWM 只适合呼吸灯、蜂鸣器、低速加热、LED 氛围灯这类对时序不敏感的场景。一旦涉及舵机闭环、电机电流环、或者任何对相位有要求的同步场景,必须回到硬件 PWM 外设,把更新动作交给定时器,而不是交给调度器。
5.3 PWM 故障保护:不能把性命押在更新路径上
前面反复强调“写寄存器到更新事件之间有延迟”,在正常工作时只是控制性能问题,在异常时就会变成安全问题。如果 CPU 因为某种原因卡死或者看门狗复位,传统 PWM 外设会保持最后一个有效占空比输出。对电机驱动来说,这可能意味着保持一个危险的高占空比,飞车事故往往就是这么来的。
所以一个合格的设计里,一定会有独立于更新路径的故障保护通道:STM32 的刹车输入 BKIN、故障比较器直接关断触发信号、TC3xx CCU6 的 TRP 机制、或者外部硬件看门狗直接拉低功率级的 EN/INH 引脚。这些保护不依赖 PWM 更新事件,走的是另一条独立的硬件链路。这一点务必记住:更新模式的延迟不能当成安全冗余,安全必须另有通道。
6. 不换方案的优化手段:把更新动作精准“卡”到正确时刻
6.1 控制预装载位和软件更新事件
如果某个应用场景确实需要让新值立刻生效,可以关闭对应通道的比较寄存器预装载位,直接写活动寄存器。但这么做之前要评估周期中途翻转带来的波形畸变风险,一般只建议在停机、急停这类非正常流程中使用,不要放在正常调制路径里。
另一种方法是软件主动产生更新事件,STM32 里就是置 UG 位。但 UG 会同时复位计数器重新装载预装载寄存器,在波形上会留下明显的相位跳变,不适合精细控制。它最合适的用法是初始化阶段逐个装载所有影子寄存器,让 PWM 从一个已知的干净状态开始运行。
6.2 DMA 批量搬运占空比表,绕过单拍更新延迟
SPWM 逆变器、数控波形发生这类场景,常用一个很有效的招:内存里放一张预先算好的占空比序列表,配置 PWM 更新事件触发 DMA,DMA 自动把表里的下一个值搬运到比较寄存器。CPU 完全不参与逐周期更新,自然也就不存在“CPU 写入 → 等待更新事件”的延迟问题。
配置要点有三个:内存数组开启循环模式,外设地址指向定时器比较寄存器,DMA 触发源选为定时器更新事件。这样每个 PWM 周期结束,DMA 自动把新值搬进去,更新事件一到就生效,整个过程是流水线式的。CPU 只需要在运行过程中偶尔刷新整张表,粒度可以放宽到几百个周期一次。
这个方案的代价是内存占用和预计算量,但对逆变器、变频空调压缩机这类应用来说非常划算。它本质上是用“批量更新”代替“逐次更新”,有效绕开了传统更新模式单拍延迟的约束。
6.3 重复计数器把更新和采样彻底错峰
RCR 重复计数器可以设定每隔 N 次计数满才产生一次更新事件。利用这个功能,可以让更新和采样在两个不同子周期完成:比如计数峰值时做 ADC 采样,计数谷值时执行更新,两者在不同半周期发生,打破了“采样点被更新点挤压”的困局。
在实际配置中,RCR 改的是更新事件频率,对比较寄存器的预装载行为也有连带影响,必须查参考手册确认。我一般配合 DMA 一起用:RCR 把更新事件频率压低,DMA 在每次更新时搬运新占空比,ADC 在另一个相位触发采样,CPU 只在中断里跑控制算法。这样整个信号链路由硬件时钟统一驱动,软件部分几乎不会引入不确定延迟。
6.4 选型判断:什么时候必须换高分辨率或可配更新相位的方案
如果项目已经踩到传统更新模式的天花板,与其在软件里打补丁,不如回头评估硬件选型。下面几个判断条件,如果满足两条以上,建议尽早换平台:
- 控制周期和 PWM 周期接近,一拍延迟对环路稳定性影响显著;
- 需要周期内多次更新或者动态死区调节;
- 对占空比分辨率要求高于传统定时器计数位数的能力,尤其是 MHz 级 PWM;
- 多个 PWM 通道需要严格保持同相位输出,软件同步成本偏高;
- 项目要求高带宽电流环,采样点必须精确落在某几个固定相位上。
TI 的高分辨率 PWM(HRPWM)用 MEP 微边沿定位可以把占空比精度做到 150ps 量级,英飞凌的 CCU6 和新一代电机控制 MCU 也提供了更灵活的更新点配置。这些方案比传统定时器 PWM 贵,但省下来的调试时间和性能收益往往远超差价。
最后分享一个我个人的经验习惯:拿到任何新平台,第一件事不是跑 demo,而是用逻辑分析仪去测“从软件写寄存器到 PWM 输出边沿变化”的延迟。如果延迟严格等于一个 PWM 周期且相位固定,说明是最传统的更新模式;如果延迟在随机波动,说明更新路径里混入了软件调度;如果延迟小于一个周期,说明硬件支持立即更新或者多更新点。这个测量结果基本决定了整套控制策略怎么写。做 PWM 相关的项目,先把更新模式研究透,比盲目调 PID 参数有效得多。