ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入浅出SVPWM:从原理到STM32代码实现与调试

深入浅出SVPWM:从原理到STM32代码实现与调试 做FOC电机控制的工程师到了最后都会面对SVPWM——空间矢量脉宽调制。它是FOC算法链路的最后一公里决定了电流环输出的电压矢量能不能变成实际加到电机三相绕组上的PWM波形。很多人学FOC时Clarke变换、Park变换、PI调节器都理解了偏偏卡在SVPWM这里总觉得扇区判断、矢量作用时间、七段式切换这些概念绕来绕去代码更是怎么抄都感觉心里没底。这篇文章我就把标准SVPWM从头到尾拆开不讲虚的直接结合我自己的实现经验把原理、公式、代码和调试方法串起来讲。你不需要提前懂空间矢量数学只需要有基本的PWM和三相逆变器概念跟着走一遍回去就能自己写出一份能跑的SVPWM并把它嵌进你的FOC控制环里。文章默认你用的是STM32这类带高级定时器的MCU三相全桥逆变器采样电阻在下桥臂或三电阻方案都行。1. 先搞清楚SVPWM在整个FOC里到底是干嘛的1.1 FOC的最后一公里FOC控制的本质是让电机定子电流矢量始终与转子磁场保持特定角度从而产生恒定转矩。整个算法链路上电流采样得到Ia、Ib经过Clarke变换得到静止两相坐标系下的Iα、Iβ再经过Park变换得到旋转坐标系下的Id、Iq然后和给定值做PI调节输出电压Vd、Vq再反Park变换得到Vα、Vβ。到这里你知道控制器想让电机产生什么样的电压矢量但MCU的引脚只会输出高低电平不可能直接输出一个连续变化的电压。SVPWM就是负责把Vα、Vβ这个抽象的电压矢量翻译成三相桥臂上六个功率管具体什么时候开、什么时候关、导通多长时间。这个过程本质上是在一个PWM周期内用八个基本开关状态去“拼”出任意方向和任意大小的电压矢量。这句话理解了SVPWM就算入门了。1.2 为什么不用SPWM可能有人问我用最普通的SPWM就是三路正弦波和三角波比较不也能驱动电机么确实可以早期的变频器很多就是SPWM。但SPWM有个明显短板母线电压利用率低线性区最大相电压幅值只有母线电压的一半折算到线电压也就0.866倍的母线电压。换句话说你给电机灌的电压潜能没完全发挥出来。SVPWM的优势在于它给逆变器的不是三路独立的SPWM而是一个合成的电压空间矢量。它通过在不同扇区内选择相邻的基本矢量并分配作用时间把线电压的线性输出范围拉到了母线电压的0.866倍换算下来相电压线性区能达到母线电压的1/√3。同时SVPWM的谐波特性更好电流纹波更小同样的母线电压下电机能跑出更高的转速。这就是它成为FOC标配的原因。另外一个隐藏的优点SVPWM天然把零矢量的插入考虑进去了你可以通过分配两个零矢量的时间变相调整开关频率下的共模噪声特性这在EMC设计时也是个可用的手段。1.3 这东西适合谁现在就学如果你是刚接触FOC的初学者SVPWM是你绕不开的坎建议按照“先跑起来再深入原理”的思路先把代码移植到自己板子上看到电流波形正常了再回头抠公式。如果你已经在用别人封装好的FOC库想搞清楚里面PWM更新那部分到底做了什么这篇也能帮你把知识拼图补上。如果你准备自己写一套无感FOC那SVPWM更要扎实因为后面所有观测器、初始位置检测、弱磁控制输出的最终落脚点还是它。2. SVPWM的核心原理扇区、基本矢量和作用时间2.1 逆变器只有八个开关状态三相全桥逆变器有六个功率管同一桥臂上下管不能同时导通所以每一相只有两种状态上桥臂导通记为1或者下桥臂导通记为0。三相组合起来一共有2^38种状态。其中六个是非零矢量V1(001)、V2(010)、V3(011)、V4(100)、V5(101)、V6(110)两个是零矢量V0(000)和V7(111)。这六个非零矢量在空间上把平面均匀分成六个扇区每个扇区60度相邻两个非零矢量之间正好是60度。要合成任意一个电压矢量就在它所在的扇区里用相邻的两个非零矢量和零矢量按时间比例组合。这个原理特别像调颜色红、绿、蓝三种基础色按不同比例混合可以调出各种颜色。SVPWM就是用六个方向的基础矢量按时间比例混出一个任意方向的电压矢量时间比例就是作用时间。2.2 扇区判断其实就三次比较要合成矢量首先得知道目标电压矢量落在哪个扇区。这一步网上资料很多大部分是引入三个中间变量根据符号判断。很多初学者在这里被公式吓住其实逻辑很直接。假设你已经有Vα和Vβ来自反Park变换的输出定义三个参考量U1 Vβ U2 (√3/2) * Vα - (1/2) * Vβ U3 -(√3/2) * Vα - (1/2) * Vβ如果U1 0记A 1否则A 0如果U2 0记B 1否则B 0如果U3 0记C 1否则C 0。然后扇区号 N A 2B 4C。这个N值落在1到6之间但它和实际扇区编号需要一张映射表来对应。很多参考代码里会有这样一个数组static const uint8_t sectorMap[6] {2, 4, 6, 1, 3, 5};也就是计算出的N值对应数组里存的才是真实扇区号。这段代码本身不复杂复杂的是为什么会有这张表跟U1U2U3的方向定义有关。你不需要死记直接用现成映射就行但有一点要记住一旦你换了坐标变换的方向约定比如Vα轴和Vβ轴的朝向这张映射表可能要跟着变。这说明扇区判断没有唯一标准核心是保证矢量和扇区编号的一致关系。我自己的习惯是在MATLAB/Simulink里建个简单的SVPWM模型把扇区号显示出来和手算比对一遍确认映射关系对不对然后再把这个逻辑搬进C代码。仿真一次能省下实物调试好几个小时。2.3 矢量作用时间计算从矢量合成到占空比确定了扇区接下来就是算扇区里两个相邻非零矢量各自要作用多久零矢量要插多久。总的思路是先定义两个中间变量很多教程里叫X、Y、Z计算公式是X √3 * Vβ / Vdc * T Y ( (3/2) * Vα (√3/2) * Vβ ) / Vdc * T Z ( -(3/2) * Vα (√3/2) * Vβ ) / Vdc * T其中Vdc是母线电压T是PWM周期。这里除以Vdc是为了把电压标幺化把实际电压换成对母线电压的占比。然后根据扇区查表得到T1、T2扇区T1T21ZY2Y-X3-ZX4-XZ5X-Y6-Y-Z注意这里可能有负数负值说明计算用的Vα、Vβ或者扇区判断出了问题正常线性调制范围内T1、T2应该都是非负的。如果T1 T2 T说明电压矢量超出了SVPWM线性调制范围需要做过调制处理最简单的处理是把T1、T2等比缩小T1 T1 * T / (T1 T2) T2 T2 * T / (T1 T2)这样做的含义是只改变合成矢量的幅值不改变方向。实际调试时如果出现T1T2经常饱和说明你的电压给定超过了线性范围要么母线电压不够要么应该进入弱磁区考虑了。接着算三个切换点Ta、Tb、TcTa (T - T1 - T2) / 4 Tb Ta T1 / 2 Tc Tb T2 / 2这里用的就是七段式SVPWM也叫对称SVPWM。它把零矢量时间分成四份PWM波形在一个周期内关于中心对称谐波小、开关损耗均衡。之所以除以4是因为一个PWM周期里有四段零矢量插入这个细节和后面的定时器配置直接相关。再根据扇区把Ta、Tb、Tc分配给三相比较寄存器。这里也有一张映射表网上资料很多比如扇区1对应A相装载TaB相装载TbC相装载Tc其他扇区依次轮换。其实你不一定非按别人的表来只要保证三相的切换点分配关系和扇区号定义一致就行。调试时最直观的办法是先开环输出一个固定电角度用示波器看三相PWM的占空比关系是不是符合预期如果三相波形看着乱多半就是扇区映射和切换点分配没对好。七段式的波形有一个特点每个PWM周期内三相桥臂总共只有三次开关切换开关损耗较低。这一点在电机高速运转时尤其重要因为PWM频率一般都在10kHz以上如果换成五段式虽然能进一步降低开关次数但电流纹波会变大。标准FOC里七段式是默认选择稳定、谐波小、实现简单。3. 标准SVPWM代码实现一份可以直接用的MCU实现3.1 整个函数的输入和输出先梳理清楚SVPWM模块的边界。输入是一个周期内的Vα、Vβ输出是三路PWM比较值用来更新定时器的比较寄存器。这个函数通常在PWM中断里调用因为FOC的电流环周期和PWM周期是绑定的一般10kHz到20kHz。我这里基于STM32的高级定时器TIM1或TIM8配置成中心对齐模式PWM频率20kHz死区时间用户自己设定。中心对齐模式下定时器从0往上计数到ARR再从ARR往下计数到0所以比较寄存器里的值对应的是上升沿切换点而对称的另一半由硬件自动完成。这一点正好和七段式SVPWM的对称特性匹配。下面是我整理出的一份标准SVPWM实现去掉了项目中的其他逻辑只保留核心方便你直接看#include math.h #include svpwm.h #define SQRT3_2 1.7320508075688772f static const uint8_t SectorMap[6] {2, 4, 6, 1, 3, 5}; static const uint8_t SectorSwitch[6][3] { {1, 2, 3}, // 扇区1A相装载切换点1B相装载切换点2C相装载切换点3 {2, 1, 3}, // 扇区2 {3, 1, 2}, // 扇区3 {3, 2, 1}, // 扇区4 {1, 3, 2}, // 扇区5 {2, 3, 1} // 扇区6 }; void SVPWM_Generate(float Valpha, float Vbeta, float Vdc, uint16_t timerPeriod, uint16_t *ccrA, uint16_t *ccrB, uint16_t *ccrC) { float U1, U2, U3; float X, Y, Z; float T1, T2; float Ta, Tb, Tc; float T (float)timerPeriod; uint8_t sector, N; uint8_t sA, sB, sC; // 1. 扇区判断 U1 Vbeta; U2 (SQRT3_2) * Valpha - 0.5f * Vbeta; U3 -(SQRT3_2) * Valpha - 0.5f * Vbeta; N 0; if (U1 0) N 1; if (U2 0) N 2; if (U3 0) N 4; sector SectorMap[N]; // 2. 计算X、Y、Z X SQRT3_2 * Vbeta * T / Vdc; Y (1.5f * Valpha SQRT3_2 * Vbeta) * T / Vdc; Z (-1.5f * Valpha SQRT3_2 * Vbeta) * T / Vdc; // 3. 根据扇区选择T1、T2 switch (sector) { case 1: T1 Z; T2 Y; break; case 2: T1 Y; T2 -X; break; case 3: T1 -Z; T2 X; break; case 4: T1 -X; T2 Z; break; case 5: T1 X; T2 -Y; break; case 6: T1 -Y; T2 -Z; break; default: T1 0; T2 0; break; } // 4. 过调制处理 if ((T1 T2) T) { float scale T / (T1 T2); T1 * scale; T2 * scale; } // 5. 七段式切换点 Ta (T - T1 - T2) * 0.25f; Tb Ta T1 * 0.5f; Tc Tb T2 * 0.5f; // 6. 按扇区分配PWM比较值 sA SectorSwitch[sector - 1][0]; sB SectorSwitch[sector - 1][1]; sC SectorSwitch[sector - 1][2]; *ccrA (uint16_t)(sA 1 ? Ta : (sA 2 ? Tb : Tc)); *ccrB (uint16_t)(sB 1 ? Ta : (sB 2 ? Tb : Tc)); *ccrC (uint16_t)(sC 1 ? Ta : (sC 2 ? Tb : Tc)); }这份代码的核心在最后一步。SectorSwitch数组把切换点序号和三相通道对应起来比如扇区1时A相用Ta、B相用Tb、C相用Tc。这个映射跟我前面用的扇区编号约定是一致的。如果你自己改了扇区判断的公式方向这里的表也要同步改。3.2 定时器配置和互补PWM输出SVPWM算出的是比较值真正输出到功率管还要靠定时器生成带死区的互补PWM。以STM32为例我习惯把TIM1配成中心对齐模式1输出通道配置成PWM模式1并开启互补输出和死区生成。static void TIM1_PWM_Init(void) { // 省略GPIO和时钟部分的代码 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 4200 - 1; // 20kHz具体看时钟 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); 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_Set; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC3Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); 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 200; // 死区时间根据驱动芯片调整 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_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); }这里有一个新手特别容易忽略的问题STM32高级定时器的PWM输出还要再开一下TIM_CtrlPWMOutputs也就是主输出使能。很多人配完寄存器发现引脚没波形十有八九就是忘了这一步。另外占空比是直接往CCR寄存器里写如果是中心对齐模式装载到比较寄存器的值会决定PWM的翻转点SVPWM算出来的Ta、Tb、Tc本身就是以计数值为单位不需要额外换算。死区时间的设置要参考你用的驱动芯片和功率管的开关特性。死区太小上下管子可能直通直接炸桥死区太大波形会畸变电流谐波增加。我一般先把死区设成1us左右再用示波器看上下管驱动波形慢慢往小调直到看到轻微直通风险之前的位置停下来。这一步是硬件调测不能偷懒。3.3 与FOC主循环的配合SVPWM函数不是孤立运行的它和FOC电流环是在同一个中断里串起来的。我习惯的流程是ADC采样三相电流得到Ia、IbIc可以算出来Clarke变换得到Iα、Iβ编码器或观测器得到转子电角度θePark变换得到Id、IqId、Iq分别做PI调节输出Vd、Vq反Park变换得到Vα、Vβ调用SVPWM_Generate把Vα、Vβ转成三个CCR值更新TIM1的三个比较寄存器。这里要强调一个时序问题更新CCR寄存器必须在PWM周期的合适时刻。如果你在任意时刻更新可能造成PWM波形在周期中间突然跳变产生电流冲击。STM32提供了预装载功能你把CCR值写入后要等定时器计数到特定位置才会真正生效。我一般把预装载使能打开让它在下一次更新事件时生效。如果你用DSP或者其他MCU原理类似一定要找到对应的影子寄存器或预装载机制。另外SVPWM计算出来的比较值最好做个上下限钳位防止因为PI输出饱和、计算误差等原因导致比较值超过ARR或者变成负值。虽然前面有过调制保护但边界情况仍然可能出现加一层保护总没坏处if (*ccrA timerPeriod) *ccrA timerPeriod; if (*ccrB timerPeriod) *ccrB timerPeriod; if (*ccrC timerPeriod) *ccrC timerPeriod;有些实现里还会在这里考虑死区补偿因为死区时间的插入会导致实际输出电压和理想给定值之间有偏差特别是在低速轻载的时候电流波形会出现明显的削顶和畸变。标准SVPWM可以先不管死区补偿先把系统跑通后面再根据电流方向做补偿效果会更好。4. 调试SVPWM怎么确认你写的是对的4.1 开环验证从电角度到PWM完成代码移植后第一件事不是直接上闭环而是做开环验证。方法很简单给定一个固定电角度比如30度同时给定一个较小的电压幅值跑一遍SVPWM然后用示波器看三相PWM的占空比关系。这时候你期望看到的是三相PWM占空比按照某个规律分布比如A相占空比最大B相和C相稍微小一点三者加起来的总占空比在某个范围内。因为电角度固定三相的开关状态是稳定的不会出现闪烁。如果三相PWM看起来没有规律或者占空比明显不对称说明扇区映射或者切换点分配有问题。然后让电角度按照某个恒定速度线性增加相当于开环速度模式这时候三相PWM的占空比应该按正弦规律变化。用示波器看三路PWM的平均值也就是滤波后的波形应该是三相对称的正弦波相位相差120度。这里我建议用RC低通滤波器把PWM的平均值滤出来看比较直观。滤波器的截止频率要远低于PWM频率又要远高于电频率一般取几百赫兹到几kHz具体看你的电机转速范围。一个常见的坑开环给电角度时电机转向可能和预期相反。这不是SVPWM的问题而是坐标变换的符号约定问题这时候改一下给定电角度的累加方向就行不用去动SVPWM。4.2 用电流波形确认SVPWM的正确性开环PWM波形看着正常了就可以接上电机通过电流波形进一步确认。电流采样的质量这时候会直接影响你的判断。FOC里Clarke变换只需要两相电流是有前提的就是电机三相绕组的中性点没有引出、三相电流满足基尔霍夫电流定律即IaIbIc0所以测出任意两相就能算出第三相。这也是为什么开发板通常只在两相下桥臂装采样电阻最多加一相做冗余。但要注意采样得到的电流必须是在一个PWM周期内合适的时刻采的而且三相的采样时刻要一致不然算出来的电流矢量会有相位偏差闭环跑起来会抖动或者啸叫。我用示波器看相电流波形时重点看三个地方波形是否光滑、幅值是否稳定、相位是否互差120度。如果电流波形有周期性毛刺大概率是采样时机不对采到了PWM开关切换造成的尖峰如果波形削顶除了死区影响还要检查母线电压是不是足够、PI输出是不是饱和了如果三相电流的相位关系不对要回过去检查电流采样的相序和坐标变换的方向。还有一点经验调试时先把电流环的PI参数调松一点也就是比例和积分都设小一些让系统不那么容易振荡。很多新手一上来就把PI参数设得很激进结果电流波形乱成一团根本分不清是SVPWM的问题还是PI的问题。先让系统“迟钝”地转起来确认SVPWM输出正确了再去收紧PI参数。这个顺序能帮你把问题隔离得非常干净。SVPWM本身有没有写对还有一个更直接的判据看调制比和输出电压的极限。当电压矢量幅值接近SVPWM线性区极限也就是相电压幅值接近Vdc/√3时PWM波形应该进入过调制边界占空比出现平顶但电流波形仍然不应有太大的畸变。如果你在远低于这个电压时就出现严重的削顶说明作用时间计算或扇区切换有问题要回头检查母线电压的标幺化是不是搞错了。4.3 常见问题排障速查我把调试SVPWM时最常遇到的问题整理成一张表方便你对照排查现象可能原因排查方法三相PWM无输出主输出使能没开、定时器未启动检查TIM_CtrlPWMOutputs检查定时器状态输出波形不对称扇区映射表错误或切换点分配顺序不对固定电角度开环逐扇区看PWM关系母线电流异常大死区时间太小导致直通或占空比计算饱和示波器看上下管驱动波形确认死区检查过调制处理电流波形削顶电压给定超出线性范围、死区效应检查Vd、Vq输出幅值降低给定电压测试后续做死区补偿电机啸叫、电流抖动采样时机不对或PI参数过激进调整ADC采样窗口降低PI增益转速升高后波形变差电压利用率不足进入过调制区检查是否有过调制处理或考虑弱磁控制换相时有电流尖峰SVPWM切换点分配和扇区切换不同步检查SectorSwitch表与扇区编号是否正确对应这张表里的问题我几乎都踩过尤其是死区设置那一项第一次做驱动板的时候因为死区设太小上电瞬间直接烧了一个桥臂的管子从那以后我就养成了先确认死区波形再上功率电的习惯。现在我做FOC驱动的第一个测试项目永远是先给驱动板上电用示波器确认上下管驱动波形没有直通然后再接电机。5. 从标准SVPWM再往前走一步5.1 死区补偿与过调制扩展标准SVPWM跑顺之后你会发现它在低速和轻载工况下有个明显不足死区效应让实际输出电压偏小且带有明显的谐波电流波形会出现削顶和过零畸变。这时候需要做死区补偿。最简单的做法是根据三相电流的符号判断每个PWM周期里死区时间导致的电压误差方向然后在比较值上做加减修正。方向判断是关键在电流过零点附近很容易误判所以要加滞回比较。更高级的做法是观测反电动势或者用电感电流模型来估计但作为入门符号判断加滞回已经够用。过调制则是另一个方向。当电机高速运行需要超过线性调制范围时T1T2会持续饱和。简单等比缩小只能保证输出不超过线性区但电压利用率还是上不去。进阶一点的做法是分段过调制比如把调制比分成两个区间在一区间保持SVPWM线性输出在二区间调整零矢量作用时间为零并控制非零矢量的角度让实际输出电压矢量沿着六边形的边运动。这个技术在小体积、高转速的电机驱动上用得比较多。5.2 SVPWM与无感FOC的关系热搜词里有很多“无感FOC”相关的内容很多人觉得无感FOC难在观测器其实观测器估计出转速和转子位置之后最终还是要落到SVPWM上。无感FOC的堵转检测、初始位置检测本质上是在不同电压矢量下观察电流响应而这些“注入”的电压矢量也是通过SVPWM生成和施加的。所以你把标准SVPWM吃透了等于给整个无感FOC方案打好了地基。后面无论你是用滑模观测器、龙伯格观测器还是高频注入法都需要一个准确、稳定的SVPWM来执行你的“指令电压”。反过来说如果SVPWM输出有偏差观测器估计出来的位置和转速也一定会被带偏。我个人做项目时的体会是SVPWM调试的优先级要高于观测器调参。SVPWM正确了电流环才能稳定电流环稳定了观测器才有可信的输入。很多无感FOC跑不起来回头查根因SVPWM这边占了不少比例。与其在观测器参数里耗时间不如先返回来把SVPWM的每一行代码都确认清楚把开环波形做到无可挑剔再往上层走。最后再分享一个调试小技巧在你把SVPWM移植到新平台时写一个纯软件测试函数不依赖真实电机和功率板给定一组Vα、Vβ把算出来的CCR值打印出来和MATLAB仿真的结果比对。这个测试能帮你排除硬件干扰快速定位算法移植过程中引入的低级错误。我每次换芯片平台都会做这一步五分钟的测试能省下几个小时的在板调试时间。
返回列表