基于STM32F103C8T6的小功率逆变器方案:SPWM生成与TIM1互补输出实战 1. 小功率逆变器方案选型与整体设计思路1.1 为什么是STM32F103C8T6这颗芯片做小功率逆变器主控选型基本决定了整个方案的开发难度、成本上限和后期维护的便利性。市面上能跑SPWM的MCU不少从8位的STC到32位的GD32、STM32甚至一些专用逆变芯片都能做。但如果你要的是一个资料齐全、社区活跃、成本可控、性能够用的方案STM32F103C8T6几乎是绕不开的选择。先看几个硬指标。STM32F103C8T6是ARM Cortex-M3内核主频72MHz64KB Flash20KB SRAM自带3个通用定时器TIM2/3/4和1个高级定时器TIM1。做单相逆变器核心需求就是一路互补PWM带死区TIM1的CH1/CH1N、CH2/CH2N、CH3/CH3N正好能输出三对互补信号硬件上直接满足H桥四管驱动加一路备用。死区时间可以通过BDTR寄存器配置不需要外部死区电路这一点比用普通定时器软件模拟死区要可靠得多。再说成本。这颗芯片在市场上的价格已经非常透明最小系统板几块钱就能拿到自己画板的话芯片本体成本更低。对于小功率逆变器这种对成本敏感的产品主控省下来的钱可以加到功率管和磁性元件上整体性能反而更好。还有一个容易被忽略的点生态。STM32F103的标准库和HAL库资料铺天盖地从GPIO配置到定时器PWM输出从ADC采样到串口通信几乎每一个外设都有现成的例程可以参考。做逆变器不是做科研能快速搭出原型、快速调试、快速迭代才是关键。我试过用某国产替代芯片做同样的事光是定时器互补输出这一块就卡了两天换回STM32半天就跑通了。1.2 单极性倍频与双极性调制的取舍SPWM调制方式直接决定了输出波形的质量和开关损耗。小功率逆变器常见的调制方式有两种双极性调制和单极性倍频调制。双极性调制最简单H桥的对角管同时开关输出只有正负两种电平。优点是控制逻辑简单只需要一路正弦调制波和一路三角载波比较。缺点是开关损耗大因为每个开关周期内四个管子都在动作而且输出谐波主要集中在开关频率附近滤波电感需要选得比较大。单极性倍频调制则是把H桥分成两个桥臂每个桥臂独立控制。左桥臂用正弦波和三角波比较右桥臂用反相正弦波和同一个三角波比较。这样输出电平有正、负、零三种状态等效开关频率翻倍谐波频率提高一倍滤波电感可以减小一半左右。代价是控制逻辑稍微复杂一点需要两路互补的调制信号。对于小功率逆变器我倾向于单极性倍频。原因很直接电感体积和成本在逆变器里占比不小能用控制换电感这笔账划算。而且STM32的TIM1有足够的通道和互补输出能力实现单极性倍频并不增加硬件成本。具体实现上用TIM1的CH1/CH1N驱动左桥臂CH2/CH2N驱动右桥臂。CH3可以留着做保护或者风扇控制。正弦调制波用查表法生成三角载波用定时器自动重装载值模拟。查表点数选200点或者256点根据Flash空间和输出频率要求来定。1.3 后级H桥与滤波器的基本架构小功率逆变器的后级拓扑基本固定全桥逆变 LC滤波。四个IGBT或者MOSFET组成H桥把前级升压后的直流电逆变成SPWM波再通过LC滤波器把高频谐波滤掉得到50Hz正弦波。H桥的管子选择要看功率等级。如果是几百瓦的小功率MOSFET就够用比如IRF540、IRF3205这类导通电阻小开关速度快。如果是上千瓦IGBT更合适饱和压降固定大电流下损耗比MOSFET低。热词里提到“用四个IGBT做一个H桥逆变器后级”这个思路是对的IGBT在小功率下虽然开关损耗略高但胜在皮实耐造驱动也简单。LC滤波器的参数计算是重点。截止频率一般取开关频率的1/10到1/20。假设开关频率20kHz截止频率取1kHz到2kHz。电感值根据纹波电流要求算电容值根据输出电压纹波要求算。具体公式后面实操部分会展开。驱动电路不能省。STM32的IO口输出3.3V驱动电流只有几十毫安直接推MOSFET栅极会烧IO口。必须加驱动芯片或者图腾柱电路。IR2110、IR2104这类半桥驱动芯片是标配自带死区控制和自举电路用起来省心。2. SPWM生成的核心细节与定时器配置2.1 查表法生成正弦调制波的参数计算SPWM的核心就是拿正弦波和三角波比较输出占空比按正弦规律变化的方波。在STM32上实现最稳妥的方法是查表法预先算好一个正弦周期内的N个采样点存到数组里定时器中断里依次读取更新比较寄存器的值。采样点数N的选择有讲究。N太小输出波形阶梯感明显谐波含量高N太大占用Flash空间多中断频率高CPU负担重。对于50Hz输出如果载波比取200意味着一个正弦周期内有200个PWM脉冲每个脉冲对应一个采样点。载波比200时输出谐波主要集中在200倍基频附近也就是10kHz左右滤波相对容易。具体计算过程系统时钟72MHz定时器预分频设为0自动重装载值ARR决定PWM频率。假设要20kHz的PWM频率ARR 72MHz / 20kHz - 1 3599。正弦表有200个点每个点对应一个正弦值范围映射到0到ARR之间。正弦值计算公式sin_val[i] (ARR / 2) * (1 sin(2 * PI * i / N))其中i从0到N-1。这样算出来的正弦表最小值接近0最大值接近ARR中间值在ARR/2附近。定时器比较寄存器CCR写入这个值就能得到占空比按正弦规律变化的PWM波。实际调试时ARR不要取满留一点余量比如用ARR的90%作为正弦表的幅值上限避免占空比到100%时上下管直通。2.2 TIM1互补输出与死区时间的配置要点TIM1是STM32F103的高级定时器专门为电机控制和电源设计。配置互补输出需要设置几个关键寄存器CCMR1/CCMR2设置PWM模式CCER使能互补输出BDTR设置死区时间和主输出使能。死区时间的计算死区时间 DTG[7:0] × T_dts其中T_dts是定时器时钟周期。72MHz下T_dts ≈ 13.89ns。如果死区时间设1μsDTG值约为72。实际配置时死区时间要根据功率管的开关特性来定。MOSFET的关断时间长死区要留够一般500ns到1μs。IGBT关断更慢死区要1μs到2μs。注意BDTR寄存器的MOE位必须置1否则互补输出不会使能。很多新手调了半天没波形就是忘了开MOE。配置代码大致如下// TIM1初始化PWM频率20kHz TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_Period 3599; // 72MHz / 3600 20kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 通道1和通道2配置为PWM模式1 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_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); // 死区时间配置约1us TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime 72; // 72 * 13.89ns ≈ 1us TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE);这段代码里TIM_CtrlPWMOutputs(TIM1, ENABLE)就是置MOE位少了这一句互补输出端不会有任何波形。2.3 中断服务函数与正弦表更新节奏正弦表的更新在定时器更新中断里做。每次计数器溢出触发中断在中断里读取下一个正弦值写入CCR1和CCR2。注意CCR2要写反相值因为单极性倍频需要两个桥臂的调制波反相。中断频率就是PWM频率20kHz。72MHz主频下每次中断有3600个时钟周期可用中断服务函数只要几十条指令完全跑得过来。但要注意中断里不要做浮点运算不要调用库函数直接查表赋值就行。void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); sine_index; if (sine_index SINE_TABLE_SIZE) sine_index 0; // 左桥臂用正弦表 TIM_SetCompare1(TIM1, sine_table[sine_index]); // 右桥臂用反相正弦表 TIM_SetCompare2(TIM1, sine_table[(sine_index SINE_TABLE_SIZE/2) % SINE_TABLE_SIZE]); } }这里用了一个小技巧反相正弦表不需要单独存一份直接把索引偏移半个周期就行。这样Flash占用减半代码也更简洁。3. 从零搭建工程的完整实操流程3.1 标准库工程模板的建立与关键文件说明虽然现在HAL库和LL库越来越流行但做逆变器这类对实时性要求高的项目标准库依然是很多老工程师的首选。标准库直接操作寄存器代码效率高中断响应快没有HAL库那些层层封装的开销。新建工程需要准备几个东西启动文件startup_stm32f10x_md.s、链接脚本stm32f103c8t6.ld或者Keil里的分散加载文件、标准库源码STM32F10x_StdPeriph_Driver、CMSIS核心文件。启动文件选md后缀的对应中等容量产品64KB Flash正好匹配。链接脚本里要定义好Flash和RAM的起始地址和大小。STM32F103C8T6的Flash从0x08000000开始大小0x1000064KBRAM从0x20000000开始大小0x500020KB。栈顶地址设为RAM末尾堆根据实际使用情况分配。Keil工程里需要添加的宏定义USE_STDPERIPH_DRIVER和STM32F10X_MD。前者让标准库的头文件包含外设驱动后者告诉库文件当前芯片是中容量型号。少了这两个宏编译会报一堆未定义。实操心得新建工程时先把GPIO点灯跑通确认时钟配置、启动文件、链接脚本都没问题再往上加定时器和PWM。我见过太多人一上来就写SPWM结果灯都不亮根本分不清是时钟错了还是PWM配置错了。3.2 ST-Link调试器连接与驱动安装避坑ST-Link是STM32开发最常用的调试器便宜、稳定、支持SWD和JTAG。但驱动安装经常出问题尤其是Win10和Win11系统。常见问题一Keil识别不到ST-Link。先检查设备管理器里有没有“STMicroelectronics STLink dongle”这个设备。如果没有说明驱动没装好。去ST官网下载ST-Link驱动包安装后重新插拔。如果设备管理器里显示黄色感叹号右键更新驱动手动指向驱动安装目录。常见问题二能识别但下载失败。检查SWD接线SWCLK接PA14SWDIO接PA13GND和3.3V必须接。有些最小系统板没有引出复位引脚Keil里要把“Reset after Connect”改成“Connect under Reset”否则芯片进入低功耗模式后连不上。常见问题三下载一次后第二次连不上。大概率是程序里把SWD引脚复用了。PA13和PA14默认是SWD功能如果代码里把它们配置成普通GPIO或者复用成其他外设调试器就连不上了。解决办法是在初始化代码里加一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)只保留SWD释放JTAG引脚。但注意不要禁用SWD本身否则下次真的连不上只能用串口ISP或者复位时按住复位键来救。3.3 串口打印与GBK转UTF8的编码处理调试逆变器时串口打印是必不可少的。输出电压、电流、频率、故障码这些信息通过串口发到电脑上用串口助手看比点灯直观得多。STM32的串口配置很简单USART1接PA9和PA10波特率1152008位数据位1位停止位无校验。重定向printf到串口在Keil里勾选“Use MicroLIB”然后实现fputc函数就行。int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); return ch; }编码问题是个坑。Keil默认用GBK编码保存源文件如果代码里有中文字符串串口助手用UTF8显示就是乱码。解决办法有两个一是把Keil的编码改成UTF8在Edit - Configuration - Editor里设置二是串口助手切换成GBK显示。我习惯用第一种统一UTF8省得以后跨平台出问题。如果代码里已经有GBK编码的中文批量转UTF8可以用Notepad或者VS Code。VS Code右下角点击编码选“通过编码重新打开”选GBK然后再“通过编码保存”选UTF8。批量处理的话用Python脚本跑一遍也行。4. 常见问题排查与实战经验汇总4.1 PWM无输出与波形异常的排查路径PWM调不出来是新手最常遇到的问题。按以下顺序排查基本能覆盖90%的情况。第一步确认时钟。用示波器测PA8或者PA9看有没有PWM波形。如果没有检查RCC配置里TIM1时钟是否使能GPIO时钟是否使能GPIO模式是否配成复用推挽输出。第二步确认MOE位。TIM1的互补输出受BDTR寄存器的MOE位控制。如果MOE没置1CH1N和CH2N不会有输出。可以在调试器里直接看BDTR寄存器的值或者加一句TIM_CtrlPWMOutputs(TIM1, ENABLE)。第三步确认死区时间。死区时间设得太大比如超过PWM周期的10%输出占空比会被严重压缩看起来就像没输出。用示波器看互补的两路波形中间的死区时间应该在几百纳秒到1微秒之间。第四步确认比较值。CCR寄存器的值如果大于ARR输出恒为低或者恒为高。调试时先把CCR设成ARR的一半看有没有50%占空比的方波。有方波了再换成正弦表。波形异常的情况如果输出波形有毛刺检查电源滤波和地线布局。如果波形频率不对检查ARR和预分频的计算。如果波形幅度不对检查驱动电路和功率管。4.2 逆变器带载后电压跌落与波形失真空载波形漂亮一带载就塌这是小功率逆变器的通病。原因通常有三个前级供电不足、滤波电感饱和、死区补偿不够。前级供电不足最好判断。用万用表测母线电压空载和带载时对比。如果带载后母线电压掉得厉害说明前级电源功率不够或者线损太大。小功率逆变器前级一般是升压电路或者直接电池供电电池内阻大、线细、接插件接触不良都会导致带载跌落。滤波电感饱和比较隐蔽。电感选型时要看饱和电流如果负载电流接近或者超过电感的饱和电流电感量急剧下降滤波效果变差波形失真。解决办法是换更大磁芯或者更大线径的电感或者提高开关频率减小电感需求。死区补偿是进阶话题。死区时间会导致输出电压损失尤其是在低压大电流时死区造成的波形畸变很明显。可以在正弦表里预加一个补偿量或者根据电流方向动态调整死区。小功率场合如果死区设得不大不补偿也能接受。4.3 常见问题速查表现象可能原因排查方法解决措施上电无反应供电未接或电压不足万用表测3.3V和5V检查电源接线确认电压ST-Link连不上驱动未装或SWD引脚被复用设备管理器查看检查PA13/PA14配置重装驱动代码里保留SWD功能PWM无输出MOE未使能或时钟未开示波器测PA8调试器看BDTR加TIM_CtrlPWMOutputs检查RCC波形频率不对ARR或预分频计算错误示波器测周期反推频率重新计算ARR和预分频值带载电压跌落前级功率不足或电感饱和测母线电压摸电感温度加大前级功率换大磁芯电感串口乱码编码不一致串口助手切换GBK/UTF8统一Keil和串口助手编码死区时间异常DTG值计算错误示波器看互补波形死区按公式重新计算DTG管子发热严重死区不够或驱动不足测栅极波形看死区时间加大死区换驱动芯片4.4 几个容易被忽略的实操细节VBAT引脚处理。STM32F103C8T6的VBAT引脚如果不用直接接3.3V或者悬空都行但不要接错到5V会烧。如果板子上有纽扣电池座VBAT接电池正极掉电时RTC和备份寄存器能保持。PB0不能做按键。热词里有人问这个确实PB0在某些最小系统板上接了LED或者别的外设做按键输入时要注意上下拉配置。如果PB0外部已经有上拉电阻内部就不需要再开上拉否则电平冲突。DMA通道冲突。STM32F103的DMA1有7个通道每个通道对应固定的外设请求。TIM1的更新事件对应DMA1通道5TIM2对应通道2USART1_TX对应通道4。配置DMA时先查参考手册的请求映射表别配错了通道否则DMA不触发。FreeRTOS下的中断优先级。如果项目里跑了FreeRTOSTIM1中断的优先级要设得比configMAX_SYSCALL_INTERRUPT_PRIORITY高否则中断里调用FreeRTOS的API会触发断言。逆变器的PWM中断对实时性要求高优先级设最高中断里只做查表和赋值不做任何系统调用。Flash加密。STM32F103支持读保护通过选项字节设置。量产时开启读保护防止代码被读出。但注意开启读保护后调试器无法再连接除非先解除保护而解除保护会擦除整个Flash。调试阶段不要开量产前再开。5. 方案扩展与性能优化方向5.1 从SPWM到SVPWM的升级路径SPWM实现简单但直流电压利用率只有86.6%。SVPWM空间矢量PWM能把利用率提高到100%同样母线电压下输出交流电压更高。对于电池供电的小功率逆变器这意味着可以多输出15%的功率。SVPWM的核心是判断扇区、计算基本矢量作用时间、生成PWM比较值。在STM32上实现需要用到一些三角函数和查表。TIM1依然可以用只是比较值的计算方式变了。如果SPWM已经跑通升级SVPWM主要是改中断里的计算逻辑硬件不用动。不过SVPWM的计算量比SPWM大20kHz中断下72MHz的M3内核要仔细优化代码。可以用查表加插值的方式减少实时计算量或者降低开关频率到10kHz给CPU留更多时间。5.2 加入ADC采样实现闭环控制开环逆变器输出电压随负载变化带载后电压跌落明显。加入ADC采样输出电压做PID闭环可以稳住输出。STM32F103的ADC是12位最快1MHz采样率采50Hz正弦波绰绰有余。采样时机很关键。要在PWM周期的特定时刻采样避开开关噪声。一般是在计数器溢出中断里触发ADC或者在PWM中点采样。用DMA把ADC结果搬到内存不占CPU。PID计算放在慢速循环里比如每10个PWM周期算一次减轻CPU负担。电压采样用电阻分压加运放跟随电流采样用霍尔传感器或者采样电阻加差分放大。采样电路的地要和功率地分开单点接地否则开关噪声会串进ADC导致采样值跳动。5.3 保护机制过流、过温、欠压逆变器没有保护就是炸弹。最基本的三个保护过流保护、过温保护、欠压保护。过流保护用比较器实现最快。采样电阻上的电压和基准比较超过阈值直接触发TIM1的刹车输入BKIN硬件立刻关断PWM输出比软件响应快得多。STM32的TIM1有专门的刹车引脚配置成刹车输入触发后MOE自动清零PWM输出立刻变高阻。过温保护用NTC热敏电阻接ADC采样温度超过阈值降功率或者停机。欠压保护监测母线电压低于阈值时报警并停机防止电池过放。保护阈值要留余量。过流阈值设成额定电流的1.5倍到2倍过温阈值设成管子最大结温的80%欠压阈值根据电池类型定铅酸电池单节1.8V锂电池单节3.0V。5.4 代码优化从查表到实时计算的权衡查表法简单可靠但正弦表占Flash且频率固定。如果要输出可变频率比如45Hz到65Hz可调查表法就需要动态调整索引步进或者用多个表切换。实时计算正弦值灵活但M3内核没有浮点单元算一次sin要几百个时钟周期20kHz中断下根本来不及。折中方案是用定点数查表加线性插值或者用CORDIC算法。CORDIC用移位和加法算三角函数适合没有FPU的MCU但代码复杂度高。对于小功率逆变器输出频率通常固定50Hz查表法足够。如果要做变频建议用查表加索引步进调整步进值根据目标频率计算。比如200点正弦表对应50Hz步进为1对应60Hz步进为1.2取整处理会有轻微频率误差但小功率场合可以接受。6. 写在最后的一些个人体会这个方案我从头到尾跑过不止一遍踩过的坑基本都写在上面了。STM32F103C8T6做小功率逆变器性能绰绰有余关键是把定时器和死区配置对把保护机制做扎实。很多人一上来就追求SVPWM、闭环控制结果连开环SPWM都没调稳带个灯泡都闪。我的建议是先把开环跑通波形干净了再加闭环和保护一步一步来。还有一点功率部分和数字部分一定要分开布局。我见过有人把STM32和H桥画在同一块板上地线没处理好ADC采样全是噪声PWM波形上全是毛刺。数字地和功率地单点连接驱动信号用光耦或者隔离芯片隔开这些问题就少很多。最后调试逆变器一定要有示波器最好是四通道的。看PWM波形、看死区、看输出电压、看电流四个通道同时抓什么问题都藏不住。没有示波器用万用表测平均电压也能凑合但效率低很多。