3、编码器信号采集:STM32定时器编码器模式配置、正交解码原理、脉冲计数与方向判断、倍频技术(2倍频/4倍频)

好,咱们进入正题。编码器信号采集,说白了就是让单片机听懂电机在“说什么”。电机通过AB两路脉冲告诉你位置和方向,而STM32的定时器,天生就是干这个活的。

我个人习惯,拿到一个新项目,先搞清楚硬件怎么接,再想软件怎么写。编码器接口这块,我踩过的坑不少,今天一并倒出来。

3.1 正交解码原理:为什么是AB两路?

先问个问题:单路脉冲能不能测位置?能。但方向呢?不行。正交编码器输出两路相位差90°的方波,这就是A相和B相。

你想想看,电机正转时,A相领先B相90°;反转时,B相领先A相90°。单片机通过检测这个相位关系,就能知道轴往哪转。

核心判断逻辑:

  • A上升沿时,B为高电平 → 正转
  • A上升沿时,B为低电平 → 反转

我曾经在一个伺服项目里,编码器线序接反了,结果电机越调越抖。查了半天,其实就是A、B两相在端子排上焊反了。嗯,硬件上的低级错误,软件怎么调都救不回来。

3.2 STM32定时器编码器模式配置

STM32的通用定时器(TIM2~TIM5,部分型号还有TIM8等)都支持编码器模式。配置起来其实不复杂,但有几个寄存器必须搞明白。

我直接给出一份我常用的初始化代码,基于HAL库,芯片是STM32F103系列。其他型号大同小异。

void Encoder_Init(void)
{
    TIM_Encoder_InitTypeDef sEncoderConfig = {0};
    GPIO_InitTypeDef gpioInit = {0};

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

    // 2. 配置GPIO:PA6(TIM3_CH1), PA7(TIM3_CH2)
    gpioInit.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    gpioInit.Mode = GPIO_MODE_AF_PP;
    gpioInit.Pull = GPIO_NOPULL;
    gpioInit.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &gpioInit);

    // 3. 定时器编码器模式配置
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 0;               // 不分频
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 65535;              // 16位自动重装
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);

    // 4. 编码器配置:TI1和TI2都计数,正转向上
    sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;
    sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
    sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
    sEncoderConfig.IC1Filter = 0;
    sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
    sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
    sEncoderConfig.IC2Filter = 0;
    HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);

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

个人经验:编码器模式下的GPIO,建议不要开启内部上拉或下拉。编码器输出通常是推挽或开漏,外部已经有上拉电阻了。内部上拉反而可能引入噪声,我在一个振动台项目里吃过这个亏。

3.3 脉冲计数与方向判断

配置好之后,定时器的计数器(CNT)会自动根据编码器脉冲增减。你只需要读CNT寄存器的值,就知道当前位置了。

方向怎么判断?看DIR位。在编码器模式下,TIMx_CR1寄存器的DIR位由硬件自动更新。正转时DIR=0(向上计数),反转时DIR=1(向下计数)。

我一般这样读位置和方向:

int16_t Get_Encoder_Count(void)
{
    return (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
}

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

注意:CNT寄存器是16位的,范围0~65535。如果你需要记录多圈绝对位置,必须在外部用软件做溢出处理。我曾经忘了做这个,结果电机转了几圈后位置突然跳变,差点把机械限位撞坏。

3.4 倍频技术:2倍频与4倍频

编码器本身有一个物理分辨率,比如每转1000线。但通过倍频,你可以把分辨率翻倍甚至翻四倍。

原理很简单:

  • 1倍频:只在A相或B相的上升沿计数
  • 2倍频:在A相和B相的上升沿都计数(TI1和TI2各计一次)
  • 4倍频:在A相和B相的上升沿和下降沿都计数

STM32的编码器模式,通过设置TIM_ENCODERMODE_TI12,实际上已经实现了2倍频。因为它在TI1和TI2的每个边沿都触发计数,一个完整周期内计了2次。

那4倍频怎么实现?硬件上,STM32的定时器本身不支持4倍频。你需要用两个定时器配合,或者用外部逻辑电路。我常用的软件方法是用两个定时器:一个做编码器模式(2倍频),另一个做输入捕获,捕获另一个通道的边沿,然后软件叠加。

不过说实话,大多数应用场景下,2倍频已经够用了。我只有在做高精度低速伺服时,才用到4倍频。

倍频方式 计数边沿 分辨率提升 STM32实现难度
1倍频 单相单边沿 1x 简单(普通输入捕获)
2倍频 双相单边沿 2x 简单(编码器模式TI12)
4倍频 双相双边沿 4x 较复杂(双定时器或外部电路)

避坑指南:我曾经在一个项目里,为了追求高分辨率,强行用软件模拟4倍频。结果CPU负载飙升,中断响应不过来,反而丢步了。后来老老实实换了个更高线数的编码器,问题解决。有时候,硬件升级比软件取巧更靠谱。

3.5 知识体系总览

下面这张图,把编码器信号采集的整个流程串起来了。从物理信号到软件读数,每一步都离不开。

编码器信号采集知识体系 编码器信号采集知识体系 物理信号 A相 / B相 方波 正交解码 相位差90°判断方向 定时器配置 编码器模式TI12 脉冲计数 CNT寄存器自动增减 方向判断 DIR位硬件自动更新 倍频技术 2倍频 / 4倍频 软件读取 Get_Encoder_Count() + 溢出处理 核心思路:硬件自动处理边沿和方向,软件只负责读数和溢出管理 倍频是分辨率提升手段,但要注意CPU负载和中断响应

嗯,这张图把整个流程串起来了。从物理信号到正交解码,再到定时器配置,最后落到脉冲计数、方向判断和倍频。每一步都是下一环的基础。

我个人觉得,编码器采集这块,最难的不是配置寄存器,而是理解硬件是怎么帮你干活的。你只要想明白AB两相的相位关系,剩下的,STM32的定时器基本都替你做了。