ARTICLE DETAIL

资讯详情

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

FPGA零基础实现UDP协议栈:verilog-ethernet开源工程实战解析

FPGA零基础实现UDP协议栈:verilog-ethernet开源工程实战解析 从近似0基础开始FPGA开发已经写到第10篇了前面的内容包括LED点亮、按键消抖、UART收发、SPI读写、数码管扫描这些常规操作到这一篇开始接触真正有点“工程感”的东西——借助开源的verilog-ethernet工程踏踏实实跑通一套UDP协议栈。这篇文章没有神化协议栈也没有把它当成黑盒子而是从近0基础的角度把UDP协议栈的工程结构、模块功能、仿真验证掰开来看一遍让你知道这玩意到底怎么用以及为什么它能发能收。verilog-ethernet这个开源工程在FPGA行业里讨论度挺高它把UDP/IP协议栈做到了RTL级不依赖硬核也不依赖操作系统纯靠FPGA逻辑完成MAC、ARP、UDP、ICMP的处理。和你常在STM32、Linux里用的协议栈完全不是一个路数——在FPGA里跑UDP是直接用数字逻辑去拼帧、解析帧每一比特都是时序每一字节都有时钟。这篇文章会从网络分层的概念开始讲然后拆一下工程的核心模块再带你在Linux环境下用iverilog完成仿真把一次真实的UDP来回抓出来看。适合手上有一块入门级FPGA板子也适合想了解FPGA网络通信原理的开发者来参考。1. 整体老线工程学习思路与模块地图1.1 为什么选verilog-ethernet而不是直接调现成IP很多人第一反应是Vivado里有Ethernet IP核Quartus里也有三速以太网IP干嘛还要去啃开源RTL这个问题我刚开始也纠结。商业IP核当然稳定但有几个实际痛点第一IP核配置界面复杂MAC和PCS/PMA层层嵌套生成的代码动不动就几百个文件新手很难理清数据从哪进来、又从哪出去第二IP核大多是黑盒子综合后根本看不到内部逻辑信号名也极其晦涩第三很多IP核是需要付费授权的学习板自带的免费版本往往限制速率或端口数量。verilog-ethernet这个工程最吸引人的点在于它把整个链路拆得特别干净TCP/IP协议栈里几个关键角色——MAC收发、ARP请求/应答、UDP打包/解包、ICMP回显——都做成了独立模块。你可以在一个顶层文件里看到所有模块的连接关系每一段都是可以读懂的RTL代码。对于学习而言“能读懂每一行”比“能用起来”重要得多这也是我推荐它的主要原因。而且这个工程不挑厂商。它是标准Verilog写的至少可以用于Xilinx、Altera/Intel现在叫Intel FPGA以及高云、易灵思等国产FPGA平台。没有AXI接口绑死没有vendor primitives只要你板子上的PHY芯片支持GMII/RGMII标准基本都能移植。这一点非常难得我后来在一颗高云FPGA上也跑通了这套协议栈只改了个别引脚约束和时钟频率。1.2 从串口思维到网络思维的转变如果你之前只做过UART、SPI、I2C这类低速总线第一次接触以太网可能会有点懵。串口协议简单到什么程度呢一帧数据就是起始位加数据位加停止位没有包的概念也没有目的地址和校验需求发错丢了自己知道重发就行。但以太网不是这个玩法。以太网定义了MAC帧的结构TCP/IP协议栈又在这个上面定义IP报文、UDP报文、ARP报文。每一个报文都有固定字段源地址、目的地址、类型、长度、校验缺一不可。也就是说你要在FPGA里发一个UDP包实际上要完成这些事构造MAC帧头目的MAC、源MAC、类型类型字段需要填0x0800表示IPv4构造IP头版本号、头长度、总长度、协议号17表示UDP、源IP、目的IP还要算IP头校验构造UDP头源端口、目的端口、长度、校验和校验和可以直接置0这是允许的把所有字段按字节拼接送入MAC发送模块加上前导码和CRC校验。同样收到一个UDP包你要做的是逆过程先解析MAC帧头检查目的MAC是否匹配然后看类型是IPv4还是ARP如果是IPv4还要继续解析IP头检查协议号是不是UDP最后才从UDP头里读出端口号把payload送到用户接口。这个过程如果用软件做几行代码就写完了因为CPU是顺序执行的有for循环、有堆栈、有动态内存。但FPGA没有“循环”的概念没有“函数调用”里面只有寄存器、组合逻辑、状态机和FIFO。协议里的每一步都必须对应到一组状态转移。verilog-ethernet最大的贡献就是把这个转换过程实现了出来并且做得足够模块化让你能顺着代码把协议走一遍。1.3 工程目录与模块地图我clone的是GitHub上alexforencich/verilog-ethernet仓库当前比较通用的分支是master。整个工程rtl目录下文件很多但真正核心的模块对我们理解UDP通信流程来说可以按功能分成这几组底层收发eth_mac_1g_rgmii_fifo、eth_mac_1g_gmii_fifo这两个是带FIFO的MAC层收发器前一个适配RGMII接口的PHY后一个适配GMII接口的PHY网络层解析eth_arbiter、eth_parser一个是多路发送仲裁器一个是接收解析器ARP相关arp_cache、arp_ctrl负责ARP缓存表的管理和ARP请求/应答状态机UDP相关udp、udp_lite、udp_checksum_gen、udp_checksum_gen_16负责UDP包的封装和解析IP相关ip、ip_arbiter负责IP头的生成和校验、多端口复用顶层封装eth_udp、eth_udp_tx、eth_udp_rx这几个是把UDP收发逻辑打包好的简易接口对我们学习最友好。这里有个直接的切入路径。你不需要一上来就把所有模块都读懂很多人面对几十个.v文件时直接放弃就是因为想一口吃成胖子。正确的姿势是先只看eth_udp_tx和eth_udp_rx这两个顶层封装搞清楚发送侧从用户接口收到数据之后是怎么一步步封装成UDP包的接收侧又是怎么一步步解析出payload的。等你把这两条主路径摸清楚了再回头去看ARP、IP、MAC这些底层模块会轻松很多。2. 核心模块拆解与实操细节2.1 发送通路从用户数据到以太网帧我们先走发送通路。eth_udp_tx这个模块顶层输入信号最关键的有这几个clk用户时钟我用的100MHzrst复位信号高电平有效s_axis_tdata待发送的用户数据位宽默认是8bit可配置为16bit或32bits_axis_tvalid数据有效标志s_axis_tready下游反压信号告诉上游我能不能接收s_axis_tlast一帧数据的最后一个字节标志m_axis_tdata、m_axis_tvalid、m_axis_tready连接内部IP层发送接口的AXI-Stream信号。看到这里你会理解一个关键点FPGA的协议栈模块之间大多用AXI-Stream接口互连。这种接口就是valid/tready握手再加data和可选的tlast/tuser。握手规则很简单一个数据拍只有在valid和tready同时为高时才能真正传输tready可以拉低表示当前下游忙。很多新人第一次看到AXI-Stream会发怵但实际上它就是带握手的流水线你把这个逻辑捋顺了后面看任何IP核都不会再有障碍。eth_udp_tx内部做的事情是替你把UDP头、IP头、MAC头都拼好。它有缓冲机制先把用户数据存在内部FIFO里然后从FIFO读出来按顺序先发送MAC头再发IP头再发UDP头最后发用户payload。你会发现这个模块并没有处理CRC因为CRC校验在更底层的eth_mac_1g_*模块里完成MAC层发送模块会在帧末尾自动附加4字节CRC。了解位宽配置也很重要。我之前图省事直接用8位数据通路然后把所有MAC地址、IP地址用十六进制硬编码进去。后来尝试修改成32位位宽省了很多时钟周期因为一个时钟可以传4字节但代价是各个头字段的对齐变得复杂了比如IP头的总长度字段原本是字节流中第17、18字节的位置在32位位宽下可能分布在两个不同的数据拍里处理方式完全不同。这里我的建议是学习阶段就用8位位宽跑通功能不要上来就追求性能。2.2 接收通路解析MAC、IP、UDP头接收通路对应eth_udp_rx模块。它从MAC层接收完整以太网帧内部解析逻辑主要是一个大状态机整个状态机的状态大致是这样走的等待前导码MAC模块已经处理用户层看不到接收并缓存MAC目的地址、源地址解析以太网类型字段如果是IPv4继续接收IP头校验版本号、头长度、协议号如果协议号是UDP继续接收UDP头解析源端口、目的端口和长度把UDP payload通过AXI-Stream接口输出同时在tuser信号上带上端口号、IP地址等信息。这个状态机其实就是软件协议栈里用if实现的逻辑在FPGA里给换成了状态转移。读懂了它你就理解了FPGA处理网络协议的通用套路。接收侧有一个比较容易被忽略的内部细节地址过滤。eth_udp_rx会根据你自己设置的本地MAC地址和IP地址判断收到的帧是不是发给自己的。如果目的MAC不是广播地址FF:FF:FF:FF:FF:FF也不是本地MAC整个帧会被直接丢弃。IP地址同理。所以如果你的测试工程一直收不到数据先别急着查逻辑检查一下自己设置的目的MAC对不对这是非常常见的低级错误。2.3 ARP模块的双重身份ARP在整个协议栈里承担两个角色一个是主动发起查询比如你从FPGA往电脑发UDP但不知道电脑的MAC地址就需要发一个ARP请求让电脑回一个ARP应答另一个是被动响应比如电脑ping FPGAICMP回显其实由eth_udp里的ICMP模块处理或者电脑主动向FPGA发UDP前提是先通过ARP知道FPGA的MAC地址此时FPGA需要回一个ARP应答。arp_cache是一个简易的MAC/IP地址缓存表默认容量是4条表的每一项存一对IP地址和MAC地址。arp_ctrl是状态机维护这个缓存表的查询、更新、无效化。如果你在测试中发现FPGA发的第一个UDP包电脑收不到但后面就正常了很可能就是第一次发送时ARP缓存为空FPGA先发了一个ARP请求电脑收到后更新了缓存第二个时刻才能正常发送数据。这不是什么神秘问题而是协议流程的正常现象。很多网络调试软件比如Wireshark能看到这段交互过程抓包一看就明白。严格来说FPGA里的ARP缓存实现非常简陋和电脑操作系统里那种完整的ARP老化机制完全没法比。但这也正是开源工程的价值所在给你看的是最精简、最核心的实现让你不至于被琐碎的细节淹没。学习时抓住“请求-应答-缓存”这个主流程就够了。2.4 跨时钟域与FIFO带来的稳定性保障verilog-ethernet工程里发送和接收数据通路上都加了FIFO模块最典型的是eth_mac_1g_rgmii_fifo里面的axis_fifo_adapter。为什么要加FIFO因为PHY芯片的时钟和FPGA逻辑侧的时钟往往不是同一个频率甚至不同相位FIFO起到了跨时钟域缓冲的作用。MAC层收发数据时数据要跟着PHY提供的RX时钟而用户逻辑可能跑在自己独立的时钟域频率还未必成整数倍关系这时候如果没有FIFO做缓冲数据直接跨时钟域基本必崩。初学者容易忽略这些FIFO的存在总觉得“我发一个字节它就应该立刻出去”。实际上在百兆/千兆以太网环境下数据流是连续的而用户的业务数据往往是突发性的FIFO天然肩负了“削峰填谷”的任务。这也是为什么很多商业IP核会围绕FIFO做一堆配置选项的原因。我在实际测试中还发现一个和FIFO深度有关的坑如果发送FIFO深度太小当突发数据超出FIFO容量时即便下游持续从FIFO读数据上游也仍然会被反压表现就是tready周期性拉低。此时如果用户逻辑没有正确处理握手信号就会丢数据或者把数据顺序搞乱。好在这套开源工程的FIFO模块配置很灵活默认深度用完没问题你可以按需调整。3. 仿真环境搭建与一次完整UDP往返测试3.1 用iverilog搭建学习级仿真环境商业仿真工具当然也可以用比如Vivado自带的xsim或者Modelsim。但我个人非常推荐学习阶段用iverilog加GTKWave原因是它足够轻量、免授权、启动速度快还完美支持Verilog-2001和大部分SystemVerilog语法非常适合跑这种中等规模的开源工程。尤其在看波形做调试时GTKWave虽然界面朴素但它是开源的你用起来没心理负担也不怕license过期这点比商业工具舒服很多。你需要先安装工具。Ubuntu/Debian下一条命令解决sudo apt-get install iverilog gtkwaveWindows系统可以到iverilog官网下载安装包GTKWave也有Windows版。安装完后验证一下iverilog -V gtkwave --version能看到版本号就说明工具链没问题。接下来要把verilog-ethernet仓库clone下来我用的是git clone https://github.com/alexforencich/verilog-ethernet.gitclone完成后rtl目录下就是所有源代码。注意这个工程很多文件是SystemVerilog写的.sv后缀iverilog对SystemVerilog的解析能力算是够用但遇到一些复杂的接口interface语法还是可能报错。不过eth_udp_tx、eth_udp_rx这个系列顶层模块是纯Verilog直接用没问题。3.2 编写UDP回环测试的testbench我写了一个简单的testbench目的很明确模拟PC通过GMII接口向FPGA发送一个ARP请求然后再发送一个UDP报文同时观察FPGA是不是能通过GMII发送ARP应答和UDP回环数据。回环的意思是FPGA收到UDP数据之后原封不动把这个payload再作为新的UDP数据发送回电脑。这个思路最简单也最能验证收发两条通路是否都正常。Testbench里最核心的一段是构造一个以太网帧并驱动到FPGA的GMII接收接口。注意GMII接口的标准时序是先在rx_dv为高的情况下每个rx_clk上升沿送一个字节数据到rxd上。前导码是7个字节的0x55第8个字节是帧起始定界符SFD值为0xD5然后才是真正的MAC帧内容。我摘一段UDP发包驱动的核心逻辑task send_udp_frame; // 假设已经完成ARP流程 // 目的MAC10:11:12:13:14:15 send_byte(8h10); send_byte(8h11); send_byte(8h12); send_byte(8h13); send_byte(8h14); send_byte(8h15); // 源MACaa:bb:cc:dd:ee:ff send_byte(8haa); send_byte(8hbb); send_byte(8hcc); send_byte(8hdd); send_byte(8hee); send_byte(8hff); // 以太网类型IPv4 send_byte(8h08); send_byte(8h00); // IP头开始此处省略细节仅示意 send_byte(8h45); // versionihl // 继续构造IP头... // UDP头 send_byte(8h30); // 源端口高字节 send_byte(8h39); // 源端口低字节 // payload send_byte(8hde); send_byte(8had); send_byte(8hbe); send_byte(8hef); endtask你肯定会问每次都要手动构造这么多字段不累吗其实学习阶段就得这么干因为只有亲手把字节一个个填进去你才会对协议格式有肌肉记忆。后面写自动化脚本才不慌。如果你实在不想手写工程自带了一个Python脚本py/udp.py可以生成完整UDP帧的十六进制文本我后来就是靠这个与testbench配合能省不少事。3.3 观察波形ARP请求与UDP回环仿真跑完之后用GTKWave打开vcd文件先把关键信号拖进来rx_clk、tx_clk、rx_dv、rxd、tx_en、txd、udp_rx_hdr_valid、udp_rx_payload_axis_tdata、udp_tx_payload_axis_tvalid。逐一观察就能看到完整流程。首先会看到FPGA发出一个ARP请求arp_ctrl状态机从IDLE跳到SEND_REQUEST。接下来电脑回ARP应答FPGA收到后在arp_cache里写入对应条目。然后我构造的UDP帧开始输入此时eth_udp_rx模块内部状态机会依次经历MAC解析、IP解析、UDP解析三个阶段能从udp_rx_hdr_valid信号捕获到一个脉冲紧接着udp_rx_payload_axis_tvalid拉高payload数据从udp_rx_payload_axis_tdata逐字节输出。与此同时回环逻辑把这些payload数据接到发送侧。eth_udp_tx模块开始忙碌起来先从内部FIFO读出用户数据然后拼接MAC头、IP头、UDP头最后体现在GMII发送接口上也就是tx_en拉高、txd上出现完整以太网帧。整个过程在波形上看起来就是两组“包”的输入输出节奏节奏之间有一个几个周期的处理延时这个延时主要花在FIFO写入、头字段拼接和校验计算上。这里有个值得注意的观察点接收UDP帧时tlast信号和payload数据的对齐非常重要。如果tlast比最后一个有效数据晚了一个周期接收端会认为帧还没有结束导致整个包被丢弃。这个开源工程里发送侧由eth_udp_tx负责把tlast放在正确位置接收侧解析payload长度来确立tlast时机两者配合得比较严密你从波形上能看到tlast与最后一个数据同时有效的干净波形。3.4 上板前仿真的边界不要跳过综合视角仿真通过并不代表上板一定能跑。很多人在仿真里验证了功能就急着去写约束文件结果综合后动不动就报时序违例。这个工程虽然不难但如果你在板子上把GMII时钟放到50MHz以上又不加时序约束那综合器就只能瞎猜路径延时结果大概率跑不起来。仿真验证的是“功能性正确”而综合实现验证的是“时序性正确”。这两个维度在FPGA工程里缺一不可。所以我给自己定的习惯是每个模块在仿真通过之后至少做一个OOCout-of-context综合看一下时钟频率能不能达到设计要求内部FIFO有没有被优化掉关键路径上的组合逻辑是否过长。只要某一级状态机的状态排队列太深就有可能形成长组合逻辑链路导致时钟频率上不去。verilog-ethernet这套工程本身时序设计是比较好的因为它经过了多个板卡的实测验证所以一般不会有时候问题。但如果你改了位宽、加了逻辑那就一定要重新做时序收敛检查不要因为“开源工程”三个字就掉以轻心。4. 常见问题与排查经验速查4.1 数据收发完全不通从哪里查起遇到收发完全不通的情况我推荐的排查顺序是先看PHY芯片的链路状态link信号有没有拉高如果没有说明物理层都没通用示波器或逻辑分析仪看PHY的TX_CLK、RX_CLK有没有时钟输出检查复位时序首先要保证PHY的复位引脚释放正确FPGA内部协议栈的复位也要保持足够长时间用抓包软件Wireshark看电脑网卡有没有收到FPGA发来的任何数据哪怕是乱帧也能说明MAC发送通路是活的如果电脑收到了数据但解析不出来检查MAC地址、IP地址、端口号是否和FPGA内部配置一致。如果是仿真阶段就完全不通那要先看数据有没有从测试平台的驱动进去用波形确认前导码和SFD是否正确。经常有人把SFD写成0x55那MAC层会把帧开始位置认错后面全乱套。4.2 ARP一直请求不到应答ARP请求发出去但没有应答常见原因有三个。一是FPGA的源MAC或者源IP地址设置得和网络上其他设备冲突导致ARP应答被网络里的其他设备干扰二是目的IP地址写错了电脑收到ARP请求后发现目的IP不对根本不理会三是电脑防火墙把ARP广播过滤了虽然少见但确实存在尤其是某些安全软件接管了网络协议栈之后。这里我要单独提一个我踩过的坑MAC地址的字节顺序最容易搞混。以太网传输是多字节字段按大端序也就是高字节先传但很多人直接把字符串“aa:bb:cc”翻译成字节数组时方向搞反了结果报文里的MAC地址完全错位。ARP请求发出去之后抓包看是能找到但对端根本识别不了。处理方式是把MAC地址的每个字节按顺序写进逻辑不要在脑子里做“翻转”写代码时严格按抓包工具显示的顺序来就不会错。4.3 UDP端口和校验和的坑发送侧UDP校验和可以直接置0这在IPv4协议里是合法操作接收端也不会校验这段。但要注意即使是置0UDP头的长度字段不能错UDP长度是指UDP头加payload的总长度单位是字节。你如果少算一个头的长度接收端会认为payload长度不对数据包可能被截断或者被丢弃。还有一个相对隐蔽的问题是IP头的总长度字段是包括IP头本身在内的整个IP报文长度而不是只算payload。很多人在手写帧的时候IP总长度填的是payload数量结果接收端一算印象中应该更长的数据整体被截断了。这类错误用Wireshark抓包立刻能看出来它会把“Length”字段标红。4.4 时钟频率和约束策略如果你在综合后遇到时序不收敛建议先检查三处跨时钟域FIFO有没有在约束文件里正确设置异步时钟组set_clock_groupsGMII/RGMII接口有没有对PHY时钟加input delay和output delay约束复位释放逻辑有没有做异步释放同步采集。初学者很容易在这上面耗费大量时间。我的经验是先把所有不相关时钟用异步组隔离开再对MAC接口加适当约束80%的问题都能解决。如果你用的RGMII接口还要再加上一个约束技巧RGMII的RX数据是DDR采样即上下沿都在采需要IDELAY或者专门的DDR寄存逻辑直接用GMII的流程去约束RGMII是行不通的。这个工程里eth_mac_1g_rgmii_fifo已经包含了ddio_in、ddio_out原语你只要保证对应时钟和延迟约束到位即可。4.5 仿真工具差异与平台适配细节如果你是Vivado用户直接用iverilog写好testbench之后也可以把rtl文件加到Vivado工程里做行为仿真但需要留意源的扩展名。.sv文件在Vivado里默认会被识别为SystemVerilog这没问题但有些老版本的Vivado对interface语法支持不全打开工程时会报错。如果只是想跑通UDP部分建议只把eth_udp这个顶层和它依赖的几个核心模块加进去别一股脑把整个rtl目录都加上不然会拉进来一堆你可能根本用不到的模块编译时间成倍增加。在Quartus里操作类似。Quartus对SystemVerilog的支持也一般如果遇到语法报错优先检查是不是某些module使用了interface。遇到这种情况可以先用iverilog验证功能然后手动把需要的模块做一个“白名单列表”加入工程其他模块不添加。这样既能控制编译范围也能减少工具差异带来的麻烦。最后聊一点实际体会写到这里verilog-ethernet的UDP协议栈学习主线算是过了一遍。这套开源工程给我的最大收获并不是某个模块本身而是一种意识网络协议不是只有软件才能实现只要你能把协议的状态转移、字节拼接、校验计算转换成硬件逻辑FPGA完全可以直接在物理层附近把网络包处理掉。这在图像采集、高速数据回传、工业控制这类对延迟敏感的场景非常关键。你不需要先把数据送到CPU走一遍协议栈再决定如何去处理而是可以在数据流经过FPGA的时候用RTL直接完成组帧、过滤、转发让CPU只做业务层面的决策。我个人的体会是学FPGA一定不能只停留在“点灯”和“流水灯”阶段那不是FPGA的核心竞争力。真正让FPGA有价值的是它处理并行数据流的能力而以太网协议栈恰好是一个特别好的实践载体。它既涉及状态机设计又涉及跨时钟域处理还涉及高吞吐数据通路的构造一层层剥开之后你会发现之前学的那些基础知识全部串起来了。对于还想往下深入的朋友我给你两个扩展方向一个是想办法在发送FIFO里塞一些实际业务数据比如把ADC采样结果直接打包成UDP帧发到电脑这就是一个最简单的数据采集以太网上报系统另一条是把接收侧的UDP payload按端口号做分发比如端口5001收到的数据存到FIFO端口5002收到的数据触发某个外设控制从“回环”走向“真正处理”。等你把这两个小任务做完你对FPGA网络通信的掌握程度就和只调现成IP核的人彻底拉开差距了。
返回列表