1. 整体设计与思路拆解1.1 FOC控制到底在控制什么先把这个基础问题说透后面所有代码和调参才有着落。无刷电机BLDC和永磁同步电机PMSM的定子绕组是三相对称分布的转子是永磁体。想让电机平稳运转本质就是让定子产生的旋转磁场一直追着转子永磁体磁场跑两者的夹角要控制在合适的位置磁场方向错位越厉害力矩波动越大。FOC的全称是Field-Oriented Control磁场定向控制。它做的事说起来很朴素不管电机转速多快、负载怎么变都实时算出转子磁场的方向然后把定子电流拆成两个独立的量——一个是直轴电流Id对应励磁分量一个是交轴电流Iq对应转矩分量让这两个量像直流电机那样分别控制互不干扰。你把FOC理解成给交流电机装上了一个虚拟的换向器就行只不过这个换向器是在代码里算出来的用电流环和速度环一层层闭环去逼近理想状态。标题里提到的手把手我的理解是不只是跑通一个官方例程而是把工程里每个模块为什么这么写、参数怎么定、上电之后从哪一步开始查都讲明白。我踩过的坑比想象中多得多尤其是第一版硬件直接炸了两路MOS管原因后面会细说。1.2 为什么选STM32做FOC主控STM32在FOC这件事上几乎是默认选项。为什么第一FOC算法虽然看起来公式多但真正落到定点运算或者带FPU的浮点运算上Cortex-M3/M4完全吃得消不需要上DSP。第二STM32的定时器功能对电机控制是深度定制过的——互补PWM输出、可编程死区插入、刹车输入、ADC触发同步这几个功能单独拆开看都不稀奇但它们能协同工作才是关键。就STM32F103RCT6这颗芯片来说72MHz主频三个12位ADC一个高级定时器TIM1和一个通用定时器TIM8都支持带死区的互补PWM输出完全满足最基础的FOC需求。如果你用的是F407或者G4系列支持浮点运算单元之后算法写起来更舒服PI参数直接用float类型跑也不担心性能。但从零上手和硬件兼容性角度考虑F103足够你先跑通。我见过不少新手直接跳到无感FOC无位置传感器上来就调滑模观测器然后电机转都不转就各种怀疑硬件。我的建议非常明确第一次做FOC先用带霍尔传感器或者编码器的方案把电流环速度环调通让电机转起来、转得稳再去碰无感。原因很简单无感方案需要观测器估计反电动势你没调通闭环之前连估计出来对不对都没法判断。1.3 方案总体架构双闭环FOC控制这套控制系统的整体结构可以拆成三层看。最外层是速度环输入是目标转速反馈是编码器/霍尔解算出来的实际转速输出是电流给定值Iq_ref。中间层是电流环输入是Iq_ref和Id_ref通常直接把Id_ref设为0因为表贴式永磁同步电机不需要励磁电流反馈是实际相电流经过坐标变换得到的Iq和Id输出是Vd和Vq电压矢量。最底层是SVPWM模块把Vd、Vq通过反Park变换得到Vα、Vβ再按空间矢量脉宽调制的方式计算出三对互补PWM的占空比。这个架构本身不是FOC创新点它已经非常成熟了。但难就难在每个环的采样时序怎么和PWM中心对齐、PI限幅值怎么配合母线电压、电流采样偏置怎么校准。我画了个简单的模块关系心里有这张图再写代码不会乱速度环PI 速度反馈解算 ← 霍尔/编码器中断电流环PIId环、Iq环 ← 电流采样值ADC中断/PWM中点坐标变换Clark → Park → 反Park ← 转子角度来自传感器SVPWM生成 → 查表法/实时计算 → 定时器比较寄存器三相逆变桥 → 驱动电路 → 电机相线所有环节里最容易出问题的其实是采样时刻。FOC的电流环周期一般设计成PWM周期同步PWM载波在中心处对齐此时相电流刚好在续流状态采样出来的值最接近真实相电流平均值。如果你随便在PWM边沿触发ADC采到的可能是续流毛刺或者换向瞬态反馈值一乱电流环必崩。2. 硬件准备与关键电路设计2.1 主控板选型与资源配置我推荐从STM32F103RCT6或者STM32F407VET6起步。F103的优点还是便宜稳定F407的优势是内部有硬件浮点单元调PI参数时写起来不用关心定点转换的问题。手上这块F103RCT6正好是博主和江科大STM32教程里常用的型号资料多引脚定义好查。FOC用到的外设资源如下TIM1CH1/CH2/CH3输出互补PWM控制三相上桥和下桥共6路。TIM1是高级定时器支持带死区插入的互补输出这非常重要——死区不靠软件延时靠硬件自动插入。ADC1规则通道采集两路相电流Ia、Ib通过TIM1的TRGO事件触发启动采样这样采样的时刻被硬件锁死在PWM中心。编码器接口TIM2/TIM3配置为编码器模式直接读取AB相脉冲。如果换霍尔方案用EXTI中断配合TIM定时捕获换相周期。串口调试用实时打印电流、转速、角度、占空比。引脚分配上最需要注意的是ADC采样通道尽量靠近PWM输出的定时器这样在PCB布局上采样电阻到运放再到MCU引脚的走线才能短编码器接口的输入引脚PA0/PA1PA6/PA7不能和JTAG占用冲突不然下载一次程序都要飞线。2.2 电流采样电路别在运放这里省成本三电阻采样是入门FOC最经典也最好用的方案三路下桥臂各串一个采样电阻通过运放差分放大后送给ADC。原理是利用下桥导通时的电流通路检测相电流当PWM占空比太小下桥导通时间过短或者过大时采样窗口会非常窄容易采不准。这也是业界常说FOC存在不可采样区的原因处理手段一般是调整SVPWM最小脉宽限制或者采用双电阻采样加点补偿算法。入门阶段不用太纠结这个占空比限制在5%~95%就够用。运放选型硬件成本不高但很容易踩坑。我第一版用的LM358带宽只有1.2MHz结果电流波形在高频载波下严重失真后来换了OPA4377才算正常。FOC的PWM载波频率我设的是16kHz采样运放至少要有10倍于PWM频率的增益带宽积才够也就是GBW最好在5MHz以上。另外偏置电压要设计好把采样的负半周期抬到ADC参考电压的1/2处让单电源运放也能采集双极性电流。关键参数给个参考采样电阻0.02Ω运放增益50倍那么满量程电流大约正负1.65A用3.3V参考电压计算ADC参考3.3V中心点偏置1.65V。对应关系是1A电流对应50mV × 50 2.5V换句话说ADC读到3.3V时对应1.32A。这个换算关系要在代码里做标定不能只靠理论值因为电阻误差和运放偏置会引入不可忽略的偏差。2.3 三相逆变桥与驱动电路三相逆变桥的核心是栅极驱动芯片加6个MOS管或者用集成了驱动MOS的IPM模块。栅极驱动最常用的DIY方案是IR2101/IR2103这种半桥驱动器一片控制一相上下桥三片就是三相。如果预算宽裕也可以直接上IR2110或者国产EG2134后者自举电路内置了快速恢复二极管外围少很多。选择MOS管的时候耐压值至少取母线电压的两倍以上。我用的是12V电源供电选了30V耐压的MOS但实际散热和尖峰压制一般后来换成了60V耐压的型号。Rds(on)要选小的大电流下发热才可控。驱动电路里的自举电容算是个小坑自举电容太小会导致上桥驱动电压不足。我常用的经验值是100nF到470nF之间的低ESR陶瓷电容配合1~5Ω的限流电阻可以正常工作在16kHz载波频率下。还有一个容易被忽略的问题是母线电容。逆变桥在工作时母线电流是脉动的如果电源离逆变桥比较远线路电感会产生很大的电压尖峰轻则MOS莫名其妙击穿重则把栅极驱动芯片一起带走。我实践下来的方案是在直流母线正负极之间并一个大容量电解电容470μF/35V以上再加一个0.1μF的CBB高频电容两者尽量靠近MOS管布置。2.4 转子位置传感器霍尔还是编码器带霍尔传感器的无刷电机原理很简单三个霍尔元件间隔120°电角度安装转子转动的时候霍尔输出高低电平组合一共6个状态每60°电角度换一次。霍尔方案硬件成本最低也能做到一个不错的方波驱动但直接用于FOC时60°分辨率太粗糙需要一个速度观测器去外插值估计更细的角度位置否则电流环坐标变换的精度会很差。编码器是目前做FOC最推荐的方案。增量式编码器价格不贵分辨率做到1000线甚至更细很容易加上ABZ三相后可以直接告诉MCU当前转子的绝对位置上电找一次Z脉冲过零位就行。如果是入门不想配编码器轴的电机用带霍尔的电机配合无感换向——先用霍尔测速和换向驱动电机转到一定转速后切换到无感FOC——这也是很多低成本电机驱动板的标准做法。我当时买的是一个带1024线增量编码器的云台电机配STM32F103开发板。编码器接口直接接TIM2用编码器模式默认3倍频这样不用外部中断就能实时获得位置。3600个计数对应一圈转速闭环的采样周期设为1ms速度环算出来的单位是rpm换算关系要写清楚后面调参才不糊涂。3. 核心代码实现与关键算法拆解3.1 工程框架与PWM定时器配置下面代码是基于STM32标准库写的用的是TIM1产生互补PWM。工程核心文件包含foc.c / foc.h算法主体包括坐标变换、PI控制器、SVPWMpwm.c / pwm.hPWM初始化、占空比更新adc.c / adc.h电流采样配置与断相检测encoder.c / encoder.h编码器读取与速度解算pid.c / pid.h增量式或位置式PIDmain.c初始化与主循环状态机TIM1初始化最关键的是几个地方PWM频率我设的是16kHz对应周期62.5μs、死区时间0.5~1μs具体看MOS开关速度、互补输出使能和刹车功能。通过修改ARR寄存器就行占空比由CCR1/CCR2/CCR3寄存器控制。我用PWM模式2向上计数CCR值低则输出低电平实际配置要看你的MOS管是高有效还是低有效驱动。代码如下void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_AFIO, ENABLE); // 2. 引脚配置PA8/PA9/PA10为TIM1_CH1/CH2/CH3PB13/14/15为互补通道CH1N/CH2N/CH3N GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. 定时器时基72MHz / 450 / 10 16kHz TIM_InitStructure.TIM_Prescaler 450 - 1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStructure.TIM_Period 10 - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM1, TIM_InitStructure); // 4. PWM输出配置OC1/OC2/OC3模式为PWM1 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; 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); // 5. 死区设置约0.7μs 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 50; 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); // 6. 使能TIM1主输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_UpdateDisableConfig(TIM1, DISABLE); // 7. 触发输出作为ADC同步信号 TIM_SelectOutputTrigger(TIM1, TIM_TRGOSource_Update); TIM_Cmd(TIM1, ENABLE); }很多人容易忽略TIM_BDTRInitStructure里的死区配置。如果你在硬件上没加额外的栅极驱动延时死区设置得太小上下桥同时导通母线直接短路炸管是大概率事件。我第一版因为想追求最大占空比死区设成0结果上电瞬间MOS管就热得烫手后来把DeadTime调到了50约0.7μs才稳定。3.2 ADC触发采样与电流重构PWM中心对齐采样是电流环数据质量的命根子。所谓中心对齐指的是PWM计数模式设为向上向下计数这样输出PWM的高电平脉冲以周期中心对称此时下桥续流电流和上桥导通电流的过渡点很接近在下桥导通的瞬间采样得到的相电流才是真实负载电流的平均值。我把ADC配置成由TIM1更新事件触发确保每次PWM周期中心处自动启动一次规则通道转换。电流采样的代码要解决两件事一是ADC采样结果的偏置校准零电流时运放输出不是ADC参考的一半二是两路电流重构第三路电流Id Ib Ic 0。typedef struct { int16_t ia_raw; int16_t ib_raw; float ia; float ib; float ic; } CurrentSample_t; // 调用时机在ADC完成中断里 or 主循环轮询DMA完成标志 static void Current_Read(CurrentSample_t *cur) { static float ia_offset 2048.0f; // 0A时ADC读数上电标定得到 static float ib_offset 2048.0f; static float current_scale 1.32f; // 每0.0016V对应0.0008A可换算到ADC码 // 读取DMA搬运后的两个通道值 cur-ia_raw adc_buffer[0]; cur-ib_raw adc_buffer[1]; // 零电流偏置校准采样两路对地短接保存均值作为offset cur-ia (cur-ia_raw - ia_offset) * current_scale; cur-ib (cur-ib_raw - ib_offset) * current_scale; cur-ic -1.0f * cur-ia - cur-ib; }零电流偏置校准这一步千万不要省。运放的输入失调电压、ADC参考电压的纹波、采样电阻的温度漂移都会让偏置点偏离理论值如果offset不准电流环在零电流附近就会输出一个恒定的小力矩电机转起来你会发现它低速时一顿一顿的像步进电机一样。3.3 Clark/Park变换与坐标旋转Clark变换先把三相对称电流投影到两相静止坐标系αβ上void Clark_Transform(float ia, float ib, float ic, float *i_alpha, float *i_beta) { *i_alpha ia; *i_beta (ia 2.0f * ib) * 0.57735027f; // 1/sqrt(3) ≈ 0.57735 }然后根据转子角度θ做Park变换从αβ坐标系旋转到dq坐标系void Park_Transform(float i_alpha, float i_beta, float theta, float *i_d, float *i_q) { float cos_t arm_cos_f32(theta); float sin_t arm_sin_f32(theta); *i_d i_alpha * cos_t i_beta * sin_t; *i_q -i_alpha * sin_t i_beta * cos_t; }反Park变换在SVPWM模块里用到把电压环输出的Vd、Vq转回Vα、Vβvoid Inverse_Park_Transform(float v_d, float v_q, float theta, float *v_alpha, float *v_beta) { float cos_t arm_cos_f32(theta); float sin_t arm_sin_f32(theta); *v_alpha v_d * cos_t - v_q * sin_t; *v_beta v_d * sin_t v_q * cos_t; }这里有个常见的隐藏bug角度θ的零点一定要对齐电机的电气零位。如果你的编码器Z脉冲出现的位置和A相绕组反电动势过零点有差异Id和Iq会互相耦合速度环调好了扭矩也不够用还会引起稍微带载就失步。解决办法是上电时执行一次转子角度标定方法其实很简单给d轴电压开环输出一个固定电压矢量强制转子锁到一个预定的电角度把此时编码器的读数记下来作为角度偏移。我每次上电都会做这个动作几秒钟就完成。3.4 SVPWM的查表实现SVPWM有很多实现方式有人用浮点实时计算扇区、计算矢量作用时间有人用存储好的占空比表格加上插值。入门阶段我建议用经典的浮点实时计算法虽然代码量多一些但逻辑透明出问题好查。核心思路是Vα、Vβ组成的空间矢量在一个PWM周期内由相邻两个非零电压矢量和两个零矢量共同合成。以扇区1为例相邻矢量为(100)和(110)它们的作用时间分别是T1和T2零矢量作用是T0。分配好作用时间后再转换为三路比较寄存器的值输出到定时器。void SVM_Calc(float v_alpha, float v_beta, uint16_t *c1, uint16_t *c2, uint16_t *c3) { // 归一化v_alpha/v_beta范围 -1 ~ 1对应占空比0%~100% float v1 v_beta; float v2 -0.8660254f * v_alpha 0.5f * v_beta; float v3 0.8660254f * v_alpha 0.5f * v_beta; // 辅助比较值 float ta 0.5f * (1.0f (v1 v2 v3) / 1.5f); float tb ta - 0.5f * (v2 v3) / 1.5f; float tc tb 0.5f * v3 / 1.5f; // 占空比限制防止不可采样区过大 if (ta 0.95f) ta 0.95f; if (tb 0.95f) tb 0.95f; if (tc 0.95f) tc 0.95f; if (ta 0.05f) ta 0.05f; if (tb 0.05f) tb 0.05f; if (tc 0.05f) tc 0.05f; // 转换到PWM比较寄存器值ARR10所以乘以10 *c1 (uint16_t)(ta * 10.0f); *c2 (uint16_t)(tb * 10.0f); *c3 (uint16_t)(tc * 10.0f); }这段代码简化了很多细节没有显式判断扇区而是直接用了三段式算法对于STM32F103这种带FPU的芯片算得过来。实际项目中SVPWM还要做过调制处理在电压矢量超出六边形边界时自动收缩否则输出电压饱和后电流环就会失控具体表现为电机高速时电流突然上不去或者转速升高后扭矩骤降。入门阶段先不做过调制也OK但你要知道这个瓶颈的存在。3.5 双闭环PI控制器实现电流环和速度环分别用传统的位置式PI就行。电流环要快周期等于PWM周期16kHz速度环可以慢一点1ms周期足够。PI控制公式typedef struct { float Kp; float Ki; float integral; float max_output; float max_integral; // 积分限幅 } PID_t; void PID_Init(PID_t *pid, float kp, float ki, float max_output, float max_integral) { pid-Kp kp; pid-Ki ki; pid-integral 0.0f; pid-max_output max_output; pid-max_integral max_integral; } float PID_Regulate(PID_t *pid, float ref, float fb) { float err ref - fb; float p_out pid-Kp * err; pid-integral err; if (pid-integral pid-max_integral) pid-integral pid-max_integral; if (pid-integral -pid-max_integral) pid-integral -pid-max_integral; float i_out pid-Ki * pid-integral; float out p_out i_out; if (out pid-max_output) out pid-max_output; if (out -pid-max_output) out -pid-max_output; return out; }这里必须强调一个坑积分限幅和输出限幅要区分开。很多新手只做输出限幅不做积分限幅结果电机堵转或启动时积分器疯狂累加输出达到限幅后积分还在增加一旦负载消失输出要很长时间才能回落表现就是电机一个大冲角然后过冲。积分限幅一般取最大输出值的1/2到1/4。电流环PI参数不能拍脑袋。一个粗略但有效的整定方法是基于电机电阻和电感计算理论带宽。设d轴电感Ld、电阻Rs电流环期望带宽500~1000Hz那么Kp ≈ Ld × 2π × 带宽Ki ≈ Rs × 2π × 带宽。实测后在这个基础上微调比瞎试快得多。我那块云台电机Ld大约0.2mHRs大约1Ω取800Hz带宽Kp≈1Ki≈5000注意单位换算然后上板实测微调。速度环的整定相对依赖机械系统没法纯理论。先只开I把P设成0从小往大加观察转速阶跃响应是否出现等幅振荡然后加P阻尼振荡。我习惯给速度环加前馈把目标加速度乘一个系数叠加到电流给定上对跟随性能改善明显尤其是周期性速度指令的场合。3.6 主循环状态机与中断处理FOC代码的实时性要求主要在电流环必须确保每个PWM周期内都能完成一次ADC采样、坐标变换、双环PI计算和PWM占空比更新。这些计算适合放在ADC DMA传输完成的回调里做主循环只负责速度环、串口调试、状态机切换等非实时任务。我的主循环状态机大致这样状态IDLE电机不转输出全零等待使能命令状态LOCK执行转子角度标定强制转子定位到电气零位状态RUN电流环和速度环使能速度参考来自串口指令状态FAULT发生过流/过压/驱动器报错关断PWM输出等待复位这个大循环不断扫描状态标志位中断服务函数里只干最关键的事情。// ADC DMA完成中断服务函数 void ADC_DMA_IRQHandler(void) { CurrentSample_t cur; float i_d, i_q, v_d, v_q, v_alpha, v_beta; uint16_t c1, c2, c3; // 1. 读取电流 Current_Read(cur); // 2. 读取转子角度编码器计数乘以系数 theta Encoder_GetAngleRad(); // 3. Clark Park变换 Clark_Transform(cur.ia, cur.ib, cur.ic, i_alpha, i_beta); Park_Transform(i_alpha, i_beta, theta, i_d, i_q); // 4. 电流环PI if (run_state STATE_RUN) { v_d PID_Regulate(pid_id, 0.0f, i_d); v_q PID_Regulate(pid_iq, iq_ref, i_q); } else { v_d 0.0f; v_q 0.0f; } // 5. 反Park SVPWM Inverse_Park_Transform(v_d, v_q, theta, v_alpha, v_beta); SVM_Calc(v_alpha, v_beta, c1, c2, c3); // 6. 更新PWM比较寄存器影子寄存器 TIM_SetCompare1(TIM1, c1); TIM_SetCompare2(TIM1, c2); TIM_SetCompare3(TIM1, c3); }这段代码看起来不多但里面的每个环节都有讲究。比如θ角读取必须和ADC采样使用同一时刻的值如果角度和电流差了几个PWM周期坐标变换就会引入相位误差轻则效率下降重则电流环发散。4. 调试过程与避坑指南实录4.1 上电就炸管死区与驱动电路排查我第一次给驱动板上电的时候电流直接超出电源限值检查发现MOS管烧了两路。当时第一反应是PWM逻辑反了——实际上确实反了我的驱动电路用的是低有效逻辑而代码里按高有效配置了导致上桥和下桥同时导通形成了贯穿短路。这个经历给我的教训是上电之前先用逻辑分析仪查三对PWM的互补关系再结合驱动电路确认逻辑极性。把PWM频率降到1kHz占空比固定为10%接到一个小灯泡或LED上面模拟电机负载观察换相逻辑是否正确正常确认无误后再上母线电。有条件的直接用电流钳观察母线电流一旦发现上电瞬间大电流立刻断电检查。死区设置也容易踩坑。TIM1的DeadTime寄存器换算规则是DeadTime 寄存器值 × TclkTclk是定时器时钟的倒数。Tclk72MHz时寄存器值为50对应约0.7μs。有些IGBT/MOS栅极电容大开关慢需要1~2μs死区这时候寄存器值要对应加大。如果死区太大占空比损失多电流波形畸变死区太小上下桥直通。一般来说0.5μs到1μs是入门板比较稳妥的范围。4.2 电流波形毛刺明显采样时刻和硬件滤波打架电流环跑起来之后我用DAC把Iq反馈波形导出到示波器发现电流波形上叠加了明显的锯齿状毛刺频率和PWM载波一致。排查过程很简单先怀疑硬件滤波不够在运放输出端加了RC低通滤波毛刺变小了一点但相位滞后变大了电流环跟着不稳定。后来查了ADC采样时刻发现问题出在TIM1的TRGO事件配置上。我用的是Update事件作为触发源但因为ARR值是10更新事件其实发生在计时器从ARR翻转到0的瞬间而那个时刻PWM比较输出刚发生跳变采样很容易采到开关噪声。解决办法是把触发源改成OC3REF信号这样触发时刻正好落在PWM中心附近噪声最小。具体配置可以参考STM32CubeMX中ADC触发选项里的Center-aligned mode。实测改完后电流波形干净多了。所以这里其实有个通用的排查套路先看采样时刻再看运放带宽是否不够最后才考虑加滤波。别一上来就加滤波滤波会掩盖采样时序错误的问题而且会引入额外的相位滞后治标不治本。4.3 电机尖叫、抖动、失步三个典型症状的代码根源电机尖叫但转不起来我用串口打印角度信息和对电流反馈发现转子角度追赶过程有严重延迟。问题出在编码器角度读取频率太低速度环已经按1ms更新角度计数器却因为配置错误只有几Hz的更新率。查TIM2编码器模式的配置发现没有把Encoder Mode设置成TI1和TI2计数改好之后角度信息实时跟上电机就正常了。电机刚启动时抖动厉害伴随力矩大幅跳动。我最后定位到问题是转子初始角度标定没做准确。如果初始角度估计偏差超过30°电角度启动阶段的Id和Iq会混在一起导致电机在低速时像步进电机一样一顿一顿。解决办法是把初始标定做得更严格上电分两个阶段先出固定电压矢量把转子锁位再把这个位置记下来之后每次上电先用小直流量把转子拉到标定位再切闭环。高速段电机突然失步然后电流飙到限幅值。这类问题通常和SVPWM过调制有关因为速度上升后反电动势逐渐接近母线电压输出电压遇到饱和边界电流环给定的电压值无法在逆变桥中真实合成。解决手段是加入弱磁控制或者调整最大电压利用率。入门阶段可以先不做弱磁但要把速度环的最大输出限制在0.85×母线电压以内避免进入过调制区。4.4 霍尔传感器替代编码器时的注意点如果你手里只有带霍尔的电机打算先用霍尔测速再切无感FOC有几个容易踩的点。第一霍尔信号和转子电角度的对齐关系不一定是60°整数电机装配误差会让霍尔边沿对应的电角度有偏差。如果你直接用霍尔状态对应的固定角度当转子位置电流环在换向边沿附近容易出现力矩脉动。处理办法是在每个霍尔区间内部用上一次换向周期做线性插值让角度在每个钻孔区间内连续增长。第二霍尔信号本身会有一定的迟滞和噪声直接用外部中断读取容易产生毛刺计数。我给霍尔信号加了RC滤波10kΩ100nF同时开启STM32的输入捕获滤波器配置数字滤波器和预分频这样换向周期测量稳定很多。第三切无感FOC时要确保速度已经超过无感观测器的最小观测转速。霍尔方案一般可以在5%额定转速以上运行稳定无感方案低转速段的观测器增益往往不够容易在切换点附近丢失转子位置。建议设置一个滞回区间上升到额定转速的10%时切换到无感下降回到8%时重新切回霍尔。5. 常见问题速查与实操经验总结5.1 问题排查速查表我把自己做FOC过程中遇到的问题整理成了一张表方便你对照排查。没有涉及到的细节可以对照ST官方的Motor Control SDK参考文档继续深挖。现象可能原因排查顺序与建议上电即短路上下桥同时导通、死区过小、PWM极性反先断开母线电用低占空比、低频率验证PWM组合逻辑再量驱动芯片输出电机不转有电流转子初始角度不对、电流环PI限幅过小查看转子位置标定流程检查Iq给定值确认PI输出是否进入SVPWM电机抖动严重角度反馈滞后、采样电流毛刺大、PI参数过大用串口打印角度和电流波形时序先降低电流环带宽再查采样时刻高速失步SVPWM过调制、速度环输出超限限速环输出检查母线电压利用情况考虑弱磁或降低最高速度转速波动大速度环带宽不够、机械负载变动、编码器计数抖动提高速度环采样频率观察编码器Z脉冲稳定性必要时加滤波器MOS管过热占空比过大导致采样区异常、开关损耗过大检查死区时间确认PWM载波频率是否过高考虑降低载频或换低Rds(on)管程序死机中断优先级配置错误、浮点运算处理不当确保FOC中断优先级高于其他中断检查ADC DMA中断标志清除5.2 实操中最容易被忽略的三个细节第一个是母线电压监测。很多入门级方案完全不采母线电压PI输出对应的SVPWM没有电压补偿母线电压波动时电流环表现会时好时坏。建议加一路ADC采集母线电压把Vd、Vq的幅值做归一化这样即使电源电压掉到10V电流环也能自动缩小占空比维持给定电流。第二个是FOC代码里的弧度角度处理。角度θ必须和电流环的计算频率保持一致每次ADC中断都要根据当前编码器计数重新计算不能用上一次的缓存值。如果电流环计算频率和编码器角度更新频率脱节Iq里会出现明显的与转速同频的脉动。第三个是刹车处理。FOC的电流环天然有刹车能力只要把Iq给定设为负值就能电机制动但这时能量会反向灌回母线如果母线电容不够大电压会飙升到击穿MOS或驱动芯片。所以做制动功能时母线电压上限保护是必须的超过阈值就封锁PWM并开启泄放电阻。5.3 整套调试顺序建议很多朋友拿到工程之后急着转电机结果越调越乱。我建议按下面顺序一步一步来先点亮LED确认时钟系统正常。配好串口能输出调试信息。单独测PWM波形用逻辑分析仪确认六路信号形态、死区、极性。单独测ADC给采样电阻通恒流确认电流换算线性。电机开环测试给一个固定占空比的SVPWM看电机是不是能跟着方向转。如果开环都转不齐先别谈闭环。电流闭环测试堵转电机或轻载给定一个小Iq比如0.1A确认电流环能稳住。速度闭环测试给定低速逐步提高观察稳定性。参数整定先电流环后速度环同时配合串口波形观察。我自己在实际调试中最大的体会是FOC的坑大多数不是算法原理难而是离散化实现和硬件时序配合的问题。坐标变换公式谁都能背但采样时刻、编码器角度同步、死区逻辑这些东西不踩坑根本不会形成直觉。如果你正在做这个项目卡住了也不要着急把每一步的时序图画出来用串口把中间变量打出来一点点看多数问题都能在一屏数据里找到答案。最后再分享一个小技巧给代码里增加一个遥测变量把电流环周期内的计算耗时用定时器测量出来串口输出。如果发现PWM中断里计算时间占到了整个周期的80%甚至更多说明算法开销太大得像样地优化一下查表法替代三角函数、定点化PI参数等给系统留出裕量。毕竟FOC的代码不仅是“能转”还要能在更恶劣的工况下长期稳定运行。
