增量式编码器详解

基本原理

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

v2-46107aecc8c217217f90455be716c601_1440w (1)_processed

码盘

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

在这里插入图片描述

码盘转动一周时,光传感器输出的脉冲个数是固定的,那么通过 检测一定时间内收到的脉冲个数,就可以知道在这段时间内码盘转动了多少圈,进而换算为速度。例如,一个码盘转动一周时会输出 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 恒为低电平

注意

  1. 正/反转是相对而言的,重点在于区分不同旋转方向时的波形特征
  2. 通过观察 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=901π5000.0751.31m/s

代码实现

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

STM32

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

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

基于中断的实现

打开 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:计数模式,编码器计数有三种模式可选

    1. 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

    2. TI2

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

    3. 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+1504=50
代码中每 500ms 读取一次编码器计数值,那么乘以 2 才是方波的真实频率

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

image-20230817125002851

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’b00x1
2’b01x2
2’b10x3
2’b11None

不同计数模式的计数器加减规则如下

计数模式AB计数值
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 输出
端口方向位宽同步/异步备注
clkin1*输入时钟
x4 模式下输入时钟频率应高于 ABZ 脉冲频率的 8 倍
rst_nin1async复位(低电平/下降沿有效)
clearin1async清零(检测到上升沿时清空各通道计数值)
snapshotin1async快照(检测到上升沿时将当前计数值同步至缓存)
modein2async计数模式(00: x1, 01: x2, 10: x4)
channel_idinCHANNEL_ID_WIDTHasync输出通道 ID
计数值输出 MUX 选择信号
AinCHANNEL_NUMasyncA 相
BinCHANNEL_NUMasyncB 相
ZinCHANNEL_NUMasyncZ 相
countoutCOUNT_WIDTHsync计数值输出

笔者使用 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]]

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

使用流程

  1. 上电后首先通过 rst_n 复位模块
  2. 设置计数模式
  3. 在 snapshot 端口施加上升沿信号,将当前计数值暂存至输出寄存器
  4. 通过控制 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

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

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值