目录
[1. WOL 技术介绍](#1. WOL 技术介绍)
[1.1 什么是 WOL](#1.1 什么是 WOL)
[1.2 Magic Packet 格式](#1.2 Magic Packet 格式)
[1.3 WOL 触发条件](#1.3 WOL 触发条件)
[2. 硬件平台与方案概述](#2. 硬件平台与方案概述)
[2.1 硬件平台](#2.1 硬件平台)
[2.2 方案概述](#2.2 方案概述)
[3. 硬件原理分析](#3. 硬件原理分析)
[3.1 RTL8211F INTB/PMEB 引脚](#3.1 RTL8211F INTB/PMEB 引脚)
[3.2 PMEB 引脚特性](#3.2 PMEB 引脚特性)
[3.3 硬件连接示意](#3.3 硬件连接示意)
[3.4 关键时序](#3.4 关键时序)
[4. 设备树配置](#4. 设备树配置)
[4.1 电源管理配置](#4.1 电源管理配置)
[4.2 休眠/唤醒配置](#4.2 休眠/唤醒配置)
[4.3 GMAC 节点配置](#4.3 GMAC 节点配置)
[4.4 GPIO 按键节点](#4.4 GPIO 按键节点)
[5. 驱动修改](#5. 驱动修改)
[5.1 添加模拟电源按下函数](#5.1 添加模拟电源按下函数)
[5.2 修改 stmmac 网卡驱动](#5.2 修改 stmmac 网卡驱动)
[5.2.1 添加头文件](#5.2.1 添加头文件)
[5.2.2 添加 WOL 中断处理函数](#5.2.2 添加 WOL 中断处理函数)
[5.2.3 修改 stmmac_open 函数](#5.2.3 修改 stmmac_open 函数)
[5.2.4 修改 stmmac_release 函数](#5.2.4 修改 stmmac_release 函数)
[5.2.5 修改 stmmac_suspend 函数](#5.2.5 修改 stmmac_suspend 函数)
[5.2.6 修改 stmmac_resume 函数](#5.2.6 修改 stmmac_resume 函数)
[5.2.7 修改 probe/remove 函数](#5.2.7 修改 probe/remove 函数)
[5.3 修改平台文件](#5.3 修改平台文件)
[5.4 修改 stmmac.h](#5.4 修改 stmmac.h)
[5.5 修改 RTL8211F PHY 驱动](#5.5 修改 RTL8211F PHY 驱动)
[5.5.1 RTL8211F WOL 寄存器配置](#5.5.1 RTL8211F WOL 寄存器配置)
[5.5.2 实现代码](#5.5.2 实现代码)
[5.5.3 修改 RTL8211F 驱动结构](#5.5.3 修改 RTL8211F 驱动结构)
[6. 调试过程与问题解决](#6. 调试过程与问题解决)
前言
在嵌入式 Linux 系统的开发中,功耗管理是一个重要的课题。网络唤醒(WOL)功能允许设备在休眠状态下通过接收特定的网络数据包被唤醒,从而在保持低功耗的同时保持网络可达性。近期在基于 RK3568 平台的项目中,需要在 Android13 SDK 环境下为 RTL8211F 网卡 PHY 芯片实现 WOL 功能,本文将完整记录整个适配过程,希望能够帮助到有类似需求的朋友。
1. WOL 技术介绍
1.1 什么是 WOL
Wake-On-LAN 是一种通过网络唤醒处于休眠或关机状态计算机的技术。它通过向目标设备发送一个称为 Magic Packet 的特殊广播帧来触发唤醒。
1.2 Magic Packet 格式
Magic Packet 的格式如下:
6 个字节的 FF FF FF FF FF FF
+ 16 次重复的目标 MAC 地址
+ 可选的数据(4 或 6 字节的密码)
例如,目标 MAC 地址为 00:11:22:33:44:55 的 Magic Packet 结构为:
FF FF FF FF FF FF 00 11 22 33 44 55 00 11 22 33 44 55 ... (重复 16 次)
1.3 WOL 触发条件
以 RTL8211F 为例,WOL 事件的触发需要满足以下条件:
-
目的地址匹配:收到的 Magic Packet 必须是广播、组播或单播到 PHY 地址的数据包
-
无 CRC 错误:数据包通过 CRC 校验
-
模式匹配 :Magic Packet 格式正确(6 个
0xFF+ 16 次 MAC 地址)
2. 硬件平台与方案概述
2.1 硬件平台
| 组件 | 型号/规格 |
|---|---|
| SoC | Rockchip RK3568 |
| 内核版本 | Linux 5.10 (Android13 SDK) |
| 以太网 MAC | Synopsys DesignWare GMAC (stmmac) |
| 以太网 PHY | Realtek RTL8211F |
2.2 方案概述
RTL8211F 的 WOL 功能通过 PMEB 引脚(Power Management Event B)实现。当 PHY 检测到 Magic Packet 时,PMEB 引脚会从高电平跳变为低电平,产生一个下降沿信号。该信号连接到 RK3568 的 GPIO 引脚,触发 GPIO 中断,从而唤醒系统。
3. 硬件原理分析
3.1 RTL8211F INTB/PMEB 引脚
RTL8211F 的 Pin 31 是 INTB/PMEB 复用引脚,通过寄存器 Page 0xd40, Register 22, bit[5] 配置功能:
| bit5 值 | 功能 |
|---|---|
| 0 | INTB 模式(中断引脚,默认) |
| 1 | PMEB 模式(电源管理事件引脚) |
重要:在 WOL 场景下,必须将引脚配置为 PMEB 模式。
3.2 PMEB 引脚特性
| 特性 | 描述 |
|---|---|
| 默认状态 | 高电平(通过 4.7kΩ 电阻上拉至 3.3V) |
| 触发方式 | 下降沿触发(从高到低跳变) |
| 触发后状态 | 保持低电平,直到软件清除 WOL 状态 |
3.3 硬件连接示意

重要:需要用RK3568的GPIO0组引脚做唤醒引脚。
3.4 关键时序
正常状态: PMEB 高电平 ──────────────────────────
│
收到 Magic Packet: ↓
│
PMEB 低电平: ──────────────────────────── 下降沿触发中断
│
软件清除后: ────────────────────────── 恢复高电平
4. 设备树配置
4.1 电源管理配置
休眠时需要保持相关电源供电,确保 PHY 能正常工作,包含
1.PHY即RTL8211F的供电保存
2.PHY芯片的复位引脚保持
3.PHY芯片需要用独立晶振方案
4.vdd_log电源保持即:DCDC_REG1
rk3568-evb.dts中修改
vdd_logic: DCDC_REG1 {
regulator-state-mem {
regulator-on-in-suspend; // 休眠时保持逻辑电源
};
};
vcc_3v3: SWITCH_REG1 {
regulator-state-mem {
regulator-on-in-suspend; // 休眠时保持 3.3V 电源,PHY供电
};
};
vccio_sd: LDO_REG5 {
regulator-state-mem {
regulator-on-in-suspend; //PHY芯片的复位引脚所在IO组
};
};
4.2 休眠/唤醒配置
rk3568.dtsi中修改
rockchip_suspend: rockchip-suspend {
rockchip,sleep-mode-config = <
(0
- | RKPM_SLP_ARMOFF_LOGOFF // 注释掉,保持 CPU 核心唤醒能力
+ /*| RKPM_SLP_ARMOFF_LOGOFF*/
| RKPM_SLP_CENTER_OFF
| RKPM_SLP_HW_PLLS_OFF
| RKPM_SLP_PMUALIVE_32K
- | RKPM_SLP_OSC_DIS // 注释掉,保持振荡器工作
+ /*| RKPM_SLP_OSC_DIS*/
| RKPM_SLP_PMIC_LP
| RKPM_SLP_32K_PVTM
)
>;
rockchip,wakeup-config = <
(0
| RKPM_GPIO_WKUP_EN
+ | RKPM_CPU0_WKUP_EN // 允许 CPU0 被 GPIO 唤醒
+ | RKPM_CPU2_WKUP_EN // 允许 CPU2 被 GPIO 唤醒
)
>;
};
4.3 GMAC 节点配置
GMAC1 节点(主网卡),板级设备树文件
cpp
&gmac1 {
phy-mode = "rgmii";
clock_in_out = "input";
snps,reset-gpio = <&gpio3 RK_PB0 GPIO_ACTIVE_LOW>;
snps,reset-active-low;
snps,reset-delays-us = <0 20000 100000>;
assigned-clocks = <&cru SCLK_GMAC1_RX_TX>, <&cru SCLK_GMAC1>;
assigned-clock-parents = <&cru SCLK_GMAC1_RGMII_SPEED>, <&cru CLK_MAC1_2TOP>;
assigned-clock-rates = <0>, <125000000>;
wakeup-source;
pinctrl-names = "default";
pinctrl-0 = <&gmac1m1_miim
&gmac1m1_tx_bus2
&gmac1m1_rx_bus2
&gmac1m1_rgmii_clk
&gmac1m1_rgmii_bus>;
pinctrl-1 = <&gmac1_pmeb_gpios>; //增加唤醒引脚
wolirq-gpio = <&gpio0 RK_PB3 GPIO_ACTIVE_LOW>; //增加唤醒引脚
tx_delay = <0x4f>;
rx_delay = <0x26>;
phy-handle = <&rgmii_phy1>;
phy-supply = <&vcc_3v3>;
status = "okay";
};
Pinctrl 配置
&pinctrl {
gmac-wol {
gmac1_pmeb_gpios: gmac1-pmeb-gpios {
rockchip,pins = <0 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
4.4 GPIO 按键节点
用于模拟电源键唤醒,在 rk3568-evb.dtsi 中添加:
keys: keys {
compatible = "gpio-keys";
status = "okay";
};
5. 驱动修改
5.1 添加模拟电源按下函数
为了在 WOL 中断触发时模拟按下电源键,需要在 gpio-keys 驱动中导出一个发送电源键事件的函数:
cpp
// kernel-5.10/drivers/input/keyboard/gpio_keys.c
+ static struct input_dev *sinput_dev;
+ void rk_send_power_key(int state)
+ {
+ if (!sinput_dev) {
+ printk("wol_debug: rk_send_power_key ERROR - sinput_dev is NULL!\n");
+ return;
+ }
+ if (state) {
+ input_report_key(sinput_dev, KEY_POWER, 1);
+ input_sync(sinput_dev);
+ printk("wol_debug: rk_send_power_key - KEY_POWER pressed\n");
+ } else {
+ input_report_key(sinput_dev, KEY_POWER, 0);
+ input_sync(sinput_dev);
+ printk("wol_debug: rk_send_power_key - KEY_POWER released\n");
+ }
+ }
+ EXPORT_SYMBOL(rk_send_power_key);
// 在 probe 函数中保存 input_dev
+ static int gpio_keys_probe(...)
+ {
// ...
+ ddata->input = input;
+ sinput_dev = input;
// ...
+ }
5.2 修改 stmmac 网卡驱动
修改以下路径文件
cpp
kernel-5.10/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
5.2.1 添加头文件
cpp
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/rk_keys.h>
5.2.2 添加 WOL 中断处理函数
cpp
extern void rk_send_power_key(int state);
static irqreturn_t wol_io_isr(int irq, void *dev_id)
{
struct net_device *dev = (struct net_device *)dev_id;
struct stmmac_priv *priv = netdev_priv(dev);
struct phy_device *phydev = dev ? dev->phydev : NULL;
int value;
/* struct net_device *dev = (struct net_device *)dev_id;
struct stmmac_priv *priv = netdev_priv(dev); */
// 先禁用中断,防止风暴
disable_irq_nosync(irq);
printk("wol_debug:Enter 520 - 4 wol_io_isr0");
// ========== 添加调试信息 ==========
printk("wol_debug: ===== WOL INTERRUPT TRIGGERED! irq=%d =====\n", irq);
if (priv->plat->wolirq_io > 0) {
int val = gpio_get_value(priv->plat->wolirq_io);
printk("wol_debug: wol_io_isr, GPIO %d value = %d\n",
priv->plat->wolirq_io, val);
}
// ====================================
// ========== 清除 PHY 的 WOL 状态,让 PMEB 恢复高电平 ==========
if (phydev) {
printk("wol_debug: Clearing PHY WOL status\n");
// 清除 WOL 事件
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 16, 0x0); // 禁用 WOL 事件
// 复位 WOL
phy_write(phydev, 31, 0x0d8a);
value = phy_read(phydev, 17);
phy_write(phydev, 17, value | BIT(15)); // 复位
phy_write(phydev, 31, 0x0d8a);
value = phy_read(phydev, 17);
phy_write(phydev, 17, value & (~BIT(15))); // 清除复位
// 重新启用 WOL 事件(如果需要后续再次唤醒)
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 16, 0x1000);
phy_write(phydev, 31, 0xa42);
printk("wol_debug: PHY WOL status cleared\n");
}
// ===========================================================
// 清除 GPIO 中断状态(如果有边沿触发,可能需要读寄存器)
// 对于 GPIO 中断,读取 GPIO 状态可以清除中断
if (priv->plat->wolirq_io > 0) {
gpio_get_value(priv->plat->wolirq_io);
}
wake_lock_timeout(&priv->plat->wol_wake_lock, msecs_to_jiffies(8000));
//rk_send_wakeup_key();
//pm_wakeup_event(priv->device, 0);
rk_send_power_key(1);
rk_send_power_key(0);
rk_send_power_key(1);
printk("wol_debug:Enter wol_io_isr 1");
return IRQ_HANDLED;
}
5.2.3 修改 stmmac_open 函数
在网卡打开时注册 WOL GPIO 中断:
cpp
static int stmmac_open(struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
int bfsize = 0;
u32 chan;
int ret;
ret = pm_runtime_get_sync(priv->device);
if (ret < 0) {
pm_runtime_put_noidle(priv->device);
return ret;
}
if (priv->hw->pcs != STMMAC_PCS_TBI &&
priv->hw->pcs != STMMAC_PCS_RTBI &&
priv->hw->xpcs == NULL) {
ret = stmmac_init_phy(dev);
if (ret) {
netdev_err(priv->dev,
"%s: Cannot attach to PHY (error: %d)\n",
__func__, ret);
goto init_phy_error;
}
}
/* Extra statistics */
memset(&priv->xstats, 0, sizeof(struct stmmac_extra_stats));
priv->xstats.threshold = tc;
bfsize = stmmac_set_16kib_bfsize(priv, dev->mtu);
if (bfsize < 0)
bfsize = 0;
if (bfsize < BUF_SIZE_16KiB)
bfsize = stmmac_set_bfsize(dev->mtu, priv->dma_buf_sz);
priv->dma_buf_sz = bfsize;
buf_sz = bfsize;
priv->rx_copybreak = STMMAC_RX_COPYBREAK;
if (!priv->dma_tx_size)
priv->dma_tx_size = priv->plat->dma_tx_size ? priv->plat->dma_tx_size :
DMA_DEFAULT_TX_SIZE;
if (!priv->dma_rx_size)
priv->dma_rx_size = priv->plat->dma_rx_size ? priv->plat->dma_rx_size :
DMA_DEFAULT_RX_SIZE;
/* Earlier check for TBS */
for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++) {
struct stmmac_tx_queue *tx_q = &priv->tx_queue[chan];
int tbs_en = priv->plat->tx_queues_cfg[chan].tbs_en;
/* Setup per-TXQ tbs flag before TX descriptor alloc */
tx_q->tbs |= tbs_en ? STMMAC_TBS_AVAIL : 0;
}
ret = alloc_dma_desc_resources(priv);
if (ret < 0) {
netdev_err(priv->dev, "%s: DMA descriptors allocation failed\n",
__func__);
goto dma_desc_error;
}
ret = init_dma_desc_rings(dev, GFP_KERNEL);
if (ret < 0) {
netdev_err(priv->dev, "%s: DMA descriptors initialization failed\n",
__func__);
goto init_error;
}
if (priv->plat->serdes_powerup) {
ret = priv->plat->serdes_powerup(dev, priv->plat->bsp_priv);
if (ret < 0) {
netdev_err(priv->dev, "%s: Serdes powerup failed\n",
__func__);
goto init_error;
}
}
ret = stmmac_hw_setup(dev, true);
if (ret < 0) {
netdev_err(priv->dev, "%s: Hw setup failed\n", __func__);
goto init_error;
}
/* ========== 新增在这里添加 WOL GPIO 中断申请 ========== */
if (priv->plat->wolirq_io > 0) {
printk("wol_debug: stmmac_open - wolirq_io = %d\n", priv->plat->wolirq_io);
ret = devm_gpio_request(priv->device, priv->plat->wolirq_io, "gmac_wol_io");
if (ret) {
pr_err("%s: ERROR: failed to request WOL GPIO %d, err: %d\n",
__func__, priv->plat->wolirq_io, ret);
} else {
int val = gpio_get_value(priv->plat->wolirq_io);
printk("wol_debug: stmmac_open - GPIO %d requested, initial value = %d\n",
priv->plat->wolirq_io, val);
// 设置 GPIO 为输入
gpio_direction_input(priv->plat->wolirq_io);
priv->plat->wol_irq = gpio_to_irq(priv->plat->wolirq_io);
// 设置中断触发方式为下降沿
irq_set_irq_type(priv->plat->wol_irq, IRQF_TRIGGER_FALLING);
ret = devm_request_irq(priv->device, priv->plat->wol_irq, wol_io_isr,
IRQF_SHARED | IRQF_TRIGGER_FALLING, "gmac_wol_io_irq", dev);
if (ret) {
pr_err("%s: ERROR: request wol io irq fail: %d", __func__, ret);
devm_gpio_free(priv->device, priv->plat->wolirq_io);
} else {
/* fixed first enable_irq crash issue */
disable_irq(priv->plat->wol_irq);
enable_irq(priv->plat->wol_irq);
disable_irq(priv->plat->wol_irq);
}
}
}
/* ========== 添加结束 ========== */
stmmac_init_coalesce(priv);
phylink_start(priv->phylink);
/* We may have called phylink_speed_down before */
phylink_speed_up(priv->phylink);
/* Request the IRQ lines */
ret = request_irq(dev->irq, stmmac_interrupt,
IRQF_SHARED, dev->name, dev);
if (unlikely(ret < 0)) {
netdev_err(priv->dev,
"%s: ERROR: allocating the IRQ %d (error: %d)\n",
__func__, dev->irq, ret);
goto irq_error;
}
/* Request the Wake IRQ in case of another line is used for WoL */
if (priv->wol_irq != dev->irq) {
ret = request_irq(priv->wol_irq, stmmac_interrupt,
IRQF_SHARED, dev->name, dev);
if (unlikely(ret < 0)) {
netdev_err(priv->dev,
"%s: ERROR: allocating the WoL IRQ %d (%d)\n",
__func__, priv->wol_irq, ret);
goto wolirq_error;
}
}
/* Request the IRQ lines */
if (priv->lpi_irq > 0) {
ret = request_irq(priv->lpi_irq, stmmac_interrupt, IRQF_SHARED,
dev->name, dev);
if (unlikely(ret < 0)) {
netdev_err(priv->dev,
"%s: ERROR: allocating the LPI IRQ %d (%d)\n",
__func__, priv->lpi_irq, ret);
goto lpiirq_error;
}
}
stmmac_enable_all_queues(priv);
netif_tx_start_all_queues(priv->dev);
return 0;
lpiirq_error:
if (priv->wol_irq != dev->irq)
free_irq(priv->wol_irq, dev);
wolirq_error:
free_irq(dev->irq, dev);
irq_error:
phylink_stop(priv->phylink);
for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++)
del_timer_sync(&priv->tx_queue[chan].txtimer);
stmmac_hw_teardown(dev);
init_error:
free_dma_desc_resources(priv);
dma_desc_error:
phylink_disconnect_phy(priv->phylink);
init_phy_error:
pm_runtime_put(priv->device);
return ret;
}
5.2.4 修改 stmmac_release 函数
在网卡关闭时释放资源:
cpp
static int stmmac_release(struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
u32 chan;
if (device_may_wakeup(priv->device))
phylink_speed_down(priv->phylink, false);
/* Stop and disconnect the PHY */
phylink_stop(priv->phylink);
phylink_disconnect_phy(priv->phylink);
if (priv->plat->integrated_phy_power)
priv->plat->integrated_phy_power(priv->plat->bsp_priv, false);
stmmac_disable_all_queues(priv);
for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++)
del_timer_sync(&priv->tx_queue[chan].txtimer);
/* Free the IRQ lines */
free_irq(dev->irq, dev);
if (priv->wol_irq != dev->irq)
free_irq(priv->wol_irq, dev);
if (priv->lpi_irq > 0)
free_irq(priv->lpi_irq, dev);
if (priv->eee_enabled) {
priv->tx_path_in_lpi_mode = false;
del_timer_sync(&priv->eee_ctrl_timer);
}
/* Stop TX/RX DMA and clear the descriptors */
stmmac_stop_all_dma(priv);
/* Release and free the Rx/Tx resources */
free_dma_desc_resources(priv);
/* Disable the MAC Rx/Tx */
stmmac_mac_set(priv, priv->ioaddr, false);
/* Powerdown Serdes if there is */
if (priv->plat->serdes_powerdown)
priv->plat->serdes_powerdown(dev, priv->plat->bsp_priv);
netif_carrier_off(dev);
stmmac_release_ptp(priv);
pm_runtime_put(priv->device);
/* ========== 在这里添加释放 WOL 资源 ========== */
if (priv->plat->wol_irq > 0)
free_irq(priv->plat->wol_irq, dev);
if (priv->plat->wolirq_io > 0)
gpio_free(priv->plat->wolirq_io);
/* ========== 添加结束 ========== */
return 0;
}
5.2.5 修改 stmmac_suspend 函数
关键:在休眠时手动调用 PHY suspend 配置 WOL,并启用唤醒中断。
cpp
int stmmac_suspend(struct device *dev)
{
struct net_device *ndev = dev_get_drvdata(dev);
struct stmmac_priv *priv = netdev_priv(ndev);
u32 chan;
if (!ndev || !netif_running(ndev))
return 0;
phylink_mac_change(priv->phylink, false);
mutex_lock(&priv->lock);
netif_device_detach(ndev);
stmmac_disable_all_queues(priv);
for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++)
del_timer_sync(&priv->tx_queue[chan].txtimer);
if (priv->eee_enabled) {
priv->tx_path_in_lpi_mode = false;
del_timer_sync(&priv->eee_ctrl_timer);
}
/* Stop TX/RX DMA */
stmmac_stop_all_dma(priv);
if (priv->plat->serdes_powerdown)
priv->plat->serdes_powerdown(ndev, priv->plat->bsp_priv);
/* Enable Power down mode by programming the PMT regs */
if (device_may_wakeup(priv->device) && priv->plat->pmt) {
stmmac_pmt(priv, priv->hw, priv->wolopts);
priv->irq_wake = 1;
} else {
mutex_unlock(&priv->lock);
rtnl_lock();
if (device_may_wakeup(priv->device))
phylink_speed_down(priv->phylink, false);
if (priv->plat->integrated_phy_power)
priv->plat->integrated_phy_power(priv->plat->bsp_priv,
false);
phylink_stop(priv->phylink);
rtnl_unlock();
mutex_lock(&priv->lock);
stmmac_mac_set(priv, priv->ioaddr, false);
pinctrl_pm_select_sleep_state(priv->device);
}
mutex_unlock(&priv->lock);
priv->speed = SPEED_UNKNOWN;
// ========== 新增:手动调用 PHY suspend ==========
if (ndev && ndev->phydev) {
struct phy_device *phydev = ndev->phydev;
if (phydev->drv && phydev->drv->suspend) {
printk("wol_debug: stmmac_suspend - Manually calling PHY suspend\n");
phydev->drv->suspend(phydev);
}
}
// ================================================
// ========== 添加调试信息 ==========
if(!priv->plat->is_in_suspend){
printk("wol_debug: stmmac_suspend - enabling WOL irq %d\n", priv->plat->wol_irq);
if (priv->plat->wolirq_io > 0) {
int val = gpio_get_value(priv->plat->wolirq_io);
printk("wol_debug: stmmac_suspend - GPIO %d value = %d (PMEB pin level)\n",
priv->plat->wolirq_io, val);
}
enable_irq(priv->plat->wol_irq);
enable_irq_wake(priv->plat->wol_irq);
priv->plat->is_in_suspend = true;
printk("wol_debug: stmmac_suspend - wake enabled\n");
}
// ====================================
return 0;
}
5.2.6 修改 stmmac_resume 函数
cpp
int stmmac_resume(struct device *dev)
{
struct net_device *ndev = dev_get_drvdata(dev);
struct stmmac_priv *priv = netdev_priv(ndev);
int ret;
if (!netif_running(ndev))
return 0;
/* Power Down bit, into the PM register, is cleared
* automatically as soon as a magic packet or a Wake-up frame
* is received. Anyway, it's better to manually clear
* this bit because it can generate problems while resuming
* from another devices (e.g. serial console).
*/
if (device_may_wakeup(priv->device) && priv->plat->pmt) {
mutex_lock(&priv->lock);
stmmac_pmt(priv, priv->hw, 0);
mutex_unlock(&priv->lock);
priv->irq_wake = 0;
} else {
pinctrl_pm_select_default_state(priv->device);
/* reset the phy so that it's ready */
if (priv->mii)
stmmac_mdio_reset(priv->mii);
if (priv->plat->integrated_phy_power)
priv->plat->integrated_phy_power(priv->plat->bsp_priv,
true);
}
if (priv->plat->serdes_powerup) {
ret = priv->plat->serdes_powerup(ndev,
priv->plat->bsp_priv);
if (ret < 0)
return ret;
}
if (!device_may_wakeup(priv->device) || !priv->plat->pmt) {
rtnl_lock();
phylink_start(priv->phylink);
/* We may have called phylink_speed_down before */
phylink_speed_up(priv->phylink);
rtnl_unlock();
}
rtnl_lock();
mutex_lock(&priv->lock);
stmmac_reset_queues_param(priv);
stmmac_free_tx_skbufs(priv);
stmmac_clear_descriptors(priv);
stmmac_hw_setup(ndev, false);
stmmac_init_coalesce(priv);
stmmac_set_rx_mode(ndev);
stmmac_restore_hw_vlan_rx_fltr(priv, ndev, priv->hw);
stmmac_enable_all_queues(priv);
mutex_unlock(&priv->lock);
rtnl_unlock();
phylink_mac_change(priv->phylink, true);
netif_device_attach(ndev);
// ==========新增 ==========
if(priv->plat->is_in_suspend){
printk("wol_debug: stmmac_resume - disabling WOL irq %d\n", priv->plat->wol_irq);
disable_irq(priv->plat->wol_irq);
disable_irq_wake(priv->plat->wol_irq);
priv->plat->is_in_suspend = false;
printk("wol_debug: stmmac_resume - wake disabled\n");
// 重新启用中断(如果之前在 ISR 中禁用了)
enable_irq(priv->plat->wol_irq);
}
// ====================================
return 0;
}
5.2.7 修改 probe/remove 函数
在驱动加载/卸载时初始化/销毁 wake_lock:
cpp
// stmmac_dvr_probe 中
wake_lock_init(&priv->plat->wol_wake_lock, WAKE_LOCK_SUSPEND, "wol_wake_lock");
// stmmac_dvr_remove 中
wake_lock_destroy(&priv->plat->wol_wake_lock);
5.3 修改平台文件
修改以下路径文件
cpp
kernel-5.10/drivers/net/ethernet/stmicro/stmmac/stmmac_main.cstmmac_platform.c
来解析设备树中的 wolirq-gpio 属性:
cpp
// 添加头文件
#include <linux/of_gpio.h>
// 在 stmmac_probe_config_dt 中
enum of_gpio_flags flags;
// ...
plat->wolirq_io = of_get_named_gpio_flags(np, "wolirq-gpio", 0, &flags);
5.4 修改 stmmac.h
修改以下路径文件
cpp
kernel-5.10/include/linux/stmmac.h
来添加 WOL 相关字段到 plat_stmmacenet_data:
cpp
struct plat_stmmacenet_data {
// ...
+ int wolirq_io;
+ bool is_in_suspend;
+ int wol_irq;
+ struct wake_lock wol_wake_lock;
};
5.5 修改 RTL8211F PHY 驱动
在 realtek.c 中为 RTL8211F 实现专用的 suspend/resume 函数(替代通用的 genphy_suspend):
5.5.1 RTL8211F WOL 寄存器配置
| 步骤 | 页 (Reg31) | 寄存器 | 操作 | 说明 |
|---|---|---|---|---|
| 1 | 0x0d8a | 16 | 写 0x0 | 禁用 WOL 事件 |
| 2 | 0x0d8a | 17 | BIT15 置 1 | 复位 WOL 状态 |
| 3 | 0x0d8a | 17 | BIT15 置 0 | 清除复位 |
| 4 | 0x0d8c | 16-18 | 写 MAC 地址 | 设置 PHY 识别的 MAC 地址 |
| 5 | 0x0d8a | 17 | 写 0x9fff | 设置最大包长度 |
| 6 | 0x0d8a | 16 | 写 0x1000 | 启用 Magic Packet WOL 事件 |
| 7 | 0x0d40 | 22 | BIT5 置 1 | INTB 切换为 PMEB 模式 |
| 8 | 0x0d8a | 19 | BIT15 置 1 | 禁用 RGMII 引脚(省电) |
5.5.2 实现代码
修改以下路径文件
cpp
kernel-5.10/drivers/net/phy/realtek.c
cpp
static int rtl8211f_suspend(struct phy_device *phydev)
{
int value;
struct net_device *ndev = phydev->attached_dev;
printk("wol_debug: ===== rtl8211f_suspend called =====\n");
if (ndev != NULL) {
// 步骤1:先清除 WOL 状态
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 16, 0x0);
phy_write(phydev, 31, 0x0d8a);
value = phy_read(phydev, 17);
phy_write(phydev, 17, value | BIT(15));
printk("wol_debug: WOL cleared\n");
// 步骤2:设置 MAC 地址
phy_write(phydev, 31, 0x0d8c);
phy_write(phydev, 16, ((u16)ndev->dev_addr[1] << 8) + ndev->dev_addr[0]);
phy_write(phydev, 17, ((u16)ndev->dev_addr[3] << 8) + ndev->dev_addr[2]);
phy_write(phydev, 18, ((u16)ndev->dev_addr[5] << 8) + ndev->dev_addr[4]);
printk("wol_debug: MAC address set\n");
// 步骤3:设置最大包长度
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 17, 0x9fff);
printk("wol_debug: max packet length set\n");
// 步骤4:启用 WOL 事件 (Magic Packet)
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 16, 0x1000);
value = phy_read(phydev, 16);
printk("wol_debug: WOL enable = 0x%04x (expected 0x1000)\n", value);
// 步骤5:最后切换 INTB 为 PMEB 模式
phy_write(phydev, 31, 0x0d40);
value = phy_read(phydev, 22);
printk("wol_debug: INTB before = 0x%04x\n", value);
phy_write(phydev, 22, value | BIT(5));
value = phy_read(phydev, 22);
printk("wol_debug: INTB after = 0x%04x, BIT5=%d\n", value, (value >> 5) & 1);
// 步骤6:禁用 RGMII 引脚(省电)
phy_write(phydev, 31, 0x0d8a);
value = phy_read(phydev, 19);
phy_write(phydev, 19, value | BIT(15));
printk("wol_debug: RGMII pad disabled\n");
phy_write(phydev, 31, 0xa42);
} else {
printk("wol_debug: ndev is NULL, skipping WOL config\n");
}
printk("wol_debug: ===== rtl8211f_suspend done =====\n");
return 0;
}
static int rtl8211f_resume(struct phy_device *phydev)
{
int value;
printk("wol_debug: ===== rtl8211f_resume called =====\n");
// 步骤1:先恢复 RGMII 引脚
phy_write(phydev, 31, 0x0d8a);
value = phy_read(phydev, 19);
phy_write(phydev, 19, value & (~BIT(15)));
printk("wol_debug: RGMII pad restored\n");
// 步骤2:PMEB → INTB(先切回中断模式)
phy_write(phydev, 31, 0x0d40);
value = phy_read(phydev, 22);
phy_write(phydev, 22, value & (~BIT(5)));
printk("wol_debug: INTB restored (PMEB -> INTB)\n");
// 步骤3:清除 WOL 事件和复位
phy_write(phydev, 31, 0x0d8a);
phy_write(phydev, 16, 0x0);
value = phy_read(phydev, 17);
phy_write(phydev, 17, value & (~BIT(15)));
printk("wol_debug: WOL cleared\n");
phy_write(phydev, 31, 0xa42);
msleep(100);
printk("wol_debug: ===== rtl8211f_resume done =====\n");
return 0;
}
5.5.3 修改 RTL8211F 驱动结构
cpp
{
PHY_ID_MATCH_EXACT(0x001cc916),
.name = "RTL8211F Gigabit Ethernet",
.config_init = &rtl8211f_config_init,
.ack_interrupt = &rtl8211f_ack_interrupt,
.config_intr = &rtl8211f_config_intr,
- .suspend = genphy_suspend,
- .resume = rtl821x_resume,
+ .suspend = rtl8211f_suspend,
+ .resume = rtl8211f_resume,
.read_page = rtl821x_read_page,
.write_page = rtl821x_write_page,
},
注意:这里需要把rtl821x_resume函数注释掉,否则编译会报错rtl821x_resume函数未使用
6. 调试过程与问题解决
1.使能网口WOL,调试没问题可以加到开机服务里去执行
bash
echo enabled > /sys/class/net/eth0/device/power/wakeup
确认使能
bash
cat /sys/class/net/eth0/device/power/wakeup
2.查看中断是否注册成功
bash
cat /proc/interrupts
可以看到如下,表示注册成功了
bash
110: 0 0 0 0 rockchip_gpio_irq 11 Edge gmac_wol_io_irq
3.可以把内核日志缓冲成文件,万一调试错误,导致睡眠后唤不醒可以重启看日志
bash
dmesg -w | grep -i wol_debug > /data/wol_log.txt &
4.执行以下命令进入睡眠
bash
echo mem > /sys/power/state
会看到日志打印:
bash
INFO: wakeup source config[0x15]:
INFO: Enable CPU0 interrupt as wakeup source
INFO: Enable CPU2 interrupt as wakeup source
5.用安装在笔记本上的magic_pkt工具通过网口给RK3568安卓板发送魔术包测试,工具我已打包绑定资源,包括一些官方文档之类。
总结
本文详细介绍了在 RK3568 平台为 RTL8211F PHY 适配 WOL 功能的完整过程,有需要很多注重的细节。本文是经过实际工程验证的,希望能为做类似功能的朋友提供参考,节约时间。