Linux驱动:RK3568 GMAC + YT8531C 千兆以太网全流程解剖

从零到跑满千兆:RK3568 GMAC + YT8531C 全流程解剖

平台:RK3568 / GMAC0 / YT8531C / RGMII

内核:Linux 5.x(DWMAC 4.20a)

目标:把一块开发板的千兆网口从"编译进内核"一路调到"iperf3 跑满 940Mbps"

0. 阅读地图

本文分七幕,从硬件到协议栈、从配置到调试,逐步深入。建议第一次读按顺序,第二次可以当手册查。

复制代码
[1] 硬件与背景          ← 新增:GMAC / PHY / 变压器 / RJ45 全解剖
      │
[2] 内核网络栈全景      ← 建立心智模型
      │
[3] Kconfig + 设备树    ← 让驱动跑起来
      │
[4] Probe → Link Up     ← 上电到链路
      │
[5] TX / RX 数据路径    ← 报文的一生
      │
[6] DHCP / ARP          ← 最后一公里
      │
[7] 调试与调优          ← 从"能通"到"跑满"

1. 硬件与背景

1.1 GMAC 是什么

RK3568 的 GMAC(Gigabit Media Access Controller)是基于 Synopsys DesignWare Ethernet QoS IP 的千兆以太网控制器,具备三个特点:

  1. 基于 Synopsys DWMAC 4.20a IP------所以你能在内核里看到大量 dwmac_*、stmmac_* 前缀的源码。
  2. 支持 RGMII 和 RMII 两种 PHY 接口(RK3568 板级通常用 RGMII)。
  3. 内置高性能 DMA 控制器------这是它跑满千兆的关键。

1.2 GMAC 内部框图

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                        CPU / AXI 总线                              │
│         APB(寄存器配置)          AXI(DMA 数据搬运)              │
└──────────┬───────────────────────────────┬───────────────────────┘
           │                               │
    ┌──────▼──────┐                 ┌──────▼──────┐
    │  CSR 寄存器  │                 │   DMA 引擎   │
    │ (配置/状态)  │                 │ (描述符管理)  │
    └─────────────┘                 └──────┬──────┘
                                           │
                                    ┌──────▼──────┐
                                    │  MTL 层     │
                                    │ (FIFO 管理)  │
                                    │ Tx 16KB     │
                                    │ Rx 32KB     │
                                    └──────┬──────┘
                                           │
                                    ┌──────▼──────┐
                                    │  MAC 层     │
                                    │ (帧处理)     │
                                    │ 流控/过滤/CRC│
                                    └──────┬──────┘
                                           │
                                    ┌──────▼──────┐
                                    │ RGMII/RMII  │
                                    │   PHY 接口   │
                                    └─────────────┘

三层分工明确:

DMA(引擎)

  • 通过 AXI 总线搬运数据。
  • 管理描述符环,通知 CPU 收发完成。

MTL(Memory Transaction Layer)

  • 位于 DMA 和 MAC 之间,本质是智能 FIFO 管理器 + 队列调度器。
  • MAC 是串行的、按位/字节流的,速度由链路速率决定;DMA 是突发的、按块的,速度由总线决定。MTL 就是这两种"节奏"之间的缓冲。
  • Tx FIFO 16KB / Rx FIFO 32KB,数据积累到阈值就启动搬运/发送。
  • 支持多队列,每个队列有独立的 FIFO 和调度算法。

MAC(Media Access Control)

  • 帧封装/解封装、CRC 校验、前导码生成。
  • 流控:全双工发 PAUSE 帧请求对端暂停;半双工用背压(制造冲突)阻止对端。
  • 地址过滤:决定哪些帧收下、哪些丢弃。
  • VLAN 处理。

1.3 RGMII 接口

RGMII 是 MAC 和 PHY 之间的千兆接口标准(Reduced Gigabit MII),信号如下:

信号 方向 说明
gmac_txc MAC → PHY 发送时钟(125MHz,双沿采样)
gmac_rxc PHY → MAC 接收时钟(PHY 提供)
gmac_txctl MAC → PHY 发送使能 + 错误
gmac_rxctl PHY → MAC 接收使能 + 错误
gmac_txd[3:0] MAC → PHY 发送数据(4 位,双沿 = 8 位/周期)
gmac_rxd[3:0] PHY → MAC 接收数据

关键约束 :RGMII 时钟和数据同源发出,接收端必须在数据稳定窗口的中央 采样。这就是后面 tx_delay / rx_delay 存在的唯一理由------人为制造时钟与数据之间的相位差。

1.4 MDIO 接口

MAC(或 CPU)通过 MDIO 读写 PHY 寄存器:

信号 说明
gmac_mdc 管理数据时钟(最高 ~2.5MHz)
gmac_mdio 管理数据输入输出(双向,需要上拉)

物理上像 I2C,但协议不同:MDIO 是单主多从,MAC 发起,PHY 应答。

1.5 数据通路(粗粒度)

发送路径:

复制代码
内存 → DMA 读描述符 → DMA 从内存取数据 → MTL Tx FIFO
     → MAC 加 CRC/前导码 → RGMII/RMII → PHY → 网线

接收路径:

复制代码
网线 → PHY → RGMII/RMII → MAC 校验 CRC/过滤 → MTL Rx FIFO
     → DMA 写入内存 → DMA 更新描述符 → 中断通知 CPU

1.6 PHY:物理层芯片

PHY 是"数字世界"和"模拟世界"之间的翻译官,职责多达七项:

  1. 数模转换:把 MAC 输出的数字信号转换成网线传输的模拟信号,反之亦然。
  2. 时钟恢复:网线没有独立时钟线,时钟嵌在数据里。PHY 接收时从数据流恢复时钟,再用它采样数据。
  3. 自协商:上电后自动和对端协商速率、双工、流控、主从模式,结果通过 MDIO 寄存器可读。
  4. 链路检测:持续检测对端是否在线。
  5. MDIO 管理接口:提供 MDIO/MDC,供 MAC/CPU 读写寄存器。
  6. 信号调理:均衡器、回声消除、串扰消除。
  7. 模拟侧输出差分线 :
    • 百兆:2 对(1 对专发、1 对专收)
    • 千兆:4 对同时收发,靠回声消除实现(发送端知道自己发了什么,接收时从混合信号中减去自己发送的部分)

1.7 网络变压器

在 PHY 和 RJ45 之间,通常还有一个网络变压器,作用有五:

  1. 电气隔离:初级/次级之间绝缘(通常 1500V AC),防止两端地电位差损坏芯片,也防雷击浪涌。
  2. 阻抗匹配:PHY 输出阻抗(100Ω 差分)和网线特性阻抗(100Ω)匹配,减少反射。
  3. 共模抑制:对共模噪声有很强抑制能力,提高 EMI 性能。
  4. 信号耦合:通过电磁感应传递差分信号,隔断直流。
  5. Bob Smith 终端:变压器中心抽头通过 75Ω 电阻 + 1nF 电容接地,提供共模终端,进一步抑制 EMI。

1.8 RJ45 接口

RJ45 是 8 引脚接口,对应 4 对差分线:

引脚 千兆 百兆
1,2 BI_DA+/- TX+/-
3,6 BI_DB+/- RX+/-
4,5 BI_DC+/- 未用
7,8 BI_DD+/- 未用

千兆下 4 对同时双向传输;百兆下只用 1,2,3,6 两对,且一收一发。

1.9 YT8531C:以太网收发器

YT8531C 是 Motorcomm(裕太)的千兆 PHY,与 RTL8211F 引脚兼容,但延时配置方式不同。

引脚总览(一端接 GMAC,一端接网线):

复制代码
                    ┌──────────────────────┐
          TRXN3  10 │                      │ 30  REG_O/LDO_O/NC
          TRXP3   9 │                      │ 29  DVDD33
          AVDDL   8 │                      │ 28  DVDD_RGMII
          TRXN2   7 │                      │ 27  RX_CLK/PHYAD1
          TRXP2   6 │                      │ 26  RX_CTL/PHYAD2
          TRXN1   5 │                      │ 25  RXD0/RXDLY
          TRXP1   4 │                      │ 24  RXD1/TXDLY
          AVDDL   3 │                      │ 23  RXD2/PLLOFF
          TRXN0   2 │                      │ 22  RXD3/PHYAD0
          TRXP0   1 │                      │ 21  DVDDL
                    │                      │
          GND(EPAD) │    YT8531(D)(P)      │
                    │                      │
          AVDD33 11 │                      │ 20  TX_CLK
          RESET_N 12│                      │ 19  TX_CTL
          MDC     13│                      │ 18  TXD0
          MDIO    14│                      │ 17  TXD1
          TXD3    15│                      │ 16  TXD2
                    └──────────────────────┘

上电配置引脚(Strapping Pins)

复位释放时,以下引脚的电平被锁存为配置信息:

配置引脚 功能 上拉 下拉
PHYAD2:0(26,27,22) PHY 地址(0~7) - -
TXDLY(24) TX 时钟延迟使能 加延迟 不加
RXDLY(25) RX 时钟延迟使能 加 2ns 延迟(默认) 不加
CFG_EXT(32) IO 电源模式 外部电源 内部 LDO
CFG_LDO1:0(34,33) LDO 输出电压/外部电压选择 见数据手册 见数据手册
PLLOFF(23) 睡眠模式 PLL 关闭 - -

注意 :Strapping 引脚只在复位释放瞬间采样。如果硬件已经把这些脚上拉/下拉固定死,软件层面就不需要再去改延时------除非硬件设计留有可配空间。

关键寄存器速查表

地址 名称 读写 用途
0x00 Basic Control RW 复位、环回、速度、自协商、掉电
0x01 Basic Status RO 链路状态、自协商完成
0x02 PHY ID1 RO 0x4F51(识别 YT8531)
0x03 PHY ID2 RO 0x3A1x(型号+版本)
0x04 Auto-Neg Advertisement RW 10/100M 能力通告
0x05 Link Partner Ability RO 对端能力
0x09 1000BASE-T Control RW 千兆能力通告、主从配置
0x0A MASTER-SLAVE Status RO 主从结果、接收器状态
0x10 PHY Specific Function Ctrl RW Auto-MDIX、极性、SQE 测试
0x11 PHY Specific Status RO 速度/双工/链路实时状态
0x12 Interrupt Mask RW 中断使能
0x13 Interrupt Status RO/RC 中断状态(读清除)
0x1E Extended Address RW 扩展寄存器地址
0x1F Extended Data RW 扩展寄存器数据
EXT_0xA001 Chip_Config RW 复位、LDO、RX 延迟
EXT_0xA003 RGMII_Config1 RW RGMII TX/RX 延迟线配置
EXT_0xA004 RGMII_Config2 RW RGMII 状态/配置
EXT_0xA005 MDIO_Cfg RW PHY 地址 0、广播地址
EXT_0xA006 Misc_Config RW Jumbo、远端环回
EXT_0xA007-9 MAC_Address_Cfg RW WOL MAC 地址
EXT_0xA00A WOL_Cfg RW WOL 使能、INT/PME 选择
EXT_0xA00B-F LED Cfg RW LED 配置
EXT_0xA010 Pad Drive Strength RW IO 驱动强度
EXT_0xA012 SyncE_CFG RW SyncE 时钟输出
MMD3/0x14 EEE Capability RO EEE 支持能力
MMD7/0x3C Local EEE Ability RW 本地 EEE 通告
MMD7/0x3D Link Partner EEE RO 对端 EEE 能力

在哪里读这些寄存器?

bash 复制代码
# 读标准寄存器
mii-tool -v eth0                 # 老工具,看基本状态
ethtool eth0                     # 看协商结果

# 读扩展寄存器(用 phytool 或 mdio-tools)
phytool read eth0/0/0x11         # PHY Specific Status
phytool read eth0/0/0xA003       # RGMII 延时配置

1.10 三块拼图总结

组件 角色 RK3568 上的实例
GMAC 以太网 MAC 控制器(DWMAC 4.20a) fe2a0000.ethernet
PHY 物理层收发器(YT8531C) MDIO 地址 0
接口 MAC ↔ PHY 之间的总线 RGMII(4 位数据,双沿采样)

数据流全景(从 CPU 到网线):

复制代码
CPU ──AXI──▶ DMA ──▶ MTL ──▶ MAC ──RGMII──▶ PHY ──变压器──▶ RJ45 ──网线──▶ 对端

2. 内核网络栈全景

2.1 一张图看清分层

复制代码
┌──────────────────────────────────────────────┐
│  用户态:socket()/send()/recv()/ioctl()      │
├──────────────────────────────────────────────┤
│  VFS 层:socket 被抽象成文件                  │
│         struct file → socket_file_ops        │
├──────────────────────────────────────────────┤
│  协议层:TCP / UDP / IP / ICMP / ARP         │
├──────────────────────────────────────────────┤
│  网络设备接口层:                             │
│    struct net_device + net_device_ops        │
│    struct sk_buff(报文载体)                 │
├──────────────────────────────────────────────┤
│  驱动层:stmmac_main.c / dwmac-rk.c          │
├──────────────────────────────────────────────┤
│  硬件:GMAC DMA → MAC → PHY → 网线           │
└──────────────────────────────────────────────┘

2.2 两个核心数据结构

struct sk_buff:报文的"集装箱"

  • 本身不存数据,只存指针:head/end 指向缓冲区起止,data/tail 指向当前协议层数据起止。
  • head ~ data 之间的空隙是预留给各层"往下加头"的空间------这就是为什么 TCP/IP 加头不需要拷贝。
  • next/prev 支持分片链表;cb[] 是各层存放私有信息的控制块。

struct net_device:网卡的"抽象基类"

  • 描述设备属性:name、dev_addr、mtu、state、features。
  • 通过两个 ops 指针暴露行为:
    • netdev_ops:ndo_open、ndo_stop、ndo_start_xmit 等
    • ethtool_ops:ethtool 工具调用的入口
  • 这是典型的策略模式 :net_device 是上下文,netdev_ops 是可替换的策略。

2.3 socket 如何接入 VFS

内核为 socket 专门注册了一个伪文件系统 sockfs:

c 复制代码
// net/socket.c
static int __init sock_init(void) {
    init_inodecache();
    register_filesystem(&sock_fs_type);   // 注册 "sockfs"
    sock_mnt = kern_mount(&sock_fs_type); // 内核内部挂载
}

struct socket 和 struct inode 被捆绑在同一个结构体 socket_alloc 里,通过 SOCKET_I() / SOCK_INODE() 互相转换。当应用调用 read()/write() 时,VFS 会调用 socket_file_ops 里的 sock_read_iter / sock_write_iter,从而进入网络子系统。

c 复制代码
static const struct file_operations socket_file_ops = {
    .read_iter  = sock_read_iter,   // read()/readv()
    .write_iter = sock_write_iter,  // write()/writev()
    .poll       = sock_poll,        // poll/epoll
    .unlocked_ioctl = sock_ioctl,   // ioctl
    .release    = sock_close,       // close()
    ...
};

小结 :socket 之所以能用 read/write,是因为内核给它伪造了一个文件系统。这是 Unix "一切皆文件"哲学在网络的延伸。


3. Kconfig 到设备树:让驱动跑起来

3.1 编译配置

最简配置(RK3568 + GMAC0 + YT8531C):

复制代码
CONFIG_NET=y
CONFIG_INET=y
CONFIG_PACKET=y
CONFIG_UNIX=y

# DWMAC 平台驱动
CONFIG_NETDEVICES=y
CONFIG_ETHERNET=y
CONFIG_NET_VENDOR_STMICRO=y
CONFIG_STMMAC_ETH=y
CONFIG_STMMAC_PLATFORM=y
CONFIG_DWMAC_ROCKCHIP=y
CONFIG_DWMAC_GENERIC=y
CONFIG_STMMAC_ETHTOOL=y
CONFIG_STMMAC_FULL=y

# PHY 框架(由 STMMAC_ETH 自动 select,一般不需要手配)
CONFIG_PHYLINK=y
CONFIG_PHYLIB=y
CONFIG_MDIO_DEVICE=y
CONFIG_OF_MDIO=y
CONFIG_FIXED_PHY=y
CONFIG_SWPHY=y

# YT 专用驱动
CONFIG_MOTORCOMM_PHY=y
CONFIG_ROCKCHIP_PHY=y

注:select 是 Kconfig 的自动依赖,不要和"用户手动开启"混淆。当你打开 STMMAC_ETH,PHYLIB/MII/PAGE_POOL 等会被自动拉进来。

3.2 设备树

设备树需要描述三件事:GMAC 控制器本身、MDIO 总线、PHY 设备。

dts 复制代码
/* GMAC 控制器 */
gmac0: ethernet@fe2a0000 {
    compatible = "rockchip,rk3568-gmac", "snps,dwmac-4.20a";
    reg = <0x0 0xfe2a0000 0x0 0x10000>;

    /* GMAC 核心中断 + 唤醒中断 */
    interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>,
                 <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-names = "macirq", "eth_wake_irq";

    /* 通过 GRF 控制 GMAC 行为 */
    rockchip,grf = <&grf>;

    /* 时钟:GMAC 主时钟、RGMII RX/TX、AXI、APB、PTP、XPCS 等 */
    clocks = <&cru SCLK_GMAC0>, <&cru SCLK_GMAC0_RX_TX>,
             <&cru SCLK_GMAC0_RX_TX>, <&cru CLK_MAC0_REFOUT>,
             <&cru ACLK_GMAC0>, <&cru PCLK_GMAC0>,
             <&cru SCLK_GMAC0_RX_TX>, <&cru CLK_GMAC0_PTP_REF>,
             <&cru PCLK_XPCS>, <&cru CLK_XPCS_EEE>;
    clock-names = "stmmaceth", "mac_clk_rx", "mac_clk_tx",
                  "clk_mac_refout", "aclk_mac", "pclk_mac",
                  "clk_mac_speed", "ptp_ref", "pclk_xpcs", "clk_xpcs_eee";

    /* AXI 域软复位(仅复位 DMA 部分) */
    resets = <&cru SRST_A_GMAC0>;
    reset-names = "stmmaceth";

    /* 可选特性 */
    snps,mixed-burst;
    snps,tso;

    /* AXI / MTL 配置 */
    snps,axi-config = <&gmac0_stmmac_axi_setup>;
    snps,mtl-rx-config = <&gmac0_mtl_rx_setup>;
    snps,mtl-tx-config = <&gmac0_mtl_tx_setup>;
    status = "disabled";

    /* 内置 MDIO 控制器 */
    mdio0: mdio {
        compatible = "snps,dwmac-mdio";
        #address-cells = <1>;
        #size-cells = <0>;
    };

    gmac0_stmmac_axi_setup: stmmac-axi-config {
        snps,wr_osr_lmt = <4>;   // 最多 4 个未完成写请求
        snps,rd_osr_lmt = <8>;   // 最多 8 个未完成读请求(RX 路径更敏感)
        snps,blen = <0 0 0 0 16 8 4>;  // 只允许 16/8/4 字节突发
    };

    gmac0_mtl_rx_setup: rx-queues-config {
        snps,rx-queues-to-use = <1>;
        queue0 {};
    };
    gmac0_mtl_tx_setup: tx-queues-config {
        snps,tx-queues-to-use = <1>;
        queue0 {};
    };
};

/* MDIO 总线上的 PHY */
&mdio0 {
    rgmii_phy0: phy@0 {
        compatible = "ethernet-phy-ieee802.3-c22";
        reg = <0x0>;
    };
};

/* GMAC 的板级配置 */
&gmac0 {
    phy-mode = "rgmii";
    clock_in_out = "output";   // GMAC 向 PHY 输出 125MHz

    /* YT8531C 复位引脚 */
    snps,reset-gpio = <&gpio2 RK_PD3 GPIO_ACTIVE_LOW>;
    snps,reset-active-low;
    snps,reset-delay-us = <0 20000 100000>;  // pre 0ms / assert 20ms / post 100ms

    /* 时钟父源与频率 */
    assigned-clocks = <&cru SCLK_GMAC0_RX_TX>, <&cru SCLK_GMAC0>;
    assigned-clock-parents = <&cru SCLK_GMAC0_RGMII_SPEED>, <&cru CLK_MAC0_2TOP>;
    assigned-clock-rates = <0>, <125000000>;

    pinctrl-names = "default";
    pinctrl-0 = <&gmac0_miim &gmac0_tx_bus2 &gmac0_rx_bus2
                 &gmac0_rgmii_clk &gmac0_rgmii_bus>;

    /* RGMII 延时:YT8531C 每步 150ps */
    tx_delay = <0x24>;
    rx_delay = <0x33>;

    phy-handle = <&rgmii_phy0>;
    status = "okay";
};

3.3 Rootfs 工具

复制代码
BR2_SYSTEM_DHCP="eth0"
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_IPERF3=y

BusyBox udhcpc 相关:

复制代码
CONFIG_UDHCPC=y
CONFIG_UDHCPC_DEFAULT_SCRIPT="/usr/share/udhcpc/default.script"
CONFIG_UDHCPC_DEFAULT_INTERFACE="eth0"
CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-R -b"  # -b 后台运行,不阻塞 init
CONFIG_MOUNTPOINT=y
CONFIG_MKTEMP=y
CONFIG_PIDOF=y

-b 非常关键:没有它,udhcpc 拿不到租约会一直阻塞 init,导致系统卡在启动阶段。


4.1 平台驱动注册

dwmac-rk.c 通过 of_match_table 匹配 rockchip,rk3568-gmac,进而调用 rk_gmac_probe:

c 复制代码
static int rk_gmac_probe(struct platform_device *pdev)
{
    /* 1. 解析设备树(时钟、复位、延迟、PHY 等) */
    plat_dat = stmmac_probe_config_dt(pdev, &stmmac_res.mac);
    plat_dat->bsp_priv = rk_gmac_setup(pdev, plat_dat, data);
    rk_gmac_clk_init(plat_dat);
    rk_gmac_powerup(plat_dat->bsp_priv);

    /* 2. 创建 net_device、初始化 MAC、注册 MDIO 与 PHY */
    ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);

    /* 3. 调试用 sysfs */
    dwmac_rk_create_loopback_sysfs(&pdev->dev);
}

stmmac_dvr_probe 内部依次做:

  1. alloc_etherdev 创建 net_device,私有数据为 stmmac_priv
  2. stmmac_set_ethtool_ops 绑定 ethtool_ops
  3. stmmac_hw_init 初始化 MAC 硬件
  4. dma_set_mask_and_coherent 设置 DMA 掩码
  5. stmmac_napi_add 注册 NAPI
  6. stmmac_mdio_register 注册 MDIO 总线
  7. stmmac_phy_setup 创建 PHY 设备
  8. register_netdev 注册网络设备到内核

4.2 net_device_ops:驱动对外的"接口表"

c 复制代码
static const struct net_device_ops stmmac_netdev_ops = {
    .ndo_open            = stmmac_open,
    .ndo_stop            = stmmac_release,
    .ndo_start_xmit      = stmmac_xmit,
    .ndo_change_mtu      = stmmac_change_mtu,
    .ndo_set_rx_mode     = stmmac_set_rx_mode,
    .ndo_tx_timeout      = stmmac_tx_timeout,
    .ndo_do_ioctl        = stmmac_ioctl,
    .ndo_set_mac_address = stmmac_set_mac_address,
    ...
};

4.3 ifconfig eth0 up 的完整调用链

复制代码
用户态: ifconfig eth0 up
   │  ioctl(SIOCSIFFLAGS, IFF_UP)
   ▼
sys_ioctl → sock_ioctl → dev_ioctl
   │
   ▼
dev_ifsioc(SIOCSIFFLAGS)
   └─ dev_change_flags(dev, flags)
        └─ __dev_change_flags()
              if ((old_flags ^ flags) & IFF_UP)
                  __dev_open(dev)
   │
   ▼
__dev_open()
   ├─ set_bit(__LINK_STATE_START, &dev->state)
   ├─ ops->ndo_validate_addr(dev)
   └─ ops->ndo_open(dev)   →   stmmac_open
   │
   ▼
stmmac_open() → phylink_start()
   └─ phylink_run_resolve() 启动状态机
   │
   ▼
[对端 Link 建立] PHY 自协商完成 → phy_state_machine()
   └─ phy_check_link_status() 读 BMSR 得 link=1
        └─ phylink_phy_change(up=true)
             └─ phylink_resolve() 得出 link_state.link=true
                  └─ phylink_link_up()
                       ├─ mac_link_up() → stmmac_mac_link_up()  设置速率/双工/流控
                       ├─ netif_carrier_on() → linkwatch → RTM_NEWLINK
                       └─ 用户态收到 netlink 事件 → udhcpc 启动

关键点 :ifconfig up 只是"打开设备",真正 Link Up 是 PHY 自协商完成后的异步事件,由 phylink 状态机驱动,最终通过 netlink 通知用户态。


5. 数据路径:TX / RX

5.1 发送路径(socket → 网线)

复制代码
用户态 send() / sendto() / write()
   │
   ▼
传输层:
   TCP: tcp_sendmsg → tcp_write_xmit → tcp_transmit_skb → ip_queue_xmit
   UDP: udp_sendmsg → udp_send_skb → ip_send_skb
   │
   ▼
网络层:ip_output → ip_finish_output → ip_finish_output2
   │
   ▼
邻居子系统:ARP 解析后 neigh_output → dev_queue_xmit
   │
   ▼
Qdisc(默认 pfifo_fast / mq):__dev_queue_xmit → sch_direct_xmit
   │
   ▼
ndo_start_xmit = stmmac_xmit
   ├─ 判断 TSO/GSO → stmmac_tso_xmit(硬件分段)
   ├─ stmmac_tx_avail 检查环形缓冲余量,不足则停队列
   ├─ 取空闲 TX DMA 描述符
   ├─ dma_map_single 首部 + skb_frag_dma_map 分片
   ├─ stmmac_prepare_tx_desc 填描述符(长度 / FS/LS / 校验和 / TBS)
   ├─ stmmac_set_tx_owner 置 OWN=1
   ├─ wmb() 屏障,保证描述符写入对 DMA 可见
   ├─ stmmac_enable_dma_transmission(写 DMA_CHAN_TX_CONTROL.ST)
   └─ stmmac_set_tx_tail_ptr(doorbell,通知 DMA 有新帧)
   │
   ▼  返回 NETDEV_TX_OK
【硬件 DMA】读描述符 → 读 buffer → 写 MAC TX FIFO
   → MAC 加前导码/帧头/FCS → PHY 编码 → 网线
   → 发送完毕清 OWN 位
   │
   ▼
发送完成中断 / 合并定时器:
   TI 中断 → stmmac_interrupt → stmmac_dma_interrupt → stmmac_napi_check
   → __napi_schedule(&ch->tx_napi)
   │
   ▼
stmmac_napi_poll_tx → stmmac_tx_clean
   ├─ 遍历 dirty_tx:stmmac_tx_status 检查 OWN,仍为 1 则 break
   ├─ dma_unmap_single/page 解除映射
   ├─ dev_consume_skb_any 释放 skb
   ├─ release_tx_desc 清描述符
   └─ 余量足够则 netif_tx_wake_queue 唤醒被停的队列

5.2 接收路径(网线 → socket)

复制代码
准备阶段:stmmac_rx_refill
   ├─ page_pool_alloc_pages 分配收包缓冲页
   ├─ stmmac_set_desc_addr 把 DMA 地址写入描述符
   ├─ stmmac_set_rx_owner 置 OWN=1
   └─ stmmac_set_rx_tail_ptr(doorbell,告诉 GMAC 有空 buffer)
   │
   ▼
网线 → PHY(解码/时钟恢复)→ MAC(校验目的地址/FCS)→ RX FIFO
   │
   ▼
【硬件 DMA】按 RX 描述符写 buffer,写回状态,清 OWN=0
   │
   ▼  产生 RI 中断
上半部:stmmac_interrupt → stmmac_dma_interrupt → stmmac_napi_check
   └─ 命中 handle_rx → 关 RX 中断 + __napi_schedule(&ch->rx_napi)
   │
   ▼  软中断 NET_RX_SOFTIRQ
下半部:stmmac_napi_poll_rx → stmmac_rx
   ├─ 取 cur_rx 描述符,stmmac_rx_status 检查 OWN
   ├─ 计算长度(含 split header)
   ├─ 组装 skb:
   │     小包:napi_alloc_skb + skb_copy_to_linear_data
   │     大包:skb_add_rx_frag + page_pool_release_page(零拷贝)
   ├─ 处理 VLAN / 时间戳 / 校验和 / RSS 哈希
   └─ napi_gro_receive(GRO 合并)
   │
   ▼
netif_receive_skb → __netif_receive_skb_core(按协议分发)
   │
   ▼
网络层:ip_rcv → ip_local_deliver → ip_local_deliver_finish
   │
   ▼
传输层:
   TCP: tcp_v4_rcv → tcp_data_queue → sk_receive_skb
   UDP: udp_rcv → udp_queue_rcv_one_skb → sk_receive_skb
   │
   ▼
sk_data_ready 唤醒阻塞进程 → 用户态 recv()/read()
   │
   ▼
NAPI 收尾:stmmac_rx_refill 补页 → napi_complete_done → 重新使能 RX 中断

一句话总结:TX 是"驱动填描述符 → 硬件 DMA 读 → 发出 → 中断回收";RX 是"驱动预填页 → 硬件 DMA 写 → 中断 → NAPI 组装 skb → 上送协议栈 → 补页"。


6. 最后一公里:DHCP 与 ARP

6.1 DHCP:让设备拿到 IP

DHCP 基于 UDP,服务端 67、客户端 68。典型四步:

步骤 方向 源 IP 目的 IP 源 MAC 目的 MAC
Discover Client → Broadcast 0.0.0.0 255.255.255.255 本机 MAC ff:ff:ff:ff:ff:ff
Offer Server → Client 服务器 IP 广播/单播 服务器 MAC 客户端 MAC
Request Client → Broadcast 0.0.0.0 255.255.255.255 本机 MAC ff:ff:ff:ff:ff:ff
ACK Server → Client 服务器 IP 广播/单播 服务器 MAC 客户端 MAC

BusyBox 的 udhcpc 关键选项:

复制代码
CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-R -b"
  • -R:释放租约
  • -b:拿不到租约时后台运行,不阻塞 init

6.2 ARP:IP → MAC 的翻译官

ping 的典型流程:

复制代码
1. 查路由表
   同网段 → 直接解析目标 IP
   跨网段 → 解析网关 IP
2. 查 ARP 缓存
   命中 → 直接发送
   未命中 → 广播 ARP Request: "Who has 192.168.1.1? Tell 192.168.1.100"
3. 目标回复 ARP Reply
4. 发送 ICMP Echo Request
5. 收到 ICMP Echo Reply

ARP 缓存有老化时间,过期后会重新广播。设备启动或 IP 变更时会发免费 ARP,用于检测 IP 冲突并通知邻居更新缓存。

6.3 兜底:DHCP 拿不到参数怎么办

情况 处理
DHCP 服务未启动 用默认静态 IP / 掩码 / 网关 / DNS
DHCP 返回不完整 使用已有 IP;掩码默认 255.255.255.255;网关默认 0.0.0.0;DNS 默认 8.8.8.8

7. 调试与调优:从"能通"到"跑满"

7.1 常用工具速查

工具 用途 关键命令
iproute2 接口启用、IP 配置 ip link set eth0 up、ip addr add 192.168.1.100/24 dev eth0
ethtool 链路状态、驱动统计 ethtool eth0、ethtool -i eth0、ethtool -S eth0、ethtool -d eth0
phytool 读写 PHY 寄存器 phytool read eth0/0/0x11
iperf3 吞吐量测试 iperf3 -c <IP> -t 10、-P 4、-R、-u -b 1G
ftrace 函数调用链追踪 echo function_graph > current_tracer、echo '*stmmac*' > set_ftrace_filter

7.2 RGMII 延时调校(最关键的一步)

原理 :RGMII 时钟和数据同源发出,接收端必须在数据稳定窗口的中央采样。做法是把时钟延迟半个周期,让时钟沿落在数据眼图中央。

三种调法:

  1. 经验值:参考 SoC 厂商 / PHY 厂商给的推荐值。
  2. 眼图测量:示波器探头接 PHY 端 GTXCLK 和 TXD,以 TXD 触发,调 delayline 使时钟沿落在眼图中央。
  3. 误码率扫描 :用 iperf3 配合 ethtool -S 统计 CRC 错误,扫描 tx_delay/rx_delay 找最优值。
dts 复制代码
tx_delay = <0x24>;   // YT8531C 每步 150ps
rx_delay = <0x33>;

诊断命令:

bash 复制代码
ethtool -S eth0 | grep -E 'rx_crc|rx_errors|tx_errors|rx_over|tx_under'

7.3 DMA 性能三要素

(1)突发长度 snps,blen

AXI 一次 Burst 连续多个 Beat,中间不释放总线。突发越长:

  • ✅ 地址/控制开销占比低,效率高
  • ❌ 单次占用总线时间长,其他主设备(VOP/GPU/USB)等待久

RK3568 的 AXI 是多主共享 ,所以设备树里限制为 16/8/4 字节,避免 GMAC 长时间霸占总线导致显示撕裂或 USB 丢帧。

(2)Outstanding wr_osr_lmt / rd_osr_lmt

AXI 支持多个未完成请求同时在途。DDR 访问延迟约 100~300ns,如果没有 Outstanding,主设备必须等一个请求完全完成才能发下一个。

实际吞吐 ≈ min(突发长度 × Outstanding / 延迟, AXI 带宽上限)。

读比写多(rd=8, wr=4)是因为 RX 路径对延迟更敏感。

(3)FIFO 深度

dts 复制代码
rx-fifo-depth = <16384>;
tx-fifo-depth = <16384>;

FIFO 越深,抗突发能力越强,但占用更多硬件资源。

7.4 ftrace 追踪函数调用

启用配置:

复制代码
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y
CONFIG_DYNAMIC_FTRACE=y

使用:

bash 复制代码
mount -t tracefs nodev /sys/kernel/tracing
cd /sys/kernel/tracing

# 设置 function_graph 跟踪器
echo nop > current_tracer
echo function_graph > current_tracer

# 只跟踪 stmmac 相关函数
echo '*stmmac*' > set_ftrace_filter

# 清空并开始
echo 0 > tracing_on
echo > trace
echo 1 > tracing_on

# 触发操作
ifconfig eth0 up
sleep 3

# 停止并保存
echo 0 > tracing_on
cat trace > /tmp/stmmac_trace.log

7.5 故障排查 Checklist

现象 可能原因 排查命令
eth0 不存在 驱动未加载 / 设备树 status 未 okay `dmesg
Link 不上 PHY 复位失败 / 延时不对 / 网线坏 ethtool eth0、ethtool -S eth0
能 Link 但 ping 不通 IP/路由/ARP 问题 ip addr、ip route、ip neigh
丢包 / CRC 错误 RGMII 延时不准 `ethtool -S eth0
吞吐量低 DMA 突发 / Outstanding 配置保守 iperf3 -c ... -P 4,调 snps,blen
系统启动卡住 udhcpc 阻塞 init 检查 CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-R -b"

8. 总结

一块千兆网口从零到跑满,路径是这样的:

复制代码
硬件(RGMII 延时、变压器、PHY 自协商)
  → 编译(Kconfig + 设备树)
    → Probe(rk_gmac_probe → stmmac_dvr_probe)
      → Link Up(phylink 状态机)
        → 数据路径(TX/RX 描述符环 + DMA + NAPI)
          → 协议(DHCP / ARP)
            → 调优(延时 + DMA 参数 + ftrace)

三条最重要的经验:

  1. RGMII 延时是千兆以太网的生命线。延时不对,能 Link 但 CRC 错误率飙升,吞吐量断崖式下跌。
  2. snps,blen / Outstanding 是共享 AXI 总线上的"礼貌参数"。GMAC 不是独占总线,跑满自己的同时不能饿死 VOP/GPU/USB。
  3. udhcpc 的 -b 是启动阶段的救命开关。没有它,DHCP 拿不到地址时系统会卡死在 init。

附:本文参考的内核源码路径

层次 文件
平台胶水 drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c
平台通用 drivers/net/ethernet/stmicro/stmmac/stmmac_platform.c
核心 drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
MDIO drivers/net/ethernet/stmicro/stmmac/stmmac_mdio.c
PHY 驱动 drivers/net/phy/motorcomm.c
PHY 框架 drivers/net/phy/phylink.c / phy_device.c / mdio_bus.c / of_mdio.c
设备树 arch/arm64/boot/dts/rockchip/rk3568.dtsi / rk3568-evb1-ddr4-v10.dtsi
Socket net/socket.c
设备层 net/core/dev.c / net/core/dev_ioctl.c
相关推荐
lisanmengmeng2 小时前
NRPE 添加命令(一)
linux·运维·服务器
树下水月3 小时前
Typora破解
linux·服务器·前端
沫璃染墨4 小时前
《从零入门Linux系统篇(五十七):线程篇·十——生产者消费者模型进阶:从环形缓冲区到POSIX信号量》
linux·运维·服务器·开发语言·c++·系统架构·信号处理
wuminyu4 小时前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
深念Y4 小时前
rime-雾凇拼音-配置记录
linux·junit·软件·拼音·kde
殷色玫瑰5 小时前
C++ string类详解:常用接口、字符串操作与模拟实现
java·linux·c语言·开发语言·数据结构·c++
忆挽篱笙歌6 小时前
gcc,g++
linux
l1t6 小时前
修复WSL CreateInstance/E_UNEXPECTED和 mounted read-only 错误
linux·windows·wsl
Ruiery7 小时前
Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC
linux·运维·服务器