
简介面向FPGA与网络协议学习者的千兆/百兆自适应以太网UDP传输工程聚焦链路速率检测与UDP硬件收发适合希望理解RGMII/GMII接口转换、以太网帧封装及低延迟传输机制的中级读者。压缩包共19个文件以12个Verilog源文件为主体搭配Xilinx IP配置文件xci、XML描述、Tcl脚本与XDC约束整体约69KB便于在Vivado中重建或对照工程结构。已有1302人学习下载。读者可从中梳理速率自适应控制、UDP发送接收状态机、CRC32校验、FIFO缓冲与时钟管理等模块的协作关系并参考顶层连接与约束文件完成仿真、综合和上板调试。工程虽小但覆盖从物理层接口到传输层组包的关键路径适合作为FPGA以太网通信入门与课程设计参考。1. 千兆百兆自适应不是多写一个 if 的事一块板子要同时活在两个时钟里有个做图像采集的同学找我说他的板子插在千兆交换机上一切正常换个百兆的老交换机就彻底哑了抓包软件里连 ARP 都看不到。拆开看代码接收通路写死了 125 MHz、8 bit 位宽前导码按 8 位一拍去匹配百兆链路下 PHY 只给 25 MHz、4 bit 半字节整个状态机的节拍全错位收到的自然全是垃圾。这就是千兆-百兆自适应以太网 UDP 传输这类 FPGA Verilog 工程真正的难点它要把自协商结果、时钟域、位宽转换、协议栈组包和校验和这几件事串成一条稳定的流水线并不是在模块里加个速率寄存器就能解决。这套方案适合三类人做毕设需要一块板子跑通 UDP 收发并讲清架构的做课设要用 Verilog 从零搭 MAC 和 UDP 的以及手里已有千兆通路、想补上百兆兼容性的从业者。下面按需求、架构、自适应、协议栈、避坑、验收的顺序把这份工程完整拆一遍。2. 总体架构双速率 MAC 与 UDP 协议栈的六个模块怎么切2.1 从 PHY 引脚到用户数据口的四段通路整条通路我一般切成四段PHY 接口适配层、MAC 收发层、协议解析层、用户缓冲层。PHY 接口适配层负责把 GMII 的 8 bit 和 MII 的 4 bit 统一成「一拍一个字节 一个 valid」的内部约定上层模块不需要关心当前跑的是哪种速率。MAC 收发层处理前导码 7 个 0x55 加 1 个 SFD 0xD5 的识别、帧尾 4 字节 FCS 的校验与生成、以及帧间隔 IFG 的计时。协议解析层按以太网类型字段分流0x0806 走 ARP0x0800 走 IPv4 再解 UDP其余丢弃。用户缓冲层就是收发各一组异步 FIFO加上一组控制状态寄存器让用户逻辑用自己熟悉的时钟读写数据不用直接对接 PHY 的恢复时钟。这样切的好处是每一层都能单独仿真。PHY 适配层用行为级模型灌 GMII 和 MII 两套时序MAC 层灌固定帧长的激励协议层直接把以太网帧喂进去看解出来的 UDP 载荷对不对。等到上板的时候哪一层出问题波形上定位非常快。比较忌讳的写法是把前导码检测、类型判断、校验和计算全塞在一个 always 块里一个信号出错整段逻辑都废调试时只能靠猜。2.2 时钟方案125 MHz 8 bit 与 25 MHz 4 bit 的换算关系速率自适应的核心其实是时钟和位宽两个维度同时变化先把这个换算关系列清楚后面所有设计都有依据。链路模式PHY 接口时钟频率数据位宽单拍字节数理论线速1000BASE-TGMII125 MHz8 bit11000 Mbps100BASE-TXMII25 MHz4 bit0.5100 Mbps100BASE-TXGMII部分 PHY 支持25 MHz8 bit1100 Mbps接收方向的时钟来自 PHY 的 rx_clk是恢复出来的时钟千兆 125 MHz、百兆 25 MHz不能拿板上的晶振去替代。发送方向千兆用 gtx_clk125 MHz由 FPGA 提供或 PHY 回送百兆用 tx_clk25 MHz。用户侧我通常统一在 100 MHz 或 50 MHz 的系統时钟上收发各用一个异步 FIFO 跨过去FIFO 深度按最大帧 1518 字节再加一点余量做到 2K 深度、8 bit 位宽基本够用。提示百兆模式下如果 PHY 配置成 GMII 接口但速率是 100M时钟是 25 MHz、位宽仍是 8 bit这种情况最容易在参数化时被漏掉判断逻辑要按「时钟频率」和「位宽」两个独立维度来做不能只用一个 gigabit 标志位。2.3 模块清单与接口约定把六个模块的职责和时钟域钉死后面写代码基本就是填空题模块时钟域主要接口职责phy_ifrx_clk / tx_clkGMII、MII、内部 8bitvalid位宽归一化、速率选择mac_rxrx_clk8bitvalid、帧长、FCS 结果前导码对齐、FCS 校验、包边界mac_txtx_clk8bitvalid、帧长前导码、FCS 生成、IFG 计时proto_rxrx_clk 或用户时钟帧数据、类型、UDP 载荷ARP/IP/UDP 解析、地址过滤proto_tx用户时钟用户数据、目标 IP/端口ARP 查表、首部组装、校验和udp_fifo跨时钟用户侧读写收发缓冲、背压接口约定里最容易含糊的是「包边界」怎么表达。我的做法是每条通路都带一个 sop帧首拍和 eop帧末拍标志配一个帧长计数器上层模块只要看到 sop 就能复位自己的解析状态看到 eop 就结账。这个约定一旦统一ARP、IP、UDP 三层解析器可以写成同一套模板改改偏移量就行。2.4 参数化还是运行时判决两种做法都见过。参数化是把速率做成parameter综合时生成两套逻辑靠一个顶层开关切换优点是时序确定、资源可控缺点是要重新编译才能换速率本质上是「手动自适应」。运行时判决是让速率信号从 PHY 的自协商结果里实时读出来走同一套逻辑切换时靠多路选择器切换时钟和位宽处理分支。工程上我倾向运行时判决因为交换机端口速率是插上网线那一刻才确定的参数化方案在现场根本没法用。代价是多路选择器会引入一点组合延迟以及切换瞬间必须做状态清理这两点在第三章会展开。真正的自适应逻辑只有一处需要小心不是「知道现在是百兆」而是「知道现在从千兆变成了百兆」那个瞬间该做什么。3. 速率自适应自协商结果怎么读、通路怎么切3.1 MDIO 读 PHY 状态寄存器别只信 link 灯PHY 的自协商结果放在标准寄存器里通过 MDIO 两线接口读。MDIO 帧结构是 32 拍前导 1、2 bit 起始码 01、2 bit 操作码读 10、写 01、5 bit PHY 地址、5 bit 寄存器地址、2 bit 转向位、16 bit 数据。读的时候 FPGA 出前导和地址之后要把数据线放开交给 PHY 驱动转向位这一段如果方向切换晚了半个周期读回来的数据就会整体偏移一位这是新手最常见的翻车点。需要读的寄存器主要是两个寄存器 1BMSR的 bit15 是链路状态、bit11:10 是速率00 为 10M、01 为 100M、10 为 1000M、bit9 是双工寄存器 0BMCR用来发重启自协商命令。判定流程是上电后先等 BMSR 的 bit15 变 1再等自协商完成位然后锁存速率字段。这里有个玄学现象热插拔网线时 link 位会抖一下如果直接拿它当时钟切换的使能状态机会被反复复位。稳妥的做法是给 link 和速率信号各打两拍同步再做一次「稳定计数」连续 20 ms 内速率字段不变才认为有效变化时先拉低内部的 link_ok清空 FIFO、复位 MAC 状态机、把 ARP 表全部标脏等稳定后再拉起。这段逻辑不长但能省掉大量「板子有时候通有时候不通」的扯皮。有些板子会把 PHY 的速率指示引脚直接引到 FPGA 的普通 IO 上读引脚比读寄存器省事。这种做法能跑但换一颗 PHY 就得改约束可移植性差毕设答辩时也不太好讲清楚自协商过程我还是推荐走 MDIO。3.2 接收通路前导码对齐与双位宽拆包状态机接收侧最关键的是把 4 bit 半字节流安全地拼成字节。MII 传输时字节是低位先出一个字节的低半字节先到、高半字节后到GMII 则是整字节一拍给到。下面这段打包逻辑是整条接收通路的入口两种模式都走同一个 always 块靠phy_is_gigabit分流// 半字节打包MII 两拍拼一个字节GMII 直通 // rx_clk 由 PHY 提供千兆 125MHz、百兆 25MHz always (posedge rx_clk or negedge rst_n) begin if (!rst_n) begin nib_lo 4d0; nib_half 1b0; rx_dv8 1b0; rx_data8 8d0; end else if (phy_is_gigabit) begin // 千兆8bit 直通不参与拼包 rx_dv8 gmii_rx_dv; rx_data8 gmii_rxd; nib_half 1b0; end else begin if (mii_rx_dv) begin if (!nib_half) begin nib_lo mii_rxd; // 低半字节先到暂存 nib_half 1b1; rx_dv8 1b0; // 拼包期间不出 valid end else begin rx_data8 {mii_rxd, nib_lo}; // 高半字节拼上组成完整字节 nib_half 1b0; rx_dv8 1b1; end end else begin nib_half 1b0; // dv 撤销丢弃半个字节 rx_dv8 1b0; end end end这段代码有两个设计点值得说明。一是nib_half在半字节状态下遇到 dv 撤销会强制清零防止一个被截断的帧把下一个帧的字节对齐搞乱二是rx_dv8比数据晚一拍出下游状态机拿 valid 打拍天然对齐。位宽换算完成后前导码检测就简单了连续数到 0x55 达到 6 个以上再来一个 0xD5 就认为帧开始之后的数据按字节进状态机。状态机本身只需要五个状态IDLE 等前导、PREAMBLE 数 0x55、HEADER 收前 14 字节目的 MAC 6 源 MAC 6 类型 2、DATA 按类型字段把剩余字节分发、FCS 收最后 4 字节并比对。帧长计数在 HEADER 结束时就锁存总长度DATA 状态里用「剩余字节数」递减到 4 时自动切 FCS不需要再解析上层长度字段这样即使上层长度写错也不会把帧边界搞乱。3.3 发送通路位宽转换与 IFG 计时按速率重算发送侧的位宽转换是反过来的内部是 8 bit 字节流百兆时要拆成两个半字节分两拍送出去千兆时直接送。拆分时钟用 tx_clk同样由速率决定。这里有个细节——内部字节流的产生时钟通常是用户时钟需要先过异步 FIFO 到 tx_clk 域再拆不能在用户时钟域直接拆半字节否则跨时钟域的组合逻辑会被综合出奇怪的东西。IFG 的计时别用固定拍数。千兆下 12 字节帧间隔等于 96 拍 125 MHz百兆下 4 bit 位宽等于 24 拍 25 MHz看起来不一样但本质都是「12 个字节时间」。我一般用一个按字节计数的定时器tx_clk 下每送出一个字节加一加到 12 就允许发下一帧这样两种速率共用一套逻辑改速率不用改常数。前导码同样按字节发7 个 0x55 加 0xD5百兆时拆半字节千兆时整字节写成同一个发送子状态即可。还有一个容易忽略的点最后 4 字节 FCS 是低位先出的CRC 计算时如果按 MSB 移位上板后抓包工具会报「CRC 错误但内容正确」很多人会误以为是 PHY 配置问题实际是位序反了。3.4 切换瞬间的处理清 FIFO、丢半包、复位状态机速率切换不是无缝的链路重新协商期间会有几十毫秒到几秒的数据中断这段时间里 FIFO 里可能残留半个帧。处理顺序我固定成三步先把内部 link_ok 拉低让用户侧写 FIFO 的使能失效再对 MAC 收发状态机做一次同步复位把所有状态拉回 IDLE最后把收发 FIFO 整体清空ARP 表 valid 位清零。等自协商稳定、速率锁存完成后再把 link_ok 拉起用户侧重新开始发包。顺序不能反。先清 FIFO 再复位状态机复位过程中状态机可能又往 FIFO 里写几个字节残留就清不掉了。这个坑我在早期项目里踩过一次表现是百兆和千兆来回插拔几次之后偶尔会发出一两个源 MAC 错乱的帧抓包看像是地址被改写实际是 FIFO 里的旧数据和新帧头拼在了一起。4. UDP 协议栈落地ARP 表、IP 首部与校验和流水线4.1 ARP先答后问一张小表够不够以太网发 UDP 之前必须先知道对端 MAC所以 ARP 是绕不开的。设计上要同时支持两个方向作为应答方收到目标 IP 是自己的 ARP 请求就回一个应答作为请求方用户给了目标 IP 但表里没有 MAC 时主动发请求并等应答。表的大小按实际场景定做点对点传输、上位机固定几个 IP 的话4 到 8 项完全够用。字段位宽说明valid1表项有效标志link 切换时清零ip_addr32对端 IPv4 地址mac_addr48对应对端 MACage_cnt24老化计数按秒递减归零清 valid查表用组合逻辑并行比较命中就把 MAC 送进发送引擎没命中就触发 ARP 请求并把当前待发数据包挂起。挂起策略有两种一是缓存最近一包等应答回来再发二是直接丢弃让用户侧重发。毕设场景我推荐缓存一包因为抓包时能看到完整的「ARP 请求 → ARP 应答 → UDP 数据」时序答辩演示很直观。缓存深度按最大帧算一块 2 KB 的块 RAM 就够。ARP 应答方向要注意两点一是请求帧的帧长是 42 字节小于以太网最小帧 64 字节必须补齐到 60 字节再算 FCS否则交换机直接丢二是应答里的源 MAC 要填自己的 MAC不能照抄请求里的。4.2 IP 首部组装与增量校验和IPv4 首部 20 字节字段固定版本加首部长度 0x45、服务类型 0x00、总长度首部加 UDP 长度、标识符递增、标志位 0x4000允许分片但实际不分片、TTL 64、协议号 17、首部校验和、源 IP、目的 IP。总长度和校验和是每包都要重算的其余字段可以预先存成常量。校验和用 16 位反码和先对 10 个 16 位字求和把高 16 位折回低 16 位再取反。写法上我倾向用函数一次算完// IP 首部校验和20 字节按 16bit 大端切分求和折叠进位后取反 // 调用前校验和字段必须先填 0 function [15:0] ip_csum; input [159:0] hdr; // 20 字节首部高位在前 integer i; reg [31:0] sum; begin sum 32d0; for (i 0; i 10; i i 1) sum sum {16d0, hdr[i*16 : 16]}; // 逐字累加 sum sum[31:16] sum[15:0]; // 第一次折叠 sum sum[31:16] sum[15:0]; // 第二次折叠吃掉残余进位 ip_csum ~sum[15:0]; // 反码 end endfunction这段是纯组合逻辑10 次加法串起来在 125 MHz 下时序会紧。量产代码里通常改成两级流水第一级算 5 对的和第二级合并折叠或者干脆用增量更新——只有标识符和总长度在变其他字段固定校验和的差值可以预先算好每包只做一次加法。毕设阶段先用组合版本跑通时序不收敛再改流水线别一上来就过度设计。4.3 UDP 校验和为什么很多实现直接写 0IPv4 下 UDP 校验和是可选的发送方填 0 表示「未计算」接收方看到 0 就跳过校验。这给了一个很实用的取舍如果对端是 PC 上的普通 socket 程序填 0 完全能通省掉一整套伪首部累加逻辑如果对端是严格的嵌入式协议栈或者要做完整性验证就必须算。真要算的话伪首部是 12 字节源 IP 4 字节、目的 IP 4 字节、一个字节 0、一个字节协议号 17、两字节 UDP 长度。把伪首部、UDP 首部 8 字节、载荷连起来做 16 位反码和同样折叠取反结果为 0 时要写成 0xFFFF。载荷长度不是偶数时末尾补一个 0 字节参与计算但不计入长度字段。我的建议是毕设和课设直接填 0把精力放在速率自适应和时序上答辩时能说清楚「IPv4 下可选、置 0 表示不校验」这个设计取舍比堆一个跑不通的校验逻辑分高得多。要是项目要求端到端完整性再补这段逻辑不复杂但要注意字节序。4.4 发送引擎三种帧共用一条流水ARP 应答、ARP 请求、UDP 数据这三种帧的头部长度不同但发送流程完全一致前导码 → 帧头 → 载荷 → 补齐到 60 字节 → FCS → IFG。所以发送引擎只写一条状态机用一个「帧描述」寄存器组告诉它这包是什么类型、头部从哪取、载荷多长。// 发送主状态机三种帧共用帧类型只影响头部来源 localparam S_IDLE 4d0, // 等发送请求 S_PRE 4d1, // 7 个 0x55 1 个 0xD5 S_HDR 4d2, // 以太网头 IP/UDP 头或 ARP 头 S_PAY 4d3, // 用户载荷从 FIFO 读 S_PAD 4d4, // 不足 46 字节补 0 S_FCS 4d5, // 4 字节 CRC S_IFG 4d6; // 12 字节间隔 always (posedge tx_clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; byte_cnt 16d0; crc_en 1b0; end else begin case (state) S_IDLE: if (tx_req) begin frame_len tx_len; // 锁存本帧总长 state S_PRE; byte_cnt 16d0; end S_PRE : if (byte_cnt 16d7) state S_HDR; // 前导码结束 S_HDR : if (hdr_done) state S_PAY; S_PAY : if (pay_done) state (frame_len 16d60) ? S_PAD : S_FCS; S_PAD : if (byte_cnt 16d60) state S_FCS; // 补齐到最小帧 S_FCS : if (byte_cnt 16d3) state S_IFG; S_IFG : if (byte_cnt 16d11) state S_IDLE; default: state S_IDLE; endcase end end状态机里frame_len锁存这一刻很关键后面所有补齐判断、FCS 触发都用它不能在 S_PAY 阶段再去看用户侧的长度信号——用户侧可能已经改了。crc_en在前导码结束后拉高、FCS 开始时拉低CRC 计算模块只在窗口内累加避免把前导码算进去。发送背压的处理方式是用户侧 FIFO 空了但帧还没发完状态机停在 S_PAY 等同时输出一个 underflow 标志给用户侧连续两个周期读不到数据就主动中止本帧回到 IDLE 并把 CRC 结果丢弃。宁可丢一帧也不能发半帧半帧会让对端的 MAC 一直等 eop严重的会直接把对端网卡挂死。5. 避坑与排查五个让板子「灯亮不通包」的现场5.1 链路协商成百兆逻辑还按千兆跑现象插千兆交换机一切正常插百兆交换机后抓不到任何包或者偶尔收到一两个明显乱码的帧PHY 的 link 灯是亮的。原因速率信息只在复位时采样了一次或者采样用的是 BMCR控制寄存器而不是 BMSR状态寄存器读到的永远是上电默认值。热插拔之后 PHY 重新协商成百兆逻辑里phy_is_gigabit还是 1接收侧按 8 bit 一拍去解 4 bit 半字节流字节全错位。解决速率判定只在 link 稳定上升沿锁存加 20 ms 稳定窗口过滤抖动并且锁存的是 BMSR 的 bit11:10 解码结果。锁存值变化时走 3.4 节的三步清理流程。验证方法很简单在复位后手动强制 PHY 只广播百兆能力看逻辑是否跟着切。5.2 抓包显示 CRC 错误但内容完全正确现象抓包软件里能看到完整的 UDP 载荷内容和预期一致但每条帧都被标记为校验和错误接收端直接丢弃。原因以太网 FCS 用的是 CRC-32多项式 0x04C11DB7实际实现用反射形式 0xEDB88320初值全 1结果取反字节内低位先出。常见错误有两种一是用非反射多项式按 MSB 移位二是忘了最后取反三是输出的 4 字节顺序反了。解决核对三个参数——多项式用反射式、初值 32hFFFFFFFF、结果取反发送时先发 CRC 的最低字节。快速自检的办法是拿标准测试向量喂进 CRC 模块比如对全 0 的 60 字节算一次和抓包软件里已知的正确值对一下比上板试错快得多。5.3 小包被交换机静默丢弃现象发大包比如 1000 字节载荷正常发几个字节的短包比如心跳包对方收不到抓包工具在本地能看到发出对端就是没有。原因以太网最小帧是 64 字节不含前导码减去 14 字节帧头和 4 字节 FCS载荷至少 46 字节。UDP 载荷小于 18 字节时整帧就不够 60 字节很多交换机直接丢弃不报错。解决发送引擎里加补齐状态帧总长不足 60 字节时补 0 到 60但 UDP 长度字段仍然填真实长度。注意补齐的 0 不参与 UDP 校验和计算——如果做了校验和的话。5.4 IP 首部校验和报错PC 直接丢包现象抓包工具提示 header checksum incorrectPC 侧 ping 都不通但 ARP 是通的。原因一是算法漏了「折叠进位」这一步只做了一次 32 位加法就取低 16 位二是字节序搞反Verilog 里按 16 位拼接时把高低字节写反了网络字节序是大端。解决按 4.2 节的函数写两次折叠之后取反拼接时记住第一个字节在高位比如首部的45 00要拼成16h4500。校验和字段本身在计算时必须先填 0算完再写回去顺序不能颠倒。5.5 第二个包开始头部错位现象第一帧正常从第二帧起目的 MAC 和 IP 全乱或者载荷里混进了上一帧的尾部。原因帧边界没对齐。常见的是发送状态机在 S_IFG 结束后没有复位字节计数器或者接收侧 FIFO 里残留了上一帧的半个包用户侧读的时候没按 sop/eop 对齐把两帧拼在一起。解决所有字节计数器在进入 S_PRE 和 S_IDLE 时强制清零FIFO 写入时用 sop/eop 打标记读出侧看到 eop 就强制结束当前包宁可丢数据也不能越界。另外 FIFO 的读写指针在 link 切换时必须一起复位只复位一侧等于没复位。6. 验收与答辩三层测试把「能通」变成「可复现的能通」验收我固定分三层从下往上每层不过不往下走。第一层是内部环回在 MAC 发送和接收之间做数字环回不经过 PHY灌一组递增数据比对收发是否一致这一层验证的是组包、CRC、状态机本身跟硬件无关仿真里就能跑完。第二层是 PHY 环回把 PHY 配成内部环回模式数据从 FPGA 发出、经 PHY 绕回来这一层验证 GMII/MII 时序和时钟是否正确尤其是百兆模式下 4 bit 半字节的采样沿。第三层是外部打流两块板子对接或者板子接 PC用 ping 验证 ARP 和 ICMP 通路再用打流工具压满带宽同时开着抓包软件看有没有 CRC 错、有没有乱序。指标测试方法合格线链路自适应分别在千兆、百兆交换机下插拔 5 次每次 3 秒内恢复收发无残留错帧吞吐打流工具发 1400 字节 UDP 包持续 60 秒千兆 ≥ 900 Mbps百兆 ≥ 90 Mbps丢包率同上统计收发计数差连续 60 秒丢包数 ≤ 10帧完整性抓包软件过滤 CRC 错误错误帧数为 0最小帧发 8 字节载荷的心跳包对端可正常接收答辩演示我一般准备两条脚本一条 ping 一条打流现场切换交换机端口做速率演示。ping 用指定包长和次数别用默认无限 ping现场手忙脚乱容易忘停# 先通 ARP发 4 个 64 字节包2 秒超时 ping -n 4 -l 36 -w 2000 192.168.1.10 # 再压吞吐UDP 模式、1400 字节、跑 30 秒观察两端计数 iperf -c 192.168.1.10 -u -b 900M -l 1400 -t 30讲的时候重点讲三张图自协商状态机的波形能看到 link 上升沿和速率锁存、发送状态机从 S_PRE 到 S_IFG 的完整跳转、以及抓包软件里 ARP 请求应答加 UDP 数据的三帧时序。资源占用讲一张表就够LUT、FF、BRAM、以及最高可达时钟频率百兆和千兆各报一组。有老师会问「百兆下为什么不用 GMII 的 25 MHz 8 bit 模式」这时候能答出「取决于 PHY 支不支持逻辑上两种都要兼容」就说明真的动手调过。还有个小技巧提前把两种速率下的抓包文件各存一份现场万一网络环境出问题直接放录好的包讲时序比现场救火体面得多。从那以后我每次交板子前都强制走一遍「拔插五次 双速率打流」的流程哪怕需求里只写了千兆——因为你永远不知道对面那台交换机是多少年前的货色。希望帮到你。本文还有配套的精品资源点击获取