4. SVPWM的软件实现:定时器配置、死区补偿、SVPWM的C代码实现与波形验证
好,咱们进入实战环节。前面讲了SVPWM的原理和扇区判断,那些都是理论基础。但电机转不转,最终要看代码怎么写进MCU里。这一章,我带你手写SVPWM的C代码,搞定定时器配置和死区补偿,最后用示波器看波形——这一步走通了,你的电机才算真正“活”了。
4.1 定时器配置:生成中心对齐PWM
SVPWM的核心是输出三对互补的PWM波,而且必须是中心对齐模式。为什么?因为中心对齐模式下,PWM的占空比更新点正好在三角波的顶点和底点,这样能最大程度减少谐波,电流波形更平滑。
我个人习惯用STM32的高级定时器TIM1或TIM8来做。配置要点如下:
- 计数模式:设置为中心对齐模式1(Center-aligned mode 1)。计数器先向上计数到ARR,再向下计数到0。
- 自动重装载值(ARR):决定了PWM的周期。比如PWM频率20kHz,ARR = 系统时钟 / (2 × 20kHz) - 1。
- 互补输出:使能CHx和CHxN,分别控制上下桥臂。
- 死区插入:在刹车和死区寄存器(BDTR)中设置死区时间。
关键点:SVPWM的占空比更新必须在定时器更新中断或DMA中完成。我建议用DMA,因为中断太频繁(20kHz × 3相 = 60kHz中断),CPU扛不住。
下面是一个典型的定时器初始化代码片段:
// TIM1 中心对齐PWM配置(简化版)
void TIM1_SVPWM_Init(uint16_t arr, uint16_t deadtime) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
// 时基配置
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1;
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// PWM1模式配置(CH1, CH2, CH3)
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);
// 死区配置
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 = deadtime; // 死区时间
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE); // 主输出使能
}
避坑指南:死区时间不是越大越好。我曾经在一个项目中把死区设成5μs,结果电机低速运行时电流波形毛刺特别多。后来发现死区太大导致电流续流不充分。一般建议死区时间设为PWM周期的1%~2%,比如20kHz周期50μs,死区设0.5μs~1μs就够。
4.2 死区补偿:别让波形“缺胳膊少腿”
死区是必须的,防止上下桥臂直通。但死区也会带来副作用——输出电压波形会失真,尤其在低速轻载时,电流过零点附近会出现“死区效应”,导致电流波形畸变,甚至引起转矩脉动。
说白了,死区让实际输出电压比期望值少了那么一点点。补偿的思路就是:根据电流方向,把丢失的电压补回去。
死区补偿的C代码实现逻辑:
- 采样三相电流:判断每相电流的极性(正/负)。
- 计算补偿量:补偿电压 = 死区时间 × 母线电压 / PWM周期。
- 调整占空比:电流为正时,该相占空比增加补偿量;电流为负时,占空比减少补偿量。
// 死区补偿函数
void DeadTime_Compensate(float *duty_u, float *duty_v, float *duty_w,
float i_u, float i_v, float i_w, float vbus, uint16_t period) {
float comp_voltage = (float)DEADTIME * vbus / (float)period; // 补偿电压值
float comp_duty = comp_voltage / vbus; // 转换为占空比增量
// 根据电流方向补偿
if (i_u > 0) *duty_u += comp_duty;
else *duty_u -= comp_duty;
if (i_v > 0) *duty_v += comp_duty;
else *duty_v -= comp_duty;
if (i_w > 0) *duty_w += comp_duty;
else *duty_w -= comp_duty;
// 限幅处理
*duty_u = CLAMP(*duty_u, 0.0f, 1.0f);
*duty_v = CLAMP(*duty_v, 0.0f, 1.0f);
*duty_w = CLAMP(*duty_w, 0.0f, 1.0f);
}
注意:死区补偿依赖电流采样精度。如果电流采样有偏置或噪声,补偿反而会引入更大的谐波。我建议在电流采样稳定后再使能补偿功能,或者加一个低通滤波。
4.3 SVPWM的C代码实现:从扇区到占空比
终于到核心了。SVPWM的C代码实现,说白了就是三步:扇区判断 → 计算作用时间 → 映射到占空比。前面我们已经推导了扇区判断的公式,这里直接上代码。
先看扇区判断的实现:
// 扇区判断函数
uint8_t SVPWM_Sector_Calc(float v_alpha, float v_beta) {
uint8_t sector = 0;
float v_ref1 = v_beta;
float v_ref2 = 0.866025f * v_alpha - 0.5f * v_beta; // sqrt(3)/2 = 0.866025
float v_ref3 = -0.866025f * v_alpha - 0.5f * v_beta;
if (v_ref1 > 0) sector |= 0x01;
if (v_ref2 > 0) sector |= 0x02;
if (v_ref3 > 0) sector |= 0x04;
// 查表映射到实际扇区(1~6)
const uint8_t sector_map[8] = {0, 1, 5, 0, 3, 2, 4, 0};
return sector_map[sector];
}
然后计算两个相邻矢量的作用时间T1和T2:
// 计算T1和T2(输入为标幺化后的v_alpha, v_beta)
void SVPWM_Time_Calc(float v_alpha, float v_beta, uint8_t sector,
float *t1, float *t2, float ts) {
float t1_val, t2_val;
switch (sector) {
case 1: // 扇区1:U0(100) 和 U60(110)
t1_val = 0.866025f * v_alpha - 0.5f * v_beta; // 对应T4
t2_val = v_beta; // 对应T6
break;
case 2: // 扇区2:U60(110) 和 U120(010)
t1_val = 0.866025f * v_alpha + 0.5f * v_beta; // 对应T6
t2_val = -0.866025f * v_alpha + 0.5f * v_beta; // 对应T2
break;
// ... 其他扇区类似
default:
t1_val = 0; t2_val = 0;
break;
}
// 标幺化到PWM周期
*t1 = t1_val * ts;
*t2 = t2_val * ts;
// 过调制处理
float t_sum = *t1 + *t2;
if (t_sum > ts) {
*t1 = *t1 * ts / t_sum;
*t2 = *t2 * ts / t_sum;
}
}
最后,把T1和T2映射到三相占空比:
// 占空比计算(七段式SVPWM)
void SVPWM_Duty_Calc(uint8_t sector, float t1, float t2, float ts,
float *duty_u, float *duty_v, float *duty_w) {
float t0 = ts - t1 - t2; // 零矢量作用时间
float t_half = t0 * 0.5f; // 零矢量分到两边
float ta, tb, tc;
switch (sector) {
case 1:
ta = t_half;
tb = ta + t1;
tc = tb + t2;
break;
case 2:
ta = t_half + t1;
tb = t_half;
tc = tb + t2;
break;
// ... 其他扇区
default:
ta = tb = tc = 0;
break;
}
// 转换为占空比(0~1)
*duty_u = ta / ts;
*duty_v = tb / ts;
*duty_w = tc / ts;
}
经验之谈:七段式SVPWM比五段式谐波更小,但开关损耗稍高。如果你的应用对效率要求极高(比如电池供电),可以考虑五段式。我一般先用七段式调通,再根据温升情况决定是否切换。
4.4 波形验证:用示波器“看”SVPWM
代码写完了,怎么知道对不对?上示波器。我个人习惯先看单相PWM波形,再看相电压波形,最后看线电压波形和电流波形。
验证步骤:
- 单相PWM:用示波器探头夹在某一相上管栅极(或输出端),看PWM频率、占空比是否随给定电压变化。中心对齐的PWM应该是等腰梯形波。
- 相电压(对地):测量U相和GND之间的电压。正常SVPWM输出应该是马鞍波,不是正弦波!马鞍波中间凸起,两端下凹,这是SVPWM的特征。
- 线电压(相间):测量U-V之间的电压。线电压应该是正弦波,频率和给定频率一致。
- 相电流:用电流探头测一相电流。正弦度越好,说明SVPWM实现得越完美。
避坑指南:我曾经在调试时发现线电压波形有“台阶”,查了半天发现是死区补偿没使能。还有一次电流波形有低频振荡,结果是扇区判断的边界条件写错了。所以波形验证这一步千万别省,它能帮你快速定位问题。
4.5 本章知识体系总览
下面这张图总结了SVPWM软件实现的完整流程,从定时器配置到波形验证,每一步都环环相扣:
嗯,到这里SVPWM的软件实现就讲完了。定时器配置、死区补偿、C代码实现、波形验证,这四个环节缺一不可。你可以在开发板上先跑一下代码,用示波器看看马鞍波和正弦波——看到波形的那一刻,你会觉得前面的努力都值了。