
简介这是一套面向三相电机驱动与变频控制场景的STM32 SPWM实现方案基于STM32F103C8T6高级定时器TIM1通过查表法生成三相六路互补且带死区的SPWM信号相位互差120度并按恒压频比策略完成调频调压适用于有一定定时器与PWM基础的嵌入式开发者学习、仿真和二次开发。资源共149个文件包含C/H源代码、KEIL5工程文件、HEX/AXF编译产物、PDF原理文档及正弦表生成工具、死区计算器等压缩包大小46.67MB目录结构清晰便于按需查阅。已有8053人学习下载程序带详细注解支持在线仿真直接观察SPWM输出波形配套文档可帮助理解SPWM生成原理、死区配置与恒压频比调频调压逻辑。正弦表生成工具与死区计算器能辅助生成自定义SPWM数据并快速配置互补输出参数配合可直接查看的调试文件可为后续移植到其他STM32型号或更大功率电机驱动提供参考。 我们项目里要做一个小型三相电机驱动器主控板预算压得紧又要快速出样验证所以把目光落在STM32F103C8T6上。这块芯片在国内电子圈子里出镜率实在太高了——72MHz主频带一个高级定时器TIM1刚好能输出三相六路互补PWM再加上硬件级的死区、刹车、空闲电平控制做低压小功率SPWM变频器主控绰绰有余。这篇文章我就把整个方案的细节完整过一遍SPWM正弦波生成的查表法怎么做、TIM1的互补输出死区怎么算、调频调压的V/F曲线怎么落地以及我在样机实测时踩过的几个坑。想用F103C8T6做逆变器、变频器、电机驱动的朋友照着这个思路走能少走不少弯路。1. 选型逻辑为什么F103C8T6能扛下整个三相驱动1.1 一颗48脚芯片的PWM资源家底先说芯片本身。STM32F103C8T6Cortex-M3内核最高72MHz64KB Flash、20KB SRAMLQFP48封装。从资源表上看这颗芯片真的很能打1个高级定时器TIM1支持三对互补PWM输出带死区、刹车、编码器接口3个通用定时器TIM2/3/4可以出单路PWM或做输入捕获3个USART、2个SPI、2个I2C、1个USB、1个CAN2个12位ADC采样速率最高1MHz三相SPWM驱动器要求什么核心就是六路PWM上桥臂三路下桥臂三路同一桥臂上下两路必须互补并且中间要有死区。F103C8T6的TIM1几乎是为这个场景量身定做的它自带三对互补输出通道CH1/CH1N、CH2/CH2N、CH3/CH3N死区由硬件生成完全不需要CPU干预。再算算性能账。F103C8T6主频72MHz做16kHz载波每次更新中断里大概要做三次查表、三次16位乘法、三次寄存器写加起来也就是几微秒的事中断占用CPU不到5%。剩下95%的资源足够跑Modbus通信、按键扫描、LED显示和基本的保护逻辑。对于中低端变频器这笔性能账是算得平的。想做更高端的FOC、伺服控制F103确实吃力但做纯SPWM的V/F控制它是妥妥的够用级。1.2 TIM1的互补通道和硬件保护TIM1和其他通用定时器最大的区别不只是一组互补输出引脚而是整套输出保护机制。BDTR寄存器统管死区时间和主输出使能BKIN引脚可以接收外部刹车信号一旦刹车有效六路输出会被硬件强制拉到预设的安全电平这个响应速度是纯软件保护追不上的。TIM1的引脚分配是第一个要背下来的知识点通道正常输出互补输出CH1PA8PB13CH2PA9PB14CH3PA10PB15BKINPB12-注意PA9和PA10同时也是USART1的TX/RX。很多新手调完PWM发现串口打印不出来一查引脚撞了。解决办法是把USART1重映射到PB6/PB7或者改用其他串口。另外很多人做软件容易忽略空闲电平Idle Level的设置。它表示刹车发生后PWM引脚停在哪个电平。如果把空闲电平配成和正常导通电平相反就能做到刹车时全桥禁止输出配反了刹车反而等于让管子全开那是灾难性的。F103每个通道有独立的CCxP和CCxNP极性位实际项目中一定要按驱动板的安全逻辑去配。1.3 最小系统板也能当驱动主控热词里很多人搜stm32f103c8t6最小系统板说明不少朋友是拿现成的最小系统板做实验。这个路线完全可行但要注意几点最小系统板的8MHz晶振和BOOT跳线一般已经排好SWD下载程序很省心把PA8/PA9/PA10和PB13/PB14/PB15引出来直接接到驱动板的PWM输入接口主控板和驱动板必须共地否则高侧驱动信号会出现抖动或误触发功率稍大时建议用光耦隔离隔离后的供电也要单独处理我第一次样机就是用蓝色最小系统板直接接IR2101驱动电路带一台小型三相异步电机跑SPWM波形干净完全够用。等方案稳定了再画一块整合主控和驱动的一体板把成本压下来。2. SPWM生成从正弦表到定时器更新2.1 SPWM的本质与三相相位关系SPWM的原理用一句话讲拿一个高频三角波当刻度尺拿一个低频正弦波当目标值两者比较正弦值大于三角波的部分输出高电平小于的部分输出低电平出来的方波脉冲宽度就按正弦规律变化。把这样的PWM波送进电机绕组绕组的电感天然就是低通滤波器电流自然接近正弦。三相SPWM就是三个相位差120°的正弦调制波同时和同一个三角载波比较得到三路占空比不同、相位各差120°的PWM。六路互补输出本质上是把这三路PWM取反用来驱动同一桥臂的上下管。数字实现上常见两种方式。一种是实时计算法在中断里直接调用sinf()算占空比另一种是查表法预先存一个周期的正弦采样值中断里直接查表。F103没有FPU也没有硬件三角函数实时算一个sin要几十微秒在16kHz中断下CPU直接被打满还会引入相位抖动。所以工程上几乎都选查表法。2.2 正弦表怎么生成建表之前先确定两件事表长和表值格式。表长我习惯用512点一个完整正弦周期采样512个点在16kHz载波下已经足够平滑。当然也可以用1024点Flash占用才2KB没压力。表值格式用Q12定点数也就是把0~1.0的正弦值放大4096倍存成uint16_t取值范围0~4095。这样在整数运算中保留足够精度避开浮点运算。生成表格可以直接用PC端脚本也可以写死在代码数组里。给一段Python生成脚本方便你做任意长度的表import math TABLE_SIZE 512 Q 12 # 1.0 映射到 4096 with open(sin_table.h, w) as f: f.write(fstatic const uint16_t sin_table[{TABLE_SIZE}] {{\n) for i in range(TABLE_SIZE): s int((math.sin(2 * math.pi * i / TABLE_SIZE) 1.0) * 0.5 * (1 Q)) f.write(f {s},\n) f.write(};\n)注意这里生成的是(sin 1) / 2 * 4096把正弦值偏移到0~4095。为什么偏移因为SPWM的占空比不能为负。偏移后正弦波被叠加在50%占空比基线上查表值从0变化到满幅对应CCR从0变化到ARR正好覆盖整个线性调制区。2.3 查表步进与三相互差120°的实现有了表如何在做调频的同时保持三相120°相位差核心是相位累加器。把360°映射到uint32_t的满量程也就是2^32次对应360°。那么120°就是0x55555555240°就是0xAAAAAAAA。三个通道各自维护一个累加器初始值就错开这两个常量。每次进入定时器更新中断三个累加器都累加一个步进step。步进由目标输出频率和载波频率决定step 2^32 × (目标输出频率 / 载波频率)假如载波16kHz目标输出50Hz那每个载波周期应该前进50/16000个正弦周期。用整数实现uint32_t step (uint32_t)((float)0xFFFFFFFF * 50.0f / 16000.0f);中断里查表并更新占空比#define TABLE_SIZE 512 #define PHASE_120 0x55555555 #define PHASE_240 0xAAAAAAAA static uint32_t phase_u 0; static uint32_t phase_v PHASE_120; static uint32_t phase_w PHASE_240; static uint32_t step 0; static uint16_t modulation 0; // 0~4096对应调制比 0~1.0 void TIM1_UP_IRQHandler(void) { if (TIM1-SR TIM_SR_UIF) { TIM1-SR ~TIM_SR_UIF; uint16_t idx_u (phase_u (32 - 9)) (TABLE_SIZE - 1); uint16_t idx_v (phase_v (32 - 9)) (TABLE_SIZE - 1); uint16_t idx_w (phase_w (32 - 9)) (TABLE_SIZE - 1); TIM1-CCR1 (uint16_t)((sin_table[idx_u] * modulation) 12); TIM1-CCR2 (uint16_t)((sin_table[idx_v] * modulation) 12); TIM1-CCR3 (uint16_t)((sin_table[idx_w] * modulation) 12); phase_u step; phase_v step; phase_w step; } }这里用相位高9位做表索引因为512 2^9。32位累加器自然溢出回卷相位永远不会越界。三个相位各自累加初始差一直保持120°关系就一直存在。2.4 中心对齐模式下的更新时机F103的TIM1支持边沿对齐和中心对齐两种计数模式。SPWM强烈建议用中心对齐计数器先向上数到ARR再向下数到0形成三角载波。这种模式下PWM脉冲对称谐波能量集中在载波频率的奇数倍附近比边沿对齐好滤除。中心对齐模式的ARR和载波频率关系f_carrier 72MHz / (2 × (ARR 1))比如想要15kHz载波ARR 1 72MHz / (2 × 15000) 2400 ARR 2399CCR的范围是0~2399CCR1200时占空比约50%。中心对齐模式下更新CCR的时机很有讲究我习惯配置为中央对齐模式1CMS01让更新事件发生在计数器向下计数到0的时刻。这样相邻两个载波周期内CCR均匀过渡波形不会出现半个周期的占空比突变。CubeMX里对应选项是Counter Mode选Center Aligned mode1或mode2然后把TIM1 update中断打开。3. 死区配置给桥臂留一道安全距离3.1 直通短路是怎么发生的先说一个物理场景桥臂上管正在导通下管关断。你给下管发关断信号它不可能立刻完全截止而是要经过一段关断延时td_off如果这时候上管已经收到开通信号并开始导通上下两个管子就会在极短时间里同时导通直流母线直接被短路。轻则波形毛刺、发热严重重则直接烧掉功放模块。死区Dead Time就是在互补PWM的上下沿之间故意插入一段两路都关断的时间。上级管关断提前一点下级管开通延后一点给功率器件留出换流空当。STM32的TIM1在硬件里就支持这个功能我们只需要往寄存器里填一个死区值。3.2 DTG寄存器与死区时间的完整换算TIM1的死区时间由BDTR寄存器的DTG[7:0]字段控制硬件根据DTG的高三位自动选择档位。设Tdts为死区时钟周期各档位的对应关系如下DTG[7:5]死区时间公式0xxDTG[7:0] × Tdts10x(64 DTG[5:0]) × Tdts110(320 DTG[5:0]) × Tdts111(544 DTG[4:0]) × 2 × TdtsTdts 1 / 死区时钟频率而死区时钟频率由定时器时钟和DTS分频决定。F103的TIM1时钟默认72MHzDTS默认等于CK_INT所以Tdts 13.89ns。举个实际计算的例子。目标死区1μs想用第一档0xx计算过程1μs / 13.89ns 72 DTG[7:0] 72 0x480x48的高三位是010确实落在0xx档配置正确。再看一个容易踩坑的例子目标死区3μs3μs / 13.89ns 216 0xD80xD8的高三位是110实际会进入110档算出来是(320 0x18)×13.89ns ≈ 6.28μs完全不是3μs。这种看似能行实则不行的档位陷阱非常隐蔽。稳妥做法是把DTS切成CK_INT/2也就是Tdts 27.78ns此时3μs / 27.78ns 108 0x6C0x6C高三位是011落在0xx档108×27.78ns 3μs正确。DTS分频由TIM1_CR1寄存器的CKD[1:0]位控制配置代码里要记得一起处理。3.3 死区时间选多大合适死区不是越短越好也不是越长越安全。它由功率器件的开关速度、驱动电路的传输延迟、以及PWM载波周期共同决定IGBT一般1μs~5μsIGBT关断拖尾电流时间长MOSFET100ns~1μs开关速度快很多驱动芯片IR2101等本身有几百纳秒的固有延迟也要算进去载波越高死区占整个周期的比例越大对输出电压影响越明显。20kHz载波下2μs死区占比4%输出波形已经能看出畸变我个人的建议是先用示波器测一下驱动芯片输入到输出的延迟再结合管子手册里的td_off两者相加再乘1.5~2倍作为死区初值。然后看母线电流波形如果没有明显的电流尖峰和额外发热再尝试逐步减小。不要一上来就按手册最大值设否则低速下死区造成的非线性会很难受。死区还会吃掉一部分输出电压让输出基波电压比理想值低一点同时带来3、5、7次谐波。如果项目对低速性能要求高可以考虑死区补偿思路是采样三相电流方向在占空比上叠加一个补偿时间量。这里不展开算法细节但你得知道这个问题的存在后续调机才不会两眼一抹黑。4. 调频调压的实现V/F曲线的工程落地4.1 为什么调频必须同步调压三相异步电机或永磁同步电机定子绕组上的感应电动势和频率、磁通的关系大致是E ≈ 4.44 × f × N × Φ如果只调频率不调电压频率升高时绕组感抗增大磁通会下降电机出现有频率没力气的疲软感频率降低时磁通增大又容易磁饱和、励磁电流飙升。所以要让电机在整个调速范围内都有平稳转矩就要保持电压和频率的比值近似恒定这就是恒压频比控制也就是常说的V/F控制。V/F控制是电机开环调速最经典、最省事的方案。不需要编码器不需要电流环只要驱动器输出频率可调、电压跟频率走的准三相正弦电压电机就能稳定转动。4.2 V/F曲线与低频补偿理想V/F曲线是一条过原点的直线V k × f。但实际低速下有个问题——定子绕组的电阻压降在低速时占大头。比如额定220V、50Hz的电机5Hz时理想电压只有22V但电阻上可能就要吃掉好几伏真正加在励磁上的电压所剩无几电机带载能力严重下降。工程上的做法是低频段抬高电压常用两段折线0 ~ f_b点 V 额定V/F斜率 × f 一个固定抬升电压 f_b ~ 额定频率 V 额定V/F斜率 × f这个抬升电压就是常说的低频转矩提升也叫Boost。Boost不是越大越好过大会导致低频励磁过饱和电机空载电流暴涨、发热。我一般先在5Hz点给5%~10%的抬升带载测试看电流有无改善再微调。程序里可以用一个分段函数实现float vf_ratio 220.0f / 50.0f; // 额定线电压 / 额定频率 float boost 8.0f; // 低频补偿电压单位V float target_volt; if (target_freq 10.0f) { target_volt vf_ratio * target_freq boost * (1.0f - target_freq / 10.0f); } else { target_volt vf_ratio * target_freq; }频率上限也要设定普通电机50Hz变频电机可到100Hz甚至更高。超过额定频率后电压一般不再上升进入弱磁区转矩自然下降这是正常特性。4.3 调制比、输出电压与ARR的换算数字SPWM里调电压的本质是调调制比m。调制比定义为正弦调制波幅值除以载波幅值线性区内m在0到1之间。我的代码里用一个0~4096的modulation变量表示4096就是m1.0。三相SPWM在线性区输出相电压基波峰值为U_phase_peak (Udc / 2) × m对于三相三线制负载线电压基波有效值约等于U_line_rms ≈ 0.612 × Udc × m如果直流母线是310V220V交流整流m1时线电压有效值约190V距220V差一点。这就是纯正弦SPWM电压利用率偏低的根源。很多变频器为了拉满电压会改用SVPWM或者注入三次谐波的鞍形波把电压利用率提升约15%。如果你的项目要求直接输出220V后续可以考虑升级到SVPWM底层六路PWM和死区配置完全一样架构不用大改。实际调压流程很简单用户给定目标电压 U_target m U_target / U_max_linear modulation (uint16_t)(m × 4096) 中断里查表后乘 modulation右移12位得到CCR这样一个整数乘法就同时完成了调频调压实现成本极低。5. 完整代码框架与实测调试记录5.1 工程结构主循环和中断各自干什么为了让方案具备可复现性我把工程的最小主干整理出来。主循环负责用户交互和参数设定PWM输出完全由TIM1更新中断驱动两者互不干扰int main(void) { SystemInit(); GPIO_Config(); TIM1_PWM_Config(2399, 2000); // ARR2399, 死区2000ns USART1_Config(); set_target_freq(50.0f); // 默认50Hz set_modulation(0.8f); // 默认调制比0.8 while (1) { process_serial_command(); // 串口命令更新目标频率和调制比 update_vf_curve(); // 根据V/F曲线计算新调制比 } }TIM1的初始化核心代码static void TIM1_PWM_Config(uint16_t arr, uint16_t dead_ns) { RCC-APB2ENR | RCC_APB2ENR_TIM1EN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; // PA8 PA9 PA10 推挽复用输出 GPIOA-CRH (GPIOA-CRH 0xFFFFF000) | 0x00000BBB; // PB13 PB14 PB15 推挽复用输出 GPIOB-CRH (GPIOB-CRH 0x000FFFFF) | 0xBBB00000; TIM1-PSC 0; TIM1-ARR arr; // 中心对齐模式1更新事件发生在计数器向下计数到0时 TIM1-CR1 | TIM_CR1_CMS_0; // 三个通道都设为PWM模式1 TIM1-CCMR1 | TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1; TIM1-CCMR1 | TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1; TIM1-CCMR2 | TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1; // 使能六路互补输出 TIM1-CCER | TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE | TIM_CCER_CC3E | TIM_CCER_CC3NE; // 死区配置按前文公式换算先配好再开主输出 TIM1-BDTR | dead_time_calc(dead_ns); TIM1-BDTR | TIM_BDTR_MOE; // 主输出使能最后一步打开 TIM1-DIER | TIM_DIER_UIE; NVIC_EnableIRQ(TIM1_UP_IRQn); TIM1-CR1 | TIM_CR1_CEN; }死区换算函数可以做成这样注意DTS分频要提前设好uint16_t dead_time_calc(uint16_t ns) { // 简化版DTS36MHz (Tdts≈27.78ns)只处理0~7μs范围 uint32_t dts_ns 28; uint8_t dtg (uint8_t)(ns / dts_ns); if (dtg 127) { // 超出第一档范围时要根据档位公式扩展这里按实际需求处理 dtg 127; } return dtg; }5.2 示波器实测波形与常见异常联调时用示波器抓三个关键位置基本能定位90%的问题。第一个是同一桥臂的CH1和CH1N。正常时两个方波互补切换边沿处能看到一段俩管都为低的平台那就是死区。如果看到两个信号有同时为高的时刻立刻断电检查说明死区没生效或者极性配反了。第二个是A相和B相PWM的时序关系。用CH1做触发看CH2脉冲中心相对位置应该错开约120°的载波周期。相位差不对时优先查phase初始值和表方向。第三个是把PWM经RC低通后的正弦轮廓。能看到干净的正弦波叠加高频三角波纹波说明SPWM基本可用。波形有削顶多半是调制比接近或超过1.0波形有低频周期性凸起三路表索引的相位累积可能不一致。我调试中常遇到的异常有以下几种完全没波形优先查BDTR.MOE查GPIO复用配置查有没有把PA9/PA10当串口用了上下管同时导通查死区配置、CCER极性位查驱动芯片是否在关断时倒灌波形不对称中心对齐模式下更新时机不对把更新事件改到过0点触发电机抖动载波频率太低或V/F曲线低频段补偿不足也可能是死区太大造成低速电流畸变5.3 我在这个项目里踩过的最深的三个坑第一个坑是死区档位的数值陷阱。当时想要2μs死区按0xx档算了144随手填成DTG0x90结果只有1.11μs。原因就是前面说的DTG高三位会自动决定档位144已经越过0xx和10x的分界线。后来我专门做了个换算表再没犯过这个错也建议你配置死区前先按表格核对一遍。第二个坑是更新中断里顺手做了浮点运算。一开始在TIM1更新中断里直接调正弦函数和浮点除法16kHz中断下CPU负载飙到50%以上串口开始丢字节还时常触发看门狗。后来全部改成查表加整数乘加中断体压缩到2μs以内系统才彻底稳定。F103没有FPU中断里的浮点能省则省这是铁律。第三个坑是只调了算法没管信号完整性。刚开始用面包板跳线连接驱动板PWM信号线没串电阻MOSFET栅极振铃严重跑一会儿主控板就复位。后来每路PWM输出串了33Ω电阻驱动芯片电源引脚加了100nF和10μF去耦电容问题才消失。做电机驱动信号完整性和电源去耦不是选修课是必修课。我个人在实际项目里还有一个习惯在PI控制或调速逻辑没跑通之前先把固定频率、固定调制比的开环SPWM跑出来用示波器确认六路波形、死区、相位全部正常再接电机。这样能让问题边界变得非常清晰——波形不对先查驱动板波形对了电机还抖再调V/F曲线和死区参数。这个顺序能帮你省下一大半调试时间。本文还有配套的精品资源点击获取