简介:面向STM32电机控制开发者的一套三相PWM变频输出资源包,以STM32F103C8T6为平台,覆盖定时器配置、三通道PWM输出、互补输出及更新事件处理等关键环节,并结合L298N驱动芯片搭建逆变器控制方案,适合单片机学习者与工业变频项目开发者参考,可作为课程设计与毕业设计素材。压缩包内含587个文件,以C源码、H头文件、汇编启动文件、链接脚本及HAL库文件为主,另有编译生成的axf/hex/map等固件与调试辅助文件,以及Keil工程文件、ioc初始化配置和调试配置文件,整体仅8.35MB,目录结构便于按模块检索。目前已有3169人学习下载。通过学习可掌握三相SPWM的工程实现思路,获得定时器高级PWM模式的实际配置代码与L298N配合的驱动框架,还可参考lib、sct、ioc等工程配置文件快速移植到自己的板卡,并了解死区、保护机制等实战细节,节省底层搭建时间,整体工程结构清晰,便于二次开发。
1. 用 STM32 高级定时器输出三相 PWM,先解决的不只是波形问题
做变频器、伺服驱动器或三相逆变器的人,迟早会碰到一个坎:明明用普通定时器能输出 PWM,为什么三相输出时总是出现上下桥臂直通、波形毛刺、频率一高就发热?问题的根源不在 PWM 本身,而在互补输出和死区时间的处理上。STM32 的高级定时器 TIM1/TIM8 之所以被称作“电机控制专用”,就是因为它把互补输出、死区插入、刹车保护和重复计数这些变频场景的硬需求做成了硬件外设,而不是靠中断里翻转 IO 去“凑”三相波形。
这篇文章围绕“Stm32 进行三相pwm变频输出”这条完整链路展开:从 SPWM 与 SVPWM 的选型逻辑,到定时器通道映射、死区时间计算、中心对齐模式下的 ADC 同步采样,再到刹车保护与故障复位。最终给出的是一套能在真实电机驱动板上跑起来的工程做法,而不是停留在 CubeMX 点几下生成代码的层面。
2. 三相 PWM 的生成原理:为什么必须用高级定时器
2.1 SPWM 与 SVPWM:变频输出的两种主流调制方式
三相 PWM 变频输出的本质,是把直流母线电压转换成三相互差 120° 的交流电压。实现这个目标的调制策略主要有两种:
SPWM(正弦脉宽调制)的思路比较直接:用三路频率相同的三角载波,分别与三路互差 120° 的正弦调制波比较,得到占空比按正弦规律变化的 PWM 脉冲序列。通过 LC 滤波之后,就能还原出正弦电压。它的优点是算法简单、容易理解,每相独立计算,不需要复杂的坐标变换;缺点是直流母线电压利用率较低,大约只有 SVPWM 的 86.6%,谐波分量也偏大。
SVPWM(空间矢量脉宽调制)则是从电机定子磁链圆的角度出发,把逆变器的 8 种开关状态映射到复平面的 6 个扇区,通过相邻两个基本矢量和零矢量的时间组合,合成期望的电压空间矢量。它比 SPWM 的直流电压利用率高约 15%,谐波畸变更小,更适合电机驱动这种对电流波形质量有要求的场景。
在 STM32 上实现时,两种方式最终都落到同一个动作:更新三个比较寄存器的值。差异在于计算占空比的算法复杂度——SPWM 只需要查正弦表再乘一个调制比;SVPWM 则需要做 Clarke 变换、扇区判断、矢量作用时间计算,这些计算放到 F103 上没问题,放到 F407 或 G4 系列上则完全可以用更高的 PWM 频率跑。如果你的项目是通用变频器,起步用 SPWM 调通全链路,再迁移到 SVPWM,这条迭代路径最稳。
2.2 互补输出与死区:上桥臂关断和下桥臂导通之间的安全间隙
三相桥式逆变器的每一相都由上下两个功率管组成。如果上管还没完全关断,下管就开始导通,直流母线就会通过两个管子直接短路,这个现象叫“直通”。功率管的关断时间通常比开通时间长,比如 IGBT 的关断延迟约几百纳秒到几微秒,所以必须在上管关断和下管导通之间插入一段“死区时间”。
STM32 高级定时器在硬件上直接解决这个问题:配置TIM_BDTR寄存器的DTG[7:0]位,或者用 HAL 库的sConfig.BreakDeadTimeConfig.DeadTime字段,定时器会在输出互补 PWM 时自动在所有边沿插入死区。这个延迟由硬件计数器和时钟分频产生,不占用 CPU 中断,也不会有软件延时的不确定性。
死区时间不是越大越好:太短,功率管关不断,仍然直通;太长,输出电压波形畸变明显,电流谐波变大,电机噪声和发热都会增加。典型 IGBT 驱动电路的死区设置在 1 到 3 微秒,MOSFET 驱动的死区通常在 100 到 500 纳秒。
2.3 为什么是 TIM1/TIM8:三相互补输出和刹车功能的硬件基础
STM32 家族的定时器从功能上分三个等级:基本定时器 TIM6/TIM7 只能计时;通用定时器 TIM2-TIM5 能输出单路或几路独立 PWM,但不支持互补;高级定时器 TIM1/TIM8 才有完整的电机控制外设特性。
具体拆开看,TIM1/TIM8 的三相 PWM 输出依赖这几个硬件模块的协同:
第一是通道重映射。TIM1 的 CH1/CH2/CH3 分别对应 U/V/W 三相,每路都有互补输出 CH1N/CH2N/CH3N,再加上 CH4 可以输出刹车或作为额外的 PWM 通道。通过TIM_OCInitTypeDef里的OCIdleState和OCNIdleState配置空闲电平,可以实现刹车后输出确定的电平状态。
第二是刹车机制。TIM_BDTR寄存器的BKE位使能刹车输入后,一旦刹车引脚收到有效电平,所有 PWM 输出立即进入预设的安全状态(高阻或低电平),同时置位标志位。这在硬件过流保护、直流母线过压保护的场景下是关键路径——不依赖中断响应时间,硬件直接切断输出。
第三是重复计数器。TIM1 的重计数器可以设置在每 N 次更新事件后才触发一次更新中断,这对 SVPWM 这种需要精确控制载波比的应用很关键,因为重装比较寄存器必须等到下个 PWM 周期才生效,重复计数器能控制“每几个周期更新一次”,保证三相波形同步。
3. STM32 三相 PWM 变频输出的最小硬件配置和工程搭建
3.1 时钟树、定时器时钟和 PWM 频率的计算
在做任何代码之前,先要把定时器的时钟频率算清楚,否则后面的死区时间和占空比全部跑偏。
以 STM32F103 为例,高级定时器 TIM1 挂在 APB2 总线上。当系统主频为 72MHz,APB2 分频系数为 1 时,TIM1 的时钟 = 72MHz。若 APB2 分频为 2,则 TIM1 时钟自动翻倍为 72MHz——这一点容易踩坑:很多人看 APB2 是 36MHz,以为 TIM1 也是 36MHz,实际定时器时钟被硬件的倍频器修正了。
PWM 频率的公式为:
PWM_Freq = TimerClock / ((PSC + 1) * (ARR + 1))变频器里常见的 PWM 载波频率是 8kHz 到 20kHz。以 8kHz 为例,若预分频 PSC = 0,则 ARR = 72MHz / 8kHz - 1 = 8999。
3.2 引脚映射和 GPIO 复用配置
三相 PWM 的输出引脚,以 STM32F103C8T6 为参考:
- TIM1_CH1: PA8,互补输出 PA7(注意 PA7 同时也是 SPI1_MOSI)
- TIM1_CH2: PA9,互补输出 PB0
- TIM1_CH3: PA10,互补输出 PB1
配置 GPIO 时要把引脚设为复用推挽输出,速度设为 50MHz 或更高。很多人在这里犯一个错误:把 PWM 引脚配置成普通推挽输出,然后主循环里翻转电平。这个方法在低频下能用,但 PWM 频率一高,中断开销和抖动根本无法接受。
下面是使用 HAL 库的标准配置代码:
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能 GPIO 和 TIM1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_TIM1_CLK_ENABLE(); // 配置 TIM1_CH1 (PA8),不配置 CH1N 时死区依然生效,互补输出只覆盖 CH2N/CH3N GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // CH1N 在 PA7,CH2N 在 PB0,CH3N 在 PB1,全部按复用推挽配置 GPIO_InitStruct.Pin = GPIO_PIN_7; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);3.3 让三路 PWM 同时启动的坑:预装载寄存器的同步更新
三相 PWM 输出最常见的故障现象是:上电瞬间有一相的输出占空比和其他两相不一样,电机抖动一下。原因是三个通道的比较寄存器不是同时更新的。
某些实现里,工程师在 PWM 中断里分别写三个比较寄存器,但由于中断响应的时间点不同,写寄存器存在先后,导致一个载波周期里三相的占空比不对称。
正确做法是使用预装载(preload)机制。把TIM_OCInitTypeDef.OCPolarity和OCIdleState配置好之后,在更新事件触发的中断里统一修改三个通道的比较值。因为所有通道的预装载寄存器都在同一个更新事件里被硬件同时锁存到影子寄存器,所以不会出现三相不同步的问题。
4. 三相 PWM 变频输出的核心代码:从寄存器配置到中断处理
4.1 HAL 库的定时器初始化三步走:时基、输出比较、刹车和死区
HAL 库的初始化流程比直接操作寄存器多几层封装,但结构更清晰。以 TIM1 输出三相互补 PWM 为例,初始化代码分三个步骤:
static void MX_TIM1_Init(void) { 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 = 8999; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(&htim1); // 第二步:三路输出比较通道 sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 4500; 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); // 第三步:死区和刹车配置 sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime = 72; sBreakDeadTimeConfig.BreakState = TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.BreakFilter = 0; sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATOUTCLEAR_DISABLE; HAL_TIM_DeadTimeConfig(&htim1, &sBreakDeadTimeConfig); // 使能主输出 MOE,这一步经常被漏掉 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_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3); }死区时间配置的说明:DeadTime = 72在 TIM1 时钟为 72MHz 时代表 72 个时钟周期,也就是 1 微秒。计算公式是DeadTime_us = DeadTime_Value / TimerClock。如果你的工程使用不同的定时器时钟,这个值要重新计算。对于 IGBT 驱动,建议从 2 微秒起步测试,观察死区时间内母线电流波形是否平直,再逐步减小;对于 MOSFET,可以从 300 纳秒起步。
4.2 更新中断里做变频:载波比、频率分辨率和调制波的刷新
三相变频输出的“变频”二字,最终落实为实时改变调制波的频率和幅值。SPWM 方式下,频率改变可以通过两种途径实现:修改定时器频率,或者修改正弦表的读取步长。
第一种方式,直接修改 ARR 改变载波频率,这种做法通常只用于额定频率附近的微调,因为 ARR 变化会导致载波比变化,输出波形的谐波特性变差。
第二种方式,保持 ARR 不变,即固定载波频率,通过修改每个载波周期更新的调制波相位步长来改变输出频率。这种方式下,变频只影响输出基波频率,不影响载波频率,谐波特性稳定。这就是变频器行业常说的“载波比不变”的实现方式。
下面是更新中断里改变三相占空比的具体代码:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t phase_index = 0; uint16_t pwm_u, pwm_v, pwm_w; uint32_t freq_step; int32_t sin_index; if (htim->Instance != TIM1) return; // 变频控制:frequency_hz 是目标频率,sin_table_size 是正弦表点数 // PWM_FREQ 是载波频率,这里计算每个载波周期应增加的表下标步长 freq_step = (frequency_hz * sin_table_size) / PWM_FREQ; // 三相相位差 120 度,对应正弦表长度偏差 1/3 sin_index = phase_index; pwm_u = mid_value + amplitude * sine_table[sin_index % sin_table_size]; sin_index = (phase_index + sin_table_size / 3); pwm_v = mid_value + amplitude * sine_table[sin_index % sin_table_size]; sin_index = (phase_index + 2 * sin_table_size / 3); pwm_w = mid_value + amplitude * sine_table[sin_index % sin_table_size]; // 三个通道的比较值在同一个更新事件里被锁存,保证三相同步 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm_u); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pwm_v); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, pwm_w); phase_index += freq_step; }值得注意的地方是freq_step是整数,当目标频率和载波频率的比值不是整数时,相位累计会出现离散误差。工程做法是使用累加器保留余数,类似 DDS 的频率字原理:
static uint32_t phase_accumulator = 0; uint32_t freq_word = ((uint64_t)frequency_hz * sin_table_size * 65536) / PWM_FREQ; phase_accumulator += freq_word; sin_index = (phase_accumulator >> 16) % sin_table_size;这种方法把相位分辨率提高到 1/65536,长时间运行也不会产生频率累积误差,频率精度只受freq_word量化误差的约束。
4.3 用HAL_TIMEx_PWMN_Start时的隐蔽错误:主输出使能位 MOE
很多人在调试三相 PWM 时遇到同一个问题:主通道有波形,互补通道没波形;或者全部都没波形。检查 GPIO 配置和时钟都没问题,最后定位到TIM_BDTR寄存器的 MOE(Main Output Enable)位。
MOE 位是高级定时器特有的总开关。即使你已经调用了HAL_TIM_PWM_Start,如果 MOE 位没有置 1,所有 PWM 输出引脚都会保持在空闲状态,引脚上量不到任何波形。HAL_TIM_PWM_Start只配置和启动了通道本身,而HAL_TIMEx_PWMN_Start才会把 MOE 置位。因此如果你只启动了主通道,没启动互补通道,整个输出都是死的。
这个错误的原因是 HAL 库的函数划分:PWM 启动函数负责普通输出,而 Ex 版本的 PWMN 启动函数负责带死区的输出。两者必须同时调用,尤其在三相全桥驱动里,主通道和互补通道都必须启动。
正确顺序是先启动三个主通道的 PWM,再启动三个互补通道的 PWMN,顺序颠倒不会报错,但 MOE 的使能时机不同,可能出现一启动就有短脉冲输出的现象。
5. 中心对齐模式和死区、刹车保护的高级调试方法
5.1 PWM 中心对齐模式 1 和模式 2 的区别怎么选
三相互补 PWM 最常使用的是中心对齐模式。在这种模式下,计数器先向上计数到 ARR,再向下计数到 0,比较匹配在上下两个方向上各发生一次,PWM 脉冲关于计数顶点对称。
中心对齐模式有 3 个子模式,区别在于更新事件和计数方向标志位的触发时机:
- 模式 1 (
TIM_COUNTERMODE_CENTERALIGNED1):计数器向下计数到 0 时置位更新标志。适用于上下计数周期对称的应用。 - 模式 2 (
TIM_COUNTERMODE_CENTERALIGNED2):计数器向上计数到 ARR 时置位更新标志。 - 模式 3:上下计数都置位更新标志,中断频率是模式 1 的两倍。
对于三相变频器,选择模式 1 的考虑是:ADC 采样时间点通常设置在计数器向下计数到 0 附近,这时 PWM 脉冲的中心点刚好对应于相电流的瞬时值。如果把 ADC 触发和更新中断对齐,采样到的电流值就是每个 PWM 周期内的平均电流,这个数据直接用于电流环 PI 控制。
5.2 再看 100% 占空比异常问题:比较寄存器饱和的边界
中心对齐模式下,100% 占空比有一个容易被忽略的边界行为:当比较值等于 ARR 或者大于 ARR 时,输出不再翻转。实测发现,当比较值达到 ARR 又加上某个偏移量时,某些型号的定时器在中心对齐模式下会在上升沿和下降沿都产生比较匹配事件,导致输出出现毛刺。
根据实测和社区反馈,100% 占空比异常在 STM32 上的触发条件通常有两种:一是比较值溢出到 0,二是比较值刚好等于 ARR 时由于计数方向不同导致极性反转。
规避方法是在更新中断里做饱和钳制:
if (pwm_value >= TIM1_ARR) pwm_value = TIM1_ARR - 1; if (pwm_value <= 1) pwm_value = 1;5.3 死区时间实测验证:用示波器和电流探头确认直通风险
写代码时死区设多大,不能只靠算。示波器测量是必须的验证步骤。
测量方法如下:把示波器两个通道分别接同一相的上下桥臂驱动信号(不接功率管,直接测驱动芯片输出),设置上升沿触发。观察上管关断沿和下管开通沿之间的时间间隔。如果上管已经降到 0V 但下管还没有开始上升,这个间隔就是实际死区时间。如果两个信号存在交叠,说明死区不够或者驱动芯片有延迟,需要加大死区。
还有一个工程技巧:在调试期间把刹车测试做成自动化的。用一个信号发生器给刹车引脚发一个高电平脉冲,所有 PWM 输出应立刻变为低电平。可以接着测量从刹车触发到 PWM 完全关断的延迟时间,这个参数对过流保护的设计非常重要。STM32 的刹车输入到 PWM 输出关断的延迟一般在几百纳秒以内,但驱动芯片的传播延迟可能长达 1 到 2 微秒。如果你的功率管短路耐受时间只有 10 微秒,那刹车链路的总延迟必须控制在几微秒内,否则保护动作就来不及了。
5.4 死区补偿:当死区引起的电压误差不能忽略时
死区时间插入后,输出电压波形会出现误差。在死区时间内,电流通过续流二极管续流,这导致实际输出电压比理想值高或低,被称为死区效应。在低频、轻载工况下,死区效应会明显加剧电流波形畸变。
死区补偿的常见实现思路是:根据相电流的极性,在调制波上叠加一个修正量。电流为正时,实际输出电压比指令值低,需要增加占空比补偿;电流为负时,比指令值高,需要减小占空比。补偿量约为:
Compensation = DeadTime / PWM_Period * VDC补偿值在更新中断里叠加到三相占空比上。有一点要注意:当相电流过零时,电流极性判断容易振荡,需要加滞回比较,否则补偿方向反复切换,反而引入更大的波形抖动。
6. 把三相 PWM 变频输出跑进电机之前,最后一件事是测相序和限流
拿到一套刚调通的三相 PWM 输出,不要直接接电机。有两次测试必须在接电机之前完成。
第一次测试是只给控制板供电,不接功率级。用示波器确认 6 路 PWM 波形存在,频率、占空比、死区时间正确,三路主通道的 PWM 互差 120 度。判断 120 度相位差的方法:用示波器的光标测量任意两路 PWM 上升沿的时间间隔,除以 PWM 周期乘以 360,结果应为 120 度左右。例如 8kHz 载波,周期 125 微秒,两路 PWM 上升沿相差 41.7 微秒时相位差才是 120 度。这个测试特别容易发现正弦表的相位步长配错、三相映射顺序不对的问题。
第二次测试是接入直流母线但限制母线电流,或者在母线回路串入限流电阻。先给定一个小幅度的调制比,比如额定电压的 5%,启动后观察三相相电流是否对称。如果某相电流明显偏大,通常是对应通道的 PWM 和实际相序不匹配。判断输出相序是否对的方法:依次减小 U 相调制波幅值,观察电机是否出现单相制动或抖动,如果相序反了,电机轴会反向脉动而不是平滑转动。
最后一个实用技巧:在固件里加一个开环斜坡启动逻辑。变频器上电后,如果给定频率从 0 直接跳到 50Hz,电流冲击会非常大。正确做法是频率从 0 开始按固定斜率爬升,比如每秒爬升 2Hz,到目标频率后保持。这个斜坡时间不是越大越好,太大会让电机长时间处于低速大电流状态;一般取额定频率下的 1 到 2 秒。模拟量给定频率的场合,斜坡时间需要和电位器的调节速度匹配,否则扭电位器时频率突变会产生同样的过流问题。
本文还有配套的精品资源,点击获取