从零到跑满千兆: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 的千兆以太网控制器,具备三个特点:
- 基于 Synopsys DWMAC 4.20a IP------所以你能在内核里看到大量
dwmac_*、stmmac_*前缀的源码。 - 支持 RGMII 和 RMII 两种 PHY 接口(RK3568 板级通常用 RGMII)。
- 内置高性能 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 是"数字世界"和"模拟世界"之间的翻译官,职责多达七项:
- 数模转换:把 MAC 输出的数字信号转换成网线传输的模拟信号,反之亦然。
- 时钟恢复:网线没有独立时钟线,时钟嵌在数据里。PHY 接收时从数据流恢复时钟,再用它采样数据。
- 自协商:上电后自动和对端协商速率、双工、流控、主从模式,结果通过 MDIO 寄存器可读。
- 链路检测:持续检测对端是否在线。
- MDIO 管理接口:提供 MDIO/MDC,供 MAC/CPU 读写寄存器。
- 信号调理:均衡器、回声消除、串扰消除。
- 模拟侧输出差分线 :
- 百兆:2 对(1 对专发、1 对专收)
- 千兆:4 对同时收发,靠回声消除实现(发送端知道自己发了什么,接收时从混合信号中减去自己发送的部分)
1.7 网络变压器
在 PHY 和 RJ45 之间,通常还有一个网络变压器,作用有五:
- 电气隔离:初级/次级之间绝缘(通常 1500V AC),防止两端地电位差损坏芯片,也防雷击浪涌。
- 阻抗匹配:PHY 输出阻抗(100Ω 差分)和网线特性阻抗(100Ω)匹配,减少反射。
- 共模抑制:对共模噪声有很强抑制能力,提高 EMI 性能。
- 信号耦合:通过电磁感应传递差分信号,隔断直流。
- 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. Probe → Link Up:上电到链路
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 内部依次做:
alloc_etherdev创建net_device,私有数据为stmmac_privstmmac_set_ethtool_ops绑定ethtool_opsstmmac_hw_init初始化 MAC 硬件dma_set_mask_and_coherent设置 DMA 掩码stmmac_napi_add注册 NAPIstmmac_mdio_register注册 MDIO 总线stmmac_phy_setup创建 PHY 设备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 时钟和数据同源发出,接收端必须在数据稳定窗口的中央采样。做法是把时钟延迟半个周期,让时钟沿落在数据眼图中央。
三种调法:
- 经验值:参考 SoC 厂商 / PHY 厂商给的推荐值。
- 眼图测量:示波器探头接 PHY 端 GTXCLK 和 TXD,以 TXD 触发,调 delayline 使时钟沿落在眼图中央。
- 误码率扫描 :用 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)
三条最重要的经验:
- RGMII 延时是千兆以太网的生命线。延时不对,能 Link 但 CRC 错误率飙升,吞吐量断崖式下跌。
snps,blen/ Outstanding 是共享 AXI 总线上的"礼貌参数"。GMAC 不是独占总线,跑满自己的同时不能饿死 VOP/GPU/USB。- 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 |