
简介基于STM32的三相SPWM波输出压缩包面向电机控制与逆变器方向的嵌入式开发者重点解决用STM32高级定时器生成相位差120度的三相正弦脉宽调制波。工程覆盖PWM模式配置、预分频、比较寄存器占空比、死区插入用三角载波与正弦表比较产生互补输出同时给出三相平衡调整、调制指数优化、中断连续更新等处理方法适合有STM32基础、想深入电机驱动的工程师。包内共174个文件以C源文件、H头文件为主另有uvproj工程文件、hex烧录文件、map映射文件、axf调试文件及编译中间结果压缩包约2.81MB目前已有564人学习下载。通过阅读源码和对比示波器波形可掌握从定时器初始化到三相SPWM输出的完整链路并将载波生成、死区控制、换相逻辑迁移到电机驱动或逆变器项目中。1. 三相SPWM输出到底在解决什么问题三相逆变器第一次上电最常见的坑不是炸管而是三个桥臂的PWM看着都不太对放到电机上却嗡嗡尖叫、电流波形畸变。根子往往不在功率级而在STM32输出的三路SPWM相位彼此差了多少、死区够不够、每个载波周期有没有按正弦规律更新。基于STM32的三相SPWM波输出就是要把三路载波同步的PWM按正弦函数调制成互差120°的占空比序列再通过半桥逆变得到三相对称电压。这个调制层做好了后续接电流环、速度环、编码器对位都不会被波形拖后腿。适合正在做变频器、UPS、伺服驱动或电机调速开发的工程师也适合用标准库或HAL库把毕设基础打牢的在校读者。2. 调制机理与定时器选型为什么是STM32的TIM1/TIM82.1 三角载波与正弦调制波的数学关系SPWM的本质是用“占空比按正弦变化”的方波去逼近正弦电压。把定时器的计数周期看作三角载波正弦调制波与三角波比较得到输出翻转点换算到每个开关周期内的平均电压就是D(t) (1 M·sin(ωt)) / 2满占空比时输出接近母线电压的一半M是调制比0到1之间ωt是当前正弦角度。由于PWM频率远高于输出正弦频率电感的伏秒积分会把高频分量滤掉桥臂中点电压的基波正比于D(t)谐波则集中在载波频率的整数倍附近。注意这个公式是单相的表达。做三相SPWM只需要把同一个公式套到三个相位上A相用ωtB相用ωt - 2π/3C相用ωt 2π/3。三路PWM的载波必须同频同相这样三相线电压里才能抵消掉共模的载波分量。这也是为什么三个通道要用同一个定时器、同一个计数器而不是随便开三个软定时器去拼。2.2 为什么高级定时器TIM1/TIM8比通用定时器更适合STM32F103里能直接用于三相半桥的定时器只有TIM1和TIM8它们在引脚上引出CH1~CH3和互补输出CH1N~CH3N还内置死区发生器和刹车输入。通用定时器TIM2/3/4/5虽然也有PWM模式但要得到六路带死区的互补波得自己搭延迟电路或用外部逻辑门时序一致性很难保证。特点对比如表特性TIM1 / TIM8TIM2/3/4/5三路独立CCR输出有有互补输出 CHx/CHxN有无可编程死区DTG有无主输出使能 MOE有无刹车输入有可做硬件过流封波无更新事件挂DMA有有工程上做三相逆变桥只需要TIM1的CH1~CH3分别驱动A/B/C相的上桥臂CH1N~CH3N驱动对应的下桥臂死区由硬件在翻转沿自动插入。如果只是做三路独立的SPWM信号不带互补TIM1同样能胜任。一个误区是以为三路PWM来自三个不同通道就能直接输出给全桥实际上六个MOS管需要的是互补波通道数不够时肯定要外扩选型时建议一步到位选带CHxN的芯片。2.3 最小可运行的TIM1初始化用HAL库配置时核心是中心对齐模式、PWM模式1、使能三个通道输出。以下按20kHz载波、72MHz定时器时钟计算配置ARR等于1800。TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 1800 - 1; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 1; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 900; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_3);关键参数解释Prescaler为0让TIM1直接工作在72MHzCCR写入精度约13.9ns。Period1800-1中心对齐模式下PWM频率 72MHz / (2 × 1800) 20kHz。RepetitionCounter1让更新事件在每个载波周期只发生一次避免上下两个计数方向各触发一次。Pulse900是占空比50%的中点运行后会被正弦表覆盖。OCIdleState/OCNIdleState用于空闲态电平如果这里配反会出现停机时桥臂直通上电前先确认。GPIO要复用为TIM1PA8/PA9/PA10对应CH1/CH2/CH3PB13/PB14/PB15对应CH1N/CH2N/CH3N。接着配置死区和刹车在主输出使能前所有输出都是关闭的这能避免上电瞬间桥臂直通sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 72; /* 72MHz下对应1μs */ sBreakDeadTimeConfig.BreakState TIM_BREAK_DISABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3); __HAL_TIM_MOE_ENABLE(htim1);DeadTime字段不是“微秒”也不是“纳秒”它是BDTR寄存器里DTG[7:0]的原始值。72MHz下写成72得到1μs死区如果定时器时钟改成其他频率这个数字必须重新算。BreakState先关闭调试正常后再把它接到硬件过流信号上配合MOE做快速封波比软件中断封波快一个数量级。3. 三相SPWM生成的三块积木正弦表、查表索引与更新触发3.1 正弦表怎么建提前算好绝不在中断里算三角函数SPWM的常见实现是查表法把正弦函数的一个完整周期量化成固定个数的点存进ROM每个载波周期按索引取出对应的CCR值。查表相比在中断里实时调用sinf()速度快、执行时间确定对72MHz的STM32F103来说能省出大量CPU。表长取2的幂后续用位与代替取模能让中断延迟更稳定。表长选择参考载波20kHz、工频50Hz时每个工频周期正好有400个载波周期所以512点足够如果载波只有10kHz256点就够。点数再往上加谐波失真改善有限但Flash占用翻倍。#define TAB_LEN 512 static uint16_t sin3_table[TAB_LEN][3]; void build_sin3_table(uint16_t arr) { for (uint32_t i 0; i TAB_LEN; i) { double w 2.0 * 3.14159265358979 * i / TAB_LEN; float d_a 0.5f 0.45f * sinf((float)w); float d_b 0.5f 0.45f * sinf((float)(w - 2.0 * 3.14159265358979 / 3.0)); float d_c 0.5f 0.45f * sinf((float)(w 2.0 * 3.14159265358979 / 3.0)); sin3_table[i][0] (uint16_t)(d_a * (float)(arr - 1)); sin3_table[i][1] (uint16_t)(d_b * (float)(arr - 1)); sin3_table[i][2] (uint16_t)(d_c * (float)(arr - 1)); } }这段代码做了什么w是当前查表相位d_a/d_b/d_c是三相调制波在一个点上的占空比。d的取值在0.05到0.95之间摆动等效调制比0.9不会出现100%占空比。乘(arr-1)把占空比换算成CCR比较值范围0~1799。如果把0.45改成0.5调制比到1但满载时上下桥臂的最小导通时间趋近于零IGBT/MOS管的驱动芯片可能来不及完全关断建议保留余量。工程机上如果不想在启动时调sinf可以直接把这个表预生成成const数组烧进Flash连建表时间都省掉。3.2 查表索引一个载波周期走一步系统上电后每次更新事件把索引pos加1再从表中取三列值写到CH1~CH3的CCR。输出正弦频率由索引步进决定f_sin f_carrier × step / TAB_LENstep1、f_carrier20kHz、TAB_LEN512时输出约39.06Hz离常见的50Hz差一些。想精确到50Hz可以改用DDS累加器用一个32位相位累加器每次更新加上一个步长取高9位做表索引步长为0x10000 × f_sin / f_carrier × TAB_LEN。这样频率调节精度比整数索引好得多而且频率变化时不会相位跳变。3.3 更新触发用TIM1更新中断把三路CCR一次刷掉最小可跑版本用更新中断在中断里更新三个比较寄存器。20kHz的更新频率在72MHz主频下完全能承受中断里只有三次写寄存器实测占用CPU在5%以内。static uint32_t pos 0; void TIM1_UP_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim1, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); uint32_t idx (pos) (TAB_LEN - 1); htim1.Instance-CCR1 sin3_table[idx][0]; htim1.Instance-CCR2 sin3_table[idx][1]; htim1.Instance-CCR3 sin3_table[idx][2]; } }这里直接写寄存器而不是调用HAL_TIM_SET_COMPARE能省掉函数进入退出的开销。TAB_LEN是2的幂所以用与运算取模如果表长改成非2的幂必须改成取余运算否则索引越界会读到垃圾数据。pos是无符号32位跑几天也不会溢出导致驱动卡死。中断里如果需要调压可以在更新事件之外动态改0.45这个系数但要注意不要让d_a超出0.5±0.5的边界否则会出现削顶。3.4 进阶把更新频率完全交给DMA如果载波频率提到50kHz以上或者主循环里要跑浮点运算可以把三路CCR的更新交给DMA。做法是预先建立三个长度等于TAB_LEN的DMA缓冲分别对应CH1~CH3用TIM1的更新事件触发DMA搬运。HAL里启动的是HAL_TIM_PWM_Start_DMA每次更新事件自动搬运一个word到CCR搬运完一轮后可用循环模式自动回卷。相比中断版本CPU完全不进ISR波形不会和主循环抖动。它的坑也在这三个通道要同时更新但三条DMA流各自搬运如果其中一路被更高优先级的DMA抢占那一周期里三个CCR的刷新时刻就不一致相电压会出现一个载波周期的毛刺。稳健做法是把三个CCR值连续放在一个结构体里用DMA的burst模式一次搬三个word或者保留一条低优先级的TIM更新中断做三路值的一致性校验。先跑通中断版再优化到DMA是效率最高的路径。4. 载波频率、死区与调制比三相SPWM的三个必调参数4.1 载波频率先解决噪音和损耗的矛盾载波频率直接决定三点输出纹波大小、开关损耗、是否落在人耳可听区。电机驱动常用16kHz以上避开听觉敏感区但功率管开关损耗线性上升工业大功率变频器载波反而会降到2~4kHz靠输出电抗器弥补谐波。参考选型应用场景载波频率主要考虑小功率BLDC/交流伺服20kHz~50kHz低噪音、滤波体积小中大功率变频器4kHz~10kHz温升与IGBT损耗平衡大功率整流/逆变电源2kHz~8kHz散热、开关损耗车载或精密电源20kHz~100kHz磁件体积、响应速度同一个定时器配置改变载波就是改Period。以TIM1时钟72MHz为例中心对齐模式下载波20kHz对应ARR180010kHz对应3600调高到50kHz则是720。注意同时检查Period和实际应用的死区是否匹配载波越高死区占一个周期比例越大输出基波损失也越明显。4.2 死区时间DTG不是微秒是寄存器值死区是给功率管上下桥臂换流留出的不导通时间防止高侧还没完全关断低侧就导通。最小死区等于驱动芯片关断延迟减开启延迟再加安全余量。IGBT一般取1~3μsMOSFET几百纳秒。上文配置里DeadTime72在72MHz下等于72个tDTS即1μs这只适用于DTG值小于128的情况。ST的BDTR寄存器对DTG[7:0]做了分段编码不是线性的。DTG在0~127时死区等于DTG × tDTSDTG[7:5]为10x时死区等于(64 DTG[4:0]) × 2 × tDTSDTG[7:5]为11x时按8或16倍增。工程上常犯的错是看到数据手册写“最大死区”直接把App里应填的秒数当DTG用导致死区比预期大几倍。设完死区后用示波器量一下实际宽度比对着寄存器算可靠。死区过大的直接后果是输出电压基波下降相电流畸变率升高电机在低速轻载时容易振动。4.3 调制比与查表点数为什么不是越高越好调制比M就是查表代码里乘的那个0.45对应的比例。M1代表占空比在0~100%间摆满整个正弦波理想情况下输出基波最大但功率管的关断时间和栅极驱动都吃不满实际工程至少要留5%~10%裕量这就是代码里用0.9倍调制比的直接原因。查表点数与载波比的匹配更重要。查表法在每个载波周期只更新一次CCR如果表长小于载波比输出阶梯就粗如果表长远大于载波比很多点根本走不到白白占Flash。这里记住一个估算公式TAB_LEN ≥ f_carrier / f_sin_max其中f_sin_max是最大输出正弦频率。20kHz载波、50Hz输出400点起要做到100Hz比如同步电机弱磁区表至少要200点工程上取512已相当宽裕。到调制比接近1且要最大母线电压利用率时三相SPWM线性区就到头了再往上要么过调制要么切SVPWM加零序分量注入。5. 验证与排错从方波瞪到三相实波5.1 三步验证从MCU到功率级第一步不接功率级示波器探头接TIM1的CH1/CH2/CH3引脚参考接GND能看到三路占空比随时间张开、合拢的方波。把两个通道开加法或余晖模式逐帧确认相位差120°。这一步可以筛掉查表索引错误和相位写反的问题。第二步看一对互补波CH1和CH1N同时接两路探头打开死区放大。死区如果在1μs左右且出现在上升沿说明BDTR配置生效。如果两只管子存在同时导通的区间波形上会看到一条直通的谷这个必须在上电前排除。第三步接阻感负载或电机测输出线电压Uab和相电流。线电压应为正负交替的SPWM脉冲电流波形应接近正弦。若电流波形有削顶多半是调制比过高查母线电压与目标电压间的裕量。5.2 常见故障与排查顺序现象优先检查项三路输出全是恒低或恒高MOE未使能、GPIO复用没配、通道没Start只有上桥臂输出下桥臂全关OCNPolarity或OCNIdleState配置错误死区比预期大很多DTG编码非线性用实测波形确认每隔固定周期出现一次毛刺DMA回卷或索引溢出先换回取余电机啸叫但能转载波频率太低或死区占空比过大先提载波上电瞬间炸管死区过小或BreakState时序不对加硬件过流刹车最后一招是拉长观察尺度把示波器时基放到10ms/div占空比包络应当是一个完整的正弦形状再缩回50ns/div量死区。硬件上建议在母线正负端各并一个0.1μF高频电容减少功率管开关噪声对TIM1参考地的影响这一条能省掉很多“波形明明看着对却偶然误触发”的定位时间。用示波器的数学通道做CH1减CH2能直接量到两相线电压的SPWM脉冲序列边沿处是否有重叠一目了然这个方法比纯肉眼看死区更可靠。本文还有配套的精品资源点击获取