发布时间:2026/9/8 9:17:40
FPGA实现UDP协议栈:从MAC帧到RGMII的完整设计指南 上一节把时钟和复位收拾干净之后我盯着板上那颗PHY芯片看了很久。UART调通了数据也能从串口收发可那东西的速率实在让人着急——115200bps传一张图要按分钟算。于是这个系列到第8篇终于要碰通信里最实用也最容易劝退的一块给FPGA写一个UDP模块。这个模块做完板子能直接跟Wireshark、网络调试助手打交道开发机不用再插USB转串口吞吐量也能拉到几十MB/s。文章适合已经会写状态机、能看懂常见时序但不知道以太网帧在RTL里怎么落地的朋友。下面前面的坑我已经替你踩过一遍代码逻辑怎么设计、为什么这么设计都会说清楚。1. 动手前先搞清楚的几个关系UDP不是最底层MAC层才是1.1 我们要写的是一个“简化UDP协议栈”很多新手一搜“FPGA UDP”看到的工程都是udp_rx、udp_tx两个模块拿来就用。移植到自己板子上之后发现不是收不到就是发出去Wireshark里显示乱码。原因很简单每个工程的PHY芯片、引脚约束、时钟频率、上位机工具都不一样而UDP从来不是一个单独层它下面还压着IP层、MAC层和物理层。在FPGA里没有操作系统帮你处理网络栈所有字段都要自己拼。好消息是我们日常UDP通信只需要处理三层内容以太网MAC帧头目的MAC、源MAC、EtherTypeIPv4首部20字节里面最关键的是总长度、源IP、目的IP、协议号UDP首部8字节源端口、目的端口、UDP长度、校验和TCP、ICMP、ARP不是必须的。尤其是ARP很多人把UDP调通了才发现PC发不过来因为PC要先发ARP请求找MAC地址FPGA不回答就没后续。这个我放在后面单独讲先记住这一篇的“UDP模块”本质是一个简化MAC 简化IP UDP收发的组合。1.2 帧结构里每个偏移量都算清楚写RTL之前最好把整个以太网帧按字节偏移写出来。我踩过的最大坑就是IP头的偏移算错一位结果Wireshark里整个包都被识别成垃圾数据。这里以一个不带头前导码的MAC帧为例偏移从目的MAC的第一个字节开始帧内偏移长度内容06目的MAC66源MAC122EtherTypeIPv4为0x08001420IPv4首部348UDP首部42NUDP数据42N若干填充字节保证以太网负载不低于46字节最后44FCSCRC32IPv4首部内部偏移一定不能错字节0版本号 IHL正常是0x45字节1DSCP/ECN通常为0字节2~3IP总长度等于20 8 UDP数据长度字节4~5Identification字节6~7Flags和Fragment Offset字节8TTL通常填64字节9ProtocolUDP是170x11字节10~11IP首部校验和字节12~15源IP字节16~19目的IPUDP首部从帧偏移34开始偏移0~1源端口偏移2~3目的端口偏移4~5UDP长度等于8 UDP数据长度偏移6~7UDP校验和这里有个容易误解的地方以太网负载最小值是46字节不是IP头UDP头数据随便多短都行。如果你的UDP数据长度小于18字节IP总长度就是28payload低于46MAC层必须补0到46字节否则PHY不一定帮你发出去PC网卡也可能直接丢帧。填充分配在UDP数据之后而且不参与IP和UDP校验和只参与FCS计算。1.3 100M/1000M的选择决定代码骨架这一篇我按1000M RGMII来写。UDP要的是低延迟和吞吐百兆虽然也能跑但玩起来总觉得差口气。千兆RGMII下发送时钟是125MHz4bit数据线在上下沿各采一次等效8bit/cycle。接口上你看到的是rgmii_txd[3:0]和rgmii_tx_ctl跟内部的8bit数据总线不是一回事。新手最容易不理解的是内部状态机跑125MHz的8bit数据到了引脚变成DDR的4bit。所以RTL里通常需要一个gmii_to_rgmii的转换层用OSERDES/ISERDES把数据打出去。这个转换层其实不属于UDP协议逻辑但缺了它你的状态机写得再完美板上也不通。2. 发送通路一段状态机把帧头、数据、填充、CRC全部串起来2.1 为什么先拼一个42字节的帧头缓存发送通路的核心任务是把用户给的UDP数据包前面加上42字节的帧头14字节MAC头 20字节IP头 8字节UDP头后面加上填充和4字节FCS然后按字节送出去。实现方式有两种。一种是每个周期都在状态机里判断当前是第几个字节临时算IP头、UDP头该输出什么。另一种是先把42个字节的帧头算好、存到一个RAM或寄存器数组里发送时按下标读出来。我强烈建议新手用第二种因为本质上你要发送的帧头大部分是常量只有IP总长度、Identification、UDP长度、IP校验和这几个地方会变。在Verilog里可以定义一个8bit宽的RAMparameter DST_MAC 48h00_11_22_33_44_55; parameter SRC_MAC 48h00_01_02_03_04_05; parameter SRC_IP 32hC0_A8_01_10; parameter DST_IP 32hC0_A8_01_64; reg [7:0] hdr_mem [0:41]; wire [15:0] udp_len 16d8 user_pkt_len; wire [15:0] ip_len 16d20 udp_len;配置好参数后在初始块里填帧头也可以在每个包开始前根据当前长度刷新动态字段。hdr_mem[0]到hdr_mem[13]是MAC层hdr_mem[14]到hdr_mem[33]是IP层hdr_mem[34]到hdr_mem[41]是UDP层。发送的时候状态机只需要一个计数器hdr_cnt从0走到41然后输出hdr_mem[hdr_cnt]逻辑非常清晰。2.2 发送状态机的状态划分与Verilog骨架发送状态机我习惯分成这么几个状态localparam [3:0] IDLE 4d0, PREAM 4d1, // 7个0x55 SFD 4d2, // 1个0xD5 HEADER 4d3, // 42字节帧头 PAYLOAD 4d4, // 用户数据 PAD 4d5, // 填充到46字节负载 FCS 4d6, // 4字节CRC DONE 4d7;每次用户发起一次发送并且发送FIFO里有数据时才从IDLE进入PREAM。注意PREAM要发7个0x55不是一两个就完事。有些PHY比较宽容但按标准写永远不会错PREAM: begin eth_tx_tvalid 1b1; eth_tx_tdata 8h55; if (cnt 6) begin state SFD; cnt 4d0; end else begin cnt cnt 1b1; end end SFD: begin eth_tx_tdata 8hd5; state HEADER; cnt 4d0; end HEADER: begin eth_tx_tdata hdr_mem[cnt]; if (cnt 41) begin state PAYLOAD; cnt 4d0; end else begin cnt cnt 1b1; end end到了PAYLOAD状态数据来源从hdr_mem切到FIFO输出。要留好tlast信号FIFO最后一个字节出来时判断是否需要进入PAD。PAYLOAD: begin if (axis_fifo_dout_valid) begin eth_tx_tdata axis_fifo_dout; if (axis_fifo_dout_last) begin if (pad_len 0) begin state PAD; cnt 4d0; end else begin state FCS; cnt 4d0; end end end end这个骨架里最关键的是每个状态下cnt的增量和状态跳转条件要对齐。很多时序问题不是状态机逻辑错而是某状态下多等了一个周期导致整个帧偏移一位。仿真的时候最好把eth_tx_tdata按ASCII或hex逐字节打印出来跟Wireshark里看到的标准帧头比对。2.3 长度、填充和CRC这三位最难伺候长度字段一定要算对。IP总长度是20字节IP头 8字节UDP头 用户数据长度不是只加UDP负载。UDP长度是8字节UDP头 用户数据长度。这两个字段我一开始经常搞混导致Wireshark里出现“length field invalid”的提示。填充逻辑也不复杂wire [7:0] pad_len (ip_len 16d46) ? 8d0 : (8d46 - ip_len[7:0]);如果IP总长度小于46需要在UDP数据之后补0。补0的过程就是eth_tx_tdata 8h00计数到pad_len-1再切到FCS。最后是FCS也就是CRC32。这个不要自己拍脑袋写IEEE802.3的CRC32要求多项式0x04C11DB7初值0xFFFFFFFF结果异或0xFFFFFFFF且进入计算时按位LSB-first。很多开源代码能直接用但建议先在仿真里验证一个固定数据包的CRC值再用在线工具或Wireshark对照。这里有个非常容易出错的点CRC计算范围是目的MAC一直到最后填充字节不包括前导码和SFD。如果你的状态机把0x55也送进CRC计算器帧就废了。2.4 发送侧FIFO和用户接口用户逻辑和发送状态机之间最好放一个FIFO。原因是用户逻辑可能在一个很高的时钟域里产生数据而UDP状态机可能因为PHY忙、FIFO满而暂停。最方便的是用标准AXI-Stream接口s_axis_tdatas_axis_tvalids_axis_treadys_axis_tlast这个接口对新手很友好因为你只需要关心tvalid和tready同时为高的周期才算有效发送。tlast表示这是当前UDP包的最后一个字节。如果你的用户数据会产生很长的流或者要和别的模块拼接这个接口比自定义握手省心很多。发送侧FIFO的深度取决于用户逻辑的突发长度。如果只是回环测试16深度就够如果要真正传图像至少要放到1K以上。FIFO读写时钟不同域时用异步FIFO不要想着用寄存器搬运。3. 接收通路解析、过滤、把UDP净荷交给用户逻辑3.1 接收状态机怎么判断“这是一个合法UDP包”接收端是顺着帧头往下解析和发送端完全反向。RGMII收到数据后rx_ctl拉高表示有效数据到来。有效数据里先是一串前导码0x55然后是SFD字节0xD5。我习惯在状态机里先扫SFD扫到0xD5之后再开始按帧偏移计数。解析出的字段主要用来做四个判断目的MAC是不是本机MAC或广播地址FF:FF:FF:FF:FF:FFEtherType是不是0x0800IPv4首部的协议号是不是17UDP目的IP是不是本机IP是否允许广播UDP目的端口是不是本模块要接收的端口为什么不要求检查源IP因为UDP本身无连接在局域网调试阶段我们默认信任这个网络。如果你做的是更严肃的设备源IP白名单应该放在上层用户逻辑里而不是堆在MAC层。3.2 报头解析用字节计数别用移位拼接接收状态机里最容易出问题的是把字段拼反。以太网是大端传输比如帧偏移12的EtherType先来0x08后来0x00拼成16bit是0x0800。如果你用移位拼接一定要写成ethertype {ethertype[7:0], rxd};而不是ethertype {rxd, ethertype[7:0]}。我更推荐直接按字节计数把每个字段用case语句填进去一眼就能对应帧结构表格case (hdr_cnt) 0: dst_mac[47:40] rxd; 1: dst_mac[39:32] rxd; ... 12: ethertype[15:8] rxd; 13: ethertype[7:0] rxd; 23: ip_protocol rxd; 36: udp_dst_port[15:8] rxd; 37: udp_dst_port[7:0] rxd; endcase这样写虽然看着啰嗦但调试的时候可以直接在波形里看每个字段不用去算左移右移有没有搞反。等到熟悉了整个流程再优化成循环或者打包赋值。3.3 RX侧的错误标记和FCS到底查不查接收端的FCS要不要严格检查是一个工程取舍。PC的协议栈收到CRC错误的帧会直接丢弃但FPGA里如果你已经把数据实时流出去了遇到最后一个字节才发现FCS错误前面的数据早就进了用户逻辑。所以我的做法是保留一个rx_crc_err标记在帧结束时输出给用户逻辑让上层决定丢弃还是容忍。对很多实时视频、采集类应用帧率比错误率重要丢一帧还不如带病处理一帧。当然如果是做TCP/IP转发这类严谨设备FCS必须查而且最好用完整帧缓冲校验通过后再释放给上层。接收状态机还要加一个超时看门狗。有些PHY在链路抖动时会拉高rx_ctl一小段无效数据如果状态机卡在某个中间状态就永远等不到下一个合法包。我的做法是rx_ctl拉低时无条件回到IDLE再配合一个帧长上限计数器超过上限就报告错误帧。4. 代码之外的两座大山RGMII引脚时序和PHY复位4.1 先分清TXC/RXC两个时钟域发送通路用的是FPGA给PHY的125MHz时钟接收通路用的是PHY从线路上恢复出来的时钟。这两个时钟虽然频率相同但来源不同存在相位和频率漂移所以必须当成异步时钟域处理。很多人直接把rx_clk接到MMCM的输入又异步复位丢包丢到怀疑人生。正确思路是发送逻辑全部跑在clk_tx125MHz域接收逻辑全部跑在clk_rx125MHz域两者之间用异步FIFO交换数据。4.2 RGMII的DDR采样和IDELAYRGMII的4根数据线是DDR采样上升沿采低4bit下降沿采高4bit。FPGA里最稳的做法是用OSERDESE2发送、ISERDESE2接收不要自己在always块里写if (posedge clk || negedge clk)这种语句综合器根本没法保证时序。接收方向经常遇到的坑是数据中心的相位不对。RGMII标准从v1.3到v2.0对时钟和数据的延迟关系有变化。有些PHY内部已经做了延迟有些没有导致ISERDES采到的数据老是差半个周期。解决办法是在RGMII输入链路上加IDELAYE2并通过约束或扫描调整tap值。这个调试我没法给你一个固定参数因为每块板子布线长度不一样。比较实用的方法是在测试模式下发一个固定的0x12345678字节序列不断调整IDELAY直到上位机能稳定收到正确数据。4.3 PHY复位与自协商PHY芯片的复位时序不能瞎来。以常见的RTL8211系列为例reset_n拉低后要保持至少10ms再拉高然后等内部PLL稳定。如果复位时间不够MDIO可能读不出寄存器RGMII链接状态也不对。如果你用的是高云、安路、Xilinx不同板子PHY型号和复位引脚可能不同最好先查原理图确认是不是低有效。自协商建议直接打开。千兆PHY默认通常支持自协商能自动匹配对端速率。如果为了固定速率和关闭EEE才需要通过MDIO写寄存器控制。MDIO的时序是管理接口和RGMII没关系晚点调通基础通信后再去碰它完全来得及。5. 联调实测从“一个字节都收不到”到“稳定跑满带宽”5.1 第一轮先做最小回环拿到代码不要一上来就接用户逻辑。先在FPGA内部做一个最简单的回环接收模块解析出来的UDP数据直接接到发送模块的输入FIFO目的端口固定成你要回发的端口。这样PC发一个包FPGA收到后再原样发回去如果Wireshark能看到连续的两个相同包说明UDP收发通路基本通了。回环测试还有个好处可以顺便验证线路上的字节序。先发一组0x00到0x0F回环回来如果顺序是乱的说明RGMII采样或者FIFO拼接有问题。这时候先不要动协议层去查引脚和时钟。5.2 Wireshark和UDP测试工具的配合PC端我推荐两样东西Wireshark抓包网络调试助手或者自己写的UDP工具发包收包。抓包用来分析工具用来模拟对端。先用网络调试助手给FPGA发一个固定内容比如00 01 02 03 04 05。FPGA收到后点亮一个LED证明接收通路有效。然后用FPGA周期发送UDP包到PCPC用调试助手绑定同一个目的端口收包。如果这一步通了再打开Wireshark看细节ip.checksum是否显示正确udp.length是否等于8 数据长度帧尾CRC是否报错Wireshark默认会校验IP和UDP。如果你的UDP校验和字段填0Wireshark会显示“not set”这是合法的不是错误。IP校验和必须正确否则包会被PC协议栈直接丢掉。5.3 常见问题排查表现象可能原因排查思路PC完全收不到包目的MAC、目的IP参数不对ARP没解决防火墙拦截先用广播目的MAC测试临时用arp -s静态绑定关掉Windows防火墙FPGA收不到PC的包FPGA源/目的IP端口参数错PHY复位没完成RGMII约束错查PHY寄存器link状态抓rx_ctl和rxd看有没有数据收到但数据乱码字节序拼反、RGMII上下沿采错定点发0x12345678逐个调IDELAYWireshark报bad C/D帧头偏移错、IP总长度错、UDP长度错先用固定包在仿真里打印整帧字节回环丢包FIFO深度不够、用户逻辑反压、PC收包缓冲太小增大FIFO降低发包速率调大OS接收缓冲还有一个特别常见的现象UDP能通但ping不通。这不是Bug因为ping走的是ICMP协议我们不写ICMP就不会回。这个在局域网里不影响UDP业务但很多人测试时会误以为网络不通。6. 这个模块到此能停但不是设计终点6.1 ARP可能是你最需要补的下一块如果你的FPGA要给PC发单播UDP目的MAC地址如果查不到PC的网卡MAC可以用广播MACFF:FF:FF:FF:FF:FF先跑通。但PC往FPGA发单播UDP前会先发ARP请求“谁有192.168.x.x这个IP”。如果FPGA不回ARPPC网卡驱动根本不知道目的MACUDP包永远不会到线上。临时调试可以用Windows的arp -s命令把IP和MAC静态绑定但项目里还是要实现一个最简单的ARP响应模块。ARP请求的EtherType是0x0806FPGA收到后判断目标IP是否等于自己的IP如果等于就把请求包里的源MAC当成目的MAC回一个ARP应答告诉对端自己的MAC。这个模块代码量不大但对工程完整性提升非常明显。6.2 包长限制、分片和校验和边界标准以太网MTU是1500字节IPv4头20字节UDP头8字节所以UDP数据最大是1472字节。超过这个值PC的操作系统会自动分片你FPGA端收到的可能是两个分片包。如果你没有实现碎片重组数据就会错乱甚至丢失。应用层控制包长比在FPGA里做分片重组省力得多建议上层协议直接把单包长度限制在1400字节左右留出余量。分片相关的字段在IPv4首部的Flags和Fragment Offset。我们这种简化模块不去处理分片但如果收到MF1或者Fragment offset ! 0的包应该在上层统计并丢弃而不是把半个UDP包交给用户逻辑。6.3 后面能往哪个方向扩展如果这个简化UDP模块已经稳定下一步可以有三个方向。第一是做TCP但TCP有连接状态、序号、超时重传、滑动窗口工程量不是UDP一个量级除非是高速网卡卸载场景否则不建议个人从头写。第二是做上位机配合比如用C#或Qt写一个UDP组播工具和FPGA做图像采集、传感器数据实时显示。第三是做协议栈拆分把MAC层、IP层、UDP层拆成独立模块以后接PCIE或者DMA总线的时候头部处理和业务数据能分开处理。这一篇我建议你从发送通路开始写先把一个写死长度的包发出去在Wireshark里看到字节能对齐再回头做接收。我在这一版里犯过最蠢的错就是把UDP长度字段和IP长度字段搞混结果Wireshark里全是length field invalid。你看到第8篇才碰到UDP说明前面基础已经很扎实了大概率会比我快。

相关新闻

2026/9/8 9:17:40

基于SpringBoot的减肥健康管理系统源码+文档+讲解视频

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/9/8 9:17:40

只会CANoe做不了HiL测试?2026年硬件在环进阶路线解析

前几天一个做台架测试的朋友跟我说,他CANoe已经能玩出花来——报文解析、Trace过滤、CAPL脚本、Panel面板都熟,准备明年去谈HiL测试岗,问我把握大不大。我反问他:HiL台架上那个能模拟整车物理信号、跑实时模型、夹故障注入的盒子&…

2026/9/8 9:17:40

基于SpringBoot的在线宠物商店系统源码+文档+讲解视频

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/9/8 12:53:19

SWTChart实战:SWT桌面应用中实时曲线图的高效集成方案

简介:SWTChart是一款基于SWT的Java图表类库,面向需要在不依赖重型框架的桌面应用或Eclipse RCP项目中集成图表的开发者,胜在轻巧且接入方便。它覆盖线图、散点图、堆栈图、柱状图、面积图、步骤图,以及对数标度、分类轴、多轴、轴…

2026/9/8 12:53:19

Spring Boot美发商城系统实战:从数据库设计到部署上线

看到这个题目,我就知道这八成是又一位要做毕设、或者想系统巩固 Spring Boot 技术栈的同学找上门了。“基于 Spring Boot 的美发商城系统”听上去常见,但真正动手做起来,踩坑的地方一点都不比普通商城少。美发这门生意跟卖衣服、卖数码完全不…

2026/9/8 12:48:19

JavaScript定时器:setTimeout与setInterval核心原理与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/8 7:15:10

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/8 7:15:10

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/8 0:01:49

踩多轮坑才跑通|OpenClaw 3.1.0 双平台本地 AI 自动化搭建实操实录

🔹 工具简述 OpenClaw 是一款备受开发者与办公人群青睐的开源本地智能工具,凭借离线本地运行、可视化图形面板、全流程自主任务处理三大核心特点,积累了众多忠实用户。与普通对话类 AI 产品不同,它能够直接调用电脑的软硬件操作权…

2026/9/8 0:01:50

拒绝复杂命令行,Hermes Agent 一键包快速解锁智能办公能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…