基本原理
增量式光电编码器主要由 发光二极管、码盘以及码盘背面的光传感器 三个部分组成,多用于测量旋转物体的角位移、角速度及转动方向。

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

码盘转动一周时,光传感器输出的脉冲个数是固定的,那么通过 检测一定时间内收到的脉冲个数,就可以知道在这段时间内码盘转动了多少圈,进而换算为速度。例如,一个码盘转动一周时会输出 100 个脉冲,在 0.1s 内我们收到了 500 个脉冲,这意味着 0.1s 内码盘转动了 5 周,即码盘的转速为
ω
=
5
0.1
=
50
r
/
s
\omega=\frac{5}{0.1}=50r/s
ω=0.15=50r/s
ABZ 相
如果编码器只输出一列方波(记为 A 相),则无论是正转还是反转,都会产生同样的方波,无法判断旋转方向。上面我们说过,码盘上均匀地刻着透光和不透光的扇形区域,我们在扇形区域内侧再均匀地刻上一圈透光的扇形区域,不同的是,外圈和内圈的透光区域是交错的

通过观察上图中的码盘我们可以得到如下结论
- 当外圈处于透光区域时,内圈对应的一半为不透光区域,一半为透光区域
- 当外圈处于不透光区域时,内圈对应的一半为透光区域,一半为不透光区域
我们在内外扇形区域各安装上一套发光二极管、码盘以及光传感器,那么当码盘转动时,编码器就会 输出两列相位差为 90° 方波(习惯称之为 A、B 相)

码盘沿不同方向旋转时 A、B 相输出如下,正转和反转输出波形具有不同特征

通过判断 B 处于上升沿时 A 的电平状态,我们就可以知道码盘旋转的方向了
- 当码盘正转时,在 B 的上升沿,方波 A 恒为高电平
- 当码盘反转时,在 B 的上升沿,方波 A 恒为低电平
注意
- 正/反转是相对而言的,重点在于区分不同旋转方向时的波形特征
- 通过观察 A 上升沿时 B 的电平亦能判旋转方向,参考下文代码实现部分
通过 A/B 相能够计算旋转的速度和方向,但无法判断当前的绝对旋转位置,因为无论从哪个起止位置开始旋转,输出的波形完全相同。若系统运行过程中因电气干扰、高速丢步等原因丢失了若干脉冲,所产生的位置计数误差将持续累积,无法自行修正。Z 相则用于提供绝对参考原点,增量式编码器仅在固定的机械位置输出脉冲,系统可每隔一段时间利用 Z 脉冲复位或修正计数值,避免误差持续累积

实际应用中各厂家编码器输出波形可能有所不同,但其 ABZ 相核心逻辑是一样的
速度计算
假如编码器码盘旋转一周 A/B 相输出的脉冲数目为 N,在时间 T 内统计到的有效脉冲数目为 S(正转脉冲数 + 1,反转脉冲数 - 1),小车轮子的直径为 D,那么小车的速度换算公式如下
v
=
π
S
D
N
T
v=\frac{\pi SD}{NT}
v=NTπSD
笔者使用的编码器码盘旋转一周输出的脉冲个数为 90,小车轮子的直径为 75mm,假如 1s 内统计得到的有效脉冲数目为 500,代入上式计算小车此时的速度为
v
=
π
∗
500
∗
0.075
90
∗
1
≈
1.31
m
/
s
v=\frac{\pi *500*0.075}{90*1} \approx 1.31m/s
v=90∗1π∗500∗0.075≈1.31m/s
代码实现
显然,速度计算的关键在于 统计一定时间内的脉冲数目。本文基于 STM32 和 FPGA 开发驱动,读者了解增量编码器原理后,移植到其它平台应该难度不大
STM32
STM32 在工业和学术界的应用非常广泛,这里提供两种思路
- 利用中断检测 B 的上升沿,触发中断时判断 A 的电平,来决定计数值加减
- 将定时器设置为编码器模式,直接读取计数值和方向
基于中断的实现
打开 CubeMX,设置相关管脚,我这里使用的是 PC2 和 PC3 来接 A、B 相

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

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

设置定时 200ms

导入 Cube 工程,定义相关变量
int encoder_count = 0; // 计数器
char msg[64]; // USART 输出字符缓冲区
float speed = 0; // 速度
int flag = 0; // 计数标志位:0 计数,1 清零
重写外部中断回调函数
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 串口向上位机发送数据
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
HAL_TIM_Base_Start_IT(&htim2);
测试效果如图

基于定时器编码器模式的实现
设置 TIM4 的 Combined Channels 为 Encoder Mode(编码器模式)

配置编码模式

-
Prescaler:分频系数 -
Counter Mode:计数模式,设置为 UP 时码盘正转计数值增加,反转计数值减小 -
Counter Period:编码器计数最大值,一般设置为 65535 以防止溢出 -
Encoder Mode:计数模式,编码器计数有三种模式可选-
TI1
只在上升沿计数,例如在一定时间内 A/B 产生了 100 个脉冲,那么编码器计数值为 200(A、B 产生脉冲数相等)。由于分频系数 Prescaler 的存在,实际调用函数得到的计数值为
200 P r e s c a l e r + 1 \frac{200}{Prescaler+1} Prescaler+1200 -
TI2
只在下降沿计数,计数值与 TI1 相等
-
TI1 and TI2
在上升沿、下降沿都计时,计数值为 TI1/TI2 的两倍,显然该模式具有更高的分辨率
-
导入 CubeMx 工程,定义相关变量
uint8_t encoder_count; // 编码器计数
uint8_t msg[64]; // 字符缓冲区
uint8_t encoder_direction; // 编码器方向
打开定时器 4
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);
读取相关值并通过 OLED 显示出来
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);
}
这里用函数发生器模拟 AB 两列 50Hz、相位相差 90° 的方波,编码器计数时采用 T1 and T2 模式,分频系数为 3,所以理论上 1s 内采集到的脉冲数目为
n
=
50
∗
4
3
+
1
=
50
n=\frac{50*4}{3+1}=50
n=3+150∗4=50
代码中每 500ms 读取一次编码器计数值,那么乘以 2 才是方波的真实频率
sprintf((char *)msg, "fre:%4d dir:%d", encoder_count*2, encoder_direction);

FPGA
嵌入式平台要检测高频 ABZ 脉冲还是比较困难的,相比之下 FPGA 的优势就比较明显,驱动设计思路与前文一致
计数逻辑
本文所设计的编码器模块支持多路 ABZ 信号输入

首先同步 ABZ 信号至当前时钟域,同步寄存器级数由参数 SYNC_STAGES 控制,建议 ≥ 4
wire [CHANNEL_NUM-1:0] A_sync, B_sync, Z_sync;
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] A_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] B_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] Z_sync_r [SYNC_STAGES-1:0];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_r[0] <= 'b0;
B_sync_r[0] <= 'b0;
Z_sync_r[0] <= 'b0;
end else begin
A_sync_r[0] <= A;
B_sync_r[0] <= B;
Z_sync_r[0] <= Z;
end
end
generate
for (genvar stage = 1; stage < SYNC_STAGES; stage = stage + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_r[stage] <= 'b0;
B_sync_r[stage] <= 'b0;
Z_sync_r[stage] <= 'b0;
end else begin
A_sync_r[stage] <= A_sync_r[stage-1];
B_sync_r[stage] <= B_sync_r[stage-1];
Z_sync_r[stage] <= Z_sync_r[stage-1];
end
end
end
endgenerate
然后检测 ABZ 信号边沿,用于支持不同的计数模式,本设计支持 3 种计数模式(未使用 Z 相)
x1:在 A 相上升沿计数x2:在 A 相上升沿、下降沿计数,相同旋转下计数值为 x1 模式的 2 倍x4:在 A 相、B 相上升沿、下降沿计数,相同旋转下计数值为 x1 模式的 4 倍
assign A_sync = A_sync_r[SYNC_STAGES-1];
assign B_sync = B_sync_r[SYNC_STAGES-1];
assign Z_sync = Z_sync_r[SYNC_STAGES-1];
/* 检测 A/B 的上升沿和下降沿 */
wire [CHANNEL_NUM-1:0] A_sync_rising_edge, A_sync_falling_edge;
wire [CHANNEL_NUM-1:0] B_sync_rising_edge, B_sync_falling_edge;
wire [CHANNEL_NUM-1:0] Z_sync_rising_edge, Z_sync_falling_edge;
reg [CHANNEL_NUM-1:0] A_sync_dly, B_sync_dly, Z_sync_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_dly <= 'b0;
B_sync_dly <= 'b0;
Z_sync_dly <= 'b0;
end else begin
A_sync_dly <= A_sync;
B_sync_dly <= B_sync;
Z_sync_dly <= Z_sync;
end
end
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
assign A_sync_rising_edge[ch] = !A_sync_dly[ch] && A_sync[ch];
assign A_sync_falling_edge[ch] = A_sync_dly[ch] && !A_sync[ch];
assign B_sync_rising_edge[ch] = !B_sync_dly[ch] && B_sync[ch];
assign B_sync_falling_edge[ch] = B_sync_dly[ch] && !B_sync[ch];
assign Z_sync_rising_edge[ch] = !Z_sync_dly[ch] && Z_sync[ch];
assign Z_sync_falling_edge[ch] = Z_sync_dly[ch] && !Z_sync[ch];
end
endgenerate
根据输入端口 mode 配置的工作模式计数,mode 与计数模式对应关系如下
| mode[1:0] | 计数模式 |
|---|---|
| 2’b00 | x1 |
| 2’b01 | x2 |
| 2’b10 | x3 |
| 2’b11 | None |
不同计数模式的计数器加减规则如下
| 计数模式 | A | B | 计数值 |
|---|---|---|---|
| x1 | 上升沿 | 0 | +1 |
| 上升沿 | 1 | -1 | |
| x2 | 上升沿 | 0 | +1 |
| 上升沿 | 1 | -1 | |
| 下降沿 | 0 | -1 | |
| 下降沿 | 1 | +1 | |
| x4 | 上升沿 | 0 | +1 |
| 上升沿 | 1 | -1 | |
| 下降沿 | 0 | -1 | |
| 下降沿 | 1 | +1 | |
| 0 | 上升沿 | -1 | |
| 1 | 上升沿 | +1 | |
| 0 | 下降沿 | +1 | |
| 1 | 下降沿 | -1 |
上述计数规则对应代码如下
reg signed [COUNT_WIDTH-1:0] cnt_r[CHANNEL_NUM-1:0];
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_r[ch] <= 'b0;
end else if (clear_rising_edge) begin
cnt_r[ch] <= 'b0;
end else begin
case (mode_sync)
COUNT_MODE_X1:
case ({
A_sync_rising_edge[ch], B_sync[ch]
})
'b10: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转
'b11: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
default: cnt_r[ch] <= cnt_r[ch];
endcase
COUNT_MODE_X2:
case ({
A_sync_rising_edge[ch], A_sync_falling_edge[ch], B_sync[ch]
})
'b100: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转
'b101: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
'b010: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 下降沿 B 为 0,反转
'b011: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 下降沿 B 为 1,正转
default: cnt_r[ch] <= cnt_r[ch];
endcase
COUNT_MODE_X4:
case ({
A_sync_rising_edge[ch], A_sync_falling_edge[ch], B_sync_rising_edge[ch], B_sync_falling_edge[ch], A_sync[ch], B_sync[ch]
})
'b100010:
cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转(rising_edge/falling_edge 为 1 时对应 A/B 信号电平同步,为 0 时对应 A/B 信号电平可能为 0/1)
'b100011: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
'b010000: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 下降沿 B 为 0,反转
'b010001: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 下降沿 B 为 1,正转
'b001001: cnt_r[ch] <= cnt_r[ch] - 1'b1; // B 上升沿 A 为 0,反转
'b001011: cnt_r[ch] <= cnt_r[ch] + 1'b1; // B 上升沿 A 为 1,正转
'b000100: cnt_r[ch] <= cnt_r[ch] + 1'b1; // B 下降沿 A 为 0,正转
'b000110: cnt_r[ch] <= cnt_r[ch] - 1'b1; // B 下降沿 A 为 1,反转
default: cnt_r[ch] <= cnt_r[ch];
endcase
default: cnt_r[ch] <= cnt_r[ch];
endcase
end
end
end
endgenerate
由于外部模块读取输出计数值过程中该模块仍在持续计数,可能导致读取的不同通道计数值不是在同一时间采集的,因此增加 snapshot 信号缓存各通道计数值
reg snapshot_sync_dly;
reg signed [COUNT_WIDTH-1:0] cnt_snapshot_r[CHANNEL_NUM-1:0];
wire snapshot_rising_edge = !snapshot_sync_dly && snapshot_sync;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
snapshot_sync_dly <= 'b0;
end else begin
snapshot_sync_dly <= snapshot_sync;
end
end
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_snapshot_r[ch] <= 'b0;
end else if (snapshot_rising_edge) begin
cnt_snapshot_r[ch] <= cnt_r[ch];
end
end
end
endgenerate
多个 ABZ 输入通道(0~CHANNEL_NUM-1)计数值可通过 MUX 输出,以减少输出端口数量

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 'b0;
end else begin
count <= cnt_snapshot_r[channel_id_sync];
end
end
完整代码
相关参数和端口定义如下
| 参数 | 备注 |
|---|---|
| SYNC_STAGES | 将 ABZ 和控制信号同步至 clk 时钟域的寄存器级数 |
| CHANNEL_NUM | 输入 A/B/Z 通道数量 |
| COUNT_WIDTH | 计数器宽度(有符号数) |
| CHANNEL_ID_WIDTH | 通道选择输入端口位宽 各通道计数值通过 MUX 输出 |
| 端口 | 方向 | 位宽 | 同步/异步 | 备注 |
|---|---|---|---|---|
| clk | in | 1 | * | 输入时钟 x4 模式下输入时钟频率应高于 ABZ 脉冲频率的 8 倍 |
| rst_n | in | 1 | async | 复位(低电平/下降沿有效) |
| clear | in | 1 | async | 清零(检测到上升沿时清空各通道计数值) |
| snapshot | in | 1 | async | 快照(检测到上升沿时将当前计数值同步至缓存) |
| mode | in | 2 | async | 计数模式(00: x1, 01: x2, 10: x4) |
| channel_id | in | CHANNEL_ID_WIDTH | async | 输出通道 ID 计数值输出 MUX 选择信号 |
| A | in | CHANNEL_NUM | async | A 相 |
| B | in | CHANNEL_NUM | async | B 相 |
| Z | in | CHANNEL_NUM | async | Z 相 |
| count | out | COUNT_WIDTH | sync | 计数值输出 |
笔者使用 AXI-GPIO 控制和读取该编码器模块,其 AXI 时钟与编码器采样时钟不同,因此增加了一些同步操作
module abz_encoder #(
parameter SYNC_STAGES = 4, // 同步寄存器级数(测试发现 200MHz 时钟采样 25MHz 输入时,同步级数至少为 5 才不会出现问题)
parameter CHANNEL_NUM = 4, // 通道数量
parameter COUNT_WIDTH = 64, // 计数器宽度(有符号)
parameter CHANNEL_ID_WIDTH = 4 // 通道 ID 宽度
) (
input clk,
input rst_n,
input clear,
input snapshot,
input [ 1:0] mode,
input [CHANNEL_ID_WIDTH-1:0] channel_id,
input [ CHANNEL_NUM-1:0] A,
input [ CHANNEL_NUM-1:0] B,
input [ CHANNEL_NUM-1:0] Z,
output reg signed [ COUNT_WIDTH-1:0] count
);
localparam COUNT_MODE_X1 = 2'b00, COUNT_MODE_X2 = 2'b01, COUNT_MODE_X4 = 2'b10;
/* 同步输入信号 */
wire [CHANNEL_NUM-1:0] A_sync, B_sync, Z_sync;
wire [1:0] mode_sync;
wire [CHANNEL_ID_WIDTH-1:0] channel_id_sync;
wire clear_sync, snapshot_sync;
(* ASYNC_REG = "TRUE" *)reg clear_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ 1:0] mode_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg snapshot_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [CHANNEL_ID_WIDTH-1:0] channel_id_sync_r[SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] A_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] B_sync_r [SYNC_STAGES-1:0];
(* ASYNC_REG = "TRUE" *)reg [ CHANNEL_NUM-1:0] Z_sync_r [SYNC_STAGES-1:0];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_r[0] <= 'b0;
B_sync_r[0] <= 'b0;
Z_sync_r[0] <= 'b0;
clear_sync_r[0] <= 'b0;
mode_sync_r[0] <= 'b0;
snapshot_sync_r[0] <= 'b0;
channel_id_sync_r[0] <= 'b0;
end else begin
A_sync_r[0] <= A;
B_sync_r[0] <= B;
Z_sync_r[0] <= Z;
clear_sync_r[0] <= clear;
mode_sync_r[0] <= mode;
snapshot_sync_r[0] <= snapshot;
channel_id_sync_r[0] <= channel_id;
end
end
generate
for (genvar stage = 1; stage < SYNC_STAGES; stage = stage + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_r[stage] <= 'b0;
B_sync_r[stage] <= 'b0;
Z_sync_r[stage] <= 'b0;
clear_sync_r[stage] <= 'b0;
mode_sync_r[stage] <= 'b0;
snapshot_sync_r[stage] <= 'b0;
channel_id_sync_r[stage] <= 'b0;
end else begin
A_sync_r[stage] <= A_sync_r[stage-1];
B_sync_r[stage] <= B_sync_r[stage-1];
Z_sync_r[stage] <= Z_sync_r[stage-1];
clear_sync_r[stage] <= clear_sync_r[stage-1];
mode_sync_r[stage] <= mode_sync_r[stage-1];
snapshot_sync_r[stage] <= snapshot_sync_r[stage-1];
channel_id_sync_r[stage] <= channel_id_sync_r[stage-1];
end
end
end
endgenerate
assign A_sync = A_sync_r[SYNC_STAGES-1];
assign B_sync = B_sync_r[SYNC_STAGES-1];
assign Z_sync = Z_sync_r[SYNC_STAGES-1];
assign clear_sync = clear_sync_r[SYNC_STAGES-1];
assign mode_sync = mode_sync_r[SYNC_STAGES-1];
assign snapshot_sync = snapshot_sync_r[SYNC_STAGES-1];
assign channel_id_sync = channel_id_sync_r[SYNC_STAGES-1];
/* 检测 A/B 的上升沿和下降沿 */
wire [CHANNEL_NUM-1:0] A_sync_rising_edge, A_sync_falling_edge;
wire [CHANNEL_NUM-1:0] B_sync_rising_edge, B_sync_falling_edge;
wire [CHANNEL_NUM-1:0] Z_sync_rising_edge, Z_sync_falling_edge;
reg [CHANNEL_NUM-1:0] A_sync_dly, B_sync_dly, Z_sync_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
A_sync_dly <= 'b0;
B_sync_dly <= 'b0;
Z_sync_dly <= 'b0;
end else begin
A_sync_dly <= A_sync;
B_sync_dly <= B_sync;
Z_sync_dly <= Z_sync;
end
end
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
assign A_sync_rising_edge[ch] = !A_sync_dly[ch] && A_sync[ch];
assign A_sync_falling_edge[ch] = A_sync_dly[ch] && !A_sync[ch];
assign B_sync_rising_edge[ch] = !B_sync_dly[ch] && B_sync[ch];
assign B_sync_falling_edge[ch] = B_sync_dly[ch] && !B_sync[ch];
assign Z_sync_rising_edge[ch] = !Z_sync_dly[ch] && Z_sync[ch];
assign Z_sync_falling_edge[ch] = Z_sync_dly[ch] && !Z_sync[ch];
end
endgenerate
/* 捕获 clear 信号上升沿 */
reg clear_sync_dly;
wire clear_rising_edge = !clear_sync_dly && clear_sync; // 同步 clear 信号上升沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clear_sync_dly <= 'b0;
end else begin
clear_sync_dly <= clear_sync;
end
end
/* 计数器 */
reg signed [COUNT_WIDTH-1:0] cnt_r[CHANNEL_NUM-1:0];
reg signed [COUNT_WIDTH-1:0] cnt_abs_r[CHANNEL_NUM-1:0]; // TODO 将计数器分为 +/- 两部分,分别判断是否溢出
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_r[ch] <= 'b0;
end else if (clear_rising_edge) begin
cnt_r[ch] <= 'b0;
end else begin
case (mode_sync)
COUNT_MODE_X1:
case ({
A_sync_rising_edge[ch], B_sync[ch]
})
'b10: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转
'b11: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
default: cnt_r[ch] <= cnt_r[ch];
endcase
COUNT_MODE_X2:
case ({
A_sync_rising_edge[ch], A_sync_falling_edge[ch], B_sync[ch]
})
'b100: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转
'b101: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
'b010: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 下降沿 B 为 0,反转
'b011: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 下降沿 B 为 1,正转
default: cnt_r[ch] <= cnt_r[ch];
endcase
COUNT_MODE_X4:
case ({
A_sync_rising_edge[ch], A_sync_falling_edge[ch], B_sync_rising_edge[ch], B_sync_falling_edge[ch], A_sync[ch], B_sync[ch]
})
'b100010:
cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 上升沿 B 为 0,正转(rising_edge/falling_edge 为 1 时对应 A/B 信号电平同步,为 0 时对应 A/B 信号电平可能为 0/1)
'b100011: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 上升沿 B 为 1,反转
'b010000: cnt_r[ch] <= cnt_r[ch] - 1'b1; // A 下降沿 B 为 0,反转
'b010001: cnt_r[ch] <= cnt_r[ch] + 1'b1; // A 下降沿 B 为 1,正转
'b001001: cnt_r[ch] <= cnt_r[ch] - 1'b1; // B 上升沿 A 为 0,反转
'b001011: cnt_r[ch] <= cnt_r[ch] + 1'b1; // B 上升沿 A 为 1,正转
'b000100: cnt_r[ch] <= cnt_r[ch] + 1'b1; // B 下降沿 A 为 0,正转
'b000110: cnt_r[ch] <= cnt_r[ch] - 1'b1; // B 下降沿 A 为 1,反转
default: cnt_r[ch] <= cnt_r[ch];
endcase
default: cnt_r[ch] <= cnt_r[ch];
endcase
end
end
end
endgenerate
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_abs_r[ch] <= 'b0;
end else begin
cnt_abs_r[ch] <= cnt_r[ch] > 0 ? cnt_r[ch] : -cnt_r[ch];
end
end
end
endgenerate
/* 将计数器同步到输出 */
reg snapshot_sync_dly;
reg signed [COUNT_WIDTH-1:0] cnt_snapshot_r[CHANNEL_NUM-1:0];
wire snapshot_rising_edge = !snapshot_sync_dly && snapshot_sync;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
snapshot_sync_dly <= 'b0;
end else begin
snapshot_sync_dly <= snapshot_sync;
end
end
generate
for (genvar ch = 0; ch < CHANNEL_NUM; ch = ch + 1) begin
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_snapshot_r[ch] <= 'b0;
end else if (snapshot_rising_edge) begin
cnt_snapshot_r[ch] <= cnt_r[ch];
end
end
end
endgenerate
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 'b0;
end else begin
count <= cnt_snapshot_r[channel_id_sync];
end
end
endmodule
实际使用时最好添加时序约束,假设 ABZ 脉冲最大频率为 25MHz,abz_encoder 输入时钟信号 clk_200M 频率为 200MHz,rst_n/mode/clear/snapshot 等控制信号工作在时钟域 FCLK_CLK1 下,则时序约束如下
create_clock -period 40.000 -name clk_enc_a [get_ports {A[*]}]
create_clock -period 40.000 -name clk_enc_b [get_ports {B[*]}]
set_clock_groups -asynchronous -group [get_clocks {clk_enc_a clk_enc_b}] \
-group [get_clocks -of_objects [get_nets bd_system_i/clk_wiz_0/clk_200M]] \
-group [get_clocks -of_objects [get_nets bd_system_i/processing_system7_0/FCLK_CLK1]]
实际使用时应根据具体设计修改
使用流程
- 上电后首先通过 rst_n 复位模块
- 设置计数模式
- 在 snapshot 端口施加上升沿信号,将当前计数值暂存至输出寄存器
- 通过控制 channel_id 读取不同通道计数值
后续读取时再次向 snapshot 端口施加上升沿信号即可
仿真
输入 2 个通道的 ABZ 信号,测试 3 种计数模式、snapshot、clear 等功能,相关代码及结果如下
`timescale 1ns / 1ps
module abz_encoder_tb;
// Parameters
localparam SYNC_STAGES = 5;
localparam CHANNEL_NUM = 2;
localparam COUNT_WIDTH = 32;
localparam CHANNEL_ID_WIDTH = $clog2(CHANNEL_NUM);
localparam CLK_PERIOD = 2;
localparam CLK_PERIOD_HALF = CLK_PERIOD / 2;
localparam CLK_SIGNAL_PERIOD = CLK_PERIOD * 4;
localparam CLK_SIGNAL_PERIOD_HALF = CLK_SIGNAL_PERIOD / 2;
//Ports
reg clk;
reg rst_n;
reg clear;
reg snapshot;
reg [ 1:0] mode;
reg [CHANNEL_ID_WIDTH-1:0] channel_id;
reg [ CHANNEL_NUM-1:0] A;
reg [ CHANNEL_NUM-1:0] B;
reg [ CHANNEL_NUM-1:0] Z;
wire [ COUNT_WIDTH-1:0] count;
abz_encoder #(
.SYNC_STAGES(SYNC_STAGES),
.CHANNEL_NUM(CHANNEL_NUM),
.COUNT_WIDTH(COUNT_WIDTH),
.CHANNEL_ID_WIDTH(CHANNEL_ID_WIDTH)
) abz_encoder_inst (
.clk (clk),
.rst_n (rst_n),
.clear (clear),
.snapshot (snapshot),
.mode (mode),
.channel_id(channel_id),
.A (A),
.B (B),
.Z (Z),
.count (count)
);
initial begin
clk <= 1;
forever #CLK_PERIOD_HALF clk <= ~clk;
end
/* 模拟 A/B 信号 */
initial begin
A[0] = 0;
forever begin
#CLK_SIGNAL_PERIOD_HALF A[0] <= ~A[0];
end
end
initial begin
B[0] <= 1;
#(CLK_SIGNAL_PERIOD / 4) B[0] <= 0;
forever begin
#CLK_SIGNAL_PERIOD_HALF B[0] <= ~B[0];
end
end
initial begin
A[1] <= 1;
#(CLK_SIGNAL_PERIOD / 4) A[1] <= 0;
forever begin
#CLK_SIGNAL_PERIOD_HALF A[1] <= ~A[1];
end
end
initial begin
B[1] = 0;
forever begin
#CLK_SIGNAL_PERIOD_HALF B[1] <= ~B[1];
end
end
initial begin
Z[0] <= 0;
Z[1] <= 0;
rst_n <= 0;
mode <= 'b00;
clear <= 'b0;
channel_id <= 'b0;
snapshot <= 0;
#10 rst_n <= 1;
// 测试 snapshot 功能
#1000 snapshot <= 1;
#10 snapshot <= 0;
#1000 snapshot <= 1;
#10 snapshot <= 0;
// 测试通道选择
#100 channel_id <= 1;
#100 channel_id <= 2;
#100 channel_id <= 3;
// 测试 clear 功能
#100 clear <= 1;
// 测试模式切换
rst_n <= 0;
#10 mode <= 2'b01;
#10 rst_n <= 1;
#1000 snapshot <= 1;
#10 snapshot <= 0;
rst_n <= 0;
#10 mode <= 2'b10;
#10 rst_n <= 1;
#1000 snapshot <= 1;
#10 snapshot <= 0;
// 测试 Z 信号
channel_id <= 0;
#100 snapshot <= 1;
#10 snapshot <= 0;
Z[0] <= 1;
#100 Z[0] <= 0;
// 测试单通道模式
#100 $stop;
end
endmodule
通道 0 中 A 相上升沿时 B 相为低电平(正转),通道 1 中 A 相上升沿时 B 相为高电平(反转),计数模式为 x1 时两个通道计数值分别 +1/-1

检测到 snapshot 上升沿时将计数值 0x000000fb、0xffffff05 缓存至输出寄存器,输出 MUX 选择信号 channel_id 为 0,因此输出通道 0 计数值 0x000000fb

输出 MUX 选择信号变为 1 时,输出通道 1 计数值 0xffffff05

mode 为 2’b01 时计数模式为 x1,在 A 相的上升沿/下降沿均计数

mode 为 2’b10 时计数模式为 x4,在 A 相、B 相的上升沿/下降沿均计数


1376

被折叠的 条评论
为什么被折叠?



