ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

增量式编码器零位校准:FOC电机控制的关键步骤与STM32实现

增量式编码器零位校准:FOC电机控制的关键步骤与STM32实现 做FOC电机控制的朋友多半都经历过这种场面程序烧进去电流环参数已经调得很温和上电给一个小指令电机非但不转反而“嗡”的一声定子发烫电流表直接往上飙。这时候十有八九不是PI参数的问题而是增量式编码器的零位没校准。FOC、电机控制、增量式编码器、零位校准、STM32这几个词放在一起就是很多刚接触FOC的工程师和新手的第一个拦路虎。这篇内容我会把增量式编码器零位校准这件事彻底讲明白从为什么要校准、校准前要做什么准备到5个关键步骤本身再到可以直接抄的STM32代码最后附上我实际调试中踩过的坑和排查方法。适合刚入门FOC、手头有用增量式编码器的电机、或者调电流环一直调不出来的人。只要照着做一遍基本半小时内就能让电机正常出力。1. 零位校准到底在解决什么问题先说清楚一个容易混淆的概念增量式编码器装在电机轴上以后反馈给控制器的是“相对位置变化”它本身并不知道转子当前转到哪个角度。但FOC的电流环在Park变换和反Park变换里需要的是转子磁极的绝对电角度这个角度差一点也不行。1.1 FOC电流环为什么非要电角度不可FOC之所以叫磁场定向控制核心就是把定子电流分解成d轴和q轴两个分量d轴对应励磁电流q轴对应转矩电流。要做到这一点先得知道转子磁极到底朝哪个方向然后才能把三相电流变换到那个坐标系里。这个变换就是Park变换公式里的sin、cos都来自电角度。如果电角度错了30度实际施加的电流方向就会偏30度q轴分量变小d轴分量变大电机出力下降、发热增加。如果角度直接偏了90度那励磁和转矩完全对调电机不仅不转还可能剧烈抖动甚至反转电流一大就把MOS管或驱动芯片烧了。打个比方推秋千要在秋千荡到最高点前发力如果发力时机完全反了秋千不但荡不起来还会被推回原地。FOC里的电角度就是决定“什么时候发力”的关键信息。1.2 增量式编码器的“增量”二字是关键增量式编码器输出的是A、B两路正交脉冲控制器通过计数得到相对位置变化再配合方向信号判断旋转方向。它跟绝对式编码器最大的区别是断电后所有位置信息清零上电之后只能从当前位置开始数脉冲不知道转子在哪个角度。所以每次上电FOC都需要一个办法来确定转子初始位置这就是“初始位置检测”的来由。有的方案用高频注入有的用霍尔传感器而用增量式编码器的方案里最实用的就是做“零位校准”先把编码器的零点或某个计数点和电机定子绕组的某个固定电气角度建立确定关系之后每次上电都能用公式换算出来。这里要特别注意电角度 机械角度 × 极对数 偏移角。机械角度由编码器计数算出来极对数是电机固有参数偏移角则跟编码器安装位置有关。同一批电机、同样的板子只要编码器装的时候有微小角度差偏移角就不一样这才是必须逐台校准的根本原因。1.3 不校准直接跑会是什么后果很多新手一上来就把电流环参数照着教程抄发现电流始终调不稳。其实不是参数问题而是电角度全错了PI再怎么调都是白费劲。未校准时的典型表现是现象可能原因上电后电机嗡嗡响、抖动、发热电角度严重偏移d轴电流冲高电流很大但转矩很小角度偏移导致q轴有效分量变小正转有力、反转无力偏移角符号反了或相序不对速度一变就剧烈震荡角度换算有跳变电角度不连续这些现象我在调试中基本全见过踩得多了才明白零位校准不是“锦上添花”而是FOC能跑起来的前置条件。增量式编码器方案里这一步做不好后面谈什么速度环、位置环都是空中楼阁。2. 校准前的硬件检查与准备在校准动作开始之前先花十分钟检查硬件和软件环境比直接改代码试错要省事得多。我见过不少朋友上来就写校准程序结果编码器线序接反、供电电压不够折腾一整天还以为是算法问题。2.1 增量式编码器的接线与信号类型增量式编码器通常输出A、B、Z三路信号A和B相差90度用来判向和倍频计数Z是每转一圈一个脉冲的零点信号。FOC日常控制只需要A、B两路Z信号一般用来辅助校验“这一圈数有没有丢步”。接线时重点确认三点。第一供电电压常见的有5V和3.3V编码器看编码器说明书5V供电的编码器输出到STM32引脚时电平可能超过3.3V需要在A、B、Z信号线上串电阻分压或加电平转换芯片直接进MCU会有烧引脚风险。第二A、B相是否接入定时器的两个通道比如STM32的TIM2 CH1和CH2或TIM3 CH1和CH2需要使用定时器的编码器模式不能随便接普通GPIO。第三部分编码器输出是开漏结构板子上必须要有上拉电阻否则信号边沿不干净计数会发生丢步或误计数。我在自己做的小板子上就吃过亏编码器5V供电、AB线直接连到STM32的3.3V引脚刚开始能用跑了十几分钟后MCU就复位后来一测发现引脚已经内伤。所以这块不要省信号电平匹配是稳定运行的前提。2.2 母线电压、限流与安全准备校准过程中最常做的操作是开环给电压矢量把转子强行拉到指定位置。这个操作如果不限流转子被拉到位之后电流会一直上升绕组很快发烫严重时烧驱动。建议在直流母线输入端串一个限流电阻或使用可调限流电源把电流限制在电机额定电流的30%以内够拉动转子就行。同时准备好串口方便把编码器计数值、计算出的偏移角实时打印出来。没有串口调试整个校准过程就像摸黑走路不方便判断到底发生了什么。另外一定要养成一个好习惯第一次上电做任何强拖操作时手不要离开电源开关。一旦发现电流过大或电机剧烈抖震立刻断电先检查硬件连接和软件配置再继续下一步。2.3 CubeMX中的关键配置说一个最基础的配置参考以STM32F103为例。使用TIM3的编码器模式A相接PA6CH1B相接PA7CH2编码器分辨率设为4倍频计数范围设为0到编码器单圈总计数减1。PWM输出用高级定时器TIM1六路互补PWM驱动三相全桥ADC采样两相电流串口1接调试口波特率115200。对于新接触STM32的人有几点容易遗漏。定时器编码器模式要选“Encoder Mode TI1 and TI2”不是普通的输入捕获。定时器计数方向默认和电机转向匹配如果后来发现计数方向和实际旋转方向相反可以在CubeMX里把“Polarity”改成相反的也可以在代码里对计数值取反。ADC要配置成双通道规则组采样触发源选择定时器更新事件确保电流采样和三路PWM同步这样采到的电流才是真实绕组电流而不是开关噪声。外设配置项关键设置TIM3编码器模式Encoder Mode TI1 and TI24倍频TIM1PWM输出中心对齐模式带互补输出和死区ADC1电流采样双通道定时器触发采样保持时间适当加大USART1调试输出115200-8-N-1配置完之后先用一个最简单的测试程序确认编码器计数是否随转子转动而增减方向是否一致。这一步通过之后再进入校准流程能减少后面很多干扰因素。3. 5个关键步骤逐一拆解重头戏来了。增量式编码器零位校准的完整流程我把它拆成5个步骤确认方向、开环强拖、锁定测偏移、写入并验证、保存与复验。这5步每步都有明确目的做完就能让FOC正常出力。3.1 第1步确认编码器方向与电机相序原理很简单给电机施加一个固定电压矢量让转子被拉到某个确定位置。然后用手或辅助工具轻轻拨动转子观察编码器计数的增减方向。如果编码器方向和期望方向相反后面所有角度换算全都会乱所以必须在最开始确认。实操方法如下。先给逆变器输出一个简单的两相通电状态比如A相接正、B相接地C相悬空这样定子会产生一个固定方向的磁场转子会被拉到一个平衡位置。然后手动缓慢旋转转子观察串口打印的编码器计数值。记录转子顺时针旋转时计数是递增还是递减。接下来换C相接正、B相接地的状态同样手动旋转转子确认计数变化方向没有因为换相改变。这个操作的目的是排除相序接反而引起的“假方向正常”。正常情况是相同旋转方向下计数增减方向始终一致。如果换了相序后计数方向变了说明有一相或两相绕组的接线顺序有误要先把UVW三个相的接线理清楚再继续。相序确认之后还有一个隐藏点要注意FOC控制的电压矢量和编码器计数方向必须形成“正反馈”关系。也就是当程序输出的空间矢量使电机正向旋转时编码器计数也应该朝正向变化。这个关系直接影响第4步验证是否顺利可以顺手在第1步里一起确认。3.2 第2步开环强拖建立初始电角度这个步骤的目的是让控制器和电机之间先建立一种“已知的关系”。在还没校准零位时电角度是未知的所以不能直接上闭环得用开环方式把转子“拖”起来让编码器计数值跟着转子同步变化。操作时程序里把q轴电流给定设为0d轴电流给定设为某个固定正值配合一个缓慢递增的电角度通过反Park变换生成期望电压矢量。这个角度不依赖编码器而是代码里用定时器累加出来的。转子会跟着磁场转动编码器计数值也会随之变化。我给一个工程里常用的开环强拖代码框架void foc_openloop_drag(float target_speed_rps, float id_command) { float theta_elec 0.0f; float angle_step 2.0f * PI * target_speed_rps * POLE_PAIRS * DT_SECONDS; while (1) { // 反Park变换输入id和iqiq0 float u_alpha -id_command * sinf(theta_elec); float u_beta id_command * cosf(theta_elec); // 经过SVPWM调制输出到逆变器 svpwm_set(u_alpha, u_beta, DC_BUS_VOLTAGE); theta_elec angle_step; if (theta_elec 2.0f * PI) theta_elec - 2.0f * PI; // 打印编码器原始计数值和当前电角度 printf(enc%d theta%.3f\r\n, TIM3-CNT, theta_elec); delay_ms(1); } }注意这里有一个容易被忽视的点强拖时给定的id方向决定了零位偏移角计算出来之后的符号。不同FOC框架里有些以d轴正向励磁有些以id负向励磁保存偏移角之前要统一符号规则否则第4步会发现角度怎么都对不上。3.3 第3步锁定测偏移算出零位偏置角这是整个校准流程的核心说白了就是“把转子锁到一个已知电角度然后读编码器的值反推出偏移角”。具体做法是依次向电机施加六个不同的电压矢量每个矢量对应一个已知电角度。比如常用的六步换向法六个矢量分别对应0度、60度、120度、180度、240度、300度的电气角度。每个矢量保持一段时间让转子稳定下来然后记录编码器计数值。为什么要做六个点而不是只做一个点因为单个锁定点受齿槽转矩、摩擦力、负载偏差影响可能存在几度误差。六个点均匀分布取平均之后能把大部分机械不对称误差抵消掉得到的偏移角更稳。我用的计算逻辑是#define ENCODER_PPR 4096 // 编码器线数×4倍频后的单圈总计数 #define POLE_PAIRS 7 // 电机极对数写自己的电机实际值 int32_t enc_locked[6]; float angle_locked[6] {0, 60, 120, 180, 240, 300}; // 单位度 // 依次锁定6个电角度 for (int i 0; i 6; i) { set_lock_voltage_vector(angle_locked[i]); delay_ms(500); // 等转子完全稳定 enc_locked[i] TIM3-CNT; } // 计算每个锁定点对应的偏移角然后取平均 float offset_sum 0.0f; for (int i 0; i 6; i) { float mech_angle (float)enc_locked[i] / ENCODER_PPR * 2.0f * PI; float elec_angle mech_angle * POLE_PAIRS; float offset angle_locked[i] * PI / 180.0f - elec_angle; // 归一到 0 ~ 2pi while (offset 0) offset 2.0f * PI; while (offset 2.0f * PI) offset - 2.0f * PI; offset_sum offset; } // 6个点取平均 float zero_offset offset_sum / 6.0f; while (zero_offset 0) zero_offset 2.0f * PI; while (zero_offset 2.0f * PI) zero_offset - 2.0f * PI; printf(zero_offset%.3f rad\r\n, zero_offset);把这个偏移角保存下来后续FOC运行时float get_elec_angle(void) { float mech_angle (float)TIM3-CNT / ENCODER_PPR * 2.0f * PI; float elec_angle mech_angle * POLE_PAIRS zero_offset; // 归一到 0 ~ 2pi while (elec_angle 2.0f * PI) elec_angle - 2.0f * PI; while (elec_angle 0) elec_angle 2.0f * PI; return elec_angle; }锁定电压矢量的实现方式有讲究。可以直接操作六个开关状态也可以用SVPWM输出固定的alpha beta分量后者更接近实际运行状态建议使用。另外锁定时长要足够我一般用500毫秒如果电机转动惯量大就加长到1秒确保机械振荡衰减完毕再读数。3.4 第4步写入偏移值并验证角度同步性偏移角算出来以后下一步不是直接跑闭环而是先做一个低速同步验证。把计算出的zero_offset代进get_elec_angle函数然后在开环强拖模式下以非常低的速度比如每秒1圈拖动转子同时打印两个角度代码里累加的期望电角度和通过编码器换算出来的实际电角度。理想情况下这两个角度之差应该稳定在一个很小的范围内不会随转子位置变化而出现明显波动。如果差值忽大忽小说明极对数设置错了或者编码器倍频配置不对。如果差值基本稳定但相差一个常数那是偏置角没算准重新检查第3步的符号。验证通过后可以在闭环电流模式下做一次正反转测试。给一个较小的q轴电流指令比如额定电流的10%观察电机是否能平稳转动。正转和反转的力矩应该基本一致电流波动小没有明显的“一顿一顿”的感觉。这一步最常见的坑是强拖同步验证看着没问题一上闭环就抖。原因往往是电流采样时序不对或者PWM死区设置不合适这两个问题跟零位校准没有直接关系但很容易被误判成校准失败。所以我一般建议闭环测试时先把d轴电流设为0只给q轴电流这样只要角度正确电机就会以正常方式出力能更快定位到真正的问题。3.5 第5步保存偏移值并做好重复性确认校准结果不能只留在RAM里掉电就丢。需要把zero_offset保存到STM32的Flash或外部EEPROM上电时读取并加载到全局变量中。保存时注意几个细节。浮点数直接存Flash需要对地址做4字节对齐或者把角度乘以一个系数转成整数存储。我习惯用角度制的整数比如把偏移角乘以1000转换成一个int32_t读回来再除以1000这样既能满足精度又避免了浮点存储的对齐问题。保存完偏移值后要断电重新上电重复做几遍第3步的锁定测量。每遍算出来的偏移角应该非常接近波动范围最好在±1度以内。如果两次测量差了十几度大概率是编码器安装松动、联轴器打滑或者强拖时转子没锁稳就记录了计数值。这种机械问题代码是治不了的先检查安装再重新标定。批量生产时每台电机的偏移角都要单独标定并把结果写入各自的控制器。不要试图“复制粘贴”某台电机标定好的参数到另一台电机上因为编码器安装角度的微小差异足以导致FOC出力明显变差。4. 代码实现与FOC电流环完整对接这一节给出一个可运行的代码组织方式帮助你把校准流程嵌入到自己的STM32工程里。代码按模块分文件逻辑清晰方便替换硬件相关部分。4.1 代码模块划分我把校准和FOC运行拆成三个文件encoder.c负责编码器计数读取和电角度换算foc_align.c负责六步锁定和偏移角计算foc_core.c负责电流环闭环控制。这样校准逻辑和运行逻辑互相独立调试和以后改成无感FOC都方便。encoder.c里对外提供三个函数void encoder_init(void); float get_elec_angle(void); // 返回电角度单位rad int32_t get_enc_count(void); // 返回编码器原始计数值foc_align.c里是对外提供标定入口float foc_align_run_zero_offset(void); // 执行6步锁定返回偏移角 void foc_align_save_offset(float offset); float foc_align_load_offset(void);foc_core.c里就是常规的电流环核心循环里直接调用get_elec_anglevoid foc_current_loop(void) { float ia get_phase_a_current(); float ib get_phase_b_current(); float ic -ia - ib; float theta get_elec_angle(); // Clarke和Park变换 float i_alpha ia - 0.5f * (ib ic); float i_beta 0.866f * (ib - ic); float id i_alpha * cosf(theta) i_beta * sinf(theta); float iq -i_alpha * sinf(theta) i_beta * cosf(theta); // PI调输出vd、vq这里略去具体实现 // vd pid_update(pid_id, id_ref, id); // vq pid_update(pid_iq, iq_ref, iq); // 反Park变换 // v_alpha vd * cosf(theta) - vq * sinf(theta); // v_beta vd * sinf(theta) vq * cosf(theta); // svpwm_set(v_alpha, v_beta, DC_BUS_VOLTAGE); }4.2 六步锁定测偏移的实现细节六步锁定的实现不是简单地把六个开关状态依次设一遍而是要确保每次锁定都对应一个明确的电角度。工程上我一般用SVPWM函数来生成固定的alpha、beta电压分量这样锁定方式和后续闭环运行方式完全一样消除了“开关状态锁定”和“SVPWM运行”之间可能存在的角度映射误差。对应关系可以这样理解。SVPWM的六个扇区边界矢量分别对应电角度0、60、120、180、240、300度正好对应六个开关状态。用SVPWM输出这些矢量时每个矢量的方向就是对应的电角度然后通过反Park变换得到alpha、beta分量再交给SVPWM模块去生成PWM占空比。代码实现如下void set_lock_voltage_vector(float angle_deg) { float angle_rad angle_deg * PI / 180.0f; // 电压小一点能锁住转子就行 float u_alpha 0.2f * DC_BUS_VOLTAGE * cosf(angle_rad); float u_beta 0.2f * DC_BUS_VOLTAGE * sinf(angle_rad); svpwm_set(u_alpha, u_beta, DC_BUS_VOLTAGE); }锁定时另一个关键项是“等待稳定”。转子是一个机械系统施加电压矢量后它会有振荡衰减的过程刚切换状态就立刻读数往往锁在了一个偏摆位置偏移角计算出来就不准。等待时间至少要覆盖机械振荡衰减周期500毫秒通常够用大惯量电机适当加长。4.3 Flash存储偏移角的示例偏移角的保存我用的Flash存储实现大致是#define ALIGN_OFFSET_ADDR 0x0800F000 // 选一个空闲Flash页 void foc_align_save_offset(float offset) { int32_t value (int32_t)(offset * 1000.0f); HAL_FLASH_Unlock(); FLASH_ErasePage(ALIGN_OFFSET_ADDR); uint32_t address ALIGN_OFFSET_ADDR; // 注意F103的Flash编程以16位为单位这里按32位写两次 *(volatile uint16_t *)address (uint16_t)(value 0xFFFF); address 2; *(volatile uint16_t *)address (uint16_t)((value 16) 0xFFFF); HAL_FLASH_Lock(); } float foc_align_load_offset(void) { int32_t value 0; uint32_t address ALIGN_OFFSET_ADDR; value | *(volatile uint16_t *)address; address 2; value | ((int32_t)*(volatile uint16_t *)address) 16; return (float)value / 1000.0f; }注意这个代码是F1系列的Flash操作方式F4、H7等系列的地址对齐要求和编程单位不同直接照搬会卡死在写Flash的函数里。用其他型号芯片时先去查参考手册的Flash编程章节避免踩坑。4.4 上电初始化顺序上电时主函数的初始化顺序建议是int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); // 编码器模式 MX_TIM1_Init(); // PWM MX_ADC1_Init(); MX_USART1_Init(); encoder_init(); // 读取标定的零位偏移 float offset foc_align_load_offset(); if (offset 0 || offset 2.0f * PI) // 简单校验读取结果是否合理 { // 不合理则进入标定模式 offset foc_align_run_zero_offset(); foc_align_save_offset(offset); } foc_core_init(offset); // 主循环开始执行FOC和速度环 while (1) { // 根据业务逻辑运行 } }这样的好处是首次烧录程序时Flash里没有有效偏移值自动进入标定模式标定完保存后下次上电直接加载不需要每次开机都做一次机械锁定。实际上很多量产设备会在产线里做专门的标定工装通过串口下发“进入标定模式”指令标定完成后自动保存并退出。调试阶段用一个按键触发标定更顺手不改主程序结构也能灵活切换。5. 常见问题与排查技巧实录这一节的内容全是我自己调试过程中遇到并解决过的真实问题挑几条经常出现且具有迷惑性的记录下来能帮大家少走很多弯路。5.1 现象排查速查表现象排查方向处理办法六步锁定时转子乱跳电压矢量方向与电机相序不匹配检查UVW三相接线顺序锁定后计数值反复变化转子未完全稳定就读取延长锁定等待时间到1秒以上计算出的偏移角每次差异大编码器联轴器松动或计数方向翻转重新固定联轴器检查A/B相接线强拖时电流快速上升PWM占空比太大或限流失效降低锁定电压系数确认限流保护有效闭环测试时正反转力矩差很大偏移角符号错误或极对数填错重新核对极对数检查偏移角符号约定速度增加后电流振荡加剧电角度换算存在跳变检查编码器计数是否溢出引发不连续这里面最坑的是“编码器计数溢出”。如果你的定时器计数范围设置为0到ARR值自动回绕那么机械角度公式里TCNT直接从4095跳到0时换算出的角度会瞬间变化如果不做归一化处理电角度会出现一个负跳变电流环看到的是一个剧烈变化的输入自然就不稳定。我的处理办法是在获取编码器角度时把计数值转成有符号的浮点累积值而不是直接用裸TCNT做比例换算。简单做法是每次中断里计算增量和方向累加到一个int32_t全局变量用这个累加值换算机械角度就不会有回绕问题。5.2 用手转电机来检验偏置角对不对有个很简单却很实用的手动校验法分享给大家。把电机从闭环控制中摘出来只给三相逆变器一个固定开关状态让电机处于锁定状态然后用手缓慢转动转子同时观察d轴电流反馈值的变化。如果偏移角正确当你手动旋转转子时反馈电流会呈现一个标准的正弦或余弦变化且峰值出现在你预期的机械角度附近。如果波形明显畸变或相位不对多半是极对数设置错误。如果波形完全跟编码器位置对不上通常是编码器计数方向反了或者接线有误。这个方法不需要示波器串口打印速度放慢一点也能看趋势。我第一次用这个方法时很快就发现自己其中一个工程把极对数少写了一半波形频率正好差一半调整之后一切正常。5.3 一个真实案例偏移角符号写反的代价有一次我调试一台带减速箱的永磁同步电机编码器装在电机尾部前后换了三版主板都出现在某一个方向电流很大但有异响的毛病。按照前面步骤重新校准校准结果显示偏移角大约在1.9弧度附近我一开始顺手就写进了代码结果正转正常、反转明显无力。反复检查后发现减速箱在正反转时存在不同方向的回程间隙导致手动锁定和旋转验证时转子实际位置跟编码器读数之间有差异。解决办法是标定时让转子先向固定方向转半圈再锁入目标位置消除减速箱间隙后再读数。修改之后正反转力矩终于一致。这件事给我最大的启发是零位校准不只是调一个软件参数它跟整个传动链的机械特性息息相关。换机械结构、换联轴器、换减速箱之后偏移角必须重新标定不要觉得软件没动就能继续用旧参数。5.4 两点避坑技巧最后说两个代码层面的小技巧。第一点所有三角函数调用尽量使用查表法或者在定点硬件加速支持下计算否则在STM32F1这种主频不高的芯片上FOC循环里频繁计算sin/cos会占掉大量CPU时间。我一般用256点的正余弦查找表精度足够电流环使用性能余量直接提升一截。第二点标定和正式运行使用同一个电角度换算函数不要在标定时单独写一套角度计算逻辑。任何“标定简单但运行复杂”的做法都可能引入两套逻辑之间的隐性差异一旦出现莫名其妙的问题排查起来非常痛苦。统一函数、统一符号、统一归一化方式是我在多个项目里一直坚持的约定。增量式编码器的零位校准说难不难说简单也不简单。核心就是建立“编码器读数”和“转子电角度”之间的固定换算关系核心工具就是六步锁定加平均。这一套方法不挑芯片、不挑电机换到任何一款自研FOC板上都能直接用。按这个流程走一遍电机大概率能立刻转得又稳又顺。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进