STM32 SPWM程序实战:正弦表、DMA与变频调制设计
2026/9/16 23:59:47 网站建设 项目流程

简介:面向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 项目里最常见的做法,原因很简单:中断里不跑浮点、不写 CCR,CPU 只负责改表或者改步长。

2.2 查表、实时计算与 DDS 的选型表

生成方式CPU 占用内存占用频率调整难度适用场景
定时器中断实时计算最高,随载波线性增加最小直接改公式里的频率参数低频正弦、教学验证
PWM + 查表 + DMA极低,中断几乎为空小,200 点表约 400 字节改表或改步长,需重载 LUT10kHz 以上载波的 SPWM
DDS 相位累加低,每次更新做一次加法中,表本身不变,累加器几个变量频率分辨率高,连续可调变频器、可调正弦电源

表中第三条其实是查表法的进阶版,第 5 章会单独展开。对固定 50Hz 工频输出,直接选第二行就够了;如果后面要做电机调速,建议一开始就按 DDS 的结构预留相位累加器变量,避免最后改架构。

2.3 用 Python 生成可移植的 16 位正弦表

假设定时器向上计数模式,ARR 对应载波周期,PWM 分辨率等于 ARR+1。为了让 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:i+10]) + ",") 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 搬运 CCR:STM32 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 / ((PSC+1) * (ARR+1))。72MHz 主频下,PSC=0、ARR=7199 得到 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_Pulse=3600对应 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基波频率
10kHz0719920050Hz
20kHz0359940050Hz
25kHz0287950050Hz

三组配置的基波都是 50Hz,区别在于载波越高,LC 滤波越容易做,但 MOS 开关损耗变大。ARR 越小,每个正弦周期内的占空比台阶越粗,输出波形 THD 会变差。20kHz 以上时开关损耗明显,一般单相逆变选 10kHz 到 20kHz 即可,特高压或大功率场合再往高载波走。

4. 调制度、死区与 LC 滤波:STM32 SPWM 上电前的三个关键参数

4.1 调制度 M 的余量设置与重算表的方式

调制度 M 决定输出正弦幅值。对单极性倍频结构,输出电压基波有效值约等于Vdc / 2 * M / sqrt(2),M=0.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% 的余量,M=0.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 求 C:fc = 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截止频率 fc
1A10mH1µF约 1.6kHz
2A5mH10µF约 712Hz

LC 滤波最大的坑是空载和轻载时电压升高。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_inc=328 大约对应 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 得有效值。M=0.8、Vdc=24V 时,正弦有效值约 6.8V,这个数值比估算电阻分压靠得住。如果没有示波器,就把spwm_lut里前 10 个值用串口打印出来,和 Python 生成的表逐项对比,能快速确认调制度和表生成公式是否一致。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询