RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
目录前言1. WOL 技术介绍1.1 什么是 WOL1.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.h5.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 什么是 WOLWake-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 硬件平台组件型号/规格SoCRockchip RK3568内核版本Linux 5.10 (Android13 SDK)以太网 MACSynopsys DesignWare GMAC (stmmac)以太网 PHYRealtek RTL8211F2.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] 值功能0INTB 模式中断引脚默认1PMEB 模式电源管理事件引脚重要在 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_REG1rk3568-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.h5.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, its 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 its 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_datastruct 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_suspend5.5.1 RTL8211F WOL 寄存器配置步骤页 (Reg31)寄存器操作说明10x0d8a16写 0x0禁用 WOL 事件20x0d8a17BIT15 置 1复位 WOL 状态30x0d8a17BIT15 置 0清除复位40x0d8c16-18写 MAC 地址设置 PHY 识别的 MAC 地址50x0d8a17写 0x9fff设置最大包长度60x0d8a16写 0x1000启用 Magic Packet WOL 事件70x0d4022BIT5 置 1INTB 切换为 PMEB 模式80x0d8a19BIT15 置 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); // 步骤2PMEB → 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 功能的完整过程有需要很多注重的细节。本文是经过实际工程验证的希望能为做类似功能的朋友提供参考节约时间。