18、以太网通信入门:OSI模型与TCP/IP协议栈、RGMII接口时序、UDP收发实现
以太网通信,说白了就是让FPGA跟电脑、路由器或者其他设备“说上话”。很多初学者一听到“协议栈”、“MAC地址”、“IP头”就头大。别急,咱们今天就把这层窗户纸捅破。
我个人习惯把以太网通信拆成三块来理解:分层模型、物理接口、数据收发。这三块搞明白了,你就能在FPGA上跑起一个简单的UDP通信。
18.1 OSI模型与TCP/IP协议栈:别被七层吓到
OSI七层模型,教科书上写得很全。但做FPGA开发,你真正需要关心的只有下面四层:
- 物理层:负责把0和1变成电信号或光信号。对应到FPGA,就是PHY芯片和RGMII接口。
- 数据链路层:负责帧的封装、MAC地址寻址、差错检测。FPGA里的MAC控制器就干这个。
- 网络层:负责IP地址路由。我们做UDP时,需要手动构造IP头。
- 传输层:负责端口到端口的通信。UDP和TCP就在这层。
你想想看,TCP/IP协议栈其实是对OSI的简化。实际项目中,我们通常只用到MAC + IP + UDP这三层。TCP?嗯,状态机太复杂,FPGA里做起来比较费劲,除非你上软核或者专用IP。
核心要点:FPGA做以太网,本质就是逐层打包和解包。发送时,从UDP数据开始,一层层套上头部;接收时,一层层剥掉头部。
18.2 RGMII接口时序:时钟沿的“双倍快乐”
RGMII是Reduced Gigabit Media Independent Interface的缩写。说白了,它用更少的引脚实现了千兆速率。
为什么叫“双倍快乐”?因为它在时钟的上升沿和下降沿都传输数据。比如,时钟125MHz,双边沿采样,数据速率就是250Mbps,四根数据线加起来就是1Gbps。
我记得第一次调RGMII时序时,被那个时钟偏移坑了一把。PHY芯片输出的RXC时钟,跟数据之间有一个固定的相位关系。有些PHY要求FPGA在发送时把时钟延迟90度,有些则不需要。你得仔细看数据手册。
避坑指南:我曾经在PGL22G上调试RGMII,发现数据老是丢包。查了两天,最后发现是PCB走线等长没做好,导致时钟和数据之间的skew超标。千兆以太网对PCB布局要求很高,DDR走线必须等长,误差控制在±50mil以内。
RGMII的典型时序参数如下:
| 参数 | 最小值 | 典型值 | 最大值 | 单位 |
|---|---|---|---|---|
| 时钟频率 | 125 | 125 | 125 | MHz |
| 数据建立时间 | 1.0 | - | - | ns |
| 数据保持时间 | 1.0 | - | - | ns |
| 时钟占空比 | 45 | 50 | 55 | % |
在FPGA内部,我们通常用IDDR和ODDR原语来处理RGMII的双边沿数据。紫光同创的PGL系列有对应的原语,叫IDDR和ODDR,用法跟Xilinx的差不多。
// RGMII接收端,使用IDDR原语
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED")
) u_rx_iddr (
.Q0(rxd_0), // 上升沿数据
.Q1(rxd_1), // 下降沿数据
.D(rxd), // RGMII数据输入
.C(rxc), // 接收时钟
.CE(1'b1),
.R(1'b0),
.S(1'b0)
);
18.3 UDP收发实现:从零开始搭一个“快递系统”
UDP是无连接的,说白了就是“发出去就不管了”。这很适合FPGA,因为我们不需要维护复杂的连接状态。
一个完整的UDP帧长这样:
- 前导码(8字节):同步用,固定为0x55 0x55 0x55 0x55 0x55 0x55 0x55 0xD5
- MAC头(14字节):目的MAC(6字节)+ 源MAC(6字节)+ 类型(2字节,0x0800表示IP)
- IP头(20字节):版本、长度、标识、TTL、协议(17表示UDP)、源IP、目的IP、校验和
- UDP头(8字节):源端口、目的端口、长度、校验和
- 数据(46~1500字节):你要发送的有效载荷
- FCS(4字节):CRC32校验,由MAC硬件自动生成
你想想看,发送一个UDP包,其实就是把这些字段按顺序拼起来,然后通过RGMII发出去。接收就是反过来,一个个字段解析。
个人经验:我建议初学者先实现一个回环测试。就是FPGA收到UDP包后,把源MAC和目的MAC互换,源IP和目的IP互换,源端口和目的端口互换,然后把数据原封不动发回去。这样用电脑上的网络调试助手就能验证通信是否正常。
下面是一个简化的UDP发送状态机:
localparam IDLE = 4'd0;
localparam PREAMBLE = 4'd1;
localparam MAC_HEAD = 4'd2;
localparam IP_HEAD = 4'd3;
localparam UDP_HEAD = 4'd4;
localparam DATA = 4'd5;
localparam FCS = 4'd6;
always @(posedge clk or posedge rst) begin
if (rst) begin
state <= IDLE;
end else begin
case (state)
IDLE: begin
if (tx_start) state <= PREAMBLE;
end
PREAMBLE: begin
if (byte_cnt == 7) state <= MAC_HEAD;
end
MAC_HEAD: begin
if (byte_cnt == 13) state <= IP_HEAD;
end
// ... 依次类推
endcase
end
end
这里有个坑:IP头校验和。它是对IP头部的每16位进行反码求和。很多初学者在这里算错,导致电脑收不到包。我一般用查表法或者流水线加法器来做,确保每个时钟周期都能算完。
曾经踩过的坑:UDP校验和是可选的,在IPv4里可以填0。但有些路由器或交换机可能会丢弃校验和为0的UDP包。我建议你还是老老实实算一下,虽然麻烦点,但兼容性最好。
接收端相对简单一些。你只需要检测前导码的结束标志(0xD5),然后开始接收MAC头,判断类型是否为0x0800,接着解析IP头,判断协议是否为17(UDP),最后提取UDP数据。
嗯,这里要注意:FCS校验通常由MAC硬核自动完成。如果你用的是紫光同创的GMAC硬核,它会在接收时自动校验CRC,你只需要关注有效数据就行。如果是纯逻辑实现,那你得自己算CRC32。
最后说一句,UDP在FPGA里的典型应用场景是高速数据采集。比如ADC采样数据,通过UDP实时上传到电脑。我做过一个项目,用千兆UDP实现了800Mbps的持续传输,丢包率控制在10^-6以下。关键点在于:发送FIFO要够深,并且要做好流控。
好了,这一章的内容就到这里。以太网通信入门,说白了就是理解分层、搞定时序、实现收发。下一章我们会深入讲ARP协议和IP配置,让你能真正跟网络上的设备“对话”。