
简介面向STM32嵌入式开发者提供一套采用单定时器产生SPWM波形的源码工程适用于电机控制、逆变器、电源转换等场景。压缩包共40个文件包含C源程序、头文件、Keil工程文件工程配置与编译脚本、编译产物与映射文件、以及调试说明文档方便直接打开工程查看或烧录验证大小仅570KB。已有703人学习下载适合具备C语言和定时器基础、正在学习脉宽调制技术的读者。资源核心是单定时器生成SPWM的例程覆盖定时器分频、通道占空比设置、死区插入与中心对齐等关键知识点同时对输出波形有调试记录。项目中保留的中间文件可帮助理解编译链接过程也便于排查配置异常对实际工程开发有较高参考价值。1. 先从“SPWM程序”这个高频搜索词说起在 stm32 工程里搜 SPWM 程序出来的压缩包往往带着不同日期的改版代码风格也差别很大。把表面差异剥掉后剩下的核心其实很固定一张正弦表、一条把正弦表搬运到 CCR 寄存器的 DMA 链路、一组带死区的互补 PWM。SPWM 本质上是用高频方波的占空比变化去逼近正弦波的平均电压这也是逆变器和电机驱动里成本最低、最容易落地到单片机的做法。适合的人有两类第一次做逆变或电机驱动的需要从最小可运行程序起步已经有驱动经验、但想优化死区、滤波和变频调节的工程师。下面从正弦表生成讲到定时器参数再落到 LC 滤波和相位累加变频。2. SPWM生成与查表法选型STM32项目里应用最广的正弦表方案2.1 面积等效是前提三种常见的spwm生成路径SPWM 的理论基础是面积等效原理在载波周期内只要脉冲的面积近似等于对应正弦波片段的面积输出电压基波就接近目标正弦。把这句话翻成工程语言就是“正弦值变成占空比占空比变成 CCR 值”。在 STM32 项目里实际实现 spwm 生成常见有三条路。第一条是用普通定时器中断翻转 IO在中断里计算下一拍的高低电平时间。这条路只能应付几百赫兹的载波IO 翻转和中断进出浪费大量 CPU 时间建议用来验证原理不上产品。第二条是定时器 PWM 输出 更新中断每次中断里用浮点库算一个正弦值并写入 CCR。对于 10kHz 载波来说F103 还能撑住但单片机 ISP 和通信任务一多定时器中断里的浮点运算会带来不确定的相位抖动。第三条是 PWM DMA 查表正弦提前算成数组DMA 在定时器更新事件触发下自动把下一个值搬进 CCR。这是 stm32 项目里最常见的做法原因很简单中断里不跑浮点、不写 CCRCPU 只负责改表或者改步长。2.2 查表、实时计算与 DDS 的选型表生成方式CPU 占用内存占用频率调整难度适用场景定时器中断实时计算最高随载波线性增加最小直接改公式里的频率参数低频正弦、教学验证PWM 查表 DMA极低中断几乎为空小200 点表约 400 字节改表或改步长需重载 LUT10kHz 以上载波的 SPWMDDS 相位累加低每次更新做一次加法中表本身不变累加器几个变量频率分辨率高连续可调变频器、可调正弦电源表中第三条其实是查表法的进阶版第 5 章会单独展开。对固定 50Hz 工频输出直接选第二行就够了如果后面要做电机调速建议一开始就按 DDS 的结构预留相位累加器变量避免最后改架构。2.3 用 Python 生成可移植的 16 位正弦表假设定时器向上计数模式ARR 对应载波周期PWM 分辨率等于 ARR1。为了让 CCR 始终在 0 到 ARR 之间并把调制度 M 预留进来正确公式是取均值中心 50%再叠加正弦偏移import math N 200 # 一个完整正弦周期的采样点数 arr 7199 # 定时器自动重装值对应 72MHz / 7200 10kHz M 0.80 # 调制度建议 0~0.95避免过调制 lut [] for i in range(N): # 中心对齐到 50% 占空比0.5 0.5 * M * sin() ccr (1.0 M * math.sin(2 * math.pi * i / N)) * arr * 0.5 lut.append(int(round(ccr))) # 输出成 C 语言数组 print(const uint16_t spwm_lut[%d] { % N) for i in range(0, N, 10): print( , .join(str(x) for x in lut[i:i10]) ,) print(};)代码里(1.0 M * math.sin()) * arr * 0.5这一步要重点解释。如果直接写arr * M * math.sin()算出的 CCR 有一半时间是负值写入 16 位寄存器后会出现很大的占空比台阶很多人做出来的 SPWM 波形底部发毛就是这个公式用错了。中心偏移到 50% 后正弦波的正半周对应占空比从 50% 到接近 100%负半周从 50% 到接近 0%桥臂输出电压的平均值才是完整正弦。提示表的大小 N 不是越大越好。N 太大会占用 Flash 并抬高 DMA 搬运频率N 太小会看到明显的阶梯状包络。经验值 N 取载波频率 / 基波频率的整数倍例如 10kHz 载波、50Hz 正弦时取 200。3. 定时器参数与 DMA 搬运 CCRSTM32 SPWM 程序的核心配置3.1 PWM 时基和输出比较通道的初值设定以 STM32F103 的 TIM1 为例。选择向上计数模式ARR 直接决定载波频率TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 72MHz 主频不分频直接做计数器时钟 TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 7199; // 10kHz 载波 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure);向上计数模式下更新事件频率等于 PWM 载波频率公式是f_pwm f_clk / ((PSC1) * (ARR1))。72MHz 主频下PSC0、ARR7199 得到 10kHz 载波。之所以不用中心对齐模式是因为中心对齐时计数器上溢和下溢都产生更新事件更新频率是载波的两倍查表点数 N 和基波频率的换算要多乘一个 2刚开始容易算错边沿对齐计算直观单相逆变和中小功率电机驱动场景谐波性能完全够用。输出比较通道的初值要给一个安全值避免上电瞬间 MOS 全开。在 DMA 第一次搬运前CCR 保持默认 0 会让占空比为 0%一上来就搬表反而容易在启动瞬间出现大电流。常见做法是初始化时把 CCR 直接写为 50% 占空比TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse 3600; // 50% 占空比安全初值 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);TIM_Pulse3600对应 ARR 的一半这一拍的输出不会产生过大冲击。整个初始化顺序也值得注意先配置 GPIO 复用推挽再初始化时基再配置输出比较最后开 DMA 和定时器顺序颠倒容易出现启动瞬间的一到两个毛刺脉冲。3.2 DMA 循环搬运 CCR 的配置顺序与常见漏配配置 DMA 把spwm_lut循环搬运到 TIM1-CCR1关键参数是外设地址固定、内存地址递增、数据宽度 HalfWord、模式为循环DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM1-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)spwm_lut; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 200; // LUT 点数 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel5, DMA_InitStructure); // 关键一允许 TIM1 更新事件触发 DMA 请求漏配会导致 CCR 不更新 TIM_DMACmd(TIM1, TIM_DMA_Update, ENABLE); // 关键二先使能 DMA再使能定时器 DMA_Cmd(DMA1_Channel5, ENABLE); TIM_Cmd(TIM1, ENABLE);DMA1_Channel5 在 F103 上对应 TIM1 更新事件其他型号要查对应的 DMA 请求映射表。这中间最容易被忽略的是TIM_DMACmd这一行很多工程只配置了 DMA 通道就开定时器结果示波器看到占空比始终是初值不动PWM 波形完全正常但不像正弦就是这个触发源没打通。提示DMA 在搬运过程中如果手动向 CCR1 写值会和外设对 CCR 的写入形成竞争产生毛刺。固定频率的 SPWM 用 DMA 即可需要变频时建议放弃 DMA改用更新中断做相位累加第 5 章就是这套做法。3.3 载波、正弦点数和基波频率三者如何联动向上计数模式下基波频率由更新事件频率除以正弦表点数决定f_sine f_pwm / N常见组合直接查表载波频率PSCARRLUT 点数 N基波频率10kHz0719920050Hz20kHz0359940050Hz25kHz0287950050Hz三组配置的基波都是 50Hz区别在于载波越高LC 滤波越容易做但 MOS 开关损耗变大。ARR 越小每个正弦周期内的占空比台阶越粗输出波形 THD 会变差。20kHz 以上时开关损耗明显一般单相逆变选 10kHz 到 20kHz 即可特高压或大功率场合再往高载波走。4. 调制度、死区与 LC 滤波STM32 SPWM 上电前的三个关键参数4.1 调制度 M 的余量设置与重算表的方式调制度 M 决定输出正弦幅值。对单极性倍频结构输出电压基波有效值约等于Vdc / 2 * M / sqrt(2)M0.8 意味着把直流母线电压的 80% 留给基波。把 M 直接乘进 LUT 是最直观的做法#include math.h void spwm_rebuild_lut(uint16_t *lut, uint16_t points, uint16_t arr, float mod) { for (uint16_t i 0; i points; i) { float s sinf(2.0f * (float)M_PI * i / points); lut[i] (uint16_t)((1.0f mod * s) * arr * 0.5f); } }这段代码放在启动时调用即可不要在定时器更新中断里跑。中断里每次算sinf会引入数十微秒的阻塞且浮点结果写入 CCR 的时机不稳定。如果要在运行中连续调压常见做法是准备两份 LUT后台算完后再切换 DMA 的内存地址F103 的 DMA 可以在关闭通道后重新设置DMA_MemoryBaseAddr再开启切换瞬间会产生一个周期的不连续对电机负载影响很小。M 不建议长期设为 1.0。接近满调制时波峰附近的 CCR 会顶到 ARR产生天然削顶输出正弦波顶部变平、低次谐波明显上升电机会额外发热。工程上习惯留 5% 到 20% 的余量M0.8 到 0.95 是最常见的区间。4.2 互补桥臂的死区设置与单极性倍频输出SPWM 电桥不能把互补 PWM 直接接到上下管。上管还没关断、下管已经导通时直流母线直接短路炸管和烧驱动基本都从这里来。TIM1 作为高级定时器通过 BDTR 寄存器配置死区和输出使能TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime 0xA0; // 72MHz 下约 1.33us TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_BDTRInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_BDTRInit(TIM1, TIM_BDTRInitStructure);TIM_DeadTime 0xA0在 72MHz 时钟下大约对应 1.33µs具体换算规则在参考手册 DTG 位段表里查不同主频对应的数值差别很大直接用0xA0前先确认自己的主频。死区太小起不到保护作用死区太大会让输出基波电压下降波形在过零点附近出现明显的畸变专业术语叫死区效应。验证方法很简单示波器两路探头分别接 CH1 和 CH1N放大上升沿看两路之间有没有一段同为零电平的时间。单极性倍频是单相全桥的常用接法一路桥臂走高频 SPWM另一路桥臂走 50Hz 低频换流。高频臂用 TIM1_CH1/CH1N 互补输出低频臂可以单独用 TIM1_CH2 输出固定 50% 占空比或者用 IO 翻转。这样整个桥臂的等效开关频率是载波的两倍LC 滤波可以选得小很多。参考常见的 stm32 电机驱动原理图半桥驱动电路里自举电容选 100nF、栅极电阻 10Ω 起步这些参数配合 1.33µs 死区在中小功率全桥里是稳定的起点值。4.3 LC 滤波器的截止频率选择和阻尼问题PWM 桥臂输出的是一串高频方波必须经过 LC 低通滤波才能得到正弦。LC 截止频率按载波频率的 1/10 到 1/20 选取10kHz 载波对应 500Hz 到 1kHz// 已知 L 求 Cfc 1 / (2 * pi * sqrt(L * C)) float L 0.005f; // 5mH float fc 700.0f; // 目标截止频率 float C 1.0f / (4.0f * (float)M_PI * (float)M_PI * fc * fc * L); // 计算得到 C 约 10uF滤波电感的电流承受能力要按额定负载电流选否则电感饱和后滤波失效。下表给组实用组合额定电流电感 L电容 C截止频率 fc1A10mH1µF约 1.6kHz2A5mH10µF约 712HzLC 滤波最大的坑是空载和轻载时电压升高。LC 二阶电路在截止频率附近有谐振峰空载时 Q 值高输出电压可能比理论值高出不少。解决办法是在电容两端并联一个百欧级电阻或者把电感的磁环改成带一定气隙的类型让阻尼始终存在。4.4 上电或带载后的常见异常排查现象优先检查处理方向上电瞬时炸管或烧驱动死区寄存器值、互补通道是否关闭先空载、低压 24V 验证死区波形PWM 正常但占空比不变化TIM_DMACmd是否使能、DMA 请求映射补上 TIM_DMA_Update 触发正弦包络出现明显台阶ARR 太小或 LUT 点数太少提高 ARR 或增大 N带载后电压跌落明显死区效应、M 余量不足增大 M、减小死区时间空载电压异常偏高LC 谐振峰无阻尼并联合适电阻做阻尼5. 用相位累加器做变频SPWM 程序怎么接进变频调速系统5.1 更新中断里的相位累加实现DMA 适合固定频率输出一旦要变频就得改表或者改 DMA 搬运间隔两条路都不方便。把查表法升级成 DDS 思路用更新中断做相位累加是 stm32 和变频器通讯调速场景里非常成熟的做法#define LUT_POINTS 512 #define PHASE_BITS 16 #define IDX_SHIFT (PHASE_BITS - 9) // 2^9 512取高 9 位做索引 static uint32_t phase_acc; static uint32_t phase_inc; // 频率字 void TIM1_UP_IRQHandler(void) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); phase_acc phase_inc; uint16_t idx (phase_acc IDX_SHIFT) (LUT_POINTS - 1); TIM1-CCR1 spwm_lut[idx]; }中断里只有一次加法、一次移位、一次与运算时间完全确定没有浮点和除法。输出频率为f_sine phase_inc * f_pwm / 65536。10kHz 更新频率下phase_inc328 大约对应 50Hz相位累加器是 16 位频率分辨率约 0.15Hz对 V/F 调速已经足够。如果做 SVPWM 或闭环可以把PHASE_BITS提到 24 位表不变只改移位位数。这套结构配合上位机做 SPWM 变频器-异步电机交流调速系统时phase_inc就是频率给定Modbus 寄存器里写目标频率单片机换算后直接更新phase_inc。如果旁边接了 K210 或其他协处理器做语音或视觉跟随本质上也是先把目标频率算好通过串口把整型频率字传过来主控只做一次更新。5.2 载波比保持与低频电压补偿变频运行时表点数固定基波降低意味着每个周期占的点数变多载波比升高波形谐波更好基波升高时载波比变小谐波变差。为此可以在频率字变化时同步切换 LUT 的点数或者保持载波不变在频率接近上限时限制调制度。多数变频器下位机用固定载波比、允许基波在 25Hz 到 75Hz 之间连续变化配合 V/F 曲线把 M 随频率抬升float vf_mod 0.1f 0.5f * f_ref / 50.0f;低频起动给 0.1 的励磁偏置避免电机低速抖动频率越高 M 越大。这样改动不会让phase_inc计算变复杂且 SPWM 程序本身的表保持固定。5.3 验证调制度和输出幅值的最快方法上电后先不要直接带电机在母线上接一个固定直流电压 Vdc比如 24V。用示波器测量滤波后的正弦波峰值理论上峰值应为Vdc / 2 * M再乘 0.707 得有效值。M0.8、Vdc24V 时正弦有效值约 6.8V这个数值比估算电阻分压靠得住。如果没有示波器就把spwm_lut里前 10 个值用串口打印出来和 Python 生成的表逐项对比能快速确认调制度和表生成公式是否一致。本文还有配套的精品资源点击获取