YAOTU INSIGHTS

FPGA实现RGMII以太网MAC:从DDR时序到CRC校验的完整设计指南

FPGA实现RGMII以太网MAC:从DDR时序到CRC校验的完整设计指南
简介一套基于RGMII接口的以太网MAC的FPGA实现工程面向FPGA开发者与网络通信方向学习者重点解决10/100Mbps以太网MAC层逻辑设计、RGMII协议对接及PHY芯片驱动等实用问题。工程共728个文件压缩包约22.91MB内容涵盖Verilog/VHDL源文件、Quartus工程配置、仿真波形与报告、数据手册及说明文档等便于从代码、工程到验证环节完整对照学习。目前已有2932人学习适合作为入门到进阶的参考实例。工程中不仅包含RGMII时序控制、CRC校验、状态机设计等核心模块还提供MDIO接口配置、PHY芯片初始化、测试平台及时序约束等辅助内容从顶层模块到PHY寄存器配置均有清晰实现工程代码结构清晰、模块注释完整可帮助理解数据帧发送接收流程、调试方法和软硬件协同开发思路是一份兼具教学与实用价值的FPGA通信设计案例。1. 基于RGMII的以太网MAC的FPGA实现它到底在解决什么问题很多FPGA工程师第一次接触RGMII时都会被它的DDR双沿采样搞得一头雾水明明是125MHz的时钟为什么每根数据线要到250Mbps打开PHY芯片手册建立时间、保持时间、中心对齐、PCB等长这些词扑面而来看起来就像一门玄学。而RGMII恰恰是千兆以太网里最常用的MAC与PHY接口它用4根数据线加1根控制线在时钟上升沿和下降沿各传4bit一个时钟周期凑出8bit正好顶上千兆线速。自己做MAC而不是直接调IP核意味着你能完全掌控帧格式、CRC、流量控制和时序约束在需要定制协议、做无操作系统裸机转发、或者做教学级开源设计时这条路径的价值立竿见影。这篇笔记适合两类人一类是想在自家板卡上把RGMII跑通、却总被时序问题卡住的开发者另一类是已经会用现成MAC IP、但想知道内部状态机怎么转、CRC怎么算、坑在哪里的进阶使用者。我会按照“接口时序 → 发送通路 → 接收通路 → 时序约束与避坑 → 回环验证”的顺序把一条从零到能上板跑通的路径拆开讲。2. 先搞清楚RGMII再动手接口信号与MAC结构选型2.1 RGMII在千兆下的信号组与DDR时序规则RGMII这个接口本质上就是MAC和PHY之间的“运输通道”。它把原来GMII的8位并行数据、单独的收发时钟、使能信号压缩成一组DDR信号引脚数量从24根降到12根左右。代价是时序裕量变小对PCB走线和FPGA内部约束的要求变高。千兆模式下TX_CLK由MAC产生并送给PHY频率125MHzRXD/TX_CTL这些信号都在时钟的两个边沿上采样上升沿传低4位下降沿传高4位。信号组方向宽度功能说明TXDMAC→PHY4发送数据上升沿发字节的低4bit下降沿发高4bitTX_CTLMAC→PHY1上升沿代表TX_EN下降沿代表TX_ERTX_CLKMAC→PHY1125MHz千兆/ 25MHz百兆/ 2.5MHz十兆RXDPHY→MAC4接收数据时序规则与TXD一致RX_CTLPHY→MAC1上升沿代表RX_DV下降沿代表RX_ERRX_CLKPHY→MAC1由PHY从接收线路恢复与RXD中心对齐MDIO / MDC双向 / MAC→PHY2管理接口用于配置PHY寄存器需要特别注意的是千兆模式下每根TXD线实际跑在250Mbps因为一个125MHz时钟周期里有上升沿和下降沿两次变化4根线合起来才有1Gbps。很多第一次上手的人在这里容易算错以为125MHz×4bit就是500Mbps忽略了DDR翻倍这件事。百兆和十兆模式下时钟频率降到25MHz和2.5MHz数据仍然走TXD[3:0]TXD[7:4]在RGMII规范里可以忽略这让一个MAC设计能通过配置PHY寄存器同时兼容三种速率。2.2 MAC内部拆成几块收发独立、管理单独、时钟域隔离自己实现以太网MAC我一般不会写一个巨大的状态机把所有事情都包进去而是按数据流向拆成四个相对独立的模块发送通路、接收通路、MDIO管理、时钟与复位。发送通路负责从用户接口拿数据计算CRC按以太网帧格式封装最后转成RGMII时序送出去接收通路反过来从RGMII双沿采样拿到数据做跨时钟域处理校验CRC再提交给用户逻辑。收发各用各的状态机原因很直接千兆线速要求收发同时工作一个状态机串行处理必然丢帧。而且RGMII的TX时钟和RX时钟不同源RX_CLK是从接收线路上恢复出来的天然带着抖动和相位偏移强行放到同一个时钟域只会给自己找麻烦。时钟域划分是MAC设计里最容易埋雷的地方。发送侧直接用本地产生的125MHz时钟接收侧先用PHY送来的RX_CLK采样采到的数据通过异步FIFO转到本地时钟域。MDIO管理接口的MDC时钟通常是2.5MHz以下由本地时钟分频得到它只需要配置PHY的寄存器不参与高速数据通路单独做一个简单的状态机就够了。选型上有一个常见的分歧发送和接收是各自独立FIFO还是共用一个存储区。我建议收发各放一个异步FIFO深度按最大帧长再加一帧余量来定比如千兆下做线速转发FIFO深度至少要有2KB。共用存储区虽然省资源但读写指针、水位线、拥塞控制全搅在一起调试时浪费的时间远超过省下的那点BRAM。3. 发送通路实现从用户数据到RGMII引脚的完整链路3.1 发送状态机前导码、帧数据与帧间隙怎么编排发送通路的核心是一个有限状态机。以太网帧的封装顺序是固定的先发7字节0x55前导码再发1字节0xD5帧起始定界符然后是6字节目的MAC、6字节源MAC、2字节长度/类型、46到1500字节负载最后是4字节FCS。帧与帧之间还要有至少96bit的帧间隙否则对端PHY来不及恢复状态。在RGMII上1字节对应一个125MHz时钟周期所以前导码加定界符一共占8个时钟周期帧间隙占12个时钟周期。下面是一个精简的发送状态机框架去掉了异常处理保留主干逻辑localparam S_IDLE 3d0; localparam S_PREAMBLE 3d1; localparam S_DATA 3d2; localparam S_CRC 3d3; localparam S_IFG 3d4; reg [2:0] state; reg [3:0] byte_cnt; reg [31:0] crc_reg; reg [7:0] data_out; always (posedge clk_tx) begin if (rst_tx) begin state S_IDLE; byte_cnt 4d0; end else begin case (state) S_IDLE: begin if (axis_tvalid axis_tready) begin state S_PREAMBLE; byte_cnt 4d0; end end S_PREAMBLE: begin if (byte_cnt 4d7) data_out 8hD5; // 第8个字节帧起始定界符 else data_out 8h55; // 前7个字节前导码 if (byte_cnt 4d8) begin state S_DATA; byte_cnt 4d0; end else byte_cnt byte_cnt 1b1; end S_DATA: begin // 从用户接口取数据送入CRC计算并同时发出 data_out axis_tdata; if (axis_tlast) begin state S_CRC; byte_cnt 4d0; end end S_CRC: begin // 按字节输出CRCcrc_reg需要预先算完 data_out crc_reg[31:24]; crc_reg {crc_reg[23:0], 8h00}; if (byte_cnt 4d3) state S_IFG; else byte_cnt byte_cnt 1b1; end S_IFG: begin if (byte_cnt 4d11) begin state S_IDLE; byte_cnt 4d0; end else byte_cnt byte_cnt 1b1; end endcase end end这里要注意S_CRC状态里我用了“先发crc_reg[31:24]再左移8位”的方式这对应以太网FCS“高位字节先发”的规定。实际工程中CRC要提前算好不能等数据发完再开始算否则要插入等待周期。所以数据通路里通常是边发边算数据发完时CRC寄存器里已经存好了最终结果状态机直接切到CRC发送即可。byte_cnt在S_DATA里没有递增是因为用户接口本身有tlast指示用tlast判断结束比数长度更可靠也天然兼容不同帧长。S_IFG这个状态在有的实现里会直接省略用“下一帧不能马上开始”的条件代替。我保留显式状态是因为它让时序仿真波形一眼能看懂出了问题也方便抓状态跳转。帧间隙长度可以根据对端PHY的要求在8到12字节之间调有些PHY对背靠背帧比较敏感帧间隙小于8字节就会出现crc错误这一点在联调时值得留意。3.2 CRC32的计算与发送顺序查表法和逐位法怎么选以太网FCS用的是CRC32多项式是0x04C11DB7但在字节序上是反射形式也就是最低位先处理。工程实现有查表法和逐位法两种主流做法。逐位法逻辑简单、资源最省但每bit都要算一次反馈千兆下如果用串行方式做要么主频拉到1GHz要么就得改并行结构基本不现实。查表法每处理一个字节做一次查表和异或125MHz下刚好一拍搞定是千兆MAC最常用的方案。function [31:0] crc32_byte; input [31:0] crc_in; input [7:0] data_in; integer k; reg [31:0] crc; begin crc crc_in ^ data_in; for (k 0; k 8; k k 1) begin if (crc[0]) crc (crc 1) ^ 32hEDB88320; else crc crc 1; end crc32_byte crc; end endfunction这个函数里0xEDB88320就是0x04C11DB7的反射多项式。每次调用对当前CRC和输入字节做一次异或然后按bit迭代8次。综合工具会把for循环展开成组合逻辑8次迭代的关键路径大约是8级异或链125MHz下通常能收敛不需要额外插流水。如果时序紧张可以把函数里的循环换成一张256×32bit的查找表用两个时钟周期完成一次CRC更新吞吐率仍然满足千兆。CRC计算完成后发送顺序是crc[31:24]、crc[23:16]、crc[15:8]、crc[7:0]这是IEEE 802.3规定的FCS字节传输顺序。注意这里说的是“字节顺序”字节内部的bit顺序是LSB-first也就是每个字节先发最低位。在RGMII上一个字节被拆成低4位和高4位上升沿先发低4位、下降沿发高4位这正好和以太网bit序一致。不要把这个和FCS的字节顺序搞混不少翻车现场都是因为“高位字节先发”和“低4位先发”两个规则在脑子里打架。3.3 ODDR输出与TX_CLK的产生方式RGMII发送的数据在FPGA内部是8bit并行但引脚上只有4根线而且要在两个边沿分别输出。常用的做法是用ODDR原语把低4位和高4位分别映射到D1和D2在每个时钟上升沿捕获、对应边沿输出。ODDR #( .DDR_CLK_EDGE(SAME_EDGE), .INIT(1b0), .SRTYPE(SYNC) ) oddr_txd0 ( .Q(eth_txd[0]), .D1(txd_low[0]), // 上升沿输出的低4bit之一 .D2(txd_high[0]), // 下降沿输出的高4bit之一 .C(clk_tx), .CE(1b1), .R(1b0), .S(1b0) );DDR_CLK_EDGE选SAME_EDGE意思是D1和D2都在时钟上升沿被采样输出时D1出现在上升沿、D2出现在下降沿。这样对上游逻辑只有一个时钟沿要满足时序比OPPOSITE_EDGE模式好收敛得多。4根数据线和TX_CTL各例化一个ODDR一共5个实例布局时尽量放在同一个I/O bank的相邻引脚上能减少片内走线偏斜。TX_CLK的产生方式直接决定对端PHY能不能正确采样数据。最简单的做法是把125MHz时钟直接通过OBUF驱动到引脚但这样时钟路径和数据路径的延迟差异没有校准PHY那边采到数据可能正好在跳变沿上。更稳妥的做法是把TX_CLK也当作一个DDR信号来生成ODDR的D1接1、D2接0输出就是一个125MHz方波并且这个方波的边沿和数据输出由同一个ODDR机制产生偏斜天然一致。对时序要求高的板卡还可以在XDC里对TX_CLK做output delay约束让它相对TXD提前或滞后半个时钟周期具体数值要查PHY手册的建立保持时间窗口。4. 接收通路实现把双沿数据安全收进来4.1 输入延迟与采样点IDELAYE2的参数怎么看接收侧第一个要面对的问题是采样点。PHY送来的RX_CLK和RXD是中心对齐关系也就是说数据稳定区在时钟沿的两侧而不是像发送侧那样边沿对齐。如果直接在RX_CLK上升沿采样采到的可能正好是数据翻转的位置结果就是偶发的CRC错误。解决方法是把RXD和RX_CTL都通过IDELAYE2延迟一段时间让采样沿落在数据窗口的中心附近。IDELAYE2 #( .IDELAY_TYPE(FIXED), .IDELAY_VALUE(16), // tap数0~31每档约78ps .DELAY_SRC(IDATAIN), .HIGH_PERFORMANCE_MODE(TRUE), .SIGNAL_PATTERN(DATA) ) idelay_rxd0 ( .IDATAIN(eth_rxd[0]), .DATAOUT(rxd_delayed[0]), .C(1b0), .CE(1b0), .INC(1b0), .CINVCTRL(1b0), .CNTVALUEIN(5d0), .CNTVALUEOUT(), .LD(1b0), .LDPIPEEN(1b0), .REGRST(1b0) );IDELAY_VALUE就是延迟档位每档大约78ps前提是IDELAYCTRL的参考时钟为200MHz。16档大约对应1.25ns延迟对于时钟周期8ns的千兆信号来说这个值可以从0开始往上扫找到CRC错误率最低的点。IDELAY_TYPE用FIXED意思是在编译时把延迟值固定下来这样上电后不需要软件参与行为最可预测。如果希望运行时动态调整可以把类型改成VAR_LOADABLE配合外部逻辑在调试时改写CNTVALUEIN我一般在调试验收阶段先用固定值等找到稳定区间再固化进代码。这里有一个容易忽略的依赖关系IDELAYE2必须配套IDELAYCTRL原语才能工作IDELAYCTRL需要一个200MHz的参考时钟并且这个时钟要单独约束。漏掉IDELAYCTRL的典型现象是综合报告里出现未放置的延迟单元或者上电后数据完全采不到。4.2 双沿采样、字节拼接与异步FIFO跨时钟域延迟调整好之后数据要在RX_CLK的两个边沿分别被采下来。上升沿采到的4bit是字节的低4位下降沿采到的是高4位把两者拼起来就是一个完整的8bit字节。这个拼接逻辑本身不复杂但要注意采到的数据仍然在RX_CLK时钟域不能直接交给本地时钟域的逻辑使用。reg [3:0] rxd_low, rxd_high; reg [7:0] rx_byte; reg rx_valid; always (posedge rx_clk) begin rxd_low rxd_delayed[3:0]; end always (negedge rx_clk) begin rxd_high rxd_delayed[3:0]; end always (posedge rx_clk) begin rx_byte {rxd_high, rxd_low}; // 上升沿采低4位下降沿采高4位拼成8bit rx_valid rx_ctl_delayed; // RX_CTL在上沿为RX_DV endrx_valid信号本身也存在跨时钟域问题。RX_CTL在RX_CLK域里是一个电平信号用两级寄存器同步只能解决亚稳态不能解决“有效电平跨越FIFO读写指针”的问题。所以数据必须走异步FIFO写时钟是RX_CLK读时钟是本地125MHzFIFO的写使能就是rx_valid。FIFO深度可以比发送侧稍微浅一点但要能容纳一整个最长帧否则大包会被截断。关于拼接顺序有一个值得说的细节RGMII规范里上升沿传低4位、下降沿传高4位拼出来是高4位在前、低4位在后恰好是8bit的标准字节序。如果拼反了表现是收下来的MAC地址字节对不上比如目的MAC变成字节交换后的样子CRC必然全错而且很难从错误值反推是拼接问题还是位序问题。4.3 接收侧CRC校验与错误统计接收通路收到完整一帧后要做CRC校验。常见做法是把从目的MAC地址开始的每一个字节一直到FCS本身全部送入CRC计算器算完后结果固定为0xDEBB20E3。这个常数是反射CRC32的“魔术数”也就是把包含FCS的整帧算完后寄存器里必然停留的值。用它校验比“先算数据再和FCS比较”少一次比较逻辑但第一次接触的人往往对这个常数没有概念容易误以为是巧合。校验失败时我习惯把错误帧的计数、长度、以及FCS值单独拉出来供ILA抓取和分析。错误计数的清零信号要接到调试逻辑而不是复位信号上因为复位会把计数清零再想定位是复位前还是复位后出错就分不清了。另外接收状态机要有超时保护如果RX_DV拉高后长时间没有结束说明PHY或对端出了问题状态机必须在超时后强制回到IDLE否则会一直卡住后续帧全部收不到。超时阈值一般设成最大帧长的两倍也就是3000字节左右的时间千兆下约24微秒。5. RGMII调试避坑四个最容易翻车的时序点5.1 坑1TX_CLK相位不对对端千兆协商失败现象PHY的链路状态指示灯亮起但收不到任何数据包用抓包工具看对端网卡发现CRC错误持续增加或者干脆显示没有载波。反复检查发送状态机和数据内容都正常。原因TX_CLK和数据TXD之间的相位关系不满足要求。RGMII标准规定数据在TX_CLK边沿附近变化但不同PHY对建立时间的要求差异很大。如果TX_CLK直接由BUFG驱动、数据由ODDR驱动两条路径的延迟差可能达到几纳秒恰好把数据变化区推到PHY的采样沿上。解决把TX_CLK改成ODDR生成的方波让它和数据走同一个原语机制偏斜可控。然后在XDC里对输出路径加约束示意见下create_clock -name clk_sys -period 8.000 [get_ports clk_125m] create_generated_clock -name clk_tx -source [get_ports clk_125m] \ -divide_by 1 [get_ports eth_tx_clk] set_output_delay -clock clk_tx -max 2.500 [get_ports {eth_txd[*] eth_tx_ctl}] set_output_delay -clock clk_tx -min -0.500 [get_ports {eth_txd[*] eth_tx_ctl}]max和min的具体数值不能照抄要看对应PHY手册里TXD相对TX_CLK的建立保持要求。先量出实际板卡的skew再反过来填约束比拍脑袋填数字可靠得多。调完约束后重新看时序报告重点看eth_txd到PHY的路径是否还有违例。5.2 坑2接收采样沿选错大包偶发CRC错现象小包转发一切正常64字节、128字节都稳定换成1518字节的大包每几千帧里出现一两帧CRC错误错误帧长度随机重发后又能通。原因小包的数据窗口短错误概率低容易被忽略大包的数据量上去了采样点偶尔落在数据跳变沿的问题就暴露出来。这是典型的采样相位没有对准IDELAY值不够或者过度采样时钟沿落在RXD翻转的瞬间采到的是不稳定电平。解决不要靠猜直接把IDELAY_VALUE从0到31扫一遍。每档固定发1000帧1518字节的包统计CRC错误率选错误率为0的区间中心。具体做法是把IDELAY_VALUE做成一个可配置寄存器用一个简单的串口或按键循环改值每改一次记录一组错误计数。扫完之后把最优值写回IDELAY_TYPE为FIXED的配置里。这个“扫tap”的过程看着笨其实是RGMII调试里最有效的手段比盯着时序报告猜要快得多。5.3 坑3复位释放与PHY状态竞争MAC偶发死锁现象上电后第一次发包大概率失败或者表现为“复位之后要按一下按钮才能正常工作”。状态机卡在一个奇怪的中间状态既不发送也不接收触发ILA一看状态既不是IDLE也不是DATA。原因复位释放的瞬间PHY可能还在上电初始化RGMII的RX_CTL处于不确定状态。如果此时接收状态机看到RX_CTL为高会误以为一帧已经开始进入接收状态等待帧结束而事实上没有后续数据状态机就永远等在那里。解决处理分成两步。第一步把异步复位信号做同步释放避免复位释放沿和时钟沿竞争第二步让接收状态机在进入数据状态前增加一个确认条件RX_CTL必须连续两个时钟周期为高才认为帧真的来了。同时给接收状态机加超时跳转超过最大帧长时间没有收到结束就强制回IDLE。这两个改动加起来不到十行代码但能彻底消除这类死锁。5.4 坑4FCS字节序和CRC初值搞反收发都算不对现象自己发的帧自己收回来CRC永远不对用标准协议栈发包FPGA收到后校验失败。更迷惑的是如果只看前导码和MAC地址一切正常让人误以为问题在PHY。原因FCS的发送顺序和CRC内部字节序搞反了。常见错误包括按crc[7:0]、crc[15:8]这样低字节先发或者CRC初值没有设成全1或者把前导码也放进CRC计算范围。以太网CRC的初值必须是0xFFFFFFFF处理范围从目的MAC开始不含前导码最终发送顺序先高字节后低字节。解决先用软件算出参考值。用Python的zlib.crc32对同样内容的帧负载算一次得到的结果和FPGA里CRC最终值应该一致zlib.crc32就是反射CRC32初值全1输出异或全1与以太网FCS完全同一个算法。我调试时通常是构造一帧固定内容把软件算出的FCS值填进发送端接收端再校验能通过就说明CRC模块和FCS顺序全部正确。这个用例建议留到工程里当回归测试每次改完代码先跑一遍这个固定向量能挡住一大半低级错误。6. 一个能证明MAC能用的最小验证方案回环自测与ILA抓包6.1 内部回环和外部回环怎么选验证RGMII MAC最忌讳的是“直接上PHY联调”因为一旦出问题PHY配置、PCB走线、RGMII时序、MAC逻辑四个变量混在一起根本没法定位。我习惯分两步走。第一步做内部回环把发送通路拼好的8bit字节直接接到接收通路的拼接输出上不经过ODDR、IDELAY和PHY。这个回环验证的是MAC逻辑本身——状态机跳转、CRC计算、FCS收发顺序。内部回环全部通过后再把PHY接入做外部回环。外部回环有两种方式。一种是把PHY配置成digital loopback让数据在PHY内部从发送通路绕回接收通路这能验证RGMII接口时序和PHY寄存器配置但不经过网线排除的是对端设备的问题另一种是接上真正的对端设备用网线联调这验证的是完整链路。顺序不能反跳步会让问题定位成本翻倍。内部回环一旦通过MAC逻辑就是可信的外部回环失败时注意力可以集中在时序和PHY配置上不再怀疑帧格式。6.2 用长度字段与CRC结果快速定位收发不一致回环测试的帧内容要精心构造不能随便塞随机数。我常用的最小帧是目的MAC全0xAA源MAC全0xBB长度/类型字段填0x0030正好48字节payloadpayload从0开始递增最后4字节FCS。有了这个固定帧定位问题就快得多。如果收端长度字段变成0x3000说明字节序反了问题大概率在拼接顺序如果payload里的递增计数出现错位比如收到的是0x01、0x03这样跳变说明采样不稳定或跨时钟域丢数据如果长度对、计数也对、但CRC校验失败问题几乎必然在FCS的发送顺序或CRC初值。ILA抓包时把触发条件设成TX_CTL的上升沿抓下来的波形应该能看到8字节前导码、紧接着的目的MAC、连续递增的payload最后是以0xDEBB20E3为结果的CRC段。任何一个环节对不上波形会直接暴露在哪一拍出了问题。我现在的习惯是每次改完RGMII相关代码都要用这个固定帧做一轮内部回环加一轮外部回环然后扫一遍IDELAY tap值记录最优档位和错误率。这三步看起来繁琐但以前每次偷懒跳过最后都是花更多时间在板子上拿ILA一遍遍抓波形得不偿失。把这些验证步骤固化成习惯之后RGMII这块基本没再让我熬夜。希望帮到你。本文还有配套的精品资源点击获取