目录
前言
1. WOL 技术介绍
1.1 什么是 WOL
1.2 Magic Packet 格式
1.3 WOL 触发条件
2. 硬件平台与方案概述
2.1 硬件平台
2.2 方案概述
3. 硬件原理分析
3.1 RTL8211F INTB/PMEB 引脚
3.2 PMEB 引脚特性
3.3 硬件连接示意
3.4 关键时序
4. 设备树配置
4.1 电源管理配置
4.2 休眠/唤醒配置
4.3 GMAC 节点配置
4.4 GPIO 按键节点
5. 驱动修改
5.1 添加模拟电源按下函数
5.2 修改 stmmac 网卡驱动
5.2.1 添加头文件
5.2.2 添加 WOL 中断处理函数
5.2.3 修改 stmmac_open 函数
5.2.4 修改 stmmac_release 函数
5.2.5 修改 stmmac_suspend 函数
5.2.6 修改 stmmac_resume 函数
5.2.7 修改 probe/remove 函数
5.3 修改平台文件
5.4 修改 stmmac.h
5.5 修改 RTL8211F PHY 驱动
5.5.1 RTL8211F WOL 寄存器配置
5.5.2 实现代码
5.5.3 修改 RTL8211F 驱动结构
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]配置功能:
| bit[5] 值 | 功能 |
|---|---|
| 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 节点(主网卡),板级设备树文件
&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 驱动中导出一个发送电源键事件的函数:
// 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 网卡驱动
修改以下路径文件
kernel-5.10/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c5.2.1 添加头文件
#include <linux/interrupt.h> #include <linux/gpio.h> #include <linux/rk_keys.h>5.2.2 添加 WOL 中断处理函数
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 中断:
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函数
在网卡关闭时释放资源:
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,并启用唤醒中断。
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函数
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:
// 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 修改平台文件
修改以下路径文件
kernel-5.10/drivers/net/ethernet/stmicro/stmmac/stmmac_platform.c来解析设备树中的wolirq-gpio属性:
// 添加头文件 #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
修改以下路径文件
kernel-5.10/include/linux/stmmac.h来添加 WOL 相关字段到plat_stmmacenet_data:
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 实现代码
修改以下路径文件
kernel-5.10/drivers/net/phy/realtek.cstatic 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 驱动结构
{ 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,调试没问题可以加到开机服务里去执行
echo enabled > /sys/class/net/eth0/device/power/wakeup确认使能
cat /sys/class/net/eth0/device/power/wakeup2.查看中断是否注册成功
cat /proc/interrupts可以看到如下,表示注册成功了
110: 0 0 0 0 rockchip_gpio_irq 11 Edge gmac_wol_io_irq3.可以把内核日志缓冲成文件,万一调试错误,导致睡眠后唤不醒可以重启看日志
dmesg -w | grep -i wol_debug > /data/wol_log.txt &4.执行以下命令进入睡眠
echo mem > /sys/power/state会看到日志打印:
INFO: wakeup source config[0x15]: INFO: Enable CPU0 interrupt as wakeup source INFO: Enable CPU2 interrupt as wakeup source5.用安装在笔记本上的magic_pkt工具通过网口给RK3568安卓板发送魔术包测试,工具我已打包绑定资源,包括一些官方文档之类。
总结
本文详细介绍了在 RK3568 平台为 RTL8211F PHY 适配 WOL 功能的完整过程,有需要很多注重的细节。本文是经过实际工程验证的,希望能为做类似功能的朋友提供参考,节约时间。