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配置,让你能真正跟网络上的设备“对话”。