3、ABZ信号建模:A/B/Z信号时序生成、正交解码原理、方向与速度提取算法

好,咱们进入第三章节。前面聊了编码器的分类和选型,现在得真刀真枪地处理信号了。ABZ信号,说白了就是编码器跟控制器对话的语言。你听不懂这门语言,电机就转不起来,或者转得乱七八糟。

我个人习惯,在FPGA里验证换相逻辑之前,先把ABZ信号的模型搭好。这一步做扎实了,后面调试能省一半时间。我记得刚入行那会儿,有次项目赶进度,跳过信号建模直接上板,结果方向判断老是反的,查了整整两天才发现是A相和B相的时序关系搞反了。嗯,从那以后,我再也不敢偷这个懒了。

3.1 A/B/Z信号时序生成

增量式编码器输出三路信号:A相、B相和Z相。A和B是正交方波,相位差90度。Z相是零位脉冲,每转一圈输出一个。

咱们先看一个典型的时序图,我习惯用这样的结构来理解:

ABZ信号时序关系图 A B Z 90° 90° A超前B 90° → 正转;B超前A 90° → 反转

你看这个图,A相上升沿的时候,B相是什么电平?如果是高电平,那就是正转。反过来,A相上升沿时B相是低电平,那就是反转。这就是正交解码最核心的判断依据。

在FPGA里生成这个时序,我一般用计数器加比较器的方式:

// ABZ信号生成模块(Verilog)
module abz_signal_gen #(
    parameter CLK_FREQ = 50_000_000,  // 系统时钟50MHz
    parameter PULSE_PER_REV = 1024    // 每转脉冲数
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       direction,      // 1:正转 0:反转
    input  wire       enable,
    output reg        a_phase,
    output reg        b_phase,
    output reg        z_phase
);

    // 每半个脉冲周期的时钟计数
    localparam HALF_PERIOD = CLK_FREQ / (PULSE_PER_REV * 2);
    reg [31:0] cnt;
    reg [9:0]  pulse_cnt;  // 脉冲计数器

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt <= 0;
            pulse_cnt <= 0;
            a_phase <= 0;
            b_phase <= 0;
            z_phase <= 0;
        end else if (enable) begin
            if (cnt < HALF_PERIOD - 1)
                cnt <= cnt + 1;
            else begin
                cnt <= 0;
                // A相翻转
                a_phase <= ~a_phase;
                // B相延迟90度(延迟HALF_PERIOD/2个时钟)
                // 实际项目中用状态机实现更可靠
                b_phase <= direction ? a_phase : ~a_phase;
                // Z相:每转一圈输出一个脉冲
                if (pulse_cnt == PULSE_PER_REV - 1) begin
                    pulse_cnt <= 0;
                    z_phase <= 1;
                end else begin
                    pulse_cnt <= pulse_cnt + 1;
                    z_phase <= 0;
                end
            end
        end
    end
endmodule
注意:上面的代码是简化模型,实际项目中B相不能直接用A相取反来生成。因为你需要精确控制90度相位差,而且要考虑方向变化时的无缝切换。我曾经在一个伺服项目里这么干过,结果方向切换时B相毛刺一大堆,差点把驱动器烧了。

3.2 正交解码原理

正交解码,说白了就是从A/B信号里提取出两个信息:转了多少步,往哪个方向转。你想想看,如果只有一路脉冲,你只能知道转了多少,但不知道方向。有了两路正交信号,这个问题就解决了。

解码的核心是状态机。A和B各有两种电平,组合起来就是4种状态。正转时状态按顺序循环:00 → 10 → 11 → 01 → 00。反转时反过来:00 → 01 → 11 → 10 → 00。

我习惯用查表法来实现,速度快,逻辑也清晰:

// 正交解码状态机
module quadrature_decoder (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       a_in,
    input  wire       b_in,
    output reg  [1:0] direction,  // 2'b01:正转 2'b10:反转 2'b00:静止
    output reg        pulse_out   // 每步一个脉冲
);

    reg [1:0] state, next_state;
    reg [1:0] ab_sync;

    // 边沿检测,防抖
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            ab_sync <= 2'b00;
            state <= 2'b00;
        end else begin
            ab_sync <= {a_in, b_in};
            state <= next_state;
        end
    end

    // 状态转移与输出
    always @(*) begin
        next_state = state;
        direction = 2'b00;
        pulse_out = 1'b0;

        case (state)
            2'b00: begin
                if (ab_sync == 2'b10) begin
                    next_state = 2'b10;
                    direction = 2'b01;  // 正转
                    pulse_out = 1'b1;
                end else if (ab_sync == 2'b01) begin
                    next_state = 2'b01;
                    direction = 2'b10;  // 反转
                    pulse_out = 1'b1;
                end
            end
            2'b01: begin
                if (ab_sync == 2'b00) begin
                    next_state = 2'b00;
                    direction = 2'b01;
                    pulse_out = 1'b1;
                end else if (ab_sync == 2'b11) begin
                    next_state = 2'b11;
                    direction = 2'b10;
                    pulse_out = 1'b1;
                end
            end
            2'b11: begin
                if (ab_sync == 2'b01) begin
                    next_state = 2'b01;
                    direction = 2'b01;
                    pulse_out = 1'b1;
                end else if (ab_sync == 2'b10) begin
                    next_state = 2'b10;
                    direction = 2'b10;
                    pulse_out = 1'b1;
                end
            end
            2'b10: begin
                if (ab_sync == 2'b11) begin
                    next_state = 2'b11;
                    direction = 2'b01;
                    pulse_out = 1'b1;
                end else if (ab_sync == 2'b00) begin
                    next_state = 2'b00;
                    direction = 2'b10;
                    pulse_out = 1'b1;
                end
            end
        endcase
    end
endmodule
小技巧:实际编码器信号会有抖动和毛刺,我建议在解码前加一级同步器和边沿检测。用三级寄存器打拍,可以有效滤除亚稳态。这个坑我踩过,不处理的话,方向误判率能到5%以上。

3.3 方向与速度提取算法

方向提取,上面已经讲了,通过状态机判断。但速度提取就有讲究了。常用的方法有两种:M法和T法。

方法 原理 适用场景 优缺点
M法 固定时间内计数脉冲数 高速(>100 RPM) 低速时误差大,高速时精度高
T法 测量相邻脉冲的时间间隔 低速(<100 RPM) 高速时精度下降,低速时很准
M/T法 结合两者,动态切换 全速域 实现复杂,但精度最高

我个人习惯在FPGA里用M/T法。低速用T法,高速用M法,中间有个过渡区做平滑切换。你看这个流程图:

M/T法速度提取流程图 脉冲输入 脉冲频率 > 阈值? 否 是 T法:测量脉冲间隔 M法:固定时间计数 速度 = 脉冲数 / 时间 速度值 + 方向

M法的实现很简单,开一个固定时间窗口(比如10ms),数这个窗口内来了多少个脉冲。速度 = 脉冲数 × 60 / (每转脉冲数 × 时间秒)。

T法稍微复杂一点,需要测量两个相邻脉冲之间的时间间隔。用高频时钟(比如100MHz)去计这个间隔的时钟周期数。速度 = 时钟频率 / (时钟周期数 × 每转脉冲数) × 60。

// M法速度计算
module m_method #(
    parameter CLK_FREQ = 50_000_000,
    parameter PULSE_PER_REV = 1024,
    parameter SAMPLE_TIME = 10_000_000  // 10ms采样窗口
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        pulse_in,
    output reg [31:0]  speed_rpm
);

    reg [31:0] pulse_cnt;
    reg [31:0] time_cnt;
    reg        sample_done;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pulse_cnt <= 0;
            time_cnt <= 0;
            sample_done <= 0;
        end else begin
            if (time_cnt < SAMPLE_TIME - 1) begin
                time_cnt <= time_cnt + 1;
                if (pulse_in) pulse_cnt <= pulse_cnt + 1;
                sample_done <= 0;
            end else begin
                // 计算速度:RPM = (脉冲数 × 60) / (PULSE_PER_REV × 采样时间秒)
                speed_rpm <= (pulse_cnt * 60 * 1000) / (PULSE_PER_REV * (SAMPLE_TIME / (CLK_FREQ / 1000)));
                pulse_cnt <= 0;
                time_cnt <= 0;
                sample_done <= 1;
            end
        end
    end
endmodule
关键点:速度提取的精度直接影响换相逻辑的稳定性。我建议在FPGA里同时实现M法和T法,根据当前速度动态选择。切换阈值设在100 RPM左右,过渡区做5%的滞回,避免频繁切换。

嗯,到这里ABZ信号的建模和提取就讲完了。你可能会问,为什么要把方向提取和速度提取分开讲?其实在FPGA里,这两个模块是并行工作的。方向解码器实时输出方向,速度计算器周期性更新速度值。两者互不干扰,这才是硬件设计的思维。