
1. 从零搞懂FOC为什么SVPWM和死区补偿是绕不开的坎搞无刷电机FOC驱动的朋友大概率都经历过这个阶段CubeMX里把定时器一配PWM一开电机嗡嗡响两声就停了或者转起来像得了帕金森低速抖得厉害电流波形一塌糊涂。我当初用STM32F103RCT6搭第一套FOC板子的时候在这个坑里蹲了整整两周。后来才明白问题八成出在两个地方SVPWM波形没吃透以及死区补偿没做对。这篇内容就是把我踩过的坑、调过的波形、算过的参数从头到尾捋一遍。核心围绕STM32CubeMX这个工具链讲清楚三件事SVPWM到底怎么生成、七段式波形为什么长那样、死区补偿在代码里怎么落地。适合正在做STM32 FOC项目、被波形和电流畸变折磨的嵌入式工程师也适合刚接触PMSM无感FOC控制、想搞明白底层原理的学生朋友。代码基于HAL库Keil5环境STM32F103RCT6这颗经典芯片你手头如果有其他F1系列或者F4系列逻辑完全通用。先说结论SVPWM不是玄学它本质上是用六个开关管的组合去逼近一个旋转的电压矢量。而死区补偿就是把这个逼近过程中因为开关管“反应慢”造成的误差给补回来。这两件事做不好FOC的电流环就是空中楼阁。2. SVPWM核心原理拆解从电压矢量到七段式波形2.1 电压矢量的空间分布与扇区判断三相逆变器有六个开关管上下桥臂互补导通。把上桥臂导通记为1下桥臂导通记为0那么三相开关状态组合起来有8种000、001、010、011、100、101、110、111。其中000和111是零矢量剩下六个是有效矢量它们在空间上均匀分布间隔60度幅值都是2/3倍的母线电压。这六个矢量把空间切成六个扇区每个扇区60度。SVPWM的核心思想就是在任意时刻用相邻的两个有效矢量和一个零矢量按照一定的时间比例去合成目标电压矢量。目标矢量落在哪个扇区就用哪两个有效矢量。扇区判断有个很实用的技巧不用算角度。设目标电压在α-β坐标系下的分量为Uα和Uβ定义三个中间变量A UβB (√3·Uα - Uβ) / 2C (-√3·Uα - Uβ) / 2然后看A、B、C的符号正为1负为0组合成一个三位数N。N和扇区的对应关系是N3对应扇区1N1对应扇区2N5对应扇区3N4对应扇区4N6对应扇区5N2对应扇区6。这个判断方法在代码里就是几个if-else比算atan2快得多在STM32F103这种没有FPU的芯片上尤其重要。我实测下来用这个符号判断法扇区切换干净利落不会出现扇区边界抖动的问题。如果你用角度判断在扇区交界处容易因为浮点误差来回跳导致PWM占空比突变电机就会听到“咔咔”的异响。2.2 基本矢量作用时间的计算与过调制处理确定了扇区接下来要算两个相邻有效矢量和零矢量各自作用多长时间。以扇区1为例相邻矢量是V1(100)和V2(110)。设PWM周期为Ts目标矢量幅值为|Uref|角度为θ那么T1 (√3 · Ts / Udc) · |Uref| · sin(60° - θ)T2 (√3 · Ts / Udc) · |Uref| · sin(θ)T0 Ts - T1 - T2这里Udc是母线电压。T1对应V1T2对应V2T0是零矢量时间平均分给000和111。关键点来了T1 T2 不能超过Ts。如果超过说明目标矢量幅值超出了内切圆范围进入了过调制区。这时候有两种处理方式一是等比缩放T1和T2让它们之和等于Ts零矢量时间为零二是直接限幅把|Uref|钳位到最大线性调制幅值Udc/√3。我一般用等比缩放因为这样输出电压的相位关系不会乱电流波形更干净。在CubeMX生成的代码框架里这个计算通常放在ADC中断里每次电流采样完成后执行。STM32F103RCT6的主频72MHz一次完整的SVPWM计算加上Clark/Park变换和PID大概占用15-20微秒PWM频率设10kHz的话中断周期100微秒余量很充足。2.3 七段式波形的生成逻辑与对称性设计七段式SVPWM顾名思义一个PWM周期内开关状态切换七次。以扇区1为例顺序是000 → 100 → 110 → 111 → 110 → 100 → 000。对应三相占空比的变化中间是对称的。为什么搞七段式而不是五段式五段式是把零矢量全放在一端开关次数少损耗低但谐波性能差一些。七段式把零矢量平均分到两端波形对称谐波集中在开关频率附近更容易滤除。对于FOC来说电流环的带宽通常远低于开关频率七段式的谐波不会干扰电流控制。具体到定时器比较值的计算以扇区1为例设三相上桥臂导通时间为Ta、Tb、TcTa (Ts - T1 - T2) / 4Tb Ta T1 / 2Tc Tb T2 / 2然后根据扇区号把Ta、Tb、Tc映射到三个通道的比较寄存器CCR1、CCR2、CCR3。这个映射关系每个扇区都不一样写代码的时候用一个switch-case或者查表法别想着用公式统一容易出错。注意七段式的对称性依赖于定时器工作在中央对齐模式。CubeMX里配置TIM1为Center Aligned Mode 1这样计数器从0向上到ARR再向下到0一个周期内比较匹配两次自然形成对称波形。如果你用边沿对齐模式七段式就变成了非对称电流谐波会明显变大。3. STM32CubeMX配置实战定时器、ADC与中断的协同3.1 TIM1高级定时器的PWM配置要点STM32F103RCT6的TIM1是高级定时器专门为电机控制设计带互补输出和死区插入功能。CubeMX里配置步骤如下第一步Clock Source选Internal ClockChannel1/2/3选PWM Generation CH1/CH2/CH3然后下面会出现CH1N/CH2N/CH3N这是互补通道自动使能。第二步Counter Settings里Prescaler设为0Counter Period设成ARR值。假设系统时钟72MHzPWM频率10kHz中央对齐模式下ARR 72M / (2 × 10k) 3600。注意中央对齐模式的实际PWM频率是时钟除以2倍ARR别算错了。第三步PWM Generation Channel的设置里Mode选PWM Mode 1Pulse设成ARR的一半初始占空比50%CH Polarity选HighCHN Polarity选Low。这样上桥臂高电平导通下桥臂低电平导通互补关系正确。第四步Break and Dead-Time Settings里Dead Time设成你需要的值。死区时间计算公式是DT (DTG[7:0]) × TdtgTdtg 1/72MHz ≈ 13.89ns。假设你需要1微秒死区DTG值约为72。CubeMX里直接填72它会自动生成寄存器配置。我一般会把死区时间设在500ns到1微秒之间。太小了上下桥臂直通风险大太大了输出电压畸变严重。具体值要看你的MOS管驱动芯片和MOS管的开关速度数据手册里都有上升下降时间参数。3.2 ADC注入通道与电流采样的时序安排FOC需要实时采样两相电流通常用ADC的注入通道由TIM1的更新事件触发。CubeMX里配置ADC1开启Inject Conversion设置两个注入通道对应两相电流的采样引脚。关键参数是采样时间。STM32F103的ADC最快转换时间是1微秒14MHz ADCCLK采样时间1.5周期。我一般设采样时间为7.5周期或13.5周期给运放和采样保持电路足够的建立时间。总转换时间 (采样时间 12.5) / ADCCLK。如果ADCCLK是14MHz采样时间13.5周期总转换时间约1.86微秒。触发源选TIM1 Trigger Out Event这样每次PWM周期中间中央对齐模式下计数器过零或到ARR时触发ADC采样点正好在PWM波形的中间避开开关噪声。这个时序安排很讲究采样点太靠近开关时刻电流振铃还没稳定采到的值不准太靠后又跟不上电流变化。实操心得我习惯在中央对齐模式下把ADC触发设在计数器等于0的时刻也就是PWM周期的正中间。这时候三相下桥臂都导通零矢量000电流采样电阻上的电压最干净。如果你用的是三电阻采样这个点尤其重要。3.3 中断优先级与执行时间的平衡FOC的电流环中断优先级要设高但不能最高。我一般把ADC注入转换完成中断设为优先级1TIM1更新中断设为优先级2。为什么因为ADC采样完成后要立刻做Clark/Park和SVPWM计算更新PWM占空比这个链条不能被打断。而TIM1更新中断里可以做速度环、位置估算这些对实时性要求稍低的任务。中断服务函数里代码要精简。浮点运算尽量用定点数或者Q格式STM32F103没有FPU一次float乘法要几十个周期。我实测过把Park变换里的sin/cos用查表法代替中断执行时间从25微秒降到12微秒效果立竿见影。CubeMX生成的代码框架里中断服务函数在stm32f1xx_it.c里但具体的FOC算法要自己写到回调函数里。我习惯在main.c里定义一个FOC_CurrentLoop()函数然后在ADC中断回调里调用它。这样代码结构清晰移植也方便。4. 死区补偿的代码实现与波形验证4.1 死区效应的成因与电流畸变特征死区是为了防止上下桥臂直通在开关切换时插入的一小段全关时间。但这段死区会导致输出电压偏离理想值当相电流为正时死区期间电流通过下桥臂续流二极管输出电压被钳位到负母线当相电流为负时通过上桥臂续流二极管输出电压被钳位到正母线。结果就是实际输出电压比理想值少了一个和电流方向相关的误差电压。这个误差电压的幅值约等于死区时间除以PWM周期再乘以母线电压。假设死区1微秒PWM周期100微秒母线24V误差电压就是0.24V。听起来不大但在低速小电流时这个误差会导致电流波形出现明显的平顶畸变电机转矩脉动低速性能急剧恶化。我当初用示波器看相电流波形低速时波形顶部和底部都是平的像被削了一刀。后来加了死区补偿波形立刻圆润了电机声音也小了很多。4.2 基于电流极性的补偿量计算死区补偿的基本思路很简单根据相电流的极性在计算出的占空比上叠加一个补偿量。如果电流为正说明实际输出电压偏低需要把占空比加大一点电流为负则减小一点。补偿量的计算公式Tcomp Tdead × sign(Iphase) / Ts。其中Tdead是死区时间Ts是PWM周期sign是符号函数。实际实现时为了避免电流过零时的抖动通常加一个死区补偿的死区当|Iphase|小于某个阈值时补偿量线性过渡到零而不是硬切换。代码实现上我在SVPWM计算出Ta、Tb、Tc之后根据三相电流的极性分别加上补偿量float Tcomp_a 0, Tcomp_b 0, Tcomp_c 0; float I_threshold 0.1f; // 电流阈值单位A if (I_a I_threshold) Tcomp_a Tdead; else if (I_a -I_threshold) Tcomp_a -Tdead; else Tcomp_a Tdead * I_a / I_threshold; // 同理计算Tcomp_b和Tcomp_c Ta Tcomp_a; Tb Tcomp_b; Tc Tcomp_c;然后要把Ta、Tb、Tc限幅在0到Ts之间防止补偿后超出PWM范围。注意补偿量的符号要和你的电流采样方向一致。如果你采样的是下桥臂电流电流为正表示电流从电机流向地补偿方向要反过来。这个坑我踩过补偿加反了波形反而更差。4.3 补偿效果验证与示波器波形对比验证死区补偿效果最直接的方法就是用示波器看相电流波形。补偿前低速轻载时电流波形顶部和底部有明显的平顶总谐波失真THD大概在8%-10%。补偿后平顶消失波形接近正弦THD降到3%以下。另一个验证方法是看电机的相电压波形。用示波器差分探头测相电压补偿前在电流过零附近有电压跳变补偿后跳变明显减小。如果你没有差分探头可以用两个单端探头分别测相电压和母线电压然后数学相减但要注意探头共地问题。我一般还会用电流钳看三相电流的对称性。补偿前三相电流的幅值可能不一致因为死区误差和电流方向有关不同相的电流方向不同误差也不同。补偿后三相电流幅值基本一致电机运行更平稳。实测数据24V母线10kHz PWM1微秒死区补偿前低速100rpm电流THD约9.2%补偿后约2.8%。电机噪声从明显的“滋滋”声变成几乎听不到。这个改善在低速场合非常明显高速时因为反电动势大死区误差占比小改善没那么显著。5. 常见问题与排查技巧实录5.1 SVPWM波形异常排查速查表现象可能原因排查方法解决方法电机不转嗡嗡响扇区判断错误用调试器看扇区变量手动旋转目标矢量角度观察扇区是否按顺序变化检查A、B、C符号判断逻辑确认N值与扇区对应关系电流波形有尖刺ADC采样时刻不对示波器同时看PWM和ADC触发信号确认采样点在零矢量中间调整ADC触发源为TIM1更新事件或修改触发时刻低速抖动严重死区补偿未加或加反看相电流波形是否有平顶检查补偿量符号根据电流采样方向调整补偿符号加电流阈值过渡高速时电流失控过调制处理不当看T1T2是否超过Ts检查限幅逻辑等比缩放T1和T2或钳位目标矢量幅值PWM输出不对称定时器模式错误示波器看三相PWM波形确认是否中央对齐CubeMX里把TIM1设为Center Aligned Mode 15.2 电流采样噪声与滤波技巧ADC采到的电流值经常带噪声尤其是电机线比较长的时候。我一般会在硬件上加RC滤波截止频率设在开关频率的1/10左右。软件上在ADC中断里做一阶低通滤波I_filtered I_filtered * 0.8f I_raw * 0.2f;这个系数0.2对应截止频率约等于采样频率的1/30。如果PWM频率10kHz电流环中断也是10kHz截止频率约333Hz对电流环带宽通常1kHz以下影响不大但能有效滤掉开关噪声。实操心得滤波系数别设太小否则电流环相位滞后严重动态响应变慢。我试过0.05的系数电机加速时电流跟不上转矩明显不足。0.2到0.3是比较平衡的值。5.3 调试工具与观测手段推荐调试FOC光靠串口打印是不够的。我强烈建议用示波器看波形至少四通道三相PWM、相电流、ADC触发信号。如果有条件用STM32的DAC输出内部变量比如目标电流、实际电流、扇区号直接打在示波器上看比串口直观一百倍。STM32F103RCT6有两个DAC通道正好可以输出两个关键变量。CubeMX里配置DAC在中断里把变量值写到DAC寄存器示波器上就能看到实时波形。我一般用DAC1输出Iq实际值DAC2输出Iq目标值两个波形叠在一起电流环的跟踪性能一目了然。另外ST-Link Utility或者STM32CubeProgrammer可以实时读取内存变量配合IDE的watch窗口能看一些慢速变化的量比如速度、位置。但高速波形还是得靠DAC和示波器。6. 代码框架与关键函数实现6.1 FOC主循环与中断服务函数结构整个代码框架分三层底层是CubeMX生成的初始化代码中间是FOC算法层上层是应用层。中断服务函数里只放最紧急的电流环速度环和位置估算放在主循环或者定时器中断里。电流环中断服务函数的结构void FOC_CurrentLoop(void) { // 1. 读取ADC注入通道的电流值 I_a ADC_GetInjectedValue(ADC_CHANNEL_1); I_b ADC_GetInjectedValue(ADC_CHANNEL_2); I_c -I_a - I_b; // 三相电流和为零 // 2. Clark变换 I_alpha I_a; I_beta (I_a 2 * I_b) * ONE_BY_SQRT3; // 3. Park变换 I_d I_alpha * cos_theta I_beta * sin_theta; I_q -I_alpha * sin_theta I_beta * cos_theta; // 4. 电流PID V_d PID_Calc(pid_d, I_d_ref - I_d); V_q PID_Calc(pid_q, I_q_ref - I_q); // 5. 反Park变换 V_alpha V_d * cos_theta - V_q * sin_theta; V_beta V_d * sin_theta V_q * cos_theta; // 6. SVPWM计算 SVPWM_Calc(V_alpha, V_beta, Ta, Tb, Tc); // 7. 死区补偿 DeadTime_Compensate(Ta, Tb, Tc, I_a, I_b, I_c); // 8. 更新PWM比较寄存器 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, Ta); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, Tb); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, Tc); }这个函数在ADC注入转换完成回调里调用执行时间要控制在PWM周期的50%以内留足余量。6.2 SVPWM计算函数的完整实现void SVPWM_Calc(float Ualpha, float Ubeta, float *Ta, float *Tb, float *Tc) { float U1, U2, U3; int sector; float T1, T2, T0; float Ts PWM_PERIOD; // 扇区判断 U1 Ubeta; U2 (SQRT3 * Ualpha - Ubeta) * 0.5f; U3 (-SQRT3 * Ualpha - Ubeta) * 0.5f; sector 0; if (U1 0) sector | 0x01; if (U2 0) sector | 0x02; if (U3 0) sector | 0x04; // 根据扇区计算基本矢量作用时间 switch (sector) { case 3: // 扇区1 T1 (SQRT3 * Ts / Udc) * Ubeta; T2 (SQRT3 * Ts / Udc) * (SQRT3 * Ualpha - Ubeta) * 0.5f; break; case 1: // 扇区2 T1 (SQRT3 * Ts / Udc) * (SQRT3 * Ualpha Ubeta) * 0.5f; T2 -(SQRT3 * Ts / Udc) * Ubeta; break; // ... 其他扇区类似 default: T1 0; T2 0; break; } // 过调制处理 if (T1 T2 Ts) { float scale Ts / (T1 T2); T1 * scale; T2 * scale; } T0 Ts - T1 - T2; // 计算三相占空比 float Ta_temp T0 * 0.25f; float Tb_temp Ta_temp T1 * 0.5f; float Tc_temp Tb_temp T2 * 0.5f; // 根据扇区映射到三相 switch (sector) { case 3: *Ta Ta_temp; *Tb Tb_temp; *Tc Tc_temp; break; case 1: *Ta Tb_temp; *Tb Ta_temp; *Tc Tc_temp; break; // ... 其他扇区 } }这个函数里SQRT3是根号3的宏定义Udc是母线电压。注意T1和T2的计算公式里不同扇区的符号和系数不一样写的时候要仔细对照扇区图别抄错了。6.3 死区补偿函数的实现细节void DeadTime_Compensate(float *Ta, float *Tb, float *Tc, float Ia, float Ib, float Ic) { float Tcomp_a, Tcomp_b, Tcomp_c; float I_th 0.1f; // 电流阈值 float Tdead DEAD_TIME; // 死区时间对应的计数值 // A相补偿 if (Ia I_th) Tcomp_a Tdead; else if (Ia -I_th) Tcomp_a -Tdead; else Tcomp_a Tdead * Ia / I_th; // B相补偿 if (Ib I_th) Tcomp_b Tdead; else if (Ib -I_th) Tcomp_b -Tdead; else Tcomp_b Tdead * Ib / I_th; // C相补偿 if (Ic I_th) Tcomp_c Tdead; else if (Ic -I_th) Tcomp_c -Tdead; else Tcomp_c Tdead * Ic / I_th; // 叠加补偿量 *Ta Tcomp_a; *Tb Tcomp_b; *Tc Tcomp_c; // 限幅 if (*Ta 0) *Ta 0; if (*Ta PWM_PERIOD) *Ta PWM_PERIOD; // 同理限幅Tb和Tc }这个函数里Tdead的单位要和Ta、Tb、Tc一致都是定时器计数值。如果你用微秒做单位最后要转换成计数值。我一般统一用计数值避免单位混乱。注意补偿量的正负号取决于你的电流采样方向。如果你采样的是下桥臂电流电流为正表示电流从电机流向采样电阻补偿方向要反过来。这个坑我踩过补偿加反了波形反而更差。验证方法很简单加补偿后看电流波形如果平顶更严重了说明符号反了。7. 从波形到性能实测数据与优化方向7.1 补偿前后电流THD对比与电机噪声变化我用一台24V、100W的无刷电机做测试PWM频率10kHz死区1微秒电流环带宽500Hz。补偿前低速100rpm时相电流THD约9.2%电机有明显的电磁噪声用手摸电机外壳能感觉到振动。补偿后THD降到2.8%噪声几乎消失振动明显减小。高速3000rpm时补偿前THD约4.5%补偿后约2.1%。改善幅度没有低速那么夸张因为高速时反电动势大死区误差占比小。但高速时电流波形更干净电机效率略有提升实测温升降低了3-5摄氏度。这个数据是用示波器的FFT功能测的采样电阻10毫欧运放增益20倍。如果你没有示波器FFT可以用电流钳配合功率分析仪或者用STM32的ADC采样后做FFT但精度差一些。7.2 低速性能优化的几个关键参数低速时FOC性能差除了死区补偿还有几个参数要调第一电流环PID参数。低速时反电动势小电流环增益可以设高一些但太高会振荡。我一般先用Ziegler-Nichols方法整定然后根据实际波形微调。比例增益从0.5开始试积分时间从10ms开始试。第二PWM频率。低速时开关损耗不是问题可以把PWM频率提高到20kHz超出人耳听觉范围电机更安静。但STM32F103在20kHz时中断执行时间要控制在25微秒以内代码要优化。第三电流采样精度。低速时电流小采样噪声占比大。可以换用更高精度的采样电阻和运放或者在软件里做滑动平均滤波。我试过16次滑动平均电流波形明显更平滑但相位滞后约1.6ms对低速影响不大。7.3 从F103到F4的移植注意事项STM32F103RCT6没有FPU浮点运算慢。如果你移植到F4系列有FPU和DSP指令SVPWM计算可以全部用浮点代码更简洁。但要注意几点第一F4的定时器时钟频率更高ARR值要重新计算。比如F407主频168MHzPWM频率10kHz中央对齐模式下ARR 168M / (2 × 10k) 8400。第二F4的ADC精度更高但注入通道的配置方式略有不同CubeMX里要重新生成代码。第三F4的DSP库里有现成的Park变换和SVPWM函数可以直接调用但要注意数据格式是Q格式还是浮点。我移植过一次从F103到F407SVPWM计算时间从18微秒降到3微秒中断余量大大增加可以把PWM频率提到40kHz电机运行更安静。8. 写在最后一些个人体会这套FOC方案我前后调了三个月从最开始电机只会抖到后来低速平稳、高速有力中间踩的坑基本都写在这篇内容里了。SVPWM和死区补偿是FOC的基础这两块搞定了后面的无感观测器、弱磁控制才有意义。如果你刚开始做建议先用开环SVPWM让电机转起来用示波器看三相PWM波形确认七段式对称性正确。然后加上电流采样和电流环看电流波形是否正弦。最后加死区补偿对比补偿前后的THD。这个顺序别乱每一步都验证清楚了再往下走。代码方面CubeMX生成的框架很好用但别完全依赖它。中断服务函数里的代码要自己写要精简要实测执行时间。我习惯在中断入口拉高一个GPIO出口拉低用示波器看中断执行时间这个方法很土但很有效。最后分享一个小技巧调试FOC的时候把目标电流设成直流比如Iq_ref 0.5AId_ref 0然后用手转动电机看电流环能不能稳住。如果能稳住说明Clark/Park变换和SVPWM都对了。如果稳不住先查扇区判断和ADC采样时序。这个测试方法比直接闭环跑电机更容易定位问题。