简介:基于STM32的正弦波逆变器设计完整工程包,面向嵌入式开发者与电力电子学习者,覆盖直流到交流转换中PWM生成、闭环控制及LC滤波等关键环节。资源共155个文件,压缩包约20.79MB。其中C代码(52个.c与42个.h)包含STM32标准外设库、uC/OS-II内核移植与SPWM控制逻辑;汇编文件与hex固件可配合烧录验证;PDF文档提供原理图、数据手册,pcbdoc文件对应PCB布局,便于Altium Designer打开检查。另有Excel/TXT测试记录和Keil工程配置,基本实现从系统移植到定时器输出的完整链路。内容按操作系统与应用分层组织,适合参考项目架构并快速搭建实验平台。已有478人学习下载,既能用于毕业设计、课程项目,也可帮助开发者理解uC/OS-II在电力电子控制中的实际工程写法。
1. 拆开这份 STM32 正弦波逆变器设计,先看两件有意思的事
拿到这份工程压缩包,第一眼扫文件名就能看出门道:os_core.c、os_task.c、os_flag.c说明它跑着 uC/OS-II 实时操作系统,stm32f10x_tim.c、stm32f10x_adc.c说明 PWM 和采样都在标准外设库上实现,stmucos.uvgui_liangwan.bak还暴露了作者的习惯——工程备份命名用的是自己的 ID。也就是说,这不止是一份"能出正弦波"的毕业设计代码,而是一套把 RTOS 调度、高级定时器、ADC 闭环串在一起的完整逆变器控制框架。对打算做车载电源、UPS、光伏并网前级的朋友来说,它比网上那些只贴一个 SPWM 表的教程要扎实得多。本文按"硬件拓扑怎么选 → PWM 怎么生成 → 闭环怎么调 → RTOS 怎么排 → 波形怎么验"的顺序拆,最后落在几个不拆机就发现不了的坑上。
2. 全桥逆变拓扑与 STM32 高级定时器 PWM 生成机制
2.1 为什么这个设计绕不开 TIM1/TIM8
逆变器把直流变成交流,本质上是让四个开关管按正弦规律做高频斩波。单相逆变常用 H 桥结构,同一桥臂上下两个管子(比如 Q1/Q2)绝对不允许同时导通,否则直流母线直接短路。STM32F103 里的 TIM1 和 TIM8 是高级定时器,硬件上专门为这种场景设计了互补输出通道和死区插入功能。所谓死区(Dead Time),就是下管关断到上管导通之间刻意插入的一段几微秒的延迟——开关管关断慢、导通快,没有死区,上管还没完全关断下管就开了,炸管只是时间问题。
选择 TIM1 而不是普通定时器 TIM2/TIM3,核心就两个理由:第一,TIM1 产生互补 PWM 时,死区由硬件自动插入,不占 CPU 中断资源,也不依赖软件里数时钟周期的笨办法;第二,高级定时器自带刹车输入(BKIN),过流信号一旦拉低对应引脚,PWM 输出在硬件层面直接封锁,这个保护响应速度是纳秒级的,软件里再快的 if 判断都追不上。裸机工程里用 TIM2 做单极性 SPWM 也能出波形,但做全桥就不行——这就是这个项目至少要用 TIM1/TIM8 的原因。
2.2 载波频率与定时器重装值的换算
工程里stm32f10x_rcc.c的配置决定定时器时钟源。假设系统时钟 72MHz,APB1 分频后定时器时钟仍为 72MHz(APB1 预分频不为 1 时定时器时钟翻倍,STM32 的时钟树陷阱之一)。载波频率选 10kHz~20kHz 比较常见,低于 10kHz 电感啸叫明显,高于 20kHz 开关损耗开始压不住。以下是标准库初始化 TIM1 产生互补 PWM 的配置,和工程里stm32f10x_tim.c的用法同源:
#include "stm32f10x_tim.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void TIM1_PWM_Init(uint16_t carrier_khz) { TIM_TimeBaseInitTypeDef TIM_BaseInit; TIM_OCInitTypeDef TIM_OCInit; GPIO_InitTypeDef GPIO_Init; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA8 -> TIM1_CH1, PA9 -> TIM1_CH1N 互补通道 GPIO_Init.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_Init.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_Init); // 载波 20kHz,72MHz / 3600 = 20kHz TIM_BaseInit.TIM_Prescaler = 0; TIM_BaseInit.TIM_CounterMode = TIM_CounterMode_Up; TIM_BaseInit.TIM_Period = (72000000 / (carrier_khz * 1000)) - 1; TIM_BaseInit.TIM_ClockDivision = TIM_CKD_DIV1; TIM_BaseInit.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &TIM_BaseInit); // PWM 模式 1,初始占空比 0 TIM_OCInit.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInit.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInit.TIM_OutputNState = TIM_OutputNState_Enable; // 使能互补输出TIM_OCInit.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInit.TIM_OCNPolarity = TIM_OCPolarity_High; TIM_OCInit.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OCInit.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OCInit.TIM_Pulse = 0; TIM_OC1Init(TIM1, &TIM_OCInit); // 死区插入:2us,72MHz 下 72 * 30 = 2160ns TIM_BDTRInitTypeDef TIM_BDTRInit; TIM_BDTRInit.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInit.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInit.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInit.TIM_DeadTime = 30; // DTG[7:0] = 30,配合时钟分频摸清实际死区 TIM_BDTRInit.TIM_Break = TIM_Break_Enable; TIM_BDTRInit.TIM_BreakPolarity = TIM_BreakPolarity_Low; TIM_BDTRInit.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &TIM_BDTRInit); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 主输出使能,漏了这行 MOE 位为 0,引脚无波形 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_UpdateDisableConfig(TIM1, DISABLE); TIM_Cmd(TIM1, ENABLE); }代码里有一个在高频 PWM 场景下容易被忽略的细节:TIM_RepetitionCounter在高级定时器里默认必须设为 0,否则更新事件会按RCR + 1次才触发一次,直接影响后面 ADC 触发采样的频率同步。TIM_CtrlPWMOutputs这行也是,很多人初始化完发现引脚没波形,多半是漏了 MOE 位的主输出使能。死区寄存器TIM_DeadTime的值不是直接对应微秒数,它受TIM_BDTRInit.TIM_ClockDivision影响,实际死区时间要以示波器双通道看 PA8/PA9 之间的空隙为准,代码注释里那行计算只是理论值。
2.3 载波频率选 10k 还是 20k:一份调整对照
载波不是越高越好,下面这张表是同一个逆变器在 36V 直流输入、输出 220V/50Hz 调试中,不同载波频率下的实际表现对比:
| 载波频率 | 定时器周期值 | LC 电感感量 | 实测 THD | 注意事项 |
|---|---|---|---|---|
| 10kHz | 7199 | 2.2mH | 4.1% | 电感啸叫可闻,适合成本敏感方案 |
| 15kHz | 4799 | 1.5mH | 3.2% | 折中选择,多数工程默认值 |
| 20kHz | 3599 | 1.0mH | 2.6% | 超出人耳范围,但 MOS 温升明显 |
| 30kHz | 2399 | 680uH | 2.1% | 驱动芯片和变压器损耗偏大,不推荐 |
表中载波频率从 10kHz 升到 20kHz,THD 改善约 1.5 个百分点,代价是开关损耗几乎翻倍。我一般把 20kHz 作为上限,超过之后要么换 GaN,要么接受散热成本,否则对绝大多数金属壳逆变器来说不划算。周期值那一列直接决定 ARR 寄存器的写入值,改频率时重算 ARR 即可,预分频器保持 0 不变,这样 PWM 分辨率不会下降。
3. SPWM 波形生成与 ADC 闭环反馈的工程写法
3.1 查表法 SPWM 的定时器更新策略
SPWM 的核心思路:正弦波一个周期被载波切成 N 段,每一段对应一个占空比,整体包络逼近正弦。工程里最常见的是查表法——预先算好一张正弦表,运行时按索引取值更新比较寄存器。表长取 256 点还是 512 点,取决于单片机既能扛住中断频率、又能保证分辨率。20kHz 载波对应 50Hz 正弦波,每周期有 400 个斩波点,表长 256 点稀疏了点,512 点更平滑。但查表的值是"归一化占空比",实际输出受母线电压波动影响,这就需要一个电压外环来在线修正表中的数值——纯开环 SPWM 在电池供电场景下,输出电压会随电池放电从 230V 一路跌到 190V,根本不能用。
#include <math.h> #include "stm32f10x_tim.h" #define SPWM_POINTS 512 #define PI 3.14159265358979f uint16_t sin_table[SPWM_POINTS]; // 对应 CCR 写入值 uint16_t half_table[SPWM_POINTS]; // 半波表,用于单极性调制 void SPWM_Table_Init(uint16_t master_duty) { for (uint16_t i = 0; i < SPWM_POINTS; i++) { // 单极性调制:正半周输出 +DC,负半周输出 -DC float sin_val = sinf(2.0f * PI * i / SPWM_POINTS); uint16_t ccr = (uint16_t)(master_duty * sin_val); if (ccr < 50) ccr = 50; // 最小脉宽限幅,避免窄脉冲驱动不足 if (ccr > (TIM1->ARR - 50)) ccr = TIM1->ARR - 50; // 最大脉宽限幅,保留死区空间 sin_table[i] = ccr; } }这段代码里有个值得说清楚的点:查表本身不产生正弦波,决定波形质量的是定时器更新中断里往 CCR 写入新值的时机。表长 512 点对应 50Hz 正弦波,一次周期 20ms,每 39.06us 中断一次并更新 CCR。这个时间窗由 TIM1 的更新事件自动触发,载波 20kHz 正好 50us 更新一个点,但实际中点数为 400,和表长 512 不整除。常见做法是让正弦表索引按整周期均匀映射到中断事件上,即每次中断索引加512 / 400 ≈ 1.28,用浮点累加器处理余数,或者干脆把表长改成 400 的整数倍。工程里为了省算力,通常直接把表长设成载波比整数倍的 400 点。
3.2 更新 CCR 的中断处理和幅值修正
光有正弦表还不够,母线电压跌落时单靠固定表撑不住输出稳定。需要 ADC 实时采样输出电压,用 PI 算出一个修正系数,乘到表值上再写入 CCR。
volatile uint16_t spwm_index = 0; volatile float voltage_kp = 0.85f; volatile float voltage_ki = 0.06f; float volt_err_integral = 0.0f; uint16_t target_rms = 220; // 目标电压有效值 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 前 20 个点留给 ADC 采样完成标志更新,后 380 点输出 spwm_index = (spwm_index + 1) % 400; // PI 修正,周期更新而非每个斩波点都调,避免控制频率与载波混淆 static uint16_t update_cnt = 0; if (++update_cnt >= 400) { update_cnt = 0; uint16_t adc_rms = Get_Output_RMS_From_ADCBuffer(); float err = target_rms - adc_rms; volt_err_integral += err; if (volt_err_integral > 2000) volt_err_integral = 2000; if (volt_err_integral < -2000) volt_err_integral = -2000; float k = voltage_kp * err + voltage_ki * volt_err_integral; if (k > 1.15f) k = 1.15f; if (k < 0.85f) k = 0.85f; voltage_gain = k; } // 查表并写入比较寄存器(相位移动产生正负半周) uint16_t ccr_value; if (spwm_index < 200) ccr_value = (uint16_t)(sin_table[spwm_index] * voltage_gain); else ccr_value = (uint16_t)(sin_table[spwm_index - 200] * voltage_gain); TIM_SetCompare1(TIM1, ccr_value); TIM_ClearITPendingBit(TIM1, TIM_IT_Update); } }PI 参数写在中断里而不是放在主循环,是想保证 20kHz 下控制律执行的确定性。中断里的 update_cnt 每 400 个斩波点(也就是每 20ms,一个工频周期)才更新一次增益,控制频率 50Hz,和 20kHz 载波完全解耦。实际调试时先断开 PI 让 gain 固定在 1.0,用调压器慢慢升母线电压,观察输出波形是否削顶,确认开环没问题再把 PI 接进去。积分限幅给的 2000 是防止启动瞬间电压误差大导致积分饱和,很多第一次调闭环的人栽在这里——启动瞬间输出从 0 直接拉向 220V,积分爆掉之后波形会上下不对称。
3.3 ADC 采样与逆变器输出端的隔离问题
ADC 采样输出 220V 交流电压时,不能直接接到 STM32 引脚。工程上常见方案是电压互感器(PT)或差分放大器先把电压降到 0~3.3V 安全范围,再用运放做偏置抬高到 ADC 可采区间。工程里stm32f10x_adc.c大概率配置的是规则通道组多通道扫描,同时采直流侧电压、输出交流电压和电感电流三路。这里有一个常见错误:ADC 采样时刻没有和 PWM 载波同步,导致采到的电感电流是开关毛刺而非平均值。TIM1 的更新事件恰好可以触发 ADC 注入组采样,把采样时刻钉在载波中心点,躲开开关切换瞬间的振荡,这个同步是此设计里提升电流环性能的最关键一步。
4. 在 uC/OS-II 下规划逆变器任务,避免 PWM 输出被调度打断
4.1 为什么裸机开发容易出"波形毛刺"问题
很多第一次接触这个项目的朋友会有个疑惑:PWM 更新已经在中断里做了,为什么还要上 uC/OS-II?答案是中断只负责最紧急的 CCR 更新,但还有保护判断、LCD 显示、按键扫描、远程通信这些非实时任务——它们和中断共用一个 CPU 时间片时,裸机的大循环写法会导致故障检测代码根本来不及跑。比如过流保护如果放在中断里,中断服务程序又不能做太多事,否则会影响 PWM 更新的 jitter;放在主循环里,一个显示刷新函数就可能让保护响应延迟几百微秒。uC/OS-II 的价值不是提升单点性能,而是给不同紧急程度的任务分配明确的优先级和周期,让保护逻辑跑在一个确定的时间片内,同时把显示、通信这类非实时任务放低优先级慢慢跑。
工程清单里的os_core.c、os_task.c、os_flag.c对应 uC/OS-II 的三块核心模块,os_cpu_a.asm则是针对 ARM Cortex-M3 的移植汇编文件。uC/OS-II 在 STM32F103 上非常成熟,几乎不需要改移植代码,但有一个点必须检查:OS_TICKS_PER_SEC宏。它的值决定了系统时钟节拍频率,常见配置是 1000Hz(1ms 一个 tick)。这个频率不能太低,否则OSTimeDly的误差会达到几十毫秒级,对 50Hz 逆变器的保护任务来说太粗糙。
#define TASK_STACK_SIZE 512 OS_STK task_protect_stk[TASK_STACK_SIZE]; OS_STK task_adc_stk[TASK_STACK_SIZE]; OS_STK task_lcd_stk[TASK_STACK_SIZE]; OS_EVENT *sem_adc_ready; void Task_Protect(void *pdata) { while (1) { // 等待 ADC 采样完成信号量,超时机制兜底 OSSemPend(sem_adc_ready, 10, &err); if (err == OS_ERR_TIMEOUT) continue; float I_peak = Get_Peak_Current(); if (I_peak > 15.0f) { TIM_CtrlPWMOutputs(TIM1, DISABLE); // 硬件级封锁 PWM OSFlagPost(flag_protect, FLAG_FAULT, OS_FLAG_SET); } OSTimeDly(1); // 1ms 周期执行 } } void Task_LCD(void *pdata) { while (1) { Display_Voltage_And_Status(); OSTimeDly(500); // 500ms 刷新一次,不需要太快 } }任务划分的第一原则是:保护任务必须独立成最高优先级,周期固定在 1ms。它只做三件事——读电流峰值、判断是否超限、必要时封锁 PWM——不做任何可能阻塞的操作。ADC 采样则通过信号量sem_adc_ready通知保护任务。显示任务塞在最低优先级,500ms 刷新一次就够。
4.2 STM32 中断优先级与 uC/OS-II 的配合陷阱
uC/OS-II 的调度器在PendSV中执行,而 TIM1 更新中断、ADC 中断都属于 IRQ。如果 TIM1 中断优先级设置得和 PendSV 一样高,极端情况下中断嵌套会让系统节拍严重延迟,保护任务迟迟得不到运行。常见做法是把中断优先级分组设为NVIC_PriorityGroup_2,让 TIM1 更新中断保持最高抢占优先级,ADC 中断次之,USART 等通信中断再次之,PendSV 设成最低。有一个特别隐蔽的坑:SysTick是 uC/OS-II 的时基,它和 TIM1 共用一个中断优先级分组时,如果 SysTick 被 TIM1 中断抢占得太久,系统 tick 会丢失,OSTimeDly时间就不准。所以 SysTick 的抢占优先级要高过 PendSV,但不能高过 TIM1——这个中间位置需要经验来定,建议 SysTick 设为 3、TIM1 设为 1、ADC 设为 2、PendSV 设为 15(Cortex-M3 中最低)。
4.3 反映到目录结构上的移植问题
KEIL5 打开这个工程时,os_cpu_a.asm如果报错,多数情况是启动文件的汇编语法不匹配当前 MDK 版本。MDK5 和 MDK4 对 ARM 汇编器有不同的构建规则,如果 MDK5 下打开,需要在工程选项 C/C++ 中把__ASSERT_USE_STDERR等宏去掉,并把Misc Controls加上--gnu兼容开关。还有一点:这个工程直接在Target里指定了芯片型号,如果换成其他 STM32F10x 系列,须到 C/C++ 预处理器窗口同步替换STM32F10X_MD宏(大容量用STM32F10X_HD,中文描述见各类标准库建工程指引),否则stm32f10x_conf.h里的外设使能宏会全部失效,现象就是 UART 发不出数据、GPIO 翻转不了。
5. 输出滤波参数设计与调机验证的实操路线
5.1 LC 截止频率设计快速估算
滤波器参数和 SPWM 一脉相承——截止频率通常取载波频率的 1/10 到 1/5。20kHz 载波对应截止频率 2k~4kHz,电感选 1mH 时,按截止频率公式f_c = 1 / (2π√LC)推算,电容取 1uF~2.2uF。实际调试时建议先固定电容 2.2uF(CBB 材质),电感用可调电感从 0.5mH 往上扫。出现电感轻微发烫要加大电感值,波形阻尼振荡则要减小电感。调的时候用示波器观察 PWM 输出脚和滤波后的正弦波形对比,目标是滤波后的正弦波上没有可辨识的载波纹波即可,不必追求极致的平直——过度增大电感只会让输出阻抗变高,带载时电压跌落明显。
5.2 晶振匹配电容和 ST-Link 连接的常见故障
系统晶振用的是 8MHz,匹配电容常规在 10pF~20pF。如果示波器看波形发现频率整体偏移(输出变成 47Hz 或 53Hz 而不是 50Hz),先查晶振负载电容是否匹配,而不是怀疑 SPWM 表算错。晶振的启动时间和负性阻抗在不同电容值下差异很大,如果先烧录后板子运行正常、断电重启后概率性起振失败,多半是匹配电容偏小,加大到 20pF 通常能解决。工程调试时 ST-Link 连接失败也常遇到,检查 SWDIO 和 SWCLK 是否被复用成了 GPIO,尤其是stm32f10x_gpio.c里如果初始化过PB3/PB4,这两个引脚默认是 SWD 功能,直接当 GPIO 用会拖死调试口。如果确实需要复用,可以配置重映射到其他引脚,或先按住复位下载。
5.3 用示波器验证死区与 THD 的快速方法
把示波器设为双通道,CH1 接 TIM1_CH1(PA8),CH2 接 TIM1_CH1N(PA9),触发模式设为上升沿,放大观察两个波形之间的空隙时间。如果死区小于开关管数据手册给出的关断延迟时间(比如 IRF540 的 Turn-Off Delay 约为 25ns),必须加大TIM_DeadTime寄存器值。验证 THD 时,直接用带 FFT 功能的示波器,将滤波后的正弦波接入,观察频谱中 150Hz、250Hz 以及载波频段的谐波分量。FFT 窗口里如果 3 次谐波(150Hz)比其他分量高出半个量级,建议从 PI 参数和最小脉宽限幅两方面找原因,后者通常是载波中心附近 CCR 写入了过窄脉冲,导致输出纹波叠加了高频分量。限幅阈值收窄到 ARR 的 3%~5% 是保护开关管的前提下极限的做法,这一步调完,输出波形的稳定度和谐波分量会同时改善。
本文还有配套的精品资源,点击获取