基本原理

增量式光电编码器主要由三个部分组成:发光二极管、码盘以及码盘背面的光传感器

码盘

码盘安装在旋转轴上,上面均匀地排列着透光和不透光的扇形区域。当码盘转动时,不透光的部分能够挡住光线,而透光区则允许光线透过,那么码盘背面的光传感器就会周期性地收到光信号,从而输出一列方波

码盘转动一周时,光传感器输出的脉冲个数是固定的,那么通过 检测一定时间内收到的脉冲个数,就可以知道在这段时间内码盘转动了多少圈,进而换算为速度。例如,一个码盘转动一周时会输出 100 个脉冲,在 0.1s 内我们收到了 500 个脉冲,这意味着 0.1s 内码盘转动了 5 周,即码盘的转速为

mylatex20230817_182047

AB 相

如果编码器只输出一列方波(假设为 A),该怎样判断码盘是正转还是反转?因为无论是正转还是反转,都会产生同样的方波,而它们对速度的贡献显然是相反的。上面我们说过,码盘上均匀地刻着透光和不透光的扇形区域,我们在扇形区域内侧再均匀地刻上一圈透光和不透光的扇形区域,不同的是,外圈和内圈的区域是“交错”的

image-20230817184040257

通过观察上图中的码盘我们可以得到如下结论

  • 当外圈处于不透光区域时,内圈对应的一半为透光区域,一半为不透光区域
  • 当外圈处于透光区域时,内圈对应的一半为不透光区域,一半为透光区域

我们在内外扇形区域各安装上一套发光二极管、码盘以及光传感器,那么当码盘转动时,编码器就会 输出两列相位差为 90° 方波(习惯称之为 A、B 相

码盘沿不同方向旋转时 A、B 相输出如下,显然正转和反转输出方波的特征是不同的

通过判断 B 处于上升沿时 A 的电平状态,我们就可以知道码盘旋转的方向了

  • 当码盘正转时,在 B 的上升沿,方波 A 恒为高电平
  • 当码盘反转时,在 B 的上升沿,方波 A 恒为低电平

注意

  1. 正/反转是相对而言的,重点在于区分不同旋转方向时的波形特征
  2. 通过观察 A 上升沿时 B 的电平亦能判旋转方向,参考下文代码实现部分

速度计算

假如编码器码盘旋转一周 A/B 相输出的脉冲数目为 N,在时间 T 内统计到的有效脉冲数目为 S(正转脉冲数 + 1,反转脉冲数 - 1),小车轮子的直径为 D,那么小车的速度换算公式如下

mylatex20230817_182231

笔者使用的编码器码盘旋转一周输出的脉冲个数为 90,小车轮子的直径为 75mm,假如 1s 内统计得到的有效脉冲数目为 500,代入上式计算小车此时的速度为

mylatex20230817_182201

代码实现

显然,速度计算的关键在于 统计一定时间内的脉冲数目,这里提供两种思路

  1. 利用中断检测 B 的上升沿,触发中断时判断 A 的电平,来决定计数值加还是减
  2. 将定时器设置为编码器模式,直接读取计数值和方向

本文基于 STM32 平台开发驱动,读者了解增量编码器原理后,移植到其它平台应该是难度不大的。下面分别介绍上述两种方法的实现

方法一

实现原理:利用中断检测 B 的上升沿,触发中断时判断 A 的电平,来决定计数值加还是减

打开 CubeMX,设置相关管脚,我这里使用的是 PC2 和 PC3 来接 A、B 相

为方便观察调试,这里启用 USART 串口

打开定时器2,定时器响应时通过 USART 串口把计数值打印出来

设置定时 200ms

导入 Cube 工程,定义相关变量

1
2
3
4
int encoder_count = 0;      // 计数器
char msg[64]; // USART 输出字符缓冲区
float speed = 0; // 速度
int flag = 0; // 计数标志位:0 计数,1 清零

重写外部中断回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// A 相上升沿中断触发
if(GPIO_Pin == GPIO_PIN_2)
{
if(flag == 1)
{
// 清零速度计时器
encoder_count = 0;
}
else
{
// 判断 B 相电平
if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_3) == 0)
{
// 码盘正转
encoder_count++;
}
else
{
// 码盘反转
encoder_count--;
}
}
}
}

定时器 2 响应后,通过 USART 串口向上位机发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
if (flag == 0)
{
// 计算速度(cm/s)
speed = (float)encoder_count * 3.14 * 0.14 / (90 * 0.2)*100/34;
// 格式化消息缓冲器
sprintf(msg, "speed:%dcm/s\r\n",(int)speed);
// 发送数据至上位机
HAL_UART_Transmit_IT(&huart1, (uint8_t*)msg, sizeof(msg));
// 清空速度计数器,进行下一轮计数
flag = 1;
}
else
{
flag = 0;
}
}
}

在主函数中打开定时器 2

1
HAL_TIM_Base_Start_IT(&htim2);

测试效果如图

方法二

使用定时器的编码器模式

设置 TIM4 的 Combined Channels 为编码器模式

配置编码模式

  • Prescaler:分频系数

  • Counter Mode:计数模式,设置为 UP 时码盘正转计数值增加,反转计数值减小

  • Counter Period:编码器计数最大值,一般设置为 65535 以防止溢出

  • Encoder Mode:计数模式,编码器计数有三种模式可选

    1. T1

      只在上升沿计数,例如在一定时间内 A/B 产生了 100 个脉冲,那么编码器计数值为200(A、B 产生脉冲数相等)。由于分频系数 Prescaler 的存在,实际调用函数得到的计数值为

      mylatex20230817_182302

    2. T2

      只在下降沿计数,计数值与 T1 相等

    3. TI and T2

      在上升沿、下降沿都计时,计数值为 T1/T2 的两倍

导入 CubeMx 工程,定义相关变量

1
2
3
uint8_t encoder_count;      	// 编码器计数
uint8_t msg[64]; // 字符缓冲区
uint8_t encoder_direction; // 编码器方向

打开定时器 4

1
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);

读取相关值并通过 OLED 显示出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (1)
{
// 获取编码器计数值和旋转方向
encoder_count = __HAL_TIM_GET_COUNTER(&htim4);
encoder_direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4);
// 格式化字符串缓冲区
sprintf((char *)msg, "fre:%4d dir:%d", encoder_count*2, encoder_direction);
// OLED 显示
OLED_Clear();
OLED_ShowString(0,0,msg);
// 清零计数器
__HAL_TIM_SET_COUNTER(&htim4,0);
HAL_Delay(500);
}

这里用函数发生器模拟 A、B 两列方波,编码器计数时采用 T1 and T2 模式,分频系数为 3,所以理论上 1s 内采集到的脉冲数目为

mylatex20230817_182332

代码中每 500ms 读取一次编码器计数值,那么乘以 2 才是方波的真实频率

1
sprintf((char *)msg, "fre:%4d dir:%d", encoder_count*2, encoder_direction);

image-20230817125002851