STM32F4 TIM3编码器四倍频与FOC角度转速换算实战 STM32F4 系列做 FOC 电机控制时编码器负责提供转子位置和转速反馈这两项数据直接决定电流环和速度环能否正常工作。很多开发者把注意力放在电流采样和 SVPWM 上结果调试时发现电机力矩方向不对、速度环抖动、低速堵转最后定位到的问题反而是编码器角度没有换算对或者 TIM 编码器模式没有配置成四倍频。这篇博客围绕真实项目中最常见的两个需求展开一是如何用 STM32F4 的 TIM3 编码器接口读取正交编码器并得到四倍频计数二是如何把计数值换算成机械角度、电角度和实际转速并说清楚这些量和 FOC 控制之间的对应关系。1. 为什么 FOC 需要编码器角度和转速而不是只靠电流FOC 的全称是 Field Oriented Control也就是磁场定向控制。它的核心思路是把三相定子电流分解成两个正交的直流分量一个控制磁链一个控制转矩。这个分解动作依赖转子的实时位置位置不准Park 变换和逆 Park 变换都会出错。1.1 角度在 FOC 中的角色两个变换都依赖电角度FOC 控制链路通常长这样电流采样 - Clark 变换 - Park 变换 - 电流环 PI - 逆 Park 变换 - SVPWM - 三相逆变器 - 电机Clark 变换把三相电流变成I_alpha和I_beta这个变换不依赖转子位置。但接下来的 Park 变换要把静止坐标系下的I_alpha、I_beta变成旋转坐标系下的I_d、I_q这一步必须使用转子电角度。注意是电角度不是机械角度。电角度和机械角度之间存在极对数关系电角度 机械角度 * 极对数如果电机是 4 对极转子每转一圈 360 度机械角度电角度就走完了 4 个 360 度。这个换算错误是新手最常见的问题来源。使用编码器时编码器输出的是机械角度必须乘上极对数才能送给 Park 变换。1.2 转速在 FOC 中的角色速度环反馈值FOC 如果不做转速控制只需要角度就够。但实际电机控制通常要做速度闭环这时就需要一个准确的实时转速反馈。编码器测速可以基于单位时间内的脉冲数来计算也可以基于固定角度间隔的时间来计算不同方法适合不同速度区间。编码器角度和转速是一体两面同一个计数器值既可以根据当前计数算出角度也可以根据两次读数之间的差算出转速。理解这一点后后面读 TIM3 计数器时就会清楚为什么要用有符号数、为什么要考虑溢出回绕。2. 先理解 STM32F4 的定时器编码器模式和四倍频原理STM32F4 的通用定时器可以工作在编码器接口模式比如 TIM1、TIM2、TIM3、TIM4、TIM5 这些。编码器接口模式下定时器不是单纯计时而是根据两个输入通道的边沿事件自动加减计数值。2.1 增量式编码器的输出信号长什么样增量式编码器通常输出两路正交方波信号叫 A 相和 B 相。A 相和 B 相之间是 90 度相位差。电机正转时A 相超前 B 相电机反转时B 相超前 A 相。有的编码器还有 Z 相或者 Index 信号每转一圈输出一个脉冲用于绝对零位校准。两路正交信号在一个 A 相周期内会出现 4 个边沿分别是A 相上升沿A 相下降沿B 相上升沿B 相下降沿如果定时器在每一个边沿都计数那每转过一个编码器线数的物理周期计数值会增加 4 次这就是四倍频的由来。2.2 TIM 编码器模式的三种计数方式STM32 定时器编码器模式可以配置成三种计数方式配置计数行为单圈脉冲数仅在 TI1 边沿计数每个 A 相边沿变化一次计数值编码器线数仅在 TI2 边沿计数每个 B 相边沿变化一次计数值编码器线数在 TI1 和 TI2 双边缘计数A 相和 B 相每个边沿都计一次编码器线数 * 4第三种方式就是常说的四倍频。使用四倍频后分辨率提高 4 倍。比如 1000 线的编码器四倍频后每圈计数值变为 4000。也就是说每个计数值代表360 / 4000 0.09度角度分辨率明显提高。2.3 为什么推荐使用四倍频而不是单倍频四倍频不只是分辨率提高。FOC 在低速和堵转状态下转子位置变化极小如果分辨率太低位置检测在相邻采样周期内可能完全没有变化电流环就无法得到平滑的转子位置反馈。使用四倍频可以在低速段获得更细的位置阶梯。另一个原因是四倍频模式下方向和速度的判断都通过硬件完成定时器自动根据 A、B 相信号决定加到最大值方向还是减到最小值方向不需要额外软件方向判断逻辑。注意STM32F4 编码器模式下计数值是带方向的正转计数增加反转计数减少。读取计数时建议使用有符号数处理否则反转时容易在溢出边界上出错。3. 环境准备硬件和软件需要提前确认在开始写代码前建议先列清楚整个调试链路需要的材料。缺少任何一个环节后面都可能在调试时反复踩坑。3.1 硬件清单硬件用途说明STM32F4 系列开发板主控推荐带 PWM 输出和 ADC 采样的型号带增量式编码器的电机测试对象常见云台电机、伺服电机均可用电机驱动板驱动电机需要支持 3 路 PWM 或 SVPWM 输入编码器连接线信号接入需要连接 A、B、Z、电源、地串口调试工具输出调试数据用于观察角度和转速换算结果示波器或逻辑分析仪检查波形可选但排查编码器信号时非常有用编码器线数需要在上电前确认它直接影响后面的倍频数和角度换算。如果编码器铭牌信息丢失可以通过示波器观察一个机械周期内的 A 相脉冲数来估算。3.2 软件环境代码工程建议使用 STM32CubeMX 生成初始化代码再导入 Keil MDK 中编写具体逻辑。这样定时器参数配置直观不容易写错寄存器。软件工具版本建议作用STM32CubeMX6.x 以上生成初始化代码Keil MDK5.x完成编译烧录和在线调试串口调试助手任意接收串口打印的角度和转速不同版本的 STM32CubeMX 生成的代码结构略有差异但定时器编码器模式的配置入口都差不多。下面以 TIM3 为例说明。4. 使用 STM32CubeMX 配置 TIM3 编码器模式TIM3 的编码器模式需要占用两个引脚分别是 PA6TIM3_CH1和 PA7TIM3_CH2。这两个引脚接收编码器的 A 相和 B 相输出。4.1 CubeMX 中的关键参数配置在 STM32CubeMX 的 Pinout 视图中选择 TIM3然后在 Mode 下拉框中选择 Encoder Mode。常用参数配置如下参数项配置值含义Encoder ModeEncoder Mode TI1 and TI2双边缘计数即四倍频Counter Period编码器线数 * 4 - 计数器周期决定计数值范围Prescaler0预分频为 1不使用分频Input Filter0 或适当值数字滤波抗干扰ARPEEnable自动重载预装载使能Counter ModeUp计数模式编码器模式下由硬件决定方向CENEnable使能计数器Polarities视编码器方向而定通道极性和方向有关Counter Period 很关键。假设编码器是 1000 线四倍频后每圈 4000 个计数那 Counter Period 可以设为 4000 减 1也就是 3999。这样计数器从 0 增加到 3999然后回到 0。反转时从 0 减到 3999继续往 3998 方向走。注意Counter Period 设置成线数*4 - 1时计数范围是 0 到 3999恰好覆盖一圈。如果设置成 3999那么计数器值本身可以直接当作“单圈内的机械角度计数值”来使用。4.2 预分频和滤波配置的取舍预分频一般保持 0因为编码器模式下的计数精度来自对每个边沿的捕捉预分频反而会丢失位置信息。输入滤波用于躲避毛刺但会增加信号延迟。低速场景可以适当增加滤波高速场景建议关闭或设置极小的滤波值。滤波值过大会导致高速旋转时计数丢步滤波值过小则抗干扰能力弱。如果没有明显干扰可以先从 0 开始调试时再根据转速误差和毛刺情况调整。4.3 生成代码并检查工程结构CubeMX 生成代码后会在tim.c中生成MX_TIM3_Init()。此时可以在主循环前调用HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL)启动编码器接口。需要注意的是编码器接口的启动和普通 PWM 输出不同它不需要单独的 PWM 输出使能只需要启动编码器输入捕获。读计数器使用int16_t encoder_count (int16_t)__HAL_TIM_GET_COUNTER(htim3);有人会直接读TIM3-CNT这也可以但推荐使用 HAL 提供的宏。注意把计数值强转为int16_t这样反转时可以正确得到负值。5. 机械角度和电角度的换算实现编码器计数得到的是原始计数值必须经过换算才能变成角度值。5.1 机械角度的计算机械角度表示电机转子实际转过的物理角度范围是 0 到 360 度。单圈计数值为PULSE_PER_REV ENCODER_LINES * 4当前机械角度可以通过取余运算得到float mechanical_angle (float)(encoder_count % PULSE_PER_REV) * 360.0f / PULSE_PER_REV;如果计数值是负数取余结果也是负数需要做一次归一化if (mechanical_angle 0.0f) { mechanical_angle 360.0f; }5.2 电角度的计算电角度等于机械角度乘以极对数float electrical_angle mechanical_angle * MOTOR_POLE_PAIRS;如果电机是 4 对极那么电角度范围会到 1440 度对 Park 变换来说角度只需要模 360 度electrical_angle fmodf(electrical_angle, 360.0f); if (electrical_angle 0.0f) { electrical_angle 360.0f; }5.3 为什么需要角度零位对齐增量式编码器没有绝对角度信息上电后计数器默认从 0 开始。但电机转子不可能刚好停在零位。因此实际项目中必须做零位校准。常见做法是给电机通一个已知方向的定角度电流矢量让转子强制运行到一个固定位置把这个位置计数值记录下来作为角度零点。另一种做法是使用编码器的 Z 相脉冲在检测到 Z 相信号时把计数值清零或捕获当前值。void encoder_zero_calibration(void) { // 记录当前计数值作为零偏 int16_t raw_zero (int16_t)__HAL_TIM_GET_COUNTER(htim3); g_encoder_zero_offset raw_zero; } int16_t encoder_get_raw_count(void) { int16_t raw (int16_t)__HAL_TIM_GET_COUNTER(htim3); return raw - g_encoder_zero_offset; }每次计算角度前先减掉零偏就能得到一个相对零位的角度值。这个设计在后面 FOC 调试时非常有用。5.4 角度换算完整代码示例假设编码器线数为 1000极对数为 4#define ENCODER_LINES 1000 #define PULSE_PER_REV (ENCODER_LINES * 4) #define MOTOR_POLE_PAIRS 4 volatile int16_t g_encoder_zero_offset 0; float encoder_get_mechanical_angle_deg(void) { int16_t raw (int16_t)__HAL_TIM_GET_COUNTER(htim3); int16_t count raw - g_encoder_zero_offset; float angle (float)(count % PULSE_PER_REV) * 360.0f / PULSE_PER_REV; if (angle 0.0f) { angle 360.0f; } return angle; } float encoder_get_electrical_angle_deg(void) { float mech_angle encoder_get_mechanical_angle_deg(); float elec_angle fmodf(mech_angle * MOTOR_POLE_PAIRS, 360.0f); if (elec_angle 0.0f) { elec_angle 360.0f; } return elec_angle; }这段代码是整个编码器驱动中最核心的部分。后续做 FOC 时只需要在每次电流环执行前调用encoder_get_electrical_angle_deg()得到电角度再换成弧度制即可。6. 基于 TIM 计数的测速方法测速方法有很多种常见的是 M 法、T 法和 M/T 法。6.1 M 法单位时间内统计脉冲增量M 法是在固定时间窗口内统计编码器计数值的变化量然后用公式计算出转速。float speed_rpm (float)(current_count - last_count) * 60.0f / PULSE_PER_REV / delta_time_s;其中current_count - last_count是时间窗口内的脉冲增量delta_time_s是两次采样之间的时间60.0f是把每秒圈数转成每分钟圈数M 法适合高速场景。转速越高单位时间内脉冲越多误差越小。低速时脉冲数量少量化误差会很明显。6.2 T 法测量固定角度间隔的时间T 法通过测量相邻两个脉冲之间的时间来计算转速适合低速。实现时可以使用另一个定时器测量时间间隔。T 法在极低速时精度高但高速时时间间隔太短计时误差变大。6.3 项目中的实测建议如果转速范围跨越较宽可以使用 M/T 法结合单位时间内的脉冲数和最后一个脉冲的细分时间来提高精度。但对大多数 STM32F4 学习项目来说先使用 M 法就能满足基本调试需求。测速周期要和 FOC 控制周期匹配。通常电流环运行在 10 kHz 到 20 kHz速度环运行在 1 kHz 附近。如果在速度环中断里测速意味着每 1 毫秒读取一次计数器低速时这个时间窗口内的脉冲量可能只有个位数速度波动就会比较大。实测定速代码如下uint32_t g_last_encoder_count 0; float g_speed_rpm 0.0f; void speed_calc_task(void) { uint32_t current_count (uint16_t)__HAL_TIM_GET_COUNTER(htim3); uint32_t delta_count (uint32_t)(current_count - g_last_encoder_count); g_last_encoder_count current_count; float delta_time_s 0.001f; // 如果速度环周期是 1ms g_speed_rpm (float)delta_count * 60.0f / PULSE_PER_REV / delta_time_s; }注意 count 差值要使用无符号数计算避免回绕变成负值。如果计数值范围覆盖一圈可以放心用无符号减法处理回绕。7. FOC 调试中如何验证编码器角度是否正常编码器程序写完后不能直接上强电跑 FOC。先做角度开环验证确认方向和角度换算都正确。7.1 手动旋转验证角度连续性用手缓慢旋转电机轴通过串口打印机械角度和电角度。正常现象是机械角度在 0 到 360 度之间连续变化电角度变化速度是机械角度的极对数倍反向旋转时角度递减并且不会出现突然从 359 跳到 0 以外的异常值如果角度在某个点跳变比如从 400 直接变成 0说明取模运算或计数值类型有问题。如果角度变化方向与实际旋转方向相反说明 AB 相接反了可以把 TIM3 CH1 和 CH2 的极性配置调整或者交换编码器 A、B 接线。7.2 用固定角度指令法确认电角度方向给定一个固定电角度给电机通一个固定电流矢量。例如设置电角度为 0 度时Iq 给定正电流Id 给定 0。正常情况下电机会旋转到该角度对应的位置并停住。手动旋转电机时如果电机会被拉回说明角度方向和电流矢量关系正确。如果电机越推越远说明电角度方向反了。解决方法是交换编码器 A、B 相或者在代码里把电角度取反float electrical_angle 360.0f - electrical_angle;7.3 串口打印调试信息的关键点串口打印频率建议设成 50 Hz 到 100 Hz既能观察角度变化趋势又不会占用太多 CPU。打印内容至少包括原始计数值、机械角度、电角度、转速四个量方便对照分析。raw:1234 mech:111.06 elec:84.24 speed:120.33 rpm如果看到 raw 值始终不变说明编码器信号没有进入定时器先检查硬件接线和引脚复用配置。8. 常见坑与排查链路这部分整理调试过程中最常遇到的问题每一条都对应具体现象和解决路径。8.1 角度反向或方向不一致现象电机正转时串口打印的角度反而递减或者 FOC 给定正向电流时电机反向转动。可能原因编码器 A、B 相接反TIM 通道极性与实际信号不匹配零位偏移设置错误排查顺序查看串口打印的原始计数值手动正转看计数是否增加。若不增加交换 A、B 接线或者在 CubeMX 中修改 CH1 或 CH2 的极性。若增加方向正确但电角度方向不对检查极对数是否填对。8.2 角度在某个点跳变现象机械角度旋转过程中从 350 度附近直接跳到 10 度或者从 10 度跳到 350 度。可能原因Counter Period 设置错误导致计数范围与单圈不匹配取模使用了错误的除数计数值强转有符号数后溢出检查方式打印原始计数值确认一圈内是否严格按照 0 到 3999 循环如果一圈脉冲数超过 3999说明 Counter Period 需要重新计算检查是不是在计算角度时用int8_t或uint8_t保存了计数值8.3 转速波动大低速尤其明显现象速度环给定恒定转速时实际转速在小范围内上下波动而且波动幅度随速度降低而变大。可能原因测速时间窗口太短编码器计数毛刺导致增量不稳定速度环周期过短但滤波不足处理建议使用 M/T 法或延长测速时间窗口对速度值做一阶低通滤波g_speed_filtered g_speed_filtered * 0.9f g_speed_rpm * 0.1f;检查编码器信号是否有毛刺必要时在 CubeMX 中增加输入滤波8.4 上电后角度随机无法复现现象每次上电后的角度初始值都不一样即使转子停在同一个位置也是如此。这是增量式编码器的正常现象不是 bug。处理方式是在上电后完成零位校准使用 Z 相脉冲或者强制电流对齐。8.5 四倍频后计数值不是线数乘以 4现象手动旋转一圈计数值变化量不是预期值。可能原因CubeMX 中编码器模式选成了只有 TI1 或 TI2 计数编码器实际线数和铭牌不一致高速旋转时丢步说明滤波值太大或计数器配置过快解决办法在 CubeMX 中确认 Encoder Mode 是 Encoder Mode TI1 and TI2用示波器查看 A、B 相的实际脉冲数低速手摇一圈核对计数值排查表格整理如下问题现象可能原因检查方式处理建议计数值不增加接线错误或未使能编码器检查引脚电平、启动函数重新接线或调用HAL_TIM_Encoder_Start方向相反A/B 接反手动正转观察计数值交换 A/B 或修改极性角度跳变计数器范围不匹配打印原始计数值修正 Counter Period转速波动测速窗口短查看增量值延长窗口或加滤波上电角度不定增量编码器无绝对位置检查是否有 Z 相增加零位校准流程9. 最佳实践与扩展方向9.1 角度数据更新频率要高于电流环频率FOC 电流环运行时需要每一个控制周期都有最新的电角度。建议在主控制中断里首先读取编码器计数值计算角度再去执行电流采样和 Park 变换。不要在控制中断外另起线程读取容易造成角度和控制周期不对齐。9.2 零位校准建议做成独立模块把零位校准做成独立函数允许通过串口指令触发。调试电机时经常需要反复校准如果写死在初始化函数里每次测试都要重新编译烧录效率很低。void motor_calibrate_zero_position(void) { // 钳位转子到固定位置 set_iq_open_loop(0.5f, 0.0f); HAL_Delay(500); set_iq_open_loop(0.0f, 0.0f); // 记录当前编码器值作为零偏 g_encoder_zero_offset (int16_t)__HAL_TIM_GET_COUNTER(htim3); }9.3 编码器线数和极对数要先确认接线前建议写好一份电机参数表至少包含编码器线数、极对数、额定电压、额定电流、额定转速。这几个参数贯穿整个 FOC 调试过程任何一步填错现象都会很诡异。参数示例值编码器线数1000四倍频后单圈脉冲数4000极对数4额定转速3000 rpm9.4 扩展方向从增量编码器到磁编码器增量编码器需要上电校准这在很多工业应用里不够方便。现在很多电机驱动板开始使用 SPI 接口的磁编码器比如 MT6701、AS5047P。磁编码器可以直接输出绝对角度省去零位校准流程。如果后续项目转向磁编码器需要注意 SPI 读取频率和角度数据格式的差异。但本文中的机械角度、电角度换算逻辑依然适用只是在数据源上从TIM3-CNT换成 SPI 读取的 14 位或 16 位绝对角度值。9.5 练习建议如果刚接触 FOC 编码器调试建议按下面的顺序走一遍先不接电机只用手旋转编码器用串口观察计数值变化。确保四倍频计数值和角度换算正确。再接入电机做开环角度控制验证电角度方向。最后才进入电流环和速度环调试。这个顺序能帮你把编码器问题从整个 FOC 链路中剥离出来避免一上来就面对“电流波形乱、力矩方向不对、转速振荡”多个问题混合在一起。编码器角度和转速看似只是 FOC 中的一个输入环节但它决定了电流环的 Park 变换准确性、速度环的反馈质量、以及低速带载的稳定性。花时间把 TIM3 四倍频、机械角度换算、电角度换算和测速方法弄扎实后续不管是调电流环还是速度环都会顺手很多。