4. FPGA编码器接口解码

任务目标

在FPGA中实现增量式编码器(A/B正交信号)的接口解码,输出当前位置(累计脉冲数)与速度信息。本案例以Xilinx Artix-7系列FPGA为硬件平台,使用Verilog HDL完成核心解码逻辑,并通过仿真验证解码正确性。

必要环境与依赖

  • 硬件平台:Xilinx Artix-7 FPGA(如XC7A35T)开发板
  • 开发工具:Vivado 2019.1及以上版本(含仿真器)
  • 编码器信号:增量式编码器A/B两相正交信号(TTL电平,3.3V),每圈脉冲数(PPR)由外部参数配置

核心实现方案

增量式编码器解码的核心是识别A/B两相信号的相位关系与边沿跳变。本设计采用4倍频解码方案:在每个时钟周期采样A/B信号,检测A、B两相的电平组合状态变化,根据状态转移方向判断旋转方向并产生位置计数脉冲。速度测量采用M法(定时计数法),在固定时间窗口内统计位置增量,计算得到速度值。

关键完整代码

// encoder_decoder.v - 增量式编码器4倍频解码与速度测量
module encoder_decoder #(
    parameter PPR = 1000,          // 编码器每圈脉冲数
    parameter CLK_FREQ = 50_000_000, // 系统时钟频率(Hz)
    parameter SPEED_WINDOW = 10_000   // 速度测量窗口(时钟周期数)
)(
    input  wire clk,               // 系统时钟
    input  wire rst_n,             // 异步复位,低有效
    input  wire encoder_a,         // 编码器A相
    input  wire encoder_b,         // 编码器B相
    output reg  [31:0] position,   // 当前位置(4倍频计数)
    output reg  [31:0] speed       // 速度(脉冲数/秒,按窗口计算)
);

    // 同步采样与去毛刺(两级触发器)
    reg a_sync1, a_sync2, b_sync1, b_sync2;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            a_sync1 <= 1'b0; a_sync2 <= 1'b0;
            b_sync1 <= 1'b0; b_sync2 <= 1'b0;
        end else begin
            a_sync1 <= encoder_a;
            a_sync2 <= a_sync1;
            b_sync1 <= encoder_b;
            b_sync2 <= b_sync1;
        end
    end

    // 状态编码:{A,B} 组合
    wire [1:0] state = {a_sync2, b_sync2};
    reg  [1:0] prev_state;

    // 位置计数
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            position <= 32'd0;
            prev_state <= 2'b00;
        end else begin
            prev_state <= state;
            case ({prev_state, state})
                // 正转:状态按 00→01→11→10→00 循环
                4'b0001, 4'b0111, 4'b1110, 4'b1000: position <= position + 1;
                // 反转:状态按 00→10→11→01→00 循环
                4'b0010, 4'b1011, 4'b1101, 4'b0100: position <= position - 1;
                default: position <= position; // 无效跳变或静止
            endcase
        end
    end

    // 速度测量(M法:固定窗口内计数位置增量)
    reg [31:0] window_cnt;
    reg [31:0] pos_snapshot;
    reg [31:0] pos_delta;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            window_cnt <= 32'd0;
            pos_snapshot <= 32'd0;
            pos_delta   <= 32'd0;
            speed       <= 32'd0;
        end else begin
            if (window_cnt == SPEED_WINDOW - 1) begin
                // 窗口结束:计算速度并重置
                pos_delta <= position - pos_snapshot;
                speed <= (position - pos_snapshot) * CLK_FREQ / SPEED_WINDOW;
                pos_snapshot <= position;
                window_cnt <= 32'd0;
            end else begin
                window_cnt <= window_cnt + 1;
            end
        end
    end

endmodule

仿真测试代码

// tb_encoder_decoder.v - 仿真激励
`timescale 1ns / 1ps
module tb_encoder_decoder;

    reg clk = 0;
    reg rst_n = 0;
    reg a = 0, b = 0;
    wire [31:0] position, speed;

    // 实例化被测模块
    encoder_decoder #(
        .PPR(1000),
        .CLK_FREQ(50_000_000),
        .SPEED_WINDOW(1000)  // 缩短窗口便于仿真观察
    ) uut (
        .clk(clk), .rst_n(rst_n),
        .encoder_a(a), .encoder_b(b),
        .position(position), .speed(speed)
    );

    // 50MHz时钟
    always #10 clk = ~clk;

    // 编码器信号生成:模拟正转(每步1个时钟周期)
    task step_forward;
        begin
            // 状态序列:00→01→11→10
            #20 a = 0; b = 1;  // 01
            #20 a = 1; b = 1;  // 11
            #20 a = 1; b = 0;  // 10
            #20 a = 0; b = 0;  // 00
        end
    endtask

    // 编码器信号生成:模拟反转
    task step_backward;
        begin
            // 状态序列:00→10→11→01
            #20 a = 1; b = 0;  // 10
            #20 a = 1; b = 1;  // 11
            #20 a = 0; b = 1;  // 01
            #20 a = 0; b = 0;  // 00
        end
    endtask

    initial begin
        // 复位
        rst_n = 0;
        #100;
        rst_n = 1;
        #100;

        // 正转4个脉冲(4倍频后应计16)
        repeat(4) step_forward();
        #100;
        $display("正转4个脉冲后 position = %0d (期望16)", position);

        // 反转2个脉冲(应减8)
        repeat(2) step_backward();
        #100;
        $display("反转2个脉冲后 position = %0d (期望8)", position);

        // 等待速度窗口完成
        #20000;
        $display("速度测量值 speed = %0d 脉冲/秒", speed);

        #500;
        $finish;
    end

endmodule

运行与验证方法

  1. 仿真验证:在Vivado中创建工程,添加上述两个文件,运行行为仿真。观察position信号:正转4个完整周期(16个状态跳变)后,位置应为16;反转2个周期后,位置应为8。速度值在窗口结束后更新,可通过波形或打印信息确认。
  2. 硬件验证:将编码器A/B信号接入FPGA的GPIO引脚(需添加约束文件),使用ILA(集成逻辑分析仪)抓取position和speed信号。手动旋转编码器,观察位置值随旋转方向增减,速度值随旋转快慢变化。

通过判定标准

  • 仿真中位置计数与手动计算的理论值完全一致(正转加、反转减,4倍频关系正确)
  • 速度输出在窗口结束后更新,且数值与设定转速成线性关系
  • 编码器静止时位置保持不变,速度趋近于0

常见问题

  • 计数方向相反:交换A/B两相输入接线,或在代码中交换状态转移判断
  • 位置抖动:编码器信号需加RC滤波或使用施密特触发器整形,确保信号边沿干净
  • 速度测量窗口选择:窗口过短导致速度分辨率低,过长则响应慢;需根据实际转速范围权衡