ARTICLE DETAIL

资讯详情

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

SVPWM从原理到代码:FOC电机控制中扇区判断与七段式实现详解

SVPWM从原理到代码:FOC电机控制中扇区判断与七段式实现详解 搞电机FOC控制绕不开SVPWM。做FOC的都知道电流环速度环那一串PID、Clarke、Park算完最终都要变成三相逆变桥上六个开关管的开关动作这个从“电压矢量指令”到“具体开关时序”的翻译过程就是SVPWMSpace Vector Pulse Width Modulation。我当年刚开始调FOC时卡最久的地方之一就是SVPWM的扇区判断以及最后那几行作用时间查表。公式推不明白代码就不敢动电机一动就怕烧管子。后来硬着头皮把整个推导从头到尾啃了一遍才真正理解那些代码为什么这么写。这篇就把标准SVPWM从原理推导到工程实现完整过一遍适合正在看FOC代码、准备把SVPWM真正跑起来的同学也适合那些已经能出波形但想搞懂每个环节为什么这样设计的工程师。1. 先理顺一件事SVPWM在FOC里到底干了什么活1.1 FOC不是“三相正弦波直接怼”很多人刚开始学FOC的时候容易有一个直觉电机需要三相正弦电流那是不是我用定时器生成三路SPWM正弦波给逆变器就行了理论上能转但工程上不会这么干。SPWM的本质是用正弦波和三角波比较产生占空比变化的方波来逼近正弦电压。它实现起来简单可有一个很大的短板母线电压利用率低。三相SPWM输出的相电压幅值最大只能到 Vdc/2也就是直流母线电压的一半。而FOC走的是另一条路。FOC的核心思路是先把三相电流通过Clarke变换到αβ两相静止坐标系再通过Park变换到dq旋转坐标系在dq坐标系里把电流分解成励磁分量 id 和转矩分量 iq分别控制。所以FOC真正算出来的其实是一个电压矢量也就是 Uα 和 Uβ 这两个正交分量。这两个分量合在一起是一个空间旋转电压矢量它的幅值和相位都在不断变化。这时候问题就来了你算出来的是一个幅值、方向都会变的电压矢量逆变器是三相桥只有六个开关管怎么把这个矢量转换成实际的开关动作SPWM那种“每相独立生成正弦波”的思路在这时不直接匹配了。你需要一种能把“空间电压矢量”映射成“三相桥开关时序”的方法这就是SVPWM的活。一句话总结FOC负责决策告诉系统“当前该输出一个多大的电压矢量”SVPWM负责执行把这个矢量翻译成六个开关管在每个PWM周期内的开和关。没有SVPWMFOC算出来的矢量就停留在数学世界里落不到电机上。1.2 和SPWM对比母线电压利用率能提升15%以上我再补一个大家常提的点SVPWM相比SPWM母线电压利用率更高。SPWM的相电压峰值最多到 Vdc/2。而SVPWM因为用了空间矢量合成相电压峰值可以到 Vdc/√3 。这两个数字对比一下母线电压 Vdc 24V 时SPWM最大相电压幅值约 12VSVPWM最大相电压幅值约 13.86V提升比例 (13.86 - 12) / 12 ≈ 15.5%。别小看这15%。对很多低压驱动场景来说母线电压本来就紧张电机高速区反电动势又大能多挤出15%的电压就意味着电机能跑更高的转速或者在同样转速下留出更多电压余量。这也是为什么无感FOC、高速电机控制几乎全部采用SVPWM。另一个角度是谐波。SVPWM实际上是在相电压里注入了一个零序分量它输出的线电压更接近正弦谐波能量主要集中在开关频率及其倍频附近后级滤波和电机本身的电感都比较容易处理。你如果用示波器看SVPWM的相电压波形它不是纯正弦但线电压波形质量很高。2. SVPWM核心原理8个开关状态、6个扇区、公共量推导2.1 三相桥的8个开关状态SVPWM的起点是三相逆变器的开关状态。三相桥每相上下两个管子互补导通所以每一相只有一个开关变量上管导通记作1下管导通记作0。三相合起来就是3位二进制数一共 2^3 8 种状态000、001、010、011、100、101、110、111。这8个状态对应到空间电压矢量前6个是非零电压矢量后2个是零电压矢量。非零矢量的幅值都等于 (2/3)Vdc方向如下图所示V1指向0°V2指向60°V3指向120°以此类推每60°一个把整个360°平面分成6个扇区。零矢量000和111位于原点幅值为0。这6个扇区就是以这个基础电压矢量为边界划分出来的0°到60°是扇区I60°到120°是扇区II120°到180°是扇区III180°到240°是扇区IV240°到300°是扇区V300°到360°是扇区VI。SVPWM的“电压矢量合成”思想是这样的任何一个目标电压矢量如果落在某个扇区里就可以用这个扇区相邻的两个非零电压矢量再加上零矢量按时间比例组合出来。这叫伏秒平衡通俗讲就是“让两个基本矢量分别作用一段时间使最终的平均效果等于目标矢量”。这里有个细节必须注意同一桥臂上下两个管子绝对不能同时导通否则直接短路炸管子。所以实际控制里开关状态切换时必须插入死区时间让上下管先都关断一小段时间再开通另一只。死区时间通常是几百纳秒到几微秒具体看功率管参数。2.2 扇区判断不用atan2用符号组合知道了目标电压矢量 Uα 和 Uβ 之后第一个问题就是这个矢量落在哪个扇区最直观的方法是用反正切函数算出矢量角度再判断角度落在哪个区间。但反正切计算量大对没有FPU的单片机来说开销太高。工程上更常用的是符号判断法不需要角度只需要比较几个线性组合的符号。具体做法是定义三个中间变量B1 Ubeta B2 (√3 / 2) * Ualpha - (1 / 2) * Ubeta B3 -(√3 / 2) * Ualpha - (1 / 2) * Ubeta然后用这三个变量的符号组合出N值if (B1 0) A 1; else A 0; if (B2 0) B 1; else B 0; if (B3 0) C 1; else C 0; N A 2 * B 4 * C;N值和扇区的对应关系实际用下来是N扇区3I1II5III4IV6V2VI这个映射关系我在代码里直接用一个数组索引就完事不需要写一堆if else。B1、B2、B3本质上代表的是目标矢量相对于三个边界0°、60°、120°方向的位置组合起来就能唯一确定所在扇区。注意网上一些资料里的N值映射表可能不同原因在于B2、B3定义时比例系数写法不同。赋值关系不同映射自然不同。最稳妥的做法是拿到代码后用 Simulink 或者 Python 仿真验证一遍把电压矢量扫一圈看扇区判断是否连续无跳变。2.3 两个非零矢量的作用时间公共量X/Y/Z的完整推导扇区判断完之后需要计算这个扇区相邻两个非零电压矢量各作用多长时间。这是很多人的“劝退点”因为对着表看一会儿就头晕。我在这里把推导过程完整走一遍。以扇区I为例。扇区I由V1100方向0°和V2110方向60°合成目标矢量。设V1的作用时间是T1V2的作用时间是T2PWM周期是Ts。根据伏秒平衡在αβ坐标系下有Ualpha * Ts |V1| * T1 * cos(0°) |V2| * T2 * cos(60°) Ubeta * Ts |V1| * T1 * sin(0°) |V2| * T2 * sin(60°)非零矢量幅值 |V1| |V2| (2/3)Vdc代入得Ualpha * Ts (2/3) * Vdc * T1 (2/3) * Vdc * T2 * 0.5 Ubeta * Ts (2/3) * Vdc * T2 * (√3/2)整理第二个式子T2 (√3 * Ubeta * Ts) / Vdc把T2代回第一个式子Ualpha * Ts (2/3) * Vdc * T1 (1/3) * Vdc * T2解得T1 (3 * Ualpha * Ts) / (2 * Vdc) - (1/2) * T2 (3 * Ualpha * Ts) / (2 * Vdc) - (√3 * Ubeta * Ts) / (2 * Vdc) (3 * Ualpha - √3 * Ubeta) * Ts / (2 * Vdc)到这里如果每个扇区都这样解一次二元一次方程代码写起来会很长。聪明一点的做法是定义一组公共量把六个扇区的计算统一起来。工程上常用的是X、Y、Z三个量X (√3 * Ubeta) / Vdc Y (3/2 * Ualpha √3/2 * Ubeta) / Vdc Z (3/2 * Ualpha - √3/2 * Ubeta) / Vdc注意这里算出来的X、Y、Z是“相对时间系数”实际作用时间要再乘以Ts。但因为我们最后还要统一限幅归一化很多工程代码里干脆不乘Ts直接拿X、Y、Z来算占空比。用我刚才在扇区I里推出来的T1、T2和X、Y、Z对照扇区IT1 ZT2 X。用同样的方法把剩下五个扇区都推一遍得到扇区T1T2IZXIIY-ZIIIX-YIV-Z-XV-YZVI-XY这里 T1 和 T2 分别对应扇区起始边和结束边的非零矢量。比如扇区II里T1对应V260°方向T2对应V3120°方向。写代码时只需要算一遍X、Y、Z然后根据扇区查表选择T1、T2这就是“公共量”的意义一次计算六扇区复用。强烈建议你自己动手推一遍至少三个扇区特别是扇区II和扇区IV。推完你再看任何SVPWM代码都不会再觉得那一串case是“魔法数字”。2.4 七段式分配与比较值算出T1、T2之后还有一个零矢量时间T0T0 Ts - T1 - T2零矢量有两种000和111。把T0平均分给这两个零矢量就形成了经典七段式SVPWM。以扇区I为例一个完整PWM周期内的开关序列是000 → 100 → 110 → 111 → 110 → 100 → 000各段时间分别是T0/4, T1/2, T2/2, T0/2, T2/2, T1/2, T0/4为什么用七段式而不是五段式七段式在每个载波周期开始和结束都是零矢量000对称性好谐波集中在开关频率及其倍频电流纹波小缺点是开关次数多开关损耗大。五段式砍掉了一段零矢量开关次数少效率高但谐波含量和电流纹波会变大。电机FOC一般用七段式性能优先。七段式的关键是把T0/4、T0/4T1/2、T0/4T1/2T2/2这三个时刻对应到三相比较值。我习惯定义Ta T0 / 4 Tb Ta T1 / 2 Tc Tb T2 / 2这三个值就是三个通道在载波周期内的切换时刻。但到底哪个时刻赋给A相、B相、C相取决于当前扇区和定时器的极性设置。我在下一章写代码的时候会具体说明映射表。3. 标准SVPWM代码实现STM32工程落地3.1 整体框架与变量定义先说一下运行环境。我用的是STM32F103系列72MHz主频TIM1做中心对齐PWMADC触发采样。浮点运算在F103上偏慢但SVPWM这个函数只算几次乘加法用浮点也能扛得住。如果你用的是Cortex-M4或者M7带FPU那就完全不用纠结直接上浮点代码清晰第一。SVPWM函数的核心输入是UalphaFOC输出的α轴电压分量Ubetaβ轴电压分量Udc直流母线电压TsPWM周期也就是载波周期输出是三个通道的比较值CCR1、CCR2、CCR3直接写进定时器寄存器。实际工程里Ualpha和Ubeta来自哪里常见有两条路。一条是电流环PI输出Uq、Ud经过逆Park变换得到Ualpha、Ubeta另一条是无感FOC里速度环输出iq_ref电流环算完再走逆Park。无论哪条路到SVPWM这里都归一化成Ualpha、Ubeta。还要注意一个“归一化”问题。很多代码里不直接输出电压值而是先把Ualpha、Ubeta除以Udc变成标幺值后续计算全是相对比例最后再乘上定时器的ARR寄存器的对应值输出。这样做的好处是适配不同的母线电压而且便于观察过调制。我下面给的示例就采用这种思路。3.2 关键代码逐段拆解先看扇区判断的代码实现void svpwm_calc(float Ualpha, float Ubeta, float Udc, float Ts) { float B1, B2, B3; float X, Y, Z; float T1, T2, T0; float Ta, Tb, Tc; int sector 0; int N 0; float temp 0; // 扇区判断用的中间量 B1 Ubeta; B2 0.866025f * Ualpha - 0.5f * Ubeta; // √3/2 * Ualpha - 1/2 * Ubeta B3 -0.866025f * Ualpha - 0.5f * Ubeta; // -√3/2 * Ualpha - 1/2 * Ubeta // 符号判定 if (B1 0) N 1; if (B2 0) N 2; if (B3 0) N 4; // N值转扇区关系见上一节 switch (N) { case 3: sector 1; break; case 1: sector 2; break; case 5: sector 3; break; case 4: sector 4; break; case 6: sector 5; break; case 2: sector 6; break; default: sector 1; break; } // 公共量X、Y、Z X (1.7320508f * Ubeta) / Udc; Y (1.5f * Ualpha 0.8660254f * Ubeta) / Udc; Z (1.5f * Ualpha - 0.8660254f * Ubeta) / Udc; // 根据扇区选择T1、T2 switch (sector) { case 1: T1 Z; T2 X; break; case 2: T1 Y; T2 -Z; break; case 3: T1 X; T2 -Y; break; case 4: T1 -Z; T2 -X; break; case 5: T1 -Y; T2 Z; break; case 6: T1 -X; T2 Y; break; default: T1 Z; T2 X; break; } // 过调制处理T1T2大于Ts时按比例缩放 temp T1 T2; if (temp Ts) { T1 T1 * Ts / temp; T2 T2 * Ts / temp; } T0 Ts - T1 - T2; // 三个切换时刻 Ta T0 * 0.25f; Tb Ta T1 * 0.5f; Tc Tb T2 * 0.5f; // 根据扇区映射到三相CCR // 以下为PWM1模式、高电平有效、中心对齐 float CCR_Ta Ta / Ts * period; // period是定时器ARR值 float CCR_Tb Tb / Ts * period; float CCR_Tc Tc / Ts * period; switch (sector) { case 1: TIM1-CCR1 (uint32_t)CCR_Tc; TIM1-CCR2 (uint32_t)CCR_Tb; TIM1-CCR3 (uint32_t)CCR_Ta; break; case 2: TIM1-CCR1 (uint32_t)CCR_Tb; TIM1-CCR2 (uint32_t)CCR_Tc; TIM1-CCR3 (uint32_t)CCR_Ta; break; case 3: TIM1-CCR1 (uint32_t)CCR_Ta; TIM1-CCR2 (uint32_t)CCR_Tc; TIM1-CCR3 (uint32_t)CCR_Tb; break; case 4: TIM1-CCR1 (uint32_t)CCR_Ta; TIM1-CCR2 (uint32_t)CCR_Tb; TIM1-CCR3 (uint32_t)CCR_Tc; break; case 5: TIM1-CCR1 (uint32_t)CCR_Tb; TIM1-CCR2 (uint32_t)CCR_Ta; TIM1-CCR3 (uint32_t)CCR_Tc; break; case 6: TIM1-CCR1 (uint32_t)CCR_Tc; TIM1-CCR2 (uint32_t)CCR_Ta; TIM1-CCR3 (uint32_t)CCR_Tb; break; } }这段代码里有一个地方需要特别说明CCR的映射表。网上能找到的SVPWM代码这段映射经常都不一样根本原因有两个一是定时器输出的极性和PWM模式不同二是扇区编号的起始位置定义不同。我这里的用法是TIM1配置为中心对齐模式1PWM模式1输出极性为高。在这种配置下通道的CCR值越大实际导通时间越长。对应关系我已经在前面2.4节推导过三个切换时刻里最早切换的相反而是导通时间最长的那相所以Ta、Tb、Tc和CCR的对应不是直接一一对应而是倒序或者交错。以扇区I为例A相导通时间最长所以它的CCR要填Tc而不是Ta。如果你把极性改成低电平有效或者用了PWM模式2CCR越大反而导通时间越短那整个映射表要整体反过来。这正是很多人直接抄网上代码后电机波形不对的常见原因。3.3 与定时器、ADC的联动SVPWM函数算出来的CCR值最终要靠定时器硬件变成真实的PWM波形。这一点配置错了前面代码全对也没用。我常用的是TIM1因为高级定时器带互补输出和死区插入功能。关键配置PWM频率我一般用16kHz到20kHz。频率太低电流纹波大电机噪音明显频率太高开关损耗大对功率管要求高。16kHz正好避开人耳听觉敏感区是个折中选择。中心对齐模式选Center-aligned mode 1CMS01。在这种模式下计数器先向上计数到ARR再向下计数到0波形关于载波中心对称这就是七段式SVPWM想要的对称效果。ARR周期72MHz时20kHz PWM对应ARR大约为3600。这个值决定了SVPWM输出的分辨率ARR越大分辨率越高。死区时间我给F103配过典型的0.5us到1us死区。功率管规格书里有最小死区要求留一点余量。死区太短上下管直通风险大死区太长波形畸变和电流过零失真明显。然后是电流采样。这个和SVPWM强相关。FOC需要采样相电流采样点如果落在开关切换时刻采样到的全是尖峰噪声电流波形根本没法看。中心对齐PWM的好处是在载波的顶点计数到ARR和谷底计数到0附近三相都是零矢量状态开关噪声最小是天然的电流采样窗。我用的是在载波顶点触发ADC采样采样保持时间尽量靠近顶点。这样采出来的电流值能真实反映一个PWM周期内的平均电流电流环的反馈质量好很多。如果你发现电流采样毛刺大先别急着改滤波看看你的采样点是不是和PWM边沿撞在一起了。ADC触发方式用TIM1的更新事件或者TRGO事件都行。注意配置好ADC的采样时间和转换通道不要占用太长时间否则影响控制周期。4. 调试经验波形、限幅、常见坑4.1 典型问题速查表调SVPWM的过程中我碰到过不少奇奇怪怪的现象这里整理成一张速查表方便卡住的时候快速对照。现象可能原因排查方法电机不转母线电流大扇区判断错误三相开关时序不对用示波器同时看三相PWM检查每相导通时间是否按规律变化电机能转但噪音大PWM频率偏低或死区设置不当提高PWM频率到16kHz以上检查死区时间是否合适波形不对称某相明显异常比较值映射表与定时器极性不匹配改极性或换一套映射表用仿真先验证顺序高速时电压上不去过调制处理不对T1T2被简单截断确保按比例缩放T1、T2而不是限幅到Ts电流波形有大量毛刺电流采样点落在开关边沿把ADC采样点移到载波顶点或谷底启动瞬间抖动大低速区SVPWM分辨率不足提高PWM频率或降低最低转速限制死区导致的电流过零畸变死区时间太长功率管较慢用带死区补偿的FOC算法或减小死区4.2 这一路我踩过的漏先说过调制。刚写SVPWM时候我忽略了T1T2可能大于Ts的情况直接在代码里做限幅超过Ts就截断。结果电机轻载没问题一重载或者高转速就出问题电压矢量幅值被非线性压缩电流波形开始扭曲。正确的做法是等比例缩放如果T1T2 Ts就把T1和T2同时乘一个Ts/(T1T2)系数让它们的相对比例保持不变。这样电压矢量的方向不变只是幅值被饱和。这是过调制的基本处理虽然不是最完美的过调制策略但已经能覆盖绝大多数FOC应用。再说死区。死区时间确实保护了功率管但也会带来副作用。死区期间相电流会通过续流二极管续流导致实际输出电压和指令电压不一致电流过零附近会出现一段平台期严重时低频电流谐波变大。对于要求高的应用可以做死区补偿但那是另一个大课题。入门阶段先把死区时间调到合适范围避免过大即可。我见过的很多开发板默认死区1us以上对低压小功率MOS来说偏大。还有一个容易忽略的坑扇区边界。当电压矢量正好落在扇区边界附近时T1或T2可能趋近于0此时扇区判断稍有抖动就会在两个扇区之间跳变。但不用担心因为SVPWM在边界两侧是连续的跳变时T1/T2的取值会自然衔接不会产生突变。真正需要留意的是N值判断一次只能映射到一个扇区别在default分支里随便赋值否则边界处会有异常。最后是CCR映射表和示波器的验证方法。我强烈建议先用开环方式调试给定一个固定的Ualpha、Ubeta比如幅值0.3Vdc旋转速度很慢然后用示波器观察三相PWM的占空比变化。你会发现三相占空比是互差120°的正弦变化这就是SVPWM正常的开环表现。如果哪一相占空比的顺序或者变化趋势不对大概率就是映射表或极性配置错了。等开环波形正确了再接电流环和速度环。4.3 先仿真再上机能省一整天最后给还在SVPWM门口徘徊的朋友一个建议先仿真再上板。我说的仿真不是要你用昂贵的仿真器而是用Simulink或者Python先把SVPWM算法验证一遍。Simulink里搭一个简单的逆变器模型输入一个旋转电压矢量看它输出的相电流波形和线电压波形是否正确。Python写个脚本也行把扇区判断、T1/T2计算、七段式分配全部仿真一遍对比目标电压矢量和实际合成矢量的差距。我自己的习惯是先在Python里把公式推一遍看扇区编号和T1/T2选对没有然后再改到STM32工程里。因为上示波器看波形虽然直观但如果问题出在映射表这类逻辑错误上调试效率很低。仿真环境下我可以直接把Ualpha、Ubeta设成任意值瞬间看结果对不对。等你把仿真这关过了再上板子基本一次就能出正常波形。我个人在实际操作中的体会是SVPWM看起来公式多、表多但核心其实就三件事——判断扇区算作用时间把时间映射成PWM比较值。推一遍公共量X/Y/Z写一遍代码再上示波器看一次波形这个知识点就再也不会忘。后面你还会遇到带死区补偿的SVPWM、五段式SVPWM、甚至是没有电流传感器的SVPWM开环启动但底子都是这里的东西。把标准SVPWM吃透了后面这些工程优化才能接得住。
返回列表