简介:面向FPGA与网络协议学习者、以太网接口开发工程师的千兆-百兆自适应UDP传输工程,围绕链路速率检测与动态适配、UDP数据收发及硬件加速展开,可用于实时数据传输类项目的原型验证与代码复用。压缩包共19个文件,约69KB,主体为12个Verilog源码,配套XCI IP配置、XML描述、TCL脚本与XDC约束,覆盖GMII/RGMII转换、时钟与FIFO管理、CRC32校验及UDP收发控制等模块。已有1302人学习下载,说明其具备一定参考价值。读者可据此理解FPGA端UDP协议栈的分层组织、百兆与千兆模式切换思路、发送接收通路和时序约束写法,并在此基础上做仿真验证、接口移植与性能优化,适合作为以太网UDP通信入门到进阶的工程样例。
1. 需求先立住:为什么这套千兆-百兆自适应以太网UDP传输要用FPGA做
某实验室做高速数据采集,前端FPGA把多通道AD数据打包上传,上位机是一台普通办公电脑。第一次联调就翻车:开发板上用的是千兆PHY,Verilog代码按125MHz时钟写死,插到实验室那台老交换机的百兆口上,链路灯亮着,数据一个字节都上不来;换回千兆口又一切正常。问题不在UDP协议栈,而在于整条通路没有为「对端速率不确定」做设计。这套千兆-百兆自适应以太网UDP传输方案,要解决的就是FPGA在未知链路速率下仍能稳定收发UDP数据报:上电后自动识别百兆还是千兆,切换发送时钟与接口时序,再按标准以太网帧封装UDP报文。适合做毕设、课设,也适合需要给采集板快速加网口的工程场景。
2. 总体架构设计:从PHY接口到用户侧接口的四层划分
动手写代码之前,先把「谁负责什么」定下来。FPGA做以太网UDP传输,最容易出现的结构问题是把MAC帧组装、CRC计算、时钟切换、用户数据缓冲全塞进一个always块,仿真能过,上板一出问题就无从下手。我的做法是切成四层,每层只暴露一组明确的接口信号,层与层之间用valid/ready握手,出问题时可以逐层打点定位。
2.1 四层模块划分与职责边界
从下往上依次是PHY接口层、MAC层、UDP/IP协议层、用户接口层。PHY接口层只负责RGMII的双沿收发、输入延迟校准和速率相关的时钟选择,不关心帧内容;MAC层负责前导码、SFD、帧定界、CRC32生成与校验、目的MAC过滤;UDP/IP协议层拼IP首部和UDP首部、算校验和、做端口过滤;用户接口层是一组简单的valid/ready信号,把用户数据流接进来。
这样切的好处是速率自适应只影响最底下两层。千兆切百兆时,协议层和用户层完全不用改,因为它们看到的始终是同一套valid/ready时序,速率变化被异步FIFO吸收掉了。反过来,如果用户逻辑直接对接RGMII引脚,速率一变就得改一大片代码,这种结构在答辩时也很难讲清楚。
| 层次 | 主要模块 | 关键职责 | 速率相关性 |
|---|---|---|---|
| 用户接口层 | user_if | 数据缓存、流控、帧边界标记 | 无关 |
| UDP/IP协议层 | udp_tx / udp_rx | 首部拼装、校验和、端口过滤 | 无关 |
| MAC层 | mac_tx / mac_rx | 前导码、SFD、CRC32、帧过滤 | 部分相关 |
| PHY接口层 | rgmii_if / speed_ctrl | DDR收发、IDELAY、时钟切换 | 强相关 |
2.2 顶层端口与时钟域规划
顶层端口按功能分组,时钟单独列出,避免后面改一处牵连一片。下面这段是顶层的端口定义,省略了复位和调试信号:
module eth_udp_top #( parameter LOCAL_MAC = 48'h02_00_00_00_00_01, // 本地MAC,避免与上位机冲突 parameter LOCAL_IP = {8'd192, 8'd168, 8'd1, 8'd10}, parameter LOCAL_PORT = 16'd8080, // UDP源端口 parameter DST_PORT = 16'd8081 // UDP目的端口 )( input wire clk_125m, // 板载晶振,经PLL倍频得到 input wire rst_n, // RGMII接口,4位数据双沿传输 output wire [3:0] rgmii_txd, output wire rgmii_txc, output wire rgmii_tx_ctl, input wire [3:0] rgmii_rxd, input wire rgmii_rxc, input wire rgmii_rx_ctl, // MDIO管理接口,用于读取PHY协商结果 output wire mdc, inout wire mdio, output wire phy_rst_n, // 用户侧数据流 input wire [63:0] s_axis_tdata, input wire [7:0] s_axis_tkeep, input wire s_axis_tvalid, output wire s_axis_tready, input wire s_axis_tlast );三个时钟域要提前想清楚:clk_125m是系统固定时钟,协议层和用户层都跑在这个域;rgmii_rxc由PHY提供,千兆时125MHz、百兆时25MHz,接收通路必须用它;rgmii_txc由FPGA产生,频率必须与当前链路速率一致。前两个是输入,第三个是输出,三者的关系决定了整个自适应方案怎么落地。
2.3 接收与发送的时钟不对称性
这里有个容易被忽略的点:接收侧的时钟是PHY给的,链路速率变了rgmii_rxc自动跟着变,接收逻辑本身不需要切换时钟,只要在它后面挂一个写时钟为rgmii_rxc、读时钟为clk_125m的异步FIFO,速率差异就被吸收掉了。发送侧则相反,rgmii_txc必须由FPGA产生,而且频率必须精确等于链路速率对应的值,否则PHY采样不到正确数据。
所以自适应的核心难点在发送侧,不在接收侧。很多方案在接收上做了大量工作,发送侧却写死125MHz,结果就是「能收不能发」或者「千兆通百兆不通」。把这条不对称性想明白,后面的时钟方案就顺了。
3. 千兆-百兆自适应的实现:速率识别、时钟切换与RGMII时序
自适应这三个字拆开看是两件事:先知道当前是百兆还是千兆,再把发送通路调整到对应速率。识别靠MDIO读PHY状态寄存器,调整靠预置两路时钟做无毛刺切换。这一章把这两步的细节和RGMII的时序约束讲透。
3.1 速率从哪里来:MDIO读状态寄存器
PHY在上电后会通过自协商确定链路速率,结果写在状态寄存器里。最直接的办法是用MDIO主机读寄存器0x01(BMSR)的bit[5]确认自协商完成,再读厂商私有的PHY状态寄存器拿当前速率位——具体是bit几要看PHY手册,不同厂家的定义不一样,这是必须翻手册的地方,不能凭经验猜。
下面这段是MDIO读操作的状态机骨架,MDIO是开漏总线,读操作时主机要在turnaround阶段释放数据线:
// MDIO读时序:前导码32个1 -> 起始码01 -> 读操作10 -> PHY地址5位 // -> 寄存器地址5位 -> TA(2位,主机释放) -> 16位数据 localparam MD_IDLE = 3'd0, MD_PRE = 3'd1, // 发送32个1 MD_ST = 3'd2, // 起始码+读写+地址 MD_TA = 3'd3, // 转向,释放总线 MD_DATA = 3'd4, MD_DONE = 3'd5; // 时钟分频:系统时钟125MHz,MDC最高2.5MHz,取50分频 reg [5:0] mdc_div; always @(posedge clk_125m or negedge rst_n) if (!rst_n) mdc_div <= 6'd0; else mdc_div <= (mdc_div == 6'd49) ? 6'd0 : mdc_div + 1'b1; assign mdc = (mdc_div >= 6'd25); // 占空比接近50%读回来的速率位要打两拍同步到clk_125m域,并做去抖:连续三次读到相同值才认为速率真的变了。网线插拔、对端设备重启都会让状态位短暂跳变,不做去抖就会频繁触发时钟切换,反而把链路搞挂。识别周期我一般设成100ms轮询一次,既不会太慢,也不会干扰正常收发。
3.2 时钟方案:预置两路时钟加无毛刺切换
发送时钟不能靠动态改PLL分频系数来实现,因为PLL失锁到重新锁定之间有一段输出不稳定的窗口,这段时间TXC是乱的,PHY会直接判定链路异常。可行的做法是让PLL同时输出125MHz和25MHz两路,用FPGA厂商提供的无毛刺时钟选择原语(类似BUFGMUX的结构)做切换,切换过程中输出会保持低电平,不会产生窄脉冲。
// 两路时钟预先锁定,切换只做选择,不改PLL配置 // is_gigabit 为 1 选 125MHz,为 0 选 25MHz wire clk_txc_sel; clk_mux_glitch_free u_txc_mux ( .clk0 (clk_25m), // 百兆:25MHz .clk1 (clk_125m), // 千兆:125MHz .sel (is_gigabit), .clk_o (clk_txc_sel) ); // 切换后先屏蔽发送,等时钟稳定若干个周期再放开 reg [3:0] sw_guard; always @(posedge clk_125m or negedge rst_n) if (!rst_n) sw_guard <= 4'd0; else if (rate_changed) sw_guard <= 4'd15; // 触发保护窗口 else if (sw_guard != 0) sw_guard <= sw_guard - 1'b1; assign tx_enable = (sw_guard == 4'd0); // 保护窗口内禁止发帧切换顺序很关键:先拉低tx_enable让MAC发送状态机回到IDLE,清空发送FIFO里的残留数据,再切时钟,等sw_guard计满后放开。如果顺序反了,切换瞬间正在发送的半帧会被截断,对端会收到一个runt帧,Wireshark里能看到「帧长度异常」的告警。
3.3 RGMII的DDR采样与输入延迟
RGMII用4位数据线在时钟双沿传输,千兆时等效8位@125MHz,百兆时等效4位@25MHz。发送侧用双沿输出寄存器,把8位数据拆成上升沿4位和下降沿4位;接收侧反过来,用双沿采样把4位还原成8位。
千兆模式下TX_CTL是TXEN和TXERR的异或,百兆模式下TX_CTL就是TXEN,这个差异必须在发送逻辑里按速率分支处理,写死一种模式是百兆不通的常见原因。
接收侧的rgmii_rxc和rgmii_rxd之间存在走线延迟,千兆下这点延迟可能占到时钟周期的很大比例,必须用输入延迟单元(IDELAY)校准。校准值不能拍脑袋定,通常的做法是上板后扫描一遍延迟抽头,找到数据稳定的窗口中点,把值固化到参数里。百兆下时序裕量大得多,同一组延迟值一般也能用,但严格来说应该按速率分别配置。
3.4 速率切换后的状态清理
切换完成后有三件事要做:清空接收FIFO,因为里面可能残留着上个速率下的半帧数据;复位CRC计算模块的中间寄存器,CRC是跨帧连续计算的,残留状态会让新帧校验失败;重新使能MAC接收过滤,避免切换瞬间的噪声帧被当成有效数据。
这套清理逻辑我一般写成一个独立的rate_change_seq模块,输入是rate_changed脉冲,输出是各个模块的软复位信号,按固定顺序释放。顺序错了会出现「接收先于发送使能」的竞争,导致第一帧丢失,虽然只丢一帧,但做吞吐测试时会被统计进去。
4. UDP协议栈实现:帧封装、校验和与收发状态机
协议层的工作是把用户数据变成符合IEEE 802.3和RFC 768的字节流,再把收到的字节流还原成用户数据。这部分逻辑不复杂,但细节多,位序、字节序、校验和的反码求和,任何一处错了都表现为「抓包能看到帧,但上位机收不到数据」。
4.1 MAC帧结构与发送状态机
一个完整的以太网帧是:7字节前导码0x55、1字节SFD 0xD5、6字节目的MAC、6字节源MAC、2字节类型0x0800、46到1500字节的IP数据报、4字节CRC32。帧与帧之间至少留12字节的帧间隔。
发送状态机按这个结构走,每个状态输出对应的数据,用valid/ready与下游握手:
localparam TX_IDLE = 3'd0, TX_PRE = 3'd1, TX_SFD = 3'd2, TX_HDR = 3'd3, TX_DATA= 3'd4, TX_CRC = 3'd5, TX_IFG = 3'd6; always @(posedge clk_txc or negedge rst_n) if (!rst_n) begin state <= TX_IDLE; tx_en <= 1'b0; end else if (!tx_enable) begin state <= TX_IDLE; // 速率切换期间强制回IDLE tx_en <= 1'b0; end else case (state) TX_IDLE: if (frame_req) begin state <= TX_PRE; // 用户侧有完整帧待发 cnt <= 7; end TX_PRE: begin tx_en <= 1'b1; txd <= 8'h55; // 前导码 if (cnt == 0) state <= TX_SFD; else cnt <= cnt - 1'b1; end TX_SFD: begin txd <= 8'hD5; state <= TX_HDR; end TX_HDR: begin txd <= hdr_data; // 目的MAC/源MAC/类型/IP头/UDP头 if (hdr_done) state <= TX_DATA; end TX_DATA: begin txd <= fifo_dout; if (fifo_empty) state <= TX_CRC; // 数据发完转CRC end TX_CRC: begin txd <= crc_data; // 4字节CRC32,低位先发 if (crc_done) state <= TX_IFG; end TX_IFG: begin tx_en <= 1'b0; // 帧间隔期间拉低使能 if (ifg_cnt == 12) state <= TX_IDLE; end endcase这段状态机里有两个设计点值得说明。一是tx_enable为低时强制回IDLE,保证速率切换不会发出残帧;二是TX_DATA段在FIFO空时不是立刻转CRC,而是要确认tlast已经拉高,否则大帧会被提前截断,表现为小包正常大包异常。
4.2 IP/UDP首部与校验和计算
IP首部固定20字节,UDP首部8字节。需要填的字段:版本4、首部长度5、总长度、标识、标志位、TTL 64、协议号17、源IP、目的IP、源端口、目的端口、UDP长度。这些字段大部分是常量,用查找表或者位移拼装都行,我一般写成组合逻辑直接拼,省一个状态。
UDP校验和是16位反码求和,计算范围包括12字节伪首部、8字节UDP首部和全部数据。伪首部里有源IP、目的IP、协议号、UDP长度。漏掉伪首部是最常见的错误,现象是Wireshark提示「校验和错误」,很多网卡会直接丢弃这类报文,用户侧看到的就是「发出去了但没收到」。
// 16位反码求和:逐16位累加,进位回卷,最后取反 // 校验和字段在计算时先置0,算完再填入 always @(posedge clk_125m) begin if (sum_init) begin sum <= 32'd0; end else if (sum_en) begin sum <= sum + {16'd0, data[15:0]}; end end // 回卷:高16位加到低16位 wire [16:0] fold = sum[31:16] + sum[15:0]; assign checksum = ~(fold[15:0] + fold[16]);注意数据长度是奇数时要在末尾补一个0字节再参与计算,这个补零不能算进UDP长度字段。这个细节不处理,奇数字节长度的报文校验和全错,而偶数字节全对,排查时非常有迷惑性。
4.3 接收通路:SFD检测、过滤与CRC校验
接收侧从rgmii_rxc域开始,先把双沿采样的4位数据还原成8位,然后找SFD。检测到0xD5后开始收帧,同时启动CRC计算。收完一帧后比对CRC结果,再检查目的MAC是不是本地MAC或广播地址、类型是不是0x0800、IP协议号是不是17、目的端口是不是配置值,全部通过才写入接收FIFO。
过滤顺序要从快到慢:先比目的MAC(6字节,最早到达),再比类型和协议号,最后比端口。这样大部分无关广播帧在MAC层就被丢掉了,不会占用后面的FIFO带宽。CRC校验放最后,因为要收完整个帧才能算完,但一旦校验失败整帧丢弃,不做部分提交。
4.4 跨时钟域FIFO与背压处理
接收FIFO的写时钟是rgmii_rxc(速率相关),读时钟是clk_125m(固定),必须用异步FIFO。深度按最大帧1518字节算,至少2K字节,留一倍余量取4K。发送方向相反,写时钟固定读时钟随速率变,同样需要异步FIFO。
背压的处理原则是:FIFO快满时向上游拉低ready,而不是丢数据。UDP本身不保证可靠,但FPGA内部丢数据和链路上丢数据是两回事,前者会让吞吐测试结果莫名其妙地低。千兆下FIFO读写速率差距不大,百兆下写慢读快,FIFO长期处于接近空的状态,反而不会溢出;真正危险的是速率切换瞬间,所以切换时清FIFO的动作不能省。
5. 常见问题与排查:仿真通过、上板不通的几类翻车
仿真里所有激励都是理想时序,上板后PHY、走线、对端设备都会引入仿真中不存在的问题。这一章列的都是我在实际项目里踩过的坑,按「看到什么现象、为什么、怎么解决」组织,遇到类似症状可以直接对照。
5.1 链路灯亮但一个字节都收不到
现象是网口指示灯正常,上位机ping不通,抓包工具看不到任何来自FPGA的帧。
原因通常是发送时钟频率和链路速率不匹配。比如PHY协商到百兆,FPGA侧rgmii_txc仍然是125MHz,PHY按25MHz的节奏去采样,收到的全是错位数据,自然组不成有效帧。另一种情况是TX_CTL在百兆下仍按千兆的异或逻辑生成,控制线语义错了,PHY认为没有有效数据。
解决方法是先用示波器量rgmii_txc的实际频率,千兆必须是125MHz、百兆25MHz,误差在±50ppm以内。频率对了再看TX_CTL在数据段是否为高、帧间隔是否为低。这两个量对了,基本就能发出合法帧。
5.2 千兆正常、百兆丢包严重
现象是千兆口跑满不丢包,换到百兆口后能通但丢包率很高,小包尚可,大包几乎全丢。
原因是接收通路的跨时钟域处理没做对。百兆下rgmii_rxc只有25MHz,如果CRC计算或者状态机误用了clk_125m,两个域的数据会对不上,帧尾的CRC校验随机失败。另一种可能是FIFO深度按千兆的突发特性设计,百兆下写慢读快,读侧过早读到空,把半帧提交给了协议层。
解决办法是检查接收通路上所有时序逻辑的时钟是不是rgmii_rxc,CDC只在FIFO一处发生。再用逻辑分析仪抓FIFO的读写指针,看百兆下是否出现读指针超过写指针的情况。
5.3 小包正常、大包被截断
现象是几十字节的报文能正常收发,超过几百字节的报文对端收到的是残帧,Wireshark提示长度异常或CRC错误。
原因多半在发送状态机的TX_DATA段。FIFO读空时如果直接跳转到TX_CRC,帧就被提前结束了。正确做法是等tlast有效才转CRC,FIFO空只表示当前没有数据可读,不表示帧结束。另一种可能是FIFO深度不够,大帧写到一半溢出,尾部数据丢失。
解决办法是在TX_DATA段加tlast判断,同时把发送FIFO深度加到能容纳一个最大帧。如果用户侧数据是连续流而不是整帧提交,还要在用户接口层做帧边界标记,不能让MAC层去猜哪里是帧尾。
5.4 插拔网线后链路不恢复
现象是首次上电正常,拔掉网线再插上,或者对端设备换了个速率,FPGA就再也发不出数据,必须重新上电。
原因是速率变化没有被正确处理。自协商期间PHY会短暂断开链路,rgmii_rxc可能停振,如果接收状态机没有超时保护,会永远卡在收帧中间某个状态。另外速率变化后没有触发FIFO清理和状态机复位,残留状态让新链路的第一帧就失败。
解决办法是给接收状态机加超时计数器,超过最大帧时间还没有收到帧尾就强制回IDLE;速率变化脉冲触发一次完整的软复位序列,按「停发送、清FIFO、切时钟、复位CRC、重新使能」的顺序执行。
5.5 校验和与CRC的隐蔽错误
现象是抓包能看到完整帧,格式看起来也对,但上位机应用层收不到数据,或者收到的是乱码。
原因有两个高频点。一是CRC32的位序搞反,以太网CRC是LSB先出,如果按MSB先出计算,算出来的值和标准值对不上,对端直接丢帧。二是UDP校验和漏算伪首部,Wireshark会标红提示校验和错误,很多网卡默认丢弃这类报文。
解决办法是用一段已知的正确帧做比对测试:把标准帧的字节流输入CRC模块,看输出是否等于约定的余数;UDP校验和用一组固定数据手算一遍,和模块输出对比。这两个模块建议单独做单元测试,不要等到整链路联调时才发现。
6. 验收与答辩:用抓包、计数器和时序报告证明链路真的达标
方案做完不等于能交付,验收环节要把「能通」变成「有数据支撑的可信结论」。答辩时最怕的是被问「你怎么证明百兆下真的不丢包」,答不上来就很被动。
6.1 分层验收:四个层级各有各的证据
物理层看波形:示波器测rgmii_txc频率,千兆125MHz、百兆25MHz,上升沿和眼图在规格内。链路层看抓包:Wireshark里能看到完整的以太网帧,前导码、目的MAC、类型字段都对。协议层看校验和:UDP校验和显示正确,没有红色告警。应用层看吞吐:用打流工具持续发送固定长度报文,统计收发包数和丢包率。
| 验收层级 | 测试项 | 通过标准 | 使用工具 |
|---|---|---|---|
| 物理层 | TXC频率、眼图 | 125MHz/25MHz,误差±50ppm | 示波器 |
| 链路层 | 帧格式、CRC | 抓包无格式告警 | 抓包工具 |
| 协议层 | UDP校验和 | 校验和正确,无丢帧 | 抓包工具 |
| 应用层 | 吞吐、丢包率 | 千兆≥900Mbps,百兆≥90Mbps | 打流工具 |
| 稳定性 | 长时间运行 | 连续24小时丢包率为0 | 打流工具+计数器 |
6.2 用内部计数器做交叉验证
抓包工具只能看到链路上的帧,看不到FPGA内部丢了多少。我一般在设计里预留一组调试计数器:发送帧数、接收帧数、CRC错误数、FIFO溢出次数、速率切换次数,通过一个调试寄存器接口读出来。验收时抓包统计的应用层帧数应该和发送帧数减去CRC错误数对得上,对不上就说明中间有丢帧,再顺着计数器定位是哪一级丢的。
这组计数器在答辩时特别好用。评委问「百兆下丢包吗」,直接调出计数器:发送10万帧,接收10万帧,CRC错误0,FIFO溢出0,比任何口头保证都有说服力。计数器本身也会占资源,我一般只在调试版本里打开,正式版本用宏关掉。
6.3 答辩时怎么讲清楚「自适应」
讲自适应不要一上来就说时钟怎么切,先讲问题:为什么写死速率不行,什么场景下会遇到百兆链路。然后讲识别:MDIO读PHY状态寄存器,去抖,轮询周期。再讲切换:预置两路时钟、无毛刺选择、保护窗口、状态清理。最后讲验证:示波器频率、抓包、计数器。
这个顺序的好处是每一层都有对应的证据,评委顺着听下来不会觉得是在堆概念。如果时间允许,现场演示一次插拔网线,让链路灯和计数器同时变化,比PPT上画十张图都直观。
从那以后我每次做这类带速率自适应的设计,都强制走一遍「示波器量频率、抓包看格式、计数器对帧数」这三步,哪怕仿真已经全绿。仿真跑的是理想世界,上板跑的是真实世界,这两者之间的差距,基本都藏在这三步里。希望帮到你。
本文还有配套的精品资源,点击获取