从零理解 Linux 网卡驱动:以 RK3576 + RTL8211F 为例,把 MAC、PHY、MDIO、RGMII、stmmac 和设备树一次讲清楚
📺 B站 嵌入式孙老师:博主个人介绍
📘 博主书籍-京东购买链接*:Yocto项目实战教程
📘 加博主微信,进技术交流群: jerrydev
从零理解 Linux 网卡驱动:以 RK3576 + RTL8211F 为例,把 MAC、PHY、MDIO、RGMII、stmmac 和设备树一次讲清楚
很多人第一次接触以太网驱动时,会天然地把“网卡驱动”理解成一个单独的 C 文件:
写几个寄存器,能发包,能收包,就算完成了。
但真上板后很快就会发现,以太网驱动根本不是这么回事。
你看到的“eth0”“网线插上了”“能不能拿到 IP”“为什么只有 100M”“为什么是 169.254.x.x”“为什么日志里有 Generic PHY / RTL8211F / stmmac / MDIO / RGMII / tx_delay / rx_delay”——这些现象,其实来自一整条分层链路,不是一个函数决定的。
如果把整个系统画成一条链,大概是这样:
应用层
↓
socket / TCP/IP 协议栈
↓
Linux net_device
↓
MAC 驱动(stmmac / dwmac / SoC glue)
↓
MDIO 总线 + PHY 驱动(realtek.c)
↓
RGMII / RMII / SGMII 等 MAC-PHY 接口
↓
外部 PHY(RTL8211F)
↓
磁性器件 / RJ45 / 网线
↓
对端交换机 / 路由器 / 电脑
理解这条链之后,你再去看驱动、设备树、日志,就会一下子变得非常清晰。
这篇文章就围绕这个思路展开。
一、先把“网卡驱动”拆开:它其实不是一个驱动,而是三层协作

在 Linux 里,以太网能工作,通常至少涉及三层:
1. MAC 驱动
MAC 是 SoC 里集成的以太网控制器。
在 RK3576 这类平台上,MAC 不直接等于“整个网卡”,它只是数据链路层里靠近 CPU 的那一部分。
它主要负责:
- 发送和接收以太网帧
- DMA 描述符管理
- 中断
- NAPI 轮询
- 网卡的
ndo_open、ndo_stop、ndo_start_xmit - 和 Linux 网络子系统对接
你当前这套平台里,核心 MAC 驱动不是你完全手写的,而是 Synopsys DesignWare MAC(stmmac / dwmac) 这一套通用框架。
也就是说,大部分发送、接收、DMA、NAPI 逻辑,实际上已经在 stmmac 里写好了。
你并不是从零发明一个以太网驱动,而是站在成熟内核框架上做 SoC 适配。
2. PHY 驱动
PHY 是外部芯片,比如你这里的 RTL8211F。
PHY 不负责 IP,不负责 socket,不负责 DMA。
它负责的是更底层的“电气”和“链路协商”:
- 检测网线插拔
- 自协商速度和双工
- 10/100/1000M 切换
- Link Up / Link Down
- RGMII 内部延时
- 低功耗模式(例如 ALDPS)
- 时钟输出
- 读取 PHY ID
- MDIO/MDC 寄存器访问
Linux 里这部分通常由 PHYLIB 框架统一管理,而具体厂商的 PHY,例如 Realtek,就放在 drivers/net/phy/realtek.c 里。
也就是说:
- MAC 驱动 关注“怎么收发帧”
- PHY 驱动 关注“物理链路是否建立、速度是多少、时序怎么配”
3. 设备树(DTS)
设备树不是驱动代码,但在嵌入式平台里它和驱动是同等重要的。
为什么?
因为驱动只认识“逻辑设备”,但板子到底怎么接线、PHY 地址多少、RGMII 模式是什么、reset GPIO 在哪、时钟怎么接、延时参数是多少,这些都不是驱动源码能自己猜出来的,必须由板级描述告诉它。
所以设备树的任务是把板级硬件信息说清楚,例如:
phy-mode = "rgmii-rxid";reg = <0x1>;snps,reset-gpio = <...>;tx_delay = <...>;rx_delay = <...>;phy-handle = <&rgmii_phy0>;
如果设备树错了,驱动再优秀也没用。
二、以你当前平台为例:RK3576 + RTL8211F 到底是怎么工作的
你当前这套板子,整体结构可以这样理解:
RK3576 GMAC0 (MAC)
│
├── MDIO/MDC → RTL8211F 的管理接口
├── RGMII TX/RX/CLK → RTL8211F 的数据接口
│
RTL8211F (PHY)
│
├── 磁性器件 / RJ45
│
对端交换机 / 路由器 / PC
这里最容易混淆的是两条线:
1. MDIO/MDC
这是一条“管理总线”,速度不高。
它不是拿来传以太网数据的,它是拿来:
- 读取 PHY ID
- 读取 link 状态
- 设置 10/100/1000
- 配 RGMII delay
- 配低功耗、时钟输出等
驱动里经常出现的:
phy@1reg = <0x1>PHY_ID_MATCH_EXACT(...)mdiobusphy_read()phy_write()
本质都属于这条链。
2. RGMII
RGMII 才是 MAC 和 PHY 之间真正传数据的高速接口。
这条链上有:
- TXD[3:0]
- RXD[3:0]
- TX_CTL
- RX_CTL
- TXC
- RXC
如果这条线的时序、引脚、延时配置不对,最常见现象不是“完全没设备”,而是:
- 只能 100M
- 千兆不稳定
- 丢包
- CRC 错误
- 10/100 能上,1000 上不去
- Link Up 但 DHCP 拿不到地址
- RX 为 0,TX 有值
这也是为什么 phy-mode、tx_delay、rx_delay、PHY 内部 delay 如此关键。
三、Linux 启动后,以太网驱动的完整工作流程
下面按时间顺序,把驱动启动的过程走一遍。
第一步:内核解析设备树,创建平台设备
内核启动后,会先读取 DTB,发现有一个 GMAC 节点,比如:
&gmac0 {
phy-mode = "rgmii-rxid";
clock_in_out = "output";
snps,reset-gpio = <&gpio2 RK_PB5 GPIO_ACTIVE_LOW>;
snps,reset-active-low;
snps,reset-delays-us = <0 20000 100000>;
phy-handle = <&rgmii_phy0>;
status = "okay";
};
这一步内核还没有“开始发网包”,只是知道:
“这里有个以太网控制器,地址在某个寄存器区,挂了一个外部 PHY。”
第二步:MAC 驱动 probe
接着平台驱动 probe,也就是 stmmac / dwmac 的 Rockchip glue 开始接管这个设备。
它会做这些事情:
- 映射寄存器
- 申请时钟
- 申请 reset
- 解析
phy-mode - 解析
tx_delay/rx_delay - 配置 pinctrl
- 初始化 DMA
- 注册 net_device
- 创建或连接 MDIO 总线
这一层最重要的一点是:
它并不直接知道板上 PHY 是谁,它只负责把“MAC 控制器”先带起来。
第三步:创建 MDIO 总线,识别 PHY
然后 MAC 驱动会通过 MDIO 去扫描 PHY。
例如设备树里:
&mdio0 {
rgmii_phy0: phy@1 {
reg = <0x1>;
};
};
这表示 PHY 地址是 1。
驱动会通过 MDIO 读 PHY 的标准寄存器:
- PHYIDR1
- PHYIDR2
从而拿到一个唯一 PHY ID,比如 RTL8211F 对应的 ID。
如果识别成功,内核就会在 PHY 驱动表里查找谁能匹配这个 ID。
第四步:PHY 驱动匹配
在 drivers/net/phy/realtek.c 里,通常会有类似这样的驱动表:
static struct phy_driver realtek_drvs[] = {
{
PHY_ID_MATCH_EXACT(0x001cc916),
.name = "RTL8211F Gigabit Ethernet",
.config_init = rtl8211f_config_init,
.config_aneg = genphy_config_aneg,
.read_status = genphy_read_status,
.suspend = genphy_suspend,
.resume = genphy_resume,
},
};
这段代码的含义非常重要:
PHY_ID_MATCH_EXACT(...):告诉内核,这个驱动认哪个 PHYconfig_init:PHY 初始化时要做什么config_aneg:如何做自协商read_status:如何读链路状态
如果这里匹配不上,就可能回退成 Generic PHY。
这时系统也许还能工作,但厂商特有配置就可能丢失,比如:
- RGMII delay
- EEE
- 低功耗
- clkout
- page 切换寄存器处理
这就是为什么“能看到网口”不等于“驱动正确”。
第五步:PHY 初始化
匹配到 RTL8211F 后,会进入 rtl8211f_config_init()。
这一层通常干什么?
- 配置 page select
- 配置 RGMII internal delay
- 配置 EEE
- 配置 ALDPS
- 配置时钟输出
- 配置 LED 行为
- 做厂商寄存器的初始化
这一步非常像“上电后的板级调参”。
如果这里 delay 配错了,就可能出现一种典型现象:
- 驱动看起来都正常
- PHY 也识别正确
- 但千兆协商不上
- 最后掉到 100M 甚至 10M
所以很多人以为“驱动没问题了”,其实只是 probe 通过了,时序未必正确。
第六步:网卡被注册成 eth0
前面 MAC 和 PHY 都搞定后,系统就会创建 eth0。
这时候你用 ifconfig -a、ip link 就能看到它。
但注意:
看得到 eth0,并不代表网络通。
这只是说明:
- net_device 已经注册了
- 驱动对象已经存在了
真正的数据路径,还要看 ndo_open、NAPI、DMA、PHY link 状态。
第七步:ifconfig up / systemd-networkd / dhcpcd 拉起接口
当网络管理器把接口拉起来时,会调用驱动的 ndo_open。
典型动作包括:
- 申请中断
- 初始化 DMA ring
- 启动 RX/TX engine
- 启动 NAPI
phy_start()或 phylink start- 允许 MAC 开始收发
到这里,才真正进入可收发包状态。
四、网卡驱动最核心的四个入口函数
很多人问,从零看网卡驱动,应该抓哪些核心函数?
我建议先盯住下面四个:
ndo_openndo_stopndo_start_xmitpoll(NAPI)
只要把这四个看明白,整个数据平面就通了。
1. ndo_open:网卡启动
大致伪代码是这样:
static int myeth_open(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
init_dma_desc(priv);
request_irq(priv->irq, myeth_interrupt, 0, ndev->name, ndev);
napi_enable(&priv->napi);
start_rx_dma(priv);
start_tx_dma(priv);
enable_mac_rx_tx(priv);
phy_start(ndev->phydev);
netif_start_queue(ndev);
return 0;
}
逻辑很直白:
- 把 DMA ring 准备好
- 开中断
- 开 NAPI
- 开 MAC 的收发
- 启动 PHY
- 允许上层发送队列启动
2. ndo_stop:网卡关闭
这是反方向动作:
static int myeth_stop(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
netif_stop_queue(ndev);
phy_stop(ndev->phydev);
disable_mac_rx_tx(priv);
stop_rx_dma(priv);
stop_tx_dma(priv);
napi_disable(&priv->napi);
free_irq(priv->irq, ndev);
return 0;
}
3. ndo_start_xmit:发包主路径
这是应用层发包最终到驱动的入口之一。
static netdev_tx_t myeth_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
dma_addr_t dma;
dma = dma_map_single(priv->dev, skb->data, skb->len, DMA_TO_DEVICE);
fill_tx_desc(priv, dma, skb->len);
kick_tx_dma(priv);
return NETDEV_TX_OK;
}
这段代码真正做的事只有三件:
- 把 skb 映射成 DMA 地址
- 填 TX 描述符
- 通知硬件开始发
注意,网卡驱动不是一个字节一个字节发,而是通过 DMA 描述符让硬件自己搬运。
4. NAPI poll:收包主路径
收包一般不是在中断里把所有包都处理完,而是中断里只做“通知”,真正收包在 poll 里做:
static int myeth_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
while (work_done < budget && rx_desc_has_packet(priv)) {
struct sk_buff *skb = build_skb_from_rx_desc(priv);
skb->protocol = eth_type_trans(skb, priv->ndev);
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done);
enable_rx_irq(priv);
}
return work_done;
}
这里面最关键的理解是:
- 中断只是提醒“来包了”
- NAPI poll 才负责真正从 RX ring 取包
- 拿到包后交给协议栈
这就是 Linux 高性能网卡驱动的基本套路。
五、PHY 驱动最核心的逻辑是什么
相比 MAC 驱动,PHY 驱动通常代码短很多,但很容易看不懂。
原因是它不是“完整数据通路驱动”,它主要是“寄存器配置 + 状态机驱动”。
最常见的 PHY 驱动核心函数有这几个:
probeconfig_initconfig_anegread_statussuspendresume
其中最关键的是 config_init 和 read_status。
1. config_init:初始化 PHY 的特殊功能
以 RTL8211F 为例,它可能需要在这里做:
- 根据
phy-mode配 RGMII delay - 开/关 ALDPS
- 开/关 EEE
- 开/关 clkout
- 配 LED
例如你可以把它理解为这种伪代码:
static int rtl8211f_config_init(struct phy_device *phydev)
{
switch (phydev->interface) {
case PHY_INTERFACE_MODE_RGMII:
disable_tx_delay();
disable_rx_delay();
break;
case PHY_INTERFACE_MODE_RGMII_ID:
enable_tx_delay();
enable_rx_delay();
break;
case PHY_INTERFACE_MODE_RGMII_RXID:
disable_tx_delay();
enable_rx_delay();
break;
case PHY_INTERFACE_MODE_RGMII_TXID:
enable_tx_delay();
disable_rx_delay();
break;
}
disable_eee_if_needed();
configure_clkout_if_needed();
configure_low_power_if_needed();
return 0;
}
这段逻辑特别重要,因为它决定的是板子是否稳定跑千兆。
2. read_status:读取链路状态
这个函数一般做:
- Link Up / Link Down
- Speed = 10 / 100 / 1000
- Duplex = half / full
- Pause 能力
它最后会把状态写回 phydev,MAC 驱动再根据这个结果调整硬件。
六、设备树到底写什么才叫“把板子描述清楚了”
很多初学者写网卡 DTS 时,容易写成“能编译过去就行”。
但真正有价值的 DTS,不是能编译,而是能完整描述板级事实。
一个比较合理的 Ethernet DTS,至少包含三部分。
1. GMAC 节点
&gmac0 {
phy-mode = "rgmii-rxid";
clock_in_out = "output";
snps,reset-gpio = <&gpio2 RK_PB5 GPIO_ACTIVE_LOW>;
snps,reset-active-low;
snps,reset-delays-us = <0 20000 100000>;
pinctrl-names = "default";
pinctrl-0 = <ð0m0_miim
ð0m0_tx_bus2
ð0m0_rx_bus2
ð0m0_rgmii_clk
ð0m0_rgmii_bus
ðm0_clk0_25m_out>;
tx_delay = <0x30>;
rx_delay = <0x0>;
phy-handle = <&rgmii_phy0>;
status = "okay";
};
这里每一项都不是装饰:
phy-mode:定义 MAC/PHY 延时模型reset-gpio:PHY 上电复位pinctrl-0:把 SoC 引脚切到以太网功能tx_delay/rx_delay:MAC 侧延时参数phy-handle:连接到哪一个 PHY
2. MDIO / PHY 节点
&mdio0 {
rgmii_phy0: ethernet-phy@1 {
compatible = "ethernet-phy-id001c.c916",
"ethernet-phy-ieee802.3-c22";
reg = <0x1>;
clocks = <&cru REFCLKO25M_GMAC0_OUT>;
phy-supply = <&vcc_3v3_phy>;
realtek,clkout-disable;
max-speed = <1000>;
};
};
这部分的作用是:
reg:PHY 的 MDIO 地址compatible:帮助绑定正确驱动clocks:PHY 的参考时钟phy-supply:供电控制max-speed:限制速度上限realtek,xxx:厂商私有配置
3. pinctrl 节点
这是最容易被忽视的一层。
驱动写得再漂亮,如果 pinctrl 没配到正确复用,接口根本不可能好。
所以以太网 pinctrl 至少要保证:
- MDIO/MDC
- TX/RX 数据线
- TXC/RXC
- 参考时钟
- reset pin(如果是 GPIO)
七、为什么“能看到 eth0”不等于“驱动没问题”
这个误区非常常见。
看到 eth0 只说明两件事:
- net_device 已经注册了
- MAC 驱动 probe 至少没死
但下面这些问题依然可能存在:
- PHY 其实绑定成 Generic PHY 了
- RGMII delay 错了
- 只有 100M,没有 1000M
- 链路一直 downshift
- RX 一直 0
- DHCP 拿不到
- 169.254.x.x fallback
这就是为什么调试以太网时,必须同时看:
dmesgethtoolifconfig/ip -s link- 设备树
- PHY 驱动匹配情况
- 板级原理图
单看一个现象,往往会误判。
八、你当前这套驱动里,真正关键的代码点有哪些
如果把“当前工程里的关键代码”提炼出来,其实就几处。
1. PHY ID 匹配
这是 PHY 驱动入口。
{
PHY_ID_MATCH_EXACT(0x001cc916),
.name = "RTL8211F Gigabit Ethernet",
.config_init = rtl8211f_config_init,
}
如果这里没有,或者 ID 错了,就很容易退回 Generic PHY。
2. rtl8211f_config_init()
这是 PHY 的“板级初始化大脑”。
这里通常做:
- 根据
phy-mode决定延时 - 配厂商扩展寄存器
- 关/开 EEE
- 配 ALDPS
- 配 clkout
这部分代码写对了,日志通常会从“Generic PHY + 100M”进化到“能正确识别 RTL8211F 并开始稳定协商”。
3. GMAC 的设备树
这个部分决定:
- 复位
- 时钟
- 引脚
- 延时
- PHY 连接
如果这个节点错了,PHY 驱动未必报错,但链路质量很可能不正常。
4. 网络管理器配置
这部分虽然不是驱动源码,但在板上经常会误导判断。
比如:
- 驱动已经好了
- 但 DHCP 没回来
- 系统给了
169.254.x.x
这时如果不了解网络层,就会误以为“驱动还有问题”。
其实 169.254.x.x 往往只是:
- DHCP 失败后的 fallback
- 或者 dhcpcd / networkd 两个管理器抢网卡
所以驱动调通后,还要把 rootfs 侧网络服务收敛成一套。
九、如果“从零开始写驱动”,到底应该怎么干
这个问题必须讲现实一点。
真正从零写一套网卡驱动,代价非常大
因为你要自己处理:
- DMA ring
- cache coherent
- NAPI
- GRO
- checksum offload
- TSO
- ethtool
- phylib
- PM
- error recovery
这对大多数项目并不划算。
工业界真正的做法是:
复用成熟 MAC 核心,只写 glue 和板级配置。
对你这个平台,正确策略是:
- 复用
stmmac - 用
realtek.c做 RTL8211F PHY - 写好 SoC glue / DTS
- 补必要的厂商特性
- 调板级时序
也就是说,真正“从零”的部分,其实主要是:
- 设备树
- SoC glue
- 少量 PHY 特化代码
- 调试脚本与日志
而不是自己重写一个完整以太网子系统。
十、一个合理的开发顺序
如果我要从零把一块新板的 Ethernet 带起来,我会按下面顺序做。
第一步:确认硬件连接
先看原理图:
- MAC 接哪个 PHY
- MDIO 地址多少
- reset pin 在哪
- 参考时钟怎么来
- RGMII 还是 RMII
- LED/ALDPS/clkout strap 怎么接
- 磁性器件怎么接
- RJ45 是否带灯
- 千兆四对线是否完整
不做这一步,后面都是猜。
第二步:写 DTS
先把最基本的节点写出来:
gmac0mdio0phy@1pinctrl
先让系统能识别出 eth0 和 PHY。
第三步:确认 PHY 驱动是否正确匹配
看 dmesg:
- 是
RTL8211F Gigabit Ethernet - 还是
Generic PHY
如果还是 Generic PHY,先别碰 DHCP,先把 PHY 驱动绑定搞定。
第四步:确认 Link Up / Speed / Duplex
看 ethtool eth0:
- 是否 Link detected
- Speed 是多少
- 对端 advertised 什么
- 本端 advertised 什么
这一层解决的是“物理链路是否真的正常”。
第五步:确认 TX/RX 是否都正常
看统计:
RX packetsTX packetserrorscrcframe
如果 TX 有、RX 没有,就别去想 IP,先回头查物理层。
第六步:最后才看 DHCP / IP
只有前面都正常了,才看:
dhcpcdsystemd-networkd- 静态 IP
- 路由
- DNS
这个顺序很重要。
很多人恰恰反过来,一开始就盯 DHCP,结果越调越乱。
十一、为什么网卡驱动调试经常会卡在“100M / 10M / 169.254”这种现象里
因为这几个现象非常有迷惑性。
1. 只能 100M
不代表驱动没加载。
常见原因是:
- 千兆四对线没全通
- RGMII delay 不对
- 磁性器件或 RJ45 接线有问题
- PHY 特殊配置没生效
2. 能 Link Up,但拿不到 DHCP
不代表 DHCP 服务一定坏了。
也可能是:
- TX 正常但 RX 不正常
- 对端回包收不到
- 实际链路质量差,广播包丢失
3. 是 169.254.x.x
这通常不是“系统给错了 IP”,而是:
- DHCP 没成功
- 自动退回到 IPv4 link-local
这时你如果把注意力全放在 IP 上,就会错过真正的物理链路问题。
十二、最后做一个全局总结
如果只用一句话总结 Linux 以太网驱动,那就是:
网卡驱动不是一个驱动文件,而是一条从设备树、MAC、DMA、NAPI、MDIO、PHY、RGMII、RJ45 一直到网络服务的完整链路。
对当前这套 RK3576 + RTL8211F 而言,真正关键的点有四个:
第一,MAC 驱动不是从零写
你复用的是 stmmac / dwmac。
所以核心收发逻辑、DMA、NAPI,已经有成熟框架,不需要从零发明。
第二,PHY 驱动决定链路层行为
realtek.c 里的 RTL8211F 匹配和 rtl8211f_config_init(),决定了 PHY 是否被正确识别,以及 RGMII delay、EEE、clkout、低功耗等行为。
第三,设备树决定板级事实
设备树不是配角,它是驱动能否正确工作的“前提条件”。phy-mode、reg、reset-gpio、tx_delay、rx_delay、phy-handle,每一项都可能直接决定链路是否稳定。
第四,网络管理器只是最后一层
eth0 出现了、Link Up 了,不代表 IP 就一定对。
DHCP、169.254、networkd、dhcpcd 这些属于更上层,只能在驱动和链路先正常的前提下再处理。
结尾:如果让我们从零做一版“最小可工作”的网卡支持,最少要有这些东西
最后把最小集合列出来,作为一份真正可落地的清单。
代码层最少要有
1. MAC 驱动
- probe
- open / stop
- start_xmit
- interrupt + NAPI
- DMA 描述符管理
- net_device_ops
2. PHY 驱动
- PHY ID match
- config_init
- read_status
- autoneg 支持
3. 设备树
- gmac node
- mdio node
- phy node
- pinctrl
- reset / clock / delay
4. rootfs 网络配置
- 选一个网络管理器
- DHCP 或静态 IP
- 不要多个服务同时抢接口
📺 B站 嵌入式孙老师:博主个人介绍
📘 博主书籍-京东购买链接*:Yocto项目实战教程
📘 加博主微信,进技术交流群: jerrydev
更多推荐




所有评论(0)