从串口调通开始接触网络通信,很多人会误以为只是换了个传输媒介,结果一上手就被MAC地址、IP校验、帧间隙、时序收敛这些概念按在地上反复摩擦。说实话,串口和网口之间的思维差距,比UART和AXI之间的差距还要大:串口是字节流,你只需要关心波特率;网口是数据帧,你要同时处理位同步、字节对齐、CRC、跨时钟域、背压控制、还有上层协议语义。我当初从近似0基础开始学FPGA,前几个部分都在做按键、流水灯、串口回环,到了part.7想做网络通信,才发现前面那些经验只能让我看懂波形,不能让我直接写出一套能跑的MAC层。
这篇文章就是给正在走同一条路的人准备的。我会从硬件平台、协议分层、UDP收发通路搭建、调试方法一直聊到后续向TCP/光口/PCIE扩展的思路。内容偏工程实操,涉及Xilinx 7系列平台,但架构思路同样适用于其他厂商的FPGA。无论你是刚学完时序约束,还是已经开始看Tri-Mode Ethernet MAC的文档,这篇都会有一点参考价值。
1. 从串口调通到网口调通,为什么很多人的第二步比第一步痛苦
1.1 串口思维与网络协议的维度差异
我见过很多同学把串口那套思路搬到网络上:写个状态机把数据一位一位发出去,然后看上位机有没有收到。串口确实是这样,时钟来了就移位,数据从TX脚出去,对端按波特率采样,没有帧的概念,没有地址,没有校验之外的任何协议负担。
但网络通信不是这样。以太网帧有明确的结构:前导码、帧起始定界符、目的MAC、源MAC、EtherType、负载、FCS。这些字段不是你想省就能省的。很多人第一次写MAC层发送逻辑,觉得“我只是要把几个字节发出去,为什么要凑够46字节最小帧长”,于是写出来的帧在交换机上直接被丢弃,或者在抓包里看到一串奇怪的短帧。这就是没有理解协议是分层约定的,不是你自己的传输规则。
到了IP层更麻烦。你要处理IP头校验和、分片、TTL,还要搞清楚本机IP和对端IP存放在哪里。很多初学设计直接把这些地址硬编码在RTL里,改一次IP就要重新综合一次工程,虽然能跑通,但离“设计”还有距离。UDP层相对简单,源端口、目的端口、长度、校验和,字段少,是初学者切入网络通信的最佳切入点。
1.2 我用RGMII初次调试失败的经历
我第一次调试RGMII接口,用的是Xilinx Artix-7加一颗国产PHY芯片。RGMII这个名字看起来只是“Reduced Gigabit Media Independent Interface”,但真正调起来才发现它把时钟和数据的关系压缩到了极限:DDR双沿采样、TXD和TXCTL要和对齐到时钟中心、发送时钟由FPGA输出、接收时钟由PHY恢复。这些细节直接决定你能不能跑起来。
我当时犯的第一个错误,是把RGMII的TX_CLK当普通时钟来约束,没有做Output Delay约束,结果链路偶尔通偶尔不通。更典型的问题是,PHY芯片在复位后需要一段时间才能完成自协商,很多人复位释放完立刻开始发数据,PHY还在IDLE状态,自然一个包都发不出去。后来我养成一个习惯:复位之后至少等个几十毫秒,或者干脆用状态机轮询PHY的状态寄存器,确认Link Up以后再启动收发逻辑。
这问题虽然最后查出来只是时序约束和复位时序,但排查过程花了我整整一个周末。回头想想,这不只是技术问题,更是思考方式的问题:串口调不通,大概率是代码逻辑问题;网口调不通,可能是PCB布线、PHY配置、时序约束、时钟相位、FCS格式任何一个环节的问题。把这种多维排错思维建立起来,才算真正进入网络通信设计的门槛。
1.3 对“近似0基础”的建议路径
如果你问我从零开始最省力的路径是什么,我的建议是:不要一上来就自己写完整MAC,先拿厂商IP核或开源MAC核跑通一条收发回路,把RGMII的时序和PHY的行为弄清楚,再考虑把内部逻辑换成自己的UDP/IP实现。部分参考设计会让你误以为“调通了”就是“理解了”,所以跑通之后一定要做两件事:一件是把抓包波形对着协议文档逐字段分析,另一件是把PHY寄存器的配置逐项查手册搞懂。这两件事做完,你就有能力自己动手改造数据通路了。
2. 硬件平台与PHY选型:RGMII/SGMII背后的信号完整性问题
2.1 PHY芯片与FPGA的物理连接
网络通信设计的底层是物理层,FPGA这边通常只做MAC层以上的逻辑,PHY负责把数字信号变成差分模拟信号送到网线上。这个分工决定了你的硬件设计里必须有一颗PHY芯片,常见的有RTL8211、KSZ9031、YT8521之类的千兆PHY,接口模式有MII、RMII、RGMII、SGMII可选。
对FPGA入门设计者来说,RGMII是平衡复杂度与性能的选项。它用4根数据线加1根时钟线加2根控制线,就能跑千兆。但这4根数据线在1Gbps速率下等效频率是125MHz,DDR模式下每个时钟沿要传两个bit,一个周期传4字节,时序预算非常紧张。相比之下,MII只有100Mbps,时钟25MHz,时序宽松得多,所以也有人建议新手先从MII起步,至少能把“能不能通信”和“信号质量好不好”分开考虑。
我个人的观点是,如果你手头开发板已经是RGMII了,没必要退回去换MII。RGMII的困难主要在约束和调试,但调试方法一旦掌握,后面再做SGMII、光口会顺畅很多。千万不要在RGMII上跑不通就怪PHY芯片,大部分问题都出在FPGA侧的输出时序和PHY配置上。
2.2 7系列FPGA的Bank电压、时钟和复位设计
选PHY还要考虑接口电平是否匹配。7系列FPGA的HP Bank支持1.5V、1.8V等,HR Bank支持1.8V到3.3V,而很多千兆PHY的RGMII接口是2.5V或3.3V电平。开发板上一般已经做好电平转换,但自己画板子就得特别小心:Bank电压没选对,IO时序根本没法收敛,甚至可能烧IO。
时钟设计方面,RGMII有两种模式。一种是PHY提供125MHz参考时钟,FPGA通过MMCM/PLL生成125MHz发送时钟,这种方案对时钟源质量要求高。另一种由FPGA提供25MHz或125MHz时钟给PHY,PHY内部PLL生成125MHz,这种对FPGA这边更主动,但要注意PHY的时钟输入范围。无论哪种方式,时钟引脚最好连接到FPGA的MRCC或SRCC引脚,才能保证时钟树质量。
还有一个容易忽略的细节是PHY的复位信号。很多PHY要求复位脉冲宽度大于10ms,复位释放后还要等待时钟稳定和自协商完成。如果FPGA逻辑里只是简单拉高复位脚,不给足够时间,PHY会处于未就绪状态。我习惯把PHY复位放在FPGA复位管理模块里,用计数器产生一个100ms的低脉冲,然后等PHY的中断或状态寄存器报告Link Up。
2.3 信号完整性:走线、电平、上拉
硬件层面,RGMII的4根数据线加TX_CLK、TX_CTL,长度尽量等长,误差控制在50mil以内。对于入门设计,做不到严格等长也不用过度焦虑,因为125MHz信号大概对应60英寸的波长,日常的走线偏差不会直接让通信失败,但最好还是遵循这个原则,能给调试省很多麻烦。
PHY芯片的LED引脚、复位引脚、时钟引脚通常有内部上拉下拉配置。上电前建议核对数据手册里的strap pin默认状态,因为很多PHY是靠这些引脚在复位后决定工作模式的。我一个朋友调试时发现PHY始终工作在100Mbps,查了很久才注意到一个mode strap引脚被外部电阻拉错了电平,导致芯片自动降低了工作速率。这个坑在RGMII调试里非常典型,遇到速率不对、接口不通,第一反应应该就是去看PHY寄存器或者strap配置。
另外,网络变压器的中心抽头接法、RJ45的屏蔽接地、差分对的100欧姆终端匹配,这些直接影响物理层能不能稳定建链。这些偏硬件的知识往往不在FPGA教程里出现,但它是“网络通信设计”无法绕开的一环。如果你用的是现成开发板,这部分不用管;你自己画板子,一定把这几个点检查清楚再上电。
3. 协议处理路径:硬件逻辑实现UDP/TCP的分层设计
3.1 为什么不在FPGA里直接跑协议栈
聊到协议路径选择,有人会问:为什么不直接在FPGA里放一个软核或硬核CPU,然后跑LwIP或者Linux协议栈?这个方法确实存在,而且对于功能复杂、业务灵活的产品是合理选择。它本质上是“用CPU的灵活性换取开发效率”,在FPGA里跑软核处理ARP、ICMP、UDP,遇到协议版本升级改软件就行。
但这里有个概念必须分清楚:如果是Zynq这种带ARM硬核的平台,跑Linux当然可以;但如果是纯FPGA如Artix-7、Cyclone V,所谓“跑LwIP”通常意味着例化一个MicroBlaze或Nios II软核。软核跑协议栈会占用大量BRAM和逻辑资源,数据吞吐也受限于CPU频率和总线带宽。做工业控制、数据采集这种低吞吐场景没问题,但做视频传输、高速数据采集场景,硬件逻辑实现协议路径才是主流。
硬件实现协议栈的缺点是开发周期长,调试困难,知识点杂。优点是确定性极强:不依赖操作系统调度,每个时钟周期做什么,逻辑都是固定的。用硬件逻辑做UDP,并不是说你必须从零开始写CRC、写校验和,而是你要理解这些功能块怎么组合在一起,最终形成一条从用户数据到网线的完整通路。
3.2 MAC、IP、UDP三个模块的职责划分
我用硬件逻辑实现UDP收发,会把它分成三个清晰层次:
- MAC层:负责以太网帧的组装与解析,包括前导码、FCS校验、帧间隙、冲突检测(半双工模式下)、流控帧处理。
- IP层:负责IP报文的封装与解封装,处理源IP/目的IP、协议字段、IP首部校验和。
- UDP层:负责端口路由和数据包长度计算,UDP校验和可选,但建议实现,避免数据被静默丢弃。
设计时可以采用流水线结构:发送方向是“用户接口 -> UDP封装 -> IP封装 -> MAC封装 -> RGMII”;接收方向相反。每一层只关注自己的头部字段,不关心上层内容。
关键一点是,各层必须能独立被测试。在搭建完整通路之前,我会先单独测MAC层:直接构造一个以太网帧,丢进发送方向,用抓包工具或ILA看波形,确认前导码和FCS没问题,再往上加IP层。逐层搭建,出了问题定位起来会快很多。
3.3 跨时钟域处理与FIFO设计
网络通信设计里很难绕开跨时钟域。PHY恢复的RX_CLK和FPGA内部用户逻辑时钟往往不是同源时钟,发送方向也有TX_CLK和用户时钟的跨越。处理不好,会出现偶发的数据错位或丢包。
我常用的做法是异步FIFO。发送方向,用户逻辑把UDP负载写到FIFO,MAC发送状态机从FIFO读出数据并封装成帧;接收方向相反,MAC解析完帧后把负载写入FIFO,用户逻辑异步读走。每个FIFO的读写侧分别用自己的时钟,配合空满标志进行背压控制。
背压控制是很多初学容易忽略的点。网络接口不能像串口那样“我发完就完事”,因为PHY和交换机的处理速度是固定的,如果你读FIFO的速度跟不上,数据就会溢出。设计里必须考虑当FIFO满时,发送状态机是暂停发送还是丢弃包,接收方向是继续接收还是反压给上游。最简单的处理是FIFO快满时拉高暂停信号,但这样可能会引入额外的延迟,具体取舍要看你应用对时延的敏感度。
4. 一步一步搭出UDP收发通路(可复现的工程过程)
4.1 核选型:Xilinx Tri-Mode Ethernet MAC 还是手写RTL
搭建完整UDP通路,第一步不是写代码,而是选型。Xilinx 7系列提供Tri-Mode Ethernet MAC(TEMAC)IP核,支持10/100/1000Mbps,内部包含GMII/RGMII接口转换、FIFO、MDIO管理接口。用它最大的好处是省去大量MAC层兼容性细节,缺点是IP核的接口和时序理解起来需要时间。
我的建议是:如果你是第一次做网络通信,用TEMAC作为MAC层是个稳妥选择,把精力集中在UDP和IP逻辑上。但不要完全当黑盒,至少要把IP核的配置界面打开,看看你选的接口模式、FIFO深度、是否使能FCS发送/接收校验,这些直接影响后面逻辑怎么写。
如果你手头没有这个IP核授权(部分厂商需要购买),也可以选择开源MAC核,比如OpenCores上的ethmac,或者自己写一个简化版MAC。自己写MAC初学做起来比较吃力,但在调试过程中反而能把MAC帧格式理解得很透彻。我身边不少朋友是先用TEMAC跑通,再回头手写简化MAC,两条路都值得走一遍。
4.2 收发数据通路的状态机设计
我用一个简化例子说明发送数据通路的状态机设计,假设要发送一个UDP包:
发送状态机状态可以定义为:
- IDLE:等待用户写入请求,收到发送脉冲后跳转到PRE。
- PRE:发送8字节前导码和SFD。
- MAC_HDR:发送目的MAC、源MAC、EtherType(0x0800)。
- IP_HDR:发送IP头20字节,包含版本、首部长度、TTL、协议、源/目的IP、IP首部校验和。
- UDP_HDR:发送UDP头8字节,源端口、目的端口、长度、校验和。
- PAYLOAD:从FIFO中逐字节读出用户数据。
- PAD:如果负载不足46字节,需要填充到46字节。
- FCS:发送4字节CRC32。
每一步的字节数、字段值、发送顺序,都要对着以太网帧格式核对。实际写状态机时,我习惯用一个字节计数器统一控制,每来一个TX_CLK就计数一次,这样状态切换的逻辑很清晰,也不会出现某个状态提前退出或漏发字节的问题。
接收方向是发送的逆过程:先检测前导码,然后解析MAC头,判断EtherType是否为IP,再解析IP头判断是否为UDP,最后把负载写入FIFO。接收链路里最重要的是一定要做CRC校验:FCS不对的帧必须丢弃。很多人第一次做接收时发现上位机收到的数据莫名多出几个字节,就是因为MAC层没有正确识别帧结束位置,或者把CRC也当用户数据交给了上层。
4.3 时序约束:从时序违例到收敛
RGMII设计的成败很大程度取决于时序约束。我们通常需要约束三类路径:
- 发送时钟到RGMII输出引脚的路径,使用set_output_delay,保证数据相对时钟的建立保持时间符合PHY要求。
- 接收时钟到内部寄存器的路径,使用set_input_delay。
- 内部跨时钟域FIFO的异步路径,使用set_false_path或set_clock_groups处理。
我建议调试时先用Vivado的时序报告确认约束是否生效。一个简单方法:布局布线后看Post-Implementation Timing Summary里有没有RGMII接口相关的时序违例。如果提示RGMII信号上的延时不满足,优先调整输出延迟约束,而不是盲目去改逻辑。
时序违例还有一个隐蔽来源:RGMII的TXD、TX_CTL、TX_CLK三者的对齐关系。吉比特模式下,TX_CLK是125MHz并且DDR沿采样,很多设计者直接把TX_CLK接到ODDR原语输出一个时钟,但数据路径可能没有经过同样的输出寄存器设置,导致相移不一致。我一开始也是在这上面栽过跟头,后来直接在TEMAC示例设计里看它对ODDR和约束的处理方法,仿照那种写法才收敛。
4.4 板级回环与上位机验证
硬件调试验证是我认为整个环节里最考验耐心的一步。推荐顺序如下:
- 先用FPGA逻辑把发送和接收直接回环,不经过PHY,确认内部通路正确。
- 再通过PHY回环模式(很多PHY支持digital loopback)确认RGMII接口时序没问题。
- 最后接外部网线,用PC抓包工具验证FPGA发出的UDP包是否被正确接收。
- 从PC发送UDP包,FPGA接收,再通过串口或ILA观察数据是否正确。
上位机工具选择很多,Wireshark抓包能看到帧的每个字段,适合检查协议格式;自己写个Python脚本发包收包,则适合反复压力测试。用一个Python脚本往FPGA发10000个UDP包,FPGA接收后把收到的包数量返回给PC,对比有没有丢包,这是我最常用的验证方式。无论是哪个环节出问题,都建议先打开Wireshark看帧内容,而不是猜。
还要提醒一点:Windows防火墙可能默认拦截UDP广播包,或者网卡驱动会过滤非本机MAC的帧。遇到包发不出去,不用急着怀疑FPGA逻辑,先检查PC网卡和防火墙设置。这类“非FPGA侧”问题是网络通信调试中的常见噪音。
5. 从UDP到TCP,再到万兆光口和PCIE:进阶路线的取舍
5.1 TCP状态机为什么比UDP难一个量级
UDP跑通以后,大概率会想实现TCP,因为很多上位机通信都是TCP。但TCP比UDP难在状态管理,远不止加几个头字段那么简单。
TCP有三个核心问题:可靠传输、流量控制、拥塞控制。这意味着你要在硬件里实现序列号管理、确认应答、超时重传、滑动窗口、慢启动、拥塞避免。这些东西在CPU上写协议栈只是调函数,但在FPGA里要变成有限状态机。一个最简单的TCP发送状态机,要处理SYN、SYN-ACK、ACK、FIN这些标志位的交互,还要处理对端乱序包、重复ACK、超时重传,状态数往往几十个。我见过不少项目直接用软核CPU做TCP控制面,用硬件做数据面加速,这种混合架构是比较务实的方案。
对于入门者,我不建议一上来就全硬件实现TCP。可以先做ARP、ICMP(Ping)和UDP,把基础打牢;TCP可以先用软核跑LwIP,等对协议有了整体把握再考虑把数据面改成硬件。这条路虽然绕了一点,但最终做出来的系统更可靠。
5.2 光口通信与SGMII的差异
如果项目要求更高带宽或远距离传输,会用到光口或SGMII。SGMII本质是串行接口,速率1.25Gbps,通过SerDes引脚连接PHY或光模块。它和RGMII最大的区别是并行转串行,信号不再一堆线并行,而是走高速差分对,时序约束也变成Gigabit Transceiver的约束方式。这部分内容对新手跨度较大,但对做过RGMII的人来说是自然的进阶。
做光口通信时,需要考虑的不只是协议,还有物理层的光模块管理:SFP的I2C接口、LOS信号、TX_FAULT信号、模块是否在位。FPGA侧通常不用自己处理PCS层,直接用Silicon Image或Xilinx的1G/2.5G Ethernet PCS/PMA或SGMII IP核即可。
如果直接跳到万兆光口,比如10G Ethernet,那逻辑设计会再上一个台阶:数据宽度从8位变成64位,时钟从125MHz变成156.25MHz,MAC层要支持64B/66B编码,处理逻辑的流水线复杂度显著增加。没有千兆的基础,直接上10G会很痛苦。
5.3 与PCIE、DDR等技术的结合
网络通信设计最终通常不是孤立的,往往要和PCIE、DDR、图像处理等模块协同。比如做数据采集卡,数据从ADC进来,通过网络发到上位机,可能需要DDR做大缓冲,PCIE做控制通道,FPGA网络部分只负责数据搬运。这种场景下,网络模块只是整个数据通路里的一环,带宽匹配变得很关键。
一个常见问题是,DDR带宽是够的,但UDP模块的处理带宽不够,导致数据在缓存里堆积。这时候你可能需要把MAC和DDR之间的FIFO深度加大,或者采用多队列调度策略,把不同优先级的数据分开处理。调试这种系统时,单纯用Wireshark看网络包还不够,还要统计FPGA内部的丢包计数、FIFO水位,形成一套从源头到目的端的完整监控链路。
PCIE和网络结合还有一个很有意思的方向:RDMA。虽然FPGA上的RDMA实现复杂,但它解决的问题是CPU拷贝开销和高延迟,在存储和高性能计算里很有价值。这块技术栈偏深,建议先把PCIE基本读写跑通,再研究ROCE协议。
最后说几个我在网络通信设计里攒下来的经验
第一点,调网络通信,不要一口气把代码写完再上板。先把发送链路做到“固定包长、固定内容、循环发送”,抓包确认全对以后,再改可变包长,再改成FIFO加载数据。每前进一步都做一次回归验证,能帮你精准锁定问题出现的位置。
第二点,学会看PHY的寄存器。很多不稳定现象,比如偶发丢包、链路自动降速、CRC错误,都是PHY芯片工作异常导致的,光看FPGA内部逻辑根本定位不到。MDIO接口其实很简单,花半天时间写一个寄存器读写模块,调试时会救命。
第三点,不要把MAC层FCS校验和IP首部校验和搞混。MAC层FCS是CRC32,覆盖整个以太网帧;IP首部校验和是16位求和校验,只覆盖IP首部。这两个字段在调试时经常被同时怀疑,搞混了会浪费很多时间。
FPGA网络通信这条路,难度在于它是一个交叉领域:要有FPGA时序设计能力,要懂硬件信号完整性,还要理解网络协议栈。单一知识背景都不够,但它们之间有清晰的学习路径。从串口到MAC,从MAC到UDP,从UDP到TCP,每一层解决一个具体问题,脚踏实地上台阶,最后回头看,你会发现这个part.7真正教会你的不是网络协议本身,而是一套从物理世界到数字世界的完整思考框架。