简介一份面向FPGA开发者的基于RGMII接口以太网MAC实现工程资料适合网络通信方向学生与硬件工程师参考。内容覆盖RGMII协议25MHz时钟、4路RX/TX数据线全双工、MAC层数据帧组装与解组装、VHDL/Verilog逻辑设计、时序约束、PHY芯片DP83848/LAN8720对接、IP核集成、仿真验证以及驱动/DMA协同等完整技术链路。资源共728个文件压缩包约22.91MB主体为Quartus工程文件.qpf/.qsf/.sdc、Verilog/VHDL源码.v/.vhd、ModelSim仿真波形.wlf及PDF/docx说明文档另含MDIO接口配置、PHY百兆配置等关键子模块代码。已有2938人学习下载。通过该工程可掌握RGMII时序控制、MAC状态机、CRC校验、错误检测与MDIO配置等设计要点并结合仿真记录快速梳理排错思路是一份兼顾原理与实操的FPGA网络接口设计参考。1. 基于RGMII的以太网MACFPGA实现到底卡在哪把以太网MAC跑在FPGA上说简单也简单说玄学也真玄学。RGMII这个接口看起来就12根线但真正上板调通、跑到线速、不丢包坑全藏在时序细节里。这份资源给的是完整的MAC层实现从RGMII收发器、MAC核状态机到CRC校验、FIFO缓存再到跨时钟域处理每一段都有对应代码。适合两类人一是刚接触以太网、想把UDP/IP协议栈往FPGA上搬的开发者二是已经在用别人IP核、但出问题不知从何下手、想自己掌握每一行逻辑的工程师。我拆完这套工程后最深的感受是MAC从来不是瓶颈RGMII的时序约束和跨时钟域才是决定能不能跑到千兆的关键。这篇文章就把整个实现链路拆开参数怎么设、代码怎么写、板上怎么调、哪些坑必须躲一并讲清。2. RGMII接口先搞懂为什么是它再谈怎么实现2.1 三选一GMII、RGMII、SGMII为何选RGMII以太网MAC和PHY之间的接口工程上最常碰到的就是GMII、RGMII、SGMII三种。GMII是8位并行数据接口发射时钟125MHz意味着每一拍要并行走8位数据在FPGA的引脚资源上开销很大——数据线、控制线加起来超过20根而且高速翻转带来的PCB布线压力也不小。SGMII则是串行接口用1.25Gbps的SerDes差分对走数据引脚省但需要在FPGA里例化高速收发器资源占用和复杂度并不低而且很多中小规模FPGA芯片根本没有SerDes模块。RGMII是Reduced GMII的缩写它的核心思路是把数据线从8位砍到4位用DDR双沿采样来弥补带宽。发射时钟125MHz上升沿和下降沿各送4位等效8位吞吐接收方向同理。12根线就能跑满千兆引脚占用小、FPGA内部逻辑简单、普通IO就能满足要求这也是绝大多数开发板、SoC的MAC-PHY接口选择RGMII的原因。资源包里的实现就是基于这个接口做的完整覆盖了MAC侧收发的全部逻辑。接口选型这件事直接决定后续工作量。选GMIIFPGA引脚多出一倍PCB绕线难度加大但逻辑简单选SGMII就得查芯片手册看有没有GTP/GTX没有就白搭。RGMII对于绝大多数项目来说是最平衡的那个点引脚少、逻辑适中、时序可控。要注意的是RGMII有两种模式——引脚上下拉配置决定是3.3V还是2.5V电平标准这个错了上板直接不亮。2.2 RGMII时序模型TSU、HOLD和时钟偏移怎么算RGMII的时序理解是整个实现能否成功的分水岭。发射方向上MAC在125MHz时钟驱动下把TXD[3:0]在时钟上升沿送低4位、下降沿送高4位TXC则是一个和TXD对齐的125MHz时钟。PHY在TXC的上下边沿分别采样。接收方向反过来PHY把RXC和RXD发给MACMAC需要在RXC的双沿采样RXD。这里就引出一个关键概念TXC和TXD之间是有相位关系的。RGMII规范要求TXC相对TXD有大约2ns的延迟这个延迟称为TSU/HOLD窗口。在FPGA实现中ODDR原语会自动把时钟和数据的相位关系处理好但前提是你得正确使用原语而不是用普通的组合逻辑去拼。我们看一组典型参数参数典型值说明时钟频率125 MHz周期8ns双沿等效250M采样TSU1 ns数据在时钟沿前需稳定的时间HOLD1.2 ns数据在时钟沿后需保持的时间数据线延迟0.2-1.5 ns受PCB走线和IOB延迟影响TXC-TXD偏移1.5-2.5 nsRGMII规范建议约2ns这段时序信息直接决定你后面写的约束文件长什么样也决定上板后能不能稳定跑在千兆速率而不随机丢包。RGMII的调节依赖于PHY侧的延时配置比如某些PHY芯片支持通过寄存器调整内部延时但如果MAC侧不做任何约束、直接把时钟和数据一起发出去PCB走线差异就会吃掉整个时序裕量。2.3 核心代码SDR转DDR是第一步PL端实现RGMII接口第一步做的是把内部的单沿数据转成双沿输出。用Xilinx的ODDR原语一句话就能解决// SDR转DDR在时钟上升沿送低4位下降沿送高4位 ODDR #( .DDR_CLK_EDGE (SAME_EDGE), // 双沿同一时刻输出 .INIT (1b0), .SRTYPE (SYNC) ) oddr_txd_inst ( .Q (rgmii_txd[3:0]), // 输出到PHY的数据线 .C (clk_125m), // 发射时钟125MHz .CE (1b1), .D1 (txd_low[3:0]), // 上升沿送低4位 .D2 (txd_high[3:0]), // 下降沿送高4位 .R (1b0), .S (1b0) );这段代码的逻辑不复杂ODDR在时钟上升沿把D1输出到Q下降沿把D2输出到Q。关键在SAME_EDGE模式——它让D1和D2在同一个时钟沿被锁存避免D2数据提前半个周期到达导致毛刺。实际工程里很多翻车就翻在这里用了OPPOSITE_EDGE模式结果D2数据需要提前送在跨时钟域场景下极易出亚稳态。接收方向用IDDR。注意IDDR的SAME_EDGE模式会在一个时钟周期后同时输出上下沿采到的两路数据这意味着接收数据天然有1拍延迟在MAC层做FIFO写入时序时要把这个延迟算进去否则首字节就错位。// DDR转SDR接收数据上下沿分别锁存 IDDR #( .DDR_CLK_EDGE (SAME_EDGE), .INIT (1b0) ) iddr_rxd_inst ( .Q1 (rxd_low[3:0]), // 上升沿采到的数据 .Q2 (rxd_high[3:0]), // 下降沿采到的数据 .C (clk_125m), .CE (1b1), .D (rgmii_rxd[3:0]), .R (1b0), .S (1b0) );还有一个细节RGMII的数据线都是4位宽但每个DDR时钟周期实际搬运了8位数据。所以内部数据总线宽度不是4位而是8位在FIFO写入端要以8位为单位组装否则帧数据错位后CRC必然报错。2.4 输入输出约束没有约束的RGMII就是裸奔代码写对了约束没写对照样跑不起来。RGMII约束的核心是set_input_delay和set_output_delay。以发射方向为例FPGA输出的TXD相对TXC的关系需要约束# 发射方向TXC相对TXD延时2ns约束输出延迟范围 set_output_delay -clock [get_clocks clk_125m] -max 2.5 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -min 0.5 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -clock_fall -max 2.5 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -clock_fall -min 0.5 [get_ports {rgmii_txd[*]}]接收方向的约束前提是PHY送给MAC的RXC和RXD关系。多数PHY的RXC相对RXD也是有延时的要对PHY手册确认是中心对齐还是边沿对齐再对应写输入延迟# 接收方向假设PHY输出数据相对时钟中心对齐 set_input_delay -clock [get_clocks clk_rx_125m] -max 1.5 [get_ports {rgmii_rxd[*]}] set_input_delay -clock [get_clocks clk_rx_125m] -min 0.5 [get_ports {rgmii_rxd[*]}]这里有个工程上常见的模糊地带——很多PHY比如某款国产PHY要求MAC侧把RXC相对RXD延时1.5-2ns否则采样点落在数据跳变沿上会出现间歇性错包。这个可以通过PHY的寄存器配置实现也可以通过FPGA的IO延时原语IDELAYE2来微调。资源包里给了两种方案用寄存器方案更省逻辑但不够灵活用IDELAYE2可以逐拍扫描延迟值找最佳采样点调试的时候非常管用。注意约束里的时钟名要和你代码里的时钟网络完全一致。Vivado里很容易出现get_clocks返回空集合导致约束静默失效的情况跑完综合后一定要看时序报告确认这些约束被真正解析到了。3. MAC收发通路从状态机到CRC逐模块拆解3.1 MAC帧格式和IPG先立规矩再写代码MAC层收发的对象是以太网帧。标准以太网帧结构从目的MAC地址开始到FCS帧校验序列结束最小帧长64字节最大1518字节不含前导码和SFD。工程上有几个预谋已久的坑点一是前导码7字节0x55和SFD1字节0xD5在接收侧需要判断并剥离但发送侧需要自动生成二是帧间隙IPGInter-Packet Gap是12字节千兆速率下就是96ns不能少。如果IPG不够PHY或对端交换机可能把连续两帧当成一帧接收或者因为缓冲溢出而丢帧。MAC实现里还有一个容易忽略的点全双工模式下不需要CSMA/CD但半双工模式下存在碰撞退避逻辑。现在绝大多数场景都是全双工RGMII接口基本只跑全双工。资源包里默认做成全双工模式简化了状态机也正好契合当前交换机环境的实际需求。MAC发送状态机的核心状态如下状态动作退出条件IDLE等待上层数据有效检测到发送请求PREAMBLE输出前导码和SFD8字节发送完成DATA逐字节搬移数据帧数据已发完PAD不足64字节时补0已凑够60字节数据FCS输出CRC32校验值4字节校验码发完IPG等待帧间隙12字节间隙计时结束3.2 发送状态机代码带自动补零和CRC插入发送状态机是整个MAC发送通路的中枢我拆到的核心代码长这样// 发送状态机从IDLE到IPG的完整流转 always (posedge clk_125m or negedge rst_n) begin if (!rst_n) begin state IDLE; tx_byte_cnt 0; crc_en 0; end else begin case (state) IDLE: begin if (tx_req) begin // 上层请求发送 state PREAMBLE; tx_byte_cnt 0; end end PREAMBLE: begin if (tx_byte_cnt 8) begin // 前导码SFD共8字节 state DATA; tx_byte_cnt 0; crc_en 1; // 打开CRC计算 end else begin tx_byte_cnt tx_byte_cnt 1; end end DATA: begin if (tx_valid tx_last) begin // 整帧数据送完 if (tx_byte_cnt 60) begin state PAD; // 不够最小帧长补零 end else begin state FCS; end end end PAD: begin if (tx_byte_cnt 60) state FCS; // 数据填充凑满60字节 else tx_byte_cnt tx_byte_cnt 1; end FCS: begin if (tx_byte_cnt 4) begin // CRC共4字节 state IPG; crc_en 0; end end IPG: begin if (tx_byte_cnt 12) state IDLE; // 12字节帧间隙 else tx_byte_cnt tx_byte_cnt 1; end endcase end end这段代码有几个值得留意的参数点。第一PAD和DATA边界计数器的关系——标准要求帧长不足64字节时要补零到64字节也就是数据域加填充至少60字节不算前导、SFD和FCS。这里计数从0开始DATA阶段每发一个字节加一FCS阶段也要占4拍加起来刚好满足最小帧长。第二CRC使能信号只在DATA和PAD阶段打开前导码和SFD不参与计算FCS本身也不参与。这个细节错一个CRC就是错的。第三IPG的长度是用周期数计数的125MHz下12字节等于96ns所以计数到12就能保证PHY侧的IPG不小于规范要求。3.3 CRC32的两种实现并行查表最省逻辑CRC32在以太网里的多项式是0x04C11DB7实现方式有串行LFSR和并行查表两种。串行LFSR最省资源但每bit一拍千兆速率下需要处理125M*8bit/s的数据流根本来不及。并行实现则要求每个时钟周期计算8bit数据的CRC组合逻辑深、时序压力大。资源包用的是查表法——预先算出256项的CRC表每拍查一次表再做异或和移位逻辑级数可控时序收敛容易。// CRC32并行查表实现数据位宽8bit function [31:0] crc32_8bit; input [7:0] data; input [31:0] crc; reg [31:0] next_crc; begin next_crc crc ^ {24d0, data}; crc32_8bit 32hFFFFFFFF; // 占位真实代码查表 // 查表逻辑先对低8位查crc_table再移位异或 end endfunction实际工程里更常见的做法是直接用IP核但用IP核会黑盒化出问题难排查。手写CRC的好处是你能在仿真里把每一拍的CRC值和标准工具比如在线CRC计算器比对定位到底是数据拼错还是计算错。这里给一个经验CRC计算时数据字节序和位序都要和以太网规范对齐帧内容从左往右发送但CRC计算是按位从LSB开始一旦字节序反了CRC永远对不上。3.4 接收通路前导码检测和FIFO写入时机接收状态机的复杂度比发送高原因在于发送的数据是自产的什么时候发什么完全可控接收的数据来自PHY什么时候开始、什么时候结束都由外部决定。前导码检测逻辑要能容忍PHY在前导码阶段可能出现的一两个bit错误但SFD字节必须严格匹配0xD5否则就丢弃并回到IDLE。接收到完整帧后MAC把数据写入FIFO。这里的关键是FIFO的写时序和IDDR的延迟对齐——IDDR SAME_EDGE模式输出的数据比线上数据晚一拍所以写使能信号也要相应延后一拍。如果直接拿RXC去当写时钟、用接收状态机的valid信号当写使能大概率会出现首字节缺失或者整帧错位。// 接收侧FIFO写入数据对齐和写使能的时序关系 always (posedge rx_clk or negedge rst_n) begin if (!rst_n) begin rx_d_valid_dly 0; end else begin rx_d_valid_dly rx_d_valid; // 写使能打一拍对齐数据 end end // 写FIFO数据是IDDR输出的延后版本使能也要延后 fifo_wr_en rx_d_valid_dly (rx_state ! IDLE); fifo_wr_data {rxd_high[3:0], rxd_low[3:0]}; // 高4位是下降沿数据注意这里数据拼位法在SAME_EDGE模式下Q1是上升沿采到的低4位Q2是下降沿采到的高4位拼成一个字节时高4位在前、低4位在后顺序错了整个帧的字节序就乱了。3.5 跨时钟域从125MHz进FIFO到用户时钟MAC的收发时钟是独立的125MHz而FPGA内部用户逻辑通常跑在更低的时钟频率比如100MHz、150MHz或者完全异步的时钟域。两个时钟域之间的数据搬运只能靠异步FIFO。资源包里在收发两端各放了一个异步FIFO读端连接到用户逻辑时钟域。用异步FIFO最怕的是FIFO本身的空满信号跨时钟域出问题。标准的做法是使用格雷码指针同步这在Xilinx的FIFO IP里已经封装好了不需要自己写。但有一个坑FIFO的almost_full和almost_empty信号在跨时钟域后会有最多3个周期的延迟如果你的MAC状态机直接拿almost_full当反压信号在最极限的连续帧场景下可能溢出。工程上稳妥的做法是留出至少4个以太网帧的缓冲深度并把almost_full阈值设为深度减16给同步延迟留余量。资源包的FIFO深度默认是1024x8bit最大帧1518字节大概需要1518个存储单元1024深度其实是装不下一整帧的。所以如果要做大帧传输深度至少要2048否则接收侧会溢出丢帧。这个参数是拿到工程后第一件要改的事。4. 时序收敛与上板验证从仿真通过到真正跑起来4.1 仿真和上板的温差为什么仿真通过还会翻车仿真通过不代表上板能跑这是硬件工程师的共识但RGMII尤其如此。仿真环境里时钟和数据的关系是理想化的没有PCB走线延迟没有IOB的Tsu/Th要求没有ODDR的时序限制也没有供电噪声。到了板上任何一个时序裕量不足的路径都可能变成随机丢包的来源。我常用的验证策略是三级递进第一级是纯RTL仿真验证MAC状态机和CRC逻辑正确第二级是加入IOB模型的仿真验证ODDR/IDDR的时序行为第三级是上板用内部逻辑分析仪抓RGMII引脚上的实际波形。工程包里提供了一个简单的以太网帧生成器可以在仿真时注入已知数据帧再在接收端比对。4.2 约束文件实战一组能直接用的XDC以下约束适用于常见的Artix-7系列引脚位置请替换成自己板卡实际原理图# RGMII引脚约束电平标准3.3V注意PCB上是否有串联电阻 set_property PACKAGE_PIN AB12 [get_ports {rgmii_txd[0]}] set_property PACKAGE_PIN AA12 [get_ports {rgmii_txd[1]}] set_property PACKAGE_PIN AB11 [get_ports {rgmii_txd[2]}] set_property PACKAGE_PIN AA11 [get_ports {rgmii_txd[3]}] set_property PACKAGE_PIN AB10 [get_ports {rgmii_tx_ctl}] set_property PACKAGE_PIN AC12 [get_ports {rgmii_txc}] set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_*}] # 时钟约束TXC是125M时钟输出 create_clock -name clk_125m -period 8.000 [get_ports {rgmii_txc}] # 发射方向的输出延迟约束关键 set_output_delay -clock [get_clocks clk_125m] -max 2.0 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -min 0.5 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -clock_fall -max 2.0 [get_ports {rgmii_txd[*]}] set_output_delay -clock [get_clocks clk_125m] -clock_fall -min 0.5 [get_ports {rgmii_txd[*]}]注意create_clock加在rgmii_txc端口上是为了让工具知道这个输出时钟的周期从而计算ODDR输出路径的时序关系。很多人一开始不建这个时钟导致ODDR路径没有时序约束工具随机布局布线上板后时序裕量可能是负的。4.3 ILA抓波形怎么判断数据到底错在哪上板后第一件事是用ILA抓RGMII接收引脚。抓之前要明确一个前提——ILA本身需要额外的时钟最常见的做法是用RXC作为ILA的采样时钟。但RXC在无帧时是125MHz连续时钟有帧时也是连续时钟作为采样时钟没问题。不过ILA的存储深度有限抓长时间数据不现实建议用触发条件匹配SFD字节0xD5来捕获帧头然后看后续数据流。抓到的波形如果发现RXD上的数据和期望不一致先不要怀疑MAC逻辑先确认RXC和RXD的相位关系——把两个信号一起抓到看数据变化沿和时钟沿的相对位置。如果数据在时钟沿附近变化说明采样点不在数据眼图中央就要调整PHY的延时配置或IDELAYE2的tap值。MAC逻辑反而很少出错因为它只跟RXC打交道。5. 避坑与调试RGMII以太网MAC的五个血泪踩坑记录5.1 现象上板后收发都不通示波器看TXC有波形但PHY不识别原因RGMII的TX_CTL和TXD共用DDR机制TX_CTL在上升沿表示TX_EN下降沿表示TX_ER。如果ODDR的D1和D2接反了TX_EN被当成错误标志PHY直接丢弃所有帧。很多人把TX_CTL的D1接成了TX_ERD2接成了TX_EN结果控制信号整个反转。解决检查自己的ODDR实例确保D1是TX_EN、D2是TX_ER。仿真里加一段断言在发送数据期间TX_CTL的高电平必须持续整个帧时间而不是只在某一半周期出现。5.2 现象能用但每传几个大帧就丢一帧速率越高丢得越狠原因FIFO深度不够。默认1024深度装不下一整帧1518字节大帧进来直接溢出。而且almost_full跨时钟域同步延迟3个周期在连续帧场景下MAC读侧根本来不及响应。解决把FIFO深度改为2048或4096almost_full阈值设为深度减16。这个改动只要改参数不需要动逻辑。改完后再压测大帧长时间传输丢帧率归零。5.3 现象接收方向偶尔错字节错误位置不固定原因要么是RXC和RXD的相位偏了采样点不在数据眼图中央要么是跨时钟域处理不当IDDR的Q1/Q2在低4位和高4位拼字节时顺序搞反。最容易出现在高速布局布线后PCB走线差异导致RXD相对RXC的延迟变化。解决用IDELAYE2对RXD做可调延迟在调试时把tap值从0到31扫一遍找出丢包率为0的区间固定到中间值。扫描方法写一个小的调试逻辑每换一个tap值就发1000帧测试包统计错包数画出延迟-错包率曲线选中间的稳定区间。5.4 现象CRC校验总是报错但数据看起来是对的原因字节序和位序问题。以太网CRC计算是从帧的第一个字节的LSB开始而很多人的数据总线是MSB-first或者MAC把FCS当作有效载荷的一部分送给了上层上层再校验时把CRC也算进数据里了。解决先确认MAC收数据时是否剥离了FCS。如果上层软件用的是标准socket内核会自动处理FCSMAC层直接把带FCS的全帧交上去就会重复校验失败。再确认CRC计算模块的输入字节序和以太网规范一致。调试时可以抓一个已知帧用Python的binascii.crc32手工算一遍和MAC算出的值对比。5.5 现象TX方向单独测试正常PC端ping不通但RX方向能收到数据原因环路测试能过说明数据通路基本正常ping不通大概率不是MAC的问题而是ARP协议没处理。MAC层只负责收发以太网帧ARP请求/响应需要上层协议栈处理。如果FPGA工程里只实现了MAC而没有对应的ARP应答模块PC的ping自然不通。解决确认自己的工程是否包含ARP模块。很多MAC资源包不带协议栈只到MAC层为止。ping需要ARP能通TCP/UDP又依赖IP层IP层又需要ARP。如果只是做MAC验证用FPGA发已知UDP帧到PC用Wireshark抓包看帧格式对不对就够了不必纠结ping不通。6. 用已有工程模板快速改出自己的千兆MAC最小改动清单拿到这套资源后最快把它变成自己工程的方法不是从头理解每一行而是按这份清单做定向修改。第一替换引脚约束——找到XDC文件里所有的PACKAGE_PIN换成自己板卡原理图上的实际引脚IOSTANDARD按板卡电平改3.3V或2.5V必须和原理图一致。第二确认时钟——RGMII的TXC是MAC输出的但这个时钟的来源有两种一是MAC自己生成125MHz二是由PHY反馈时钟。资源包的默认设计是用PLL生成125MHz驱动TXC如果你的板卡是PHY反馈时钟模式要改时钟拓扑否则收发时钟频率不一致会丢帧。第三调整FIFO深度——之前说过默认1024装不下一整帧改成2048起步如果要做巨型帧jumbo frame9000字节直接上8192。第四对照PHY芯片手册检查RX延迟配置——不同PHY对RXC和RXD的相位关系定义不同有的要求MAC侧额外加延时有的要求不加要根据实际PHY型号查寄存器手册。改完这四个地方整个工程基本就能跑起来。如果收到数据但CRC错按调试章节的字节序检查法过一遍大概率20分钟内定位。从那以后我每次接手新板卡都会先做一遍最小改动清单再上板避免从头理解所有代码浪费一整天。这套方法帮我跳过不少坑希望帮到你。本文还有配套的精品资源点击获取