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

资讯详情

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

FPGA网络通信设计实战:从UDP协议到RGMII接口的完整实现

FPGA网络通信设计实战:从UDP协议到RGMII接口的完整实现 做FPGA开发早晚会撞上这么一个问题板子上的数据怎么高效地送给电脑。刚开始觉得串口挺好用直到有一天客户说要“秒级回传”一张2MB的灰度图115200波特率算下来要三分钟当时我就沉默了。所以FPGA网络通信设计这个方向不是炫技是被需求逼出来的刚需。这一篇是part.7延续前面几篇的路子假设你已经从LED、按键、UART、SPI这些外设一路玩过来了会写一点Verilog也大概知道时序是什么东西。接下来这一篇我们把网口这根“大动脉”接上。会用到的关键内容有这么几块PHY芯片和RGMII接口怎么理解、UDP协议栈在FPGA里怎么落地、上板之后怎么用Wireshark和ILA抓问题。文章很长但每一步都是可复现的。这篇内容适合两类人一是想从零开始学FPGA、但一碰到“网络”两个字就发怵的初学者二是在项目里需要用FPGA做高速数据回传、却一直不知道从哪里下手的工程师。读完不敢说你能变成网络协议专家但至少能自己搭出一个“PC发UDP包-FPGA解析-FPGA回数据”的最小系统而且知道每一步出了问题该往哪查。1. 先想明白FPGA网络通信到底解决什么问题1.1 串口到网口一个分水岭前面几篇做的UART本质上是一个字节一个字节地搬115200bps差不多是每秒11KB。做简单传感器采集够用了但一旦数据量上了兆级串口就非常尴尬。举个例子一张640x480的8位灰度图大概300KB用串口传需要接近30秒换成千兆网口也就是3毫秒的事。这个数量级的差距直接决定了你的板子是“玩具”还是“系统”。更关键的是网络通信有一整套现成的生态。PC端抓包有Wireshark测速有iperf写脚本有Python socket调试手段比自定义私有协议不知道高到哪里去了。FPGA这个角色在网络通信里其实干的是“数据面”的活把网络报文解析出来、把数据塞进去、以线速转发出去。这个能力在数据采集、图像传输、仪器控制、板间互联这些场景下都是刚需。1.2 一个网络帧从PC到FPGA经历了什么要理解FPGA在网络里做什么先得建立一个整体画面。我用寄快递来类比你在PC上调用socket发送数据应用层的数据是“包裹内容”IP地址是“收件人地址”MAC地址是“快递站点编号”PHY芯片是“运输车辆”FPGA里的逻辑就是“分拣中心的分拣员”。实际操作中一个UDP报文从PC发到FPGA大致经历这些步骤PC应用层调用sendto数据进入内核协议栈。协议栈封装UDP头、IP头再通过ARP拿到目的MAC地址。网卡把MAC帧前导码帧头IP包FCS转成电信号发出去。板上的PHY芯片完成电平转换、时钟恢复把信号转成数字接口比如RGMII。FPGA从RGMII接口把数据收进来做CRC校验解析出IP和UDP头最后把有效载荷交给用户逻辑。注意一点我们常用的以太网帧在链路层有明确结构前导码7字节0x55SFD 1字节0xD5之后是6字节目的MAC、6字节源MAC、2字节类型、IP/UDP头和数据最后4字节CRC32。FPGA网络通信设计的核心工作就是把这些字节按规则解析出来再按规则拼回去。1.3 你要实现的是一个“够用”的协议子集很多人一听“网络协议栈”就头皮发麻觉得要把TCP/IP全栈都搬进FPGA。其实是两码事。FPGA里跑协议栈讲究的是“够用就好”不是把Linux内核搬过来。我们把网络功能拆开看PHY芯片已经帮你搞定物理层MAC层在FPGA里可以自己写也可以用IP核IP层在FPGA里只需要处理几件事解析头部、校验和、判断是不是发给自己的UDP层就更简单了端口对上就把数据拿走。这些加起来一个中等规模的FPGA工程师一周左右能搞定。下面这个表是我常用的“协议实现范围”对照表供你规划时参考功能是否实现理由ARP请求/应答必须PC发包前要查MACARP不通则UDP包根本发不出来ICMP Ping应答推荐调试利器ping通基本等于链路通了IP组包/拆包必须识别EtherType0x0800解析IP头UDP收发必须绝大部分高速数据流场景用UDPTCP按需跨网段/可靠性要求高的场景才需要纯逻辑实现很重DHCP可选固定IP调试更省事产品化再考虑动态获取2. 动手前先选型PHY、MAC、协议栈三层分清楚2.1 PHY芯片与RGMII接口板上那颗负责物理信号的芯片叫PHY常见型号有瑞昱RTL8211系列、Marvell 88E1512、Micrel KSZ9031等。PHY负责的事情很多编码、电平转换、时钟恢复、自动协商甚至还要管理网线状态。FPGA通常不直接处理模拟信号而是通过标准接口跟PHY打交道。这里重点讲RGMII因为它几乎是现阶段FPGA开发板上的标配引脚少、速率高、时序模型清晰。RGMII的数据线只有4根TXD、4根RXD比GMII的8根少一半靠的是DDR技巧在时钟上升沿传低4位、下降沿传高4位合起来才是一个完整的8位字节。需要理解的核心时序参数千兆模式GTX_CLK125MHzTXD在时钟上下沿各传4bit等效速率1000Mbps。百兆模式CLK25MHz等效速率100Mbps。十兆模式CLK2.5MHz具体由PHY决定其实很少用。在实际FPGA工程里发送侧用的gtx_clk通常由FPGA内部的MMCM/PLL产生同时送给PHY做TX_CLK保证时钟同源。接收侧rx_clk则由PHY恢复出来送给FPGA作为接收采样时钟。这个时钟的方向关系新手特别容易搞混画原理图或者写约束的时候要看清楚。2.2 MAC层自己写还是用IP核MAC层是FPGA网络设计里绕不开的主体发送时把IP包封装成以太网帧、加上前导码和CRC接收时做相反的工作。这个问题“自己写还是用IP核”不同阶段有不同答案。Xilinx官方有Tri-Mode Ethernet MACTEMACIP核配合gmii_to_rgmii IP能帮你把RGMII物理接口跑起来。优点是成熟稳定、支持各种速率自适应缺点是不透明出了问题你不知道到底是IP核的问题还是你自己的问题调试起来像隔着一层雾。我的建议分两步走如果是第一次做网络设计先用官方IP核把链路跑通确认PHY没问题、时钟没问题、Wireshark能抓到正确的帧等基本盘稳了再尝试自己写一个极简MAC。别一上来就从头造轮子——硬件调试里变量越少越好否则卡了三天你可能都不知道问题出在PHY还是你自己写的CRC算法。自己写MAC也不是很难核心逻辑就是状态机发送侧从IDLE状态开始依次发前导码、SFD、帧头、数据、FCS最后留帧间隔接收侧检测SFD后开始收数据边界校验CRC错帧直接丢弃。难点集中在时序约束和CRC计算这两块后面单独展开讲。2.3 协议栈为什么优先UDP而不是TCP这个话题值得单独拿出来说因为几乎所有初学者都会问为什么FPGA例程里都是UDP没有TCP答案很简单TCP在FPGA里非常难做。UDP是一种无连接协议一个UDP包发出去就不管了不确认、不重传、不保证顺序。FPGA只要解析出端口号把数据放进FIFO就算完成任务状态机十几行就能写完。TCP则是一个有连接、有流量控制、有拥塞控制、有重传机制的复杂协议。完整的TCP状态机有十几种状态还要维护序号、确认号、滑动窗口纯逻辑实现的工作量至少是UDP的十几倍而且性能还未必好。什么场景必须上TCP两个一是跨越复杂的三层网络经过路由器、防火墙NAT和ACL策略基本不鸟UDP裸奔二是业务本身对丢包零容忍比如远程控制指令。如果真到了这一步我的建议是别在纯逻辑里硬刚TCP直接用ZYNQ的ARM核跑LwIP协议栈或者用MicroBlaze软核跑LwIP。FPGA做数据面吞吐ARM做控制面协议各干各的比在Verilog里攒TCP状态机划算得多。3. 三条技术路线怎么选3.1 纯逻辑路线开销小、延迟最低先用一张图感受下纯逻辑路线的全貌文字版接收路径RGMII RX - IDDR解出8bit字节 - 检测SFD - 对帧做CRC校验 - 解析MAC/IP/UDP头 - 把有效载荷写入FIFO - 用户逻辑读取。发送路径用户逻辑写FIFO - 组合并填充MAC/IP/UDP头 - 发送状态机按帧格式逐字节发送 - 计算CRC并附加 - ODDR把8bit转成DDR - RGMII TX。这条路线的关键词是“线速”和“确定性”所有逻辑都在FPGA内部完成没有处理器取指、中断、协议栈调度的开销延迟可以做到微秒级甚至更低。数据采集、高速ADC回传、图像传感器输出这类应用几乎都是这个方案。但代价是实现工作量最大而且一切调试手段都得自己造。对于part.7的定位我建议你把纯逻辑路线作为学习主线——因为只有亲手写完一遍你才知道网络帧每个字段到底是怎么来的。3.2 软核路线MicroBlaze LwIP如果需求不是纯粹的“数据搬运”而是夹杂着配置管理、参数计算、状态上报这类控制逻辑可以考虑在FPGA里放一个MicroBlaze软核跑一个精简版的LwIP协议栈。好处是协议栈用C语言改起来方便支持TCP、DHCP、HTTP这些复杂协议开发效率高。代价是性能天花板明显。MicroBlaze本身是通用处理器架构跑LwIP单线程处理千兆网口实际吞吐能到几百Mbps就算不错了而且会占用大量Block RAM和LUT。适合数据量不大、但交互逻辑复杂的场景比如仪器控制面板。3.3 硬核路线ZYNQ的PS LwIPZYNQ这类带ARM硬核的芯片是另一种选择ARM跑Linux和LwIPPL侧可以接高速RGMII/SGMII接口或者做DMA搬运。这套组合的好处是生态完整TCP/UDP随便用文件系统、网络服务都能跑起来坏处是要接触嵌入式Linux学习成本一下子拉高很多。对初学者来说ZYNQ方案最大的价值其实在“以后”。你会在很多产品里看到ZYNQ做网络通信但如果你现在连纯逻辑RGMII都没调通直接上ZYNQ反而容易迷失在Linux内核配置和设备树里。先把纯逻辑的基础打牢后面再迁移到ZYNQ你会觉得很多东西是相通的。3.4 我的选择建议如果是学习底层原理、想把网络搞明白选纯逻辑哪怕是简化版MAC也要自己写一遍。如果目标是快速出活做一个带网口的小工具用IP核自研简化UDP协议。如果目标是做正式产品、需要稳定可靠又复杂的网络协议上ZYNQ不要犹豫。part.7接下来的内容我按纯逻辑路线来展开因为这条路上的知识点最密集学会之后另外两条路线都顺理成章。4. 核心模块实现从零写一个UDP发送链路4.1 UDP帧到底长什么样动手写代码之前先把帧结构刻在脑子里。一个完整的UDP帧在以太网上的样子是字段长度字节内容示例说明前导码70x55 x7用于接收端时钟同步SFD10xD5帧起始定界符目的MAC60xAA BB CC DD EE FF本机MAC或广播源MAC60x00 0A 35 01 22 33自己的MAC类型20x0800IPv4帧IP头20见IP协议版本、长度、IP地址等UDP头8见UDP协议源端口、目的端口、长度数据N你的有效数据0~1472为凑最小帧长可能要填充FCS4CRC32对目的MAC到数据结束计算这里有个很容易踩的坑以太网规定最小帧长是64字节从目的MAC到FCS如果IP总长度小于46字节MAC层要填充0到46字节。也就是说UDP载荷只有10字节时帧尾会有大段的padding接收端不能按帧长去截数据而要用IP头里的“总长度”字段知道真正有效的字节数。举个具体的例子一个发送“Hello”的UDP帧不含前导码FF FF FF FF FF FF 00 0A 35 01 22 33 08 00 45 00 00 21 00 01 00 00 40 11 00 00 C0 A8 01 0A C0 A8 01 64 04 D2 1F 90 00 0D 00 00 48 65 6C 6C 6FIP头里0x00 2133就是20字节IP头8字节UDP头5字节“Hello”。UDP头里0x00 0D138字节UDP头5字节数据。之后填充到46字节才算完最后附4字节FCS。建议新手在纸上手写一次这个十六进制串感受一下字段是怎么拼接的比任何教程都管用。4.2 校验和的计算方法UDP帧里有两处校验IP头校验和与UDP校验和。计算方法统一是“16位反码和的补码”步骤并不复杂把需要校验的数据按16位2字节一组切分。不够一组的前面补0。所有16位数相加加法过程中溢出的进位回卷加到低位这叫“回卷”。最终结果取反得到校验和。IP头校验只覆盖20字节IP头先把校验和字段本身置0然后按上面的算法算一遍结果填回去。UDP校验有点不同它除了UDP头和数据还要加一个“伪头部”伪头部的内容是源IP、目的IP、协议号17和UDP长度目的是防止IP层转发错目标。实操中如果你的FPGA接收端不检查UDP校验和PC通常也能正常收很多开发板例程干脆把UDP校验和字段填0照样能跑。但要注意有些路由器、防火墙会丢弃UDP校验和为0的报文。建议还是花半小时把校验和算法写对图个安心。4.3 发送状态机的设计与实现接下来是重头戏发送状态机。我写过一个极简版本核心状态如下localparam S_IDLE 3d0; localparam S_PREAMBLE 3d1; localparam S_HEADER 3d2; localparam S_PAYLOAD 3d3; localparam S_PADDING 3d4; localparam S_CRC 3d5; localparam S_IFG 3d6; always (posedge clk) begin if (rst) begin state S_IDLE; end else begin case (state) S_IDLE: if (tx_req !fifo_empty) begin // 把构造好的帧头加载到移位寄存器 load_header(); state S_PREAMBLE; end S_PREAMBLE: begin // 先发7字节0x55再发1字节0xD5 if (send_cnt 7) data_out 8hD5; else data_out 8h55; if (send_cnt 8) state S_HEADER; end S_HEADER: begin // 逐字节发送MAC/IP/UDP头 data_out header_buf[head_idx * 8 : 8]; if (head_idx 13) state S_PAYLOAD; end S_PAYLOAD: begin if (!fifo_empty) begin data_out fifo_rd_data; if (payload_cnt total_len) state S_PADDING; end end S_PADDING: begin data_out 8h00; if (pad_cnt pad_len - 1) state S_CRC; end S_CRC: begin // 输出计算好的CRC32共4字节 data_out crc_buf[crc_idx * 8 : 8]; if (crc_idx 3) state S_IFG; end S_IFG: begin // 帧间隔至少12字节96ns if (ifg_cnt 11) state S_IDLE; end endcase end end这个状态机的关键设计是提前把MAC/IP/UDP头拼好放在header_buf里状态机里只用顺序取字节而不是在case里现拼字段。这样做的好处是状态逻辑简单、时序容易收敛改协议头也只需要改拼表的那个模块。还有一个必须强调的点帧间隔IFG不能省。IEEE 802.3规定两帧之间至少12字节间隔千兆下就是96ns。如果你连续发帧不给间隔很多交换机或者PC网卡会直接丢帧现象就是“Wireshark能抓到但应用层收不到”排查起来很隐蔽。我的开发板实测IFG给到16字节更稳带宽损失可以忽略。4.4 接收链路的几个关键点接收方向比发送复杂一些因为你是被动接收无法控制对端什么时候发数据。核心流程是用IDDR解出RGMII的8bit数据按字节拼接。检测SFD后面就是完整的MAC帧。过一遍CRC32校验错误直接丢弃。解析目的MAC不是本机也不是广播直接丢掉。判断EtherType0x0800再解析IP头验证版本4和目的IP。看协议号UDP则解析端口号ICMP则回Ping应答ARP则回ARP应答。把有效载荷写入FIFO同时产生一个“帧有效”脉冲。接收侧最容易出问题的是“背压”如果用户逻辑消费数据的速度跟不上FIFO就会满这时候要有机制丢弃新帧或者暂停写入。比较稳妥的做法是FIFO深度至少能容纳两个1500字节的帧同时把FIFO的prog_full信号反馈到接收状态机满了就不收下一个包。很多初学者的丢包问题十有八九是FIFO深度不够或者没有背压处理。5. 调试三板斧仿真、ILA、Wireshark5.1 先把仿真跑通再上板上板之前千万不要跳过仿真。我见过太多人直接综合下载然后被RGMII时序问题折磨到怀疑人生。前仿真也许无法完全覆盖PHY的模拟行为但至少能把你的状态机逻辑错误暴露出来。建议的Testbench结构是生成125MHz时钟模拟PHY的rx_clk用task函数构造带随机数据的UDP帧送入接收逻辑同时驱动发送请求观察发送侧TXD和TX_CTL的时序。这样你能在波形里看到状态机是否正确经过PREAMBLE、HEADER、PAYLOAD、CRC这些状态。仿真时用一个技巧打印关键信号变化。比如当状态变化时用$display打印当前状态和发送计数这样比肉眼盯波形快得多。跑通仿真后上板问题就会集中在“实际时序”而不是“逻辑功能”。5.2 上板第一步先跑通ARP不要直接测UDP这是我自己踩过最大的坑。第一次上板我直接写UDP发送结果PC端Wireshark什么都收不到。查了半天才发现PC要往FPGA发UDP需要先知道FPGA的MAC地址如果ARP请求没人应答PC认为目标不可达应用层的数据根本不会丢到网卡。所以上板之后的第一个网络调试命令永远应该是ping。配置好PC的静态IP比如192.168.1.100/24开发板IP设为192.168.1.10连上网线先看FPGA能不能应答ARP请求。在Wireshark里过滤arp你应该能看到PC发出的ARP Request和FPGA回的ARP Reply。看到Reply说明底层的MAC接收、MAC发送、CRC、时钟全部正常这比直接测UDP省心太多。如果ping不通优先检查这几项开发板PHY的复位时序是否正确MDIO能不能读到PHY寄存器。网线接的是不是千兆口交换机/PC网卡是否协商到千兆。目的MAC和源MAC是不是填反了源IP和目的IP是不是填到了对方的字段。5.3 Wireshark的正确打开方式Wireshark是调试网络通信的第一神器。几个常用点说一下。过滤条件是基本功udp.port5000看指定端口eth.addrxx:xx:xx:xx看指定MACarp或icmp看协议类型。抓包的时候建议把网卡设置为混杂模式否则可能过滤不到。如果帧发出了但Wireshark提示“Bad CRC in frame”问题几乎可以锁定在MAC发送侧CRC计算错误、字节顺序错、或者CRC字段没有对齐到帧尾。如果提示“UDP checksum offload”或者“未验证”不一定代表错误很多网卡驱动会偷懒不做UDP校验和。还有一个高级技巧用Wireshark的“Expert Information”窗口分析-专家信息能自动汇总异常帧比如短帧、CRC错误、重传等全网卡扫一遍比逐条翻包高效得多。5.4 ILA逻辑分析仪怎么用才高效上板调试时ILA是你的“第二双眼睛”但用不好反而会添乱。这里有几个实操经验。ILA的采样时钟一定要选对。如果是看接收路径优先选PHY输出的rx_clk作为ILA时钟如果是看发送路径选FPGA的gtx_clk。时钟选错ILA采到的数据全是乱的看起来像时序问题其实是你采样用的时钟不对。触发条件也要设置得有意义不要只设一个“always true”。比如想看CRC错误就把ILA的触发条件设为CRC_ERROR信号的上升沿想看UDP解析成功就设为UDP_DONE的上升沿。这样抓到的一帧数据才是你真正关心的。另外一个建议是ILA深度不要贪大。4096深度对调试MAC层协议完全够用太深反而因为布线资源紧张导致采样时钟跑不到125MHz得不偿失。6. 常见问题与排查实录6.1 综合排障速查表我把实际项目中积累的排障经验整理成一张表按“现象-原因-排查方法”排列基本覆盖了FPGA网络通信开发80%的卡壳场景。现象可能原因排查方法PC ping不通开发板ARP没应答MAC/IP填错CRC错导致ARP包被丢弃Wireshark过滤ARP看有没有Reply核对帧头字段Wireshark看到发送帧但报CRC错误发送状态机CRC计算或输出顺序错先仿真CRC模块ILA抓发送侧核对CRC的4字节顺序UDP数据能到FPGA但回程不通回程目的MAC没配对ARP缓存过期Wireshark看ARP请求FPGA回ARP同意后再测UDP低速不丢包高速就丢FIFO深度不够背压没处理加深FIFO到2KB以上加prog_full反馈帧长度异常Wireshark提示short frame最小帧长填充没做或IP总长度字段错检查发送填充逻辑确认IP头长度字段正确时好时坏重新编译后表现不同RGMII时序没约束好PVT变化导致采样点漂移加input/output delay约束必要时用IDELAY连接速率协商到100M而不是1000M网线质量差PHY自动协商失败换网线用MDIO读写PHY寄存器看link状态6.2 RGMII时序约束才是隐藏的大坑如果逻辑功能全对、仿真也过了但上板后时好时坏问题大概率是RGMII的时序约束没有做好。125MHz的DDR接口数据窗口只有几百皮秒没有正确的输入输出延迟约束Vivado综合出的电路时序报告会一片红但仿真又看不出来因为仿真里没有真实的延迟。这里给一个最小可用的XDC约束模板# 时钟约束 create_clock -name gtclk -period 8.000 [get_ports gtclk] # 输入延迟RX方向从PHY到FPGA引脚 set_input_delay -clock gtclk -max 2.0 [get_ports {rxd[*] rxc}] set_input_delay -clock gtclk -min 0.5 [get_ports {rxd[*] rxc}] # 输出延迟TX方向从FPGA引脚到PHY set_output_delay -clock gtclk -max 2.0 [get_ports {txd[*] txc}] set_output_delay -clock gtclk -min 0.5 [get_ports {txd[*] txc}]具体的max/min值要从PHY芯片手册里查。如果懒得算可以先用Xilinx官方的gmii_to_rgmii IP它内部已经把IDELAY和约束都处理好了。等你自己写了RGMII逻辑再回来琢磨这些数值不迟。还有一个偷懒但有效的方案很多PHY会输出一个125MHz参考时钟给FPGA如果能把它作为发送时钟的参考配合MMCM的相位调整多数情况下能避开最麻烦的输入延迟问题。这个方法不是标准做法但作为快速验证链路非常管用。6.3 带宽和帧大小这两个实际问题调通了网口之后很多人会关心一个问题我的FPGA网络带宽到底能跑多高这里有个经常被忽略的计算以太网的协议开销是固定的但帧大小对效率影响巨大。算一笔账每帧要额外承担前导码8字节、IFG 12字节加上MAC头14字节和FCS 4字节合计38字节固定开销。如果帧的载荷是1500字节有效利用率是1500/(1538)≈97.5%。如果帧的载荷只有64字节有效利用率是64/(6438)≈62.7%。如果发的是40字节的UDP探测包利用率更是低得可怜。所以做吞吐测试时一定要用大帧测。一个常用的测试方法是PC端用Python写一个循环发送1500字节的UDP包到FPGAFPGA收到后原样打回PC端统计一段时间的总字节数除以时间就是单向吞吐。实测在千兆模式下纯逻辑UDP回环能做到900Mbps以上剩下的带宽全被帧间隔和帧头开销吃掉了。如果你的应用是“大量小帧高频发送”那么真正限制速率的是帧间隔而不是带宽这时候考虑合并多个小数据到一个UDP包性能提升会非常明显。最后分享一个小技巧这篇文章写到这里主线内容已经讲完了。最后我再分享一个自己在做网络调试时的习惯永远在代码里留一个“调试寄存器区”比如把收到的帧计数、CRC错误计数、ARP请求计数、UDP帧计数都挂上一组只读寄存器用简单的串口或者片上逻辑分析仪就能查看。这样遇到问题的时候第一步不是抓波形而是先看这些计数器有没有异常跳动往往能迅速定位问题出在收路径还是发路径。FPGA网络通信设计本身是个很大很深的领域但入门的关键就两步先把一个UDP帧彻底搞懂再亲手把收发链路调通。做到这两步你已经能应对绝大多数数据采集和传输场景了。下一步如果还想深入可以考虑往高速接口方向走比如PCIe、SGMII、JESD204B这些你会发现底层思路其实是一样的。
返回列表