简介:基于STM32F103C8T6高级定时器TIM1的查表法实现三相六路互补SPWM输出,信号相位互差120°、带死区,支持调频调压,并采用恒压频比策略驱动三相电机。资源面向嵌入式与电机控制开发者,配套KEIL5工程包含详细注释,可在线仿真直接观察六路PWM波形。包体内共149个文件、约46.67MB,以C/H源程序、HEX/AXF编译与调试产物、PDF原理文档为主,另有正弦表生成工具和死区计算器,覆盖从原理到实践的完整链路。目前已有8067人学习下载,适合需要快速搭建三相SPWM驱动并深入理解死区与查表原理的工程师和学生。
1. 一个定时器搞定的三相逆变:STM32F103C8T6 的 SPWM 工程能干什么
接手过一个三相变频电源的预研任务,最初的想法是至少三个定时器分工,一个出频率、一个出占空比、一个做保护。看完这份基于 STM32F103C8T6 的 SPWM 三相六路互补输出工程后,发现一块最便宜的 C8T6 最小系统板加一个高级定时器 TIM1,就把正弦波调制、互补输出、死区插入、调频调压全做完了,甚至还能在 KEIL5 里直接仿真跑波形。这工程适合两类人:一是刚接触电机控制和逆变器、想把 SPWM 原理落到寄存器层面的初学者;二是手上只有 C8T6 想快速出六路 PWM 波形做算法验证的开发者。读完这篇,你能知道它的调制策略怎么选、调频调压各自动哪些寄存器、KEIL5 仿真怎么配,以及把开环改成 V/F 曲线控制该从哪下手。
2. 技术选型与整体结构:为什么是高级定时器 TIM1 加软件查表法
2.1 硬件资源与引脚分配:三对互补通道到底怎么布局
STM32F103C8T6 的定时器资源里,只有 TIM1 是高级定时器,它带 6 个独立的比较/捕获通道,映射到 PA8、PA9、PA10 和 PB13、PB14、PB15 上,天然形成三对互补输出,而且硬件层面直接支持死区插入和刹车功能。通用定时器 TIM2、TIM3、TIM4 没有互补输出,要做三相桥必须外接反相器和延时电路,可靠性差一截,所以选 TIM1 不是偏好,是性价比最优解。
引脚分配上,本工程把三对互补通道按「U、V、W 三相」划分:PA8/PA13 对应 U 相上桥和下桥,PB13/PB14 对应 V 相,PB15/PA10 对应 W 相。这里有个容易被忽略的细节:PA13、PA14、PA15 默认是 SWD 下载引脚,本工程却没把它们当普通 IO 用,把 U 相下桥放到 PA13 而不是复用通道对应的默认引脚,就是为了烧录调试时不被仿真器占用冲突。实际做板时如果要换成标准引脚,硬件上必须把跳线改掉,软件里也要同步改 CCR 通道映射。
TIM1 还有一个独有的特征,它的时基单元里 ARR、PSC、CCR 都有影子寄存器,意味着这些寄存器不是写入立即生效,而是在更新事件到达时才被硬件锁存。这个特征对 SPWM 有双重意义:好的方面是输出不会有撕裂的毛刺,坏的方面是调频调压时更新时刻没对齐,会导致周期跳变。后面核心代码部分会专门讲怎么规避。
2.2 软件结构:从正弦表到三相桥的完整数据流
整体软件分四层,最顶层是调制策略层,它决定「当前要输出多大频率、多大电压」,这层就是调频调压算法的核心,也是后续做 V/F 控制要扩展的地方。第二层是波形生成层,负责把目标频率换算成定时器的 PSC、ARR 值,把目标电压换算成比较寄存器 CCR 的值,并写入 TIM1。第三层是硬件适配层,封装了 GPIO 复用配置、TIM1 通道配置、死区计算等寄存器操作。最底层就是标准外设库。
我把这个工程下载下来后大概扫了一遍,目录结构是这样分的:
STM32F103C8T6_SPWM/ │ main.c // 主循环:启动调制,处理按键调频调压 │ stm32f10x_it.c // 中断:更新事件标志,故障标志归零 ├─ App/ │ ├─ spwm.c // SPWM 核心:正弦表、调频、调压入口 │ └─ spwm.h // 对外接口:SPWM_Init / SPWM_SetFreq / SPWM_SetDuty ├─ Driver/ │ ├─ tim1_config.c // TIM1 时基、互补输出、死区、刹车配置 │ └─ gpio_config.c // PA/PB 复用推挽配置 ├─ StdPeriph/ │ ├─ src/ // 标准外设库源码 │ └─ inc/ └─ Output/ └─ SPWM.hex // 可直接烧录的固件这个分层不算复杂,但每个文件职责很清晰,改调频算法不会碰到底层定时器初始化,换芯片平台时只需重写 Driver 层,App 层算法代码几乎不用动。
2.3 为什么不用硬件 DAC 和 SVPWM:调制策略的选择逻辑
有人可能会问,F103 也有 DAC,直接把正弦波送到外部比较器不也能出 SPWM?确实能,但那是模拟方案,温漂和噪声问题都很麻烦,而且外部还需要电平转换电路,远不如数字 PWM 直接驱动功率管方便。SVPWM 在母线电压利用率上比 SPWM 高约 15%,控制异步电机时优势明显,但它的扇区判断和矢量作用时间计算比较复杂,代码量是查表法的三倍以上。这工程定位是「先把三相桥转起来、把调频调压的框架跑通」,用查表 SPWM 是投入产出比最高的选择。
查表法本身也有讲究,工程里采用的是规则采样,正弦表长度取 256 点,和定时器 ARR 的 255 对齐,一个载波周期里更新一次比较值。256 点对基波来说足够平滑,谐波分量主要集中在载波频率及其边带,对大多数电机负载不敏感。如果换成 512 点,波形质量提升有限,但查表和比较中断的负担翻倍,所以 256 是工程上权衡后的典型值。
3. 定时器配置与死区计算:六路 PWM 从初始化到输出的完整链路
3.1 TIM1 时基和通道初始化:载波频率怎么定
先看最关键的一段初始化代码,它决定了 PWM 的载波频率、计数模式以及六个通道的工作方式。工程里把载波频率定为 8kHz,对于 16 位的 ARR 来说余量很充足。
void TIM1_SPWM_Init(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 时基配置:向上计数,ARR 决定载波周期 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频,72MHz 时钟除以 psc+1 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = arr; // 自动重载值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // PWM 模式:OC1、OC2、OC3 各自配置为 PWM2 模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = arr / 2; // 初始 50% 占空比 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); }这段代码的要点是 PWM2 模式的选择。PWM2 模式下,当计数器值小于 CCR 时输出有效电平,大于 CCR 时输出无效电平,而 PWM1 模式正好相反。选 PWM2 是为了让 CCR 数值增大时占空比增大,调压逻辑更直观。另一个细节是 TIM_OutputNState_Enable,它使能了互补输出通道,也就是 CH1N、CH2N、CH3N 这三路下桥 PWM,这是普通定时器根本不存在的寄存器位。
3.2 死区配置:一个寄存器位搞错就烧功率管
死区插入是高级定时器最实用的功能,它保证了同一桥臂上下管在切换瞬间不会同时导通。硬件实现原理是在互补输出的上升沿插入一段延迟,这段延迟由 BDTR 寄存器的 DTG 位决定。
// 死区时间 2us,72MHz 下 DTG = 2us * 72MHz = 144 TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = 144; // 死区 2us 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);TIM_DeadTime 等于 144 时,实际死区时间在 72MHz 主频下正好是 2 微秒,这个值对 IGBT 和 MOS 管都比较安全。值得留意的是 TIM_AutomaticOutput_Enable 这个位,它是 MOE 主输出使能的自动恢复机制,一旦产生刹车事件 MOE 被硬件清零,六路输出全部进入高阻态,而自动输出使能会在下一个更新事件时自动恢复 MOE,这在过流保护需要「跳闸后自动重启」的场景下很有用。工程里把它关掉,改为手动控制 MOE,这样故障复位时机由软件决定而不是硬件自动恢复,对电机控制更安全。
3.3 急停与加载时刻:SPWM 长期运行的稳定性保障
六路 PWM 不能一初始化就立刻输出,否则上电瞬间占空比是乱的,MCU 的复位期间 IO 默认是浮空输入,功率管栅极可能被噪声误触发。工程的做法是先用 GPIO 配置把六个通道全部拉低,再初始化 TIM1,最后才在主循环中置位 MOE 使能输出。
void SPWM_Start(void) { TIM_Cmd(TIM1, ENABLE); // 必须等第一个更新事件产生,让影子寄存器加载完初始值 while ((TIM1->SR & TIM_FLAG_Update) == 0); // 硬件控制主输出使能,此时六路 PWM 才开始实际输出 TIM1->BDTR |= TIM_BDTR_MOE; }这里有个容易翻车的地方:如果在置位 MOE 之前就改 CCR,写入的是影子寄存器而实际输出寄存器还是复位值,导致第一波 PWM 波形的占空比和预期完全不同。所以我一般会在置 MOE 前先强制产生一个软件更新事件,让所有影子寄存器一次性锁存,再开 MOE,波形第一个周期就是完整的。
4. 调频调压核心代码拆解:改哪几个寄存器,影响什么波形
4.1 调频原理:改预分频还是改重载值
调频的本质是改变定时器的计数周期,但存在两种改法:改 PSC 预分频或改 ARR 自动重载值。PSC 的取值范围是 12 位,ARR 是 16 位,两者都能改变 PWM 频率,但影响方式不同。改 PSC 时计数时钟步进变大,每一拍的计数器增量不再是 1,波形质量会变差;改 ARR 时计数步进不变,只是周期长度变化,更适合精调。工程里采用「粗调 PSC、细调 ARR」的组合方式,避免单一寄存器调节范围不足。
// 目标频率 5Hz 到 400Hz,载波比固定为 32,载波频率随基波同步变化 void SPWM_SetFreq(uint16_t freq_hz) { uint32_t carrier = freq_hz * 32; // 载波频率 = 基波频率 x 载波比 uint16_t psc = (72000000 / carrier / 256) - 1; uint16_t arr = (72000000 / (psc + 1) / carrier) - 1; TIM1->PSC = psc; TIM1->ARR = arr; // 手动触发更新事件,让影子寄存器的值立即生效 TIM1->EGR |= TIM_EGR_UG; }这段代码的关键是 EGR 寄存器的 UG 位,它在置 1 后会产生一个软件更新事件,把所有影子寄存器的值一次性锁存到实际寄存器。如果不写这行,AR R和 PSC 的改动会等到下一个自然更新事件才生效,调频响应会慢半个载波周期,在闭环控制里这是不能接受的延迟。载波比固定为 32 意味着基波频率升高时载波频率也跟着升高,这属于同步调制,变频变压时载波比恒定,谐波特性更稳定。
4.2 调压原理:正弦表幅值叠加整体缩放
调压的核心是把每个点的正弦表数值按目标电压比例整体缩放,然后写入对应通道的 CCR。工程里正弦表幅值设计为 2400,留出 250 的余量给死区补偿,防止调制比过高时上桥还没关断下桥就导通。
// 电压调节:duty 范围 0 到 1000,对应 0% 到 100% 调制深度 void SPWM_SetDuty(uint16_t duty) { uint16_t i; uint32_t compare_value; for (i = 0; i < SPWM_TABLE_SIZE; i++) { compare_value = (uint32_t)sinTable[i] * duty / 1000; TIM_SetCompare1(TIM1, compare_value); // 只更新 CH1,需配合通道映射 // CH2、CH3 相位差 120 度,索引偏移需要在查表处处理 } }三相 SPWM 的关键在于相位偏移,U 相索引从 0 开始,V 相索引加 85(256 的 1/3),W 相索引加 171(256 的 2/3)。三个通道的 CCR 更新必须放在同一个中断服务函数里连续执行,中间不能有任何延时或调度切换,否则三相的相位关系会漂移。工程里没有在 for 循环里逐点写入,而是把三相的查表索引放在同一个中断里算好,一次性写三个 CCR 寄存器。
4.3 KEIL5 仿真配置:没有开发板也能看波形
这份工程能在 KEIL5 里直接仿真,关键配置有两处:一是将调试器选择为 ST-Link 或 DAP-Link,并在 Settings 里勾选「Download to Flash」;二是启用 Simulator 模式下的波形窗口。实际用仿真模式时,要注意 TIM1 的时钟在仿真环境中默认是关闭的,需要在 RCC 配置里手动使能 TIM1 时钟,否则波形窗口一片平。
// main.c 里的时钟初始化必须包含这一句 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 仿真时 TIM1 挂在 APB2 上,主频 72MHz,分频系数 1 // 如果仿真波形频率偏高一倍,检查 APB2 预分频是否被设为了 /2仿真器模式下外设库的 RCC 配置是真实执行的,但示波器窗口的刷新率有限,载波 8kHz 时建议把仿真时间轴拉长,观察基波周期而不是载波周期,更容易确认调频调压是否正确生效。一个血泪经验是,仿真模式看不出死区插入效果,因为波形窗口只显示逻辑电平,不反映时间差,死区还是要拿到示波器上看。
5. 调试避坑记录:六路 PWM 工程里最容易翻车的几个地方
5.1 现象:上电一瞬间上下桥同时导通,电流猛增
原因:TIM1 初始化完成但 MOE 没置位时,六路输出并非全是低电平,而是保持上一次的输出状态,如果功率管栅极有下拉电阻,复位期间可能处于半导通状态。
解决:GPIO 初始化阶段先把六个通道全部配置为推挽输出并输出低电平,等 TIM1 完成全部配置后再切换到复用功能模式。关键代码如下:
// 初始化 GPIO 时先用通用推挽输出拉低 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_WriteBit(GPIOA, GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10, Bit_RESET); // 定时器初始化完成后再改成复用推挽 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;5.2 现象:调频瞬间输出波形出现一个异常长周期
原因:直接修改 ARR 后,如果恰好计数器已经计过了新的 ARR 值,计数器需要先计到最大值回绕,多出一个完整的载波周期。
解决:调频时必须先停计数,写入新 ARR 和 PSC 后再启动。ST 库的 TIM_Cmd 能实现开关,但更干净的做法是操作 CR1 寄存器的 CEN 位,先清零再置位。
TIM1->CR1 &= ~TIM_CR1_CEN; // 停止计数 TIM1->ARR = new_arr; TIM1->PSC = new_psc; TIM1->EGR |= TIM_EGR_UG; // 产生更新事件,锁存影子寄存器 TIM1->CR1 |= TIM_CR1_CEN; // 重新启动5.3 现象:三相波形频率对,但相位差不是 120 度
原因:查表时索引偏移计算用了 256 除 3,得到 85 和 171,但 85 比理想值 85.33 少了 0.33,累积若干点后相位漂移超过 1 度。
解决:索引不用整数加法,改用浮点累加或查表时单独计算浮点索引再取整。工程里为了省时间用了整型偏移,做高精度电机控制时必须修正。
// 相位差累加方式:用浮点步进,每点累加后再取整 float phase_v = 256.0f / 3.0f * i; uint16_t index_v = (uint16_t)phase_v % 256;5.4 现象:KEIL5 仿真时波形窗口看不到任何输出
原因:仿真模式下外设库往 TIM1 写寄存器是成功的,但 Keil 的波形窗口默认只捕捉模拟引脚状态,引脚没有连接虚拟示波器通道。
解决:在 Keil 的 View-System Viewer 里找到 TIM1,手动把 CH1、CH1N、CH2、CH2N 等引脚添加到 Logic Analyzer,然后设置合适的显示比例。另外仿真时如果开了优化选项,部分寄存器写操作会被编译器优化掉,把 C 优化级别设置为 -O0 是调试阶段的标准姿势。
5.5 现象:下载程序后板子不跑,电流极小
原因:C8T6 的 BOOT0 引脚悬空或被外部电路拉高,芯片进入了系统存储器引导模式而不是用户 Flash 引导模式。
解决:确认 BOOT0 接 10k 下拉电阻到地,BOOT1 随意。同时检查启动文件是不是选的 startup_stm32f10x_md.s,C8T6 属于中等容量,选成 HD 的启动文件会导致中断向量表错位,程序直接跑飞。
6. 二次开发技巧:从固定载波比改成 V/F 曲线控制
拿到这份工程后如果想把它改成真正的变频器,第一个要动的就是调频调压的联动逻辑。现在的 SPWM_SetFreq 和 SPWM_SetDuty 是独立接口,实际电机控制里电压和频率必须按 V/F 曲线同步变化,否则低频段磁通过饱和、高频段力矩不足。常见的做法是把频率和电压的对应关系做成一张查找表,主循环里按目标频率查表得到电压百分比,再同时调用两个接口。
// V/F 曲线表:频率(Hz) -> 电压百分比(%) const uint16_t vf_curve[][2] = { { 0, 0 }, // 0Hz 输出 0V { 5, 18 }, // 5Hz 输出 18%,低频补偿 { 25, 50 }, // 25Hz 输出 50% { 50, 80 }, // 50Hz 输出 80%,预留恒压区 { 400, 100 }, // 400Hz 满载 };查表时用线性插值可以得到任意频率点的电压值。低频补偿段(0 到 5Hz)的电压不能直接按比例给,否则电机启动力矩不足,这是变频控制的常识。扩展后就有了完整的变频器开环算法,不需要改动 Driver 层任何代码。
如果想从查表法升级为实时运算,把正弦表改成运行时计算也不难,但 F103 做浮点 sin 运算在一个载波周期里要算 256 次,CPU 占用偏高。折中方案是保持查表法,只把幅值缩放从循环改成 DMA 搬运,用 DMA 循环模式不停从正弦表搬运数据到 TIM1 的 CCR 寄存器,CPU 占用几乎为零。工程里预留了三相比较中断,要上 DMA 的话需要把查表索引改成硬件触发方式,这是一个值得动手的实验方向。
从那以后我每次改定时器参数,都强制自己在纸上画出计数器的波形图,标清楚写入时刻和影子寄存器锁存时刻,再上电实测。硬件控制没有「我觉得应该没问题」这种说法,一个寄存器的时序错了,功率管就在那里等着你。希望帮到你。
本文还有配套的精品资源,点击获取