FPGA零基础实现以太网数据链路层与UDP通信协议栈 从近似0基础开始FPGA开发 -- part.9 数据链路层代码设计与UDP通信测试一直在慢慢填FPGA学习的坑这一篇是很多人催更的“网络篇”。之前几篇把时序约束、FIFO、串口、DDR这些基础模块都过了一遍这次干脆来点实战性更强的——在FPGA上撸一个简易的以太网数据链路层再往上叠一个UDP协议栈最后连线到电脑上,用网络调试助手真正把数据收发跑通。先说清楚这篇做完你能得到什么一块开发板插上网线连电脑电脑端软件能收到FPGA发来的UDP数据包同时FPGA也能解析电脑发给它的UDP数据并产生动作。整个过程不依赖任何付费IP核完全用Verilog手写实现对理解网络协议栈底层的运作机制特别有帮助。适合什么样的读者会Verilog基础语法知道怎么建工程、写testbench但还没碰过以太网的人。如果你对MAC、PHY、MII这些名词还是一头雾水这篇正好带你从零把它啃下来。1. 内容整体设计与思路拆解1.1 为什么在FPGA里自己写UDP而不是用现成IP核先说动机。很多FPGA开发者在遇到以太网需求时第一反应是打开Vivado或Quartus里的Tri-Mode Ethernet MAC IP核。这个方案成熟稳定但有个问题:IP核把底层细节全封装死了出问题你根本不知道往哪儿查而且授权、时序约束、跨时钟域处理这些对新手来说都是坑。我在做的一个高速数据采集项目里需要把FPGA从ADC采到的数据以超过100MB/s的速率传给上位机。试过用IP核方案配置繁琐不说调试时看到的是黑盒子内部行为,根本没法定位问题。后来干脆自己写数据链路层整个收发链路完全可控坑踩完反而比调IP核快。自己写还有个好处你能真正理解一帧以太网数据是怎么从应用层数据变成物理线上的电平信号的。这个过程对后续做RGMII调试、千兆网、甚至MAC层定制功能比如帧过滤、时间戳插入都是必经之路。1.2 数据链路层在FPGA网络里到底管什么很多人刚开始容易迷糊FPGA连网口的完整链路到底是什么关系我用一句话理清OSI七层模型里FPGA内部的用户逻辑通常负责网络层和传输层IP和UDP而MAC介质访问控制这一层既属于数据链路层又是FPGA里需要实际写的部分至于物理层就交给PHY芯片比如常用的RTL8211、KSZ9031去干。数据链路层在FPGA里的核心职责可以拆成两块。发送方向把上层传来的数据包封装成以太网帧加上前导码、帧起始定界符、MAC地址、类型字段和CRC校验接收方向检测线路上的帧起始、恢复数据、检查CRC、剥离MAC头再把有效载荷上报给上层处理。我这次用的开发板PHY芯片是RTL8211支持千兆工作在RGMII接口模式。如果你用的是百兆PHY比如LAN8720代码改动不大主要是数据位宽和时钟频率要重新算一下。1.3 方案选型RGMII、FIFO缓存和时钟域划分动手前先把架构定下来避免写到一半推倒重来。我最终采用的是这个结构发送路径用户逻辑把待发送数据写入发送FIFO发送控制状态机读出数据按序拼装前导码、MAC头、IP头、UDP头、载荷和CRC以RGMII时序送给PHY芯片。接收路径PHY芯片恢复的RGMII信号先做时钟域同步和数据对齐再经过接收控制状态机完成帧解析和CRC校验有效载荷写入接收FIFO供用户逻辑读取。跨时钟域处理FPGA逻辑工作在125MHz千兆RGMII需要用户逻辑和其他模块工作在各自时钟域通过异步FIFO连接。这个设计最核心的一点就是把“协议处理”和“数据缓存”拆开。协议状态机只关心字节流的拼装和解析数据缓存只负责跨时钟域Buffering互不干扰调试的时候也容易定位问题。2. 核心细节解析与实操要点2.1 以太网帧格式必须倒背如流数据链路层的核心是帧格式我在编码时把所有长度和偏移量的定义放到了宏定义头文件里方便到处引用。标准以太网帧长这样前导码7个字节的0x55用于物理层时钟同步帧起始定界符SFD1个字节的0xD5目的MAC地址6字节源MAC地址6字节类型/长度字段2字节IPv4协议对应0x0800载荷至少46字节最多1500字节帧校验序列FCS4字节CRC32校验范围从目的MAC地址到载荷末尾。前导码和SFD是PHY芯片在RGMII模式下自动帮你加/减的FPGA侧不需要处理但我的代码里还是保留了生成逻辑方便以后换PHY或改用SGMII时可以直接复用。真正需要FPGA逻辑处理的从目的MAC地址开始。CRC32的生成多项式是0x04C11DB7初始值为0xFFFFFFFF结果要取反。这个坑我踩过后面会详细说。2.2 发送控制状态机设计发送状态机是整个发送链路的指挥中心。我的状态机分7个状态IDLE、SEND_PREAMBLE、SEND_MAC、SEND_IP、SEND_UDP、SEND_PAYLOAD、SEND_CRC。从IDLE状态收到发送请求后开始组帧。SEND_MAC状态输出6字节目的MAC和6字节源MAC接着输出2字节以太网类型然后进入IP层组装流程。这里有个细节MAC地址在发送时是低位先出还是高位先出以太网规定字节内的bit先发低位LSB first但我们在FPGA里通常处理的是完整字节数据所以只在PHY侧的RGMII接口做bit顺序调整就够了状态机内部不需要关心bit级序。控制发送节奏的核心是“忙信号”。当发送FIFO里没有足够数据时状态机必须暂停在SEND_PAYLOAD状态拉高busy信号直到数据凑齐再继续。如果数据量不够46字节还要自动填充0到46字节长度。这个填充逻辑很多人会忘结果导致抓包时出现“runt frame”错误包。2.3 接收控制状态机设计接收方向比发送复杂因为发送是你自己控制节奏接收则是外来数据什么时间到什么不确定。接收状态机从RGMII接口检测到SFD后启动解析状态依次为IDLE、RX_MAC、RX_IP、RX_UDP、RX_PAYLOAD、RX_CRC。比较棘手的是接收FIFO的写入时机。以太网是流式协议你不知道一帧数据何时结束只能靠CRC位置判断。所以我在接收状态机里维护一个帧长度计数器每收到一个字节加1同时把数据写入FIFO当CRC字段检测到边界时停止写入并拉高帧结束信号。如果校验发现CRC错误怎么办标准的做法是把这一帧数据丢弃。但接收FIFO里可能已经写入了数百字节所以设计时要在FIFO里加“回滚”能力——要么用读指针恢复的快照机制要么干脆把坏帧直接跳过不读。我用的是后者在CRC结果出来之前不真正交付数据给用户逻辑而是把数据先暂存在一个“预交付FIFO”里校验通过后再搬移到用户可读的正式FIFO。代价是多了约1500字节的BRAM但逻辑大大简化。2.4 CRC32计算的Buffer实现CRC32在以太网里是必须自己动手的部分。最常用的是查表法或逐bit计算法。查表法适合以字节为单位处理的场景效率高我实测在125MHz下每一拍算一个字节完全没问题。CRC32计算逻辑理解不难把数据字节逐位异或到CRC寄存器的最高位如果移出的位是1就跟多项式0x04C11DB7进行异或。实际代码里一般用“左移版本”实现也就是CRC寄存器初始化为0xFFFFFFFF每个字节先跟寄存器高8位异或然后做8次移位和多项式异或。容易出错的两个地方第一对载荷数据和MAC头/类型字段的校验范围要包含完整从目的MAC到载荷结束但前导码和SFD不算第二以太网要求最后对CRC结果取反再按字节逆序发送。如果直接拿标准CRC32代码算完就发出去抓包软件会报“incorrect FCS”错误。我在代码里加了一个自检信号crc_error_count在testbench里可以连续发几十帧数据观察是否有任何一帧CRC校验失败。这个方法帮我抓出了一个时钟域没对齐导致的偶发错位问题建议你也这样做。2.5 UDP校验和的计算方法UDP协议头里有一个可选校验和字段IPv4下可以设为0表示不使用。但为了严谨和兼容性我在设计里把它算上了。UDP校验和是一个16位反码和校验范围包括伪头部源IP、目的IP、协议号、UDP长度加上UDP头部和数据。计算方法是把所有16位字累加溢出回卷最后取反码。在FPGA里实现的方式很直白用一个加法器和一个寄存器按16位逐字累加处理完所有数据后回卷一次取反。我在发送端计算校验和的时机是载荷数据全部就绪之后在发送FIFO读出数据的同时联算发送完自动把校验值填入UDP头部的校验和字段。接收端的校验逻辑大同小异把所有字段聚合后算完看是否为0xFFFF不为0就是校验失败。3. UDP协议栈与MAC层的协同实现3.1 从上层接口到MAC帧的数据组装流程实际写代码时我会把整个设计分成三个层级用户接口层、UDP/IP协议层、MAC帧封装层。用户接口层提供给上级应用的接口非常简单——一组写数据信号、地址信号和写请求信号外加一个发送完成中断。用户应用只需要关心“往哪个IP的哪个端口发数据”其余拼装动作全部由协议层自动完成。UDP/IP协议层拿到用户数据后自动填入IP头部和UDP头部。IP头部的版本号固定为4头部长度固定为5即20字节总长度字段是20字节IP头 8字节UDP头 载荷长度的和。UDP头部的源端口号和目的端口号由用户寄存器配置长度字段是8字节头加上载荷长度。MAC帧封装层则负责把整个IP包当作载荷包进以太网帧里加上MAC地址和类型字段最后计算CRC。这三级拆分的最大好处是每一层都可以独立测试。我当时的调试顺序是先测MAC层用testbench直接喂一个构造好的以太网帧看CRC是否匹配然后测UDP/IP层检查IP头各字段是否正确最后才把三层串起来联调。3.2 IP层和UDP层的头部字段填充逻辑IP头部的填充逻辑有几个要注意的点。版本号和头部长度固定写死服务类型TOS一般填0标识字段ID每发一帧加1标志和片偏移都填0生存时间TTL填64协议号填17表示UDP头部校验和单独计算。IP头部校验和计算方式跟UDP校验和类似但只覆盖20字节的IP头本身不涉及载荷。因为IP头在校验和计算完之后不能再改动否则校验值就失效了。这个顺序问题在代码里要特别小心——先算出校验和填进头部再启动发送流程确保不会再修改头部任何字段。UDP头部的填充相对简单源端口和目的端口各16位长度字段指UDP头加数据的总长度校验和按前面说的方式计算。UDP校验和计算时源IP、目的IP等信息要提前准备好因为这些在IP头里但计算时又需要所以最好把IP地址配置寄存器也暴露给UDP计算逻辑。3.3 收发FIFO深度与宽度的选型考量FIFO的参数直接决定了系统能跑多快、能缓存多少帧。我这里用的是Xilinx和Altera都通用的标准异步FIFO宽度设为8bit因为RGMII是字节流接口深度选2048刚好能缓存最大以太网帧1518字节还有余量。深度选2048还有一个考虑很多时候你要做的不只是转发一帧而是要支持背靠背连续收发。比如上位机一次性下发10帧数据如果每帧之间间隔很小接收FIFO必须能暂存至少一帧半的数据才能保证不丢。2048深度实测在背靠背接收时稳定不溢出。如果你要做大流量吞吐建议把发送FIFO做成“双帧缓冲”也就是深度翻倍并且增加“当前帧完整写入再开始发送”的乒乓操作。否则会出现发送状态机读FIFO时数据还没写完的尴尬局面。3.4 ARP请求的处理策略纯UDP回环测试时很多人会忽略ARP结果电脑端总是收不到FPGA的响应。原因很简单电脑要发UDP包给FPGA但不知道FPGA的MAC地址于是会先发一个ARP广播请求“谁的IP是192.168.1.10告诉我你的MAC地址”。FPGA如果不回应ARP电脑根本不会把UDP包发出来。所以我还实现了一个精简ARP响应模块。它的逻辑不复杂监听接收链路上的帧判断是否为ARP请求且请求的IP地址等于FPGA自己的IP地址如果是就自动构造一个ARP应答包发回去。应答包里填上FPGA的MAC地址。如果不做ARP响应也可以退而求其次在电脑端用命令行静态绑定ARP表项。但项目要给别人用就不可能让人手动绑所以ARP自动响应是一次到位、必要的。我单独写了一个测试项验证ARP响应用电脑ping FPGA的IP能通说明ARP没问题。4. UDP通信测试的设备连接与完整实测4.1 硬件连接和开发环境准备测试环境我列个清单你照着准备就行FPGA开发板一块板载RTL8211 PHY芯片我用的是黑金AX515但代码是通用RGMII接口其他板子也能移植一台电脑千兆网口装好网络调试助手我用的是NetAssist和Wireshark网线一根电脑直接连开发板的RJ45口开发工具Quartus或Vivado都行我的代码纯Verilog不依赖器件。连好线以后用网络调试助手先设置好本机端口比如监听在9000端口。然后配置FPGA端的寄存器目标IP设为电脑IP目标端口设为调试助手的监听端口源端口设为9001。同时在电脑上给本地网卡配一个静态IP跟FPGA在同一个网段比如FPGA是192.168.1.10电脑是192.168.1.20。4.2 上板测试的三种基本联调方法第一个测试回环测试。把FPGA收到的UDP数据直接原样发回电脑。这个测试的逻辑等于“先收后发”只要电脑能收到自己发出的数据说明接收链路和发送链路的前半段都是通的。我第一次跑通时对比了发和收的数据完全一致瞬间心里踏实了。第二个测试主动发送测试。在FPGA里写一个简单的计数器每秒钟通过UDP向电脑发送一帧数据内容包含计数值和时间戳。电脑端网络调试助手如果能持续收到递增的数据并且字节数正确说明主动发送链路没问题。第三个测试命令响应测试。电脑端发送特定格式的命令帧FPGA解析后回传状态信息。这个用来验证接收解析的准确性和协议栈的完整性。前两个测试适合验证链路通断第三个测试更接近真实项目需求。我的建议是先把前两个跑通再上第三个这样出问题时排查范围更小。4.3 Wireshark抓包检查的几个关键点接线、配置都正常网络调试助手却收不到数据这种时候别急着怀疑代码先拿Wireshark看一眼线路上到底有什么。Wireshark里设置过滤器为udp重点看这几项。第一是源MAC地址是否为FPGA的MAC如果不是说明MAC头拼错了第二是IP头部的总长度是否符合实际数据长度如果偏大或偏小多半是长度字段计算错误第三是UDP目的端口是否跟网络调试助手的监听端口一致第四是IP头部校验和和UDP校验和是否显示有效。我自己在测试时碰到过一个情况Wireshark里能看到FPGA发出的包但网络调试助手收不到。后来发现是因为Wireshark开启了混杂模式能看到所有包而网络调试助手所在的Windows防火墙把入站UDP给拦截了。解决办法是给对应端口加防火墙放行规则。这个坑特典型纯软件层面的问题跟FPGA一点关系没有容易被忽略。4.4 连续长时间打流的稳定性验证单帧收发通了接下来说说连续打流。我用iperf3工具从电脑向FPGA发送UDP流带宽从100Mbps逐步提高到600Mbps观察FPGA的接收FIFO是否溢出以及CRC错误计数是否增长。这个测试很重要因为单帧调试时很多时序问题不会暴露只有高速连续流转起来像跨时钟域抖动、FIFO读写指针不同步、信号亚稳态这些问题才会浮出水面。我实测在500Mbps持续打流30分钟CRC错误计数为0FIFO从没满过这个结果算比较健康。如果你的板子在高速打流时CRC错误计数持续增长重点排查PHY芯片配置是否正确比如RGMII的TX/RX时钟相位偏移、数据线延时以及FPGA侧IO约束是否严格。这类问题用逻辑分析仪很难抓我建议在FPGA内部加一个计数器对接收到的总帧数和错误帧数分别统计通过串口或UDP把计数发出来排查效率会提高很多。5. 调试中的典型问题与排查思路5.1 CRC错误导致的丢帧问题现象Wireshark里能看到FPGA发出的帧但连续多帧后开始出现“bad FCS”提示网络调试助手收到的数据偶尔缺帧。排查过程我最初怀疑是CRC计算模块本身有bug于是回testbench单独跑CRC验证用标准的CRC32参考值比对结果完全一致。然后怀疑CRC校验范围不对又把所有字段的边界拉出来逐字节打印仍然找不到问题。最后发现是时钟问题发送状态下数据在125MHz时钟上升沿输出但RGMII接口对数据建立时间要求非常严格IO时序余量不足导致偶尔采样错误。解决办法是在RGMII输出路径上加了一级寄存器打拍延迟并检查了引脚约束里的IO delay设定。改了之后连续跑了12小时再没出现FCS错误。这个问题的教训是FPGA逻辑仿真是验证功能但时序问题是仿真看不到的务必检查时序报告特别是RGMII这种跟外部芯片接口相关的路径。5.2 电脑发的UDP包FPGA收不到问题现象电脑网络调试助手发送数据FPGA这边接收FIFO一直空逻辑分析仪看不到任何接收帧信号。排查过程第一反应是PHY芯片的配置有问题可能复位或MDIO配置没成功。于是先单独回读PHY芯片的寄存器确认链接状态寄存器显示已建立千兆链接。再看RGMII接口的RX信号发现RX_CLK有信号但RXD数据线电平异常一直为低。深入查下去发现是原理图设计的锅开发板的PHY芯片的RGMII接口跟FPGA引脚之间有一个引脚位序交换我们的代码按顺序直接映射导致数据位错乱。把引脚映射关系按原理图修正后接收立刻正常了。5.3 UDP校验和为0导致的部分设备不响应问题现象FPGA发给电脑的UDP包Wireshark显示checksum状态是0x0000电脑能收到但换到另一台设备上就收不到。排查思路UDP校验和为0表示校验和未计算这在IPv4协议里是合法的很多Linux系统默认不检查入站UDP校验和但某些嵌入式协议栈会直接丢弃校验和为0的包。处理办法把UDP校验和计算补上。我在发送端的UDP头填充逻辑里增加了校验和计算模块所有发出的UDP包都带有效校验和。补完之后在多个不同系统上测试均可正常通信。这里明确一点UDP校验和为0虽然合法但能做完整校验和就尽量做完整省去兼容性问题。5.4 MAC地址和IP地址配置错误导致的ARP不响应问题现象电脑ping不通FPGA的IPARP请求一直在重复发送但FPGA没有应答。排查过程用Wireshark抓包确认电脑确实在发ARP请求且请求的目标IP正确。然后检查FPGA的ARP响应模块发现它只匹配ARP请求的IP地址但没检查目标MAC地址是不是全F的广播地址。在局域网里这问题不大但某些情况下设备会向特定MAC地址发送ARP请求导致FPGA不响应。修复方法ARP响应模块的判断条件增加一项目的MAC必须是全F广播地址或者等于FPGA的MAC地址。之后ping通了UDP通信也就顺理成章地正常了。5.5 实用调试技巧速查现象可能原因排查动作Wireshark看不到FPGA发出的包PHY芯片没完成链接读PHY寄存器确认link状态抓包显示bad FCSCRC计算错误/IO时序余量不足先验证CRC模块再查时序报告和管脚延时电脑发数据FPGA收不到RGMII位序错乱/PHY配置异常对照原理图查引脚映射读PHY寄存器确认模式网络调试助手收不到包Windows防火墙拦截加端口放行规则ARP请求一直没响应判断条件不完整/源MAC错误抓包检查ARP应答是否发出、内容是否正确高速打流丢帧接收FIFO溢出/跨时钟域亚稳态加帧计数器和错误统计持续打流观察计数6. 整个项目做完之后的经验沉淀把数据链路层和UDP协议栈从零写一遍之后我对FPGA做网络通信这件事有了一些新的理解。说到底以太网协议并不复杂它的复杂度在于一是位级时序要和外部PHY芯片严格对齐二是多层的操作得在同一个状态机框架里和谐工作。这两点没有亲自动手做一遍光看文档是体会不到的。在设计层面我强烈建议三层分离架构。用户接口层只管收发数据UDP/IP层只管填充和解析头部MAC层只管组帧、CRC和PHY时序。这样分开以后每一层都可以独立做testbench验证出问题也能快速定位。一开始懒得分层把代码全写在一个大文件里后期光是查一个IP长度字段就翻了好几页代码效率极低。调试工具上的心得也分享一下Wireshark绝对是最强的网络协议调试工具没有之一。很多看似神秘的故障只要抓包看一眼立刻就能判断是链路问题还是协议问题。强烈建议把Wireshark的过滤器和着色规则学熟它对以太网帧头、IP各字段做了非常清晰的标注能有效节省排查时间。如果你也想把速度推到满速千兆或者在多端口、多FPGA互联的场景下继续扩展建议下一步研究这几个方向RGMII的IDELAY精确调优、多个MAC的共享缓存仲裁、以及把UDP功能扩展为TCP状态机。TCP比UDP复杂至少十倍需要处理序列号、窗口、重传和连接状态管理没有这套基础直接上TCP会很痛苦。最后分享一个我实际用小技巧在顶层模块里暴露一组“回环模式”选择信号调试时可以在FPGA内部任意切换数据路径——直接把PHY收到的数据交给发送链路发回或者走完整协议栈。这样能秒级区分是PHY链路问题还是协议栈问题对快速定位前几类故障非常有效。这个习惯我后来每个网络项目都保留下来了省下了不少跟硬件联调扯皮的时间。