1. 项目概述:为什么要在FPGA里折腾UDP通信
做FPGA通信的同学肯定都有过这种经历:调通了串口觉得太慢,想上以太网,查了一堆资料发现全是在讲PC端的Socket编程,真正讲FPGA侧怎么把Triple-Speed Ethernet IP核跑起来的文章少得可怜。我去年接手一个数据采集项目,前端ADC采出来的数据要通过千兆网实时上传到上位机,带宽需求大概在700Mbps左右。串口肯定不行,USB方案又要挂处理器,最后定了纯FPGA实现UDP卸载引擎这条路——也就是标题里说的,用ALTERA(现在叫Intel)的Triple-Speed Ethernet IP核配合自研UDP协议栈做高速数据传输。
这个方案非常适合以下场景:数据采集卡、软件无线电前端、图像采集与传输、工业现场实时控制。它解决的核心问题是在不依赖CPU和操作系统的情况下,让FPGA直接以线速收发以太网报文,把UDP的打包、校验、拆分全部用硬件逻辑完成,延迟可以压到微秒级甚至亚微秒级。
1.1 先搞清楚TSE IP核到底是干啥的
Triple-Speed Ethernet(简称TSE)IP核是Intel FPGA官方提供的以太网MAC软核,支持10/100/1000Mbps三档速率。它内部实现了完整的MAC层功能,包括帧的封装与解析、CRC32校验、流控(pause帧)、VLAN标签处理等。它的对外接口分两路:一路是数据通路,采用Avalon-ST接口;另一路是寄存器配置通道,采用Avalon-MM接口。
很多新手会混淆的一个点是:TSE IP核只到MAC层,不包含PHY芯片,也不包含TCP/IP协议栈。它管的是OSI七层模型里的第二层,也就是数据链路层。三层以上的事情——IP地址、端口号、UDP校验——全部要你自己在FPGA逻辑里写出来。这既是这个IP核的痛点,也是它的灵活之处:不需要的协议层完全可以砍掉,只保留必要的硬件逻辑,资源占用和延迟都能做到最优。
1.2 方案选型:TSE IP核 vs 现成协议栈芯片
市面上实现FPGA以太网通信其实有三条路:第一条是直接用TSE IP核自己写UDP协议栈,也就是本文要讲的方案;第二条是挂一个WIZnet W5500这类硬件协议栈芯片,通过SPI接口访问,芯片帮你把TCP/UDP都处理好了;第三条是用带ARM硬核的SoC FPGA(比如Cyclone V SoC),在Linux里跑标准Socket。
三条路线对比下来:W5500方案开发最快,但带宽上限只有100Mbps左右,而且多一颗芯片成本摆在那;SoC方案功能最全,但实时性受操作系统调度影响,不适合硬实时场景;TSE IP核方案前期开发量大,但跑满千兆线速没问题,延迟完全可控,而且IP核本身是免费的(部分器件要License,不过Cyclone系列一般直接能用)。
我当时选TSE IP核还有一个重要原因:项目后续要扩展多通道以太网,一片FPGA上可能要跑4路千兆MAC。TSE IP核例化多个实例很方便,资源占用也清晰可预估,这在纯硬件方案里是天然的优势。
2. TSE IP核配置实战:逐项拆解Quartus里的选项
打开Quartus(我现在用的是Quartus Prime Standard 20.1),在IP Catalog里搜“Triple-Speed Ethernet”,会看到“Triple-Speed Ethernet Intel FPGA IP”。双击之后进入配置界面,这里面的选项密密麻麻,但真正影响后续开发的核心配置其实就那么几项,我一个个说。
2.1 速率与接口模式:GMII、RGMII还是SGMII
第一个要选的是MAC与PHY之间的接口类型。常见的选项有:
GMII:并行接口,发送和接收各8根数据线,加时钟和控制线,总共大概24根信号。1000Mbps时时钟125MHz,100Mbps时时钟25MHz,10Mbps时2.5MHz。优点是时序简单,调试方便;缺点是引脚太多,布线压力大。
RGMII:源同步DDR接口,数据线减到4根,时钟125MHz,上升沿和下降沿各采4位。引脚省了一半,但对PCB布线等长要求更高,而且FPGA侧要处理DDR采样,逻辑上稍微麻烦一点。
SGMII:串行接口,用FPGA的高速收发器(transceiver)和PHY通信,速率1.25Gbps(含8B/10B编码开销),物理层上相当于一根差分对。引脚极少,EMI特性好,也是目前工业千兆PHY的主流接口。
我这次用的是SGMII接口,原因是板子上选的PHY芯片是TI的DP83867IR,只支持SGMII。这里有个容易踩的坑:SGMII有两种配置方向——TSE IP核既可以把SGMII PCS做在IP内部,也可以只提供MAC逻辑,把SGMII/PCS功能留给外部。在TSE IP核里,如果选择了“SGMII”作为与PHY的接口,实际上IP核内部已经包含了PCS层,你需要外接的只是物理层PHY芯片。
值得注意的是,热搜里提到“SGMII IP核与PHY芯片一起使用时,应配置成MAC模式”,这说的就是TSE IP核充当MAC角色,PHY芯片工作在PHY模式。两者通过SGMII接口对接,中间不需要其他逻辑。如果反了——比如把TSE也配成PHY模式——两边都是PHY,谁发时钟、谁做自协商就全乱了,链路根本不可能协商起来。
2.2 Avalon-ST数据通路配置
数据通路是整个TSE IP核的命脉。配置界面里会让你选数据接口宽度,有8位和32位两种(对应发送侧还有64位选项,不过一般用32位足够)。这里建议直接选32位,因为32位接口在千兆速率下时钟125MHz,逻辑时序压力小很多。如果选8位,千兆时数据时钟会跑到125MHz的4倍即500MHz,这在普通FPGA逻辑里非常难收敛时序。
还有个关键选项是“Enable Magic Packet”和“Enable Wake-on-LAN”,这两个是做远程唤醒用的,普通项目直接关掉。流控(Flow Control)选项里,如果做UDP传输且不跑TCP,建议把流控也关掉——UDP本身不保证可靠传输,流控只会增加额外逻辑,而且某些交换机配置不对的时候,PAUSE帧反而会把链路搞死。
2.3 MDIO管理接口配置
MDIO(Management Data Input/Output)是MAC管理PHY的串行接口,用来读写PHY的寄存器,配置速率、自协商、环回测试等。TSE IP核里有一个MDIO控制器,通过Avalon-MM接口访问。配置界面里会让你填MDIO时钟分频,这个要根PHY芯片的规格来。DP83867的MDIO最高时钟是2.5MHz,而MDC时钟由系统时钟分频得到,我当时系统时钟是125MHz,分频系数填了50(125/50=2.5MHz)。
MDIO这一块很多人会忽略一个细节:TSE IP核有一个功能叫做“MDIO与内部寄存器共用Avalon-MM地址空间”,默认情况下MDIO的命令寄存器、数据寄存器会映射到IP核的寄存器空间里,你直接通过Avalon-MM总线读写这些地址就行。但要注意,不同版本的TSE IP核,MDIO寄存器偏移地址不一样。我用的IP核版本里,MDIO命令寄存器偏移0x90,数据写寄存器偏移0x94,数据读寄存器偏移0x98,PHY地址通过命令寄存器高位指定。如果发现读PHY寄存器读不出正确值,先查一下IP版本对应的寄存器手册,别上来就怀疑硬件。
2.4 时钟与复位:最容易翻车的地方
TSE IP核的时钟体系看起来简单,实则细节很多。它主要有以下几组时钟:
Avalon-ST收发时钟:也就是数据通路时钟,千兆模式下125MHz,100M模式25MHz,10M模式2.5MHz。这个时钟是TSE MAC根据配置自动从GMII/SGMII时钟域转换后输出的,你不用自己切换,但要注意它在你配置速率变化时会跟着变。
寄存器配置时钟:一般接100MHz或125MHz,用于Avalon-MM寄存器读写。
PHY接口时钟:SGMII模式下,PHY接口时钟来自FPGA的高速收发器参考时钟,通常125MHz。
复位方面,TSE IP核有独立复位和整体复位的区别。IP核会输出一个clk_ref_status信号,如果参考时钟没锁住,这个信号会拉低,这时候即便你复位了IP核也不工作。排查时钟问题时,第一步永远是看clk_ref_status,第二步才是看复位。我见过好几个同事调了一天TSE,最后发现是收发器参考时钟没给对,clk_ref_status一直是0。
3. UDP协议栈的FPGA实现:从帧格式到状态机
IP核配置好只完成了一半工作,真正的大头是UDP协议栈的硬件实现。别被“协议栈”这三个字吓到,UDP是一个极其精简的无连接协议,相比TCP那堆状态机、重传机制、窗口管理,UDP在FPGA里实现起来要轻松得多。
3.1 先搞清楚报文长什么样
要做UDP协议栈,脑子里必须时刻记住三种帧格式:以太网帧、IP报文、UDP报文。它们是一层套一层的关系。
以太网帧由14字节头部加数据加4字节FCS组成。头部依次是6字节目的MAC、6字节源MAC、2字节以太网类型。咱们发的是IPv4 UDP报文,所以以太网类型填0x0800。这里特别强调一下:这14字节的数据在TSE IP核发送时,是要你自己拼好给它的,IP核不会帮你加以太网头——它默认你给的数据就是完整的MAC帧(从目的MAC到负载)。
IP报文头标准是20字节,关键字段有这么几个:
- 版本号和头部长度:一个字节,IPv4固定为0x45
- 总长度:2字节,IP报文总长,包含IP头本身
- 协议号:1字节,UDP填17(0x11)
- 源IP、目的IP:各4字节
- 首部校验和:2字节,只校验IP头,注意不是全包校验和
- 还有标识、标志、片偏移这几个字段,我们做单包传输,不分片,所以标识可以随便填个自增数,标志字段填0x4000(禁止分片)或者直接填0
UDP头8字节:2字节源端口、2字节目的端口、2字节UDP长度(UDP头加负载的长度)、2字节校验和。UDP校验和是可以不做的——IPv4协议里UDP校验和是可选的,全0表示未计算。接收端如果校验和字段为0,直接跳过校验。在UDP卸载引擎里,特别是发送方向,为了省逻辑资源,完全可以把校验和填0。但接收方向呢?我强烈建议还是做一下UDP校验和解析里的校验判断——因为上位机发下来的包如果被网卡硬件计算了校验和,你没有校验逻辑容易把错误数据当正常数据处理,调试排查的时候会非常恼火。
3.2 发送通路:打包流水线
发送侧的核心思路是:应用层数据进FIFO,TSE MAC从FIFO读出数据时,我们在前面拼接以太网头、IP头、UDP头,同时在数据末尾追加FCS由MAC硬件自动计算添加。
我用的是Altera官方推荐的发送时序:Avalon-ST发送接口有一个sof(开始帧)信号和eof(结束帧)信号,配合valid/ready握手。发送流程如下:
第一步,在sof拉高的第一个周期里,往sop(start of packet)标志里写入帧控制字。这里的“帧控制字”是TSE特有的一种前置控制信息,32位数据里高16位是帧长度(从以太网目的MAC开始算,不包含CRC),低16位是各种控制位。这里有个巨坑:TSE IP核对帧最大长度的默认限制是1518字节,也就是标准以太网帧长(14头+1500负载+4CRC)。如果你发的UDP包负载超过1472字节(1500-20IP头-8UDP头),MAC会把超长的帧直接丢掉。解决办法是在Avalon-MM寄存器区里把MAX_FRAME_LENGTH寄存器改大,比如改成2048或者更大,否则用户数据稍微多一点就发送失败。
第二步,紧接着帧控制字之后,发出以太网目的MAC、源MAC、类型字段0x0800。这些数据用连续几个时钟周期依次送出。
第三步,发IP头。IP头20字节,可以预先在逻辑里生成好,除了总长度、源IP、目的IP和校验和以外其他字段都是固定值。IP校验和使用的是累加和法——把所有16位字相加,进位回卷,最后取反。这个计算可以用组合逻辑在几个周期内流水完成,也可以查表法,但为了通用性建议直接用加法树。
第四步,发UDP头。UDP长度等于IP总长度减20,也就是8加负载长度。端口号是配置寄存器里的值,可以随时改。
第五步,负载数据从FIFO读出,一路直通到Avalon-ST数据线上。此时要把IP总长度和UDP长度这两个值在FIFO读指针走到末尾时计算出来回填到包头里。这里有个工程技巧:我们是在负载通过的时候用计数器统计实际字节数,统计完后再重发一个包的时候填到包头里。所以你看到的实现里有一个“长度寄存器更新逻辑”和“首包丢弃/重试逻辑”,处理不好就会出现第一包长度错的、后面包长度对的情况。我推荐一个更稳的办法:应用层在写FIFO的时候,往一个单独的sideband FIFO里写入本次数据包的字节数,协议栈从sideband FIFO提前读到长度,这样就不存在回填滞后的问题了。
第六步,MAC内部自动计算并追加CRC32,发送结束。
3.3 接收通路:从线速里把UDP头挑出来
接收方向比发送麻烦,因为你面对的是满速到达的数据流,没办法“先缓冲再处理”,必须在线(in-line)完成解析。
TSE IP核的Avalon-ST接收接口输出的是完整以太网帧(不包含CRC,CRC已经被MAC验证并丢弃,只通过一个status字告诉你好坏)。接收数据流里,sof标志对应目的MAC的第一个字节,eof对应帧最后一个数据字节。接收方向要做这样几件事:
第一,验证MAC帧状态。TSE IP核在每个帧结束时,会送出一个16位的status字,包含CRC错误、帧过长、截断错误等标志。收到任何错误标志,直接把这一帧丢弃,数据不进应用FIFO。
第二,以太网类型判断。从目的MAC开始数14个字节,之后是2字节类型字段。如果这个值不是0x0800(IPv4),整帧丢弃。
第三,IP头校验和验证。如果源IP是PC发来的,PC网卡硬件生成的IP校验和肯定是对的,但保险起见还是验一下。校验和验算就是整个IP头按16位累加,结果应该等于0xFFFF(因为校验和字段本身是取反存入的,加一起刚好全F)。我见过某些驱动写的IP校验和是错的,尤其是一些老网卡,所以接收侧做一次校验还是值得的。
第四,UDP端口匹配。只有目的端口等于配置值的包才接收。注意UDP端口是2字节,大端序,FPGA侧要注意字节序转换。
第五,负载写入FIFO,并附带长度信息给应用层。这里要留心一帧内的总字节数,如果大于我们内部FIFO的容量,会出现截断,需要设置一个“帧长超限”标志,直接把帧丢掉,避免给上层一个半截数据包。
接收侧还有个大坑是“帧间空隙”和背压。TSE IP核的Avalon-ST接收接口如果下游ready拉低,MAC会暂停接收,但PHY/SGMII链路还在源源不断进数据,这会导致MAC内部FIFO溢出,溢出时MAC会丢弃当前帧并置一个溢出标志。所以你的接收侧处理逻辑必须保证足够的吞吐——说白了就是,读FIFO的速度必须大于等于写FIFO的速度。千兆线速下,纯接收是125MB/s,而一个简单的解析状态机加FIFO读写,在FPGA里做到这个速度毫无压力,但前提是不要让锁存逻辑、长组合链路拖后腿。
3.4 ARP协议处理:第一次联调前必须解决的绊脚石
很多第一次调试UDP的人会卡在一个诡异的问题上:FPGA发的UDP包,用Wireshark能看到,但上位机网络调试助手就是收不到。原因十有八九是ARP没处理。
PC的TCP/IP协议栈有个脾气:它要发任何IP包给一个目标前,会先查ARP缓存表。如果缓存里没有目标的IP到MAC映射,它会先发一个ARP请求,对方回了ARP应答,把映射关系记录到缓存里,然后才肯发那个真正的UDP包。问题来了——如果你的FPGA不处理ARP,PC发ARP请求来问“谁是192.168.1.10”,你没有任何响应,PC的缓存表里永远不会有你的MAC地址,于是PC发给你的UDP包就一直卡在ARP阶段。
解决办法是在FPGA里加一个极简的ARP处理器:收到一个请求帧,如果目的IP是我们自己的IP,就发一个ARP应答,把自己的MAC地址填进去。ARP应答帧长42字节(14以太网头+28 ARP数据),字段都是现成的,把ARP请求帧里的发送方MAC和IP,和目的方MAC和IP对调,操作码从1(请求)改成2(应答),源MAC改成我们自己的MAC,其他照抄即可。
我见过有人图省事,在PC上手动加静态ARP表项,命令是arp -s 192.168.1.10 00-11-22-33-44-55。这种方法调试时可以,但换台电脑、重启一次就丢了,不可能作为量产方案。老老实实写个ARP处理模块,大概30行状态机的活,别偷懒。
4. 上板调试与网络联调:从灯不亮到跑满带宽
所有代码写完、综合通过,只是万里长征第一步。真正的噩梦从烧录开始。
4.1 调试环境搭建:Quartus、USB-Blaster和常见驱动事故
开发工具方面,ALTERA FPGA现在统一用Quartus Prime。Cyclone IV/V/10系列用Quartus Prime Standard或者Lite(Lite只支持部分中低端器件),Arria/Stratix系列要用Pro版本。我项目里用的是Cyclone V GT,Quartus Prime Standard 20.1就够了。
调试器是USB-Blaster,这里就绕不开热搜里的那个词“altera usb-blaster代码39”。代码39是Windows设备管理器里的一种错误状态,表示设备驱动加载失败。我遇到了不止一次,绝大多数情况是驱动签名问题——Windows 10/11强制驱动签名,而Altera的老版驱动(特别是Quartus 13.0及之前自带的)没签名,系统直接拒绝加载。
解决办法有几个:第一个是右键驱动文件inf,选择“安装”,如果还不行就要在高级启动选项里禁用驱动程序签名强制;第二个是换新版本Quartus自带的USB-Blaster驱动,新版驱动是签名过的;还有个常见坑是用了山寨USB-Blaster,这种克隆版驱动和官方不一样,需要装卖家提供的替代驱动,装的时候注意选“WinUSB”模式,而不要选“JTAG”模式。另外用USB 3.0口连接的时候偶尔出现识别不了的情况,换到USB 2.0口或者换个Hub口往往就好了——别问我为什么,硬件兼容性的玄学问题,实测有效。
4.2 网络调试助手联调实录:必须抓包的双端口思维
FPGA侧写好了UDP发送逻辑,接下来就是和PC联调。我的做法是:PC上装两个工具,一个是Wireshark,一个是网络调试助手(我用的是NetAssist,国人写的挺好用,当然其他类似工具也行)。
第一轮测试先把板子和PC直连,不用交换机。以太网直连是交叉线还是直通线,现在大部分网卡都支持自动翻转,所以市面上买的成品网线基本都能直连。FPGA板上自带的是RJ45座加网络变压器,连着PHY DP83867。
直连后Wireshark选择一个关键选择:抓包时选对网卡。很多人电脑有多个网卡,虚拟机虚拟网卡、无线网卡、蓝牙网卡一堆,选错网卡什么都抓不到。抓包看到FPGA发的UDP包后,先看三个点:以太网类型是不是0x0800,IP头长度和校验和是否正确,UDP校验和字段是否填了0但被Wireshark标红。有些网卡有硬件UDP校验和卸载功能,可能把FPGA发来的、校验和为0的UDP包直接丢弃。这种情况在Windows上比较少见,Linux上遇到过,解决方法是把PC网卡属性里的“接收校验和卸载”关闭。
4.3 常见问题速查表:联调时我遇到过的坑
调试过程我整理了这份速查表,基本覆盖了90%的新手问题:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 网线插上,PHY链路灯不亮 | PHY芯片供电/复位/时钟异常 | 量PHY的1.0V/2.5V供电,检查复位引脚时序,检查25MHz晶振 |
| 链路灯亮,但PC显示网络未识别 | PC和FPGA之间ARP不通 | 抓包看有没有ARP请求发出,FPGA侧看是否有收到帧 |
| 能收到ARP请求但不回ARP应答 | ARP处理模块状态机问题 | SignalTap抓接收通路的数据,看ARP帧是否被正确解析 |
| 收到UDP包但负载数据全错 | 字节序颠倒 | 检查Avalon-ST数据的字节序;SGMII接口有10/100M与1000M的字节序差异 |
| 偶尔丢包,频率随机 | 接收FIFO溢出或者UDP长度超限 | 看TSE的status字里溢出标志,加大FIFO,或者检查帧长寄存器配置 |
| 发送速度上不去,只有几Mbps | 握手效率低或者FIFO深度不够 | 检查Avalon-ST的ready信号拉低占空比,优化流水线减少气泡周期 |
5. 性能调优与项目经验复盘
UDP通信调通只是第一步,真正体现功力的地方在于传输效率和稳定性。这一部分我总结几条实实在在的经验。
5.1 吞吐率优化:深挖Avalon-ST握手的每一个气泡
千兆线速即125MB/s的裸数据速率,看似很快,但如果你在Avalon-ST接口上处理不好,实际吞吐可能只有几十MB/s。带来吞吐损失的几个关键因素:
帧间隔时间:以太网标准要求帧与帧之间至少要留96比特时间的间隔(即12字节的IFG)。千兆下这个间隙是96ns,这是协议层面的硬性要求,IP核已经处理了。但如果你在发送侧手动插入额外的等待周期,吞吐就会下降。我见过有人写发送状态机时多加了一个空闲时钟周期,千兆直接掉了近1%的吞吐,可别小看这一点——当你要跑满带宽时,每个周期都得省。
FIFO几乎满/空阈值:如果发送FIFO的几乎满信号配置得太保守,比如水位线设到90%就停止写入,那么实际可用FIFO容量只有90%,当应用层突发数据稍大,FIFO满后握手反压,吞吐就会降。建议把almost_full阈值调到95%以上,并对上层做适当的突发块大小控制。
跨时钟域低延迟设计:如果FPGA内部是200MHz逻辑时钟,而TSE数据通路是125MHz,中间用异步FIFO桥接是自然的做法。我建议异步FIFO的读侧和写侧都采用“show-ahead”模式(即first-word fall-through),可以有效降低一拍的读延迟。
用这些优化后,我实测的单路UDP发送吞吐可以到999Mbps(占线速的99.9%),接收方向也能跑到980Mbps以上,主要损耗还是来自协议帧头本身的开销。
5.2 资源占用与关键路径优化
一个32位Avalon-ST接口的TSE MAC核,在Cyclone V上大概占用2000-3000个ALM(自适应逻辑模块),加上你自己的UDP协议栈(不含FIFO)大概1500-2500个ALM。整体下来,单路千兆UDP方案在Cyclone V上占用的逻辑不到10%,资源非常充裕。
时序方面最容易出问题的路径是状态机里的帧长度计数器——它是从帧开始一直计数到帧结束的组合加法链。如果FIFO读出的数据位宽是32位,而你要统计的是字节数,这个字节计数器累加逻辑的位数会很长(16位以上),而且是每个时钟周期都要更新。优化方法是把加法拆成流水级,或者用两个计数器分摊:一个算32位字个数,另一个算尾字节余数,最后在帧尾处组合算出总字节数,这样关键路径能短不少。
5.3 稳定性验证:别只测一包数据
调通UDP之后,稳定性验证是决定项目能不能交付的关键。我建议做三类测试:
第一类是长时间满负荷灌包测试,用上位机以最大速率连续发UDP包超过24小时,统计丢包率。FPGA侧要设计一个包序号字段——每次发送把序号加1,上位机端检查序号连续性,就能精确统计丢包。这个测试能暴露FIFO边界问题和异步FIFO空满标志的亚稳态bug。
第二类是速率阶梯测试,从1Mbps开始,逐级加到1000Mbps,每档跑5分钟记录丢包率。这个能找到系统在某个中间速率下的特殊问题——比如时钟切换瞬间的状态机跑飞。
第三类是热插拔测试,反复拔插网线,验证PHY从掉线到重新建链的长链路恢复逻辑。这里有个工程细节:PHY掉线时,TSE IP核内部的收发时钟可能会停振或者产生毛刺,你得通过PHY的link状态寄存器轮询来检测链路状态,一旦发现链路断开,主动复位TSE IP内部所有状态机,并丢弃所有未发完的帧。否则链路恢复后,状态机可能停留在上一帧的中间状态,导致后续所有帧都错位。
我测试中还遇到过一个问题:长时间运行后偶发一帧“坏状态”,status字里出现CRC错误。排查后发现是PHY芯片的SGMII接口偶发误码,导致MAC接收侧CRC校验失败。解决办法有两条:一是看PHY有没有内置的FEC或者重传机制(很多PHY没有);二是应用层做重传——UDP协议本身不允许重传,但应用层可以加确认包机制,发现丢包就请求重发。这就是为什么很多工业以太网协议虽然跑在UDP上,但上层都会再加一层自己的可靠传输算法。
5.4 字节序问题:调试时最隐蔽的敌人
整个TCP/IP协议栈的网络字节序是大端(Big-Endian),也就是高位字节在前。而FPGA里常用的Nios II软核、内部FIFO、SRAM等默认是小端存储。很多第一次做以太网的工程师,会对着一堆看起来像是“错位”了的数据发呆。
举个例子:发送一个UDP端口号0x1388(十进制5000),在以太网帧里的字节序应该先是0x13,再是0x88。如果你在设计逻辑时,把Avalon-ST接口的32位数据直接按小端方式拼接,那发出去就会变成0x88 0x13。要知道数据在传输线上并没有“大小端”之分,大小端只存在于处理器的内存视图里,所以你必须在发数据进入TSE之前,把字节顺序调整成“以大端顺序在线上依次出现”。TSE IP核本身不负责这个转换,它的数据线上你给什么,它就按什么顺序发送出去。
解决办法一般是在协议栈输出的32位数据上做字节重排,比如把d[31:24]和d[7:0]交换。这个逻辑写起来容易,调试起来难,最好的办法是从头就在代码里明确注释好每一字节的位置,别等到板上调试才来猜。
6. 写在最后的几句实在话
TSE IP核的UDP通信方案,我前前后后做了快两个月才完全跑稳。回头看去,真正花时间的不是IP核配置,而是那些文档里永远不会写明白的细节——字节序、ARP处理、帧长限制、PHY链路状态恢复。这些东西网上资料零散,官方手册写得像是给熟手看的摘要,每个坑都得自己踩一遍。
如果你正准备做类似的项目,我建议路径是这样的:先用最简单的GMII接口配合一个便宜的RTL8211 PHY调通UDP通路,把协议栈逻辑验证熟了,再切换到SGMII接口或者换更高端的FPGA。上来直接搞SGMII,一旦出问题,你根本分不清是协议栈的bug还是串行收发器没配置对,排错难度直接翻倍。
另外,Quartus自带的SignalTap逻辑分析仪一定要用熟练。我调试TSE IP核内部信号时,几乎全靠SignalTap看Avalon-ST接口上的sof/eof/valid/ready时序,以及MDIO总线上读写PHY寄存器的波形。没有SignalTap的话,靠肉眼在板子上量引脚,等于是蒙着眼睛走路。
后续如果你想把方案升级,可以考虑的方向包括:加入TCP协议栈(代价是逻辑资源和开发时间翻好几倍)、做多通道MAC聚合、实现DMA对接PCIe接口、用TSE IP核的VLAN功能做数据流分类。路还长,但第一步——把UDP跑起来——走通了,后面都是水到渠成的事。