
做FPGA做到第七篇LED、按键、串口这些基础玩法玩腻之后很多人会把目光转向网口。毕竟开发板上最显眼的高速接口就是那个RJ45看起来就比串口高级。但一动手才发现FPGA网络通信完全不是串口那种随便拉两根线就能收发的概念什么PHY、MDIO、RGMII、CRC、ARP、UDP校验和一堆名词直接压过来。网上资料又散得不行要么是纯理论讲TCP/IP七层模型要么是甩给你一个几十万行的开源IP核新手根本不知道从哪里下手卡一个月没进展的比比皆是。这篇就把FPGA网络通信这条路从头拆一遍。目标读者是近似0基础、但已经会Verilog基本语法、会点灯会串口的朋友。我会尽量不绕弯子告诉你哪些东西必须自己写哪些直接用厂商IP就行以及最容易被忽略的时序和联调问题。1. FPGA网络通信的整体设计思路别一上来就硬啃协议栈很多人拿到网口第一反应是“我要用Verilog实现TCP/IP”然后去搜Linux内核协议栈源码或者去GitHub找个巨型开源项目折腾半个月还是跑不通。这个方向不能说错但完全不适合入门。FPGA做网络通信和CPU跑协议栈是两个思路。CPU是用软件一条条指令去解析包、维护状态每个包的处理时间不确定遇到大流量时CPU占用率飙升。FPGA的逻辑是“流水线并行”数据从一个模块流到下一个模块每个包在时钟节拍里几乎固定延迟地过完整个处理链路天然适合线速转发和极低时延的场合。但FPGA也不是神仙它在逻辑资源上实现完整TCP/IP协议栈非常吃力尤其TCP的滑动窗口、重传、拥塞控制、连接状态管理这些在硬件里做不仅代码量爆炸调试难度也极高。所以业界的常规做法是分层切片把协议栈里适合硬件加速的部分留在FPGA把复杂的逻辑交给软件。1.1 网络栈在FPGA里到底长什么样一个标准以太网包从网线进来依次经过物理层、数据链路层、网络层、传输层。FPGA里的大致分工是这样的物理层由PHY芯片完成芯片负责编解码、信号整形、自适应协商、时钟恢复。FPGA不直接碰模拟信号只通过MII/RGMII/SGMII这类接口和PHY交换数字数据。数据链路层通常用MAC核来做负责封装以太网帧、CRC校验、流控。厂商的三速以太网MAC IP就是这个角色。网络层和传输层的简化版也就是IP和UDP的解析、封装、校验和计算通常是自己写Verilog。再往上的应用层一般就交给CPU或者上位机了。所以一个比较现实的FPGA网络通信架构是PHY芯片 MAC IP 自研UDP/IP模块。你真正需要写Verilog的其实只有UDP/IP这部分外加一个AXI Stream或简单FIFO接口来衔接MAC核。这个工作量对新手是可控的。1.2 从哪个层次切入最合适有一个常见的误区就是有人一上来就买一块带SGMII光口的板子想直接做万兆。如果你连RGMII都还没调通直接上SGMII和10G PCS/PMA等于考完科目一直接上秋名山。我的建议是分三步走。第一步先把开发板上自带的千兆网口跑通。确认PHY型号看清是RGMII还是GMII先做MAC内回环和PHY回环测试。第二步调通PC和FPGA的UDP通信FPGA能回ARP、能收能发自定义UDP包。第三步再根据项目需求决定是否上光口、PCIe或者更高速的接口。这个顺序能在每个阶段把问题范围缩到最小不会一出问题就是一堆因素纠缠在一起。我自己带过不少人学FPGA凡是按这个顺序走的基本两个月内都能自己写出一个带UDP收发的小核。1.3 不同方案的取舍对比这里把常见的几条FPGA网络通信路线放在一起对比方便你根据自己情况选。方案需要写Verilog的量学习收益开发风险推荐指数开源GMII MAC 自研UDP大MAC底层都要自己调最高能彻底搞懂链路层PHY时序和MAC bug容易卡很久有基础再考虑厂商三速MAC IP 自研UDP中只写IP层以上高兼顾原理和效率需要学IP配置但资料多新手首选Zynq硬核GEM SDK协议栈很少主要写C代码偏嵌入式软件弱化硬件功底容易变成纯软开偏离FPGA学习不推荐纯FPGA入门现成开源TCP/UDP核少但难改低出问题看不懂黑盒式使用后期难维护仅限快速交付我个人的意见是想通过这个项目锻炼FPGA能力的老老实实选第二行。既不会把你淹没在MAC层细节里又能真正碰到帧解析、校验和计算这些核心内容。2. 硬件底子PHY、RGMII与MDIO先把和网络通信相关的硬件基础讲清楚这些不过关的话后面写多少Verilog都白搭。很多新手写了一大堆UDP状态机最后发现是PHY复位没做对这就很憋屈。2.1 PHY芯片到底管哪些事PHY中文叫物理层收发器开发板上通常长成一个小方片子外接一个RJ45座子。它负责的工作包括把MAC送过来的并行数字信号编码成适合双绞线传输的电平信号反过来把网线收到的模拟信号解码成数字信号还有链路协商、状态指示、MDIO管理接口等。常见的千兆PHY芯片有瑞昱RTL8211系列、美满88E1512、裕太微YT8531等。它们的功能大同小异区别主要在寄存器细节和初始化流程上。你拿到一块开发板第一件事就是查原理图确认PHY型号、PHY地址MDIO地址、RGMII接口是3.3V还是2.5V电平、有没有复位引脚连接FPGA。PHY芯片有一个非常重要的设计思想就是它本身也算一个小处理器有自己的寄存器空间。FPGA通过MDIO接口读写这些寄存器可以配置速率、双工模式、自适应开关、回环模式等。所以你控制PHY本质就是控制寄存器。2.2 RGMII接口的引脚与时序RGMII是千兆以太网最常见的MAC-PHY接口。注意它是在双沿采样时钟上升沿和下降沿都会传数据这是很多新手第一次接触DDR信号很不习惯。以千兆模式为例RGMII接口有TXD[3:0]和RXD[3:0]四根数据线加上TX_CLK、RX_CLK和TX_CTL、RX_CTL。在TX_CLK的上升沿发送TXD低4位和TX_CTL的TX_EN在下降沿发送TXD高4位和TX_CTL的TX_ER。接收同理。这里最容易出问题的是时钟。GTX_CLK在千兆时是125MHz由MAC提供给PHY。RX_CLK是由PHY恢复出来的125MHz时钟作为接收侧同步时钟。很多开发板的PHY参考时钟是25MHzPHY内部通过PLL倍频到125MHz这个细节容易让人在看IO约束时晕头转向。还有FPGA侧发送数据要DDR输出一般用ODDR原语把两个半字节拼到一根线上接收侧用IDDR把数据拆开。如果你用的是Vivado或Quartus里的MAC IP这些DDR转换逻辑IP会自动处理但如果你是自己写接口逻辑去对接PHY那就要非常小心建立保持时间。对应到约束上RGMII的input delay和output delay一定要根据PCB走线长度和数据手册计算填好不然时序分析就是一团糟板上实测也会时不时丢包。2.3 MDIO读写与PHY初始化MDIO也叫SMI接口是一种非常简单的两线管理接口一条MDC时钟线一条MDIO数据线。时序比I2C简单得多没有地址应答概念就是固定格式的“前导码preamble操作码PHY地址寄存器地址数据”。不过实际使用中我们一般不用自己写时序厂商的MAC IP通常自带MDIO接口模块或者用Vivado的IO IP都能解决。PHY初始化的标准流程是拉低复位引脚至少几十毫秒释放复位然后等待自协商完成。千兆PHY的自协商时间一般要两三秒所以不要一上电就立刻去配置寄存器容易失败。读取PHY状态寄存器比如0x1F或者通用状态寄存器0x11确认链路已经是up状态、协商速率是1000Mbps再进入下一步。实际操作中还有一个非常关键的调试手段就是把PHY设置成回环模式。比如RTL8211的0x00寄存器bit14置1就是digital loopback。在这种模式下PHY会把发送的数据直接从接收方向环形回来不经过网线和外部设备。这样可以快速验证FPGA和PHY之间的接口、时钟、数据宽度是否正确。如果回环通信还不对那问题一定出在FPGA与PHY这一侧和对方设备没关系。2.4 先用回环验证底层链路新手最常见的困惑是程序烧进去灯闪得很欢网线一插却什么反应都没有。这时候千万别急着跑UDP要先分清问题在哪一层。你的第一层验证应该是在MAC IP内部做回环发送的数据不经过PHY这能验证FPGA内部的FIFO和MAC IP配置。第二层验证是在PHY做digital loopback这会经过FPGA到PHY的RGMII链路。第三层才是插网线和PC联调。每一层回环都能帮助你缩小故障范围这是很多老工程师调试网络接口的基本功。注意回环测试时发送端和接收端走的是同一颗芯片不涉及外部网络所以也不用关心PC网卡配置。等到回环完全通了再把网线接上这时才需要配置本机IP地址。3. 自己动手写一个最小的UDP协议栈这个部分是整篇的核心也是你在FPGA网络通信项目里真正展现技术价值的地方。我会把它拆成接收和发送两个方向来讲并把帧格式和校验和算法说透。3.1 为什么要选UDP而不是TCP这个问题几乎每次都有朋友问。我的回答很直接硬件实现TCP不是不行而是性价比太低。TCP是面向连接的可靠传输协议它需要维护序列号、确认号、重传超时定时器、接收窗口等一系列状态。这些状态在CPU里是不需要什么成本的内存变量但在FPGA里每多一个连接状态就意味着多一套寄存器和状态机而且每个包的处理时延都不一样违背了FPGA硬实时低时延的初衷。UDP就简单多了它是一种无连接协议发出去就不用管没有重传不需要维护连接表。在实验仪器、高速数据采集、图像传输这些场景上位机和FPGA之间都是直连的点对点通信偶尔丢一帧重发一下完全没问题。所以对新手而言UDP是首选也是后续做TCP的基础。等你把UDP跑通了再去看别人的TCP实现才有能力去分析和裁减。3.2 帧头结构一个字节都不能错以太网线上实际传输的报文从DMAC开始。MAC层帧头共14字节前6字节是目的MAC接着6字节是源MAC最后2字节是以太网类型值为0x0800表示IPv40x0806表示ARP。IPv4头部固定部分是20字节包含版本号、IHL、总长度、标识、标志、片偏移、TTL、协议号、头校验和、源IP、目的IP等。UDP头部固定8字节包含源端口、目的端口、UDP长度、校验和。所以一个IPv4 UDP包从目的MAC开始算头部总共是14 20 8 42字节后面跟着payload最后4字节是FCS帧校验序列一般由MAC硬件自动生成用户数据不用管。我这里给一张极简的帧结构表方便编码时对照字段长度备注目的MAC6B上位机网卡MAC源MAC6B可以是开发板的MAC随便定义但别全0EtherType2B0x0800 / 0x0806IP头20B版本4IHL5TTL64协议17UDP头8B源端口、目的端口、长度Payload0~1472B千兆无Jumbo帧时最大1472FCS4B由MAC IP处理有一个细节我要强调一下网络上所有多字节字段都是大端序也就是高字节在前。比如0x1234在线上是0x12、0x34。这和你写Verilog时的字节比较多很容易写反。我的经验是在写解析状态机的时候把帧头的每个字节在注释里按顺序标出来跟着注释调Header千万不要凭印象打。3.3 接收通路设计从MAC核到自定义FIFO接收方向数据从MAC IP出来通常是一个AXI4-Stream接口或者简单的FIFO接口。不论哪种接口核心就是数据线和有效标志。在AXI4-Stream里tvalid为高时数据有效tlast表示包结束tuser可能表示帧起始位置可能标记错误帧不同IP定义不一样。接收部分我建议拆成三个模块。第一个模块负责逐字节解析头部提取目的MAC、源MAC、EtherType、IP头的协议字段、目的IP等并判断这个包是否应该接收。第二个模块处理ARP请求如果识别出EtherType是0x0806就解析ARP请求把发送方MAC和IP记录下来然后组织一个ARP应答包。第三个模块处理UDP根据端口号过滤把payload写入FIFO同时向用户逻辑输出一个包有效标志。判断是否接收的逻辑本质是几个“与”条件目的MAC是否等于本机MAC或者广播FF:FF:FF:FF:FF:FF、目的IP是否等于本机IP、EtherType是否等于0x0800、IP协议号是否等于17。这些条件在状态机里逐字段比较只要一个不满足就把当前包丢弃等待下一个tlast。这里给个简单的状态机思路接收路径的数据流可以这么设计localparam IDLE 3d0, MAC_HDR 3d1, IP_HDR 3d2, UDP_HDR 3d3, PAYLOAD 3d4, ERR_DROP 3d5;在MAC_HDR状态下计数前14字节判断EtherType。如果确定不是需要的包类型直接跳到ERR_DROP直到收到tlast再把状态机拉回IDLE。这样能够省掉大量无效的FIFO写入防止无关广播包淹没你的接收缓存。3.4 发送通路设计组帧、校验和与最小包长发送方向比接收方向麻烦一点因为你需要自己把MAC头、IP头、UDP头按顺序组装起来。当然MAC IP会帮你加FCS但你得保证IP头里的总长度字段、UDP头里的长度字段、校验和都计算正确。发送路径我建议用一个小的发送控制状态机先从用户接口读到本地配置的源IP、源MAC、目的IP、目的MAC以及一条待发送的FIFO然后状态机按以下顺序发送目的MAC - 源MAC - EtherType - IP头 - UDP头 - payload - 补齐到最小帧长。以太网最小帧长是64字节如果payload太少需要往后补0否则对端会丢弃。MDIO驱动、PHY寄存器操作结合一个工程来调会比较顺手。IP头校验和的计算方法是把IP头按16位一组相加如果有进位则回卷继续加最后取反得到十六位校验和。UDP校验和虽然IPv4下可以置0但我建议还是算一下特别当你之后想把报文发到公网、或者对接某些严格校验的抓包工具时置0会被直接丢弃。UDP校验和的计算范围是伪头部加上UDP头和payload伪头部由源IP、目的IP、协议号和UDP长度构成它不从网线上传输只参与计算。发送数据最小帧长我遇到过不少漏掉这个细节的PC网卡可能没反应Wireshark上也看不到包排查很久发现是FPGA发出去的数据包短于64字节被PHY直接丢弃了。// IP checksum example, only for illustration reg [31:0] checksum_sum; always (*) begin checksum_sum 0; ... end3.5 与MAC IP的接口对接注意事项很多朋友自己写的UDP模块单独仿真没啥问题一接到厂商MAC IP上就懵最常见的问题有三个。第一个是字节序和首字节位置。MAC IP的数据宽度如果是32位那么第一个有效字节可能是byte0也可能是byte3取决于IP的字节交换设置。不同厂家的设置完全不同连Xilinx和Intel都不一样。建议先用固定长度的测试帧比如发一个长度正好是8字节的UDP包在仿真里看每个字节落到哪个数据线上确定映射关系。第二个是tlast和tuser的处理。AXI4-Stream下一个包结束时tlast要拉高并且如果最后一个周期数据不满总线宽度剩余的字节也要用KEEP信号标成无效。tuser如果被MAC IP用来标记错误帧那接收端看到tuser有效时要主动丢弃整包。第三个是发送FIFO的背压。MAC IP在发送FIFO快满时会拉低tready你的状态机必须支持暂停不能在tready为低时继续给数据否则会丢包。很多自己写的UDP模块一接上就出现间歇性丢包十有八九是这个原因。4. 联调、问题排查与高速接口扩展前面的模块写完后真正让人涨经验的往往是联调阶段。这个阶段你会遇到各种千奇百怪的现象但只要定位方法对大部分问题都能在两三个小时内解决。4.1 PC和FPGA直连联调的实用流程先把PC网卡设成静态IP比如192.168.1.10子网掩码255.255.255.0FPGA侧对应配置192.168.1.20。FPGA需要知道PC的MAC地址最简单的方式是FPGA自己发一个ARP请求PC会自动响应从响应包中学习到PC的MAC。实操中我习惯用两个阶段来排查。第一阶段验证发送路径FPGA主动周期性发送UDP包到PCPC跑一个UDP监听工具看看能不能收到。第二阶段验证接收路径PC发送UDP包到FPGAFPGA收到后点亮LED表示收到或者直接回发一个响应包。如果两个方向都通说明双工双向基本OK。抓包工具强烈建议用Wireshark。打开后在过滤栏输入udp观察PC网卡上是否有FPGA发来的包点开某一个包看头字段是否合理。有一次我发现FPGA发过来的包目的IP是对的但源IP是0.0.0.0Wireshark一眼就能看出来因为IP头字段的解析能展开每个字节非常适合排查这类错误。4.2 新手最容易踩的坑与排查表我把这些年带人调试FPGA网络通信遇到的典型问题做了一个速查表顺着这个表查命中率很高。现象可能原因排查建议PHY寄存器读不出来MDIO地址错误、MDC时钟没给、PHY复位一直拉低查原理图确认PHY地址用示波器看MDC/MDIO波形回环模式能通但插网线不通PHY未完成自协商、网线质量、RJ45差分走线错误插网线后看PHY状态寄存器link是否置1PC ping不通FPGAARP应答没回、IP校验和错、MAC地址不匹配Wireshark抓包先看有没有ARP请求进来UDP只收不发发送FIFO空没检查、tready背压处理不对、头组装状态机卡住仿真先看发送头状态机是否走完再抓线测UDP只发不收接收FIFO写入条件错、tlast位置不对、payload长度不匹配在FPGA侧加计数器统计收到几个包对端看到几个偶发丢包FIFO深度不够、数据没有做跨时钟域处理、用户读取速率慢把FIFO改成独立时钟域增加深度观察丢包时序RGMII时序报错ODDR/IDDR没加、input/output delay约束没写查时序报告按PHY手册填set_input_delay/set_output_delay我特别想强调一下时序约束这件事。网络通信的RGMII接口如果不用约束Vivado可能会按照默认的时钟延迟去分析结果很可能和实际的PCB走线不匹配。尤其RX方向数据时钟由PHY恢复和FPGA内部主时钟并不是同步关系必须通过IDDR原语加约束把数据对齐。很多新手在这块吃亏其实只要跑一下Implementation后的时序报告看到RGMII接口时序violated基本就能猜到是约束缺失。4.3 资源评估与时序收敛的小经验当你把UDP核写完可以打开综合报告看看LUT、FF、BRAM的使用率。一个精简的千兆UDP核大概会占用几百个LUT、几百个FF和几个BRAMFIFO一般不到整个芯片资源的5%。如果资源突然暴涨很可能是因为你在帧解析时用了很多位宽很大的向量比较或者状态机写成了一堆if else嵌套代码风格影响是巨大的。时序方面250MHz以下的设计通常压力不大但RGMII接收时钟域和逻辑主时钟域是两个异步时钟域最好的做法是把PHY提供的RX时钟直接驱动IDDR把解析完的数据用异步FIFO跨到主时钟域。千万不要直接用逻辑代码去搬多比特数据跨时钟域否则极易出现亚稳态。4.4 如果后面想上光口、PCIe、TSN网口通了以后很多人会想往高速方向发展。这里简单给几个方向省得到时候再去翻一堆资料。光口方面如果你用的是SFP光模块端口速率是1G接口通常走1000BASE-X或SGMII需要例化厂商的PCS/PMA IP用户侧接的还是GMII或者精简接口。逻辑和千兆电口的UDP核基本复用主要区别在PHY层。如果直接上10G光口接口变成XGMII或10G PCS/PMA数据宽度从4位变成64位但帧格式仍然没变你的UDP解析状态机稍作调整就能用。PCIe方向如果你需要把高速数据从CPU搬到FPGA、再通过网口发出去一般会用到XDMA IP。XDMA有完整的DMA描述符和中断机制配上Linux驱动后可以直接把PC内存里的数据搬到FPGA的发送FIFO里然后由你的UDP核发出。这部分相当于给FPGA网络通信加了一个PCIe前端逻辑量不小建议先跑厂商自带的XDMA例子确认DMA链路通后再接UDP核。TSN方向是FPGA最发挥优势的地方。802.1Qbv时间感知调度、802.1AS时间同步这些需要纳秒级精度CPU很难做到FPGA则非常合适。不过这些内容需要比较深的背景至少先把基础UDP通信做扎实了再碰。4.5 联调之后的下一步实践建议走到这里你的FPGA网络通信基本已经入门了。接下来可以试着做一个小项目巩固一下比如把ADC采样数据通过UDP包实时发到PC显示波形或者接收PC下发的参数去控制开发板上的LED亮度。这类项目能把采样时钟域、跨时钟域FIFO、UDP传输层这些问题串在一起非常锻炼人。也可以用厂商的参考设计当对照。Vivado和Quartus都有三速以太网MAC的example design里面包含了完整的回环测试、仿真实例和约束文件。很多人觉得example design太复杂但我建议不要直接跑而是打开工程看它是怎么约束RGMII、怎么处理复位时序的再看MAC的AXI Stream信号怎么接的。对照自己的设计你就能发现自己少了哪些细节。我个人在实际调试中的体会是FPGA网络通信这个项目真正的难点其实不在写代码而在调试思路。协议栈是个层层嵌套的东西如果你能用回环测试把每一层隔离出来每一步都验证通过了再往后走整个项目就会极其顺滑。反倒是那种一次把全部逻辑写完、烧到板卡上才开始查问题的做法真的会把人折磨到怀疑人生。所以最后再分享一个小技巧在网络通信调试中永远保留一个“无脑发包模式”的开关。在你的FPGA代码里加一个内部触发按下按键或者收到一个特殊命令就自动周期性地发送一串已知内容的标准UDP包然后再去上位机抓包。这样一个简单的测试模式往往能在相互扯皮的联调里直接定位出问题到底在FPGA侧还是PC侧比你翻协议栈日志高效得多。