3、增量式编码器原理与数据读取:ABZ编码器输出信号解析、定时器编码器模式配置(STM32)、读取位置与速度、方向判断

好,咱们今天聊聊增量式编码器。这东西在电机控制里太常见了,几乎每个做运动控制的工程师都得跟它打交道。我个人习惯把它叫做「电机的眼睛」——没有它,你根本不知道电机转到了哪里、转得有多快。

3.1 ABZ编码器输出信号解析

先说说增量式编码器到底输出什么信号。说白了,它就是个光栅盘加几个光耦,转起来之后输出几路脉冲。

A相和B相:这两路信号相位差90度。你想想看,如果电机正转,A相领先B相90度;反转的话,B相领先A相90度。这就是判断方向的依据。

Z相:也叫零位信号。编码器每转一圈,Z相输出一个脉冲。这个信号特别有用——可以用来做位置校准。

我在项目中遇到过一个问题:有个设备每次上电后位置都不对,后来发现是没做Z相归零。从那以后,我只要用增量式编码器,上电后第一件事就是找Z相。

关键点:A/B相用于计数和方向判断,Z相用于绝对位置参考。

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

STM32的定时器有个很贴心的功能——编码器模式。说白了,就是硬件帮你把A/B相的脉冲直接转换成计数值,不用你在中断里一个个数。

我刚开始做的时候,傻乎乎地用外部中断去读A/B相,结果发现高速时根本来不及。后来改用定时器编码器模式,CPU占用率直接降到了几乎为零。

配置其实不复杂,我一般这么干:

// 以STM32F407为例,配置TIM3为编码器模式
TIM_EncoderInterfaceConfigTypeDef sEncoderConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};

// 使能定时器时钟
__HAL_RCC_TIM3_CLK_ENABLE();

// 配置GPIO:PA6为TIM3_CH1,PA7为TIM3_CH2
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 配置编码器模式
sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;  // 双沿计数
sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC1Filter = 0;
sEncoderConfig.IC2Filter = 0;
HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);

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

嗯,这里要注意:TIM_ENCODERMODE_TI12 表示在A相和B相的双沿都计数。这样分辨率能提高4倍。比如一个1000线的编码器,用这个模式就能得到4000个计数/圈。

我的习惯:编码器模式配置好后,先用手转一下电机,看看计数值是不是在变化。如果不动,八成是GPIO配置错了。

3.3 读取位置与速度

位置读取很简单,直接读定时器的计数器值就行:

int32_t position = __HAL_TIM_GET_COUNTER(&htim3);

但速度计算就有点讲究了。我见过不少新手直接用位置差分来算速度,结果噪声大得没法看。

两种常用方法

  • M法(测频法):固定时间窗口内数脉冲个数。适合高速场合。
  • T法(测周法):测量两个脉冲之间的时间。适合低速场合。

我个人习惯在项目中用M法,因为实现简单,而且大多数应用场景下电机转速都不算太低。

// M法测速示例
#define SPEED_SAMPLE_TIME_MS  100   // 采样周期100ms

uint32_t last_count = 0;
uint32_t current_count = 0;
float speed_rpm = 0;

// 在定时器中断中调用(每100ms一次)
void Speed_Calculate(void)
{
    current_count = __HAL_TIM_GET_COUNTER(&htim3);
    int32_t delta = current_count - last_count;
    
    // 处理溢出
    if(delta > 0x7FFFFFFF)
        delta = delta - 0xFFFFFFFF;
    else if(delta < -0x7FFFFFFF)
        delta = delta + 0xFFFFFFFF;
    
    // 假设编码器1000线,4倍频后4000计数/圈
    // 速度 = (delta / 4000) / (100ms / 60000ms) = delta * 15 / 1000
    speed_rpm = (float)delta * 15.0f / 1000.0f;
    
    last_count = current_count;
}

避坑指南:我曾经因为没处理计数器溢出,导致速度值突然跳变。电机在高速运行时,计数器很容易溢出。一定要做溢出处理!

3.4 方向判断

方向判断其实不用你操心——STM32的编码器模式会自动处理。你只需要看计数值是增加还是减少就行。

但如果你非要用外部中断自己实现,原理也很简单:

  • 在A相的上升沿读取B相的电平
  • 如果B相为高,说明正转
  • 如果B相为低,说明反转

不过说实话,有硬件支持就别自己折腾了。我早期做的一个项目就是自己写逻辑判断方向,结果在电机抖动的时候频繁误判。换成硬件编码器模式后,问题迎刃而解。

总结一下

  • ABZ信号:A/B相差90度用于计数和方向,Z相用于归零
  • 定时器编码器模式:硬件自动处理,省心省力
  • 位置读取:直接读计数器值
  • 速度计算:M法适合高速,T法适合低速
  • 方向判断:硬件自动完成,别自己写

好了,这一章就到这里。下一章咱们聊聊怎么用这些数据做闭环控制——那才是真正有意思的部分。