4. 编码器原理与接口:AB相编码器工作原理、STM32定时器编码器模式配置、位置与速度读取

做电机控制,你第一个要面对的问题就是:我怎么知道电机转了多少?

我刚开始搞这个的时候,以为随便接两根线读个脉冲就行。结果电机一抖,数据乱跳,位置根本对不上。后来才明白,编码器不是简单的「数脉冲」,它有自己的脾气。

这一章,我们就把它彻底搞明白。

4.1 AB相编码器工作原理

AB相编码器,也叫增量式编码器。它输出两路方波信号,相位差90°。我习惯叫它们A相和B相。

核心概念:通过A、B两相的相位关系,就能判断电机的旋转方向。

  • A相领先B相90° → 正转
  • B相领先A相90° → 反转

你想想看,如果只有一路脉冲,你只能知道转了多少,但不知道往哪转。有了AB相,方向和位置就都有了。

另外,很多编码器还有一个Z相(零位信号),每转一圈输出一个脉冲。我一般在做位置归零时用它,精度很高。

4.1.1 倍频技术

STM32的定时器可以检测A、B相的上升沿和下降沿。这样一来,一个脉冲周期内能捕获4个边沿变化。

举个例子:

  • 编码器物理分辨率:1000线/圈
  • 4倍频后:4000个计数/圈
  • 角度分辨率:360° / 4000 = 0.09°

嗯,这个精度对于大多数电机控制场景已经够用了。

我的经验:实际项目中,编码器线数不要盲目选高。线数越高,信号频率越高,对硬件抗干扰要求也越高。1000~2500线是工业上最常用的范围。

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

STM32F407的定时器有一个专门的编码器模式。说白了,就是硬件帮你把AB相的边沿检测、方向判断、计数增减全做了。你只需要配置好,然后读寄存器就行。

我一般用TIM2或TIM3来做编码器接口,因为它们支持编码器模式3(最常用的模式)。

4.2.1 配置步骤

  1. 使能时钟:RCC_APB1PeriphClockCmd() 或 RCC_APB2PeriphClockCmd()
  2. 配置GPIO:将A、B相引脚设置为复用功能(AF1或AF2,具体看定时器)
  3. 配置定时器:设置SlaveMode为编码器模式3,配置计数方向、滤波器等
  4. 配置中断(可选):如果需要溢出中断或捕获中断

下面是我常用的配置代码,你可以直接拿去用:

// 编码器模式配置示例(TIM3,PA6=CH1,PA7=CH2)
void Encoder_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;

    // 1. 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

    // 2. 配置GPIO
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_TIM3);

    // 3. 配置定时器时基
    TIM_TimeBaseStructure.TIM_Prescaler = 0;     // 不分频
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;   // 最大计数值
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    // 4. 配置编码器模式(模式3:双沿检测)
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x0F;     // 滤波,抗干扰
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, 
                               TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

    // 5. 使能定时器
    TIM_Cmd(TIM3, ENABLE);
}

注意:编码器模式配置完成后,定时器的计数器会自动根据AB相脉冲增减。你不需要手动干预。但要注意,计数器是16位的,最大65535。如果电机转得快,很容易溢出。我一般配合溢出中断来处理。

4.3 位置与速度读取

配置好之后,读取位置和速度就很简单了。但简单归简单,坑也不少。

4.3.1 位置读取

位置就是当前计数器的值。直接读TIMx->CNT就行。

// 读取当前位置(单位:脉冲数)
int32_t Get_Encoder_Position(void)
{
    return (int32_t)TIM3->CNT;
}

// 转换为角度(假设4倍频,1000线编码器)
float Get_Encoder_Angle(void)
{
    int32_t pulse = Get_Encoder_Position();
    return (float)pulse * 360.0f / 4000.0f;
}

这里有个细节:计数器是有符号的。正转时增加,反转时减少。但如果你配置的Period是0xFFFF,反转时计数器会从0跳到0xFFFF。所以读取时最好用int32_t做强制转换,避免符号问题。

4.3.2 速度读取

速度读取有两种常用方法:

方法 原理 适用场景 缺点
M法(测频法) 固定时间内统计脉冲数 中高速 低速时精度差
T法(测周法) 测量两个脉冲之间的时间 低速 高速时精度差
M/T法 结合M法和T法 宽速域 实现稍复杂

我个人的习惯是:低速用T法,中高速用M法。如果项目要求全速域都准,那就上M/T法。

下面是一个M法的实现:

// M法测速(每10ms调用一次)
float Get_Encoder_Speed_Method(void)
{
    static int32_t last_pulse = 0;
    int32_t current_pulse = Get_Encoder_Position();
    int32_t delta = current_pulse - last_pulse;
    last_pulse = current_pulse;

    // 处理溢出
    if(delta > 30000) delta -= 65536;
    else if(delta < -30000) delta += 65536;

    // 速度 = 脉冲差 / 时间间隔(单位:脉冲/秒)
    float speed_pulse = (float)delta / 0.01f;

    // 转换为RPM(假设1000线,4倍频)
    float speed_rpm = speed_pulse / 4000.0f * 60.0f;
    return speed_rpm;
}

避坑指南:我曾经在读取速度时忘记处理计数器溢出,结果电机正转时速度突然变成负的。排查了半天才发现是溢出问题。所以,溢出处理一定要做,哪怕你觉得电机转得没那么快。

4.4 知识结构总览

下面这张图把编码器相关的核心逻辑串起来了,你可以对照着看:

编码器知识结构图 编码器知识结构总览 增量式编码器 A相 + B相 Z相(零位) 4倍频技术 定时器编码器模式 GPIO复用配置 滤波器设置 位置读取(CNT) 速度读取(M/T法) 溢出处理 PID控制中的位置/速度反馈 核心思路:硬件捕获 → 软件读取 → 溢出处理 → 反馈给PID 每一步都有坑,但踩过一次就记住了

4.5 常见问题与避坑

  • 编码器线接反了怎么办?——A、B相交换一下就行。或者软件里把计数方向取反。
  • 电机抖动时计数乱跳?——加硬件滤波(RC低通)或软件滤波(定时器自带的ICFilter)。
  • 上电后位置不对?——编码器是增量的,上电时位置是随机的。需要做一次归零操作。
  • 速度值波动大?——采样时间太短了。我一般用10ms~50ms的采样窗口。

重要提醒:编码器的地线一定要和STM32共地。我见过有人没共地,结果信号全是噪声,位置数据完全没法用。另外,编码器线缆尽量用屏蔽线,屏蔽层单端接地。