基本原理

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

v2-46107aecc8c217217f90455be716c601_1440w (1)_processed

码盘

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

方波

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

ω=50.1=50r/s\omega=\frac{5}{0.1}=50r/s

ABZ 相

如果编码器只输出一列方波(记为 A 相),则无论是正转还是反转,都会产生同样的方波,无法判断旋转方向。上面我们说过,码盘上均匀地刻着透光和不透光的扇形区域,我们在扇形区域内侧再均匀地刻上一圈透光的扇形区域,不同的是,外圈和内圈的透光区域是交错的

码盘

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

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

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

A&B相

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

正反转

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

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

注意

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

通过 A/B 相能够计算旋转的速度和方向,但无法判断当前的绝对旋转位置,因为无论从哪个起止位置开始旋转,输出的波形完全相同。若系统运行过程中因电气干扰、高速丢步等原因丢失了若干脉冲,所产生的位置计数误差将持续累积,无法自行修正。Z 相则用于提供绝对参考原点,增量式编码器仅在固定的机械位置输出脉冲,系统可每隔一段时间利用 Z 脉冲复位或修正计数值,避免误差持续累积

ABZ

实际应用中各厂家编码器输出波形可能有所不同,但其 ABZ 相核心逻辑是一样的

速度计算

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

v=πSDNTv=\frac{\pi SD}{NT}

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

v=π5000.0759011.31m/sv=\frac{\pi *500*0.075}{90*1} \approx 1.31m/s

代码实现

显然,速度计算的关键在于 统计一定时间内的脉冲数目。本文基于 STM32 和 FPGA 开发驱动,读者了解增量编码器原理后,移植到其它平台应该难度不大

STM32

STM32 在工业和学术界的应用非常广泛,这里提供两种思路

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

基于中断的实现

打开 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 为 Encoder Mode(编码器模式)

配置编码模式

  • Prescaler:分频系数

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

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

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

    1. TI1

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

      200Prescaler+1\frac{200}{Prescaler+1}

    2. TI2

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

    3. TI1 and TI2

      在上升沿、下降沿都计时,计数值为 TI1/TI2 的两倍,显然该模式具有更高的分辨率

导入 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);
}

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

n=5043+1=50n=\frac{50*4}{3+1}=50

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

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

image-20230817125002851

FPGA

嵌入式平台要检测高频 ABZ 脉冲还是比较困难的,相比之下 FPGA 的优势就比较明显,驱动设计思路与前文一致

计数逻辑

本文所设计的编码器模块支持多路 ABZ 信号输入

abz_encoder

首先同步 ABZ 信号至当前时钟域,同步寄存器级数由参数 SYNC_STAGES 控制,建议 ≥ 4

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
27
28
29
30
31
32
33
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 倍
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
27
28
29
30
31
32
33
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

上述计数规则对应代码如下

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 信号缓存各通道计数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 输出,以减少输出端口数量

counter_mux

1
2
3
4
5
6
7
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 时钟与编码器采样时钟不同,因此增加了一些同步操作

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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 下,则时序约束如下

1
2
3
4
5
6
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]]

实际使用时应根据具体设计修改

使用流程

  1. 上电后首先通过 rst_n 复位模块
  2. 设置计数模式
  3. 在 snapshot 端口施加上升沿信号,将当前计数值暂存至输出寄存器
  4. 通过控制 channel_id 读取不同通道计数值

后续读取时再次向 snapshot 端口施加上升沿信号即可

仿真

输入 2 个通道的 ABZ 信号,测试 3 种计数模式、snapshot、clear 等功能,相关代码及结果如下

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
`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

image-20260208162900316

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

image-20260208163513576

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

image-20260208163958394

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

image-20260208164322162

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

image-20260208164508086