简介:面向嵌入式初学者与电机控制方向学习者的STM32实战资源,围绕STM32F103C8T6高级定时器TIM1,用查表法生成三相六路互补SPWM信号,相位互差120°,支持死区插入与调频调压,并以恒压频比方式驱动三相电机,适合作为电机驱动入门与课程设计的参考工程。压缩包共149个文件、约46.67MB,以c与h源码、Keil工程文件(uvprojx/uvoptx)、hex与axf固件、map与lst编译产物为主,另附SPWM原理学习文档、正弦表生成工具与死区计算器,便于边看代码边理解波形生成逻辑。程序在KEIL5下编写并带详细注解,可在线仿真观察六路互补输出波形,帮助读者掌握死区时间设置、正弦表查表与调压调频的实现细节。目前已有8068人学习下载,适合对照源码与工具逐步复现并排查问题。
1. 三相 SPWM 六路互补输出:为什么“调频调压”不是改两个参数那么简单
很多人第一次在 STM32F103C8T6 上做三相 SPWM,直觉是:定时器配好中心对齐模式,三路 PWM 出来,再取反得到六路,频率改 ARR、电压改 CCR,收工。真跑起来才发现,互补输出的死区一旦加上,占空比和输出电压的关系就不再是线性的;调频时载波比跟着变,正弦表步进也得跟着换;六路里任何一路相位错半个周期,电机或逆变桥立刻抖给你看。这套东西的难点从来不在“会不会配定时器”,而在于调频、调压、死区、互补这四件事互相耦合,动一个牵三个。把这份资源里的实现逻辑拆开看,能搞清楚 STM32 的高级定时器到底是怎么把“三相正弦”这件事硬件化掉的,以及为什么工程上宁可多算一张表,也不肯在中断里现算 sin。
2. 高级定时器:六路互补输出到底是谁在硬件里完成的
2.1 中心对齐模式与三相互补的硬件映射关系
STM32F103C8T6 上能直接吐六路互补 PWM 的只有 TIM1 和 TIM8 这类高级定时器,通用定时器 TIM2/3/4 做不到带死区的互补对。原因在硬件结构:高级定时器的每个通道都有 OCx 和 OCxN 两个输出,中间夹着一个死区发生器 BDTR,互补关系是硅片里连好的,不是软件取反。这一点决定了整个方案的骨架——三相六路必须落在 TIM1 的 CH1/CH1N、CH2/CH2N、CH3/CH3N 上,三对互补,正好对应逆变桥的三个半桥。
计数模式选中心对齐(CMS 位非零),计数器从 0 数到 ARR 再数回 0,比较值 CCR 在上升和下降两个方向各匹配一次,输出波形天然关于中点对称。对 SPWM 来说这不是可选项:中心对齐让每路 PWM 的脉宽以周期中心为轴分布,三相互补的相位关系才稳定,边沿对齐模式下互补对的死区插入点会偏,谐波明显变差。常见的实现思路是把 ARR 设成载波周期的一半计数,实际开关频率 = 72MHz / (2 × ARR),这个 2 倍关系是很多人第一次算频率时翻车的地方。
三相互补的相位差靠什么保证?不是靠三个定时器同步,而是同一个定时器的三个通道共用同一个计数器 CNT,CH1/CH2/CH3 的 CCR 值分别对应三相正弦在 0°、120°、240° 的采样点。硬件上它们天然同相锁定,软件只需要保证三张(或一张偏移索引的)正弦表对齐。这是单定时器方案相对三定时器方案最大的优势:不存在同步误差,也就没有三相不平衡的隐患。
2.2 死区发生器 BDTR:为什么死区时间必须用硬件而不是软件延时
死区的作用是防止同一半桥的上下管直通。软件延时的做法是在关断一路后插一段空循环再开另一路,问题在于中断响应、编译优化、主频波动都会让这段延时不可控,血泪经验是软件死区在低频时看着没事,一上调频就炸管。硬件死区发生器挂在 BDTR 寄存器上,死区时间由 DTG[7:0] 位编码,从几个 ns 到十几 us 分档,插入位置精确到时钟周期,且与 CCR 更新完全解耦。
DTG 的编码不是线性的,这是必须讲清的一个细节。以 72MHz 时钟为例,DTG 低段按 t_dtg = DTG × t_DTS 计算,高段带偏移和倍率,具体分档在参考手册里有表。工程上一般先定死区时间(比如 1us 量级,视功率管开关速度而定),再反查 DTG 值,而不是随手填一个数。填错档位的现象很典型:示波器上看互补对之间要么没有间隔,要么间隔大得离谱导致有效占空比被吃掉一大块,输出电压上不去。
// TIM1 死区与刹车配置(寄存器级,便于看清每一位的作用) TIM1->BDTR = 0; TIM1->BDTR |= (0x1C << 0); // DTG[7:0]:按 72MHz、t_DTS=1/72us 反查得到的死区分档 TIM1->BDTR |= (1 << 11); // OSSR:运行模式下关闭后输出空闲电平 TIM1->BDTR |= (1 << 12); // OSSI:空闲模式下输出空闲电平 TIM1->BDTR |= (1 << 13); // BKE:使能刹车输入(过流保护用) TIM1->BDTR |= (1 << 15); // MOE:主输出使能,缺这一位六路全哑这段配置里最容易漏的是 MOE 位。高级定时器的输出使能是独立的一级开关,CCER 里把通道配成输出、BDTR 里死区也设了,只要 MOE 没置位,六个引脚就是死的,示波器一片平。排查时先看 MOE,再看 CCER 的 CCxE/CCxNE,最后才怀疑正弦表,这个顺序能省掉大量时间。DTG 那一位的具体数值必须对着时钟树和手册分档表算,代码里写死一个魔数而不写注释,是后面接手的人最想骂人的地方。
2.3 CCR 预装载与更新事件:调压时波形为什么不能“撕裂”
调压的本质是改 CCR。但 CCR 不能在计数器跑到一半时随便改,否则当前这个 PWM 周期的上升沿和下降沿会来自两个不同的比较值,脉宽畸变,输出上表现为毛刺。高级定时器的解法是 CCR 预装载(OCxPE 位):写进 CCR 的值先进影子寄存器,等更新事件 UEV 到来(中心对齐模式下在计数器到 0 时)才整体生效。三路 CCR 在同一个 UEV 时刻一起换,三相正弦的相位关系不会被打乱。
这里有个设计取舍值得说透。如果每个 PWM 周期都更新一次 CCR,正弦表的步进就固定为 1,载波比 = 表长 / 每周期的步进,调频时要么改表长要么改步进,前者占 Flash、后者要重算。资源里的做法是把正弦表按固定点数存好,调频时改 ARR 同时改查表步进(用定点累加器做相位累加),调压时改一个全局的幅度系数去乘表值。相位累加器是这套方案的核心数据结构:
// 相位累加 + 幅度缩放,放在 UEV 中断里执行 static uint32_t phase_acc = 0; // 32 位相位累加器,高 16 位作查表索引 static uint32_t phase_step = 0; // 每个 PWM 周期的相位增量,决定输出频率 static uint16_t amplitude_q8 = 256; // Q8 定点幅度,256 表示满幅 void TIM1_UP_IRQHandler(void) { if (TIM1->SR & TIM_SR_UIF) { TIM1->SR = ~TIM_SR_UIF; phase_acc += phase_step; // 相位连续累加,频率无跳变 uint16_t idx = phase_acc >> 16; // 取高 16 位做表索引 uint16_t a = (sin_table[idx] * amplitude_q8) >> 8; // 调压:乘幅度系数 uint16_t b = (sin_table[(idx + TAB_120) & TAB_MASK] * amplitude_q8) >> 8; uint16_t c = (sin_table[(idx + TAB_240) & TAB_MASK] * amplitude_q8) >> 8; TIM1->CCR1 = a; TIM1->CCR2 = b; TIM1->CCR3 = c; // 预装载,UEV 后统一生效 } }相位累加器的好处是频率分辨率极高:phase_step 加 1,输出频率只变一点点,而且相位永远连续,调频过程中不会出现波形跳变。代价是中断里要做三次乘法和查表,72MHz 下这点开销完全扛得住,但如果把 sin() 现算放进来,一次浮点三角函数就够中断超时了。幅度用 Q8 定点而不是浮点,也是同一个考虑:Cortex-M3 没有 FPU,软浮点在中断里是灾难。TAB_120 和 TAB_240 是 120°、240° 对应的表偏移,用掩码取模避免除法,这些细节决定了这套代码能不能在 72MHz 上稳定跑满开关频率。
3. 调频调压的耦合:正弦表、载波比与相位累加器怎么配合
3.1 载波比变化时正弦表点数为什么不能死守一个值
SPWM 的载波比是开关频率与输出基波频率之比。固定正弦表点数、靠改 ARR 调频,载波比会随输出频率漂移:低频时载波比高、谐波远,高频时载波比掉下来,低次谐波逼近基波,滤波器和电机都难受。资源里的处理思路是让 phase_step 与 ARR 联动——输出频率 f_out 满足 f_out = f_pwm × phase_step / 2^16,而 f_pwm = 72MHz / (2 × ARR),两个式子一联立,给定目标 f_out 就能反算出 phase_step。这样正弦表点数不变,载波比随频率自然变化,在常用频段内保持在合理区间。
死守一个载波比、强行改表长的做法也有,但表长一变,Flash 占用和查表掩码全要跟着改,代码复杂度陡增,收益在多数应用里不明显。这里的取舍是:用相位累加器换掉“表长必须整除”的约束,把调频的自由度从表结构里解放出来。我一般会这么理解——正弦表是“波形模板”,相位累加器是“播放速度”,两者解耦之后,调频调压才互不干扰。
3.2 调压的两种路径:改幅度系数还是改直流母线
调压有两条路:软件改幅度系数(上面代码里的 amplitude_q8),或者硬件调直流母线电压。前者响应快、无额外器件,但占空比整体压低后,死区占用的相对比例变大,低压输出时线性度变差,这是 SPWM 的固有短板。后者线性度好,但需要可调电源,成本和体积上去了。资源走的是软件调幅路线,适合中小功率、对低压段精度要求不极端的场景。
改幅度系数时要注意 CCR 的下限。死区已经吃掉了每个边沿附近的一小段,如果 amplitude_q8 压得太低,算出来的 CCR 值落进死区区间,实际输出直接归零,表现为“调压调到某个点以下就断输出”。工程上的做法是给 CCR 设一个下限钳位,并在幅度系数上做标定,把有效调压范围映射到可用区间。这个坑不踩一次很难想到,因为示波器上看波形是“突然没了”,而不是“慢慢变小”。
3.3 六路互补的相位校验:怎么确认三相互差真的是 120°
代码写完不等于相位对。验证方法很直接:把三对互补输出接到示波器,看 CH1 与 CH2 的基波过零点间隔是否等于 1/3 个基波周期,CH2 与 CH3 同理。更省事的做法是在调试阶段临时把 amplitude_q8 设成满幅、输出频率设低(比如几 Hz),用示波器的余晖或长存储抓三相包络,肉眼就能看出相位是否均匀。如果三相互差不对,先查 TAB_120/TAB_240 的偏移量是否按表长正确换算,再查掩码是否覆盖了表长,最后才怀疑中断里的更新顺序。
提示:相位校验一定要在死区使能的状态下做。死区会改变每路的有效脉宽,但不会改变基波相位,如果关掉死区校验完再打开,容易掩盖互补对配置本身的错误。
4. 避坑与排查:这套方案最容易翻车的几个点
4.1 六路全哑,示波器一片平
现象:定时器配置看起来全对,CCER 也开了,六个引脚没有任何输出。原因:BDTR 的 MOE 主输出使能位没置位,或者刹车输入 BKE 使能后 BKIN 引脚被拉低触发了刹车。解决:先确认 MOE=1,再检查 BKIN 引脚电平,调试阶段可临时关闭 BKE 排除刹车干扰,量产前必须恢复并接好过流保护。
4.2 调频时波形跳变、电机异响
现象:改输出频率的瞬间,波形相位不连续,负载上有明显抖动。原因:直接改 ARR 而没同步改 phase_step,或者相位累加器在改频率时被清零。解决:频率切换只改 phase_step,phase_acc 保持连续累加;ARR 的修改走预装载,等 UEV 生效,避免当前周期被截断。
4.3 低压段输出突然消失
现象:幅度系数往下调,输出先变小后突然归零。原因:CCR 计算值落进死区区间,有效脉宽被死区完全吃掉。解决:给 CCR 设下限钳位,标定有效调压区间,必要时在低压段适当减小死区或提高开关频率。
4.4 三相互差不是 120°,且随频率漂移
现象:低频时相位看着还行,频率一高三相就不均匀。原因:TAB_120/TAB_240 的偏移量按固定表长写死,但查表索引的掩码或步进在调频时没跟着一致处理,导致偏移错位。解决:偏移量统一用“表长/3”的整数表达,索引取模用同一个掩码,调频只动 phase_step 不动表结构。
4.5 中断里现算 sin() 导致输出频率上不去
现象:开关频率一提高,波形畸变甚至中断丢拍。原因:Cortex-M3 无 FPU,软浮点三角函数在中断里耗时过长。解决:正弦值全部预存成表,中断里只做查表、乘法和移位;幅度用 Q8/Q15 定点,杜绝浮点运算进入中断路径。
5. 设计取舍:为什么这套结构值得照着源码走一遍
把整条链路串起来看,这套方案的核心取舍其实只有一句话:把“波形生成”从软件计算降级成“查表 + 相位累加 + 硬件互补”,软件只负责每个 PWM 周期更新三个 CCR,其余全部交给 TIM1 的硬件结构。这个取舍带来的好处是确定的——死区精度由硅片保证、三相同步由共用计数器保证、调频调压解耦由相位累加器保证;代价也同样确定——正弦表占 Flash、调压线性度受死区限制、载波比随频率漂移。
值不值得照着源码走一遍,取决于你想验证什么。如果只是想“让六路出波形”,抄配置就够;但如果你想搞清楚高级定时器在 SPWM 场景下每一级硬件到底承担了什么,就得把 BDTR 的 DTG 分档、CCR 预装载的生效时机、UEV 中断与相位累加器的配合这三处对着手册逐位看一遍。这三处是整套方案里“软件想管但管不了、硬件默默兜住”的地方,也是这类资源最有价值的部分——它把寄存器级的细节和工程上的标定经验放在了一起,而不是只给一份能跑的工程。
验证方法上,我习惯先断开功率级,只测六路 GPIO 波形:确认互补对间隔等于设定死区、三相互差 120°、调频时相位连续、调压时占空比线性变化,四项全过再上功率管。这个顺序看起来慢,但比上电炸管之后再回头查寄存器快得多。从那以后我每次碰高级定时器的互补输出,都强制先把 MOE、DTG、CCER 三位在示波器上验一遍再往下走,后悔药这东西,能不吃就不吃。希望帮到你。
本文还有配套的精品资源,点击获取