2. STM32定时器编码器模式:定时器架构回顾、编码器模式配置(TI1与TI2)、计数方向与速度计算

好,咱们进入第二章。上一章我们把编码器的电气接口和信号类型理清了,这一章要动真格的了——让STM32的定时器直接读懂编码器的A、B相脉冲。

说白了,就是用硬件帮你数脉冲、判方向。你想想看,如果全靠外部中断去数,CPU得累死,而且高速下容易丢步。STM32的定时器编码器模式,就是专门干这个的。

2.1 定时器架构回顾:别小看这个外设

先快速回顾一下STM32定时器的核心骨架。我习惯把定时器想象成一个「万能计数器」:

  • 时基单元:预分频器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)。这是定时器的灵魂,决定了计数范围和精度。
  • 输入捕获通道:TI1、TI2、TI3、TI4。每个通道都可以配置成捕获模式、PWM输入模式,或者——咱们今天的主角——编码器模式。
  • 从模式控制器:这是编码器模式能工作的关键。它让定时器可以「听」外部信号的话,而不是只靠内部时钟。

嗯,这里要注意:不是所有定时器都支持编码器模式。一般通用定时器(TIM2~TIM5)和高级定时器(TIM1、TIM8)都支持。基本定时器(TIM6、TIM7)不行,它们没有输入捕获通道。

核心要点:编码器模式本质上是「从模式」的一种特殊用法。定时器不再用内部时钟驱动,而是用编码器的A、B相信号作为时钟源和方向指示。

我曾经在项目选型时吃过亏,选了个只有基本定时器的低端型号,结果编码器接口得用外部中断模拟,代码写起来那叫一个痛苦。所以,选型时一定要确认定时器资源。

2.2 编码器模式配置:TI1与TI2的配合

编码器模式的核心,就是让定时器同时监视TI1和TI2两个通道。这两个通道分别接编码器的A相和B相。定时器内部有个「编码器逻辑」,它会根据两个信号的边沿顺序,自动判断旋转方向。

配置其实不复杂,但有几个坑。我直接给出一段我常用的初始化代码,咱们边看边讲:

void Encoder_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_Encoder_InitTypeDef EncoderConfig = {0};

    // 1. 使能时钟
    __HAL_RCC_TIM3_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 2. 配置GPIO:PA6(TIM3_CH1), PA7(TIM3_CH2)
    GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;   // 内部上拉,防止浮空
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置编码器模式
    EncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;  // 双沿计数
    EncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
    EncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    EncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
    EncoderConfig.IC1Filter = 0;   // 不滤波,高速场景可加

    EncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
    EncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    EncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
    EncoderConfig.IC2Filter = 0;

    HAL_TIM_Encoder_Init(&htim3, &EncoderConfig);

    // 4. 启动编码器
    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}

这段代码里,最关键的参数是 TIM_ENCODERMODE_TI12。它告诉定时器:同时用TI1和TI2的上升沿和下降沿来计数。说白了,就是「四倍频」模式。

编码器模式 计数边沿 每圈脉冲数(举例) 分辨率
TIM_ENCODERMODE_TI1 仅TI1边沿 1000 1x
TIM_ENCODERMODE_TI2 仅TI2边沿 1000 1x
TIM_ENCODERMODE_TI12 TI1和TI2双沿 1000 4x

我个人习惯用TI12模式,因为分辨率直接翻四倍,而且方向判断最可靠。代价是计数器溢出更快,需要合理设置ARR值。

经验之谈:如果编码器线数很高(比如5000线),用TI12模式时,ARR要设得足够大,否则计数器会频繁溢出。我一般把ARR设为0xFFFF,然后用32位变量累加。

2.3 计数方向与速度计算

配置好之后,定时器就开始自动计数了。你只需要读 TIMx->CNT 寄存器,就能知道当前的位置。那方向呢?

方向信息藏在 TIMx->CR1 寄存器的第4位(DIR位)。如果DIR=0,表示正转(向上计数);DIR=1,表示反转(向下计数)。

我一般这样读方向和计数值:

int16_t Get_Encoder_Count(void)
{
    return (int16_t)htim3.Instance->CNT;
}

uint8_t Get_Encoder_Direction(void)
{
    // 读取DIR位:0=正转,1=反转
    return (htim3.Instance->CR1 & TIM_CR1_DIR) ? 1 : 0;
}

注意,我把CNT强制转成了 int16_t。为什么?因为编码器模式下的计数器是双向的,正转时从0往上加,反转时从ARR往下减。用有符号数读取,可以直接得到带符号的位置值,方便后续计算。

速度计算呢?说白了就是「单位时间内的位置变化」。我常用的方法是:

  1. 每隔固定时间(比如10ms)读取一次计数值。
  2. 用当前值减去上次值,得到差值。
  3. 差值除以时间间隔,就是速度。
int16_t last_count = 0;
float speed = 0.0f;

void Calculate_Speed(void)
{
    int16_t current_count = Get_Encoder_Count();
    int16_t delta = current_count - last_count;

    // 处理溢出:如果差值超过半个周期,说明发生了溢出
    if (delta > 32767) delta -= 65536;
    else if (delta < -32768) delta += 65536;

    // 速度 = 差值 / 时间间隔(单位:脉冲/秒)
    speed = (float)delta / 0.01f;  // 假设10ms采样一次

    last_count = current_count;
}

嗯,这里要注意溢出处理。如果计数器从0xFFFF跳到0x0000,直接相减会得到一个巨大的负值。我上面那个if判断就是干这个的——把差值修正到正确的范围内。

避坑指南:我曾经在一个高速电机项目里,采样周期设得太长(100ms),结果电机一转快,计数器来回溢出了好几次,速度计算完全乱套。后来把采样周期缩短到5ms,同时用32位变量做累加,问题才解决。

最后,咱们用一张图来总结整个编码器模式的工作流程:

STM32定时器编码器模式工作流程 编码器A相 TI1 (PA6) 编码器B相 TI2 (PA7) 定时器编码器逻辑 边沿检测 方向判断 计数更新 溢出处理 寄存器 CNT:计数值 DIR:方向位 SR:状态标志 ARR:自动重装载 应用层读取 位置计算 | 速度计算 | 方向判断 💡 关键点 TI12模式:同时检测A、B相双沿,分辨率4倍 DIR位自动更新,无需软件判断方向

这张图把整个流程串起来了:编码器信号进入定时器,经过编码器逻辑处理后,自动更新CNT和DIR寄存器。你只需要在应用层定时读取这些寄存器,就能得到位置、方向和速度。