十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA入门必学:手写UDP协议栈实现以太网通信

FPGA入门必学:手写UDP协议栈实现以太网通信 1. 写在前面为什么这个part值得认真做写FPGA入门系列文章到第9篇一路走来的朋友应该已经有感觉了前面的例程再花哨本质都是在“点灯”或者“跑跑状态机”离真正的通信还差一层窗户纸。到了数据链路层这块才算是第一次真正摸到“接口”和“协议”的门槛。我见过不少人板子买了、流水灯跑得溜但一说UDP就发怵总觉得那是软件工程师的活硬件这边顶多调调IP核。其实不是这样。UDP这种无连接的传输方式恰好是FPGA入门通信最好的切入口。它没有TCP那套复杂的连接管理、重传机制和滑动窗口你要处理的就三件事把应用数据打包成IP报文、加上MAC头和校验字段、然后按以太网帧的时序发出去。反过来收数据就是把这三层一层层拆开把负载抽出来交给下游逻辑。这条链路走完你以后去碰PCIe、MIPI、甚至自定义的高速串行协议思路都是相通的——无非是“字节流时序状态机校验”这套组合拳。这篇文章我会从数据链路层的职责入手把UDP协议栈拆成一个一个可实现的模块然后拿出正经能跑仿真的RTL代码讲清楚每个模块为什么这么写最后再带你上板实测用Wireshark和网络调试助手来验证收发。整个过程面向近0基础的读者Verilog基础有前几篇打底就够剩下的跟着思路走就行。2. 数据链路层到底在干什么2.1 用快递类比理解协议分层先说个容易绕进去的问题UDP和以太网是什么关系很多人一上来就盯着UDP端口号结果看到IP头、MAC地址就晕了。我用快递来打比方。你要寄一个包裹应用层你的FPGA逻辑只关心“这包数据要发给谁”所以只写清楚收件人和内容这就是UDP层它记录源端口、目的端口、长度和校验和。运输层快递公司内部会把你的包裹装进一个标准纸箱贴上运单运单上有源IP、目的IP、协议类型等这就是IP层。链路层快递员再把纸箱放进货车货车车门上写的是发货仓库和收货仓库的编号也就是MAC地址这就是以太网层。三层都有各自的“地址”缺一不可。所以你在FPGA里发一个UDP包实际发到网线上的是前导码8字节用于时钟同步目的MAC6字节 源MAC6字节 以太网类型2字节UDP/IP固定为0x0800IP头20字节UDP头8字节应用数据任意字节但整个帧长度最好≥46字节小于46字节要补0FCS校验4字节CRC32收包方向就是把上面这个顺序反过来一层一层剥洋葱。数据链路层这个名字容易让人误以为它只负责MAC地址和FCS其实在FPGA实现里我们通常把MAC层和IP/UDP解析打包全放在一起做因为对硬件来说它们本质上就是“字节流不同偏移位置的内容填充和提取”。2.2 FPGA实现数据链路层的三种思路确定方案之前先看看市面上常用的几种做法。直接用厂商IP核比如Xilinx的Tri-Mode Ethernet MAC或者Altera的TSE MAC。优点是不用自己写MAC层时序和校验都处理好了缺点是IP核配置复杂带许可证限制而且初学者对着IP核的接口文档大概率一头雾水出了问题不知道往哪查。参考开源IP比如Alex Forencich的Verilog Ethernet项目。代码成熟功能齐全支持1G/10G缺点是代码量非常大上万行的工程让新手完全没法下手而且很多设计针对高级功能做了抽象反而不适合学习。自己写一个精简版UDP协议栈。只支持10/100M以太网只支持IPv4和单播只支持UDP够用就行。这个方案非常适合入门代码量控制在几百行每个模块都能吃透出了问题自己能调试改起来也灵活。我选择第三种。原因很直接第一我们做的是学习项目目的是搞懂协议而不是跑通一个黑盒第二百兆以太网在板级调试时非常友好示波器和逻辑分析仪都能方便地观察第三后续如果你需要做千兆这个精简协议栈的框架依然可以复用无非是数据位宽从8bit变成32bit时钟从25MHz变成125MHz。2.3 明确我们要实现的目标功能这个part最终要交付的东西用一个网络调试助手就能验证FPGA上电后自动发送一串UDP报文到PC端指定IP和端口PC用网络调试助手能收到这些数据。PC通过网络调试助手向FPGA发送任意数据FPGA接收后能把payload提取出来驱动一个LED灯或数码管显示证明数据成功解析。用Wireshark抓包能观察到标准的ARP帧和UDP帧CRC校验无误。能做到这三点就说明数据链路层的发送通道、接收通道和校验逻辑都通过了验证。3. 发送通道的设计与实现3.1 模块划分别把鸡蛋都放一个篮子里发送通道我分成三个小模块来处理这是整个工程最关键的架构决策udp_tx最顶层的发送状态机负责从FIFO读出应用数据依次拼装UDP头、IP头、MAC头最后输出完整的以太网帧。crc32计算FCS校验和放在发送模块的底层接收方向也复用它。arp_request开机先发一个ARP请求把PC的MAC地址学到手才能填充目的MAC字段。为什么要单独分一个ARP模块因为如果你把目的MAC地址写死在代码里虽然也能发通但一旦PC的网卡换了MAC地址变了板子的收发就全部失效还得重新综合下载非常痛苦。用ARP动态获取的目的MAC让整个系统更像一个真正的网络设备。3.2 发送状态机的骨架Verilog下面这段是发送状态机的核心框架我简略掉了一些局部变量的赋值细节保留关键跳转逻辑方便对照状态图理解// udp_tx.v —— 发送通道顶层 localparam IDLE 4d0; localparam ARP_WAIT 4d1; // 等待ARP学习完成 localparam PREAMBLE 4d2; // 发前导码 localparam MAC_HEAD 4d3; // 发MAC头 localparam IP_HEAD 4d4; // 发IP头 localparam UDP_HEAD 4d5; // 发UDP头 localparam DATA_SEND 4d6; // 发送应用数据 localparam FCS_SEND 4d7; // 发CRC校验 localparam IFG_WAIT 4d8; // 帧间隔 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: begin if (ip_rx_valid tx_req_accepted) state PREAMBLE; end PREAMBLE: begin if (byte_cnt PREAMBLE_BYTES - 1) state MAC_HEAD; end // ... 后面每个头的发送与字节计数逻辑类似 DATA_SEND: begin if (fifo_empty send_done) state FCS_SEND; end FCS_SEND: begin if (byte_cnt FCS_BYTES - 1) state IFG_WAIT; end endcase end end关键是在每个状态里维护好一个byte_cnt计数器和tx_byte数据输出。前导码是8字节的0x55最后1字节是0xD5MAC头是66214个字节IP头20字节UDP头8字节。每个头的内容按偏移拼接到一个寄存器数组里然后用同一个byte_cnt驱动一个取字节的case表达式就能顺序输出。我在第一次写的时候踩了个坑IP头里的总长度字段total length和UDP头里的长度字段都与应用数据的字节数相关。如果不提前算好这个长度而是在发送过程中才去计算就会因为时序冲突导致头信息错误。解决办法是让上层在发起tx_req的同时把一个16位的payload_len信号一起传进来状态机直接用这个信号算长度然后体现在IP头和UDP头的字节拼接里。3.3 为什么CRC必须“先算后发”CRC32在以太网里是对整个帧从目的MAC开始到payload末尾做校验计算完的结果反码之后按字节序从低到高放到帧尾。这里要特别注意CRC32发送时序和CRC计算引擎之间存在一个“推进”关系。如果你等所有数据都发完了才开始计算CRC那么校验码就会晚一个时钟周期才有效跟正在发送的数据流对不上。正确的做法是从MAC头的第一个字节进入“发送线路”那一刻起CRC计算器就同步开始吞字节。当payload最后一个字节发完时CRC计算器里刚好沉淀出最终的校验值。紧接着把校验值按字节顺序送出。因为CRC的输出天然比输入晚几个时钟周期所以设计时要在状态机里安排好对齐关系——说白了CRC计算模块随时在跑FCS_SEND只是把已经算好的结果读出来而已。我用的是经典的crc32_d64查表式Verilog模块。你可以从网上找到很多版本这里直接引用一个常见实现的接口module crc32_d64( input wire clk, input wire rst_n, input wire data_valid, input wire [7:0] data_in, output reg [31:0] crc_out );有的初学者会问CRC32多项式那么多版本以太网用的是哪个答案是IEEE 802.3标准多项式0x04C11DB7初始值是全1输出要异或0xFFFFFFFF。如果你在仿真里看到校验值总是差那么几个字节多半是初始值或最终异或没处理对。4. 接收通道的拆包技巧4.1 ARP响应和UDP数据如何区分接收通道的思想可以用“滑窗”来理解数据以字节流的形式不断进来你只需要按固定的偏移量去截取字段即可。比如收满14个字节后检查以太网类型字段如果是0x0806说明这是ARP帧解析里面的“发送端MAC”、“发送端IP”把目标MAC学习到寄存器里供发送通道使用。如果是0x0800说明这是IP帧。再往下解析IP头看到协议字段为0x11UDP协议号才真正进入UDP解析。这里有个先后顺序问题ARP帧也必须先接收下来否则发送通道没有目的MAC就发不出去。我在状态机里专门留了一个“ARP学习”的阶段ARP帧传送时payload一共28字节不含填充和FCS这28字节里面就包含PC的MAC地址和IP我直接用它填充内部寄存器。4.2 跨时钟域处理板载PHY芯片在百兆模式下一般是25MHz的时钟域。如果你的系统里其他逻辑跑在50MHz或100MHz接收通道就必须做跨时钟域。最简单的做法是异步FIFO。很多入门者喜欢直接在这里写always (posedge clk) fifo_data rx_dv ? rx_data : ...但遇到跨时钟域这种直接打拍子的方式在数据位宽变大时就容易出问题。我用Xilinx的fifo_generatorIP核配置成8bit输入输出读时钟用系统时钟写时钟用PHY提供的25MHz几乎零成本解决了跨时钟域问题。注意IP核只是用来解决缓存不要让它变成“黑盒依赖”。FIFO的读侧仍然要在应用侧自己写状态机才能在学习中保持对每一个数据字节的掌控感。4.3 接收方向的关键经验别急着丢废弃帧接收方向有一个特别容易踩的坑网络中混杂着广播帧、组播帧、VLAN帧甚至长度过小的帧。初版代码通常一遇到非目标MAC地址的帧就直接丢弃但调试时你根本看不到任何输出分不清是“没收到”还是“收到但丢掉了”。我的建议是先在接收模块的调试版本里加上一个统计计数器分别记录“收到总帧数”“CRC错误帧数”“非本机帧数”“成功解析UDP帧数”。上板后用Wireshark同时发一些混杂流量通过计数器差别就能快速定位卡在哪一环。这个排查思路比你在ModelSim里反复仿真管用得多。5. 仿真与上板测试5.1 搭一个能自己发数据的Testbench仿真期间不要用真实PHY的MDIO接口去驱动直接在testbench里实例化udp_tx然后给一组模拟的payload数据经过串行发送后观察输出线上的字节流是否与预期帧结构一致。我通常会在testbench里写一个“参考帧生成器”用同样的UDP协议规则把期望的帧逐字节生成出来存到数组里。然后等实际模块发送完毕后用$display打印错误字节位置。这个方法虽然土但排查问题非常快——仿真期间发现CRC错、头字段错都能立刻定位不会带到板上去。5.2 板级联调Wireshark与网络调试助手双保险上板调试前要检查一根网线的类型。FPGA开发板一般通过网络变压器直接接PHY网线用普通直连线插到PC即可。但有些开发板的网络接口是MDI交叉的用错交叉线也可能让link起不来。这个细节不贵但很典型的“基础但致命”。联调步骤我建议这样走上电后先用Wireshark抓包确认能看到FPGA发的ARP请求。PC上配一个静态IP与FPGA默认IP比如192.168.1.10同网段。网络调试助手开一个UDP Server端口比如6666稍等片刻看能不能收到FPGA自动发送的“Hello FPGA”字符串。收到之后再从网络调试助手向FPGA的IP和端口比如6666发一串数据观察板载LED或数码管变化。我调试时遇到过非常诡异的现象ARP请求能抓到但Wireshark里显示UDP数据包全是“bad checksum”。排查后发现是IP头校验和算法写反了字节序——因为我在组装IP头时把校验和字段的字节高低位颠倒了。这个问题如果不借助Wireshark的校验和提示光靠网络调试助手根本发现不了。5.3 Wireshark筛选UDP时间间隔的小技巧顺便分享一个热词里提到的技巧如何筛选出UDP前后两包的时间间隔。Wireshark没有直接的“时间差”筛选器但可以用“显示列”的方式突破在任意一帧上右键选择“列首选项”。添加一个新的列列类型选择“Delta time displayed”。排序这个列就能看到相邻两包UDP的时间差。如果要深入统计还可以用右上角“统计”菜单里的“IO图表”把UDP流量按时间画出来在打流测试时特别直观。6. 常见问题与排查实录6.1 CRC校验一直错误首先检查CRC模块的输入数据位序。PHY芯片在MII接口上的数据是高字节在前还是低字节在前不同芯片有不同配置。如果你的CRC计算的输入顺序和发送顺序不一致算出来的校验码必错。解决办法是仿真时从“MAC层数据”而不是“PHY的4bit nibble”去分析。二者换序是常见的隐蔽问题。其次确认CRC计算范围。“FCS覆盖整个帧”这句话说起来容易但实际发送时前导码不算SFD也不算只有从目的MAC开始到IP/数据结束才算。很多人把前导码也算进去了结果错得离谱。6.2 ARP请求能收到但UDP发不出去这种情况八成是ARP学习没完成目的MAC寄存器还是全0。可以在发送状态机里加一个arp_done标志只有该标志置1才允许从IDLE进入PREAMBLE。如果arp_done一直为0就去查接收通道的ARP解析逻辑特别检查ARP应答里的“发送端MAC”和“发送端IP”字段是否提取对了偏移。6.3 FPGA发出来的数据PC收不到但PHY link是up的优先检查MAC地址是否正确。有些PHY芯片在回环测试模式下会收到自己发出的帧但PC收不到。可以先在FPGA逻辑里把发送数据旁路到接收通道看看能否自环快速确认PHY的TXD/RXD方向没有接反。6.4 IP校验和算法字节序问题IP头的校验和计算原来是对16bit字网络序求反码和。很多人的实现表面上正确但因为FPGA里数据是字节流拼16bit的时候要遵循“大端”规则。如果你看到校验和老是差那么一点用Wireshark展开IP头对照校验和字段的字节序直接改拼接那一段即可。6.5 UDP端口冲突或PC防火墙拦截Windows系统的防火墙默认会拦截外部发来的UDP包网络调试助手绑定端口后最好在防火墙入站规则里放开UDP端口。这个虽然是“非硬件”问题但骗了不少初学者值得记一笔。7. 一点实操心得这个part做完之后你再回头看之前跑的点灯和数码管例程应该能明显感觉到“系统”和“例程”的区别。协议栈虽然精简但它把硬件的并行思维体现得很彻底发送通道、接收通道、CRC计算、FIFO缓存所有这些模块都在同一时刻各干各的全靠状态机和握手信号协调。这个感觉只有亲手写过协议才能建立起来。关于后续扩展我建议下一步把发送数据源从固定字符串换成ADC采集数据或者图像传感器的像素流这样你就有了一个“FPGA图像传输”项目的雏形。再往后做千兆以太网只是把数据位宽从8bit改32bit、时钟从25MHz改125MHz模块划分和状态机架构基本可以原样沿用。
返回列表