STM32F103实现三相SPWM互补输出与调频调压实战详解
2026/9/9 21:01:25 网站建设 项目流程

简介:一套基于STM32F103C8T6的SPWM三相逆变控制方案,利用高级定时器TIM1查表法生成三相六路互补PWM,带死区且相位互差120°,通过恒压频比实现调频调压,适用于三相电机驱动、变频电源等电机控制场景,适合嵌入式与电力电子方向学习者参考。资源共149个文件,压缩包约46.67MB,包含C/H源码、KEIL5工程文件、编译产物(hex/axf)、PDF原理文档,以及正弦表生成工具、死区计算器等;其中26个头文件与18个C源文件构成完整工程,HEX与AXF文件可直接烧录或仿真验证,PDF文档帮助理解SPWM生成原理。源码注释详细,可在KEIL5中在线仿真直接观察SPWM输出波形。目前已有8053人学习下载。除工程代码外,附带的学习文档和实用工具便于对照理解查表法、死区设置与调频调压实现思路,可直接用于项目起步、教学实验或二次开发。 最近在倒腾三相逆变和电机驱动,手头正好有一块STM32F103C8T6最小系统板,把SPWM三相六路互补输出这套东西完整跑通了,顺带把调频调压也做了进去。这个组合在变频器、三相BLDC/PMSM驱动、UPS电源里都很常见,核心就是让单片机输出三组带死区的互补PWM,再通过改变正弦表的频率和幅值实现调速调压。这篇文章就把我从原理到代码到调试踩坑的全过程记录下来,给后面做类似项目的朋友一个参考。

这板子虽然便宜,但资源做这套东西完全够用。整个方案说白了就是:用高级定时器TIM1生成三路互补PWM,占空比按正弦规律变化,同时插入死区防止上下桥臂直通,再根据目标频率动态更新正弦表的查表步长和幅值,实现V/F恒压频比控制。下面按模块把思路和实现细节拆开讲。

1. 方案设计与核心参数定标

1.1 为什么用STM32F103C8T6做SPWM

F103C8T6属于Cortex-M3内核,主频72MHz,片上资源包括3个通用定时器、1个高级定时器TIM1、多个ADC和通信接口,价格在几块钱到十几块钱之间。做三相SPWM输出,核心需求是至少6路PWM、能配置互补输出和死区、更新频率足够快。TIM1刚好是高级定时器,自带互补输出、死区生成和刹车保护功能,硬件上天然适合干这件事。

相较于用DSP或者专门的电机控制芯片,F103C8T6的性价比很突出。国产替代料也遍地都是,即使货源紧张也能找到兼容型号顶上,这一点在项目落地时很重要。对于学习用途或者中小批量产品,这个方案非常稳妥。

1.2 系统总体架构

整个系统的信号流是这样的:

  • 单片机内部生成正弦表,通过查表法得到三相互差120°的占空比序列。
  • TIM1的三个通道配置为PWM模式,每个通道输出一对互补信号,分别是CHx和CHxN。
  • 互补输出的上升沿/下降沿之间插入死区时间,防止上下桥臂同时导通。
  • 通过软件定时器或者定时器更新中断,周期性修改正弦表查表指针和占空比幅值,实现调频和调压。
  • 输出的六路PWM经过驱动芯片放大后驱动三相全桥逆变器,最终输出到电机或负载。

六路互补输出是电动机驱动的基本盘,因为三相全桥有6个开关管,每一相桥臂的上下管必须严格互补导通,一旦出现上下管同时导通,母线直接短路烧管子,这也是死区存在的根本原因。

1.3 关键参数预计算

动手写代码之前,需要先把几个核心参数定下来,后面所有配置都围绕这几个数值。

  • PWM载波频率:常见选16kHz或者20kHz。16kHz高于人耳听觉上限,电机运行噪音小,同时F103在72MHz下处理这个频率的更新中断毫无压力。我做的是16kHz。
  • 正弦表点数:取256点。点数越多波形越细腻,但占用存储空间也越多,256点是精度和资源的平衡点。
  • 调制比(调制度):最大值取0.9左右,留出死区影响余量,防止过调制导致波形畸变。
  • 死区时间:取决于功率器件和驱动电路,我用的MOS管加IR2101驱动,设置1.5us比较安全。这个后面细说怎么算。

有了这些参数,定时器的ARR和PSC就定了。定时器时钟72MHz,PWM频率16kHz,则计数周期为72MHz / 16kHz = 4500个计数单位,所以ARR = 4499。死区时间若设为1.5us,对应计数个数为1.5us × 72MHz = 108个计数单位。

2. SPWM生成原理与调频调压思路

2.1 从正弦波到SPWM脉冲序列

SPWM的核心思路是用一系列宽度按正弦规律变化的脉冲,去等效一个正弦波。根据面积等效原理,脉冲宽度越接近正弦曲线在该时刻的积分值,经过低通滤波之后得到的波形就越接近理想正弦波。数字实现时,典型做法是“三角波载波比较法”:正弦调制波与高频三角载波比较,交点决定开关切换时刻。

STM32里做这件事不需要真的生成三角波比较,直接用定时器的计数器当作载波,ARR对应三角波峰值,比较寄存器CCR的值对应调制波瞬时值。CCR按正弦规律变化,输出脉冲宽度就按正弦规律变化了。CCR的计算公式是:

CCR = ARR × (0.5 + 0.5 × M × sin(θ))

其中M为调制比(0到1之间),θ为当前采样角度。公式里的0.5偏置让占空比在0到100%之间变化,M控制正弦波的峰值幅度。

2.2 查表法生成三相互差120°的波形

单片机上实时计算sin函数耗CPU资源,不划算。常规做法是提前算好一张正弦表存到Flash里,运行时按索引查表。比如256点正弦表覆盖0°到360°。A相从0°开始查表,B相滞后120°即滞后256/3≈85.33个点,C相滞后240°即滞后170.67个点。实际用整数索引时,可以取85和171,或者采用更高精度的做法:

#define TABLE_SIZE 256 #define PHASE_SHIFT 85 // 120°对应的查表步长

三路CCR值这样更新:

ccr_a = (uint16_t)(ARR * (0.5f + 0.5f * M * sin_table[phase_a])); ccr_b = (uint16_t)(ARR * (0.5f + 0.5f * M * sin_table[(phase_a + PHASE_SHIFT) % TABLE_SIZE])); ccr_c = (uint16_t)(ARR * (0.5f + 0.5f * M * sin_table[(phase_a + 2 * PHASE_SHIFT) % TABLE_SIZE]));

实际工程中更高效的做法是用定点数和整数查表,避免浮点运算。本文为了可读性用浮点说明,实测F103跑浮点也没问题,只是中断里尽量少算,最好提前算好。

2.3 调频调压的核心逻辑

调频就是改变正弦波的角频率,体现在代码里就是改变每次中断查表指针的增量(步长)。系统时钟固定,PWM周期固定(16kHz),每次更新中断对应一个PWM周期。若每个PWM周期更新一次,则正弦表更新步长step与输出频率f的关系为:

step = f × TABLE_SIZE / f_pwm = f × 256 / 16000

比如要输出50Hz,step = 50×256/16000 = 0.8。输出5Hz时step = 0.08。实际中用定点数累加器来做,每次累加step并取整。

调压则是改变调制比M。典型电机控制用恒压频比(V/F)曲线,即电压与频率之比保持恒定,以维持磁通恒定。基频以下采用V/F恒定,基频以上电压达到最大值,进入弱磁区。

// V/F控制:频率升高时调制比线性增加 if (freq <= BASE_FREQ) { m = M_MAX * freq / BASE_FREQ; } else { m = M_MAX; }

低频时需要加电压补偿,因为死区效应和管压降在低频时占比大,不补偿的话电机启动无力甚至堵转。这一点后面调试部分细讲。

3. TIM1互补输出与死区配置详解

3.1 为什么必须加死区

全桥电路里,同一相上下两个开关管如果同时导通,电源正负极直接短路,瞬间大电流烧毁功率管。理想的互补PWM在切换瞬间,上管关断的同时下管导通,但实际功率器件都有开通延迟和关断延迟,而且关断通常比开通慢。如果不插入死区,切换瞬间必然存在上下管同时导通的时间窗口。

死区时间就是在上下管切换时,主动插入一段“上下管都不导通”的时间。比如上管从高变低后,等待1.5us再让下管从低变高。这样即使器件有延迟,也不会产生直通风险。死区过短会烧管,过长会导致输出电压畸变和电流谐波增大,取值需要在安全性和谐波之间权衡。

3.2 死区时间的计算与设置

死区时间需要覆盖“最坏情况下的器件关断延迟 + 驱动电路延迟 + 余量”。以IR2101驱动加IRFP460 MOS管为例:

  • 驱动芯片传输延迟:典型值约100-200ns
  • MOS管关断延迟:约100-200ns
  • 安全余量:取1us以上比较放心

所以总死区设置在1us到2us之间比较合理。我最终定1.5us。如果你用IGBT,关断延迟在几百ns到1us级别,IGBT还有拖尾电流,死区一般要2-5us,具体要查器件手册。

STM32F103的TIM1死区由BDTR寄存器的DTG位配置。死区时间的计算公式按DTG位段分四种情况:

  • DTG[7:5] = 0xx:DT = DTG[7:0] × Tdtg
  • DTG[7:5] = 10x:DT = (64 + DTG[5:0]) × Tdtg × 2
  • DTG[7:5] = 110:DT = (32 + DTG[4:0]) × Tdtg × 8
  • DTG[7:5] = 111:DT = (32 + DTG[4:0]) × Tdtg × 16

其中Tdtg = 1/72MHz ≈ 13.89ns。我需要的死区是1.5us = 1500ns,先除以13.89ns得到108,也就是说DTG位段可以直接取108(0x6C),落在第一种格式。用标准库配置如下:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 时基配置:72MHz / 16kHz,ARR = 4499 TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 4499; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // PWM模式1,输出极性高电平有效 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); // 死区设置:1.5us,对应DTG = 108 TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = 108; 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);

补充一点:使用TIM1时必须使能MOE主输出,否则PWM不会输出。库函数中TIM_CtrlPWMOutputs(TIM1, ENABLE)就是干这个的,很多人漏了这一步,结果示波器看不到波形。

3.3 更新PWM占空比的方式选择

SPWM要求每个PWM周期都更新CCR值,更新方式有两种:

  • 定时器更新中断里写CCR:简单直观,但每个周期都要进中断,16kHz下中断频率较高,中断服务函数要尽量精简。
  • DMA方式更新CCR:可以在内存中准备好一个周期的CCR序列,用定时器更新事件触发DMA搬运到CCR寄存器,CPU负担极小。

我调试初期用中断方式方便观察,稳定后改成了DMA环形缓冲方式。如果只是学习和实验,中断方式完全够用,不追求极限效率的话没必要上DMA。

3.4 用CubeMX快速生成初始化代码

如果不想手写寄存器或者标准库,可以用STM32CubeMX图形化配置。

选择STM32F103C8T6芯片,在TIM1界面配置:

  • Clock Source选择Internal Clock
  • Channel1、2、3全部选择PWM Generation CHx and CHxN
  • Prescaler设为0,Counter Period设为4499,Pulse设个初始值
  • Dead Time设为108(对应1.5us,注意CubeMX里单位是时钟周期数)
  • Break设置根据需要开或关

生成代码后,在main函数里调用HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1)等函数启动三个通道,注意互补通道不用单独启动,硬件自动跟随。HAL库生成的代码基础上再写中断或DMA更新逻辑即可。用CubeMX的好处是初始化代码不容易写错,特别适合刚接触定时器配置的读者。

4. 完整代码实现与调试实录

4.1 中断方式更新占空比的参考代码

下面给出一套用中断方式实时更新三路CCR的参考代码骨架。中断频率为16kHz,每个周期计算一次三相占空比。为减少中断内浮点运算,实际工程建议把正弦表存为0-255范围的整数,用整数运算计算CCR。

// 全局变量 volatile uint16_t phase_acc = 0; // 相位累加器,定点数 uint16_t step_acc; // 频率对应的步长 float current_m = 0.9f; // 当前调制比 const uint16_t sin_table[256] = { /* 预先生成的正弦表,值域0~4095 */ }; // 定时器更新中断 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); uint16_t phase = phase_acc >> 8; // 高8位作为查表索引 uint16_t phase_b = (phase + 85) & 0xFF; uint16_t phase_c = (phase + 171) & 0xFF; uint16_t ccr_a = (uint16_t)((uint32_t)sin_table[phase] * current_m >> 12); uint16_t ccr_b = (uint16_t)((uint32_t)sin_table[phase_b] * current_m >> 12); uint16_t ccr_c = (uint16_t)((uint32_t)sin_table[phase_c] * current_m >> 12); TIM_SetCompare1(TIM1, ccr_a); TIM_SetCompare2(TIM1, ccr_b); TIM_SetCompare3(TIM1, ccr_c); phase_acc += step_acc; // 步长决定输出频率 } } // 设置输出频率:freq单位为0.1Hz void set_frequency(uint16_t freq_x10) { // f = step * f_pwm / 256 => step = f * 256 / f_pwm // 避免浮点,f_pwm = 16000,256/16000 = 1/62.5 // 用定点:step_x16 = f_x10 * 256 * 16 / 16000 * 10 step_acc = (uint16_t)((uint32_t)freq_x10 * 256 * 16 / 16000 / 10); }

这里面phase_acc用定点小数表示相位,低8位是小数部分,高8位是整数索引,频率分辨率能做到很高。步长的计算实际就是f × 256 / 16000,代码里乘16再除10是为了处理0.1Hz精度的输入。

4.2 正弦表的生成与精度分析

正弦表可以用PC端脚本生成,也可以用单片机启动时计算,推荐前者。下面用Python生成一段C语言数组,值域0到4095(对应12位精度):

import math TABLE_SIZE = 256 MAX_VAL = 4095 data = [] for i in range(TABLE_SIZE): value = int((math.sin(2 * math.pi * i / TABLE_SIZE) + 1) * MAX_VAL / 2) data.append(value) lines = [] for i in range(0, TABLE_SIZE, 12): lines.append(", ".join(str(v) for v in data[i:i+12])) print("const uint16_t sin_table[256] = {") for line in lines: print(" " + line + ",") print("};")

12位精度对应4096个量化等级,在16kHz载波频率下完全够用。调制比M通过移位实现,上面对应的其实是current_m = 调制比 × 4095,所以计算时直接相乘再右移12位。

4.3 调频调压的人机接口设计

调频调压最终要能改参数,最简单的方案用两个电位器接ADC,一路控制频率、一路控制调制比。也可以加按键和OLED显示,做成微型变频器面板。我做的实验板就是两个旋钮,ADC采样后数字滤波,频率范围5Hz到120Hz,调制比0到0.95。

ADC读取电压换算成频率时,要注意设置频率变化速率限制,也就是工程上说的S曲线加减速。如果频率突变太快,电流冲击很大,容易过流保护甚至烧管子。我实测从50Hz直接切到5Hz,母线电流峰值会翻倍还多,加了加减速限制后就好很多。

4.4 硬件接线与驱动板注意事项

单片机输出的PWM电平通常是3.3V,不能直接驱动功率管,必须经过驱动芯片。我用的是IR2101半桥驱动,自带自举电路,一片驱动一相桥臂。需要注意:

  • 自举电容选择:典型用1uF到10uF的陶瓷电容或钽电容,耐压要大于母线电压。电容太小会导致高端驱动电压不足,输出波形异常。
  • 驱动芯片的地要和功率地严格分开,单点连接,避免功率电流干扰控制信号。
  • GPIO配置为复用推挽输出,速度设为50MHz,否则PWM边沿不够陡。
  • 电源去耦:靠近芯片引脚放置100nF陶瓷电容,模拟数字分开铺地。

如果直接用6N137等光耦隔离,需要注意光耦的传输延迟,死区计算时要考虑进去。我调试时发现一个有趣的现象:光耦导致的延迟会让实际死区变长,但方向上不会造成直通,只是波形失真更明显。

5. 调试踩坑记录与常见问题速查

5.1 上电烧管子的几个经典原因

第一,先给PWM还是先给母线电压。正确顺序是:先让单片机输出确定电平的PWM,确认六个管子的栅极信号稳定后,再给母线电压上电。我刚开始偷懒,一上电就全开,直接烧了两个MOS管,后来加了软启动逻辑才稳定。

第二,刹车功能(Break)没配置或者引脚悬空。TIM1的BKIN引脚如果没做初始化且悬空,很容易受干扰误触发刹车,PWM被强制拉低,表现为时好时坏。如果不用刹车功能,最好配置为禁用状态,或者把BKIN引脚上拉到确定电平。

第三,驱动芯片的死区时间和单片机死区叠加。如果驱动芯片自身带有死区插入功能,单片机的死区要相应减小或者关闭,两边叠加会导致死区过长,波形畸变严重。

5.2 输出波形异常的排查思路

如果示波器看到某一相上下管波形不对称,检查CCR的更新时机和PWM周期是否对齐。定时器的影子寄存器在溢出时才更新,如果中断里写入CCR但更新事件被其他代码阻塞,就会出现都挤在同一个周期更新,导致脉冲宽度乱跳。

如果三相波形没有互差120°,检查查表索引是否溢出。我用uint8_t做索引时,85和171的加法会自然溢出回绕,这是没问题的。但如果你把索引声明成uint16_t,又没有对256取模,就会访问数组越界,波形自然就乱了。

如果PWM频率调高后波形失真,观察载波比。载波频率f_pwm固定为16kHz,输出频率越高,每个正弦周期内的脉冲数越少(即载波比越低),谐波增大。我用120Hz输出时,每个周期约133个脉冲,波形看起来还可以;到200Hz就只有80个脉冲,失真明显。这就是同步调制和异步调制需要切换的问题。

5.3 死区时间调整的实测经验

我把死区从0.5us到3us分别测了波形,总结如下:

  • 0.5us时,一上电母线电流就有明显尖峰,MOS管发烫,说明死区不够,存在瞬间直通。
  • 1.5us时波形干净,电流平稳,温升正常。
  • 3us时波形出现明显台阶,电流谐波增加,电机噪音变大。

调试时最靠谱的验证方法是用电流探头观察母线电流,或者观察电机运行噪音。没有电流探头的话,可以摸管子温度,直通严重时管子在几秒内就会烫到不能碰。

5.4 低频性能与死区补偿

低频输出时死区造成的电压损失占比更大。举一个具体的数:死区1.5us,PWM周期62.5us,死区占比2.4%。输出50Hz时,这个比例的电压损失对整体波形影响有限,但输出5Hz时,正弦波幅值本身很小,同样的死区时间损失在正负半周交替点会产生明显的波形削顶,表现为电流波形在过零点附近出现非线性畸变。

简单有效的补偿方法是按电流极性叠加补偿量:电流为正时增加占空比,电流为负时减小占空比,补偿量等于死区时间对应的占空比。这个方法需要检测电流过零,实际电路要加电流传感器或者采样电阻。如果只是做简单的V/F控制,不做电流环,可以先用固定的开环补偿,在中低频段给调制比加一个固定偏置,也能明显改善启动性能。

5.5 常见问题速查表

现象可能原因解决办法
PWM无输出未使能TIM1主输出MOE调用TIM_CtrlPWMOutputs(TIM1, ENABLE)
上下管同时导通死区设置过小增大BDTR中的死区时间
某一相波形异常CCR更新时机不对检查中断优先级和更新事件标志
频率调高后波形失真载波比过低更换更高的载波频率或降低最高输出频率
电机低频抖动死区电压损失未补偿加入死区补偿或低频电压补偿
上电偶尔烧管PWM未就绪就上电增加软启动时序,先PWM后母线
GPIO波形边沿缓GPIO速度配置过低设置GPIO_Speed_50MHz

5.6 个人实操心得

这套系统我从画板到调通前后折腾了小两周。回头总结,最大的心得是:SPWM本身不复杂,复杂的是边界条件的管理——死区够不够、顺序对不对、保护到不到位。调试时不要一上来就满压满频跑,用低压直流电源(比如12V或24V)先验证六路PWM波形,确认死区和互补关系完全正确后再升压。低压调试能烧的管子有限,成本低太多。

另外买个靠谱的逻辑分析仪或者双通道示波器很有必要。六路PWM同时看需要4通道以上,但调试死区只需要看同一相的上下管两路波形,双通道示波器就够用。把触发模式设为脉宽触发,能快速抓到异常脉冲。

最后说一个扩展方向:这套代码稍加改动就能跑方波驱动(六步换相),或者加个霍尔传感器接口变成BLDC驱动器。如果换成TIM8做同样的配置,还能再做一组三相输出,双电机控制也不在话下。F103C8T6这个板子确实是一块性价比极高的学习平台,吃透这套SPWM方案,后面再上FOC或者更高级的电机控制算法,会顺畅很多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询