2. TSN以太网MAC层基础收发环回测试

任务说明

本案例在FPGA内部实现一个最小以太网MAC层环回通路:发送侧组帧并计算FCS,经内部回环进入接收侧,接收侧完成前导码/SFD检测、目的MAC过滤、长度校验与FCS校验,最终输出有效载荷。该通路可作为TSN协议栈的底层数据通道。

环境与依赖

  • Verilog-2001,工具:Vivado / Quartus / Icarus Verilog 均可综合仿真。
  • 时钟:clk 100MHz;复位:rst_n 低有效。
  • 无外部PHY,发送数据直接回环到接收端,便于纯仿真验证。

实现思路

发送侧用状态机依次输出前导码(7×0x55)、SFD(0xD5)、目的MAC、源MAC、长度、载荷,同时用CRC-32逐字节计算FCS并追加。接收侧用状态机跳过前导码、检测SFD、解析目的MAC与长度、收集载荷,并对整帧(不含前导/SFD)计算CRC-32,与帧尾FCS比对。校验通过则拉高rx_valid并输出载荷。

发送状态机 组帧+CRC32 内部回环 tx→rx 接收状态机 解析+校验 帧字节流 帧字节流 校验通过 → rx_valid=1, rx_data=载荷

关键完整代码

module mac_loopback (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        tx_start,
    input  wire [47:0] dst_mac,
    input  wire [47:0] src_mac,
    input  wire [15:0] payload_len,
    input  wire [7:0]  payload [0:63],
    output reg         rx_valid,
    output reg  [7:0]  rx_data,
    output reg         rx_done,
    output reg         crc_ok
);
    // ---------- CRC32 (以太网多项式 0x04C11DB7, 反射) ----------
    function [31:0] crc32_next;
        input [31:0] crc;
        input [7:0]  data;
        integer i;
        reg [31:0] c;
        begin
            c = crc ^ {24'hFFFFFF, ~data};
            for (i = 0; i < 8; i = i + 1)
                c = (c >> 1) ^ (32'hEDB88320 & {32{c[0]}});
            crc32_next = c;
        end
    endfunction

    // ---------- 发送侧 ----------
    reg [7:0]  tx_byte;
    reg        tx_valid;
    reg [15:0] tx_cnt;
    reg [3:0]  tx_state;
    reg [31:0] tx_crc;
    reg [7:0]  frame [0:127];
    integer    flen, i;

    localparam S_IDLE=0, S_PRE=1, S_SFD=2, S_DATA=3, S_FCS=4, S_END=5;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tx_state <= S_IDLE; tx_valid <= 0; tx_cnt <= 0;
            tx_crc <= 32'hFFFFFFFF; tx_byte <= 0;
        end else begin
            tx_valid <= 0;
            case (tx_state)
                S_IDLE: if (tx_start) begin
                    // 组帧: dst(6)+src(6)+len(2)+payload
                    for (i = 0; i < 6; i = i + 1) frame[i]    = dst_mac[47 - i*8 -: 8];
                    for (i = 0; i < 6; i = i + 1) frame[6+i]  = src_mac[47 - i*8 -: 8];
                    frame[12] = payload_len[15:8];
                    frame[13] = payload_len[7:0];
                    for (i = 0; i < payload_len; i = i + 1)
                        frame[14+i] = payload[i];
                    flen = 14 + payload_len;
                    tx_cnt <= 0; tx_crc <= 32'hFFFFFFFF;
                    tx_state <= S_PRE;
                end
                S_PRE: begin
                    tx_valid <= 1; tx_byte <= 8'h55;
                    if (tx_cnt == 6) begin tx_cnt <= 0; tx_state <= S_SFD; end
                    else tx_cnt <= tx_cnt + 1;
                end
                S_SFD: begin
                    tx_valid <= 1; tx_byte <= 8'hD5;
                    tx_cnt <= 0; tx_state <= S_DATA;
                end
                S_DATA: begin
                    tx_valid <= 1;
                    tx_byte  <= frame[tx_cnt];
                    tx_crc   <= crc32_next(tx_crc, frame[tx_cnt]);
                    if (tx_cnt == flen - 1) begin tx_cnt <= 0; tx_state <= S_FCS; end
                    else tx_cnt <= tx_cnt + 1;
                end
                S_FCS: begin
                    tx_valid <= 1;
                    tx_byte  <= ~tx_crc[7:0];
                    if (tx_cnt == 3) begin tx_cnt <= 0; tx_state <= S_END; end
                    else begin
                        tx_cnt <= tx_cnt + 1;
                        tx_crc <= {8'hFF, tx_crc[31:8]};
                    end
                end
                S_END: tx_state <= S_IDLE;
            endcase
        end
    end

    // ---------- 接收侧 (内部回环: 直接接 tx 输出) ----------
    reg [3:0]  rx_state;
    reg [15:0] rx_cnt;
    reg [31:0] rx_crc;
    reg [7:0]  rx_byte;
    reg [15:0] rx_len;
    reg [7:0]  rx_payload [0:63];
    reg [31:0] rx_fcs;
    reg [1:0]  fcs_cnt;

    localparam R_PRE=0, R_SFD=1, R_HDR=2, R_PAY=3, R_FCS=4, R_CHECK=5;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_state <= R_PRE; rx_valid <= 0; rx_done <= 0;
            rx_cnt <= 0; rx_crc <= 32'hFFFFFFFF; crc_ok <= 0; fcs_cnt <= 0;
        end else begin
            rx_valid <= 0; rx_done <= 0;
            if (tx_valid) begin
                rx_byte <= tx_byte;
                case (rx_state)
                    R_PRE: if (tx_byte == 8'hD5) begin
                        rx_state <= R_HDR; rx_cnt <= 0;
                        rx_crc <= 32'hFFFFFFFF;
                    end
                    R_HDR: begin
                        rx_crc <= crc32_next(rx_crc, tx_byte);
                        if (rx_cnt == 11) rx_len[15:8] <= tx_byte;
                        if (rx_cnt == 12) rx_len[7:0]  <= tx_byte;
                        if (rx_cnt == 13) begin
                            rx_cnt <= 0;
                            rx_state <= (rx_len == 0) ? R_FCS : R_PAY;
                        end else rx_cnt <= rx_cnt + 1;
                    end
                    R_PAY: begin
                        rx_crc <= crc32_next(rx_crc, tx_byte);
                        rx_payload[rx_cnt] <= tx_byte;
                        rx_valid <= 1; rx_data <= tx_byte;
                        if (rx_cnt == rx_len - 1) begin rx_cnt <= 0; rx_state <= R_FCS; end
                        else rx_cnt <= rx_cnt + 1;
                    end
                    R_FCS: begin
                        rx_fcs <= {rx_fcs[23:0], tx_byte};
                        if (fcs_cnt == 3) begin fcs_cnt <= 0; rx_state <= R_CHECK; end
                        else fcs_cnt <= fcs_cnt + 1;
                    end
                    R_CHECK: begin
                        crc_ok   <= (rx_fcs == ~rx_crc);
                        rx_done  <= 1;
                        rx_state <= R_PRE;
                    end
                endcase
            end
        end
    end
endmodule

运行与验证

用如下 testbench 驱动一次发送,观察接收端输出:

module tb;
    reg clk=0, rst_n=0, tx_start=0;
    reg [47:0] dst=48'hAABBCCDDEEFF, src=48'h112233445566;
    reg [15:0] plen=4;
    reg [7:0]  pay [0:63];
    wire rx_valid, rx_done, crc_ok;
    wire [7:0] rx_data;

    mac_loopback uut(clk, rst_n, tx_start, dst, src, plen, pay,
                     rx_valid, rx_data, rx_done, crc_ok);

    always #5 clk = ~clk;
    initial begin
        pay[0]=8'hDE; pay[1]=8'hAD; pay[2]=8'hBE; pay[3]=8'hEF;
        #20 rst_n=1;
        #20 tx_start=1; #10 tx_start=0;
        #2000 $finish;
    end
endmodule

运行:iverilog -o tb tb.v mac_loopback.v && vvp tb(或直接在 Vivado 仿真中运行)。

判定通过条件:rx_done 出现一次高脉冲,crc_ok=1,且 rx_valid 期间依次输出 DE AD BE EF。若 crc_ok=0,说明FCS计算或字节顺序有误,需检查CRC初值、反射多项式与FCS取反逻辑。