资讯动态

FPGA以太网MAC设计:基于RGMII接口的Verilog实现与上板验证

发布时间:2026/10/19 3:00:07 来源:尧图企业网站定制
简介基于RGMII的以太网MAC FPGA实现完整工程面向FPGA开发者和网络通信工程师适合需要掌握RGMII接口时序、MAC层协议以及外部PHY芯片适配的实战学习者。压缩包内共728个文件整体约22.91MB文件类型涵盖Verilog/VHDL源文件、Quartus工程文件、FPGA配置比特流、仿真波形与脚本以及PDF、docx设计文档并包含综合布线后的数据库文件可完整体现从RTL设计到布局布线的工程流程。已有2938人次学习下载。工程除MAC收发核心逻辑外还实现了MDIO接口配置、PHY芯片DP83848/LAN8720初始化、数据帧组帧拆帧、CRC校验、错误处理与状态机控制等模块并涉及25MHz时钟下的RGMII数据同步、全双工通信机制、时序约束与IP核集成等要点。配套文档和调试脚本能够辅助定位收发异常、分析RGMII波形时序同时为DMA集成、性能优化和系统级联调提供可参考的实现路径适合从FPGA逻辑入门到网络通信开发的进阶参考。1. 从RGMII到MAC一个可复现的FPGA以太网链路拿到一块带千兆PHY的FPGA开发板点灯和串口打印通常几分钟就能跑通但把网口真正收发数据这件事很容易卡在RGMII接口时序和MAC状态机上。这个基于RGMII的以太网MAC的FPGA实现用纯Verilog完成了MAC层的发送通道、接收通道、MDIO管理和跨时钟域处理不依赖商业IP核能在常见FPGA平台上直接综合上板。适合两类人一是想把以太网协议栈底层逻辑吃透的学生和入门开发者二是需要在自定义嵌入式系统中做低成本以太网通信的工程师。下文会从RGMII协议细节讲到核心代码、仿真与踩坑记录给你一条能照抄的落地路径。2. 总体架构与数据通路RGMII接口、MAC核和三组时钟域的配合2.1 RGMII接口协议与信号定义RGMIIReduced Gigabit Media Independent Interface把GMII的二字节并行数据位宽砍成4位然后用DDR方式在时钟上下沿各传一次这样千兆模式下的时钟仍然是125MHz引脚数却从24个左右降到10个以内。第一次接触这个接口的人最容易踩的坑就是想当然认为“低4位是低字节高4位是高位”。实际发送逻辑是上升沿送出TXD[3:0]下降沿送出TXD[7:4]两个半字节拼成一个字节。控制信号TX_CTL也一样上升沿是TX_EN下降沿是TX_ER。下表给出常用的RGMII信号方向和作用做顶层约束和阅读PHY手册时可以直接对照。信号方向作用GTX_CLK输出千兆模式发送时钟通常125MHzTXD[3:0]输出上升沿发送数据低4位TXD[7:4]输出下降沿发送数据高4位TX_CTL输出上升沿TX_EN下降沿TX_ERRX_CLK输入PHY恢复的接收时钟RXD[3:0]输入上升沿接收数据低4位RXD[7:4]输入下降沿接收数据高4位RX_CTL输入上升沿RX_DV下降沿RX_ERMDC/MDIO输出/输入PHY管理接口MDC时钟MDIO数据注意在10/100M模式下GTX_CLK不再是125MHz而需要切换到2.5MHz或25MHz。很多MAC会直接用PHY反馈的TX_CLK做低速率发送时钟但RGMII规范里千兆模式用的是GTX_CLK。另一个容易忽略的点是PHY芯片的RGMII引脚往往自带可配置的延迟用来补偿PCB走线和内部时钟偏移如果PHY内部没有加延迟FPGA内部就需要用IO延时或者让接收时钟反向否则采样会正好落在数据跳变沿上。2.2 顶层模块结构与数据流这个设计没有用厂商提供的MAC IP而是把MAC拆成几个独立的小模块这样每一段逻辑都能单独仿真和替换。典型的分层如下rgmii_mac_top ├── tx_path // 用户数据 - TX FIFO - 发送状态机 - RGMII TX ├── rx_path // RGMII RX - 接收状态机 - RX FIFO - 用户数据 ├── crc32 // CRC32 生成与校验 ├── mdio_ctrl // MDIO 配置 PHY └── clk_rst // 复位同步与时钟标志rx_path和tx_path分别处理两个方向互不阻塞。用户侧接口没有强行上AXI-Stream而是用一组简单的valid / start-of-frame / end-of-frame握手信号这样无论是接CPU总线还是接自定义DMA改起来都容易。MAC内部的FIFO深度可以参数化默认发送32×64位、接收32×64位够放几个小包做回环测试用。数据流可以这样理解发送时用户把字节流写入TX FIFO发送状态机在检测到帧头后先发8字节前导码和帧起始定界符SFD然后从FIFO读取数据每两个时钟周期完成一个字节的RGMII发送。发送结束后计算CRC32并附在帧尾。接收时RGMII接收状态机先跳过前导码然后把每个字节暂存到接收侧寄存器整包送入RX FIFO同时给出帧长度和CRC校验结果。MDIO模块独立运行上电后自动读取PHY状态寄存器和配置速度/回环模式也可以由软件通过一组寄存器读写。2.3 三组时钟域与跨时钟处理实际工程里时钟域不是只有“系统时钟”一个。一般会有系统时钟比如50MHz或100MHz、MAC发送时钟125MHz、PHY恢复的RX时钟125MHz而且和本地时钟不同源如果用户数据来自软核或者外部总线那还有第四个异步时钟。为了避免把跨时钟问题揉成一团每个方向都用异步FIFO切一刀。发送方向上的跨时钟处理最直接用户时钟域写入TX FIFOMAC发送时钟域读出。这里用的是标准异步FIFO读指针和写指针分别用格雷码同步到对端时钟。关键参数是读侧和写侧的位宽要一致比如用户侧一次来32位FIFO就设置成32位发送状态机按字节读取时用一个位选寄存器拆字节。接收方向需要注意一个坑RX_CLK是PHY从数据流里恢复出来的和FPGA主时钟完全异步。接收状态机本身跑在RX_CLK上采样、拆字节、攒包都在这个时钟域里做因此逻辑上不用把RXD同步到主时钟只需要保证RX_CLK到接收FIFO的写时钟是对的。比如接收FIFO用RX_CLK作为写时钟用户时钟作为读时钟这样跨时钟就收缩到一个点上。复位也要跨时钟。比较稳妥的做法是每个时钟域各生成一份同步释放的复位顶层用慢时钟域产生复位再打拍同步到125MHz和RX_CLK域。直接对异步复位全局使用有时会导致状态机内部出现部分复位、部分未复位的中间状态这就是后面会说的“复位释放卡死”问题。3. 核心模块拆解发送状态机从AXI-Stream到RGMII的完整链路3.1 发送状态机握手、前导码和CRC插入发送通道的核心是一个状态机。简化后的骨架代码如下module rgmii_tx #( parameter MAX_FRAME_LEN 1536 )( input wire clk_tx, // MAC发送时钟125MHz input wire rst_n, input wire [7:0] s_axis_tdata, input wire s_axis_tvalid, input wire s_axis_tlast, output reg s_axis_tready, output reg [3:0] rgmii_txd, output reg rgmii_tx_ctl, output wire rgmii_tx_clk ); localparam IDLE 3d0; localparam PREAMBLE 3d1; localparam DATA 3d2; localparam PAD_CRC 3d3; localparam IFG 3d4; reg [2:0] state; reg [3:0] byte_cnt; reg [5:0] preamble_cnt; reg [31:0] crc_reg; assign rgmii_tx_clk clk_tx; always (posedge clk_tx or negedge rst_n) begin if (!rst_n) begin state IDLE; s_axis_tready 1b0; rgmii_txd 4h0; rgmii_tx_ctl 1b0; byte_cnt 4d0; end else begin case (state) IDLE: begin s_axis_tready 1b1; byte_cnt 4d0; if (s_axis_tvalid) begin s_axis_tready 1b0; preamble_cnt 6d8; state PREAMBLE; end end PREAMBLE: begin // 前导码55和SFD D5由发送字节缓冲统一发出 if (preamble_cnt ! 0) begin preamble_cnt preamble_cnt - 1; // 这里把8字节逐次送入“字节转半字节”逻辑 end else begin state DATA; s_axis_tready 1b1; end end DATA: begin if (s_axis_tvalid s_axis_tready) begin // 接收用户字节送入发送逻辑同时计算crc if (s_axis_tlast) begin s_axis_tready 1b0; state PAD_CRC; end end end PAD_CRC: begin // 补PAD到64字节随后把crc_reg拆成4字节发出 state IFG; end IFG: begin // 最小帧间隙9.6us100M千兆下为12字节时间 rgmii_tx_ctl 1b0; state IDLE; end default: state IDLE; endcase end end endmodule代码的逻辑很直观IDLE检测到用户数据有效后拉低tready进入前导码发送前导码发完进入DATA每拍接受一个字节收到tlast表示用户侧通知这是帧尾转去PAD和CRC最后输出至少一个帧间隙。这里的MAX_FRAME_LEN参数用来限制接收或发送的最大帧长默认1536字节如果做巨型帧可以调到9000以上。preamble_cnt位宽为6最多可以容纳64拍实际上只要8拍扩大只是为了方便调试时数数。需要说明的是真正的RGMII半字节发送并不在状态机里直接完成而是把tx_byte寄存器的[7:4]和[3:0]分别送到两个时钟沿。常见做法是调用FPGA厂商的DDR输出原语或者用双沿寄存器的硬核单元否则纯逻辑很难保证上下沿数据的偏斜在0.5ns以内。3.2 接收通道DDR采样、字节拼装和FIFO缓冲接收侧的难点在采样不在状态机。RGMII的RXD在RX_CLK上升沿输出低4位下降沿输出高4位FPGA内部需要用IDDR把两个沿的数据锁到同一个上升沿。代码片段如下wire [3:0] rx_low; wire [3:0] rx_high; genvar i; generate for (i 0; i 4; i i 1) begin : gen_rgmii_rxd_iddr IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED) ) u_iddr ( .D (rgmii_rxd[i]), .C (rx_clk), .CE (1b1), .R (1b0), .S (1b0), .Q1 (rx_low[i]), // 上升沿数据[3:0] .Q2 (rx_high[i]) // 下降沿数据[7:4] ); end endgenerate wire [7:0] rx_byte {rx_high, rx_low};这里选择SAME_EDGE_PIPELINED模式它的含义是Q1和Q2在同一个时钟上升沿输出其中Q2是下降沿锁存的数据经过一级打拍后与Q1对齐。这样在后续接收状态机里可以直接用rx_byte作为完整的字节不再需要关心上下沿。如果使用SAME_EDGE模式组合逻辑会更紧张但Q2的时序对齐较差在高频下容易翻车。接收状态机随后把跳过前导码后的字节写入RX FIFO。一个简单的状态跳转如下reg [1:0] rx_state; localparam RX_IDLE 2d0; localparam RX_SFD 2d1; localparam RX_DATA 2d2; always (posedge rx_clk or negedge rst_n) begin if (!rst_n) begin rx_state RX_IDLE; end else begin case (rx_state) RX_IDLE: begin if (rx_ctl rx_byte 8hD5) rx_state RX_DATA; end RX_DATA: begin if (!rx_ctl) begin // 帧结束把长度和CRC状态写到接收描述符 rx_state RX_IDLE; end else begin // rx_byte 写入fifo end end endcase end end接收FIFO的写时钟使用rx_clk读时钟使用用户时钟clk_sys。前面提到过这是整个接收通路唯一需要做跨时钟处理的地方。异步FIFO的深度至少要能容纳一个最大帧的字节数考虑到以太网帧最长为1518字节不含前导码32×64位的FIFO即2048字节正好够用。如果做巨型帧FIFO深度要按最大帧长重新计算。3.3 CRC32与MDIO发送侧字节流校验和PHY管理CRC32算法是MAC层的核心。以太网标准的CRC32是右移式生成多项式为0x04C11DB7初始值0xFFFFFFFF结果按位取反后按字节倒序发送。为了在125MHz时钟下完成逐字节计算常见做法是每周期对当前CRC和新的字节做8次迭代function [31:0] crc32_next; input [31:0] crc; input [7:0] d; integer i; reg [31:0] c; begin c crc ^ d; for (i 0; i 8; i i 1) begin if (c[0]) c (c 1) ^ 32hEDB88320; else c c 1; end crc32_next c; end endfunction这里的crc是当前累加值d是当前字节。注意多项式使用了0xEDB88320这是0x04C11DB7的位反转形式和右移算法配套。实际发送时初始值在帧的前导码结束后清零回0xFFFFFFFF数据字节每拍调用一次crc32_next最后一拍结果按位取反。很多初学者忘了“取反”这一步导致计算出的CRC和标准以太网抓包工具对不上。MDIO的相对简单它本质上是一个两线控制总线。MDC由系统时钟分频得到最高不超过2.5MHz。MDIO串行协议包含前导码、起始码、操作码、PHY地址、寄存器地址和16位数据。这个项目里MDIO模块负责上电初始化PHY包括配置速率、使能RGMII内部延迟等。调试时我会用逻辑分析仪抓MDC和MDIO对比PHY手册的时序图检查ST 0x01、OP码等是否对齐。4. 工程搭建与仿真验证目录结构、Testbench和上板抓包4.1 工程目录与文件组织如果完整源码包解压后建议按下面的目录结构摆放这样无论是仿真还是综合都能快速定位文件rgmii_mac/ ├── rtl/ │ ├── rgmii_mac_top.v │ ├── rgmii_tx.v │ ├── rgmii_rx.v │ ├── crc32.v │ ├── mdio_ctrl.v │ ├── async_fifo.v │ └── clk_rst.v ├── sim/ │ ├── tb_rgmii_mac.v │ └── phy_model.v ├── syn/ │ ├── build.tcl │ ├── constraints/rgmii.xdc │ └── bit/ // 生成的bit和bin └── doc/ └── register_map.mdrtl/下的async_fifo.v是标准格雷码异步FIFO不建议自己重写除非你需要理解内部指针同步。phy_model.v是仿真用的RGMII PHY模型它会对发送数据增加一个固定延时后返回给接收端模拟PHY在真实链路中的延迟。register_map.md文档里记录着用户侧寄存器地址比如发送长度寄存器、接收状态寄存器、MDIO读写寄存器等。4.2 Testbench如何用PHY回环模型验证MACTestbench的核心思路是让PHY模型把FPGA发送的RGMII数据直接回环给接收通道这样不用接外部设备就能验证MAC收发。示例代码如下module tb_rgmii_mac; reg clk_sys; reg rst_n; // 用户侧发送接口 reg [7:0] user_tdata; reg user_tvalid; reg user_tlast; wire user_tready; // RGMII接口 wire [3:0] fpga_txd; wire fpga_tx_ctl; wire fpga_tx_clk; wire [3:0] fpga_rxd; wire fpga_rx_ctl; wire fpga_rx_clk; rgmii_mac_top dut ( .clk_sys (clk_sys), .rst_n (rst_n), .user_tdata (user_tdata), .user_tvalid (user_tvalid), .user_tlast (user_tlast), .user_tready (user_tready), .rgmii_txd (fpga_txd), .rgmii_tx_ctl (fpga_tx_ctl), .rgmii_tx_clk (fpga_tx_clk), .rgmii_rxd (fpga_rxd), .rgmii_rx_ctl (fpga_rx_ctl), .rgmii_rx_clk (fpga_rx_clk) ); // 模拟PHY回环接收时钟用发送时钟数据加2.5ns延时 always (posedge fpga_tx_clk) begin #2.5; fpga_rx_clk fpga_tx_clk; fpga_rxd fpga_txd; fpga_rx_ctl fpga_tx_ctl; end initial begin clk_sys 0; forever #10 clk_sys ~clk_sys; // 50MHz系统时钟 end ... endmodule这段代码中的#2.5注释说明RGMII数据从TX到RX本质上是异步关系但PHY回环只是为了验证MAC逻辑因此用固定延迟模拟。真实仿真里还需要加入PHY的延迟模式变化比如千兆模式延迟1.5ns~2ns百兆模式延迟更大否则某些采样边界问题在仿真中看不出来。Testbench还应该在发送完一帧后检查接收FIFO的长度寄存器和CRC状态标志断言收到的数据与发送一致。4.3 上板验证从约束到ILA抓信号上板验证的第一步是做引脚约束。RGMII信号的IO位置必须严格按PCB原理图填写同时要给时钟建立约束create_clock -name sys_clk -period 20.000 [get_ports clk_sys] create_clock -name tx_clk -period 8.000 [get_ports rgmii_tx_clk] create_clock -name rx_clk -period 8.000 [get_ports rgmii_rx_clk] set_false_path -from [get_clocks tx_clk] -to [get_clocks rx_clk]set_false_path在这里不是偷懒而是告诉综合工具发送时钟和接收时钟虽然频率相同但相位关系不固定不需要做跨时钟路径时序收敛。如果漏掉这条约束时序报告中会大量出现红色路径严重时综合工具会为了满足这些伪路径去反复布线导致编译时间爆炸。上板后先用短帧回环模式测试。把PHY配置成内部回环然后从PC端ping FPGA的MAC地址。如果通了再把PHY切到正常模式用PC发包工具发送固定pattern。此时FPGA侧用ILA抓rx_byte和rx_ctl信号检查字节顺序是否符合预期。我习惯先用UDP包做测试因为UDP包结构简单接收方只要看到目标MAC和源MAC正确就可以直接看负载内容。5. 常见问题与避坑指南时钟约束、时序收敛与数据错位5.1 仿真正常但上板偶发丢包现象仿真回环完全通过上板后PC发1000个包收到的只有600个而且丢包位置不固定。原因通常是PHY的RGMII接收数据采样不稳定FPGA内部没有做任何延迟补偿导致采样点落在数据跳变沿附近。解决方法是先将PHY配置成内部延迟模式或用FPGA侧IDELAY给rx_clk加一个固定相移。对于某厂商PHY需要设置RGMII TX/RX内部延迟寄存器对于没有内部延迟的PHY则要在FPGA中例化IDELAYE。一般先加2ns延迟观察误码率再逐步调整到1.5ns。5.2 RGMII半字节顺序反了现象发送的0x0800变成了0x0008或者整个帧内容被字节颠倒抓包软件里只能看到奇特的MAC地址。原因是对“上升沿低4位、下降沿高4位”理解有偏差接收端拼成了{rx_low, rx_high}而不是{rx_high, rx_low}。解决方法是先构造一个0x12, 0x34, 0x56, 0x78的测试包上板后用ILA看接收FIFO的第一个字节。如果第一个字节是0x21说明低半字节和高半字节互换了如果第一个字节变成了0xDE之类的值还要考虑RX_CTL拆开的DV/ER顺序。这类问题在仿真里非常容易忽略因为很多仿真PHY模型只传递字节流不看真实半字节时序。5.3 时序收敛失败未约束GTX_CLK和RX_CLK现象综合能过布局布线后时序报告大量红色或者生成bit流时直接报错。原因是GTX_CLK、RX_CLK这些引脚没有被约束成真实时钟综合工具默认把它们当作普通信号导致内部寄存器时钟树只按系统时钟做平衡。解决方法是像前文一样给每个RGMII时钟单独create_clock并检查它们是否进入全局时钟网络。另一个容易漏掉的是MDC引脚如果MDC做普通计数器分频约束可以不用创建时钟但如果你用了ODDR产生MDC就必须创建时钟并确认它没有跑到内部逻辑时钟域里。5.4 复位释放导致状态机卡死现象上电后偶尔第一次发包不成功需要多试几次才能恢复。原因很可能是复位信号在125MHz或RX时钟域未被同步释放导致部分已复位、部分未复位。比如发送状态机的seed寄存器已经被复位但FIFO的写指针还在旧值上状态机会从错误状态开始。解决方法是使用两级触发器同步复位信号并且每个时钟域各自独立做异步复位同步释放。代码里放一个clk_rst.v模块内部生成rst_n_sync所有子模块的rst_n都应该接这个同步后的复位而不是顶层原始复位。5.5 MDIO读写超时或读到全FF现象MDIO读取PHY寄存器全返回0xFFFF或者读写操作一直busy。原因是MDC时钟频率太高MDIO时序不满足。标准MDC最高频率是2.5MHz很多FPGA的100MHz系统时钟直接分频到1MHz以下用如果用了20MHz分频得到5MHzPHY可能完全无响应。解决方法是写一个可参数化的分频器把MDC频率控制在1MHz~2.5MHz之间。注意MDIO是双向信号在FPGA内部需要三态缓冲读操作时要在读取采样点前把mdio方向切换成输入否则你读到的可能是自己驱动的强0或强1。6. 二次开发把MAC扩展成自定义协议接口6.1 改造用户侧接口如果不想用AXI-Stream可以把发送侧改成简单的双寄存器接口。比如给MAC增加一个cmd_start、cmd_len[10:0]和data_bus[31:0]由外部DMA按32位突发写入。你只需要在顶层把用户侧总线翻译成tdata、tvalid、tlast不需要动MAC内部状态机。接收侧同理增加一个rx_irq中断脚当接收FIFO长度寄存器不为0时拉高软件读取后通过rx_rden消耗数据就可以减少对轮询的依赖。6.2 加入错误统计与CRC硬件校验项目里CRC32模块已经能输出校验结果你可以在接收描述符里增加三个字段帧长度、CRC错误标志、长度错误标志。发送侧也应该在TLAST后检查实际发送字节数是否小于64如果小于64自动补PAD到64字节。这一步很多商用MAC会做但自己手写MAC时经常漏掉导致短包被交换机直接丢弃看起来就是“发不出包”。6.3 验证方法与回归习惯改完接口后一定要先做一次FPGA内部回环再做外部PHY回环最后才接真实网线。内部回环可以在MAC顶层把tx_byte直接连接给接收逻辑绕开RGMII引脚外部PHY回环则是我前面说的PHY寄存器回环。从那以后我每设计一个新接口都会强制自己先跑一遍这三个步骤确保改动是收敛的希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑