☰
FPGA实现RGMII以太网MAC:架构拆解与上板调试实战
2026/10/19 13:38:55 网站建设 项目流程

简介:基于RGMII的以太网MAC的FPGA实现资源包,面向有一定Verilog/VHDL基础、希望深入掌握以太网协议与FPGA接口设计的开发者。资源围绕RGMII接口与PHY芯片交互,涵盖MAC层逻辑、状态机、CRC校验、时序约束、仿真验证及软件驱动等关键环节,并以Quartus工程形式提供完整代码与配置。压缩包共728个文件,约22.91MB,主要包含Quartus工程文件(qpf/qsf)、Verilog/VHDL源码(v/vhd/tdf)、仿真波形与脚本(do/wlf)、说明文档(pdf/docx)以及大量编译生成文件(cdb/hdb/rpt等),便于对照学习和二次开发。已有2938人学习下载,既可作为FPGA以太网通信入门进阶的参考实例,也能为搭建RGMII数据通路、调试PHY寄存器提供可复用的工程模板。通过阅读源码与仿真结果,可快速理解MAC帧组装、MDIO配置和RGMII时序配合等实用知识点。

1. 基于RGMII的以太网MAC的FPGA实现:先从一次上板翻车说起

基于RGMII的以太网MAC的FPGA实现,听起来像是一个标准外设的demo,但真正把它跑上板子那一刻,你会发现仿真里那些绿色的通过波形并不能证明什么。我第一次做完这块逻辑时,link能起来,却丢包,CRC校验失败的报错不停刷,前后折腾了两周才发现问题出在RGMII时钟相位约束上。

这份资源不是一份简单的演示工程,而是一条能走通的完整链路:MAC核心、RGMII I/O适配、CRC算法、寄存器控制,以及配套的仿真和约束参考。它解决的是“在FPGA里做出一个能接标准千兆PHY的以太网控制器”这件事,适合正在做SoC原型、数据采集板卡、或者想在自己的逻辑里加网络通道的开发者。

你可以照着它把工程跑起来,也可以只取TX/RX状态机和CRC模块,接到自己的数据通路上。接下来我按照我拆这个项目时的路线,把架构、代码、跑起来的步骤和踩过的坑一次说清楚。

2. 整体架构拆解:MAC 核心、RGMII I/O 与寄存器怎么分工

2.1 顶层模块划分和数据流向

在动手读代码之前,先看模块图。这套MAC大致拆成六个部分:mac_user_if负责与业务逻辑对接,mac_tx完成组帧和FCS插入,mac_rx做帧解析和CRC校验,mac_reg是一组AXI4-Lite控制寄存器,rgmii_io处理DDR信号和可调延迟,再加一个async_fifo把PHY返回的RX时钟域搬到MAC时钟域。

模块时钟域职责
mac_user_ifclk_mac接收用户帧数据,封装成 AXI4-Stream
mac_txclk_mac前导码、SFD、填充、CRC32 插入
mac_rxclk_rx / clk_mac帧解析、剥离前导、CRC 校验
mac_regclk_macAXI4-Lite 寄存器堆,控制/状态/统计
rgmii_ioclk_tx / clk_rxDDR 收发、时钟输出、可调延迟
async_fifoclk_rx -> clk_mac跨时钟域隔离

数据流向分两步走。发送路径很简单:mac_user_if收下用户侧写进来的完整帧,交到mac_tx,mac_tx加前导码、SFD、填充和CRC32,然后通过rgmii_io把字节拆成两个半字节,在125MHz双沿发送。接收路径多一步跨时钟域处理:PHY把恢复出来的rx_clk和数据一起给FPGA,rgmii_io用rx_clk采集并组装成字节,先放进async_fifo,再由mac_rx在mac_clk侧读出并解析。

模块划分上有一条原则我提过很多次:各时钟域之间不要互相碰。mac_tx和mac_rx虽然都是MAC核心,但mac_tx工作在本地时钟,mac_rx的自然工作时钟是PHY提供的rx_clk,跨到mac_clk之后才做协议处理。这样不至于让综合工具把两个时钟域的路径混在一起优化,时序收敛会容易很多。

2.2 RGMII 接口时序:DDR 采样怎么落到 FPGA 引脚

RGMII比标准MII少了一半引脚。数据线只有txd/rxd各4bit,千兆模式下时钟是125MHz,DDR双沿采样,所以4bit × 125MHz × 双沿 = 1Gbit/s,正好对上。10/100M模式下调到2.5MHz/25MHz,仍然是DDR。只要记住一句话:RGMII所有数据都是双沿有效,控制信号也是双沿有效。

控制信号的处理是初学时最容易懵的地方。发送方向tx_ctl由tx_en和tx_err异或得到,正常传帧时为高,帧间隙为低。在RGMII标准里这其实是一种奇偶编码,接收方拿到tx_ctl之后,结合当前数据半字节的奇偶校验可以判断是普通数据还是错误标志。常见工程里不强制做这种奇偶校验,遇到少数PHY开启这个检查,先在PHY寄存器里关掉即可。

时钟策略上,我一般会做两层延迟处理。发送方向把GTX_CLK经过ODDR打出去当tx_clk,同时给txd和tx_ctl加一个约2ns的数据相对时钟延迟;接收方向把rx_clk接到BUFG作为接收采样时钟,进IDELAYE2调节数据相位,之后再用异步FIFO与本地时钟隔离。这套做法的原因是RGMII规范约定数据与时钟之间的相位在到达对端时应该满足建立/保持窗口,而这个窗口在上板后要靠约束和可调延时来保证。

2.3 管理接口与寄存器:MDIO 和 PHY 状态怎么读

寄存器管理模块是整个MAC的控制入口,常见配置包括软复位、混mac模式、速度选择、本地MAC地址、统计计数。下面是一张典型的寄存器映射表,地址偏移按32bit对齐。

偏移名称读写说明
0x00控制寄存器RW软复位、速度选择、promiscuous 模式
0x04状态寄存器ROlink up、工作速率、full duplex
0x08MAC 地址低 32 位RW过滤目的 MAC 低 4 字节
0x0CMAC 地址高 16 位RW过滤目的 MAC 高 2 字节 + 使能位
0x10统计寄存器RO收到的帧数、CRC 错误数

MDIO是标准的双线管理接口,MDC由MAC侧产生,MDIO是双向信号。读时序是这样的:32个1的前导码、2bit的ST(01)、2bit的OP(10读/01写)、5bit的PHY地址、5bit的寄存器地址、2bit的turnaround,之后才是数据。写操作没有turnaround需要。实现时最容易漏的是读操作在turnaround阶段要释放MDIO总线,让PHY驱动数据,如果三态门控制晚了,读回来的就是一串0。

上板调试时我习惯先通过MDIO读PHY ID寄存器确认管理链路通,再配置自适应或强制速率。这一步能过,说明MDIO时序基本没有问题,后面所有link层问题都可以跟它隔离开。

3. 核心代码级拆解:TX 状态机、RX 状态机与 CRC32

3.1 TX 发送路径:从用户数据到 RGMII 引脚

发送状态机是整个MAC里最直观的一块。它要完成的事情是:等用户数据有效,发出前导码0x55和SFD 0xD5,再逐字节发用户数据,不够60字节就补0,最后发CRC32并对间隔计时。代码里我按字节来计数,RGMII的双沿转换放到IO层处理,状态机本身不需要关心半字节。

// mac_tx.v 发送状态机(简化版) localparam IDLE=3'd0, PREAMBLE=3'd1, DATA=3'd2, PAD=3'd3, FCS=3'd4, IFG=3'd5; reg [2:0] tx_state; reg [15:0] tx_cnt; // 已经发出的字节数 reg [31:0] crc; // 当前 CRC reg [7:0] tx_byte; // 当前要发的字节 reg tx_valid; always @(posedge clk_mac or negedge rst_n) begin if (!rst_n) begin tx_state <= IDLE; tx_cnt <= 16'd0; crc <= 32'hFFFFFFFF; tx_byte <= 8'd0; tx_valid <= 1'b0; end else begin case (tx_state) IDLE: begin if (s_axis_tvalid) begin tx_state <= PREAMBLE; tx_cnt <= 16'd0; crc <= 32'hFFFFFFFF; end end PREAMBLE: begin // 前导码 7 字节 0x55,第 8 字节为 SFD 0xD5 tx_byte <= (tx_cnt == 8'd7) ? 8'hD5 : 8'h55; tx_valid <= 1'b1; tx_cnt <= tx_cnt + 1'b1; if (tx_cnt == 8'd8) tx_state <= DATA; end DATA: begin if (s_axis_tvalid) begin tx_byte <= s_axis_tdata; tx_valid <= 1'b1; crc <= crc32_next(s_axis_tdata, crc); tx_cnt <= tx_cnt + 1'b1; if (s_axis_tlast) begin if (tx_cnt + 1 < 60) tx_state <= PAD; else tx_state <= FCS; end end end PAD: begin // 不足 60 字节补 0,同时继续累计 CRC tx_byte <= 8'd0; tx_valid <= 1'b1; crc <= crc32_next(8'd0, crc); tx_cnt <= tx_cnt + 1'b1; if (tx_cnt == 60) tx_state <= FCS; end FCS: begin // FCS 先取反再发,先发最低字节 tx_byte <= ~crc[7:0]; tx_valid <= 1'b1; crc <= {8'd0, crc[31:8]}; tx_cnt <= tx_cnt + 1'b1; if (tx_cnt[1:0] == 2'b11) tx_state <= IFG; end IFG: begin tx_valid <= 1'b0; if (tx_cnt == 12) tx_state <= IDLE; end default: tx_state <= IDLE; endcase end end

参数说明:tx_cnt在PREAMBLE阶段从0到8,发出8字节的前导加SFD;在DATA阶段累计的是用户有效载荷字节数;FCS阶段tx_cnt的低两位用来数4个CRC字节,高位置不需要关心。IFG要求帧间距至少96bit,对应12字节,所以计数器走到12才回IDLE。

这个状态机里PAD状态的出口时序值得注意:DATA阶段结束时如果用户帧太长,直接进FCS;太短则进PAD补0,直到tx_cnt等于60。这里的tx_cnt + 1 < 60判断是为了处理tlast和tvalid同时拉高的那一拍,避免少计一个字节。

RGMII的DDR输出在IO层完成,核心代码里只需要给出字节和有效标志。IO层的做法是用ODDR原语,把字节的低半字节放到上升沿、高半字节放到下降沿:

// 把字节拆成两个半字节,DDR 输出到 RGMII 引脚 wire txd_low = tx_byte[3:0]; wire txd_high = tx_byte[7:4]; ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) oddr_txd0 ( .Q(rgmii_txd[0]), .D1(txd_low[0]), .D2(txd_high[0]), .CE(1'b1), .R(1'b0), .S(1'b0), .C(rgmii_tx_clk) );

DDR_CLK_EDGE选SAME_EDGE,意思是两个输入都在时钟上升沿之后被采样,输出在上升沿给D1、下降沿给D2。这样组合逻辑到ODDR输入端的路径被限制在同一时钟沿,时序更容易约束,不会出现半个字节的组合偏差。

3.2 RX 接收路径:从 RGMII 采样到 CRC 校验

接收侧的状态机比发送侧要小心,因为帧结束不是由用户告诉你的,而是从rx_ctl拉低判断的。RGMII的rx_ctl在高电平期间,数据线一直有效,包括前导、数据段和CRC段。所以在RX状态机里,CRC的累计要从SFD之后开始,直到rx_ctl拉低那一拍结束。

// mac_rx.v 接收状态机(时钟域:clk_rx) localparam RX_IDLE=3'd0, RX_PRE=3'd1, RX_DATA=3'd2; reg [2:0] rx_state; reg [31:0] crc_acc; // CRC 累加器 reg [15:0] rx_cnt; // 收到的字节数 reg [7:0] rx_last_byte; reg fifo_wr_en; always @(posedge clk_rx or negedge rst_n) begin if (!rst_n) begin rx_state <= RX_IDLE; crc_acc <= 32'hFFFFFFFF; rx_cnt <= 16'h0; rx_last_byte <= 8'h0; fifo_wr_en <= 1'b0; end else begin case (rx_state) RX_IDLE: begin if (rgmii_rx_ctl) begin rx_cnt <= 16'h0; crc_acc <= 32'hFFFFFFFF; rx_state <= RX_PRE; end end RX_PRE: begin if (rgmii_rx_ctl) begin // 等待 SFD:上一拍是 0x55,这一拍是 0xD5 if (rx_last_byte == 8'h55 && rx_byte == 8'hD5) rx_state <= RX_DATA; else if (rx_byte != 8'h55) rx_state <= RX_IDLE; rx_last_byte <= rx_byte; end else rx_state <= RX_IDLE; end RX_DATA: begin if (rgmii_rx_ctl) begin // 从目的 MAC 地址开始累计 CRC,直到 FCS 结束 crc_acc <= crc32_next(rx_byte, crc_acc); rx_cnt <= rx_cnt + 1'b1; fifo_wr_en <= 1'b1; // 有效数据写 FIFO end else begin // 帧结束,FCS 已经包含在 CRC 累加结果中 fifo_wr_en <= 1'b0; if (crc_acc == 32'h2144DF1C) stats_rx_good_frames <= stats_rx_good_frames + 1'b1; else stats_rx_crc_errors <= stats_rx_crc_errors + 1'b1; rx_state <= RX_IDLE; end end default: rx_state <= RX_IDLE; endcase end end

这里的关键参数是crc_acc的最终检查值0x2144DF1C。以太网CRC32是反射多项式实现,发送端在数据末尾追加取反后的CRC,接收端把数据连同FCS一起算完之后,累加器残值固定是0x2144DF1C,不是0。这个值如果记错或者字节序不对,上板就会出现“明明帧是对的,却报CRC错误”的灵异现象。

接收字节的组装跟发送正好相反。RGMII先到低半字节,使用IDDR在同一时钟沿拿到两个半字节,拼成完整字节:

// 使用 IDDR SAME_EDGE 模式,在 clk_rx 上升沿同时得到两个半字节 IDDR #(.DDR_CLK_EDGE("SAME_EDGE")) iddr_rxd0 ( .Q1(rxd_low[0]), // 上升沿数据 .Q2(rxd_high[0]), // 下降沿数据 .C(rgmii_rx_clk), .CE(1'b1), .R(1'b0), .S(1'b0), .D(rgmii_rxd[0]) ); assign rx_byte = {rxd_high, rxd_low};

特别提醒一下:rx_ctl拉低的判断要跟数据对齐。有些PHY在rx_ctl释放时,数据线还会保持一拍,如果状态机在释放瞬间就去读rx_byte,会误吃一个无效字节。常见做法是在接收路径里加一级打拍,让rx_ctl和rx_byte对齐后再进状态机。

3.3 CRC32 的两种写法与 FCS 插入

CRC32模块是MAC核心里的数学担当。以太网用的多项式是0x04C11DB7,但实现时用的是反射形式0xEDB88320。逐字节算法在工程里最常见,也最容易看懂:

// 标准 CRC-32/以太网逐字节函数 function [31:0] crc32_next; input [7:0] din; input [31:0] crc_reg; integer i; reg [31:0] c; reg [7:0] d; begin c = crc_reg; d = din; for (i = 0; i < 8; i = i + 1) begin if (c[0] ^ d[0]) c = (c >> 1) ^ 32'hEDB88320; else c = c >> 1; d = d >> 1; end crc32_next = c; end endfunction

参数说明:crc_reg是上一拍算完的CRC值,初始要置成0xFFFFFFFF;din是当前字节;循环里的c[0] ^ d[0]是在做比特级的移位异或,低位先算,这是反射CRC的特征。发送方向在FCS阶段取~crc输出,接收方向把所有字节算完后检查0x2144DF1C残值。

如果时钟频率紧张,逐字节函数里8次迭代的组合链路可能成为关键路径。我会在性能吃紧时改成按4bit计算的版本,每个RGMII边沿只算一个半字节,逻辑深度减半。代价是状态机和CRC函数的接口从“字节”变成“半字节”,但RGMII本来就是双沿数据,天然适合这种写法。实际上很多商用IP核就是这么处理的,把CRC计算直接嵌进DDR时序里。

4. 跑起来:仿真建模、约束文件与上板验证

4.1 写一个 RGMII PHY 行为模型当 testbench

MAC链路没法单独仿真,需要一个假的PHY在testbench里陪跑。这个行为模型不必实现完整的RGMII协议,但至少要把DDR时序和链路延迟模拟出来。最简单的做法是把MAC侧的txd、tx_ctl延迟一段固定时间后回送到rxd、rx_ctl,同时把tx_clk也延迟后作为rx_clk返回。

// tb_rgmii_mac.v:简易 PHY 行为模型 reg [3:0] rgmii_rxd; reg rgmii_rx_ctl; reg rgmii_rx_clk; // 把 MAC 的发送时钟延迟后作为接收时钟回送 always @(posedge rgmii_tx_clk) begin #2.0; rgmii_rx_clk <= rgmii_tx_clk; rgmii_rxd <= rgmii_txd; rgmii_rx_ctl <= rgmii_tx_ctl; end // 模拟 PHY 主动发送一帧:先把字节拆成两个半字节 task drive_frame_byte(input [7:0] d); begin @(posedge rgmii_rx_clk); #1.8 rgmii_rxd = d[3:0]; rgmii_rx_ctl = 1'b1; @(negedge rgmii_rx_clk); #1.8 rgmii_rxd = d[7:4]; end endtask

这里的#2.0和#1.8是故意模拟PHY和PCB走线引入的数据相对时钟skew。你的IDELAY配置和XDC里的set_input_delay能不能处理这个skew,在仿真里就能看出来。

testbench的激励顺序我一般是这样:先通过AXI4-Lite配置MAC地址和速度寄存器,再往mac_user_if写一帧已知数据,等待RX输出,在接收侧比对数据和CRC。如果回环路径都能通过,再放宽#2.0到#3.0,看IDELAY的调节余量够不够。

4.2 XDC 约束里最关键的几条

RGMII能不能稳定工作,一半看代码,一半看约束。约束的核心是让工具知道rgmii_rx_clk的周期,以及引脚上数据和时钟之间的延迟窗口。

create_clock -name clk_mac -period 8.000 [get_ports clk_mac] create_clock -name rgmii_rx_clk -period 8.000 [get_ports rgmii_rx_clk] # 接收数据相对 rx_clk 的建立/保持窗口 set_input_delay -clock rgmii_rx_clk -max 2.500 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}] set_input_delay -clock rgmii_rx_clk -min 1.000 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}] # 发送数据相对 tx_clk 的要求,模拟 RGMII 约 2ns 的数据偏斜 set_output_delay -clock rgmii_tx_clk -max 0.500 [get_ports {rgmii_txd[*] rgmii_tx_ctl}] set_output_delay -clock rgmii_tx_clk -min -0.500 [get_ports {rgmii_txd[*] rgmii_tx_ctl}]

参数说明:接收方向的set_input_delay告诉工具数据在时钟沿之后1.0到2.5ns到达,综合工具会据此约束IDDR的建立时间;发送方向的负min值表示数据比时钟沿早0.5ns变化,实质上就是让时钟对数据的相位往后推,满足RGMII约2ns的skew要求。

还有一条很多人会漏:rgmii_rx_clk是从普通引脚进来的,如果它没有接到时钟专用引脚,Vivado会报CLOCK_DEDICATED_ROUTE错误。实际板卡上应该把RGMII的rx_clk接到MRCC或SRCC时钟引脚,如果设计上实在绕不开,可以临时用set_property CLOCK_DEDICATED_ROUTE ANY_CMT_COLUMN让它走普通布线,但这只适合调试,量产板还是要遵守引脚规划。

4.3 上板验证流程:回环测试、抓包和统计寄存器

上板之后不要急着接PC,先做三级验证,每一步都盯住统计寄存器。第一步是MDIO读PHY ID,确认管理链路通;第二步是PHY回环,把PHY的loopback寄存器打开,让RX和TX在PHY内部短接,验证MAC到PHY之间的RGMII时序;第三步才接网线跟PC通信。

PC端验证命令很常规,但最好配合统计寄存器一起看:

ethtool eth0 # 确认 link 状态和速率 ifconfig eth0 192.168.1.20 netmask 255.255.255.0 up ping -c 100 192.168.1.1

如果ping正常,再看MAC内部的stats_rx_good_frames和rx_crc_errors两个计数。只要后者持续增长,哪怕ping没断,也说明RGMII时序有隐患,切换大流量时一定会爆发。

我习惯在验证阶段加一个内部回环模式:MAC发出的帧不走PHY,直接在RGMII引脚处用MUX把发送数据接回接收路径。这样可以在没有外部网络环境的情况下长时间压测,把IDELAY、FIFO、CRC逻辑全部覆盖一遍。

5. 避坑记录:RGMII 调试中常见的 5 个问题

5.1 CRC 错误率居高不下

现象:link正常,ethtool -S里rx_crc_errors每隔几秒就上涨,ping偶尔通偶尔超时。

原因:这是RGMII调试里最常见的翻车点,十有八九是txd/tx_ctl相对tx_clk的相位不对。RGMII要求发送数据相对时钟有约2ns的偏斜,但代码里如果直接把ODDR的输出和经过BUFG的时钟同时给引脚,数据和时钟几乎零偏斜,对端PHY采样就会踩在数据跳变沿上。

解决:在XDC里把输出延迟约束做上,用ODELAYE2给txd加固定tap,或者在时钟路径上想办法把tx_clk多延一下。我自己的做法是先跑一遍时序报告,看rgmii_txd到regmii_tx_clk的output delay slack,再反推IDELAY tap数,而不是盲改代码。

5.2 千兆正常、百兆不通

现象:把PHY强制到100M后,link能起来,但大包不通,碰撞错误和小帧错误一起涨。

原因:千兆和百兆下RGMII虽然都是DDR,但时钟频率差了5倍。很多设计只在千兆下调过IDELAY的tap值,切到百兆后延迟参数还停在千兆档位,采样点偏了。另外从125MHz切到25MHz时,如果发送时钟MUX切换瞬间产生毛刺,PHY端会直接丢link。

解决:把RGMII的发送时钟做成可切换的两路,一路125MHz、一路25MHz,用寄存器的速度字段控制MUX,切换后等PHY重新建立link。接收侧的IDELAY也需要按速率配置不同的tap值,我一般预留一组软寄存器,在MDIO读到link速度后自动更新延迟。

5.3 MDIO 读不到 PHY ID

现象:上板后读PHY的ID寄存器,返回值要么是0xFFFF,要么全0。

原因:读操作的两拍turnaround时序里,MDIO三态门释放晚了。PHY要在这两拍里驱动MDIO总线,如果总线还握在MAC手里,读到的就是MAC自己拉的电平。

解决:写一个专门的状态,在turnaround的第一拍置MDIO为高阻,第二拍结束之后才采样数据。三态控制信号和MDIO数据之间还要加一拍延迟匹配,这个在仿真里仿真不出来的,只能靠逻辑分析仪抓波形比对。

5.4 大流量下偶发丢包

现象:小包ping没问题,用iperf跑UDP大流量,两端统计丢包率在万分之一到千分之一之间浮动。

原因:接收侧async_fifo的满信号处理得不干净。rx_ctl释放的那一拍,状态机虽然已经判断帧结束,但FIFO的写使能可能还多拉一个时钟周期,多写了一个无效字节;或者FIFO的满信号没有及时同步到clk_rx域,导致写入端在满之后继续写。

解决:在MAC核心和FIFO之间插入一个两个时钟域的握手信号,帧结束时先把状态同步到clk_rx域,等FIFO确认当前帧已经写入完成,再复位下一帧的写指针。满信号用格雷码同步两次,避免亚稳态带入逻辑。

5.5 把 rx_clk 当普通时钟用

现象:时序报告全绿,上板跑着跑着整个链路崩溃,复位也不好使。

原因:rx_clk来自PHY,频率和相位都不归你管。如果布局布线时它没有走到全局时钟网络,或者complier把它当普通数据信号处理,时钟偏斜会直接吃掉IDDR的建立时间,崩溃只是时间问题。

解决:在RTL里明确把rgmii_rx_clk接到BUFG,在XDC里create_clock声明这个时钟,并且用set_property CLOCK_DEDICATED_ROUTE把输入路径绑定到时钟专用引脚。从那以后我每次新建工程,第一件事就是检查时钟引脚分配,而不是等上板后再排查。

6. 二次开发思路:把它接到你自己的业务模块上

6.1 替换 AXI-Stream 为用户 FIFO 接口

这套MAC的用户侧默认走AXI4-Stream,但很多自研模块用的是简单的FIFO读写时序。改起来不复杂,核心是把tvalid/tready握手机制翻译成wr_en/wr_ready,把tdata直接接到FIFO数据总线。

// 把 AXI-Stream 转换成简单 FIFO 写侧 assign wr_en = s_axis_tvalid; assign wr_data = s_axis_tdata; assign s_axis_tready = fifo_wr_ready;

这里要注意tlast的处理。AXI-Stream里一帧的结束靠tlast标识,改成FIFO接口后,用wr_cnt记录当前帧长度,wr_en拉低即代表帧结束。如果你的业务模块本身能给出帧长度,直接在MAC配置寄存器里预写长度也行,少走一根信号。

6.2 加 MAC 地址过滤和 VLAN 处理

默认情况下MAC核心工作在混杂模式,所有帧都会上传。要做目标地址过滤,在RX_DATA状态里额外维护一个字节偏移计数,前6个字节就是目的MAC。每收一个字节,跟本地mac_reg里的地址逐字节比较,全部匹配则正常写FIFO,否则把后续字节继续吸收但丢弃fifo_wr_en。注意不要在中途关闭状态机,否则剩下来的帧尾会被当成新帧头,反而制造错误帧。

VLAN处理更简单。在字节计数到12和13时判断EtherType是否为0x8100,如果是,VLAN tag后面还有2字节的TPID和2字节的TCI,真正的帧长度要从16字节之后开始算。很多MAC核是直接用硬件跳过这4字节,把VLAN帧当作普通帧交给上层,由软件去剥。

6.3 怎么验证你的改动

每次改完接口或者加过滤逻辑,我最怕看到红色断言,所以仿真testbench里固定存了三帧基准数据:一帧最小长度帧、一帧1522字节带VLAN的最大帧、一帧CRC人为改错的坏帧。跑完这三帧,寄存器里的统计值必须落在预期区间。

然后回到上板,用内部回环模式先验证你自己的业务模块和MAC之间的数据一致性,再用PC侧双向压测。我后来养成了一个习惯:每次改完MAC,先跑一段长时间回环再做速度切换,否则我不动下一步。这种接口设计好不好,只看仿真是不够的,时序和PHY的协同只能靠上板说话。

这套RTL工程、testbench和XDC约束文件可以直接拿来做底子,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询