ARTICLE DETAIL

资讯详情

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

SVPWM从原理到代码:基于STM32的FOC电机控制实现与调试

SVPWM从原理到代码:基于STM32的FOC电机控制实现与调试 电机控制这个圈子聊到FOC就绕不开SVPWM。我自己入坑时也走了弯路一开始直接抄了一套FOC代码SVPWM当成黑盒用结果开环测试时三相电流畸变、电机嗡嗡叫示波器一看调制波根本不是预期的马鞍波。后来花了一个周末从扇区判断到X/Y/Z公共量推导重新捋了一遍才发现问题出在扇区编号和映射表对应不上。这篇文章就是把我踩过的坑和标准的SVPWM实现从头到尾整理一遍覆盖数学原理、STM32F103代码、实测波形判据和工程化注意事项适合刚接触FOC、想把SVPWM真正搞懂的工程师和研究生。1. SVPWM在FOC链路中的位置为什么最后一级必须用它1.1 FOC链路回顾SVPWM的前一级和后一级FOC的完整链路是三相电流采样→Clarke变换得到iα/iβ→Park变换得到id/iq→电流环PID输出Ud/Uq→反Park变换得到Uα/Uβ→SVPWM→三相逆变桥。很多人学到这里会忽略一个关键点反Park变换输出的是两相静止坐标系下的电压指令它是一个连续的电压矢量而不是三相分别的占空比。SVPWM就是负责把这个连续电压矢量翻译成六个开关管的开关时序。SVPWM的后一级是三相全桥逆变器。六个开关管只有8种组合状态每种组合对应一个固定的电压矢量。SVPWM要做的事情就是用这8个固定的矢量去“拼”出任意的期望电压矢量。这跟在二维平面里用两个基向量合成任意向量是一个道理只不过这里的基向量有6个非零向量和2个零向量6个非零向量把平面分成6个扇区。1.2 电压矢量空间6个扇区是怎么来的设上桥臂导通为1、下桥臂导通为0三相状态组合为001~110的6个非零矢量和000、111两个零矢量。每个非零矢量在αβ平面上指向一个固定方向模长为2Udc/3。相邻两个矢量夹角恰好60°所以6个非零矢量均匀分布在360°平面上自然把平面切成6个扇区每个扇区60°。期望电压矢量落在哪个扇区就用这个扇区的两个边界矢量和零矢量按时间比例去合成。这里必须强调一个容易忽略的点SVPWM合成的是“平均电压矢量”而不是瞬时电压。因为一个PWM周期内开关管在不断切换产生的是多个电压脉冲的序列但它们的伏秒平均值恰好等于期望矢量。之所以能这样做是因为电机绕组的电气时间常数远大于PWM周期电流基本是连续的只会响应平均值。1.3 和SPWM的对比多出来的15%母线利用率从哪来SPWM是三相分别用正弦波和三角载波比较每个PWM周期内三相桥臂独立调制而SVPWM是把三相看成一个整体直接控制电压矢量。二者都能让电机转起来但SVPWM的母线电压利用率更高。具体说线性调制区内SPWM的最大相电压基波幅值是Udc/2而SVPWM可以达到Udc/√3比值是1.1547正好多出约15.47%。对于同一个直流母线多了这15%意味着电机最高转速可以更高、力矩余量更大对电池供电的设备尤其明显。我曾经在一台24V驱动的BLDC上对比过同样一套电流环只把SVPWM换成SPWM最高转速掉了差不多一成五可见这个差距不是理论书上的数字游戏而是实际体验。另外SVPWM的数字化实现非常简单Uα/Uβ一进来就可以直接算比较值不需要查找正弦表对MCU反而是更友好的方案。2. SVPWM数学内核扇区判定与X/Y/Z公共量的完整推导2.1 扇区判定的高效实现三条直线分六区判断电压矢量落在哪个扇区最直接的方法是算出相角再用atan2。但atan2在MCU上开销很大而且相角从180°到-180°跳变时容易在边界产生抖动。实际工程中很少这么干。标准做法是构造三个判定量Vref1 UβVref2 (√3/2)Uα - (1/2)UβVref3 -(√3/2)Uα - (1/2)Uβ这三个量的正负对应三条过原点的直线Uβ0α轴、√3Uα-Uβ0与α轴成60°、-√3Uα-Uβ0与α轴成-60°。三条直线把αβ平面分成六个60°扇区。用三个符号位拼成一个数字比如sector (Vref10) 2*(Vref20) 4*(Vref30)得到1~7之间的数再查表映射到实际扇区1~6。这里有两个边界处理的细节第一三个判定量的正负用判断而不是否则0°、60°这些边界会落到错误的扇区导致波形出现毛刺第二三个符号位拼出的值需要查表映射到实际扇区。有人觉得这个查表看不懂没关系表不是背出来的。你写程序的时候先开环给一组确定的Uα、Uβ用示波器看三相桥输出状态就能验证映射是否对。我在4.4节会写一次具体排查过程。2.2 中间变量X/Y/Z推导从扇区I的两个方程说起扇区判断只是第一步真正决定SVPWM性能的是两个非零矢量的作用时间T1、T2怎么算。网上很多代码直接给X/Y/Z公式但没解释为什么导致出了问题不知道从哪里查。这里我把推导过程写出来顺便回答很多初学者问的“公共量推导过程”。先说归一化基准。把Uα、Uβ归一化使线性调制区最大相电压幅值对应1最大值为Udc/√3。此时基本电压矢量幅值为(2/3)Udc / (Udc/√3) 2/√3 ≈ 1.1547。归一化后所有计算都是标幺值最后乘载波周期就得到实际时间。以扇区I为例。扇区I夹在矢量100和110之间。设T1是100的作用时间T2是110的作用时间Ts是PWM周期。由伏秒平衡可得两个方程Uα_n (2/√3)(T1/Ts) (2/√3)(1/2)*(T2/Ts)Uβ_n (2/√3)(√3/2)(T2/Ts) T2/Ts第二个方程直接给出T2/Ts Uβ_n。代回第一个方程得到T1/Ts (√3/2)Uα_n - (1/2)Uβ_n。现在定义三个公共量X Uβ_nY (√3/2)Uα_n (1/2)Uβ_nZ -(√3/2)Uα_n (1/2)Uβ_n于是扇区I中T1/Ts -ZT2/Ts X。对剩余5个扇区做同样推导会得到一个很有规律的表格。因为扇区之间是旋转对称的每个扇区的T1、T2都只是X、Y、Z的某种正负组合。这就是为什么叫公共量算好X、Y、Z之后六个扇区的时间计算全部变成简单的取正取负效率极高。2.3 各扇区T1/T2取值表与7段式时序由于不同资料对扇区编号约定不同我这里列出的是和后面代码一致的约定扇区1~6按逆时针排列扇区T1T2扇区1-ZX扇区2ZY扇区3X-Y扇区4-XZ扇区5-Y-Z扇区6Y-X注意以上T1、T2都是相对Ts的标幺值范围在0到1左右。如果T1T2超过1说明期望电压矢量已经跑到线性调制区外面进入过调制区。此时最简单可靠的做法是按比例缩放T1 T1/(T1T2)T2 T2/(T1T2)。这样保持矢量方向不变只是幅值被限制到最大。得到T1、T2之后还需要决定零矢量怎么放。最常用的是7段式发波一个Ts的序列在扇区I是000→100→110→111→110→100→000首尾放000中间放111。三个切换点的标幺时间为Tcm1 (1 - T1 - T2)/4Tcm2 Tcm1 T1/2Tcm3 Tcm2 T2/27段式的优势是每个桥臂在一个Ts内只开关两次且波形关于中心点对称谐波集中在载波频率附近方便后续滤波和处理。5段式的切换次数更少但谐波更大工程上要根据系统取舍。到这里SVPWM的数学骨架就完整了扇区判断用三个符号位时间计算用X/Y/Z公共量发波时序用7段式。3. STM32F103标准SVPWM代码实现与数值细节3.1 定时器初始化中心对齐、互补PWM与死区STM32F103上实现SVPWM推荐直接用高级定时器TIM1或TIM8因为它们能输出三相互补PWM且自带死区插入和刹车功能不用外部逻辑电路。我用的配置是TIM1时钟72MHz中心对齐模式1载波频率16kHz。中心对齐模式下PWM频率等于时钟频率除以2倍的(ARR1)所以ARR 72MHz/(2*16kHz) - 1 2249。这个ARR后面会作为SVPWM输出CCR的分母很关键。死区时间根据你选的功率管驱动芯片和开关管决定。我这边用的驱动芯片死区时间配置为约1us对应的TIM_DeadTime值需要查参考手册换算。初学者最容易漏的一步是TIM_CtrlPWMOutputs(TIM1, ENABLE)高级定时器主输出默认是关闭的不使能的话桥臂完全没有输出。三个通道配置为PWM1模式、互补输出使能CH1/CH1N、CH2/CH2N、CH3/CH3N。极性根据你驱动芯片是高有效还是低有效来设后面调试时如果发现占空比反了先把极性反一下再用。3.2 核心SVPWM函数逐段说明下面是完整的SVPWM计算函数输入Ualpha、Ubeta已经归一化到±1.1547以内输出是三个通道的CCR值。#define TIM_ARR 2249 void SVPWM_Calc(float Ualpha, float Ubeta, uint16_t *ccr_a, uint16_t *ccr_b, uint16_t *ccr_c) { float Vref1, Vref2, Vref3; float X, Y, Z; float T1, T2, sum, inv; float Tcm1, Tcm2, Tcm3; uint8_t sector 0; // 1. 扇区判定三个符号位 - base值 Vref1 Ubeta; Vref2 0.866025f * Ualpha - 0.5f * Ubeta; Vref3 -0.866025f * Ualpha - 0.5f * Ubeta; if (Vref1 0.0f) sector 1; if (Vref2 0.0f) sector 2; if (Vref3 0.0f) sector 4; static const uint8_t sectTab[8] {0, 2, 6, 1, 4, 3, 5, 0}; sector sectTab[sector]; // 2. 公共量 X/Y/Z X Ubeta; Y 0.866025f * Ualpha 0.5f * Ubeta; Z -0.866025f * Ualpha 0.5f * Ubeta; // 3. 根据扇区选择 T1, T2标幺值相对Ts switch (sector) { case 1: T1 -Z; T2 X; break; case 2: T1 Z; T2 Y; break; case 3: T1 X; T2 -Y; break; case 4: T1 -X; T2 Z; break; case 5: T1 -Y; T2 -Z; break; case 6: T1 Y; T2 -X; break; default: T1 0; T2 0; break; } // 4. 过调制限幅保持矢量方向 sum T1 T2; if (sum 1.0f) { inv 1.0f / sum; T1 * inv; T2 * inv; } // 5. 7段式切换点标幺时间 Tcm1 (1.0f - T1 - T2) * 0.25f; Tcm2 Tcm1 T1 * 0.5f; Tcm3 Tcm2 T2 * 0.5f; // 6. 扇区到三相CCR映射 // 注意以下换算针对中心对齐模式1、PWM模式1、输出极性高有效 // 如果极性相反改成 (uint16_t)(TcmN * TIM_ARR) switch (sector) { case 1: *ccr_a (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); break; case 2: *ccr_a (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); break; case 3: *ccr_a (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); break; case 4: *ccr_a (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); break; case 5: *ccr_a (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); break; case 6: *ccr_a (uint16_t)(TIM_ARR - Tcm1 * TIM_ARR); *ccr_b (uint16_t)(TIM_ARR - Tcm3 * TIM_ARR); *ccr_c (uint16_t)(TIM_ARR - Tcm2 * TIM_ARR); break; } }逐段解释几个关键点。扇区判定里的sectTab表是把符号位组合映射到实际扇区1~6的查表。很多初学者直接抄这段代码却不明白为什么数组顺序是那样。我给你一个验证方法开环固定Uα和Uβ比如Uα0.5、Uβ0这个矢量应该落在扇区10°位置。如果示波器看输出状态不是预期的100就说明sectTab和switch里的映射配合有问题需要调整表或switch。第6步的CCR换算为什么用TIM_ARR减去Tcm因为中心对齐模式1配合PWM模式1时计数器向上计数过程中CNT小于CCR输出有效向下计数过程中CNT大于CCR输出有效这意味着CCR越大该相有效占空比越大。Tcm代表的是从周期起点到首个开关切换点的位置直接把它作为CCR会得到相反的导通关系。所以用TIM_ARR - TcmTIM_ARR做一次取反。如果你的PWM极性配置成低有效那直接改成TcmTIM_ARR就行逻辑恰好反过来。这里最容易搞混建议写完之后用开环小电压实测确认。3.3 没有FPU的STM32F103运算开销与定点化思路这段代码里用了8个浮点乘法和几个分支在72MHz的Cortex-M3上编译器开优化后大概几百个周期放在16kHz中断里完全没问题。但如果你用的是STM32F103C8T6这种小容量片子且中断里还跑3个PID、Clarke/Park变换、编码器读取就要注意整体时序。建议打开编译器的数学优化或者把SVPWM计算放到电流环中断末尾用示波器测一下中断执行时间留出余量。如果想进一步压开销可以把Ualpha和Ubeta用Q15格式表示范围-32768~32767对应归一化电压。这样X/Y/Z也变成整数扇区判断只需要比较符号限幅判断改成sum 32768。由于STM32的CCR寄存器本身是16位最终映射甚至可以直接用Q15的值省去浮点到整形的转换。代价是少了动态范围和标幺化精度但对16kHz的电流环完全够用。我实际项目里最初用浮点后来为了在F103上多塞一个速度环观测器把SVPWM部分改成了定点电流环仍然稳定。这说明标准SVPWM的计算量在F103上不是瓶颈真正吃时间的是无感FOC里的状态观测器或滤波器如果你只是做有感FOC直接用浮点就行不用纠结。4. 用示波器验证SVPWM波形判据与常见问题排查4.1 开环验证方法让SVPWM“空转”起来拿到新板子我强烈建议先不开电流环也不接电机用一个开环程序让SVPWM模块单独工作。具体做法设一个固定电压幅值amp比如0.4一个固定电角速度speed每个PWM周期执行theta speedTs然后令Ualpha ampcos(theta)Ubeta amp*sin(theta)调用SVPWM_Calc。示波器探头接法要谨慎。测相电压时最好以直流母线电容中点或虚拟中性点作为参考点否则看到的是对直流负端的脉冲波叠加了母线电压很难判断。如果你手头没有差分探头可以用两个无源探头分别测U相和V相再用示波器的A-B数学通道算出线电压也能用。开环验证时先别给太大电压amp从0.2开始频率设50Hz或100Hz。此时即使有接线错误也不会烧管子。通过调大amp应该能看到调制波幅值随之变大频率变高三相之间保持120°相位差。4.2 波形判据马鞍波 vs 正弦波判断SVPWM实现是否正确最直观的是看相电压对直流母线中点的低频波形。SVPWM注入的零序分量让相电压呈现马鞍形而不是干净的正弦。这其实是SVPWM的特征很多初学者第一次看到马鞍波会以为波形不对实际上这就对了。线电压就不同了。因为三次谐波在相间抵消线电压应该是干净的正弦。如果你测得线电压里也带着马鞍特征说明三相不对称或者某一路比较值分配有问题。如果相电压完全是正弦那基本可以断定你当前用的不是SVPWM而是退化的SPWM。这种情况常见于扇区映射全错导致每相独立按正弦调制或者你直接用了三相互补PWM加正弦查表而没走SVPWM计算。我调试时遇到过这个问题当时换了一个工程模板发现相电压是标准正弦查了半天发现是模板默认用了SVPWM但扇区映射表抄错了退成了类SPWM输出。还有一种常见现象相电压在马鞍波基础上叠加了明显尖峰或台阶。尖峰通常是死区或开关噪声台阶则多半是扇区切换瞬间计算不连续比如边界处Vref恰好为0导致扇区抖动。解决方案是在扇区判定时统一用0或者对Vref加一点点滞回。4.3 常见问题排查表我整理了一下实测中最高频的几个问题按现象、可能原因、检查顺序列成表遇到问题可以直接照着查现象可能原因检查顺序相电压是正弦不是马鞍波扇区映射表错、实际走了SPWM开环给固定矢量验证各扇区输出三相波形的三相对称性差扇区映射表部分错误、某相CCR通道配置反交换通道或检查定时器CCR映射相电压有台阶或毛刺扇区判断边界不连续、死区设置不当检查Vref比较符号DTG值电流波形畸变、噪声大过调制、死区过大、采样点落在开关区看幅值是否超过1、减小amp、对齐ADC触发电机转向反Uα/Uβ相序反、UVW接线相序不对仅用Iq正方向判断或交换任意两相线输出完全无PWM主输出未使能、定时器触发配置错误检查TIM_CtrlPWMOutputs、RCC时钟这个表不一定覆盖所有情况但排查SVPWM相关问题时按这个顺序走十有八九能找到根因。尤其要提醒的是很多人一上来就怀疑是硬件问题先从软件排查大部分情况是扇区映射或者极性配置。4.4 一次扇区映射表错误的完整排查经历举个例子。我之前在F103上做了一个低压PMSM的FOC电流环16kHz跑起来后电流波形一直不对。U相和W相看着还行V相畸变明显像U相和W相之间的波形被“拆开”重组了。一开始我怀疑编码器角度不对但开环跑VF时电机转得很平稳角度应该没问题。于是我把电流环关掉开环给一个固定电压矢量比如Uα0.4、Ubeta0用示波器看三相桥的导通组合发现应该输出100状态U上桥通、V和W下桥通实际上却是110。再给Uα-0.4、Ubeta0应该在180°方向结果也偏了一个扇区。这就很明确了不是公式问题是扇区编号和switch分配表对不上。我用的sectTab把某几个扇区映射反了导致T1、T2的顺序错乱切换点分配给三相时也错位。后来把开环固定矢量的测试做全分别给0°、60°、120°……每60°测一次导通状态确认每个扇区对应的三相输出组合再反推修正映射表。修完之后相电流立刻变正弦V相畸变消失。这个教训让我养成一个习惯任何一套新SVPWM代码先开环全扇区过一遍每个扇区验证一次基本矢量输出再进电流环。花10分钟能省好几个小时的定位时间。5. 从标准实现到工程落地调制比、死区、采样时序5.1 标幺化与调制比Udc/√3从哪里来前文多次提到归一化到Udc/√3这个值很多初学者会疑惑为什么是它原因是在三相桥输出中线电压基波最大幅值等于Udc而线电压幅值与相电压幅值之间差√3倍所以相电压基波最大幅值就是Udc/√3。SVPWM在线性调制区的边界就是在这个值上。因此在你把Ud、Uq反Park得到Uα、Uβ之后如果要交给上面的SVPWM函数别忘了做归一化Ualpha_svpwm Ualpha * 1.732 / Udc具体根号3的系数取决于你的基准定义。如果少做这一步你会发现小电压时SVPWM输出还好大电压时电压突然不再上升而且波形失真严重因为已经撞上过调制限幅了。5.2 死区与最小脉宽对波形的影响死区是为了防止上下桥臂直通但它是真实插入的等待时间会让实际输出电压低于指令。低转速、轻负载时死区造成的电压误差占比很大电流波形会出现明显的过零畸变严重时转矩脉动很烦人。解决方向有两个一是硬件上选短死区的驱动芯片二是软件上做死区补偿根据相电流方向在CCR上补偿死区时间对应的计数值。最小脉宽是另一个容易被忽视的点。如果SVPWM算出某一相的导通时间只有0.1us而驱动芯片和开关管的开通延迟都接近这个量级实际开关管根本没法完全导通输出电压既不线性也不重复。工程上会在输出前对所有CCR做最小脉宽钳位若计算脉宽小于阈值就把这一拍强制拉成0或者阈值具体策略要看应用对死区时间和电流纹波的容限。5.3 电流采样窗口与SVPWM时序配合FOC的电流采样点和SVPWM息息相关。7段式发波的中心点是零矢量111的中点此时三相桥臂都处于上桥导通状态没有开关动作噪声最小是采样的黄金窗口。因此主流做法是用定时器更新事件或中心对齐模式的周期匹配事件触发ADC注入组采样让采样时刻精确落在零矢量中心附近。如果采样点没有和SVPWM对齐比如采到了开关切换瞬间ADC采到的电流会叠加尖峰电流环PID输出跟着抖动严重的会让系统不稳定。调试中可以在电流环输出的Ud/Uq上看到高频毛刺基本就是采样时序没对齐。5.4 再往前走五段式、同步调制与无感FOC标准SVPWM的7段式发波不是唯一选择。在开关频率受损耗限制的场合5段式用一个零矢量代替两个零矢量每个载波周期少两次开关动作能降低开关损耗代价是电流纹波变大。两者切换只需改切换点公式扇区判断和T1/T2计算完全一样。电机转速升高后载波比下降每个电角度周期内SVPWM的采样帧变少此时要用同步调制或过调制策略避免某一扇区连一帧都分不到。我自己在高速PMSM上遇到过载波比降到9以下时电流畸变加剧后来按电角度触发SVPWM刷新问题才解决。无感FOC里需要注入特定电压矢量来辨识转子位置或者通过PWM占空比估算反电动势这些东西都是建立在SVPWM之上的。你把标准SVPWM的扇区、时间、切换点彻底搞明白之后理解这些高级话题会顺畅很多。最后再分享一个个人习惯。拿到任何一套SVPWM代码我都不会直接接电机调试而是先开环跑一个固定电压矢量用示波器把6个扇区的导通状态逐个确认一遍。这个动作看起来费时间实际只要10分钟却能提前暴露扇区映射、通道极性、死区配置这些最隐蔽的问题。SVPWM的公式可以不用背但“每个开关状态对应哪个电压矢量、每个扇区用哪两个矢量、切换点怎么分配”这三个概念一定要形成肌肉记忆。
返回列表