STM32F103实现三相SPWM互补输出与调频调压实战详解
发布时间:2026/9/9 21:01:30 锦皓数字建站

简介一套基于STM32F103C8T6的SPWM三相逆变控制方案利用高级定时器TIM1查表法生成三相六路互补PWM带死区且相位互差120°通过恒压频比实现调频调压适用于三相电机驱动、变频电源等电机控制场景适合嵌入式与电力电子方向学习者参考。资源共149个文件压缩包约46.67MB包含C/H源码、KEIL5工程文件、编译产物hex/axf、PDF原理文档以及正弦表生成工具、死区计算器等其中26个头文件与18个C源文件构成完整工程HEX与AXF文件可直接烧录或仿真验证PDF文档帮助理解SPWM生成原理。源码注释详细可在KEIL5中在线仿真直接观察SPWM输出波形。目前已有8053人学习下载。除工程代码外附带的学习文档和实用工具便于对照理解查表法、死区设置与调频调压实现思路可直接用于项目起步、教学实验或二次开发。 最近在倒腾三相逆变和电机驱动手头正好有一块STM32F103C8T6最小系统板把SPWM三相六路互补输出这套东西完整跑通了顺带把调频调压也做了进去。这个组合在变频器、三相BLDC/PMSM驱动、UPS电源里都很常见核心就是让单片机输出三组带死区的互补PWM再通过改变正弦表的频率和幅值实现调速调压。这篇文章就把我从原理到代码到调试踩坑的全过程记录下来给后面做类似项目的朋友一个参考。这板子虽然便宜但资源做这套东西完全够用。整个方案说白了就是用高级定时器TIM1生成三路互补PWM占空比按正弦规律变化同时插入死区防止上下桥臂直通再根据目标频率动态更新正弦表的查表步长和幅值实现V/F恒压频比控制。下面按模块把思路和实现细节拆开讲。1. 方案设计与核心参数定标1.1 为什么用STM32F103C8T6做SPWMF103C8T6属于Cortex-M3内核主频72MHz片上资源包括3个通用定时器、1个高级定时器TIM1、多个ADC和通信接口价格在几块钱到十几块钱之间。做三相SPWM输出核心需求是至少6路PWM、能配置互补输出和死区、更新频率足够快。TIM1刚好是高级定时器自带互补输出、死区生成和刹车保护功能硬件上天然适合干这件事。相较于用DSP或者专门的电机控制芯片F103C8T6的性价比很突出。国产替代料也遍地都是即使货源紧张也能找到兼容型号顶上这一点在项目落地时很重要。对于学习用途或者中小批量产品这个方案非常稳妥。1.2 系统总体架构整个系统的信号流是这样的单片机内部生成正弦表通过查表法得到三相互差120°的占空比序列。TIM1的三个通道配置为PWM模式每个通道输出一对互补信号分别是CHx和CHxN。互补输出的上升沿/下降沿之间插入死区时间防止上下桥臂同时导通。通过软件定时器或者定时器更新中断周期性修改正弦表查表指针和占空比幅值实现调频和调压。输出的六路PWM经过驱动芯片放大后驱动三相全桥逆变器最终输出到电机或负载。六路互补输出是电动机驱动的基本盘因为三相全桥有6个开关管每一相桥臂的上下管必须严格互补导通一旦出现上下管同时导通母线直接短路烧管子这也是死区存在的根本原因。1.3 关键参数预计算动手写代码之前需要先把几个核心参数定下来后面所有配置都围绕这几个数值。PWM载波频率常见选16kHz或者20kHz。16kHz高于人耳听觉上限电机运行噪音小同时F103在72MHz下处理这个频率的更新中断毫无压力。我做的是16kHz。正弦表点数取256点。点数越多波形越细腻但占用存储空间也越多256点是精度和资源的平衡点。调制比调制度最大值取0.9左右留出死区影响余量防止过调制导致波形畸变。死区时间取决于功率器件和驱动电路我用的MOS管加IR2101驱动设置1.5us比较安全。这个后面细说怎么算。有了这些参数定时器的ARR和PSC就定了。定时器时钟72MHzPWM频率16kHz则计数周期为72MHz / 16kHz 4500个计数单位所以ARR 4499。死区时间若设为1.5us对应计数个数为1.5us × 72MHz 108个计数单位。2. SPWM生成原理与调频调压思路2.1 从正弦波到SPWM脉冲序列SPWM的核心思路是用一系列宽度按正弦规律变化的脉冲去等效一个正弦波。根据面积等效原理脉冲宽度越接近正弦曲线在该时刻的积分值经过低通滤波之后得到的波形就越接近理想正弦波。数字实现时典型做法是“三角波载波比较法”正弦调制波与高频三角载波比较交点决定开关切换时刻。STM32里做这件事不需要真的生成三角波比较直接用定时器的计数器当作载波ARR对应三角波峰值比较寄存器CCR的值对应调制波瞬时值。CCR按正弦规律变化输出脉冲宽度就按正弦规律变化了。CCR的计算公式是CCR ARR × (0.5 0.5 × M × sin(θ))其中M为调制比0到1之间θ为当前采样角度。公式里的0.5偏置让占空比在0到100%之间变化M控制正弦波的峰值幅度。2.2 查表法生成三相互差120°的波形单片机上实时计算sin函数耗CPU资源不划算。常规做法是提前算好一张正弦表存到Flash里运行时按索引查表。比如256点正弦表覆盖0°到360°。A相从0°开始查表B相滞后120°即滞后256/3≈85.33个点C相滞后240°即滞后170.67个点。实际用整数索引时可以取85和171或者采用更高精度的做法#define TABLE_SIZE 256 #define PHASE_SHIFT 85 // 120°对应的查表步长三路CCR值这样更新ccr_a (uint16_t)(ARR * (0.5f 0.5f * M * sin_table[phase_a])); ccr_b (uint16_t)(ARR * (0.5f 0.5f * M * sin_table[(phase_a PHASE_SHIFT) % TABLE_SIZE])); ccr_c (uint16_t)(ARR * (0.5f 0.5f * M * sin_table[(phase_a 2 * PHASE_SHIFT) % TABLE_SIZE]));实际工程中更高效的做法是用定点数和整数查表避免浮点运算。本文为了可读性用浮点说明实测F103跑浮点也没问题只是中断里尽量少算最好提前算好。2.3 调频调压的核心逻辑调频就是改变正弦波的角频率体现在代码里就是改变每次中断查表指针的增量步长。系统时钟固定PWM周期固定16kHz每次更新中断对应一个PWM周期。若每个PWM周期更新一次则正弦表更新步长step与输出频率f的关系为step f × TABLE_SIZE / f_pwm f × 256 / 16000比如要输出50Hzstep 50×256/16000 0.8。输出5Hz时step 0.08。实际中用定点数累加器来做每次累加step并取整。调压则是改变调制比M。典型电机控制用恒压频比V/F曲线即电压与频率之比保持恒定以维持磁通恒定。基频以下采用V/F恒定基频以上电压达到最大值进入弱磁区。// V/F控制频率升高时调制比线性增加 if (freq BASE_FREQ) { m M_MAX * freq / BASE_FREQ; } else { m M_MAX; }低频时需要加电压补偿因为死区效应和管压降在低频时占比大不补偿的话电机启动无力甚至堵转。这一点后面调试部分细讲。3. TIM1互补输出与死区配置详解3.1 为什么必须加死区全桥电路里同一相上下两个开关管如果同时导通电源正负极直接短路瞬间大电流烧毁功率管。理想的互补PWM在切换瞬间上管关断的同时下管导通但实际功率器件都有开通延迟和关断延迟而且关断通常比开通慢。如果不插入死区切换瞬间必然存在上下管同时导通的时间窗口。死区时间就是在上下管切换时主动插入一段“上下管都不导通”的时间。比如上管从高变低后等待1.5us再让下管从低变高。这样即使器件有延迟也不会产生直通风险。死区过短会烧管过长会导致输出电压畸变和电流谐波增大取值需要在安全性和谐波之间权衡。3.2 死区时间的计算与设置死区时间需要覆盖“最坏情况下的器件关断延迟 驱动电路延迟 余量”。以IR2101驱动加IRFP460 MOS管为例驱动芯片传输延迟典型值约100-200nsMOS管关断延迟约100-200ns安全余量取1us以上比较放心所以总死区设置在1us到2us之间比较合理。我最终定1.5us。如果你用IGBT关断延迟在几百ns到1us级别IGBT还有拖尾电流死区一般要2-5us具体要查器件手册。STM32F103的TIM1死区由BDTR寄存器的DTG位配置。死区时间的计算公式按DTG位段分四种情况DTG[7:5] 0xxDT DTG[7:0] × TdtgDTG[7:5] 10xDT (64 DTG[5:0]) × Tdtg × 2DTG[7:5] 110DT (32 DTG[4:0]) × Tdtg × 8DTG[7:5] 111DT (32 DTG[4:0]) × Tdtg × 16其中Tdtg 1/72MHz ≈ 13.89ns。我需要的死区是1.5us 1500ns先除以13.89ns得到108也就是说DTG位段可以直接取1080x6C落在第一种格式。用标准库配置如下TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 时基配置72MHz / 16kHzARR 4499 TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 4499; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 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); TIM_OC3Init(TIM1, TIM_OCInitStructure); // 死区设置1.5us对应DTG 108 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 108; 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);补充一点使用TIM1时必须使能MOE主输出否则PWM不会输出。库函数中TIM_CtrlPWMOutputs(TIM1, ENABLE)就是干这个的很多人漏了这一步结果示波器看不到波形。3.3 更新PWM占空比的方式选择SPWM要求每个PWM周期都更新CCR值更新方式有两种定时器更新中断里写CCR简单直观但每个周期都要进中断16kHz下中断频率较高中断服务函数要尽量精简。DMA方式更新CCR可以在内存中准备好一个周期的CCR序列用定时器更新事件触发DMA搬运到CCR寄存器CPU负担极小。我调试初期用中断方式方便观察稳定后改成了DMA环形缓冲方式。如果只是学习和实验中断方式完全够用不追求极限效率的话没必要上DMA。3.4 用CubeMX快速生成初始化代码如果不想手写寄存器或者标准库可以用STM32CubeMX图形化配置。选择STM32F103C8T6芯片在TIM1界面配置Clock Source选择Internal ClockChannel1、2、3全部选择PWM Generation CHx and CHxNPrescaler设为0Counter Period设为4499Pulse设个初始值Dead Time设为108对应1.5us注意CubeMX里单位是时钟周期数Break设置根据需要开或关生成代码后在main函数里调用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1)等函数启动三个通道注意互补通道不用单独启动硬件自动跟随。HAL库生成的代码基础上再写中断或DMA更新逻辑即可。用CubeMX的好处是初始化代码不容易写错特别适合刚接触定时器配置的读者。4. 完整代码实现与调试实录4.1 中断方式更新占空比的参考代码下面给出一套用中断方式实时更新三路CCR的参考代码骨架。中断频率为16kHz每个周期计算一次三相占空比。为减少中断内浮点运算实际工程建议把正弦表存为0-255范围的整数用整数运算计算CCR。// 全局变量 volatile uint16_t phase_acc 0; // 相位累加器定点数 uint16_t step_acc; // 频率对应的步长 float current_m 0.9f; // 当前调制比 const uint16_t sin_table[256] { /* 预先生成的正弦表值域0~4095 */ }; // 定时器更新中断 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); uint16_t phase phase_acc 8; // 高8位作为查表索引 uint16_t phase_b (phase 85) 0xFF; uint16_t phase_c (phase 171) 0xFF; uint16_t ccr_a (uint16_t)((uint32_t)sin_table[phase] * current_m 12); uint16_t ccr_b (uint16_t)((uint32_t)sin_table[phase_b] * current_m 12); uint16_t ccr_c (uint16_t)((uint32_t)sin_table[phase_c] * current_m 12); TIM_SetCompare1(TIM1, ccr_a); TIM_SetCompare2(TIM1, ccr_b); TIM_SetCompare3(TIM1, ccr_c); phase_acc step_acc; // 步长决定输出频率 } } // 设置输出频率freq单位为0.1Hz void set_frequency(uint16_t freq_x10) { // f step * f_pwm / 256 step f * 256 / f_pwm // 避免浮点f_pwm 16000256/16000 1/62.5 // 用定点step_x16 f_x10 * 256 * 16 / 16000 * 10 step_acc (uint16_t)((uint32_t)freq_x10 * 256 * 16 / 16000 / 10); }这里面phase_acc用定点小数表示相位低8位是小数部分高8位是整数索引频率分辨率能做到很高。步长的计算实际就是f × 256 / 16000代码里乘16再除10是为了处理0.1Hz精度的输入。4.2 正弦表的生成与精度分析正弦表可以用PC端脚本生成也可以用单片机启动时计算推荐前者。下面用Python生成一段C语言数组值域0到4095对应12位精度import math TABLE_SIZE 256 MAX_VAL 4095 data [] for i in range(TABLE_SIZE): value int((math.sin(2 * math.pi * i / TABLE_SIZE) 1) * MAX_VAL / 2) data.append(value) lines [] for i in range(0, TABLE_SIZE, 12): lines.append(, .join(str(v) for v in data[i:i12])) print(const uint16_t sin_table[256] {) for line in lines: print( line ,) print(};)12位精度对应4096个量化等级在16kHz载波频率下完全够用。调制比M通过移位实现上面对应的其实是current_m 调制比 × 4095所以计算时直接相乘再右移12位。4.3 调频调压的人机接口设计调频调压最终要能改参数最简单的方案用两个电位器接ADC一路控制频率、一路控制调制比。也可以加按键和OLED显示做成微型变频器面板。我做的实验板就是两个旋钮ADC采样后数字滤波频率范围5Hz到120Hz调制比0到0.95。ADC读取电压换算成频率时要注意设置频率变化速率限制也就是工程上说的S曲线加减速。如果频率突变太快电流冲击很大容易过流保护甚至烧管子。我实测从50Hz直接切到5Hz母线电流峰值会翻倍还多加了加减速限制后就好很多。4.4 硬件接线与驱动板注意事项单片机输出的PWM电平通常是3.3V不能直接驱动功率管必须经过驱动芯片。我用的是IR2101半桥驱动自带自举电路一片驱动一相桥臂。需要注意自举电容选择典型用1uF到10uF的陶瓷电容或钽电容耐压要大于母线电压。电容太小会导致高端驱动电压不足输出波形异常。驱动芯片的地要和功率地严格分开单点连接避免功率电流干扰控制信号。GPIO配置为复用推挽输出速度设为50MHz否则PWM边沿不够陡。电源去耦靠近芯片引脚放置100nF陶瓷电容模拟数字分开铺地。如果直接用6N137等光耦隔离需要注意光耦的传输延迟死区计算时要考虑进去。我调试时发现一个有趣的现象光耦导致的延迟会让实际死区变长但方向上不会造成直通只是波形失真更明显。5. 调试踩坑记录与常见问题速查5.1 上电烧管子的几个经典原因第一先给PWM还是先给母线电压。正确顺序是先让单片机输出确定电平的PWM确认六个管子的栅极信号稳定后再给母线电压上电。我刚开始偷懒一上电就全开直接烧了两个MOS管后来加了软启动逻辑才稳定。第二刹车功能Break没配置或者引脚悬空。TIM1的BKIN引脚如果没做初始化且悬空很容易受干扰误触发刹车PWM被强制拉低表现为时好时坏。如果不用刹车功能最好配置为禁用状态或者把BKIN引脚上拉到确定电平。第三驱动芯片的死区时间和单片机死区叠加。如果驱动芯片自身带有死区插入功能单片机的死区要相应减小或者关闭两边叠加会导致死区过长波形畸变严重。5.2 输出波形异常的排查思路如果示波器看到某一相上下管波形不对称检查CCR的更新时机和PWM周期是否对齐。定时器的影子寄存器在溢出时才更新如果中断里写入CCR但更新事件被其他代码阻塞就会出现都挤在同一个周期更新导致脉冲宽度乱跳。如果三相波形没有互差120°检查查表索引是否溢出。我用uint8_t做索引时85和171的加法会自然溢出回绕这是没问题的。但如果你把索引声明成uint16_t又没有对256取模就会访问数组越界波形自然就乱了。如果PWM频率调高后波形失真观察载波比。载波频率f_pwm固定为16kHz输出频率越高每个正弦周期内的脉冲数越少即载波比越低谐波增大。我用120Hz输出时每个周期约133个脉冲波形看起来还可以到200Hz就只有80个脉冲失真明显。这就是同步调制和异步调制需要切换的问题。5.3 死区时间调整的实测经验我把死区从0.5us到3us分别测了波形总结如下0.5us时一上电母线电流就有明显尖峰MOS管发烫说明死区不够存在瞬间直通。1.5us时波形干净电流平稳温升正常。3us时波形出现明显台阶电流谐波增加电机噪音变大。调试时最靠谱的验证方法是用电流探头观察母线电流或者观察电机运行噪音。没有电流探头的话可以摸管子温度直通严重时管子在几秒内就会烫到不能碰。5.4 低频性能与死区补偿低频输出时死区造成的电压损失占比更大。举一个具体的数死区1.5usPWM周期62.5us死区占比2.4%。输出50Hz时这个比例的电压损失对整体波形影响有限但输出5Hz时正弦波幅值本身很小同样的死区时间损失在正负半周交替点会产生明显的波形削顶表现为电流波形在过零点附近出现非线性畸变。简单有效的补偿方法是按电流极性叠加补偿量电流为正时增加占空比电流为负时减小占空比补偿量等于死区时间对应的占空比。这个方法需要检测电流过零实际电路要加电流传感器或者采样电阻。如果只是做简单的V/F控制不做电流环可以先用固定的开环补偿在中低频段给调制比加一个固定偏置也能明显改善启动性能。5.5 常见问题速查表现象可能原因解决办法PWM无输出未使能TIM1主输出MOE调用TIM_CtrlPWMOutputs(TIM1, ENABLE)上下管同时导通死区设置过小增大BDTR中的死区时间某一相波形异常CCR更新时机不对检查中断优先级和更新事件标志频率调高后波形失真载波比过低更换更高的载波频率或降低最高输出频率电机低频抖动死区电压损失未补偿加入死区补偿或低频电压补偿上电偶尔烧管PWM未就绪就上电增加软启动时序先PWM后母线GPIO波形边沿缓GPIO速度配置过低设置GPIO_Speed_50MHz5.6 个人实操心得这套系统我从画板到调通前后折腾了小两周。回头总结最大的心得是SPWM本身不复杂复杂的是边界条件的管理——死区够不够、顺序对不对、保护到不到位。调试时不要一上来就满压满频跑用低压直流电源比如12V或24V先验证六路PWM波形确认死区和互补关系完全正确后再升压。低压调试能烧的管子有限成本低太多。另外买个靠谱的逻辑分析仪或者双通道示波器很有必要。六路PWM同时看需要4通道以上但调试死区只需要看同一相的上下管两路波形双通道示波器就够用。把触发模式设为脉宽触发能快速抓到异常脉冲。最后说一个扩展方向这套代码稍加改动就能跑方波驱动六步换相或者加个霍尔传感器接口变成BLDC驱动器。如果换成TIM8做同样的配置还能再做一组三相输出双电机控制也不在话下。F103C8T6这个板子确实是一块性价比极高的学习平台吃透这套SPWM方案后面再上FOC或者更高级的电机控制算法会顺畅很多。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。