
简介基于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 时钟除以 psc1 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 位决定。// 死区时间 2us72MHz 下 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 相索引加 85256 的 1/3W 相索引加 171256 的 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.sC8T6 属于中等容量选成 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 的话需要把查表索引改成硬件触发方式这是一个值得动手的实验方向。从那以后我每次改定时器参数都强制自己在纸上画出计数器的波形图标清楚写入时刻和影子寄存器锁存时刻再上电实测。硬件控制没有「我觉得应该没问题」这种说法一个寄存器的时序错了功率管就在那里等着你。希望帮到你。本文还有配套的精品资源点击获取