用 STM32 的 TIM 编码器模式做高分辨率测速:溢出处理与低速抖动抑制的工程实现

在电机控制、机器人关节、云台等应用中,测速精度直接影响控制性能。STM32 的定时器编码器模式可以硬件自动计数正交编码器的脉冲,配合高线数编码器能获得很高的分辨率。但实际工程中,计数器溢出和低速抖动是两个绕不开的坑。本文从原理到代码,给出完整的解决方案。

1. 编码器模式原理回顾

STM32 的 TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 等高级/通用定时器支持编码器接口模式。当配置为编码器模式时:

  • 编码器 A、B 相分别接到 TIMx_CH1、TIMx_CH2。
  • 硬件根据两相边沿自动增减 CNT,无需 CPU 干预。
  • 计数方向由 A、B 相位关系决定,CNT 在 0~ARR 之间循环。
  • 常用 4 倍频(TI1 和 TI2 都计数),分辨率 = 编码器线数 × 4。

例如 1000 线编码器,4 倍频后每转 4000 个计数。若定时器 ARR=65535,则每转计数远小于 ARR,不会溢出;但若编码器线数很高或需要长时间累积,溢出就不可避免。

2. 溢出处理:扩展计数器位宽

2.1 为什么需要溢出处理

假设每 10ms 读取一次速度,电机高速旋转时 10ms 内可能产生超过 65535 个计数,导致 CNT 回绕,直接读 CNT 会得到错误的小值。即使不溢出,频繁读取也会丢失计数。

2.2 硬件方案:利用溢出中断累加

最可靠的方法是开启定时器更新中断,在中断中记录溢出次数,将 16 位 CNT 扩展为 32 位或 64 位。

// 全局变量
volatile int32_t encoder_overflow = 0;   // 溢出次数(有符号)
volatile uint16_t last_cnt = 0;

// TIM 更新中断服务函数
void TIM2_IRQHandler(void)
{
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
    {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
        // 根据计数方向判断溢出方向
        if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2))
            encoder_overflow--;
        else
            encoder_overflow++;
    }
}

2.3 软件读取:原子操作获取总计数

读取时需保证 encoder_overflow 和 CNT 的一致性。推荐先关中断再读:

int32_t Encoder_GetTotalCount(void)
{
    uint16_t cnt;
    int32_t ovf;
    __disable_irq();
    cnt = __HAL_TIM_GET_COUNTER(&htim2);
    ovf = encoder_overflow;
    __enable_irq();
    return (ovf * 65536) + cnt;
}

2.4 测速计算

在固定周期 T(如 10ms)内读取两次总计数,差值即为增量:

int32_t delta = Encoder_GetTotalCount() - last_total;
last_total = Encoder_GetTotalCount();
float speed_rpm = (delta / (4.0f * ENCODER_LINES)) * (60.0f / T);

3. 低速抖动抑制:动态滤波与阈值

3.1 抖动来源

低速时,编码器脉冲稀疏,一个采样周期内可能只有 0~2 个脉冲,量化误差导致速度值剧烈跳动。此外,机械振动、电磁干扰也会引入噪声。

3.2 工程方案:一阶低通 + 死区

  • 一阶低通滤波:y = α*x + (1-α)*y,α 根据速度自适应。
  • 死区:当 |delta| 小于阈值时,认为电机静止,速度置 0。
#define ALPHA_FAST  0.5f
#define ALPHA_SLOW  0.1f
#define DEAD_ZONE   2      // 计数阈值

float speed_filter(float raw, float prev)
{
    if (abs(raw) < DEAD_ZONE)
        return 0.0f;
    float alpha = (fabsf(raw) > 100.0f) ? ALPHA_FAST : ALPHA_SLOW;
    return alpha * raw + (1.0f - alpha) * prev;
}

3.3 更优方案:M/T 法混合测速

低速用 T 法(测脉冲间隔),高速用 M 法(测固定时间脉冲数),结合两者可在全速域获得平稳输出。实现稍复杂,但效果显著。

4. CubeMX 配置步骤

  1. 选择 TIMx,Combined Channels 设为 Encoder Mode。
  2. Encoder Mode 选 TI1 and TI2(4 倍频)。
  3. 极性根据实际接线调整,通常 Rising Edge。
  4. 开启 TIMx 全局中断(NVIC)。
  5. 生成代码,在 MX_TIMx_Init() 后调用 HAL_TIM_Encoder_Start(&htimx, TIM_CHANNEL_ALL)。

5. 完整代码示例(核心部分)

#include "stm32f4xx_hal.h"

extern TIM_HandleTypeDef htim2;

#define ENCODER_LINES  1000
#define SAMPLE_TIME_S  0.01f

static int32_t last_total = 0;
static float   speed_filt = 0.0f;

void Encoder_Init(void)
{
    HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
    __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
}

int32_t Encoder_GetTotalCount(void)
{
    uint16_t cnt;
    int32_t ovf;
    __disable_irq();
    cnt = __HAL_TIM_GET_COUNTER(&htim2);
    ovf = encoder_overflow;
    __enable_irq();
    return (ovf * 65536) + cnt;
}

float Encoder_GetSpeedRPM(void)
{
    int32_t total = Encoder_GetTotalCount();
    int32_t delta = total - last_total;
    last_total = total;

    float raw_rpm = (delta / (4.0f * ENCODER_LINES)) * (60.0f / SAMPLE_TIME_S);
    speed_filt = speed_filter(raw_rpm, speed_filt);
    return speed_filt;
}

6. 注意事项

  • 中断优先级:编码器溢出中断不宜过高,避免影响其他实时任务。
  • 计数器方向:不同定时器方向判断位可能不同,需查参考手册。
  • 滤波参数:α 和死区需根据实际电机、负载调试,过大导致响应慢,过小滤波不足。
  • 采样周期:与速度环控制周期一致,避免额外延迟。
  • 编码器信号质量:建议使用差分编码器或加 RC 滤波,减少毛刺。

通过溢出扩展和动态滤波,STM32 编码器模式可以在全速域提供高分辨率、低抖动的速度反馈,为高性能控制打下基础。