FPGA开发板秒变UDP网卡:verilog-ethernet协议栈实战笔记
学FPGA到一定程度很多人都会卡在同一个地方UART串口调通了但数据量大一点串口根本扛不住。这时候自然就想上以太网而以太网里最解渴的就是UDP协议栈。我一开始对“FPGA上写协议栈”这件事非常抗拒总觉得这是软件工程师的活后来被项目逼着用了verilog-ethernet这个开源工程才发现FPGA做UDP收发并没有想象中那么神秘。这篇笔记适合已经会点Verilog语法、跑过几个简单外设、但没碰过网络的FPGA学习者目标是让你在一个晚上内把手头开发板变成一块能收能发的UDP网卡。verilog-ethernet是github上一套用Verilog写的以太网UDP/IP协议栈它把MAC层、IP层、ARP、UDP这些脏活累活都封装成了一个个模块你只需要把PHY芯片接口对上然后把用户逻辑的数据通过一组标准信号喂进去剩下的事它全包了。而且它自带很多example工程不是给你一个孤零零的IP核就完事而是真的能编译、能上板跑的。下面从最基础的问题开始拆。1. 动手之前先用“一句话”讲清UDP协议栈在FPGA里到底在做什么1.1 为什么很多FPGA工程师只敢用串口不敢上以太网说句实话FPGA上做串口这个事情本质上就是一个移位寄存器的事RX一根线TX一根线clk分频配好就能跑。但以太网不一样它是有严格分层模型的物理层要处理差分信号、时钟恢复、DDR采样MAC层要处理前导码、帧头、CRC校验到了IP层还要处理报文长度、校验和、分片重组再往上UDP/TCP还有端口和连接状态。每一层拆开看都不难串在一起就把一个本来只想传几个字节的人吓跑了。还有一个现实原因串口调错了顶多收不到数据逻辑分析仪一抓就明白。以太网一旦不通你根本不知道是PHY配置不对、RGMII时序不行、MAC没起来还是ARP没回、IP校验和算错定位问题非常痛苦。所以大部分人宁愿在板子上多焊一个USB转串口也不想碰网口。但需求是躲不掉的。图像采集、高速AD数据、多通道传感器哪个都是十几MB甚至上百MB的吞吐量串口跑死也就几Mbps。这时候以太网是性价比最高的选择而在这条路上verilog-ethernet这样的开源工程能帮你把工程量直接砍掉一半以上。1.2 为什么选UDP而不是TCP聊到网络所有人第一反应都是TCP因为TCP可靠。但FPGA里做TCP老实说不是不能做只是成本和复杂度完全不是一个量级。TCP有复杂的连接管理三次握手、四次挥手、序号确认、超时重传、拥塞控制、滑动窗口。这一套放在CPU上操作系统几千行代码就搞定了放在FPGA里就是一堆状态机和定时器而且每个连接都要维护独立的上下文。等你把这些都调稳定了可能笔试都能考网络工程师了。UDP就不一样它就是往信封上写地址然后直接扔邮筒里不收确认不保证送达没有连接状态。看起来好像很简陋但恰恰是这个“简陋”让它非常适合硬件实现。板卡间传实时数据、图像帧、控制指令本来就有重传机制或者干脆能容忍丢包UDP那点开销完全可以忽略。所以对FPGA开发者来说第一优先级永远是UDP把UDP跑通了再考虑高性能TCP offload engine那是另一个深坑。1.3 为什么非要抱着verilog-ethernet这条大腿如果只看UDP的格式其实很天真源端口、目的端口、长度、校验和加起来8个字节。IP头复杂一点但也只是格式问题。那为什么不自己写一个呢因为在UDP下面还有一大堆基础工作。MAC层要生成前导码、CRC32CRC32算法本身不难但你要在100MHz甚至125MHz时钟下按字节并行算还要兼容各种PHY芯片接口差异还要处理RGMII这种DDR模式的数据采样PCB走线稍差一点时序就不满足。这些代码自己写一遍很可能写一个错一个而且错的位置非常难查。verilog-ethernet这个项目不是我吹它被大量FPGA开发者和公司用在真实产品里代码质量高、模块边界清晰每个模块都有详细的参数和端口注释而且自带的example工程基本能覆盖主流开发板。你不需要完全读明白每一行只需要能看懂模块之间的数据流然后按它的连接方式例化就能在相当短的时间里把网络打通。对比项自己写UDP协议栈用verilog-ethernet熟悉Verilog语法需要需要熟悉以太网协议细节必须可以边用边学工程调试周期数周起步一晚到几天踩坑概率极高较低社区用的人多可定制性完全自主模块化设计改起来也不难所以我的观点很明确初次上手永远优先用开源成熟方案先把端到端跑通再回头读源码理解细节。这叫站在巨人的肩膀上踩坑。2. 上板条件与工程准备2.1 板卡上到底需要哪些电路开始写代码之前先确认你手头这块板子能不能跑千兆以太网。绝大多数FPGA开发板都会配一个PHY芯片常见型号有RTL8211、88E1512、YT8512、AR8031等等这些都是千兆PHY支持RGMII接口。如果你的板子只有百兆PHY也没关系协议栈可以用MII接口只是速度和时序标准略有不同。这里有个概念要先分清FPGA芯片本身不直接连网口中间隔着一颗PHY芯片。PHY负责把FPGA侧的数字信号RGMII/MII/GMII转换成网线上的差分模拟信号。所以你的板子上必须有以下几样东西FPGA的普通IO连到PHY的RGMII数据线至少包含txd[3:0]、rxd[3:0]、tx_ctl、rx_ctl、txc、rxc这些信号PHY连接到RJ45网口RJ45一般是带网络变压器的不用你操心板上晶振通常给PHY提供参考时钟然后PHY向FPGA输出125MHz的接收时钟rxc有些板子还需要FPGA给PHY提供tx clockRGMII的实现方式有两三种具体看原理图我见过不少新手拿到板子后不看原理图直接把一堆用户手册示例的管脚约束抄进去结果PHY的rx_clk引脚根本没约束综合也过了但上板就是不通。所以第一件事永远是把原理图翻出来把PHY部分的连接捋一遍。2.2 下载源码与目录结构verilog-ethernet的获取非常简单找一个能访问GitHub的环境直接搜“verilog-ethernet”就能找到仓库建议用git clone方式拉下来或者直接下载zip包都行。拿到源码后不要急着编译先大概看一下目录结构。仓库里大致是这样一个布局verilog-ethernet/ ├── lib/ │ ├── axis/ # AXI-Stream相关的FIFO、寄存器 │ ├── eth/ # 以太网MAC层、RGMII时序处理 │ ├── ip/ # IP协议和ARP相关模块 │ └── udp/ # UDP收发模块 ├── example/ # 完整的示例工程和顶层文件 └── rtl/ # 部分版本的顶层组合文件不同版本目录名可能略有差异但结构基本不变。example目录是整个项目的宝藏里面通常会有一个UDP回环之类的完整示例你可以直接拿它当模板改。这也是我推荐的学习路径找到example先编译跑通再逐个模块看代码然后在上面加自己的逻辑。2.3 时钟复位和PHY初始化这些基础坑别踩这一节看起来基础但恰恰是上板失败的根源。先说时钟RGMII下千兆模式的接收时钟rxc是125MHz如果PHY跑在100M模式rxc降到25MHz10M模式则只有2.5MHz。verilog-ethernet的example大多默认千兆跑所以首先要确认你的PHY的默认速率配置别让PHY协商到百兆结果协议栈还按千兆时序收。其次是复位。PHY芯片需要一个上电复位过程而且很多PHY要求复位后保持一段时间比如至少10ms才能开始配置。verilog-ethernet里有一个ssio_phy_reset模块就是干这事的你只需要把它例化在顶层传入时钟和复位信号它会自动拉出一个定时复位脉冲给PHY。这个模块不要省我一开始偷懒直接用全局复位拉phy_reset结果PHY工作不正常坑了很久。还有一个小知识点PHY芯片一般都能通过MDIO接口配置内部寄存器来调整速率、是否自动协商、LED灯状态等。verilog-ethernet本身提供了MDIO驱动的例子但大部分情况下你不配置也能跑因为PHY默认会自动协商到千兆全双工。除非你的板子PHY默认配置不对才会遇到link起不来的情况。3. 核心模块拆解从PHY到UDP的这条“流水线”3.1 接收方向的四级模块协议栈说到底就是一条数据流水线。以接收方向为例数据是从网线进入PHY再进入FPGA的整个链条大概是这样的PHY芯片把RGMII信号还原成数字信号然后FPGA内部先经过eth_rgmii_rx模块。RGMII有一个挺反直觉的地方它在时钟的上升沿和下降沿都采样数据所以4根数据线rxd[3:0]在125MHz时钟下实际能传出1Gbps。eth_rgmii_rx的作用就是把这些时序信号用IDDR之类的原语转成普通的并行字节流并提取出控制信号。接下来是eth_mac_rx它处理链路层逻辑。以太网帧的开头是前导码和帧起始符中间是目的MAC、源MAC、类型/长度字段后面跟着payload最后是4字节CRC。MAC层接收模块会去掉前导码和FCS校验字段把剩下的有效数据整理成AXI-Stream流输出。走到这里数据已经是一个标准的以太网帧了。再往上走是ip_rx模块。它会检查这个帧是不是IPv4报文如果是就剥离IP头同时校验IP头校验和是否正确然后把TCP/UDP等上层协议的数据部分传下去。这里有个细节IP头长度不是固定的最少20字节最大60字节所以模块内部要解析IHL字段不熟练的话看代码很容易绕晕。最后是udp_rx模块。它检查UDP目的端口是否等于你配置的local_port如果匹配就剥离8字节的UDP头把用户数据通过一组AXI-Stream信号输出给你自己的逻辑。这样一路下来一个完整的以太网数据包最终变成用户逻辑可以直接消费的数据流。整个过程就像流水线工厂每个模块负责一道工序各干各的活。3.2 发送方向它其实只是收方向倒过来发送方向不要把它想得太复杂它就是接收的镜像过程。用户逻辑先把UDP payload写进udp_tx模块udp_tx帮你拼上UDP头包括源端口、目的端口、长度还有可选的校验和。然后数据进入ip_txIP模块会拼上IP头填上源IP、目的IP、协议号、总长度并计算IP头校验和。再往下是eth_tx它负责拼上以太网头包括源MAC、目的MAC、类型字段并且会计算整个帧的CRC32。最后的eth_rgmii_tx模块把并行数据转换成RGMII时序信号通过txd和tx_ctl引脚送到PHY芯片PHY再把它发到网线上。这里有个容易懵的地方MAC层怎么知道目的MAC地址这就要靠ARP了。当你的板子要给PC发UDP包时它只知道PC的IP地址不知道PC的MAC地址。IP模块会自动生成一个ARP请求问“谁是这个IP地址把你的MAC地址告诉我”PC收到后会回报ARP响应协议栈把响应里带的MAC地址缓存下来然后用它去组成太网帧。verilog-ethernet把这一步已经封装好了但你得知道自己板上MAC和IP是在哪个模块配置的别填错。3.3 模块间的接口信号AXI-Stream一封到底verilog-ethernet整个项目中模块和模块之间的数据接口统一采用AXI-Stream协议。这个协议可以说是现代FPGA开发里最常用的数据流标准了如果你之前没用过建议花半小时把它搞明白后面接触任何高速接口都用得上。AXI-Stream最核心的就是valid/ready握手。发送方置valid表示当前数据有效接收方置ready表示自己能接收当valid和ready同时为高这一拍的数据就算成功传输。除此之外还有tlast表示一帧数据的最后一个节拍tdata是数据总线tuser通常用来带一些辅助标志比如错误标志。// 一个最简单的AXI-Stream发送端示例 always (posedge clk) begin if (axis_tready) begin axis_tvalid 1b0; end else if (send_en) begin axis_tvalid 1b1; axis_tdata send_data; end end这套协议最大的好处是通用你在example里会看到UDP接收出来的信号是rx_axis_tdata、rx_axis_tvalid、rx_axis_tready而UDP发送需要的信号是tx_axis_tdata、tx_axis_tvalid、tx_axis_tready。名字高度统一只要你懂AXI-Stream整条流水线的连接就没有陌生感。3.4 回环前必须改的几个参数MAC、IP、PORT不用急着改代码先搞明白三个最关键的参数本机MAC地址、本机IP地址、UDP监听端口。在verilog-ethernet中这些通常以参数的形式出现在顶层模块或者各层模块的例化位置。MAC地址是48位的以太网要求它全球唯一但我们自己做实验完全可以用本地管理地址比如52:55:42:20:44:4e这样的值。注意这个写进代码里是一长串16进制数32位IP地址也类似。IP地址192.168.10.10在Verilog里通常写作32hc0a80a0a这种十六进制的表达方式很容易让新手看晕建议换算的时候心算一下每段十六进制值。端口号就是UDP端口你自己选一个高位端口就行比如1234、5000、8000避开公认端口。这里有个小坑数据在内存和网络字节序之间是高位在前还是低位在前的问题Verilog里拼接时要看清每个模块接口的说明否则你配置的IP地址和Wireshark里抓到的不一样。提示如果你临时改源码里的参数记住IP地址别和你的PC处于不同网段。最简单粗暴的做法是PC端设静态IP192.168.10.20板卡设192.168.10.10子网掩码255.255.255.0这样就不容易出问题。4. 实操5步把开发板变成一台能回显的UDP网卡4.1 第一步把源代码放进工程具体用Vivado还是Quartus看你的FPGA芯片决定我是用Vivado做的下面操作以它为例原理通用。新建一个空工程器件选对型号然后把下载下来的verilog-ethernet源码里所有用得到的.v文件全部添加进工程。千万不要自作聪明只添加几个核心文件因为模块之间依赖关系很多。最稳妥的做法是把lib目录下的整个源码都加进来然后用verilog的include路径机制去解析或者直接逐个Add Sources让综合工具自己去找。有些版本的源码里会有ipcore目录或者xci文件那是Xilinx平台相关的生成IP建议在example目录下找有没有自带的tcl脚本如果有就先跑一下工程脚本能少踩很多兼容性坑。4.2 第二步顶层例化与管脚约束接下来是最关键的一步把协议栈例化到你的顶层模块里。这一块我建议你不要自己从零写而是打开example目录里的顶层示例文件把它当作模板然后把和自己的板子相关的部分替换掉。一个典型的顶层连接关系大概是这样的// 伪代码具体模块名以项目example为准 udp_stack u_udp_stack ( .clk (clk_125m), .rst (rst_n), // PHY接口方向 .rgmii_rxc (phy_rxc), .rgmii_rx_ctl (phy_rx_ctl), .rgmii_rxd (phy_rxd), .rgmii_txc (phy_txc), .rgmii_tx_ctl (phy_tx_ctl), .rgmii_txd (phy_txd), // 用户数据方向 .rx_axis_tdata (rx_axis_tdata), .rx_axis_tvalid (rx_axis_tvalid), .rx_axis_tready (rx_axis_tready), .rx_axis_tlast (rx_axis_tlast), .rx_axis_tuser (rx_axis_tuser), .tx_axis_tdata (tx_axis_tdata), .tx_axis_tvalid (tx_axis_tvalid), .tx_axis_tready (tx_axis_tready), .tx_axis_tlast (tx_axis_tlast), .tx_axis_tuser (tx_axis_tuser) );当然具体模块名不同版本差异很大有些版本把RGMII接口封装到MAC层的FIFO里了有些还引出了MDIO接口。你以example里的顶层文件为准先保证连出来的模块至少能综合通过。管脚约束按手头板子的原理图写把PHY相关的引脚约束好时钟约束也不能省建议在XDC里对125MHz时钟做create_clock约束不然时序分析结果会误导你。4.3 第三步把接收和发送直接短接做回环第一次上板不要急着加自己的数据处理逻辑先做回环。什么叫回环就是把UDP接收模块输出的数据原封不动接回UDP发送模块的输入PC端发什么板子就回什么。// 最简单的回环收和发之间不加任何处理 assign tx_axis_tdata rx_axis_tdata; assign tx_axis_tvalid rx_axis_tvalid; assign tx_axis_tlast rx_axis_tlast; assert (tx_axis_tready rx_axis_tready) // 这里只是示意实际要处理反压但真正严谨的回环不能这么裸接因为发送端如果反压了接收端数据就会丢。所以最稳妥的做法是在中间加一个异步FIFO接收端一直往里写发送端从里面读。verilog-ethernet的example里一般自带这种回环结构你把顶层里用户逻辑替换成一根FIFO连接就行。编译下载之后观察板子上的PHY link灯如果网线插上后灯亮了说明物理link已经起来了这是第一个里程碑。4.4 第四步用网络调试助手做UDP回环测试PC端把网口IP设成静态IP固定到和板卡同一个网段比如192.168.10.20子网掩码255.255.255.0。注意如果电脑有两个网口一定要指定网口别让数据走无线网卡去了。打开网络调试助手这类工具协议类型选UDP本地IP设为PC的IP本地端口随便填一个比如6000然后设置远程主机为板卡的IP和端口比如192.168.10.10:1234。输入一串测试数据点发送。如果一切正常你会看到板子原封不动把数据回传回来。这个回传的成功意味着从PHY、MAC、IP、ARP到UDP整条链路全部通了也意味着你的FPGA开发已经进入“能上网络通信”的阶段了。提示如果回传数据没问题但Wireshark里看到PC发送的包还有大量重传可能是PC端的防火墙或者网络调试助手占用了端口关掉系统防火墙试试。4.5 第五步从回环改成“能干活”的版本回环只是证明协议栈工作正常接下来才进入正题。把你收到的rx_axis_tdata丢进自己的逻辑比如解析一个自定义的指令帧前两个字节是指令后面是数据或者写一个小型FIFO缓存把数据通过另一个串口输出出去还可以把收到的数据转成内部寄存器读写操作实现对板子的远程控制。这里以最典型的“收到UDP数据后点亮开发板LED”为例每收到一个完整的UDP包就把rx_axis_tlast拉高那一拍作为触发翻转一个LED计数器。把这个用户逻辑放在UDP RX和UDP TX之间你就能通过发网络包来控制硬件了。这一步建议边改边测每加一点功能就发一次UDP包验证一下避免一次改动太多导致问题不好定位。5. 排查实录我从这个工程里踩到的坑5.1 坑一PHY物理层根本没起来这个坑最隐蔽也最致命。我当初用了一个自定义板卡PHY芯片地址不是默认的0而是1当时没在意结果MDIO配置全部写到了错误地址上PHY的link灯死活不亮。后来拿着万用表量PHY的复位引脚才发现板上复位电路设计得不太好上电后有毛刺导致PHY一直在复位状态。所以物理层排查顺序应该是先看link灯亮不亮再看PHY复位引脚电平是否正常再看PHY的时钟引脚有没有时钟输入最后查PHY地址和接口模式配置。链路层调试最怕的就是跳过物理层直接查协议栈你会发现怎么查都是白忙。5.2 坑二RGMII时序问题导致“能link但不能通”RGMII的信号在DDR模式下时钟边沿和数据之间的建立保持时间非常紧张如果PCB走线较长或者板卡质量一般很容易出现“link灯亮但发包几乎全部丢”的现象。verilog-ethernet的eth_rgmii_rx和eth_rgmii_tx模块内部都考虑到了时序问题有些版本还提供了IDELAY参数供你调节输入延迟。如果你遇到丢包或者数据错乱尝试调整IDELAY的tap值或者确认PHY芯片的时钟相位是否偏移了90度。还有一个细节RGMII发送时钟txc到底是由FPGA生成还是PHY生成取决于板卡设计和PHY的配置。example里的默认连接不一定适合你手里的PHY这一步看原理图为准别想当然。5.3 坑三跨时钟域——协议栈工作在125MHz你的逻辑可能在任意频率很多新手把UDP收到的数据直接接给用户逻辑以为时钟都用了系统主时钟就没问题。但协议栈内部的时钟一般就是PHY的rxc125MHz。你的用户逻辑如果跑在50MHz或者100MHz系统时钟域两者直接连接必然出现跨时钟域问题轻则偶发数据错误重则寄存器出现亚稳态。正确做法是在协议栈和用户逻辑之间加异步FIFO。verilog-ethernet库里面本身就带了axis_fifo模块专门处理这种跨时钟域的AXI-Stream数据流。接收方向写时钟用125MHz读时钟用你的用户逻辑主时钟发送方向反过来。我实测下来不接FIFO直接回环在极短包下也能跑通但一旦数据量一大、时间一长必然出问题。跨时钟域的坑只有踩过几次才会长记性。5.4 坑四IP校验和和字节序的“魔法数字”你在源码里配置的local_ip是32hc0a80a0a看起来就是C0 A8 0A 0A这是网络字节序。如果你把这串数填反了Wireshark里看到的IP地址就变得怪模怪样而且PC端根本不会回复。IP头校验和的计算也有讲究verilog-ethernet的ip_tx模块是自动算校验和的不需要你操心但如果你自己修改了IP头结构比如加了VLAN标签那校验和就必须重新算。这部分如果出问题抓包时会看到Wireshark提示“incorrect checksum”能帮你快速定位。5.5 问题速查表现象排查方向link灯不亮PHY复位、PHY地址、PHY时钟输入、网线质量link灯亮ping不通检查ARP包是否正常检查local_mac/local_ip配置UDP包发出去但没回显看UDP目的端口是否匹配local_port检查回环FIFO是否正常偶尔丢包或数据错乱检查RGMII时序、IDELAY调节、跨时钟域FIFOPC收到包但内容变了检查PHY工作速率是否协商到千兆检查复位后MDIO配置Vivado时序不过检查时钟约束有没有写RGMII信号是不是要加false_palh约束我在实际项目里反复吃了不少亏最后总结一个经验碰到网络类问题一定要开着Wireshark抓包去看到底是哪一层断了。链路层有没有ARP回包、网络层有没有ICMP响应、传输层有没有UDP回显这一层层看下来问题基本腰斩一半。verilog-ethernet这个工程真的值得反复读。前几次用把它当黑盒调用就行后面你一定会遇到定制需求比如改成多端口UDP、加上用户校验、处理超大帧等这时候就需要回头把udp_rx、ip_rx这类模块源码啃透。从“能通”到“懂为什么通”之间只隔着一块开发板和一个抓包软件的距离。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →