前面几期咱们一直在跟按键、数码管、串口较劲,到了 part.7 这个位置,总有人跑过来问:FPGA 到底怎么跟电脑传数据?于是我们终于要碰 FPGA 的网络通信设计。先别慌,把它当成串口的一次升级就可以。串口是一个字节一个字节地挪,网络包本质也是一段一段地搬,区别只是多了很多“名字”——MAC、IP、端口、校验和,还有一块负责把电信号搬上线的 PHY 芯片。
这一篇的受众很明确:你手头有一块带网口的 FPGA 开发板,已经能写简单 Verilog 模块,但对以太网协议、PHY 芯片、MAC 地址这些东西没有系统概念。跟着这条路线走,你至少可以在两三天内让 FPGA 把一个自定义的 UDP 包发给 PC,再把 PC 回传的数据收回来。这个能力对你后面做图像传输、数据采集、高速设备控制都特别有用,因为它是未来几乎所有板级联调的基础。
1. 先别急着写Verilog:网口通信是四条不同“路”的配合
很多新手一上来就在网上找“UDP收发模块源码”,结果拿回来之后发现根本不工作。原因很简单:网络通信不是单模块问题,而是从 RJ45 座子到 FPGA 内部逻辑的一条完整链路。任何一个环节掉链子,整条链路都不通。这一节先建立正确的物理认知。
1.1 从 RJ45 到 FPGA:一条网线里究竟发生了什么
你自己画开发板链路的时候,可以看到这条路线:
RJ45 网口 → 网络变压器 → PHY 芯片 → FPGA → 用户逻辑
RJ45 网口大家都很熟,就是网吧插网线的地方。它后面那个小方块或者小芯片是网络变压器,用来做电平隔离和对干扰的抑制,很多 1G 网口上长得像一个小滤波器。然后是 PHY 芯片,这名字听起来很神秘,但它的职责很纯粹:处理物理层的信号编解码、时钟恢复、线路驱动。比如“网线里传输的是差分模拟电信号,但 FPGA 想要的是干净的 0/1 数字逻辑”,这个转换工作就是 PHY 干的。
FPGA 这一侧该干什么?在以太网体系里它通常承担 MAC 子层和更上层协议。也就是把 PHY 收上来的字节整理成以太网帧,识别目的 MAC、判断类型、剥离或者填充数据,然后交给用户逻辑。反过来,用户逻辑给出数据,FPGA 把它打包成以太网帧,再交给 PHY 发出去。
我把这四个角色做了一次类比,方便你记:
- RJ45 和变压器:小区门口的快递驿站。
- PHY:跑腿的快递员,负责把“货”装上车,运到楼道口。
- FPGA 里的 MAC:快递仓库的分拣员,负责贴面单、拆包裹、核对收货地址。
- 用户逻辑:你的业务部门,决定这批货里装的东西怎么处理。
这样看下来你就明白了,如果 FPGA 只写了一个 UDP 应用模块,但 PHY 没配置好、时钟乱了、地址不对,数据根本走不出那块板子。所以刚开始不要问“以太网代码怎么写”,要先问“我的板上这套链路是怎么接线的”。
1.2 开发板上那几颗芯片:先确认 PHY 型号和 MDIO 地址
不同开发板带的 PHY 芯片非常杂,比较常见的有 Realtek 的 RTL8211 系列、Marvell 的 88E1512、TI 的 DP83867,国内很多板子也会用裕太微、景略这类国产方案。虽然底层寄存器不完全一致,但大体套路是通用的:通过 MDC 和 MDIO 两根线去读写 PHY 内部寄存器。
我踩过最伤的一次,就是没看原理图,想当然把 PHY 地址设成 0x01,结果 MDIO 怎么读都读不到 ID。后来翻开发板原理图才发现,板上的 PHYAD 引脚被拉到 0x00。所以拿到新板子的第一件事不是写收发模块,而是打开原理图搜“PHYAD”和“MDIO”,找到以下信息:
- PHY 的 MDIO 地址是几。
- PHY 的复位脚接到 FPGA 的哪个管脚,是高有效还是低有效。
- PHY 的时钟是板载晶振提供,还是由 FPGA 输出。
- 网口连接的 RGMII 信号命名是带
_txd还是_tx_data,方便后面写约束。
有一个小技巧,MDCO 和 MDIO 是低速管理接口,通常跑 2.5MHz 左右。配置代码可以先假写,也可以直接读寄存器 2 和 3,也就是 PHY 厂商 ID。能读到预期的值,说明 FPGA 到 PHY 的管理通道是通的,这下你才算是站在了同一条起跑线上。
1.3 最容易劝退新人的 GMII/RGMII,其实把它看成“搬砖车道”就行
FPGA 和 PHY 之间的数据接口大致有两类。古老一点的是 GMII,8 根数据线加时钟和控制;而现在绝大多数板子用的是 RGMII,4 根数据线,在时钟上升沿和下降沿各传一次数据,用一半的引脚达到同样的吞吐。很多教程提到 RGMII 就默认你是资深工程师,会给你甩一堆“DDR”、“IO delay”、“约束”之类的名词。
如果你从 0 开始,我建议你先不要陷入 RGMII 的时序细节,而是把它当成一件通用事实:PHY 在一个时钟里能挤进 8 bit 的数据,RGMII 用 4 根线分两个沿传完。你的逻辑里仍然可以按 8 bit 为单位去组织帧,最后用 ODDR 原语或者厂商 IP 把 8 bit 拆成“前 4 bit、后 4 bit”送出去。等到后面你真正接触千兆以太网,再深入了解延迟、相位、约束也不迟。第一次做网络通信,目标是把包发出来、收进来,而不是去啃协议栈的每个角落。
2. 通信方案定在哪一层,决定你三天做出原型还是三周还在调握手
有人会说:网络通信设计不是应该直接写 TCP 吗?不然传数据怎么可靠?这个问题的答案取决于“谁在传、传到哪、多大数据量”。在 FPGA 上实现 TCP 不是一个轻松的事,因为 TCP 是有连接的协议,要维护序号、确认号、超时重传、接收窗口,这在 PC 上用软件做很容易,但在 FPGA 里做纯硬件逻辑会非常消耗状态空间。
2.1 先用 UDP 打通“数据管道”,TCP 后面再说
UDP 没有连接的概念,发出去就完事了。这听起来不“可靠”,但它在很多工程场景里恰恰是优点。比如高速数据采集、图像/视频流、传感器上报,这类应用本来就不希望因为一帧数据丢失而停下来重传,因为下一帧马上又来。PC 端通过网卡接收到 UDP 包的时候,如果校验和正确,直接丢给应用层,时延非常小。
从 FPGA 开发角度看,UDP 的状态机复杂度比 TCP 小一个量级。UDP 要处理的字段很少:源端口、目的端口、长度、校验和,如果嫌校验和麻烦,IPv4 的 UDP 校验和甚至允许发 0,虽然我不建议你长期这样干。TCP 则要在同一份逻辑里处理连接建立、序列号、滑动窗口、重传定时器,0 基础阶段真的会写到怀疑人生。
所以我给周围朋友的建议一直是:第一版通信,只要业务不强制要求文件级可靠传输,统统先用 UDP。让数据先跑起来,再考虑要不要在数据帧里加序号、增加重传机制,或者干脆把复杂协议放到上位机软件里做。
2.2 UDP、TCP、裸 MAC 放在一起选,你要的到底是什么
我做了一个简单对比,理解之后你就能在当前项目里做判断:
| 关注点 | UDP | TCP | 裸以太网帧 |
|---|---|---|---|
| 连接维护 | 无 | 有完整状态机 | 无 |
| 应用场景 | 实时数据/图像 | 文件/可靠流 | 专用私有协议 |
| 协议复杂度 | 低 | 高 | 最低 |
| 抓包可读性 | 高,常用工具支持 | 高但重传干扰 | 需要自定义解析 |
| FPGA 资源开销 | 小 | 大 | 最小 |
| 典型延迟 | 低 | 中 | 低 |
| 是否需要 IP 层 | 需要 | 需要 | 不需要 |
“裸 MAC” 最常见于两台 FPGA 之间点对点通信,不经过路由器,也不用被 PC 的协议栈约束,直接构造以太网帧按目的 MAC 发过去。缺点是 WireShark 抓包时只看到普通以太网类型,不方便调试。如果你目标是跟 PC 联调,第一版老老实实走 UDP 最划算,既保留协议标准性,又不会在实现细节上卡你两周。
2.3 ARP、IP 校验和、CRC,这三个“周边技能”躲不开
既然要跟 PC 通信,有些事是不能假装不存在的,哪怕你用现成 IP 核也一样会撞到:
- ARP:PC 要往某个 IP 发数据前,先问一句“谁是 192.168.1.20?把你的 MAC 告诉我”。FPGA 如果不回 ARP,看起来就像“IP 不存在”,UDP 包根本不会发给我们。
- IP 校验和:IP 头里有一个 16 bit 校验和,算法是“按 16 bit 累加并回卷、最后取反”。这一层是对 IP 头本身的防护,不是对数据负载的防护。
- CRC:以太网帧末尾的 FCS 字段,由 MAC 层生成,PHY 不负责。如果你不用现成 MAC IP,这个 CRC 就需要自己在硬件里算,是一个很容易漏掉的小尾巴。
这三个东西听起来是协议细节,但它们直接决定你能不能跟 PC 正常对话。后面第 3 节会具体展开,这里先建立概念:不要以为 UDP 就是“发个端口号再把数据扔出去”。
3. 最小 UDP 回环逻辑:从发送状态机到接收判活的完整骨架
我一直觉得,FPGA 网络通信最有效的“初学验收项目”不是往电脑发“Hello World”,而是做一个 UDP 回环:PC 发给 FPGA 一个 UDP 包,FPGA 把负载原封不动再发回 PC。它覆盖了发送、接收、帧解析、字段构造,却没有掺入复杂业务,所有问题都能在 PC 端一票到底看清。
3.1 发送方向:数据从 FIFO 到以太网帧需要做哪几件事
我要先说清楚,下面这种状态机只是教育向的“思路描述”,不是可以直接落板的完整工程。实际工程里你还需要处理跨时钟域、FIFO 空满、复位不同步、CRC 生成和 RGMII 位宽转换,但骨架逻辑是通用的。
FPGA 发送 UDP 包时,它应该按顺序输出这些字节:
- 前导码加帧起始符:连续 7 个 0x55,再跟一个 0xD5。
- 目的 MAC:6 字节,表示这个帧要发给谁。
- 源 MAC:6 字节,也就是 FPGA 自己的 MAC 地址。
- 以太网类型:0x0800 表示后面是 IPv4 包。
- IPv4 头:这里要填版本、头长度、总长度、标识、TTL、协议号 17,以及源和目的 IP。
- UDP 头:源端口、目的端口、UDP 长度,计算校验和。
- 业务数据。
- FCS 校验字段:4 字节 CRC。
写成 Verilog 状态机,就是逐个状态“吐”固定字节。比如:
localparam [7:0] PREAMBLE = 8'h55; localparam [7:0] SFD = 8'hD5; localparam [7:0] IP_TYPE = 8'h08, IP_TYPE_H = 8'h00; // 0x0800 reg [3:0] state; reg [15:0] byte_cnt; reg [7:0] tx_data; reg tx_valid; always @(posedge user_clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; tx_valid <= 1'b0; tx_data <= 8'h00; end else begin case (state) IDLE: begin if (fifo_empty == 1'b0) begin state <= PREAMBLE; byte_cnt <= 16'd0; tx_valid <= 1'b1; end end PREAMBLE: begin tx_data <= PREAMBLE; if (byte_cnt == 16'd7) begin byte_cnt <= 16'd0; state <= SFD_ST; end else begin byte_cnt <= byte_cnt + 1'b1; end end SFD_ST: begin tx_data <= SFD; state <= ETH_DST_MAC; end // 后续依次发送目的 MAC、源 MAC、类型、IP 头、UDP 头、负载、CRC ... endcase end end每个字段发送的字节数固定,所以你可以用byte_cnt驱动一个大的 case,也可以把整个发送内容做成 ROM 表。为清晰起见,通常会把固定头字段做成一组参数或寄存器,负载部分从 FIFO 读出。
发 UDP 数据时有个经典错误,就是负载少于 18 字节时,整个帧长度不足 64 字节。以太网标准要求最短帧 64 字节(从目的 MAC 到 FCS),所以如果负载很小,你要在负载和 FCS 之间补 0,直到总长度到 64 字节。很多新手第一次量波形时百思不得其解,为什么数据后面多了一串 0,就是因为没做 padding。直接用现成 MAC IP 的话,IP 核通常会自动处理这个补充,但判断是否在线仍要看你自己的逻辑。
3.2 字段精确到字节:IP 头里每一个数都是怎么算的
我给你一个比较完整的 IPv4 头构造流程,这样代码不容易漏。
| 偏移(字节) | 字段 | UDP 发送时的取值 |
|---|---|---|
| 0 | 版本与 IHL | 0x45,IPv4,头长 20 字节 |
| 1 | DSCP/ECN | 0x00,暂不关心 |
| 2-3 | IP 总长度 | 20 + 8 + payload_len |
| 4-5 | Identification | 可自增,也可固定 |
| 6-7 | Flags 与 FragmentOffset | 0x0000 |
| 8 | TTL | 0x40 或 0x80 |
| 9 | Protocol | 0x11,UDP |
| 10-11 | Header Checksum | 对 IP 头 20 字节按 16 bit 累加后取反 |
| 12-15 | Source IP | FPGA 的 IP |
| 16-19 | Destination IP | PC 的 IP |
UDP 头相对简单:源端口、目的端口、UDP 长度(8+payload_len)、校验和。UDP 校验和比较特殊,它算的不仅包含 UDP 头和数据,还要把 IP 头里的“伪头”带进去。伪头包括源 IP、目的 IP、协议号、UDP 长度,是一个只在计算校验和时存在的逻辑结构。
校验和算法上,硬件里最常用的是循环进位累加:把数据按 16 bit 切分,依次相加,如果产生进位就回卷到最低位,等所有字段加完之后取反。
// 伪代码,表达思路 // sum = 0; // for (each_word) begin // sum = sum + each_word; // if (sum 超过 16 bit) sum = (sum[15:0] + 1'b1); // end // checksum = ~sum[15:0];这个计算本身不复杂,但它必须在发送 IP 头之前算完。因此很多实现里是先攒完一整个 UDP 包,最后把校验和回填到 header 的固定位置,再用 DMA 方式发出。如果你的吞吐要求不高,可以先存 BRAM 再发,省去大量现场计算时序。
3.3 接收方向:FPGA 不是每个收到以太网帧都要管
接收逻辑的第一步是找帧头。PHY 通过 RGMII 接口把字节送进来,前导码和 SFD 在物理层上已经被 PHY 的 MAC 侧剥离或者仍可见,取决于你接入的是 MAC 核还是裸 RGMII 数据。如果是在 RGMII 原始信号上自己拼 MAC,就要靠状态机搜索连续的 0x55 和随后的 0xD5,遇到 SFD 之后才能认定这是一个完整帧的开始。
第二步是做目的 MAC 过滤。FPGA 的网卡口收到的帧目的 MAC 通常有三类:单播给自己的、广播 ff:ff:ff:ff:ff:ff、多播。如果你不想做多播,只比较单播和广播即可。目的 MAC 不匹配的帧直接丢掉,否则你的 FIFO 会被局域网里的无关广播塞满。
第三步是对 IP 头做基本解析。检查版本是 0x45、协议字段是 0x11,然后看目的 IP 是否是自己的 IP,再看 UDP 目的端口是不是自己关心的端口。这一步看起来是一堆 if,实际上就是用一个小的状态机把帧头逐个字节扫过去,边扫边把负载写入 FIFO。比较符合人直觉的做法是先收到一个完整帧到 BRAM,再解析;但高吞吐场景下,真正高效的是流水解析,也就是边走边判断。
3.4 回环先加业务数据:从“能通”到“能干活”的过渡
真正让新手有成就感的瞬间,不是 WireShark 上出现一个 UDP 包,而是 PC 发进去一个包,回环出来的负载和原来一模一样。验证方法很直接:写一个上位机或者用网络调试助手,发送一串0x01 0x02 0x03 0x04,再查看收到的内容是否一致。
这个阶段有一个非常隐蔽的坑,我当年排查了好几天。接收和发送用的是不同时钟,接收时钟来自 PHY 提取的 RX_CLK,发送时钟是系统 125MHz 全局时钟。如果你直接把接收数据送到发送 FIFO,完全不做异步处理,偶尔会出现字节错乱。这就像两条不同节奏的传送带,A 传送带和 B 传送带速率差一点,交接的时候就会掉箱子。解决办法也很常规,中间加一个异步 FIFO,并且要正确计算 FIFO 深度。UDP 包最大负载大约 1472 字节,加上头也就是 1500 字节左右,一个 2KB 或 4KB 的异步 FIFO 在这个场景完全够用。
4. 板子不开口说话?我从 PHY 寄存器一路查到 WireShark 的排查顺序
前面把原理和简单逻辑讲明白了,接下来是最有价值的实战经验。新板子第一次调网络,很可能出现“灯亮但不通”“抓不到包”“报 CRC 错误”等各种问题。这时候最忌讳的就是乱试,正确做法是按一条稳定链路从底层往上层排。
4.1 先查 PHY 是不是真的“活着”:MDIO 读 ID 和链接状态
如果你的 PHY 管理接口都还没通,后面 FPGA 里的 MAC 再正确也没有用。调试的第一步,就是用 VIO 或者临时仿真去读 PHY 寄存器。
读寄存器 2 和寄存器 3,可以拿到 PHY 的厂商型号 ID。比如 RTL8211 系列的 ID 是固定的,你用这个值逆推,厂商 ID 能对上,说明 MDC/MDIO 通路没问题。随后读取寄存器 1,它的 bit 位通常记录当前链接状态、速率和双工模式。记得给你 FPGA 的程序里留出几个只读寄存器,让 CPU 或者 JTAG 能把 PHY 状态读出来,这样后续联调方便很多。
这个环节最常出现的三个现象:MDIO 一直在回 1、回 0、读到 ID 全 F。回 1 或全 F 通常说明 MDIO 总线上没有设备应答,可能是 PHY 地址错了,也可能是上电复位没完成,或者是 PHY 的复位引脚一直没释放。回 0 可能是 PHY 地址配置成了另一个值,或者时钟根本没起来。
4.2 插上网线看灯:Link 状态比任何代码指示都可靠
很多开发板上,RJ45 座子本身就有 LED 指示灯,其中一个用于指示 Link。插上网线,Link 灯亮说明 PHY 的协商过程已经完成了。如果 Link 不亮,基本上不用继续调 FPGA,先检查网线、接口电平、PHY 供电和复位。有时候是网线质量问题,有时候是千兆口插到了百兆交换机,它会用不同线对工作,问题表现完全不一样。
这一步优先做,是因为它可以把你从“FPGA 代码背锅”里救出来。我见过太多人调了一天,最后发现是开发板那个网口是百兆 PHY,而自己的代码按千兆 125MHz 发数据,速率从根上就不对。看一眼 PHY 手册支持的速率,再配置对应的时钟,能省掉好几轮自我怀疑。
4.3 用 WireShark 接收 PC 网卡数据,这是最快判断帧内容的方式
FPGA 主动周期性向 PC 发送 UDP 包,比被动等 PC 发包更容易验证。你只需要在 FPGA 里做一个 1 秒翻转的计数器,每次触发就发一个 UDP 广播包,目的 IP 可以是 255.255.255.255,目的端口固定一个数。PC 端 WireShark 打开后选择有线网卡,立刻能看到源地址是 FPGA 的 MAC。
这个阶段我建议不要开网络调试助手,直接上抓包工具。因为网络调试助手只会告诉你“收到了数据”,却不会告诉你“这一帧的源 MAC、IP 校验和、以太网类型到底对不对”。WireShark 有比较完整的报文解析,如果 IP 头校验和错误、UDP 长度不匹配,它会直接标红。看到具体字段之后,再对应回代码里的逻辑,往往一眼就能定位。
抓包时注意把 PC 的有线网卡 IP 设成一个静态地址,比如 192.168.1.10,但不要开 DHCP,也不要让 PC 同时连着 WiFi。多网卡场景下 WireShark 默认抓包会看到大量广播,容易干扰你的注意力。如果你没有办法直连,也可以把 FPGA 接到一个普通交换机上,但要注意交换机会过滤一些非法 MAC 帧,也会给 ARP 带来额外行为,第一次调还是直连最干净。
4.4 RGMII 采不到数据的几个隐蔽原因
当你做板级调试时,可能会遇到 PHY 和上层都在跑,但 FPGA 就是采不到有效数据的情况。排除了代码状态机问题后,剩下的原因大多是 RGMII 的时序问题。
RGMII 是双沿采样,数据信号和时钟之间有相对延迟要求。有的 PHY 芯片靠板卡设计来保证时序,有的则需要你在 FPGA 内部加 IDELAY。Xilinx 7 系列 FPGA 上,经常要在接收时钟的输入路径上加IDELAYE2,把延迟调到 2ns 左右,否则每个 bit 都可能采在跳变沿上。这个问题最让人崩溃的地方是:仿真里一切正常,rx_valid一直拉高,但一到真实板子,数据就开始间歇性出错。原因不是你的逻辑错了,而是输入 delay 没有约束。如果你是跟着某个开发板的设计走,先确认它的参考工程是否加了set_input_delay,然后原封不动搬过来。别自己“优化”删掉,我删过一次,后果就是接收数据整段错位。
还有一个看起来很小但经常被忽略的问题:时钟缓冲。PHY 返回的 RX_CLK 在 FPGA 里最好经过IBUFG或者专用时钟引脚进来,不要随便经过普通逻辑再当地址用。否则时钟抖动和 skew 会在高速下无限放大,日常抓包偶尔通偶尔不通,就是典型的时钟没处理好。
4.5 典型抓包现场:看到这些异常能直接反应出问题点
我总结了几类非常典型的现象,方便你以后对号入座:
| WireShark/工具现象 | 最可能的根因 |
|---|---|
| 完全看不到 FPGA 发出的包 | PHY 没发数据、FPGA 发送状态机没启动、目的 MAC 全 0 |
| 看到包但标 Red or Bad CRC | FCS 生成错误,或发送帧长度不足,CRC 位置算错 |
| IP Header checksum 错误 | IP 校验和字段没写,或者发送顺序把高低字节弄反 |
| UDP checksum 为 0 且工具显示“可能无效” | 你没有计算校验和,发包器允许但大多数协议栈不推荐 |
| 能看到 ARP 请求但 PC 不继续发 UDP | FPGA 没回 ARP,PC 不知道目的 MAC |
| 收到数据但内容错位几个字节 | RGMII 采样时序偏了,或者接收 FIFO 位宽拼接反了 |
| 数据包正常但 PC 应用收不到 | UDP 目的端口被防火墙挡了,或本地端口没绑定 |
第二个问题的典型例子:如果你自己写 MAC 但没有在帧末尾追加 FCS,网卡会认为整个帧少 4 字节或者 CRC 计算失败。PC 网卡通常不会把坏帧交给应用层,于是你现象上看到“PC 什么都收不到”,但 WireShark 却能抓到该帧并错误标记。所以抓到包但应用收不到,优先去看 WireShark 里的错误提示,而不是去上位机代码里翻。
5. 回环已经通了,顺着这条路往真实项目走还需要补什么
如果只是做一个演示,到回环通了就已经可以收工了。但从实际项目角度说,网络通信只是数据通路里的一个环节。绝大多数时候,你要把这个 UDP 通道接到自己的摄像头、ADC 或高速信号采集模块里。这一步会带来几个新问题。
5.1 负载怎么组装:从“一次性发一个包”到“连续流式发送”
真实业务里,不是等数据攒好了再发,而是业务数据源源不断产生,你需要在固定时间内把它打包发送。这里有一个关键思路:把负载做成“定长帧”,每个 UDP 包固定承载 1024 字节或 1400 字节,包头里加上帧序号。比如图像数据,一帧 1280x720x2 字节的数据可以拆成几十个 UDP 包,接收方通过序号重组。这样比在 FPGA 里做巨型 FIFO 等整帧图像要节省资源得多。
发送侧的状态机也不再只是“从 FIFO 读几个字节”,而是需要从业务 FIFO 固定读取一段长度,按顺序拼好各个字段,然后启动发送。业务 FIFO 的读指针要跟着包边界走,不能跨包乱读。如果业务数据超过 MTU,就要在 IP 层做分片。但分片会增加复杂度,接收端重组也更麻烦,最常见的做法是主动把负载控制在 1400 字节以下,避免分片。
5.2 时钟约束与复位设计,会成为板级稳定性的分水岭
网络通信模块牵扯多个时钟域:用户逻辑时钟、发送时钟、接收时钟、MDIO 时钟。板级联调之前,你最好先用综合工具查看一下时序报告,看看那些跨时钟域路径有没有被约束。专业做法是给发送时钟、接收时钟分别建立 clock group,再用异步 FIFO 隔离。不要以为程序能跑就万事大吉,很多“偶尔丢包”“连续传几分钟后死掉”的问题,最后查出来都是跨时钟域信号没有同步好。
复位设计也一样。不要用全局异步复位把整个发送通路打一下就完事,你最好做“异步复位、同步释放”,同时保证复位释放的时刻不在发送一帧的中间。因为如果复位发生在某帧的第 3 个字节,发出去的是一段半截帧,对端要么丢弃,要么把它当成坏帧送进上层。好的习惯是:复位只让状态机回到 IDLE,FIFO 的读写指针单独清空,清空完成后延时几个时钟再放开。
5.3 什么时候你该把目光从 UDP 转向 TCP 或 Linux 侧
这里是一个比较功利但真实的问题。如果业务要求高可靠传输、对乱序敏感、需要确认应答,纯 FPGA UDP 方案就不再合适。你有两条比较主流的方向。
第一条:继续用 FPGA 做数据通路,但把上层协议交给 Zynq 里的 ARM 或 MicroBlaze 软核,跑 LwIP 这类协议栈。这种做法的优点是把复杂 TCP 实现交给人更熟悉的软件环境,FPGA 只提供 DMA 接口和高速收发通道。第二条:如果项目里干脆有一颗带网口的 MCU 或应用处理器,那就没必要在 FPGA 里硬磕 TCP,FPGA 通过 PCIe 或 AXI 把数据送给处理器,处理器负责 TCP 协议栈和文件写入。很多高速数据采集设备都是这种“FPGA 采集 + CPU/SoC 协议处理”的拓扑。
换句话说,FPGA 网络通信设计并不是要求你在 FPGA 里把七个网络分层全部实现一遍。你需要做的是:站在工程全局,决定哪些层放在 FPGA 硬逻辑里,哪些层放在处理器软件里。初期学习时用纯 RTL 实现一个 UDP 发送通路,是因为这能让你真正理解每一字节的来龙去脉。到了产品阶段,合适地用 IP 核、用 ARM、用现成协议栈,都是更聪明的选择。
我在做回环实验时最后还有一个习惯:无论用哪块开发板,我都会先把 FPGA 的 MAC、IP、源端口做成寄存器参数,方便后面的模块随时改地址。这样调试时不需要重新综合整个工程,只为换一个 IP 又等 30 分钟。网络通信调试本身就是个“眼力活”,抓包、看状态、改参数、再抓包,循环次数比写代码多得多。把这套排查顺序跑顺之后,你回头再看 part.7 的标题,应该会觉得网络通信其实并没有那么高不可攀,它只是一串零件加一堆状态机的组合而已。