ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FPGA手写UDP协议栈:Verilog状态机、ARP与CRC32踩坑全记录

FPGA手写UDP协议栈:Verilog状态机、ARP与CRC32踩坑全记录 简介这是一份面向FPGA开发者的UDP协议Verilog实现资源包适合有一定Verilog基础、希望在FPGA中搭建高速实时以太网传输通道的工程师学习参考。压缩包内共113个文件以Quartus工程数据库cdb/hdb/ddb、Verilog源码.v、工程配置文件qpf/qsf以及可烧写文件sof/mif为主整体约4MB目录结构完整便于按模块查找。资源围绕UDP协议栈的硬件实现展开完整给出UDP包处理、IP接口、MAC接口、CRC校验与控制逻辑等核心模块包处理模块负责头部解析与构造IP接口处理IP层解析与分片重组MAC接口完成以太网帧封装CRC模块计算并验证校验码控制逻辑协调发送接收状态。同时结合乒乓缓冲与同步处理讨论时钟亚稳态、错误检测与可配置参数等设计要点帮助读者理解FPGA上实现传输层协议的完整流程。压缩包中还包含readme说明与备份源码方便对照阅读和二次修改。目前已有964人学习参考对从事网络设备、嵌入式高速数据传输的开发人员具有实用价值。 前阵子调一块FPGA图像采集板MIPI摄像头数据进来DDR3做缓存最后要走千兆网口把实时图像扔到PC端显示。方案评审的时候接口选型在PCIe和以太网之间纠结了很久最后定了一个看起来最“传统”的方向UDP协议用Verilog在FPGA里纯逻辑实现。现在回头看这个决定救了我也坑了我——救在于它让我从头到尾吃透了协议栈的每个字节坑在于我为此至少多加了三个星期的班。这篇文章就是把那三个星期踩出来的完整经验写出来送给准备在FPGA上碰UDP、但还没决定是自己写还是用IP核的朋友。1. 为什么我放弃IP核用Verilog手写UDP协议栈1.1 三条实现路线IP核、软核CPU、纯逻辑在FPGA上做UDP通信大体上有三条路线我先把它们摆在一起对比一下方便你根据自己项目的情况做选择。路线开发速度可控性吞吐性能典型坑厂商自带UDP/IP核最快拖进去就能用差内部是黑盒中上取决于核的实现license限制、配置项多、报文格式想改没地方下手软核CPU跑lwIP协议栈中等C代码改起来方便中协议栈逻辑不透明一般受CPU频率和总线瓶颈限制中断频繁、延迟抖动大、高带宽场景容易丢包纯Verilog状态机实现慢但是链路完全透明最好每个bit都是自己写的高可以做到逐时钟处理所有细节都要自己负责坑多且深我当时的需求是MIPI摄像头输出1080p60fps裸数据量大概每秒1.5Gbps千兆网口理论带宽1Gbps所以必须做压缩或者裁剪但即便是裁剪后的数据也要求非常稳定的持续输出。用软核CPU跑lwIP中断一多DDR3读写一忙帧率就会掉数据流不均匀。而IP核的问题在于我需要精确控制每一帧的发送时机要在应用层自己组UDP包IP核反而成为瓶颈。1.2 我踩过的“IP核黑盒”坑第一版我确实用了某大厂IP核UDP发送和接收各例化一个前仿很快过上板却一直不通。用ILA抓MAC层的信号发现IP核内部的CRC计算、填充逻辑根本看不到报错只能对着配置界面瞎猜。最让我崩溃的是它默认把IP首部校验和卸载checksum offload掉了Wireshark抓包显示校验和错误一票红排查了大半天才发现是IP核自动把IP首部校验和算好了但是UDP校验和我没法控制。后来我直接把这个IP核从工程里删了决定自己写。虽然慢但至少出了问题我能知道是哪一行代码的锅。2. UDP报文里最容易搞错的字节序和字段布局2.1 从网线bit到应用数据的完整链路如果你以前只写过应用层socket代码可能从来没关心过UDP包在网线上到底是什么样的。在FPGA上你要自己组帧就必须把这条链路上的每一层都拆清楚。一个标准的UDP报文在以太网上实际传输的数据帧长这样字段长度说明目的MAC地址6字节接收方网卡地址源MAC地址6字节本FPGA板卡的MACEtherType2字节0x0800表示上层是IPv4IP首部20字节版本号、总长度、TTL、源IP、目的IP等UDP首部8字节源端口、目的端口、长度、校验和UDP有效载荷N字节应用数据最大1472字节MTU1500FCS校验4字节CRC32由MAC层硬件计算并追加这里的关键在于网络字节序是大端big-endian。也就是说一个16位的端口号0x1234在网线上先发0x12再发0x34。而很多FPGA开发者在做DDR3或者内部FIFO数据拼接时习惯小端序这两个一混就会出现“我明明发了0x1234PC端收到的却是0x3412”这种奇怪问题。2.2 一个UDP帧的Verilog表示方法我自己在用Verilog构造发送帧时习惯把头部做成固定字节数组用计数器逐字节发送。给你一个参考框架// 发送FIFO输出侧逐字节拼接UDP帧 reg [7:0] frame_mem [0:63]; // 存放头部 reg [5:0] hdr_cnt; always (posedge clk) begin if (tx_state SEND_HDR) begin tx_data frame_mem[hdr_cnt]; hdr_cnt hdr_cnt 1; end endframe_mem里的字节顺序必须严格按照2.1表格里从左到右的顺序填先目的MAC再源MAC再类型再IP头再UDP头。这个顺序错了整个包PC端根本不会认。2.3 IP首部校验和的计算方式IPv4首部校验和覆盖的是20字节IP头算法很简单把20字节按16位一组累加如果累加过程中产生进位则回卷加到最低位最后取反。给出一个可以直接抄的伪代码思路function [15:0] ip_checksum; input [159:0] ip_header; integer sum; begin sum 0; for (i 0; i 10; i i 1) sum sum ip_header[i*16 : 16]; // 处理进位 sum (sum 16hFFFF) (sum 16); sum (sum 16hFFFF) (sum 16); ip_checksum ~sum; end endfunctionUDP校验和如果不想算可以把4字节的UDP校验和字段填0这在IPv4下是合法的接收方不会因为这个把包扔掉。很多FPGA项目图省事都这么干Wireshark会标红但实际通信没问题。不过如果你的PC端程序用了严格的checksum校验那最好还是在FPGA里把UDP校验和也算出来。算法跟IP首部校验和类似但要额外在UDP数据报前面加一个12字节的伪首部源IP、目的IP、协议号、UDP长度等我只在后期图像传输稳定之后才补上前期排错阶段全部填0。3. 发送通路状态机、CRC32和FIFO调度3.1 发送状态机应该怎么设计整个UDP发送通路的核心是一个状态机我的工程里大概是这样的状态跳转IDLE - ARP_REQUEST - WAIT_ARP_REPLY - SEND_ETH_HEADER - SEND_IP_UDP_HEADER - SEND_PAYLOAD - SEND_FCS - IFG_WAIT为什么发送UDP之前要先处理ARP因为你要把数据发给PC必须知道PC网卡的MAC地址。PC在接收到一个目的MAC不是自己的帧时会直接丢弃。所以第一包UDP发送之前FPGA必须先发一个ARP请求问“192.168.1.100的MAC地址是多少”PC会回应一个ARP应答FPGA把应答里的MAC地址存下来后续发UDP帧的目的MAC就用这个值。最常犯的错是每次发UDP之前都发ARP网络带宽浪费不说PC侧如果开了防火墙或者ARP应答慢了整个发送节奏就被打乱。正确做法是维护一个ARP缓存表存一次用很久除非需要重连。3.2 CRC32以太网FCS的正确算法以太网帧最后的4字节FCS是CRC32标准多项式是0x04C11DB7初值为0xFFFFFFFF计算完成后结果要取反。这里有两个坑第一计算范围是从目的MAC开始到UDP载荷结束但不包括FCS本身。第二很多教程里给的CRC32代码是数据最低位LSB先算的而以太网标准要求也是LSB first如果你照着常见的CRC32软件算法抄方向弄反了CRC永远不匹配。下面是我在工程里用的CRC32核心逻辑片段欢迎抄// 使用 CRC-32 多项式 0x04C11DB7LSB-first reg [31:0] crc_reg; always (posedge clk) begin if (crc_init) begin crc_reg 32hFFFFFFFF; end else if (crc_en) begin crc_reg[0] crc_reg[31] ^ data_in; crc_reg[1] crc_reg[0] ^ crc_reg[31] ^ data_in; crc_reg[2] crc_reg[1] ^ crc_reg[31] ^ data_in; // ... 其余位按CRC32公式展开 end end完整展开太长我当时是在Vivado的IP catalog里直接例化一个CRC Generator IP核对参数比自己展开32位公式省事得多。如果你也不想手推公式用IP核生成CRC32模块参数选CRC-32/MPEG-2或者CRC-32/ISO-HDLC选法要看你的数据方向。我建议仿真阶段先做一个自检已知一个以太网帧的标准FCS值跑一下你的CRC模块对上再上板。3.3 FIFO调度用户的业务数据从哪来纯逻辑实现UDP数据的源头往往不是CPU而是另一个模块——我这里是MIPI摄像头采集图像缩放之后的行缓存再经过DDR3仲裁读出的数据流。数据从DDR3读出来是64bit宽、几百MHz的时钟域而MAC发送是8bit宽、125MHz的时钟域千兆网1Gbps/8125MHz两边时钟完全不同步。跨时钟域的标准解法是异步FIFO。我直接例化了Xilinx的FIFO IP核写侧数据宽度64bit读侧8bit深度选了2048。这里有一个效率问题UDP最大有效载荷是1472字节如果FIFO深度不够一次只能发几百字节就得等FIFO空带宽利用率会非常难看。深度取2048可以容纳一个完整的UDP帧发送时从FIFO里稳定读出来不会断流。4. 接收通路ARP处理与UDP解析4.1 主机为什么不直接发UDP很多人会忽略接收通路以为只要FPGA会发包就行。但实际上你要让PC主动往FPGA发一条控制指令必须处理接收方向的数据。PC要往FPGA发UDP包第一步依然是ARP。PC会先检查自己的ARP缓存如果不知道FPGA的MAC地址就广播一个ARP请求“192.168.1.50的MAC地址是多少”这时FPGA必须回复ARP应答。如果FPGA不处理ARPPC端ping不通UDP包也发不出来因为PC认为链路层根本没有这个节点。所以接收通路里第一个模块就是ARP应答模块收到EtherType0x0806的帧解析出操作码1是请求2是应答如果目的IP是自己就把自己的MAC填回去源和目的MAC互换发一个ARP应答帧。4.2 接收状态机与关键过滤条件接收方向的完整处理链路是RGMII - 解码 - 判断EtherType - 解析ARP / 解析IPv4UDP - FIFO - 应用逻辑解析UDP帧时的过滤条件每一项都很关键目的MAC必须等于本板卡MAC地址或者FF:FF:FF:FF:FF:FF广播地址。EtherType必须等于0x0800IPv4。IP首部中协议号必须等于17UDP。目的IP必须等于本板卡IP。UDP目的端口必须等于本板卡绑定的端口。如果上面任何一项对不上直接丢弃不做任何响应。我踩过的一个坑是在判断目的MAC时PC发给板卡的帧可能带VLAN标签EtherType会变成0x8100VLAN标签后面才是真正的网络层协议。如果调试环境里交换机开了VLAN你会发现FPGA收不到任何包。排查半天后发现是VLAN标签的问题——最简单的办法是让PC和FPGA直连不要过交换机这样能省掉一大部分网络环境带来的干扰。4.3 在FPGA里同时管理ARP和UDP发送发送和接收其实是两个独立方向但ARP缓存这个资源是共享的。我的做法是ARP请求模块在检测到PC的ARP广播时自动更新内部寄存器UDP发送模块在启动时先检查这个寄存器是否有效无效则触发一次ARP请求并等待应答有效则直接开始发送。这两个模块之间用一个简单的握手信号连接避免出现重复请求和死锁。5. 仿真验证用Icarus Verilog模拟真实网络交互5.1 为什么仿真阶段就要模拟主机回ARP很多人在FPGA仿真里只做一个loopback测试把发送数据直接接到接收端看能不能收到自己发的包。这种测试能验证收发数据通路但验证不了最重要的交互流程——你发ARP请求对方回ARP应答你再发UDP这个完整的时序依赖loopback根本模拟不出来。我在testbench里模拟了一个“虚拟PC”它有一块虚拟网卡MAC和IP行为完全仿照真实主机。当FPGA发出ARP请求时虚拟PC在若干时钟周期后返回ARP应答当FPGA发出UDP数据时虚拟PC解析出载荷并回发一个ACK。这样就能在仿真里完整验证“发送方状态机在收到ARP应答前不会乱发UDP”这个关键逻辑。5.2 Testbench框架参考用Icarus Verilog跑起来非常简单iverilog -s testbench -o tb.vvp tb.v udp_tx.v udp_rx.v arp_module.v crc32.v vvp tb.vvp gtkwave tb.vcdtestbench里关键的激励逻辑// 虚拟PC检测到EtherType0x0806且opcode1则回ARP应答 always (posedge clk) begin if (rx_frame_valid eth_type 16h0806 arp_op 16h0001) begin // 构造ARP应答帧发回给FPGA tx_arp_reply_en 1b1; end end这里要注意ARP应答帧的发送时机不能在收到请求的同一拍就回真实网络是有延迟的。我会在testbench里插入一个50~200个时钟周期的随机延迟用来模拟网络排队时延这样更贴近真实情况。5.3 仿真阶段看什么信号我仿真最常盯的三个点状态机是否按预期跳转特别是在ARP_WAIT状态会不会卡死。FCS是否正确可以在testbench里用同样的CRC32模块算一遍对比。发送完一个完整UDP帧后是否有IFG帧间隙等待太短的IFG会导致部分交换机丢帧。6. 上板联调ILA、Wireshark和MTU分片6.1 上板第一件事不是接网线而是抓ILA上板调试最忌讳的是一上来就插网线看Wireshark。我习惯先在FPGA工程里挂上ILA观察RGMII接口的tx数据、tx_valid、tx_last这些信号。先用ILA确认FPGA确实在发数据发的是什么内容再去PC端看抓包结果。这里有一个经验ILA的采样深度要够最好能采一整个UDP帧的数据而不是只采头几个字节。我遇到过一个问题帧格式看起来完全正确但只有前100个字节发送正常后面全是垃圾数据。原因是我内部FIFO读出的数据在帧中间发生了空读FIFO下溢导致数据错位。这种问题用Wireshark根本看不出根因只有在ILA里看到FIFO的rd_en和empty信号才能定位。6.2 Wireshark上看到的“假错误”RGMII接口调试通了以后PC端用Wireshark抓包经常能看到满屏的红色校验和错误。如果UDP校验和字段是0Wireshark会标记为“校验和为0因特网校验和卸载导致的未校验”这是正常的不影响通信。但有一个情况需要警惕Wireshark报的“IP校验和错误”如果伴随大量乱序、长度异常往往是FPGA发出的MAC帧长度和IP头里的总长度字段不一致。IP头里的total_length是IP包总字节数IP头UDP头载荷如果这个字段比实际发送的数据少几个字节接收方会把多余的字节当成填充或者直接丢弃导致上层应用收到的数据长度对不上。6.3 MTU限制1472字节以上的数据怎么办UDP载荷最大1472字节是因为以太网MTU1500减去20字节IP头和8字节UDP头1472正好是上限。如果你要传一帧1920x1080的RGB565图像一帧就4MB一个UDP包根本装不下。两种做法一是让IP层自动分片设置IP头里的identification、fragment offset字段把大包拆成多个小包。但IP分片在实操中很麻烦而且一旦中间路由器做了过滤分片包可能被丢弃接收端重组失败。二是应用层分片我在FPGA里直接把图像数据按1400字节一块切开每块加一个包头包含帧序号、块序号、总块数发送端顺序发送PC端按包头重组。FPGA端分片逻辑很直接就是一个计数器发满1400字节就切下一块。这种方式比IP分片可靠得多也方便PC端做丢包重传。7. 复盘最让我熬夜的几个坑7.1 跨时钟域FIFO深度计算异步FIFO的深度不是随便选的。我当时写侧是DDR3读出的数据突发读256字节然后等待仲裁读侧是MAC以125MHz逐字节发送。FIFO深度如果小于一个UDP帧的1472字节发送过程中必然下溢。我自己最开始选了512结果每发300字节左右就断一次流PC端收到的UDP包全是零散的。后来改成2048一次性把整个UDP帧装进FIFO发送过程再也不打断。7.2 回环测试的误导我一开始在板卡上把发送通路的输出直接接到接收通路输入端做回环测试数据收发看起来一切正常。但把FPGA接到真实交换机后再测PC根本收不到任何数据。排查了很久才发现回环测试把MAC层的FCS、IFG时序全部“自己人放行”了而真实交换机对帧间隙、FCS格式要求非常严格稍有不规范就把帧丢弃。所以强列建议上板联调一定要尽早接真实交换机或者直连PC网卡不要迷信回环测试。7.3 开发板PHY芯片的寄存器配置很多开发板的PHY芯片出厂默认是自适应模式RGMII时序可能和你FPGA侧不一致导致发送正常但接收不稳定。我当时用ILA抓接收数据发现偶尔会出现半个字节错位的问题最后是改了PHY的delay配置RGMII TX/RX delay才解决。Vivado工程里涉及到RGMII接口时约束文件里对T(clk)和T(data)的约束必须认真写不然时序收敛问题会让你怀疑人生。写这篇文章的时候这个协议栈我已经复用了三个项目。第一个项目花了三周第二个项目只花了两天第三个项目直接把发送接收模块当黑盒用半天完事。所以如果你正准备在FPGA上自己做UDP不要怕前期慢这套东西一旦吃过一遍透后面所有带网络口的板子都是复制粘贴的活。最后分享一个我自己的习惯写任何网络相关模块先在testbench里把“对端回ARP”这个行为模拟出来再上板遇到链路层问题就抓ILA遇到协议层问题就抓Wireshark两个视角一交叉问题基本都能缩小到具体模块里。祝你也早日从UDP的坑里爬出来。本文还有配套的精品资源点击获取
返回列表