PWM波生成全解析:从占空比计算到死区、DMA与故障保护实战 📅 发布时间:2026/9/13 13:04:58 👁 浏览次数: 我用一个小场景开头吧。去年我调一块无刷驱动板三路互补PWM死活出不来波形示波器一挂六路信号里有两路是齐的另外四路互相乱抖。查了一整天最后发现是死区寄存器没配上下桥臂直通电流直接把你板子上的一路采样电阻烧冒烟。从那天起我就意识到PWM波这件事会写两行代码配置定时器只是入门真正决定波形能不能用、电路会不会炸的是那些藏在高级定时器里没人给你画重点的寄存器位和硬件特性。这篇学习笔记就是把PWM波的生成从底层到实战完整梳理一遍涵盖占空比计算公式、不同芯片平台51、STM32、PY32、Mega2560、RK3588的生成方式、死区与中心对齐模式、PWM DMA搬运、故障保护以及AO3400A这类MOS管驱动电路里容易被忽略的细节。适合刚接触PWM的新手建立体系也适合已经写过PWM但一直“知其然而不知其所以然”的开发者查漏补缺。1. 先从波形本身说起占空比、频率与等效电压1.1 占空比决定的是负载的“平均感受”PWM波从外观上看就是一个方波高电平、低电平、再高电平循环往复。但真正驱动负载的时候负载并不关心那一瞬间是多少伏它感受的是整个周期内的平均效果。比如一个5V的方波高电平占四分之一时间、低电平占四分之三时间等效平均电压就是1.25V。LED会按这个平均电压呈现亮度电机会按这个平均电压决定转速加热棒按它决定功率。这个“平均”对不同类型的负载有不同的物理含义。对LED这类阻性负载来说电流和电压线性相关人眼又有视觉暂留效应所以1kHz左右的PWM就能实现无级调光。对电机这类感性负载来说电感会产生续流效应电流不会立即归零而是被电感平滑成带有一定纹波的直流电流因此PWM调压的效果比想象中更接近模拟电压。理解了这一点很多后续问题——比如为什么同一个PWM信号驱动LED不闪但驱动舵机乱抖——就都解释得通了。核心计算公式写在这里占空比D T_on / (T_on T_off) × 100%等效电压V_avg V_high × D频率f 1 / (T_on T_off)1.2 频率选择不是越大越好频率代表的是“一秒开关多少次”。选低了LED会闪、电机会一顿一顿选高了开关损耗上升、驱动电路可能跟不上。具体怎么选取决于负载属性和系统设计目标。这里给出几个我实际用过的参数区间负载类型推荐频率选择理由LED调光1kHz ~ 5kHz人眼在300Hz以上基本无闪烁感过高意义不大标准舵机50Hz接收机/舵机协议固定周期20ms脉宽0.5ms~2.5ms普通直流电机10kHz ~ 30kHz避开人耳可听范围同时纹波可被电感有效滤除TEC帕尔贴100Hz ~ 1kHz过高会导致制冷效率降低过低会出现温度波动和可闻噪声BLDC/开关电源20kHz ~ 100kHz需要配合电机电感/变压器参数做开关纹波设计这里有一个反直觉的点PWM频率不是越高越好。频率提高确实能减小电流纹波但开关损耗和驱动电路损耗会同步上升。尤其是在用MOS管做功率开关时一个开关周期内的开通损耗和关断损耗都是实实在在的发热来源频率翻倍发热几乎翻倍。所以工程上选频率的原则是“够用就好”先看负载对纹波和噪声的要求再反推开关频率。1.3 定时器里的占空比是怎么算出来的写代码之前得先理解单片机的定时器是如何“翻译”占空比的。以最常见的通用定时器为例里面有三个关键寄存器PSC预分频器对定时器时钟进行分频ARR自动重装载值计数器从0数到ARR数满就是一个PWM周期CCR捕获/比较寄存器计数器计到CCR时输出电平翻转所以PWM频率f 定时器时钟 / ((PSC1) × (ARR1))高电平时间T_on CCR / 定时器时钟 × (PSC1)占空比D CCR / (ARR1) × 100%我举个实际算过的例子。某STM32芯片的定时器时钟是84MHz我想生成一个20kHz的PWM。选PSC41、ARR99频率就是84,000,000 / (42×100) 20kHz。占空比50%时CCR50。这里要注意的是ARR和PSC是两回事PSC决定的是“计数一次要多长时间”ARR决定的是“一共数多少次算一个周期”。两者共同决定频率但CCR只与ARR共同决定占空比。很多人刚开始会误以为调节PSC也能改变占空比其实PSC只影响频率。2. 生成PWM的三条路线软件翻IO、定时器中断和硬件输出2.1 纯软件延时翻转只适合理解概念最直觉的写法就是拉高IO延时拉低IO延时再拉高循环。核心代码大概长这样while (1) { GPIO_SetPin(HIGH); delay_us(50); // 高电平持续时间 GPIO_SetPin(LOW); delay_us(150); // 低电平持续时间 }这种方式的优点只有一条不依赖任何外设只要会GPIO就能写。我第一次做呼吸灯用的就是它LED确实能亮能灭但做出来的效果很粗糙。问题出在三个方面CPU被完全占死。延时期间什么都做不了没法同时处理按键、显示、通信。占空比不准。只要来了一个中断ISR执行期间电平状态就被拉长了波形直接失真。频率做不高。延时函数的精度有限想生成几十kHz的PWM基本不可能。所以我的建议是纯软件翻转只适合学概念的时候跑一下确认“哦原来PWM就是这个意思”然后就没有然后了正经项目千万别这么干。2.2 定时器中断翻转解决了占用但精度依然有限把延时改成“在定时器中断里翻转IO”可以解决CPU被占死的问题。做法是让定时器按固定周期触发中断每次中断里翻一次IO再用一个变量记录当前处于高电平还是低电平状态。void TIM_IRQHandler(void) { count; if (count CCR) { GPIO_SetPin(HIGH); } else { GPIO_SetPin(LOW); } if (count ARR) { count 0; } }这种方式比纯延时好CPU可以在主循环里干别的。但依然有隐患中断响应本身有延迟从“比较条件成立”到“真正执行GPIO翻转”之间隔着中断入栈、变量读取、IO操作的一条龙时间。频率越高这个延迟占周期的比例越大波形就越不规范。所以我用它做过呼吸灯、简单的舵机控制但一旦涉及电机驱动或者对波形时序有要求的场合还是得回到定时器硬件的轨道上。2.3 硬件PWM输出工程上的正解大多数MCU的定时器都有“输出比较”或“PWM模式”。它的原理简单说就是计数器的计数值是硬件自动累加的无需软件参与当计数值等于CCR时硬件自动翻转输出引脚当计数满ARR溢出时硬件自动复位。整个过程中CPU只需要在初始化时写一次寄存器之后就可以去睡觉了。硬件PWM和软件PWM最大的区别在于确定性。电平翻转的时机由数字比较器硬件完成不依赖指令执行时间、不依赖中断响应时间所以波形的频率和占空比是严格确定的。打开“PWM模式”和“自动重装载预装载”之后运行中修改CCR还能让占空比在下个周期平滑生效这也就是呼吸灯、云台舵机、电机调速这类动态调占空比场景的标准做法。后面的内容基本都围绕硬件PWM展开因为这才是工程上真正能用、用了不慌的生成方式。3. 不同平台的PWM生成实操从51到RK35883.1 51单片机定时器模拟PWM以及驱动WS2811的硬骨头51单片机的大部分型号不带硬件PWM或者只带一个极简功能的PCA模块。所以51上做PWM的标准姿势是用定时器产生一个固定时间片中断比如10us在里面维护一个计数器把计数器和目标比较值做比较决定IO高低电平。void timer0_isr() interrupt 1 { counter; if (counter compare_value) { P1_0 1; } else { P1_0 0; } if (counter period_value) { counter 0; } }这套逻辑能跑出占空比可调的波形做LED呼吸灯绰绰有余。但如果你拿去驱动WS2811/WS2812灯带就会撞到南墙。WS2811的数据协议是纳秒级的0码要求高电平约350ns、低电平约800ns1码要求高电平约700ns、低电平约600ns。51的定时器中断进一次至少要几微秒根本来不及逐bit输出。所以网上那些“51单片机驱动WS2811”的方案要么用汇编做精细延时要么用硬件SPI模块按位搬运比如把8bit的亮度数据映射到16bit SPI数据用2MHz左右的SPI时钟模拟时序。这种做法能跑但相当折腾而且主频不够的51很容易翻车。3.2 STM32/PY32通用定时器做PWM的寄存器级写法到了STM32这个级别的MCUPWM就变成了正经的外设功能。以STM32F103的TIM2为例用标准库或HAL之外我更推荐先看一眼寄存器级写法能帮助理解硬件到底在干什么// 1. 开启定时器时钟这里以APB1上的TIM2为例 RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // 2. 配置PA0为复用推挽输出对应TIM2_CH1 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; GPIOA-CRL ~(0xF (0 * 4)); GPIOA-CRL | (0xB (0 * 4)); // 复用推挽输出50MHz // 3. 配置PSC和ARR假设时钟72MHz目标频率20kHz TIM2-PSC 71; // 72MHz / (711) 1MHz TIM2-ARR 49; // 1MHz / (491) 20kHz // 4. 配置通道1为PWM1模式设置占空比 TIM2-CCMR1 | (TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1); // 110: PWM模式1 TIM2-CCMR1 | TIM_CCMR1_OC1PE; // 输出比较预装载使能 TIM2-CCR1 25; // 占空比 25/50 50% // 5. 使能输出和定时器 TIM2-CCER | TIM_CCER_CC1E; TIM2-CR1 | TIM_CR1_CEN;这段代码跑通了PWM就在PA0上出来了。PY32F003这类国产小MCU思路完全一致只是库函数不同。我之前用PY32F003做过一个呼吸灯demoPWM输出接LED主循环里每隔几毫秒把CCR从0递增到ARR再递减回来LED的亮度就流畅地“呼——吸——”。整个过程几乎不占CPU时间效果比软件PWM强太多。3.3 Mega2560和RK3588从Arduino到嵌入式Linux的PWMMega2560用的是AVR的Timer如果不想折腾寄存器Arduino的analogWrite()可以直接输出固定频率的PWM。但这里有个坑analogWrite()的频率是固定的大约490Hz取决于Timer分频想要调节频率得直接操作寄存器。比如说要配置Timer1输出10kHz的PWM就必须自己改WGM、COM和OCR。Arduino封装了易用性也把灵活性藏起来了。RK3588这类带操作系统的SoCPWM生成的思路又不一样。Linux下PWM通常由片上PWM控制器驱动通过设备树配置好节点后操作/sys/class/pwm/下的文件即可。我调RK3588风扇PWM时流程是这样的# 在设备树里使能pwm-fan节点配置好pwm通道和频率范围 # 系统启动后导出PWM通道 echo 0 /sys/class/pwm/pwmchip0/export # 设置周期单位ns比如40kHz echo 25000 /sys/class/pwm/pwmchip0/pwm0/period # 设置占空比50%就是12500ns echo 12500 /sys/class/pwm/pwmchip0/pwm0/duty_cycle # 使能输出 echo 1 /sys/class/pwm/pwmchip0/pwm0/enable从51到STM32再到Linux平台变了但底层逻辑完全一致频率由周期寄存器决定、占空比由比较寄存器决定、最终输出由硬件自动翻转。把这套抽象模型吃透换什么平台都只是换个寄存器名字的问题。3.4 常见负载的PWM参数参考我把几个典型场景的参数整理成一张表方便直接抄作业应用对象频率占空比范围说明LED呼吸灯1kHz0% ~ 100%动态改CCR即可标准舵机50Hz2.5%~12.5%脉宽0.5~2.5ms1.5ms脉宽对应中位直流电机调速20kHz0%~100%超过20kHz人耳听不到啸叫TEC温控100Hz~1kHz0%~100%需要兼顾制冷效率三相BLDC20kHz~50kHz由FOC算法计算需要6路互补PWM死区4. 高级定时器才有的功能死区、中心对齐、DMA和故障保护4.1 死区为什么H桥/BLDC控制绕不开它H桥或三相逆变桥驱动电机时上下桥臂的两个开关管MOS管或IGBT是串联在电源两端的。如果上下两个管子同时导通电源直接短路电流瞬间飙升板子必炸。理想情况下PWM切换瞬间是“上面关断、下面开通”的完美接续但实际器件不是瞬间完成开关的——关断延迟比开通延迟长。解决办法是在上下桥切换之间插入一段“都不导通”的时间这就是死区。死区时间里上下桥臂全部关断电流靠MOS管体二极管或外部反并联二极管续流。STM32的高级定时器TIM1/TIM8、英飞凌TC3xx的CCU6这类外设都支持硬件死区插入只要配置了互补PWM和死区时间硬件会在每一条互补边沿自动插入延迟不需要软件干预。死区时间不是越大越好。死区太长波形失真、电机电流谐波增加、输出效率下降太短关断延迟覆盖不掉直通风险仍在。常见做法是先查开关管的关断延迟和栅极驱动电路的传播延迟再留出30%~50%的余量。我之前用TC3xx CCU6输出三相六路PWM控制BLDC死区设置在1us左右使用示波器观察上下桥的导通波形确认没有交叠区间后才敢真正上强电。4.2 中心对齐模式与ADC采样时刻的选择常规PWM叫边沿对齐模式计数器从0数到ARR然后立刻回0输出波形的高电平集中在周期一侧。中心对齐模式不一样计数器从0数到ARR再从ARR数回0输出波形的高电平被对称地放在周期中间。中心对齐的优点是输出波形对称性好谐波含量低电机相电流波形更平滑所以FOC矢量控制里普遍用中心对齐模式。但代价是同样的计数器时钟和ARR中心对齐的PWM频率降为边沿对齐的一半或者说要保持同频率需要的计数频率翻倍。这个模式下有一个非常实际的坑ADC采样的时刻。电机控制电流采样要避开功率管开关瞬间产生的噪声尖峰最佳采样时刻是计数器的顶端或底端——也就是PWM周期的正中心此时上下桥臂状态刚稳定噪声最小。我当时第一次调STM32高级定时器中心对齐模式加ADC同步采样总采到毛刺很大的电流波形后来把触发源配置为定时器更新事件再偏移一点采样点波形才干净。这件事说明PWM的波形质量不仅在于生成端还在于和ADC采样的时序配合。4.3 PWMDMA用内存搬运替代CPU干预光看“PWM输出”本身CPU只需要配置一次。但很多场景需要动态改变占空比比如WS2812整条灯带串行数据、多路舵机的连续角度轨迹、音乐频谱灯。一波数据几百上千个点如果每个点都在中断里改一次CCRCPU忙死且时序不稳。PWMDMA的思路很简单把占空比序列预放在一个数组里利用定时器的更新事件作为DMA请求源DMA自动把数组中的值逐个搬运到CCR寄存器。CPU只管准备数组和启动DMA中间过程不参与。以STM32 HAL库为例// 假设已经初始化好PWM输出 uint32_t duty_buffer[100]; // 占空比序列提前计算好 HAL_TIM_PWM_Start_DMA(htim, TIM_CHANNEL_1, duty_buffer, 100);数据搬完可以触发DMA半传输/传输完成中断在中断里填充下一段数据。这样就实现了“数据生成交给CPU、时序执行交给硬件”的流水线。我做WS2812灯带驱动的时候就是用的这个方案一路PWM模拟数据位的脉宽DMA把一个灯珠的GRB数据按bit展开成序列几百个灯珠的刷新全程不卡顿。这也是网上“PWM DMA HAL”那类搜索词背后最典型的应用。4.4 PWM故障保护Break/刹车机制高级定时器通常都带故障保护引脚比如STM32 TIM1/TIM8的BKIN引脚、TC3xx CCU6的故障输入。这个功能的设计初衷是当外部电路检测到过流、过压、过温时硬件在微秒或纳秒级内把PWM输出强制置于安全状态比如全部输出低电平不需要软件参与判断。故障保护为什么重要因为PWM控制电机或功率变换器时软件响应速度根本赶不上故障的破坏速度。你可能在1us内就能烧掉一个MOS但CPU从检测到中断到执行关断指令至少需要几百ns到几微秒中间这段空白足以让功率器件冒烟。硬件故障保护直接锁死输出是最后一道安全防线。配置故障保护时要确认几个参数故障输入极性高有效还是低有效、故障响应电平输出高还是低、故障后是否需要软件解锁。我在调试中遇到过一种情况设置了BKIN后PWM一有负载就立即保护查了半天发现是外部比较器的参考电压设低了正常工作时噪声触发了故障误判。这类问题排查起来确实头疼但总比真炸管好。5. PWM波生成之外的硬件细节驱动级和电路设计的坑5.1 单片机IO直驱还是驱动芯片电流能力说了算单片机的GPIO输出驱动能力一般在几毫安到二十毫安之间带个LED没问题带继电器、电机、灯带这类负载就力不从心了。PWM信号本身只是“控制信号”真正驱动负载的是功率器件。工程上最常见的接法PWM从MCU引脚输出经过一个限流电阻接到三极管或MOS管的栅极/基极功率管再开关负载回路。这样负载的大电流走电源回路不经过MCU引脚MCU只负责提供几毫安的控制电流。如果负载电流更大或者开关频率更高就用专门的栅极驱动芯片比如半桥驱动、MOS驱动它们内置电平转换、死区控制甚至故障反馈。5.2 AO3400A这类N-MOS在PWM电路里的角色AO3400A是一颗非常常见的低压N沟道MOS管SOT-23封装Vgs阈值电压大约在0.65V到1.4V之间也就是说以3.3V或5V逻辑电平可以直接驱动不需要额外电平转换。它的导通电阻Rds(on)在Vgs4.5V时大概在几十毫欧级别作为低压侧的PWM电子开关非常合适。很多人直接把单片机PWM引脚接到MOS管栅极就算完事但有几个细节值得注意。第一个是栅极电阻栅极串联一个几欧到几十欧的电阻用来抑制开关瞬间的振铃。电阻太小栅极回路寄生电感会和栅极电容形成LC振荡波形上出现明显过冲电阻太大开关边沿变缓开关损耗增大。第二个是栅极下拉电阻10k到100k左右的栅源下拉防止MCU复位期间引脚悬空导致MOS误开通。这一点对“PWM上电瞬间乱跳”这类问题特别关键。5.3 H桥PWM电路的数学原理H桥是直流电机正反转控制的基础拓扑四个开关管组成一个“H”形——负载接在中间横杠上四个开关交替组合让电流可以双向流过负载。PWM调速时通常是固定方向用PWM控制有效占空比。假设电源电压是Vbus那么负载两端的平均电压就是V_motor D × Vbus前提是方向组合固定且上下桥未直通这也是直流电机PWM调速的基本公式。实际波形并不平滑电机两端等效一个电阻加电感电感对PWM电压起滤波作用电流纹波大小和PWM频率、电感量、母线电压都有关系。频率越高、电感越大、母线电压越低电流纹波越小。但频率高到一定值后开关损耗成为主要矛盾所以要综合权衡。6. 我在实际调试PWM时踩过的几类坑6.1 上电瞬间的意外PWM导致误动作MCU上电复位期间GPIO默认状态是浮空输入或弱上拉PWM外设还没初始化。如果这个GPIO直接接到MOS管栅极浮空状态下栅极电压可能超过阈值负载在上电瞬间会“抖”一下或者误动作。我遇到过一上电风扇就猛转一下才停的问题排查了很久最后在栅极加了一颗100k下拉电阻同时把初始化顺序改成“先配置GPIO为低电平输出再初始化PWM复用”问题彻底消除。6.2 示波器探头带来的假波形PWM频率上到几十kHz之后示波器测量本身的寄生参数就开始影响测量结果。最典型的是那条带鳄鱼夹的接地线线长十厘米左右就等效于一个分布电感会和探头电容形成谐振波形上出现看起来像信号本身在“振铃”的假像。我一度以为是驱动电阻没调好折腾了几个小时最后换成一个短接地弹簧振铃立刻消失。用示波器看高频PWM时接地线尽量短测量点尽量靠近引脚。6.3 电机啸叫和PWM频率的恩怨直流电机在几百赫兹到几千赫兹的PWM下会发出刺耳的“吱吱”声这是PWM电流纹波中的音频分量通过电机壳体辐射出来的。解决办法很直接把开关频率提高到20kHz以上超出人耳听觉范围。但提高频率后要留意两个问题一是开关损耗变大MOS管温度会升高二是如果你的驱动芯片本身开关速度不够高频下波形会变差。所以提高PWM频率前先确认驱动方案能扛住这个开关速度。6.4 舵机和WS2811这类“挑剔”负载舵机的PWM虽然占空比只有几个百分点到十几个百分点但脉宽的绝对值是关键。标准舵机要求周期20ms50Hz脉宽0.5ms到2.5ms对应0度到180度。如果你的定时器频率设计得不合适CCR的步进精度不够舵机转动时就会一跳一跳不平滑。WS2811则是另一个极端它的数据协议对高低电平的脉宽精度要求是纳秒级用普通定时器中断模拟基本没戏老老实实用硬件SPI或PWMDMA方案。最后说说我自己做笔记的习惯。每次调完一个PWM相关的问题我都会把三样东西记下来第一是波形截图标出频率、占空比和异常点第二是寄存器配置代码注明关键参数怎么算出来的第三是踩坑过程记录现象、排查链路和最终原因。这些笔记积累起来之后再遇到类似问题基本能凭记忆快速定位不用每次都从头查数据手册。建议你也试试这个办法比收藏一堆教程有用得多。这篇PWM波生成的笔记就先整理到这里下一步我打算把PWMDMA驱动WS2812灯带的完整工程和波形实测数据整理出来到时候再继续聊。