
写 FOC 电机控制时角度和速度这两个量就像控制器的“眼睛”。角度算不准电流环解耦就是错的速度算不准速度环就是抖的。很多初学者拿到一套 STM32F4 的电机开发板跟着教程调通了开环一到闭环就出问题很大概率是编码器这一环没吃透TIM3 编码器模式怎么配四倍频到底做了什么机械角度和电角度怎么换算速度又该怎么测本文围绕这些核心问题从编码器工作原理讲到 STM32F4 上的实战代码全程使用 TIM3 编码器模式进行讲解涵盖 CubeMX 配置、角度计算、M 法测速、零点对齐和常见坑点适合正在学习 FOC 的开发者参考。1. FOC 控制与编码器的关系1.1 FOC 控制到底在控制什么FOC全称 Field-Oriented Control中文常叫“磁场定向控制”或“矢量控制”。它的核心思路是把三相交流电机内部的定子电流分解成两个正交的直流分量一个负责产生磁场的“d 轴电流”一个负责产生力矩的“q 轴电流”。通过这种解耦三相永磁同步电机PMSM和无刷直流电机BLDC可以像直流电机一样用电流环、速度环、位置环进行高性能控制。这个解耦过程不是凭空完成的它需要实时知道转子磁场的位置。因为 d 轴和 q 轴是建立在转子磁场方向上的坐标系如果不知道转子当前位置就无法把三相电流准确地投影到这个旋转坐标系中。在 FOC 算法里从三相静止坐标系变换到两相静止坐标系是 Clarke 变换从两相静止坐标系变换到旋转坐标系是 Park 变换。Park 变换需要输入一个角度这个角度就是电角度而电角度最终来源于编码器反馈的机械角度。所以FOC 控制系统中最基本的信息链路是编码器 → 机械角度 → 电角度 → Park 变换 / SVPWM → 三相电压输出只要编码器角度误差偏大Park 变换出来的 d/q 轴电流就不再是“真”的 d/q 轴电流电流环控制效果就会明显退化严重时电机噪声大、发热高、甚至失步。1.2 编码器在 FOC 中的三个关键作用编码器在 FOC 系统里的作用可以精简成三个词角度反馈、速度反馈、相序判断。角度反馈是给电流环用的。FOC 的电流环需要知道电角度才能把采集到的相电流转换到旋转坐标系。速度反馈是给速度环用的。速度环通常位于电流环外层它的输出是 q 轴指令电流速度反馈精度直接影响转速稳态误差和动态响应。相序判断则体现在电机能不能正常启动上如果编码器正反转方向和电机绕组相序不匹配FOC 会直接反转或堵转这是新手非常容易踩的坑。值得注意的是FOC 对编码器的最低分辨率要求并不算苛刻。只要电角度分辨率能到几度以内速度环性能一般就能接受。但是编码器反馈延迟、零点漂移和信号毛刺对性能的影响却是致命的这也是本文后续要重点展开的部分。1.3 正交增量式编码器的工作方式电机控制中常用的编码器有增量式编码器、绝对值编码器和磁编码器。增量式编码器因为性价比高、结构简单在小体积电机和低成本驱动方案中应用最广。增量式编码器输出两路正交方波信号一般称为 A 相和 B 相。电机每转一个单位角度A 相和 B 相分别输出一定数量的方波脉冲两路信号在相位上相差 90°。正是这个 90° 相位差让控制器可以同时判断转速和转向A 相超前 B 相 90° 时正转B 相超前 A 相 90° 时反转。除了 A 和 B 两相很多增量式编码器还有 Z 相零位信号。Z 相每圈输出一个脉冲用于确定绝对零位。在 FOC 项目中Z 相可以作为编码器零点对齐的参考但很多简化的驱动方案不使用 Z 相而是通过软件开环对齐的方式确定零点后面会详细说明。2. 编码器线数、四倍频与 TIM 编码器模式2.1 编码器线数与单圈分辨率编码器“线数”这个词指的是编码器每旋转一圈A 相或 B 相输出的脉冲个数。常见的线数有 250 线、500 线、1000 线、2000 线、2500 线、4096 线等。需要注意线数不等于最终的位置分辨率因为控制器可以对 A、B 两路的上升沿和下降沿同时计数从而把每圈分辨率提升到线数的 4 倍。单圈分辨率 编码器线数 × 4这个“×4”的过程就是大家常说的“四倍频”。例如一个 1000 线编码器四倍频后每圈可以产生 4000 个计数脉冲。对 7 对极电机来说机械角度每变化 0.09°电角度就变化 0.63°足以满足大部分 FOC 应用。2.2 四倍频原理四倍频的原理并不复杂。A、B 两路正交信号在一个完整周期内有 4 个电平跳变边沿A 上升沿、A 下降沿、B 上升沿、B 下降沿。如果控制器对这 4 个边沿都进行计数那么每个编码器线数周期就能计 4 个数相当于把分辨率放大到原来的 4 倍。实际判断转向时控制器需要在这 4 个跳变边沿上检测另一路信号的电平状态。例如在 A 相上升沿时检测 B 相是高电平还是低电平B 相电平决定当前是正转还是反转。这个过程如果用纯 GPIO 中断来实现也可以但会占用大量 CPU 资源因为高速旋转时中断频率很高。更好的办法是利用 STM32 定时器自带的编码器接口硬件自动完成方向判断和计数。2.3 STM32F4 定时器编码器模式为何适合四倍频STM32F4 系列的大多数高级和通用定时器都支持编码器接口模式。定时器把这个本来需要逐边沿判断逻辑的活儿集成进了硬件CPU 只需要定期读取计数器寄存器TIMx-CNT就能获得带方向的位置信息。在编码器接口模式下定时器的 CH1 和 CH2 分别对应编码器的 A、B 相。定时器可以配置为三种计数模式之一TI1 模式只在 A 相边沿计数相当于二倍频TI2 模式只在 B 相边沿计数相当于二倍频TI1 TI2 模式在 A、B 两相的所有边沿计数相当于四倍频。FOC 项目中使用最广的就是 TI1 TI2 模式也就是 CubeMX 里的Encoder Mode: TI1 and TI2。硬件根据两路信号的相位关系自动决定计数器加计数还是减计数软件完全不需要额外判断方向。在 STM32F4 中TIM3 的 CH1 和 CH2 通常映射到 PA6 和 PA7这是非常常用的编码器输入引脚。不过不同封装、不同型号引脚可能不同使用前需要查阅对应芯片的数据手册或 CubeMX 的引脚分配。2.4 TIM3 编码器模式的 CubeMX 配置下面以 STM32F407VET6 为例演示 CubeMX 中 TIM3 编码器模式的具体配置步骤。第一步在 Pinout Configuration 视图中选中 TIM3将 Channel1 和 Channel2 的模式都设置为Encoder Mode。这一步很关键Channel1 和 Channel2 必须同时配置为 Encoder Mode否则编码器模式不会生效。第二步在 TIM3 的 Parameter Settings 中设置以下参数参数推荐值说明Prescaler0编码器模式要求预分频为 0否则丢脉冲Counter Mode由硬件自动处理编码器模式下方向由硬件判断Counter Period6553516 位定时器最大计数值充分利用计数范围Encoder ModeTI1 and TI2四倍频工作模式IC1/IC2 Filter0 或 10数字输入滤波抗毛刺高速时建议 0IC 滤波器是一个值得关注的参数。如果电机 PWM 对编码器信号造成干扰可以通过增大输入滤波值来滤除毛刺。但滤波会引入信号延迟编码器频率越高延迟造成的影响越明显。因此实际项目中滤波值需要根据编码器输出频率和现场干扰情况综合调整。第三步生成代码后在用户代码中调用HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); __HAL_TIM_SET_COUNTER(htim3, 0);HAL_TIM_Encoder_Start的作用是使能 TIM3 的编码器接口TIM_CHANNEL_ALL表示两个通道同时使能。第二行把计数器清零保证角度计算从 0 开始。3. 角度计算机械角度与电角度3.1 机械角度的获取编码器计数器的值本身并不直接等于角度需要结合编码器四倍频后的单圈总脉冲数进行换算。机械角度的换算公式如下机械角度 计数值 / 单圈总脉冲数 × 360°其中单圈总脉冲数 编码器线数 × 4。假如编码器是 1000 线四倍频后每圈 4000 个脉冲那么计数器的值与角度的对应关系就是每 1 个脉冲对应 0.09° 机械角度。实际编码器安装后机械零点一般不是 0°。也就是说当电机处于我们认定的机械零位时计数器不一定等于 0。因此在软件上通常要定义一个零点偏移量zero_offset每次读取实际角度后先减去这个偏移量再做换算。下面是一个通用性较好的机械角度读取函数// 文件路径Core/Inc/encoder.h #ifndef ENCODER_H #define ENCODER_H #include main.h #define ENCODER_LINES 1000u // 编码器线数 #define ENCODER_PPR (4u * ENCODER_LINES) // 四倍频后每圈脉冲数 #define MOTOR_POLE_PAIRS 7u // 电机极对数 float Encoder_GetMechanicalAngle_Deg(void); float Encoder_GetElectricalAngle_Deg(void); float Encoder_GetSpeed_RPM(void); void Encoder_AlignToZero(float electrical_angle_deg); #endif// 文件路径Core/Src/encoder.c #include encoder.h static int32_t s_zero_offset 0; float Encoder_GetMechanicalAngle_Deg(void) { // 使用 int16_t 处理有符号角度兼容正反转 int16_t cnt (int16_t)(__HAL_TIM_GET_COUNTER(htim3) - s_zero_offset); float deg (float)cnt * 360.0f / (float)ENCODER_PPR; if (deg 0.0f) { deg 360.0f; } return deg; }这里使用(int16_t)强转的目的是把计数器的无符号形式转成有符号形式从而让反向旋转时角度呈现负值便于后续归一化。当然这种写法成立的前提是ENCODER_PPR小于 32768即单圈总脉冲数不能超过int16_t的表示范围。常见 1000 线、2000 线、2500 线、4096 线编码器四倍频后为 4000~16384符合这个条件。3.2 电机极对数与电角度机械角度是转子实际转过的物理角度但在 FOC 算法中Park 变换需要的不是机械角度而是电角度。电角度和机械角度之间相差一个“极对数”的倍数电角度 机械角度 × 极对数“极对数”这个概念新手容易混淆。一个电机的极数是 N那么极对数就是 N / 2。例如一个 14 极电机极对数是 7。每对磁极在空间上对应 360° 电角度。转子每旋转一圈定子磁场经历 7 个完整的电周期也就是电角度变化了 7 × 360°。极对数是 FOC 参数配置中最容易出错的点之一。极对数填错时电机电流环虽然看起来在运行但输出力矩会明显波动电机噪声大、效率低严重时转子根本无法平稳旋转。如果电机是向厂家定制或自己绕制的最好通过电机说明书或反电动势波形确认极对数不要依赖猜测。3.3 电角度的归一化处理由于电角度是机械角度的多倍计算得到的数值经常会超过 360°。而角度本身是周期性的超过 360° 需要映射回 0°~360° 范围否则后续的 sin/cos 计算会浪费精度。归一化可以使用fmodf实现float Encoder_GetElectricalAngle_Deg(void) { float elec_deg Encoder_GetMechanicalAngle_Deg() * (float)MOTOR_POLE_PAIRS; elec_deg fmodf(elec_deg, 360.0f); if (elec_deg 0.0f) { elec_deg 360.0f; } return elec_deg; }有些工程习惯把电角度换算成弧度再传给 FOC 算法两种方式等价。推荐的实践是底层驱动统一输出角度值FOC 算法内部决定用角度还是弧度。这样分层清晰也方便调试。3.4 编码器零点与 FOC 零点的对齐机械角度到电角度的换算公式成立的前提是编码器零点与电机反电动势零点一致。但实际安装时编码器机械零位通常和电机电气零位没有必然关系。如果不做零点对齐计算出的电角度会有固定偏移FOC 就无法稳定工作。工程中最常用的对齐方法是“开环强制对齐”给电机施加一个已知方向和大小的电流矢量例如让 q 轴电流为固定值d 轴电流为 0。等待转子稳定锁死此时转子会被拉到电角度某个固定位置。读取当前编码器计数值把它作为零点偏移量保存下来。代码示例思路如下void Encoder_AlignToZero(float electrical_angle_deg) { // 1. 开环控制电机让转子锁到指定电角度 // 具体实现和电机驱动相关例如 // Motor_OpenLoopSetElectricalAngle(electrical_angle_deg, 0.2f); HAL_Delay(300); // 等待转子稳定 // 2. 读取当前计数器值作为零点偏移 s_zero_offset __HAL_TIM_GET_COUNTER(htim3); }为了消除电流力矩波动造成的误差可以在对齐过程中采集多次计数值取平均。更好的做法是分别对齐到多个不同电角度然后根据多组数据进行拟合不过对小项目来说一次平均已经足够。4. 转速计算M 法测速实战4.1 M 法测速原理FOC 速度环需要实时转速反馈。最常用且最容易实现的方法是 M 法测速也叫“测频法”。M 法的原理是在固定的时间窗口 T 内统计编码器计数器的变化量 ΔN然后换算成转速。转速计算公式如下转速转/秒 ΔN / 单圈总脉冲数 / T 转速转/分 转速转/秒 × 60M 法在高速段精度很好因为单位时间内脉冲数多量化误差小。但在低速段如果时间窗口内只有几个脉冲甚至没有脉冲速度更新就会变得迟钝精度也差。对于 FOC 控制通常速度环执行周期就是 1ms~10msM 法已经能满足大部分中高速应用。4.2 核心代码实现下面是通过 M 法实现测速的完整示例核心思路是每次调用时读取当前计数值和上一次计算差值再除以两次调用的时间间隔float Encoder_GetSpeed_RPM(void) { static int16_t s_last_cnt 0; static uint32_t s_last_tick 0; int16_t cur_cnt (int16_t)__HAL_TIM_GET_COUNTER(htim3); uint32_t cur_tick HAL_GetTick(); int16_t diff (int16_t)(cur_cnt - s_last_cnt); uint32_t dt cur_tick - s_last_tick; s_last_cnt cur_cnt; s_last_tick cur_tick; if (dt 0) { return 0.0f; } // 每秒转速 diff / ENCODER_PPR / (dt / 1000) float speed_rps (float)diff / (float)ENCODER_PPR * 1000.0f / (float)dt; return speed_rps * 60.0f; }这里有一个细节值得注意__HAL_TIM_GET_COUNTER(htim3)返回的是uint16_t类型把它强转成int16_t后即使计数器发生了回绕差值diff依然能正确表示真实脉冲变化。这个技巧在处理 16 位定时器回绕时非常关键也是新手最容易写错的地方。如果直接使用无符号相减再转浮点会遇到两个问题一是反向旋转时差值是负数无符号运算会得到一个很大的正数二是正转跨越 65535 回绕到 0 时直接相减会得到一个负的脉冲差。使用int16_t强转后这两个问题都能自动解决因为它相当于把计数器映射到了 -32768~32767 的环形空间。4.3 溢出与方向处理M 法测速的溢出问题核心要点是确保相邻两次采样之间计数器的变化不超过 32768 个脉冲。大多数情况下这个条件都成立因为 32768 个脉冲意味着电机在 1ms 内至少转了好几圈已经远超一般电机应用范围。还需要注意符号问题。diff大于 0 表示正转小于 0 表示反转。转速函数直接返回带符号的 RPM 值速度环可以据此判断当前是在加速还是减速、正转还是反转。4.4 速度滤波原始转速数据往往带有量化噪声直接送速度环会导致电流指令波动进而产生明显的电机噪声。常用做法是在速度环最外层加一阶低通滤波#define SPEED_LPF_ALPHA 0.1f float Speed_LowPassFilter(float raw_speed) { static float filtered_speed 0.0f; filtered_speed (raw_speed - filtered_speed) * SPEED_LPF_ALPHA; return filtered_speed; }这个滤波器的系数SPEED_LPF_ALPHA需要根据控制周期和期望的截止频率调整。系数越小滤波越强但动态响应也越慢。实际调试中可以从 0.05~0.2 之间尝试观察速度环阶跃响应后再微调。5. 完整工程示例与运行验证5.1 工程目录结构下面给出一个基于 STM32CubeMX 生成的 Keil 5 工程示例目录结构如下Project/ ├── Core/ │ ├── Inc/ │ │ ├── encoder.h │ │ ├── main.h │ │ └── usart.h │ └── Src/ │ ├── encoder.c │ ├── main.c │ ├── usart.c │ └── system_stm32f4xx.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ └── MDK-ARM/ ├── Project.uvprojx └── ...这个工程里TIM3 用于编码器输入USART1 用于串口打印调试信息。5.2 编码器驱动完整代码为了让示例更完整把编码器驱动合并为一个模块。文件encoder.c包含初始化、机械角度、电角度、转速读取和零点对齐函数。#include encoder.h static int32_t s_zero_offset 0; void Encoder_Start(void) { HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); __HAL_TIM_SET_COUNTER(htim3, 0); s_zero_offset 0; } float Encoder_GetMechanicalAngle_Deg(void) { int16_t cnt (int16_t)(__HAL_TIM_GET_COUNTER(htim3) - s_zero_offset); float deg (float)cnt * 360.0f / (float)ENCODER_PPR; if (deg 0.0f) { deg 360.0f; } return deg; } float Encoder_GetElectricalAngle_Deg(void) { float elec_deg Encoder_GetMechanicalAngle_Deg() * (float)MOTOR_POLE_PAIRS; elec_deg fmodf(elec_deg, 360.0f); if (elec_deg 0.0f) { elec_deg 360.0f; } return elec_deg; } float Encoder_GetSpeed_RPM(void) { static int16_t s_last_cnt 0; static uint32_t s_last_tick 0; int16_t cur_cnt (int16_t)__HAL_TIM_GET_COUNTER(htim3); uint32_t cur_tick HAL_GetTick(); int16_t diff (int16_t)(cur_cnt - s_last_cnt); uint32_t dt cur_tick - s_last_tick; s_last_cnt cur_cnt; s_last_tick cur_tick; if (dt 0) { return 0.0f; } float speed_rps (float)diff / (float)ENCODER_PPR * 1000.0f / (float)dt; return speed_rps * 60.0f; } void Encoder_AlignToZero(float electrical_angle_deg) { // 开环给电机施加指定电角度电流矢量使转子稳定到该位置 // Motor_SetOpenLoopCurrent(electrical_angle_deg, 0.3f); HAL_Delay(300); s_zero_offset __HAL_TIM_GET_COUNTER(htim3); }Encoder_AlignToZero中的电机开环控制部分和具体驱动强相关这里使用注释占位。实际项目中需要把电机控制代码接入这个位置。5.3 主循环逻辑主函数中完成系统初始化和编码器启动后循环读取角度和转速并通过串口输出。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM3_Init(); Encoder_Start(); while (1) { float mech_deg Encoder_GetMechanicalAngle_Deg(); float elec_deg Encoder_GetElectricalAngle_Deg(); float speed_rpm Encoder_GetSpeed_RPM(); printf(Mech: %.2f deg, Elec: %.2f deg, Speed: %.1f RPM\r\n, mech_deg, elec_deg, speed_rpm); HAL_Delay(10); } }如果使用串口打印需要确保在usart.c中已经重定向fputc函数这样printf才能将格式化字符串输出到串口。这属于基础配置不再展开。5.4 预期输出手动缓慢转动电机轴时串口输出大致如下Mech: 12.34 deg, Elec: 86.38 deg, Speed: 12.5 RPM Mech: 15.67 deg, Elec: 109.69 deg, Speed: 18.2 RPM Mech: 20.01 deg, Elec: 140.07 deg, Speed: 25.6 RPM观察电角度可以发现机械角度增长 1°电角度增长 7°本例极对数为 7符合电角度等于机械角度乘极对数的规律。如果出现角度跳变、速度抖动或计数方向相反等问题可以对照下一节的排查表处理。6. 常见问题与排查思路编码器调试过程中遇到的问题很多都有固定的排查模式。下面整理了一份实际问题清单方便对照处理。问题现象常见原因解决思路TIM3 计数器一直为 0编码器 AB 信号未接入TIM3 通道未配置为 Encoder Mode用示波器检查 PA6/PA7 波形CubeMX 中确认 CH1/CH2 均配置为 Encoder Mode电机正转但计数方向相反A、B 相接反交换 A、B 接线或者修改 IC1Polarity/IC2Polarity角度跳变到负值零点偏移未处理ENCODER_PPR超过 int16_t 范围增加零点偏移换用 32 位计数方案转速数值在 0 附近来回跳编码器信号毛刺机械抖动测速周期过短增大 IC 输入滤波值增加速度低通滤波适当延长测速周期FOC 电流抖动、电机噪声大极对数设置错误零点偏移未校准确重新确认极对数重做零点对齐并观察电角度波形高速时角度误差突然变大编码器信号频率超过定时器计数能力输入滤波延迟过大计算最高编码器频率 线数 × 4 × 最高转速(RPS)减小输入滤波值确认 TIM 时钟配置Keil5 烧录程序 STM32F4 失败设备型号选错Flash 下载算法缺失SWD 接线问题在 Target Options 中选择对应芯片型号在 Flash Download 中添加 STM32F4xx Flash 算法检查 SWDIO/SWCLK 接线还有一个值得特别提示的坑编码器信号属于高速方波信号和电机驱动桥的高压、大电流回路在布局上要尽量分离。如果布线不规范编码器信号很容易被 PWM 噪声干扰导致计数毛刺这类问题通过软件滤波只能缓解不能根治最好从硬件上解决。7. 工程最佳实践与后续建议7.1 编码器与电机选型的搭配编码器线数并不是越高越好。线数越高单圈分辨率越高但信号频率也越高对输入滤波、信号隔离、导线屏蔽的要求就相应提升。对于中小功率 BLDC/PMSM 的 FOC 驱动1000 线到 2500 线是比较常见的选型范围。如果电机空间有限、又希望上电就能知道绝对位置也可以考虑 SPI/I2C 接口的磁编码器它们的安装方式和使用逻辑与正交增量式编码器差异很大需要单独适配。7.2 零点标定流程零点标定建议放在整机出厂校准流程中而不是每次上电都做。常见的工程做法是上电后首先开环给一个固定电角度电流矢量转子稳定后记录编码器偏移。将偏移写入 Flash 或 EEPROM下次开机直接加载。如果标定过程中发现编码器安装位置发生机械变化需要重新标定。零点偏移和电角度计算是两个独立的问题偏移解决的是“零点在哪”极对数解决的是“角度放大多少倍”。把这两个参数分离管理会减少很多调试困惑。7.3 从开环到闭环的调试路径如果你正在从零调 FOC建议遵循“开环 → 角度 → 电流 → 速度”的顺序逐步推进。第一步先让电机开环旋转确认驱动板和电机本体没问题。第二步把编码器角度读准用手转动电机用示波器或上位机观察机械角度与电角度波形是否平滑。第三步接入电流环重点看 d/q 轴电流是否能解耦是否出现高频振荡。第四步再加速度环此时编码器测速的稳定性和速度滤波时间常数会明显影响系统表现。建议在每一步都加上控制变量改动一个参数后只观察一个指标不要同时调整多个参数否则出了问题难以定位。7.4 后续可以继续深入的方向本文使用 TIM3 编码器模式完成了四倍频、机械角度、电角度和 M 法测速这是 FOC 中编码器应用的核心闭环。接下来如果你时间充裕可以继续研究 T 法测速和 M/T 法测速它们在低速段的性能会更好也可以研究基于观测器的无感 FOC 方案其中滑模观测器和龙贝格观测器是常见的两条路线。不过无论走哪条路线角度和速度的可信度永远是第一位的。先把编码器反馈做扎实后面的控制算法才谈得上性能。