ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于STM32的SPWM逆变器调压调频设计与实现

2026/9/8 2:03:29 拓冰建站 浏览量
基于STM32的SPWM逆变器调压调频设计与实现 简介基于STM32实现的单相逆变工程代码面向嵌入式与电力电子领域学习者适合需要掌握全桥逆变电路、SPWM生成以及V/F调压调频控制策略的开发者。压缩包共270个文件约9.48MB以C语言源码46个c、47个h为核心包含主控程序、PWM生成模块与V/F控制算法同时提供编译生成的axf、hex固件、工程配置文件以及PDF、DOCX说明文档便于直接查阅与二次开发。已有4266人学习下载可作为课程设计、电子竞赛或工业变频器入门实践的参考资料。通过源码可理解定时器配置、占空比调节、故障保护等关键实现配合原理图、调试输出文件与文档能快速搭建实验环境并验证逆变输出效果适合有一定STM32基础、希望深入电力电子控制实践的读者。 先说明一下这个项目我实际调过几版F103、F407都跑过从最初只输出一个固定50Hz的SPWM波到后面加上调压调频的接口中间踩了不少坑。所以这篇文章不会只贴一段代码就完事我会把整个方案从选型到调试的关键节点都拆开讲特别是“调压调频”这块最容易搞糊涂的相位累加器和调制比控制争取让拿到代码的人能真正改得动、调得好。这个项目适合三类人看一是做毕设想选逆变器方向的在校生二是工作中要做一个可调频调压的电源模块的嵌入式工程师三是想深入理解SPWM生成原理、不想停留在“抄一份代码跑通”阶段的单片机爱好者。如果你只是想要一个开箱即用的工程本文也有完整可落地的核心代码可以直接搬。1. 方案选型与整体设计思路1.1 为什么选STM32F103来干这件事单相逆变的本质就是把直流电变成频率和幅值都可控的交流电。实现方式有很多纯硬件振荡器加比较器能做DSP能做STM32也能做而且STM32是这里面性价比最高、上手门槛最低的。STM32F103之所以适合核心在于它的高级定时器TIM1和TIM8带互补PWM输出和硬件死区插入。互补输出是驱动全桥逆变上半桥和下半桥必须的而硬件死区可以在两个管子切换时插入一个极短的“同时关闭”时间防止上下管直通炸管。你如果用普通定时器自己软件翻转IO来模拟互补波形且不说实时性难以保证死区这种细节一旦处理不到位上电瞬间就可能冒烟。调压调频的本质也简单调压就是改变SPWM的调制比正弦波幅值相对于载波的比值调频就是改变正弦波的频率。这两个参数都体现在正弦表和定时器中断处理里STM32在72MHz主频下用定时器更新中断逐点刷新比较寄存器完全可以跑出20kHz以内的SPWM载波这个指标覆盖绝大部分单相逆变需求。1.2 主电路拓扑选择和驱动方案单相逆变最常见的拓扑是H全桥由四个开关管组成STM32输出两路互补PWM驱动左右两个桥臂。选择全桥而不是半桥是因为同样的母线电压下全桥输出幅值多一倍像做220V市电输出时全桥对母线电压的要求明显更低。功率器件可以选择MOS管或IGBT。做小功率几百瓦以内用MOS管开关频率可以拉到20kHz以上噪声小大功率上千瓦才考虑IGBT。驱动电路我习惯用IR2104这种自举半桥驱动芯片一片驱动一个桥臂电路简单自举电容取值1uF到10uF之间都能工作。注意自举二极管一定要用快恢复二极管否则高频下自举电容充不上电上管会间歇性不导通。供电方面低压调试阶段用可调直流电源给母线供电比如12V或24V先验证波形没问题了再升到目标电压。千万不要一开始就上220V整流后的310V直流电来调试这一步省不得。1.3 整体软件框架和我为什么这样分文件软件上我的代码分三层应用层负责调压调频接口和逻辑控制中间层是SPWM生成核心底层是定时器初始化和中断服务。代码写在main.c、spwm.c、sin_table.c三个文件里这样你在其他项目上复用移植时只需要改底层的定时器初始化正弦表和相控逻辑基本不用动。核心的SPWM生成思路是这样定时器配置成中心对齐模式产生三角载波正弦表里存一个周期的正弦波采样值定时器更新中断里每次取一个表值去刷新比较寄存器。通道比较输出高电平的宽度与正弦值成正比就得到了等幅不等宽的SPWM脉冲序列经过LC低通滤波之后就能还原出平滑的正弦波。2. 定时器配置与SPWM生成的底层细节2.1 中心对齐模式下的载波频率计算我用的是高级定时器TIM1的中心对齐模式这个模式计数器先向上计数到ARR再向下计数回0输出的PWM波形是两边对称的对称的SPWM谐波含量比边沿对齐模式更少滤波更容易。配置代码如下void TIM1_PWM_Init(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_7; // CH1和CH1N GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseInitStructure.TIM_Period arr; // 3599 TIM_TimeBaseInitStructure.TIM_Prescaler psc; // 1 TIM_TimeBaseInitStructure.TIM_ClockDivision 0; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_CenterAligned1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseInitStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 144; // 由DTG计算得到约4us TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, TIM_BDTRInitStructure); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 使能MOE主输出 }载波频率的计算公式是f_pwm 72MHz / ((PSC 1) * (ARR 1))。代入PSC1、ARR3599得到72MHz / 7200 10kHz。注意这是中心对齐模式下一个完整三角波周期对应的PWM频率更新中断频率也正好是10kHz。死区时间的计算上面代码里TIM_DeadTime 144这个值的物理时间为144 / (72MHz / (PSC1)) 144 / 36MHz 4us。一般MOS管驱动死区取0.5us到2us就够我当时为了调试安全裕量留得大了点实际稳定后改小到1us左右波形失真更小。2.2 正弦表的生成方式和查表原理正弦表我习惯在初始化时程序生成而不是手写一堆常量数组。这样改表长、改幅值都很灵活代码也干净。生成方式如下#define TABLE_SIZE 256 #define PWM_FREQ 10000 // 载波频率10kHz uint16_t sin_table[TABLE_SIZE]; uint32_t phase_acc 0; uint32_t phase_step 0; uint16_t modulation_index 3000; // 对应输出比较值的幅值上限 void sin_table_init(void) { for (int i 0; i TABLE_SIZE; i) { float angle 2.0f * 3.1415926f * i / TABLE_SIZE; sin_table[i] (uint16_t)((sinf(angle) 1.0f) * 32767.5f); } }这里sin_table[i]的范围是0到65535对应占空比0%到100%。在中断里取到一个采样值之后要跟调制比相乘再映射到定时器的ARR上。为什么要这样设计因为正弦表的采样值代表的是“正弦波的瞬时形状”它的峰值是固定的65535而调制比决定了实际输出的最大占空比两者相乘就把正弦波峰值压到你想要的幅度。中断服务函数里的核心代码是相位累加器查表这部分后面第3章会展开。这里先说明一个容易出错的地方F103高级定时器使能后还要再调用一次TIM_CtrlPWMOutputs来打开主输出否则示波器上什么都看不到这个坑很多新手会踩。3. 调压调频的实现原理与代码详解3.1 调频的核心相位累加器调频如果用“改变定时器ARR改变载波频率再配合固定点数查表”这种笨办法虽然能实现但有两个致命问题一是修改ARR会影响PWM频率和滤波参数载波频率一变后面滤波器和死区都得跟着重算二是频率切换时波形相位会突变输出可能瞬间出现很大的畸变毛刺。所以我用的是相位累加器方案原理可以这样理解有一个32位的相位累加器phase_acc它不断累加一个步进值phase_step溢出后自动回绕。把phase_acc的高8位作为正弦表的索引相当于在一个正弦表上做“变速播放”累加速度越快波形频率越高。输出频率和步进的关系是f_out phase_step * f_pwm / 2^32反推得到相位步进的计算公式phase_step f_out * 2^32 / f_pwm用C实现就是void set_output_frequency(uint16_t freq_hz) { phase_step (uint32_t)((uint64_t)freq_hz * 0xFFFFFFFFu / PWM_FREQ); }用32位累加器的好处是频率分辨率极高PWM频率10kHz下最小频率分辨率只有10k / 4.29亿 ≈ 0.000023Hz你要输出50Hz输出50.5Hz这种小数频率也完全没有压力而且频率切换只是改一个步进值相位不会中断。查表索引的取法也要注意中断里取phase_acc的高8位uint8_t index (uint8_t)(phase_acc 24); uint16_t sample sin_table[index];因为我用的是256点表高8位正好覆盖0到255。如果你把表改成512点就取高9位改成128点就取高7位逻辑是一样的。3.2 调压的实现调制比控制和软启动调压看起来简单改变定时器比较值的大小就行但实际工程里面有细节。电压调节的公式是输出交流电压峰值 母线电压 × 调制比调制比m的范围是0到1当m1时输出峰值等于母线电压。在STM32里调制比不直接用浮点数而是用一个定点数modulation_index来表示它的物理意义是“当前期望的CCR最大值”。void set_output_voltage_percent(uint16_t percent) { if (percent 95) percent 95; // modulation_index最大值取ARR * 0.95留出一点裕量防止过调制 modulation_index (uint16_t)((uint32_t)3599 * percent / 100); }为什么调制比上限只给95%而不是100%因为SPWM在调制比接近1时会出现过调制正弦波峰值处的一段时间内占空比饱和在100%输出波形顶部会被削平谐波增大。另外死区的存在也让最大有效占空比达不到100%留5%裕量是实际调出来的经验值。还有一个关键细节是软启动。如果开机时直接让调制比从0跳到目标值输出变压器或滤波电感上会产生很大的冲击电流严重时可能触发过流保护甚至烧管子。我一般把调制比分成100步每隔一个正弦周期加一步大约200ms内从0缓升到目标值。这样输出电压是慢慢建立起来的带载冲击小很多。uint16_t soft_start_target 0; uint16_t soft_start_current 0; void set_output_voltage_with_softstart(uint16_t target_percent) { soft_start_target target_percent; // 在main循环中每隔若干ms调用soft_start_tick(); } void soft_start_tick(void) { if (soft_start_current soft_start_target) { soft_start_current; set_output_voltage_percent(soft_start_current); } }3.3 中断服务函数的完整代码下面给出中断服务函数的完整实现。定时器更新中断频率10kHz在这个中断里查一次正弦表刷新一次比较寄存器。注意这是基础版实际项目里还可以放电压电流采样、过流保护标志判断等。void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); uint8_t index (uint8_t)(phase_acc 24); uint16_t sample sin_table[index]; uint32_t ccr (uint32_t)sample * modulation_index / 65535u; // 限制比较值不超过ARR也留出最小死区时间 if (ccr 3400) ccr 3400; TIM_SetCompare1(TIM1, ccr); phase_acc phase_step; } }这里我把CCR上限设成3400而不是ARR的3599两个目的一是给死区留出空间防止上下桥臂实际导通宽度过满二是避免调制比给满时输出波形顶部被削平。这个值你可以根据实际死区大小微调但一定不要去掉。整个代码跑起来以后调频调压就变成了两个简单的函数调用set_output_frequency(50); // 输出50Hz set_output_voltage_percent(85); // 调制比85%3.4 调压调频联动的一个工程经验实际做变频电源或者电机驱动类的逆变器时电压和频率不是独立调节的一般要遵循恒压频比V/F曲线。低频时如果还输出和工频一样的电压电机铁芯会饱和电流飙高发热严重。所以频率降到30Hz时电压也要跟着按比例降到60%附近。这个逻辑实现起来很简单在应用层加一个判断就行void set_vf_output(uint16_t freq_hz) { set_output_frequency(freq_hz); if (freq_hz 50) { // 恒压频比控制V/F 1 set_output_voltage_with_softstart(freq_hz * 2); } else { set_output_voltage_with_softstart(100); } }如果只做普通的稳压电源输出不需要V/F联动固定50Hz下独立调电压就行。这个经验提出来是想说明同样一套SPWM核心应用层的策略可以灵活变化这也就是代码框架分层设计的好处。4. 硬件调试、常见问题与实战心得4.1 调试顺序和第一个波形怎么抓第一次调试千万不要直接上高压。我的习惯是先用12V直流电源给母线供电示波器探头接在LC滤波后的输出端先不接负载看空载波形。上电前检查三件事第一测一下驱动板供电是否正常IR2104的VCC一般在10V到20V之间我用12V第二确认PWM输出引脚用示波器抓到的波形是互补且带有死区的用示波器的脉宽测量功能看一眼死区时间是否在us级别第三检查LC滤波器的电感电容值L一般取1mH到10mHC取10uF到100uF之间截止频率设计在几百Hz到1kHz。第一次上电建议用示波器的单次触发模式抓波形如果程序有问题可以在炸管之前看到异常波形及时断电。确认空载波形正常后再尝试加载负载不要直接用电机这种感性负载先用电阻负载或者白炽灯等波形稳定了再扩展。4.2 ST-Link连接失败的快速排查调试STM32最崩溃的报错是“error: no stm32 target found! if your product embeds debug authentication”明明代码没问题就是连不上仿真器。我遇过的原因主要有四种。第一种是目标板供电问题这个最常见。如果板子用的是稳压芯片供电电流不够或者接线太细导致电压跌落SWD接口就工作不稳定。排查方法是单独给板子供一路5V或3.3V确认VDD引脚量到稳定电压。第二种是SWDIO和SWCLK线序接反或者接触不良。杜邦线调试的连接线越短越好超过20cm就可能有波形畸变特别是SWCLK频率默认比较高时。实在要长线可以把SWCLK频率降下来。第三种是芯片被读保护锁住了。程序里不小心使能了读保护或者之前烧录时发生过异常芯片的调试端口会被锁死。解决方法是把BOOT0拉高让芯片从系统存储器启动然后连接ST-Link在ST-Link Utility里执行全芯片擦除擦完恢复BOOT0再重新连接。第四种是GPIO被复用成其他功能导致调试口失效。如果在代码里把PA13、PA14这两个SWD引脚重新映射成普通GPIO了程序一跑起来调试口就断开。解决方法是按住复位键在IDE里点击连接在连接成功的瞬间松开复位或者用镊子把BOOT0短到3.3V再上电绕过用户程序直接连调试器。4.3 输出波形畸变的三种典型情况波形平顶削峰也就是正弦波峰值处变成平的一截多半是调制比超过了线性区或者CCR限幅值设得太小。先看自己的调制比是不是给到了100%如果是降回90%以内如果调制比不高但依然平顶查一下母线电压是不是跌落得厉害满载时电解电容容量不够会导致母线电压被拉低。输出电压只出正半周或者只出负半周波形像半波整流这个基本可以断定是全桥有一路PWM没有正常工作。用示波器分别看两个桥臂的PWM输出重点检查是不是有一路PWM的CCR在某个时间段内一直是0或者对应的通道输出引脚因为初始化遗漏没有配置成复用推挽。输出波形上有明显的高频毛刺和振铃这个在开关电源里几乎无法避免重点是判断毛刺大小是否在可接受范围内。如果是几伏以内的尖峰可以通过在输出端加RC吸收电路、改善PCB布局来抑制如果是几十伏的尖峰多半是LC滤波参数选得不合理或者驱动电路的走线环路太大引入了过多寄生电感要从Layout上解决。4.4 死区时间和直通炸管的教训直通是逆变器最危险的故障一旦上下桥臂同时导通母线相当于短路瞬间大电流会把MOS管炸掉或者驱动芯片烧掉。我调试早期就烧过一片驱动芯片原因就是初始化时BDTR寄存器没有配置正确MOE主输出没有打开但此时PWM电平不受控制硬件复位期间输出引脚默认状态不受驱动限制外电路在半桥电源正常的情况下直接导通。这里给新手的忠告是调试全桥逆变功率部分和控制部分最好分开供电而且先给控制部分上电、确认PWM波形正确之后再给母线功率部分上电。另外MOS管栅极到驱动芯片之间的串联电阻不能省一般在10到22欧姆这个电阻能有效抑制栅极振铃。补充一点用示波器看死区时间时要把两个探头同时接到互补PWM上用Math功能做A-B运算观察两路同时为高或者同时为低的时间间隔。死区时间不是越大越好太大占空比损失严重、波形畸变太小起不到保护作用。5. 从代码到完整作品还差哪些模块5.1 电压电流采样与闭环控制的扩展思路目前这套代码是开环控制给多少调制比就输出多少电压如果负载变化或母线电压波动输出电压会跟着波动。要稳住输出电压就需要闭环最简单的方案是用互感器或者分压电阻采样输出电压经过运放和ADC送进STM32做一个数字PI调节器实时微调调制比。采样点的时序很关键。如果ADC采样时刻落在PWM开关动作附近就会采到巨大的开关噪声算出来的电压值根本没法用。我建议用定时器触发的注入组采样把采样时刻放在PWM周期的中间点也就是三角载波的波峰处此时开关管处于稳定的导通或关断状态采样值最干净。这个细节是闭环PI能否调稳的关键。ADC采样到输出电压有效值后和设定值做差经过PI调节器输出调制比修正量。PI参数一般先给一个较小的Kp比如0.5Ki给0.01然后根据响应微调。闭环调好后输出电压在负载从空载到满载变化时波动能控制在百分之几以内。5.2 保护功能的实用建议做电源类项目保护功能不是可选项是必选项。我在这套代码上至少会加三种保护过流保护、母线过压保护和过热保护。过流保护用采样电阻或者电流互感器比较器输出直接接到TIM1的刹车输入引脚TIM1_BKIN。这个引脚的作用是一旦检测到故障硬件立即封锁所有PWM输出不需要软件参与响应时间在微秒级别。软件里也可以开一个PWM周期中断每次中断里检查电流采样值是否超过阈值超过就软件拉低输出并置位故障标志。这里有一个细节硬件保护和软件保护要配合使用。硬件保护负责急停兜底软件保护负责记录故障状态、控制故障指示灯和恢复逻辑。如果你只靠软件在中断里判断过流万一中断被更高优先级任务堵住反应不及时就是炸管的下场。5.3 项目后续还能往哪些方向扩展这个框架调通以后后续扩展空间很大。比如把单相全桥扩展成单相H桥逆变配合SPWM调制加双极性或单极性调制策略也可以把SPWM输出频率推到20kHz改成输出400Hz中频电源给航空设备或特殊测试设备供电还可以加一个LCM显示模块和一个旋转编码器做一个脱机的手动调压调频电源面板。如果想往三相方向走方案是换用F407这类有更多定时器的芯片用三路SPWM分别驱动三相桥臂相位差120度核心逻辑还是这套查表和相位累加器。掌握了单相这一套三相无非是查表三次、中断里多算两路的事。我最早做这个项目的时候只想跑一个固定50Hz的波形交差但后来发现把调压调频的接口留好整个项目的复用价值完全不一样。现在我再做一个变频电源或者UPS基本是改改参数就能用不用从头写。最后分享一个小技巧调试调频功能时不要只盯着一路PWM看同时测一下几个不同频率下的输出波形50Hz、60Hz、100Hz甚至400Hz都看一眼。如果高频波形在一个周期内点数变少导致失真不用急着改硬件把正弦表的点数从128改成256或者把PWM载波频率从10kHz提高到20kHz往往就能解决。这个选择本质上是在载波频率、正弦表大小和MCU软件中断开销之间找一个平衡点。本文还有配套的精品资源点击获取