
1. 为什么要在FPGA上做网络通信1.1 先搞清楚你要传什么数据做FPGA网络通信之前我建议你先停下来问自己一个问题数据从哪来要到哪去实时性要求多高这个问题回答不清楚后面技术选型全是空中楼阁。我自己接过不少半路来找我咨询的项目一上来就说能不能用FPGA做个网口细问之下才发现需求五花八门有人是要把ADC采样的波形数据实时推给上位机有人是要在FPGA和摄像头之间传图像帧还有人纯粹是想在开发板上把网口调通为后面做高速数据采集打基础。这三种场景对网络设计的约束完全不同。ADC波形数据通常是连续流带宽需求稳定讲究低延迟和低抖动图像数据是突发型的大包讲究高吞吐和高缓存而只是调通信的话随便写个回环就能跑。我的习惯是先用一张表把自己的需求量化出来再动手选型需求维度要问自己的问题影响的设计点数据速率峰值多少Mbps持续多少Mbps百兆/千兆选型、FIFO深度数据形态连续流还是突发包缓存策略、流控方式实时性端到端延迟容忍多少是否用中断、缓冲级数对端环境连PC连交换机连另一块FPGA是否做ARP、是否支持组播协议复杂度需要TCP吗要重传吗纯UDP还是有CPU介入掉过最狠的一个坑是给一个图像采集项目选了纯逻辑UDP方案结果对端PC用Windows系统做接收Windows的UDP接收缓冲默认只有几十KB图像突发包一多就疯狂丢包。后来不是FPGA的问题是上位机缓冲没调。这种问题在设计阶段就要预留沟通空间别等联调了才互相甩锅。1.2 三条技术路线纯逻辑、软核CPU、硬核SoCFPGA做网络通信大体有三条路线我按学习曲线陡峭程度和最终能力上限两个维度排个序。第一条是纯逻辑方案也就是完全用Verilog或VHDL自己写MAC控制器、ARP模块、IP/UDP协议栈。这是最硬核的路线也是我个人最推荐入门者走一遍的路。它最大的优势是你对每一个时钟周期做了什么完全心里有数延迟可控到纳秒级资源消耗也最小。缺点是开发周期长协议栈一复杂就非常痛苦学到TCP重传、分片这些光调试就能掉一层头发。第二条是软核方案典型的是Xilinx MicroBlaze配合NlIPXilinx自家精简协议栈IP核或者Altera/Intel的Nios II配合lwIP。这套方案的本质是在FPGA里跑一个微型CPU让协议栈跑在CPU上FPGA逻辑只负责MAC和DMA。优势是开发效率高TCP、UDP、ARP这些协议栈直接有现成库你能用C语言写网络应用不用懂太多硬件细节。缺点是延迟高、资源占用大而且一涉及到IP核授权和版本兼容就麻烦。第三条是硬核SoC方案比如Zynq系列片内直接集成ARM Cortex-A9/A53外设里就有千兆以太网控制器GEM跑Linux系统后用标准socket编程。这是网络功能最强、最接近正常软件开发的路线你甚至可以跑完整的TCP/IP栈、HTTPS服务。但问题是你已经不是在做FPGA开发了而是在用ARM做嵌入式Linux开发纯PL端逻辑反而变成一个外设。很多做图像处理的朋友最终会走到这条路上PL端做采集和算法加速PS端做网络和调度。三条路线不是互斥的。我见过很务实的组合先用纯逻辑实现一个最小UDP收发确认链路通了、时序对了再在这个骨架上决定要不要引入CPU来做上层协议。这个先硬件后软件的顺序能让你对网络瓶颈在哪有直观感受后面无论走哪条路都不慌。1.3 为什么入门首选纯逻辑UDP很多新手一上来就想做TCP觉得UDP不可靠、不高级。我的观点完全相反入门做FPGA网络通信一定先从纯逻辑UDP开始这是性价比最高的路径。第一UDP的包处理逻辑极其简单没有三次握手、没有序列号、没有滑动窗口、没有重传机制。你只需要把用户数据包上UDP头、IP头、MAC头然后按帧发出去接收端反过来解包把有效载荷取出来放到FIFO里。整个状态的复杂度连一个复杂状态机都算不上。第二UDP天然匹配FPGA擅长的高速低延迟数据传输场景。FPGA最常干的活是采集和预处理它不需要可靠传输它需要的是稳定地把数据流倒给上位机UDP把协议栈都省了延迟能压到微秒级以下。TCP那套确认-重传机制在硬件里做要么占用大量逻辑资源要么延迟高到失去FPGA的优势。第三从学习角度讲UDP收发链路覆盖了以太网帧、CRC、ARP、RGMII时序、跨时钟域处理这些FPGA网络开发的核心知识。这些知识点学会了TCP只是在这上面加复杂度而已到时候你自然知道自己该不该上CPU。我经常跟人说先花两周时间把UDP收发调通你对FPGA的信心会涨一大截。这个小目标看起来不起眼但它是后面做千兆图像传输、高速数据采集的基石。2. 做一个网口需要吃透的几个底层概念2.1 以太网帧长什么样FPGA做网络通信本质上是按帧干活。你不必像学计算机网络那样背OSI七层模型但帧结构必须吃透因为它直接决定你RTL代码里的打包和解包逻辑。标准以太网帧分这么几段前导码7个字节0x55交替帧起始定界符SFD 1个字节0xD5目的MAC地址6字节源MAC地址6字节类型/长度字段2字节数据区46到1500字节帧校验FCS 4字节。如果你用的是千兆以太网还在前面加了8字节的扩展前导码不过这是PHY芯片帮你处理的事情MAC层逻辑通常不用管。放一个我常用的帧结构对照表写代码的时候直接对照着来字段长度说明Preamble7B0x55用于接收端时钟同步SFD1B0xD5帧真正开始的标志DST MAC6B广播是FF:FF:FF:FF:FF:FFSRC MAC6B本端MAC要向PHY/交换机声明EtherType2B0x0800是IPv40x0806是ARPPayload46~1500B如果是IP包这里就是完整的IP报文FCS4BCRC32由发送端算好附上注意那个46字节的最小长度限制。这是CSMA/CD时代留下来的规矩意味着如果你的UDP数据很短比如只有10个字节IP层和UDP层头加起来占208字节MAC层数据区一共才38字节不够46字节必须填充。很多新手第一次抓包发现数据后面莫名其妙多了一串0就是填充字节。FPGA实现时这个填充逻辑要写清楚否则发出去的包会被对端认为帧长错误。CRC32的坑后面专门讲这里先记住一点发送端算CRC时覆盖的是从目的MAC到Payload的所有字节结果按特定字节序拼在帧尾接收端需要把整个帧一起校验。我在第一次做的时候直接用了网上抄来的CRC32代码结果上位机一直报错查了半天才发现是初值和字节反射顺序的问题。这个算法看着简单细节非常刁钻强烈建议用仿真先验证你拿到的代码能算出标准测试向量。2.2 RGMII接口是真功夫MAC和PHY之间的接口目前最主流的是RGMIIReduced Gigabit Media Independent Interface。名字里带个Reduced是因为它把GMII原本8bit并行数据接口压成了4bit然后在时钟的上沿和下沿各采一次用DDR技术维持同样的数据吞吐率。RGMII的信号线不多TXC发送时钟、TX_CTL发送控制、TXD[3:0]发送数据RXC接收时钟、RX_CTL接收控制、RXD[3:0]接收数据。以千兆为例TXC是125MHzTXD在TXC上升沿发送低4位下降沿发送高4位。TX_CTL也是这样上升沿发TX_EN使能信号下降沿发TX_EN异或TX_ER发送错误标志。接收方向同理RX_CTL上升沿是RX_DV下降沿是RX_DV异或RX_ER。写时序逻辑的时候发送方向直接用TXC做时序参考把8bit数据拆成两个4bit分别在不同沿送出去就行。麻烦的是接收方向。PHY芯片输出的RXC和数据是有相位关系的但不同的PHY芯片、不同的PCB布线RXC相对RXD的延迟特性不一样。数据在RXC的跳变沿稳定可能只有1到2纳秒的窗口直接用RXC去采RXD非常容易采到边沿上出现亚稳态或采样错误。解决这个问题通常有两种做法一种是用FPGA的IDELAY原语给RXD加可调延迟然后用RXC延迟90度去采样另一种是直接在代码里用RXC的上升沿去采RXD但把内部时钟相位反过来即在RXC的下降沿打一拍再处理。实际做的时候我强烈建议在Vivado或Quartus里用专门的约束把接收数据总线的时序余量查一下别偷懒。我再补一个经验如果你用的是黑金、正点原子这类开发板板上PHY芯片和FPGA之间的RGMII走线通常已经由厂家优化过可以直接用RXC上升沿采RXD。但如果自己画PCB走线长度哪怕偏离几十个mil采样时序就完全不一样。所以做自定义板卡时RGMII接收方向一定要预留IDELAY调延迟的余地否则后面改版你哭都来不及。2.3 时钟和复位也别忽略网络通信模块的时钟设计新手最容易忽视但也最容易出问题。千兆RGMII的TXC是125MHz但你的用户逻辑可能跑在100MHz、150MHz甚至更高那就必须在两个时钟域之间做数据交换标准做法是用异步FIFO。前面part里如果学过FIFO的跨时钟域处理这边就能直接复用没学过的话我建议你先回头补一下不然整个网络模块就是豆腐渣工程。还有一个大坑是复位。网络模块的复位不能随意因为MAC和PHY之间的MDIO管理接口需要初始化流程PHY芯片上电后要等一段时间才能稳定输出时钟。很多PHY芯片的复位信号要求低有效脉冲至少持续一定宽度而且释放后还需要等待内部PLL锁定。如果你在代码里把复位信号一拉高就开始发包很可能PHY那边还没ready你说为什么我的网线插上灯都不亮其实十有八九是PHY初始化没做完。厂家给的例程里通常有详细的初始化时序照着做最稳妥。以太网设计中复位释放也是要讲究的我自己的习惯是把外部复位信号先打两拍同步到对应时钟域再做一个上电延时计数器延时个10毫秒再释放内部复位给PHY留足初始化时间。别小看这几毫秒能帮你省掉无数莫名其妙的上板首报故障。3. RTL实现一个最小UDP收发通路是怎么搭起来的3.1 顶层架构一句话讲清整个最小系统的逻辑其实一句话就能说清把你应用中产生的用户数据帧写入发送FIFO发送模块从FIFO读出来依次添加UDP头、IP头、MAC头算好CRC后通过RGMII发给PHY接收方向反过来从RGMII收进来的数据流先剥离MAC头、解出IP/UDP头把有效载荷写入接收FIFO供你应用层读取同时在收到ARP请求时自动回复应答包。我画过一个自己常用的模块划分按这个思路写代码不会乱eth_top.v顶层例化所有子模块连接FIFO和寄存器接口mac_tx.v发送状态机负责MAC封装、CRC计算、RGMII发送时序mac_rx.v接收状态机负责RGMII接收、MAC头部解析、CRC校验arp_module.vARP请求检测和应答生成维护一个简单的IP-MAC映射表udp_ip_tx.v在MAC发送基础上增加IP和UDP头部封装udp_ip_rx.v在MAC接收基础上解析IP/UDP头部输出有效载荷async_fifo_tx.v/async_fifo_rx.v跨时钟域缓冲这个架构说不上精巧但胜在直观、好调试。每个模块的职责单一出问题的时候能用仿真波形快速定位到具体的状态机。别一开始就追求一个超复杂的流水线架构那只会让你的bug藏得更深。整个通路的延迟从用户数据写入发送FIFO到数据真的从PHY发出去大概在几个微秒级别。这个延迟对绝大多数采集系统都够用但如果你要做闭环控制这种需要精确同步的东西就得对延迟做精细约束了。3.2 发送通路从FIFO到RGMII发送通路的核心是三个状态机层叠UDP层状态机负责打包用户数据IP层状态机负责封装IP头MAC层状态机负责把整个IP报文再封装成以太网帧。实际写的时候可以把UDP和IP层合成一个状态机因为它们的头部长度和字段都是固定的合成起来反而能少很多状态切换。这里放一个我常用的发送状态机核心代码骨架帧头字段基本都能查表填重点是数据通路切换localparam IDLE 3d0, MAC_HDR 3d1, // 发送MAC头IP头UDP头 DATA 3d2, // 发送有效载荷 PAD 3d3, // 填充到最小帧长 CRC 3d4, // 发送CRC32 DONE 3d5; always (posedge clk125 or negedge rst_n) begin if (!rst_n) state IDLE; else case (state) IDLE: state tx_fifo_empty ? IDLE : MAC_HDR; MAC_HDR: if (byte_cnt HDR_LEN-1) state DATA; DATA: if (byte_cnt payload_len-1) state PAD; PAD: state CRC; CRC: state DONE; DONE: state IDLE; endcase end发送方向有四个细节必须注意。第一个是数据宽度匹配。RGMII每次只能送4bit而用户数据处理往往是8bit、16bit甚至32bit宽。所以发送模块内部要先做一个位宽转换把用户数据先拼成8bit再按字节拆分到两个半字节分别在不同沿发送。这一步看起来简单实际写的时候容易在字节顺序上翻车。以太网是大端传输高字节先发低字节后发如果你的用户数据本身是按小端组织的很多ADC采样数据就是这样那发送前一定要做字节序转换不然对端收到数据后要花很长时间才能发现数据怎么是反的。第二个是CRC计算的时机和插入位置。CRC32必须在整帧数据通过的同时实时计算算完立刻附加到帧尾输出。因为FPGA的流式处理特性CRC要在数据发送过程中同步算而不是先缓存整帧再算那样会引入一帧的延迟和额外存储。这里要用crc32串行计算模块每来一个字节算一次算完整个数据区后把结果按字节序发送出去。第三个是帧填充逻辑。前面说了MAC层数据区最小46字节如果你的UDP包总长小于这个值在打包时要填充0否则PHY芯片可能不会正常发送这个帧或者交换机直接丢弃。填充逻辑放在发送状态机的PAD状态里很容易但要记得填充字节不计入UDP和IP头里的长度字段很多新手在这里犯迷糊。第四个是帧间隙IFG。以太网规定两帧之间至少要有96bit时间的间隔对应千兆就是12个时钟周期。如果连续发包时不插入IFG对端接收状态机根本来不及复位就会把两帧当成一个长帧来解析后面全是错。这个IFG设置成12个周期就行别贪多贪多会降低有效吞吐率。3.3 接收通路从RGMII到FIFO接收通路是发送通路的逆过程但难度高一个档次因为你对收到的数据没有任何控制权来了什么就得接什么。接收状态机第一步是检测SFD。RGMII接收端要先连续检测到7个0x55的前导码然后在第8个字节检测到0xD5认为帧开始此时RX_DV信号拉高。之后每个时钟周期采4bit两个沿拼出一个字节按字节流处理。这里要注意前导码和SFD是PHY芯片已经剥离了还是原样送给MAC不同PHY芯片的默认配置不一样厂家例程里一般会通过MDIO配置PHY来决定。这个细节上板前最好先确认否则你会看到接收数据里莫名其妙多出8字节。第二步是解析MAC头。判断目的MAC是不是自己的MAC或者广播MAC不是就丢弃。这里别做太复杂的过滤逻辑最简单就是比较两个48bit地址再查一个广播使能位。做完地址过滤后看EtherType字段0x0806就转给ARP模块处理0x0800就继续往IP层解析其他类型直接丢弃。第三步是IP层校验。IPv4头部是20字节前4bit版本号必须是4IHL字段通常是5表示20字节头部协议字段是17UDP。遇到不是IPv4或者分片片偏移非0的包直接丢。真正要小心的是IP头校验和它是一个16位的补码和校验算法发送端在打包IP头时就要算好接收端在做解析时最好也校验一下虽然UDP对IP层错误没有检测机制但你主动加一层校验能省不少调试时间。第四步是解析UDP头取出源端口、目的端口、UDP长度和校验和。如果目的端口和你的配置不一致丢弃。取出有效载荷后把数据写入接收FIFO同时把源MAC地址、源IP地址、源端口、数据长度这些信息一并存入一个头部信息寄存器组供应用层读取。接收方向我踩过最深的坑是RGMII下的RX_DV和RX_ER。RX_DV高电平表示接收有效数据低电平表示帧结束。但有些PHY在链路由千兆降速到百兆时RX_DV换成了RX_DVRX_ER的含义也变了。如果你的PHY配置支持速率自动协商接收状态机一定要把速率的判断逻辑也写清楚否则一插百兆交换机整个接收就是乱的。3.4 ARP模块不解决表项就没人理你很多纯逻辑UDP实现里最容易忽略的就是ARP模块我见过有人直接把源IP和目的IP都硬编码然后说不通。只要你的对端是一台PCPC发送UDP数据前必然会先发一个ARP请求查询你的MAC地址。如果你不回ARPPC就会认为这个IP地址对应的设备不存在连UDP包都不会发过来。ARP请求帧的格式不复杂目的MAC是全F广播地址EtherType是0x0806然后是一个28字节的ARP数据。其中关键字段是硬件类型1、协议类型0x0800、硬件地址长度6、协议地址长度4、操作码1请求或2应答、发送端MAC/IP、目标MAC/IP。ARP模块的实现逻辑分两部分。接收方向检测到EtherType为0x0806后解析ARP头部。如果操作码是1请求并且目标IP与自己的IP匹配就生成一个应答帧。应答帧的内容很机械把自己的MAC填入发送端MAC把请求里的发送端MAC填入目标MAC把对方的IP填入目标IP操作码改成2然后按以太网帧原样打包发出去。这里有个细节ARP应答包是不需要算IP层校验和的因为ARP报文根本没有IP头整个ARP格式就是MAC头ARP头FCS很多人拿IP层校验和的逻辑去套ARP然后发现上位机收不到应答查了半天才明白过来。发送方向应用层要主动向某个目的IP发包时需要先查自己的ARP表看目的IP对应的MAC已知还是未知。如果未知就要先发一个ARP请求广播出去等收到应答后再把对方的MAC存进表里然后才发送UDP包。这个查表-等待-发送的流程在纯逻辑里实现需要一个小型状态机外加一个存储表项的小RAM。很多初学者图省事把目的MAC硬编码成广播地址或者自己的MAC地址这样在上位机上用Wireshark抓包能看到发出了包但对端网卡根本不会把这种包交给协议栈应用层收不到任何数据。我自己的做法是最开始的调试版本允许硬编码MAC直接用PC的MAC方便抓包但正式版本一定要有完整的ARP处理否则别人换台PC就连不上了。4. 仿真到上板的避坑实录4.1 仿真阶段最容易翻车的地方写网络模块仿真调得越细致上板越顺利。但我见到的初学者仿真十个有八个是仿真波形出来了但不知道对不对因为网络协议的时序是高度周期性的波形一拉长肉眼根本看不出逻辑问题。我的建议是仿真阶段第一件事就是建立一个标准的以太网报文测试向量。也就是说你要在testbench里构造一个合法的UDP包包括正确的MAC头、IP头、UDP头和CRC然后喂给接收模块验证你的接收状态机能不能正确解析出有效载荷。同时你也要准备一个错误的包比如CRC故意算错验证你的接收模块能把它正确丢弃。CRC校验这块仿真阶段一定要用已知的测试向量验证。随便在网上找一段CRC32代码就拿来用十有八九出问题。以太网用的是CRC-32多项式0x04C11DB7但初始值、输入输出是否反射、结果是否异或这些细节直接影响结果。我给你一个简单的验证方法用在线CRC计算器算一个已知数据的CRC值把你的RTL代码仿真结果跟它对比一致了再用到工程里。还有一个我特别想提醒的仿真坑不要只给一个时钟域做仿真。真实系统里发送FIFO是跨时钟域的写时钟和读时钟频率不同。仿真里如果偷懒把所有时钟都用一个频率和相位跨时钟域的问题完全暴露不出来一上板就死给你看。正确的做法是在testbench里生成两个异步时钟有意识地模拟真实的时钟关系。4.2 上板之后Wireshark是你的眼睛上板调试是网络模块开发中最刺激也最折磨人的阶段。我的调试习惯是先在开发板上跑回环测试FPGA接收到的数据原样发回去然后在PC端用Wireshark抓包看现象一步一步缩小问题范围。具体调试顺序推荐这样做第一步检查PHY芯片状态。用MDIO接口读PHY的寄存器重点看0x01基本状态寄存器里的链路状态位和协商速率。这一步能确认物理层是否OK。如果链路都没起来网线接口的灯不会亮后面全部白搭。第二步从PC端ping一下FPGA的IP地址。如果ARP模块正常应该能收到ICMP Echo RequestWireshark里能看到而且FPGA会自动回ARP应答前提是你先实现了ICMP应答或者至少能回ARP。如果只看到请求没有应答问题大概率在ARP模块。第三步用上位机工具我自己常用Python的socket或者简单的UDP调试助手发一个UDP包给FPGA看FPGA有没有数据输出。这时用逻辑分析仪或Vivado的ILA抓FPGA内部信号重点看接收状态机、FIFO写使能、用户接口是否有数据。第四步FPGA往PC发UDP包时用Wireshark看PC是否收到。如果收到了但数据不对检查字节序和帧格式如果压根没收到检查发送状态机和PHY的配置。上板调试的黄金法则一次只改一个变量。我发现新手最喜欢一次改好几处然后出问题了根本不知道是哪个改动引入的。网络模块的调试维度多MAC地址、IP地址、端口、PHY配置、CRC一次只验证一个小环节出问题才能快速定位。4.3 常见问题速查表把我在实际项目中遇到的高频问题整理成一张表分享出来现象具体现象描述常见原因排查方向链路灯不亮网线插上PHY灯无反应PHY复位/初始化失败MDIO没配置好先用MDIO读PHY寄存器0x01能收到ARP请求但不应答Wireshark看到PC发ARP广播FPGA无响应目标IP不匹配、ARP模块状态机出错查ARP模块的目标IP判断逻辑发出UDP包PC收不到Wireshark完全没有任何UDP包目的MAC硬编码错误、CRC错误、PHY未进入传输模式先抓FPGA内部发送波形PC收到UDP但数据乱码数据能到上位机但是内容错乱字节序反转、位宽转换顺序错比对原始数据和接收数据的字节顺序偶发性丢包高速发送时部分包丢失FIFO溢出、跨时钟域处理不当、IFG设置太短查FIFO水位、跨时钟域同步逻辑长帧收发正常短帧异常大包都能通小包对端说帧长错误缺少帧填充逻辑PAD确认发送端PAD状态机填充到46字节再补一条排查心法遇到问题先确认物理层正常、MAC层正常、IP层正常、UDP层正常这个顺序逐层排查。很多时候你觉得是UDP层的问题实际是MAC头的类型字段写错了或者CRC错了导致整个帧被对端丢弃。别跳层排查网络协议栈天生就是分层的排错也要分层来。另外如果你手头有逻辑分析仪建议在调试阶段把RGMII的TXC、TX_CTL、TXD[3:0]和RXC、RX_CTL、RXD[3:0]都引出来看。两块板子联调时这个习惯能帮你省至少一周时间。最后再分享一个我常用的调试小技巧写到这里整个UDP收发的核心内容基本讲完了。按我个人的习惯每调通一个网络模块我都会做一件事在FPGA内部做一个计数器每发出一个UDP包就累加一次然后在需要发给PC的UDP数据里捎带这个计数值。这样PC端收到的数据如果连续你就能精确判断有没有丢包、丢了多少包比单纯看波形直观得多。另一个用了很多年的技巧是在接收路径上做一个包统计寄存器统计收到多少合法帧、多少CRC错误帧、多少MAC地址不匹配帧。这些寄存器平时不占多少资源但排查现场问题时它们是第一手证据。很多时候用户说数据丢了你一看CRC错误帧数是零、丢的全是地址过滤的包那问题根本就不在FPGA这边。这个系列走到这里你已经能把数据从FPGA里搬上网络了。下一步如果想进阶可以考虑做高速DMA将数据从DDR搬到MAC再发出去或者往图像传输、千兆光口收发这些方向延伸。再往后就是PCIE跟网络结合那又是一个大世界。不过万变不离其宗底层这套MAC、ARP、UDP的处理逻辑你掌握扎实了后面就是往上面叠功能的活。