STM32F103三相SPWM生成:互补PWM、死区与V/F调制的工程实现 简介面向三相电机驱动、变频调速与电源逆变等嵌入式应用开发者内容基于STM32F103C8T6高级定时器TIM1查表法实现相位互差120°的三相六路互补SPWM输出并配置死区与恒压频比调频调压策略可直接用于三相电机驱动。压缩包共149个文件包含C源码、头文件、Keil5工程配置、hex/axf固件、PDF原理文档以及xls正弦表生成工具和exe死区计算器等整体约46.67MB目录结构清晰方便按模块查阅、修改和复用。已有8067人学习下载可见其受关注程度较高。代码内注释详细工程支持在线仿真观察SPWM波形内容预览显示源码按定时器、PWM生成、串口、延时等模块拆分配套SPWM生成原理文档、正弦表工具和死区计算器适合逐步理解查表生成、互补输出、死区设置、调频调压的全流程并为相关项目开发提供可运行的参考模板。1. 三相六路互补SPWM为什么“六路”比“三路”难十倍三相逆变器的SPWM输出看起来是六路PWM拆开之后其实是三对互补信号CH1与CH1N、CH2与CH2N、CH3与CH3N任意一对的上下管都绝不允许同时导通。难点不在生成三路正弦调制的占空比——那只要一张正弦表加三次查表就够了——而在处理上下管切换瞬间的“谁都不能导通”死区以及让输出频率和输出电压成比例地同时变化。死区太短会直通烧管死区太长会吃掉输出电压导致波形畸变频率变了而电压不跟着变电机不是磁饱和发热就是转矩不够。这些机制互相牵制而F103C8T6上唯一能同时给你互补输出、死区发生器和刹车保护的就是TIM1。用KEIL5做软件仿真能验证占空比变化和120°相移却验证不了纳秒级死区——理解了整套原理你才不会在仿真通过之后第一次上板就翻车。2. 用TIM1生成三相PWM从一张正弦表到六个引脚2.1 为什么只能选TIM1F103高级定时器的不可替代性F103里有8个定时器但能直接输出互补PWM的只有TIM1以及大容量型号才有的TIM8。TIM2、TIM3、TIM4这些通用定时器只有OCx输出没有OCxN输出也没有内置的死区发生器。想把通用定时器拿来驱动三相桥你只能把两路PWM接到GPIO上在外部或中断里手动插死区这既不精确又危险。TIM1的高级之处在于这三个硬件单元是配套出现的三对互补输出引脚OC1/OC1N、OC2/OC2N、OC3/OC3N、一个可编程死区发生器BDTR寄存器、一个刹车输入BKIN。在LQFP48封装的C8T6上PA8/PA9/PA10复用到CH1/CH2/CH3PB13/PB14/PB15复用到CH1N/CH2N/CH3N刚好凑齐六路。这意味着你在初始化时只需要做一次引脚复用配置之后定时器硬件自己负责“上下管不要同时开”这件事。2.2 中心对齐模式SPWM偏爱对称波形的原因SPWM有两种常见的定时器计数方式边沿对齐和中心对齐。边沿对齐模式下计数器从0递增到ARR波形只有一侧有调制边沿占空比变化引起的谐波不对称电机绕组里的电流纹波偏大。中心对齐模式下计数器先从0递增到ARR再从ARR递减到0一个PWM周期内比较事件出现两次输出脉冲关于周期中心对称谐波能量更集中这是驱动三相电机时更常用的选择。中心对齐模式下PWM1的有效条件可以理解为递增段和递减段都满足CNT小于CCR时输出有效。因此占空比就是CCR/ARR这是这个模式最好的地方——你不需要在脑子里来回换算方向。72MHz定时器时钟、ARR3600时PWM频率等于72MHz除以两倍的ARR也就是10kHz单个计数步长13.89ns。一个载波周期100μs这和后面的死区时间、正弦表点数都能对上。初始化代码的关键部分长这样/* 72MHz 定时器时钟, 中心对齐模式, ARR3600 - PWM频率10kHz */ TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 0; tb.TIM_CounterMode TIM_CounterMode_CenterAligned1; tb.TIM_Period 3600; tb.TIM_ClockDivision TIM_CKD_DIV1; /* tDTS tCK_INT 13.89ns */ tb.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM1, tb); /* OC1~OC3 都用PWM1模式, 同时使能OCx和OCxN */ TIM_OCInitTypeDef oc; oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_Pulse 1800; /* 初始50%占空比, 对应CCR中点 */ oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_OutputNState TIM_OutputNState_Enable; oc.TIM_OCPolarity TIM_OCPolarity_High; oc.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OC1Init(TIM1, oc); TIM_OC2Init(TIM1, oc); TIM_OC3Init(TIM1, oc);这段配置把TIM1变成了三路互补输出框架但此刻它还没真正输出——BDTR寄存器里的主输出使能位MOE还是0所有引脚保持无效电平这是刻意的。后面会看到这个“先搭框架、最后开闸”的顺序是避免上电炸管的关键。2.3 相位推进与查表三个CCR如何在中断里同步刷新SPWM的本质是让CCR值按正弦规律逐周期变化。基波频率50Hz、载波10kHz时一个基波周期里有200个载波周期所以把一张包含200个采样点的正弦表放进Flash每个载波周期取一个点写入CCR就能得到50Hz的正弦调制。U相直接用当前相位查表V相偏移1/3个周期W相偏移2/3个周期三相120°相位差由此而来。运行时的查表逻辑放在TIM1的更新中断里#define N_SAMPLE 200 /* 基波周期内采样点数 */ #define CENTER 1800 /* CCR中点, 对应50%占空比 */ #define PWM_HZ 10000 #define MIN_CCR 120 /* 给死区预留的最小脉宽 */ static uint16_t sin_table[N_SAMPLE]; /* 初始化时生成: 1800 1710*sin */ static uint32_t phase_q16; /* Q16定点相位累加器, 高16位查表 */ static uint32_t step_q16; /* 每载波周期的相位增量, 由频率决定 */ static uint16_t mod_q15; /* 调制比, 0~32768 对应 0~1.0 */ void TIM1_UP_IRQHandler(void) { if ((TIM1-SR TIM_SR_UIF) 0) return; TIM1-SR ~TIM_SR_UIF; uint16_t idx phase_q16 16; /* 相位整数部分 */ uint16_t iv (idx N_SAMPLE / 3) % N_SAMPLE; /* V相滞后120° */ uint16_t iw (idx 2 * N_SAMPLE / 3) % N_SAMPLE; /* W相滞后240° */ /* 调制: CCR CENTER (表值-CENTER)*mod_q15/32768 */ TIM1-CCR1 CENTER (int32_t)(sin_table[idx] - CENTER) * mod_q15 15; TIM1-CCR2 CENTER (int32_t)(sin_table[iv] - CENTER) * mod_q15 15; TIM1-CCR3 CENTER (int32_t)(sin_table[iw] - CENTER) * mod_q15 15; phase_q16 step_q16; /* 推进相位 */ if (phase_q16 (uint32_t)N_SAMPLE 16) phase_q16 - (uint32_t)N_SAMPLE 16; /* 绕回表头 */ }三个寄存器在同一中断里顺序赋值时间差只有几条指令对相间关系的影响完全可以忽略。正弦表在初始化时用浮点sin计算一次存下来运行时不调用任何浮点函数这是F103这类没有硬件FPU的芯片上最常见的做法把代价前置把运行期的中断负载压到最低。相位累加器用Q16定点格式step_q16的高16位对应“每个载波周期走几个采样点”低16位是亚采样点的相位余量。这么做的好处是低频时相位不会停滞——如果直接用整数步进5Hz时可能连续好几个载波周期步进都是0波形会出现肉眼可见的台阶。定点累加让频率分辨率不再受采样点数限制这是DDS思想的直接迁移。3. 死区机制那625纳秒“谁都别导通”的时间是怎么来的3.1 直通是硬伤开关管暂态决定死区必须存在所谓的互补输出逻辑上已经保证了上下管不会同时导通一个有效另一个必然无效。但真实功率管不是理想开关。MOS管有开通延迟、关断延迟和米勒平台IGBT还有拖尾电流。你在程序里看到的“同步翻转”反映到管子上是一条几十纳秒到几百纳秒的斜线。如果上管还没完全关断、下管就开始导通母线正负极通过两个管子直接短路这就是所谓的直通。直通的后果不是逻辑错误是物理损坏——管子瞬间过热炸管或者烧驱动。死区就是在这条斜线里硬性插入的一段“两管都关断”时间。工程上的做法很简单死区时间取功率管的关断延迟最大值再留一倍以上的裕量。对于C8T6常见的小功率板如果用的MOS管toff_max在200ns左右死区设在500ns到1μs之间既能覆盖暂态又不至于明显影响波形。3.2 死区发生器BDTR寄存器如何按档位量化时间死区不需要在中断里软件插入TIM1的硬件死区发生器会在OCx和OCxN的每个边沿自动处理。你只需要往BDTR寄存器的DTG位段写一个值。DTG是8位分成四档刻度这个分档设计是为了覆盖不同功率等级的需求小功率MOS管需要几十到几百纳秒的精细分辨率大功率IGBT需要几微秒的粗粒度。DTG[7:5]死区时间72MHz下的范围分辨率0xxDTG × tDTS0 ~ 1.76μs13.9ns10x(64 DTG[5:0]) × 2 × tDTS1.78 ~ 3.53μs27.8ns110(32 DTG[4:0]) × 8 × tDTS3.56 ~ 7.0μs111.1ns111(32 DTG[4:0]) × 16 × tDTS7.11 ~ 14.0μs222.2nstDTS由TIM_ClockDivision决定配置为DIV1时就是定时器时钟周期13.89ns。要得到625ns的死区写DTG45即可45×13.89ns≈625ns落在第一档里分辨率足够精细。/* 死区时间 45 x 13.89ns ≈ 625ns */ TIM_BDTRInitTypeDef bd; bd.TIM_OSSRState TIM_OSSRState_Disable; bd.TIM_OSSIState TIM_OSSIState_Disable; bd.TIM_LOCKLevel TIM_LOCKLevel_OFF; /* 锁定级别为OFF, 后续可再改DTG */ bd.TIM_DeadTime 45; bd.TIM_Break TIM_Break_Disable; bd.TIM_AutomaticOutput TIM_AutomaticOutput_Disable; TIM_BDTRInit(TIM1, bd); /* 最后一步才打开主输出, 在此之前六路引脚保持安全电平 */ TIM_CtrlPWMOutputs(TIM1, ENABLE);注意BDTR里的MOE位是整个输出链路的总闸。MOE为0时即使CCR和CNT的比较逻辑在跑引脚上也不会出现有效PWM。所以初始化顺序应该是GPIO拉低安全电平 → 时基配置 → OC配置 → BDTR配置 → 最后置位MOE。把这个顺序反过来很多板子第一次上电就烧驱动。3.3 死区的代价为什么“安全”和“波形质量”天然冲突死区保护了功率管但它不是免费的。死区期间两管全关负载电流没有路径从母线取电只能靠桥臂反并的续流二极管续流。此时桥臂中点电压不再由PWM指令决定而是由电流方向和二极管压降决定——输出波形里出现了一段不受控制的区域。我一般会这么理解死区误差每个PWM周期里上下管换流两次每次换流都损失一个死区时间。载波10kHz、死区625ns时这个损失约为2×625ns/100μs即1.25%的占空比。问题在于这个损失对正负半周不对称——电流为正时电压被压低电流为负时电压被抬高宏观表现就是基波电压变小同时叠加额外的低次谐波。死区越大、载波越高这个畸变越明显。这也是为什么正弦表的满幅不能直接用到3600即100%占空比。接近满幅时有效脉宽可能比死区还窄脉冲会被死区彻底吞掉。常见做法是把调制比上限限制在0.9~0.95让CCR的最大值不超过CENTER0.95×CENTER留出足够的脉宽给死区消耗。这个限幅看起来是“浪费”了调制范围实际是在安全性和波形质量之间取的平衡点。4. 调频调压为什么频率一变电压就必须跟着变4.1 从电机磁通说起V/F恒定的物理依据三相异步电机的定子绕组本质上是一个电感电机内部的感应电动势和频率、磁通的关系可以写成E≈4.44×f×N×Φ。搬一下这个公式就能看出要保持气隙磁通Φ不变E/f必须近似恒定。变频调速时只改频率、不动电压会出现两种后果频率降低时E/f变大磁通上升铁芯进入饱和区励磁电流暴涨电机没转起来先发热频率升高时E/f变小磁通不足电机的转矩输出跟着掉带不动负载。所以基频以下的调速策略是恒压频比也就是V/F恒定让磁通始终维持在额定值附近。反映到SPWM里载波频率固定在10kHz不动需要变的是两个量相位累加器的步进量决定基波频率和调制比决定输出电压幅值。调频和调压这两个动作必须同步不能只改其中一个。4.2 代码层的调频与调压一个步进量和一个调制比调频在代码里就是改step_q16。每个载波周期推进多少采样点直接决定基波频率void set_frequency(uint16_t freq_hz) { /* 相位步进: 每个载波周期推进的采样点数 */ step_q16 (uint32_t)freq_hz * N_SAMPLE * 65536U / PWM_HZ; if (freq_hz RATED_HZ) { mod_q15 32768; /* 额定及以上: 满调制, 恒压 */ } else { /* 基频以下 V/F 恒定: 调制比与频率成正比 */ mod_q15 (uint32_t)32768 * freq_hz / RATED_HZ; /* 低频补偿: 死区与管压降在低速时占比大, 需要额外抬电压 */ if (freq_hz BOOST_HZ) mod_q15 BOOST_Q15; } if (mod_q15 MAX_MOD_Q15) /* 限幅, 给死区留脉宽 */ mod_q15 MAX_MOD_Q15; }假定额定频率50Hz那么10Hz时mod_q15就是32768的1/5CCR的摆动幅度等比例缩小输出电压约为额定值的1/5。step_q16和mod_q15是成对更新的这保证在任何频率点上都满足V/F曲线。代码里MAX_MOD_Q15对应前面说的0.95调制比上限。即使freq_hz到了额定频率调制比也只能逼近0.95而非1.0原因就是死区要吃掉一部分脉宽——这是硬件机制对软件上限的约束不是你不想输出满压是物理上做不到还不产生畸变的满压。4.3 低频段为什么要做额外补偿V/F恒定是理想电机的法则实际逆变器在低频段会“失守”。原因有两层一是死区时间占总周期比例固定但低频时输出电压本身很小死区吃掉的那部分占比变得很大二是功率管导通时还有压降电流越大压降越明显输出电压比指令值低一截。实际中常用的是分段补偿比如10Hz以下把调制比按开环经验值额外抬升几个百分点抵消死区和管压降造成的电压损失。更精确的做法是检测电流方向按方向做符号相反的占空比补偿这就是死区补偿算法。在C8T6这类小功率场景里低频段用简单的BOOST_Q15加法已经能把空载和轻载调速做得比较平滑。若在重载或极低速下还感觉转矩不够再考虑往矢量控制方向走那已经是另一个量级的项目了。5. 常见问题避坑仿真能过、上板翻车的几类典型坑5.1 软件仿真看不见死区别把它当示波器用现象KEIL5里跑软件仿真用逻辑分析仪窗口看PA8和PB13两边看起来是严格互补的方波完全找不到死区的痕迹。于是怀疑死区配置没生效。原因软件仿真模拟的是寄存器状态和引脚电平变化采样粒度受指令执行步进限制纳秒级的硬件死区在这个模型里不会被展开而且仿真模型里没有功率管开关暂态本来也不需要死区。再一个原因是C8T6这种中等容量芯片在纯软件仿真下定时器外设模型本身就有简化。解决死区是否生效最终要以示波器实测为准。临时验证的办法是先把DTG写成一个大值比如0xFF对应约3.5μs档再上板看波形如果能看到明显的“两管都低”台阶说明死区极性方向正确然后改回正常值。软件仿真只用来验证相位关系、占空比变化和调频调压逻辑。5.2 上电瞬间上下管同时导通板子还没初始化就烧了现象刚接通母线电源驱动芯片或功率管立刻冒烟程序根本还没运行到PWM初始化。原因MCU复位期间GPIO处于浮空输入状态引脚电平不确定。如果外部驱动电路对浮空电平的默认响应是“上下管都有效”那么在程序接管引脚之前桥臂已经直通了。这种情况和死区多大无关纯粹是上电时序问题。解决启动代码里尽早把六个PWM引脚全部配置为推挽输出并拉低或按驱动电路的安全电平拉高然后再初始化TIM1最后才置位MOE。MOE为0期间引脚始终由GPIO控制不会输出PWM。要养成“先控脚、再控定时器”的顺序这是血泪经验。5.3 改了死区时间实际波形纹丝不动现象把DTG从45改成180期望死区从625ns变成更大的值示波器上看完全没变化。原因BDTR寄存器在MOE置位后对某些位有保护逻辑如果配置了LOCK位TIM_LOCKLevel不为OFFBDTR的关键位段还会被锁定必须复位才能改。很多标准库例程把LOCK设成了OFF但代码里之后又一次调用了BDTR写操作覆盖了之前的值。解决在初始化阶段一次性写好死区不要在运行中频繁改DTG。如果必须改先清MOE改完再重新使能MOE。同时检查TIM_LOCKLevel是否为OFF以及整个工程里是否有第二个地方重新初始化了TIM1。5.4 空载电流偏大相电流波形有台阶凹陷现象电机能转但空载电流接近额定值用示波器电流探头看相电流正弦波顶部或底部出现明显的削顶和台阶。原因调制比限幅没加CCR被推到接近0或3600的位置。中心对齐模式下这部分区域的有效脉宽已经小于死区时间死区发生器把整个脉冲吞掉输出电压进入非线性区电流波形自然畸变。解决给调制比设上限MAX_MOD_Q15按0.9到0.95折算同时在中断里检查CCR边界低于MIN_CCR时钳位到MIN_CCR。从公式上算MIN_CCR至少要大于死区对应的计数个数乘以2目前代码里留的120个计数对应约1.67μs大于死区的45个计数是安全的。5.5 调试暂停后再继续PWM输出再也回不来现象KEIL里打了断点单步看完某段代码后按全速运行电机不动了PWM引脚没有任何输出。原因仿真器暂停时默认冻结外设时钟TIM1计数器停住如果此时发生了刹车事件或者MOE被调试器意外清除恢复运行后MOE不会自动置位输出始终处于关闭状态。从软件模型上看寄存器值都对但就是没有输出。解决调试时把TIM1从DBGMCU的冻结列表里排除或者在恢复运行的代码路径上重新调用一次TIM_CtrlPWMOutputs(TIM1, ENABLE)。更稳妥的办法是不要在主循环里打断点把断点打在无关紧要的辅助函数里避免触碰PWM输出路径。6. 设计取舍中断查表、DMA搬运与死区验证的选型判断这套SPWM方案有两个关键取舍三相CCR的更新方式以及死区时间的验证方法。中断查表方案的优势是灵活。调制比限幅、低频补偿、起动时的S曲线都可以在中断里顺手做代码路径短10kHz中断每次执行不超过几十条指令对72MHz的C8T6来说CPU占用率不到2%。缺点是每个载波周期都要进一次中断如果以后还要叠加PID、编码器采样和串口通信中断负载会逐渐吃紧。DMA搬运方案把三相数据在内存里交错排布用TIM1的更新事件触发DMA把三个半字依次写入DMAR寄存器定时器内部的Burst机制会自动把它们分发到CCR1、CCR2、CCR3。CPU零负载但代价是补偿逻辑没法动态做了调压、低频补偿都得提前算进表里每次改变工况可能要切换整张表灵活性差很多。我的判断是做产品原型和教学验证中断查表是首选逻辑透明、容易调参做量产固件且已经确认控制算法收敛再迁到DMA方案节约中断开销。死区验证这件事我一般用两个手段交叉确认。第一是代数核对载波频率10kHz时最小有效占空比约等于2×死区时间×载波频率625ns死区对应1.25%的死区损失因此调制比上限设0.95是合理的选择第二是示波器实测CH1和CH1N两路接A/B通道把时基放到500ns量级直接量两路同时为低的台阶宽度。空载首次通电时在母线回路串一个限流电阻或灯泡确认六路波形正常再撤掉——这个习惯救过我两次。从那以后我每次改死区都会先算一遍最小脉宽再回头检查DTG值和调制比上限把这三件事当成一个整体来调而不是单独拧一个参数。希望帮到你。本文还有配套的精品资源点击获取