简介:基于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))。代入PSC=1、ARR=3599,得到72MHz / 7200 = 10kHz。注意这是中心对齐模式下一个完整三角波周期对应的PWM频率,更新中断频率也正好是10kHz。
死区时间的计算:上面代码里TIM_DeadTime = 144,这个值的物理时间为144 / (72MHz / (PSC+1)) = 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,当m=1时输出峰值等于母线电压。在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到10mH,C取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.5,Ki给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软件中断开销之间找一个平衡点。
本文还有配套的精品资源,点击获取