简介:围绕STM32实现三相SPWM波输出的工程资料包,面向嵌入式开发和电机控制方向学习者,适用于逆变器、交流电机驱动等电能转换场景。资料系统梳理了SPWM的关键环节:高级定时器PWM模式配置、死区时间设定、三相相位互差120度的实现、三角载波与正弦参考波的比较逻辑,并涉及中断处理与参数实时更新,便于理解从原理到工程落地的完整链路。压缩包共174个文件,以C语言源文件(.c/.h)为主,同时包含编译中间文件(.o/.d)、Keil工程配置与链接脚本,以及可烧录的hex文件,总大小约2.81MB,目录结构完整,方便快速定位到核心代码与生成文件。目前已有564人学习下载。通过该资源,使用者可获得一个可运行的三相SPWM输出工程范例,结合代码与编译信息可快速对照学习定时器初始化、占空比调节等关键配置;对于需要开展变频调速、并网逆变器等项目开发的工程师,也是一份有参考价值的二次开发底座。
1. 三相SPWM不是“算出来”的,是配合定时器“排出来”的
三相SPWM的核心不是sin()函数,而是定时器。A、B、C三相各需要一个互差120°的正弦调制波,三路PWM共用同一个三角载波,STM32的高级定时器TIM1恰好能输出三对互补PWM,直接驱动三相全桥的六个开关管。大多数从网上下载的“基于STM32的三相SPWM波输出.rar”工程包,代码逻辑并不复杂,真正容易翻车的是载波频率、死区时间、更新中断频率和引脚复用这几处定时器参数。与其逐行读代码,不如先把定时器行为吃透,再回头对照工程里的寄存器配置。这篇文章按载波参数、工程结构、关键代码、双极性驱动、验证移植的顺序展开,让新手半小时跑起来,也让熟手能定位波形畸变的根源。
2. 三相SPWM的载波频率与STM32定时器参数:先设对ARR和PSC
2.1 载波频率决定波形质量,10kHz是默认起点
载波频率就是PWM频率,SPWM本质上是用高频三角载波去切割正弦调制波。STM32用定时器计数来形成三角波,计数溢出频率由预分频器PSC和自动重载值ARR共同决定。中心对齐模式下,计数器从0加到ARR再从ARR减到0,一个完整PWM周期对应两次计数,所以PWM频率公式是:
f_pwm = f_clk / (PSC + 1) / (2 * (ARR + 1))系统时钟72MHz时,目标10kHz载波可以取PSC=71、ARR=99。这里有个经验值:载波频率越高,电流波形越平滑,但开关管损耗线性上升。三相异步电机调速常用10kHz到16kHz,IGBT模块用8kHz到10kHz,MOSFET方案可以上20kHz。做毕业设计或初学者验证,10kHz最稳妥,因为一个50Hz正弦周期内正好有200个载波周期,波形阶梯感小,示波器上观察也直观。
| 目标载波频率 | 系统时钟 | PSC | ARR | 每个工频周期载波数 |
|---|---|---|---|---|
| 8kHz | 72MHz | 89 | 49 | 160 |
| 10kHz | 72MHz | 71 | 49 | 200 |
| 12kHz | 72MHz | 59 | 49 | 240 |
| 16kHz | 72MHz | 44 | 49 | 320 |
调制深度m的定义是正弦波峰值与载波幅值之比,在定时器里体现为CCR的最大值与ARR的比值。m接近1.0时输出线电压利用率最高,但会进入过调制区,产生明显削顶。工程上一般把最大CCR限制在0.9倍ARR左右,留出死区时间和母线电压波动的余量。
2.2 中心对齐模式与PWM1模式:三角载波怎么从定时器来
STM32通用定时器和高级定时器都支持中心对齐模式。以标准外设库为例,TIM_TimeBaseInitTypeDef里TIM_CounterMode选TIM_CounterMode_CenterAligned1或CenterAligned2,对应CMS位01/10。需要注意,CMS位只控制更新事件在向上计数还是向下计数时产生,不影响比较输出,中心对齐PWM波形的对称性始终存在。
输出极性上,SPWM工程最常用PWM1模式,也就是计数值小于CCR时输出有效电平。配合TIM_OCPolarity_High,CCR值越大,高电平脉宽越宽,占空比与CCR值单调对应。如果选PWM2模式,整个逻辑反向,初学者套公式时会搞混,建议固定用PWM1。
提示:中心对齐模式下,若CMS=11,向上和向下计数都会产生更新事件,中断频率是载波频率的两倍。要让中断频率恰好等于载波频率,用CMS=01或10即可,具体见3.3节。
2.3 死区时间与互补输出:不设死区就是炸管
三相全桥同一桥臂上下管不能同时导通,否则电源直通。STM32高级定时器在互补输出时可以在两路信号之间插入死区时间,这个值写在TIM1的BDTR寄存器里。死区时间太小会直通,太大则波形畸变明显,输出相电压在过零点附近出现平台。
死区时间估算看功率器件的关断延迟。IGBT模块一般2us到3us,低压MOSFET驱动电路0.5us到1us就够。以STM32F103在72MHz系统时钟下为例,DeadTime字段单位是DTS周期(约13.9ns),写入144对应2us。标准库初始化代码如下:
TIM_BDTRInitTypeDef BDTR; BDTR.OSSRState = TIM_OSSRState_Enable; BDTR.OSSIState = TIM_OSSIState_Disable; BDTR.LOCKLevel = TIM_LOCKLevel_OFF; BDTR.DeadTime = 144; // 144 * (1/72MHz) = 2us BDTR.Break = TIM_Break_Disable; BDTR.BreakPolarity = TIM_BreakPolarity_High; BDTR.AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &BDTR);这段代码里,OSSRState决定主输出在空闲状态时是否仍保持,通常使能;OSSIState关闭,让空闲时输出高阻;Break禁用,不用刹车引脚。这里最关键的DeadTime值,换到不同主频的芯片时必须重新算,不能直接照抄。
提示:凡是用了TIM1或TIM8的任何输出,都必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)打开主输出MOE位,否则IO口上一路PWM都不会有。
3. 解析.rar工程包:文件结构、正弦表与更新中断
3.1 先确认芯片型号与固件库,再找main入口
网上流传的“基于STM32的三相SPWM波输出.rar”通常包含Keil工程、标准外设库或者HAL库源码、说明文档。打开工程的第一件事不是看代码,而是确认芯片型号。同样一份工程,STM32F103ZET6和STM32F103C8T6的TIM1通道引脚一致,但Flash容量和启动文件不同;如果是F407的工程,高级定时器映射引脚有变化,直接烧到F103上不会有输出。Keil里芯片型号不匹配时,Options for Target里改型号之后,还要重新选择正确的启动文件,并核对系统时钟初始化函数里的外部晶振频率。
固件库风格也要先分清。标准外设库的初始化代码长这样:GPIO_InitTypeDef、TIM_TimeBaseInitTypeDef配合RCC_APB2PeriphClockCmd。HAL库则完全不同,MX_GPIO_Init和MX_TIM1_Init都由CubeMX生成,回调函数HAL_TIM_PWM_PulseFinishedCallback刷新占空比。如果原来工程是标准库写的,没必要强行改成HAL库,维持原风格反而出错少。
Keil打开工程后依次检查三处:Target选项里的芯片型号、C/C++选项里的Define宏(STM32F10X_HD还是STM32F10X_MD)、工程文件树里的启动文件。这三处不一致,编译能过但运行诡异,比如定时器不进中断。
3.2 生成三相正弦表:120°相位差在表索引里实现
查表法是三相SPWM最常见的实现方式。载波10kHz、调制波50Hz时,一个工频周期正好200个采样点。A、B、C三相的差异不是算出来的,是同一张正弦表错开索引位置得到的。相位差120°对应表长的三分之一,所以B相起点在表的第67个点,C相在133个点,取整后实际相位差略有偏差,但对电机运行影响很小。
生成正弦表可以在PC上用Python完成:
import math TABLE_SIZE = 200 # 一个工频周期的采样点数,对应10kHz载波 ARR = 99 # 定时器自动重载值 MAX_CCR = int(ARR * 0.9) # 调制度0.9,防止过调制削顶 phases = [0, 2*math.pi/3, 4*math.pi/3] for phase in phases: table = [] for i in range(TABLE_SIZE): duty = 0.5 + 0.5 * math.sin(2*math.pi*i/TABLE_SIZE + phase) ccr = int(duty * MAX_CCR) table.append(ccr) print(", ".join(map(str, table)))这段脚本输出的数组直接粘贴到C源文件里即可。MAX_CCR按0.9倍ARR取值,防止正弦波峰值处占空比达到100%,否则出现过调制,输出波形顶部被削平。假如死区时间较大,还要把MAX_CCR再调小一点,因为死区占用的是有效脉宽。
有了表之后,工程里一般会建三个数组或一个二维数组存放三相数据。二维数组在一级缓存较小的MCU上会多占一点RAM,但代码更清晰:
const uint16_t spwm_table[3][TABLE_SIZE] = { { 44, 46, 48, ... }, // A相 { 67, 69, 71, ... }, // B相,索引起点偏移 { 89, 90, 92, ... } // C相 };3.3 在更新中断里刷新CCR:中断频率与查表方式的取舍
查表之后的问题是什么时候把表中的值写入比较寄存器。常用做法是在定时器更新中断里,按顺序递增索引,取出三相CCR值,调用TIM_SetCompare1/2/3。下面是这个逻辑的核心:
volatile uint16_t pwm_index = 0; void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); TIM_SetCompare1(TIM1, spwm_table[0][pwm_index]); TIM_SetCompare2(TIM1, spwm_table[1][pwm_index]); TIM_SetCompare3(TIM1, spwm_table[2][pwm_index]); pwm_index++; if (pwm_index >= 200) pwm_index = 0; } }注意中心对齐模式CMS=01时更新事件只在向上计数结束产生,中断频率等于载波频率10kHz,每100us进入一次,三次数组访问耗时极短,对CPU占用率几乎无感。若把CMS配置成了11,更新事件在上下计数都产生,中断频率变成20kHz,必须在TIM_TimeBaseInit之后用TIM1->RCR = 1,让每两次更新产生一次中断,频率回到10kHz。
中断刷新方式的极限在于:如果载波频率继续提高,比如电机控制用到20kHz,同时系统还在跑编码器采样和通信协议,中断内要做的事情变多,CPU会吃紧。此时可以改用DMA:把正弦表放到内存,用定时器更新事件触发DMA搬运到TIM1的CCR寄存器,三个通道可以由一条DMA流依次完成。不过DMA方式对表和重载值的排列有要求,中小型项目直接用中断查表即可。
固件库工程里还有一个细节,TIM1是高级定时器,NVIC配置要同时使能TIM1_UP_IRQn。有些工程里输出正常但频率不对,多半是中断没开,或者更新标志被其它代码清掉了。
4. 三相互补输出接线与双极性SPWM的驱动差异
4.1 三相全桥六个管脚:CH1/CH1N/CH2/CH2N/CH3/CH3N分配
三相SPWM最终要驱动三相全桥逆变器,一共六个开关管。STM32F103的TIM1刚好提供三路输出和三路互补输出,对应关系如下:
| 定时器通道 | STM32F103引脚 | 连接对象 |
|---|---|---|
| TIM1_CH1 | PA8 | A相上桥臂 |
| TIM1_CH1N | PB13 | A相下桥臂 |
| TIM1_CH2 | PA9 | B相上桥臂 |
| TIM1_CH2N | PB14 | B相下桥臂 |
| TIM1_CH3 | PA10 | C相上桥臂 |
| TIM1_CH3N | PB15 | C相下桥臂 |
PCB上通常是上下桥臂分别接电源正和电源负,电机三相绕组接在两个桥臂中点。有刷电机或无刷电机的驱动板走线不同,但STM32这一端永远是六路PWM输出。接线之前先看原理图确认驱动芯片输入的信号极性,有的驱动电路要求高电平关断,需要把PWM极性反过来。
GPIO配置里要注意复用推挽和重映射。F103上TIM1默认使用PA8/PA9/PA10和PB13/14/15,部分板子做了重映射,需要检查AFIO的重映射寄存器。GPIO模式设成AF_PP(复用推挽)之后,IO方向交给定时器,代码里不要再调用GPIO_SetBits操作这些引脚。
4.2 为什么三相全桥只能用双极性SPWM
单极性SPWM常见于单相逆变器,正半周只有一组开关在动作,另一组保持导通,开关损耗低,但输出含有较大的低频分量。三相全桥不能这样做,因为三个桥臂必须共用同一条母线,各相电位互相牵制,每一相都需要在正负母线之间连续切换,才能在电机绕组上得到对称的三相线电压。所以三相SPWM实际运行的必然是双极性方式。
双极性SPWM的每个PWM周期里,上管和下管各动作一次,输出电压在正负母线电压之间来回翻转。载波频率越高,输出电流波形越接近正弦,但开关损耗和死区影响同步变大。死区导致输出电压误差与负载电流方向相关:电流为正时,误差使输出电压减小;电流为负时,误差使输出电压增大。低速工况下这种误差会引起电流过零点畸变,电机噪音明显。想改善就得做死区补偿:实时采样相电流方向,在CCR值上叠加或减去死区对应的计数值。STM32内部没有专门的死区补偿硬件,代码里要自己判断电流过零,这也是SPWM项目进阶的常见方向。
4.3 别忘了MOE主输出使能:高级定时器和通用定时器的差别
用TIM3写PWM例程的时候,初始化完直接就有波形。换成TIM1,很多新手会在这一步卡住:代码一模一样,示波器就是没有输出。原因是TIM1/TIM8这类高级定时器多了一个主输出使能位MOE,位于BDTR寄存器中,系统复位后默认关闭。标准库中必须调用:
TIM_CtrlPWMOutputs(TIM1, ENABLE);这一行放在TIM_BDTRConfig之后、使能定时器之前。工程中如果找不到这一行,PWM是永远不会出来的。除此之外,TIM1初始化顺序建议为:先开RCC时钟,再配GPIO复用,然后初始化时基、输出比较、互补输出和死区,最后使能CCx和CCxN输出通道。顺序颠倒会引起寄存器写入失效,比如死区还没配置就先开了主输出,死区值可能会被覆盖。
提示:MOE位还受刹车输入影响。工程里Break没有使能时,对应引脚不能接地,否则MOE被硬件清零,PWM瞬间消失且不会自动恢复。
顺带提一句,SPWM是变频调速的基础,但电压利用率只有约86%。同样一个三相全桥,想提高直流母线利用率,下一步值得研究SVPWM。SVPWM通过八个基本电压矢量合成期望电压矢量,代码复杂度高一些,但波形谐波更小,是商业变频器的主流方案。这里先用SPWM把定时器、正弦表、死区这些底子打好,后续切SVPWM时复用TIM1的配置框架即可。
5. 三相SPWM怎么验证,以及换板子时改哪些参数
5.1 示波器三步验证:频率、削顶、相序
写完整段代码,下载到板子之后,建议按下面顺序验证波形。
第一步,看单路PWM频率。探头接PA8与GND,观察PWM频率是否等于设计载波频率10kHz。如果频率偏离,检查PSC和ARR;如果完全没有波形,先查MOE和互补输出使能。
第二步,看调制度是否削顶。把探头改为交流耦合,观察PA8输出PWM脉冲宽度是否周期性变化。把时基拉到20ms左右,看到正弦包络后测量包络峰值,若顶部明显削平,说明MAX_CCR取值过大,或母线电压不足以支撑当前调制深度。减小MAX_CCR系数即可。
第三步,看三相相序。A、B、C三路分别接示波器CH1、CH2、CH3,触发CH1,测量CH2与CH3相对CH1的相位差,应约为120°和240°。如果观察到的相序反了,把B相和C相的索引起点交换即可。
没有示波器时,可以用Proteus仿真观察六路PWM,不过Proteus对TIM1死区和更新中断的时序仿真与实际芯片有差异,只能验证逻辑,不能验证死区波形。另一个低价方案是用逻辑分析仪,采样率在50MHz以上的型号能看清死区时间是否插入正确。用逻辑分析仪看PB13与PA8,正常情况应看到两路波形互补且中间有一小段时间都是低电平,这段低电平就是死区。
5.2 换芯片、改载波频率时,只需动这几个参数
工程换到另一块STM32板子上,不需要重写全部代码,但有四个地方必须重新计算。
第一个是系统时钟。外部晶振8MHz或25MHz会改变PLL配置,进而影响APB2时钟和TIM1时钟,PSC和ARR都依赖这个时钟数值。先用RCC_GetClocksFreq读取TIM1时钟频率,再反推PSC和ARR。
第二个是死区时间。换芯片后DTS可能不同,DeadTime字段按新DTS重新计算。例如原来72MHz下填144得到2us,换成84MHz的F407之后,同一字段对应的时间是1.7us左右,需要按84MHz重新算。
第三个是正弦表点数。把载波频率从10kHz改成20kHz、调制波频率保持50Hz时,一个周期采样点数变成400。此时要重新生成数组,并且把更新中断里的TABLE_SIZE同步改掉,否则索引溢出导致输出波形周期性错乱。
第四个是引脚复用重映射。不同封装或不同板卡对TIM1_CH1N等引脚的引出位置不同,GPIO初始化只改引脚号还不行,部分型号还需要配置AFIO重映射寄存器。查芯片数据手册的Alternate Function Mapping表,确认所有六路输出实际复用位置后再接线。
调试工具推荐用STMStudio或Keil的Variable窗口监视pwm_index变量的变化范围。正常情况应从0轮转到199后清零,若观察到跳变或回绕异常,优先查中断里是否有其它代码修改了该变量。控制类项目里,三相PWM输出往往要和ADC采样联动,比如逐波限流需要每个载波周期采样一次电流,此时建议把采样触发信号取自TIM1的更新事件或TRGO引脚,确保采样点落在PWM中心,而不是在主中断里用软件延时采样,那样采到的电流时刻会随负载波动,电流环PI参数很难调稳。
本文还有配套的精品资源,点击获取