由于提前返回引起的待机状态下唤醒 HDMI 无输出

概述

为解决 RTC 定时唤醒(ALARM_EVENT)时 HDMI PHY 异常上电,在 rtk_drm_resume()rtk_hdmi_resume() 中加入 ALARM_EVENT 判断后提前 return 0。这把 RTC 静默唤醒本身的 HDMI 上电问题解决了,但提前返回跳过了 resume 路径中的关键配对操作,导致跨 suspend/resume 周期的状态污染——下次手动唤醒时 HDMI 概率无输出。

经日志验证(等待一次RTC唤醒,然后重新进入待机后,手动退出待机后HDMI无输出.txt),问题稳定复现。


drm_mode_config_helper_resume() 源码

rtk_drm_resume() 通过调用此函数完成 DRM 层 resume。以下为内核源码(drm_modeset_helper.c),ALARM 路径的裸 return 0 跳过了这个函数的全部三步操作。同时贴上对应的 suspend 函数作为配对参考。

/* drm_modeset_helper.c L195-223 */
int drm_mode_config_helper_suspend(struct drm_device *dev)
{
    struct drm_atomic_state *state;

    if (!dev)
        return 0;

    if (dev->mode_config.poll_enabled)
        drm_kms_helper_poll_disable(dev);                // ③ 配对: poll_disable

    drm_fb_helper_set_suspend_unlocked(dev->fb_helper, 1); // ② 配对: fb→SUSPENDED
    state = drm_atomic_helper_suspend(dev);                // ① 配对: 分配 state
    if (IS_ERR(state)) {
        drm_fb_helper_set_suspend_unlocked(dev->fb_helper, 0);
        if (dev->mode_config.poll_enabled)
            drm_kms_helper_poll_enable(dev);
        return PTR_ERR(state);
    }

    dev->mode_config.suspend_state = state;
    return 0;
}

/* drm_modeset_helper.c L240-263 */
int drm_mode_config_helper_resume(struct drm_device *dev)
{
    int ret;

    if (!dev)
        return 0;

    if (WARN_ON(!dev->mode_config.suspend_state))
        return -EINVAL;

    // 步骤 ①: 恢复原子状态 + 释放内存
    ret = drm_atomic_helper_resume(dev, dev->mode_config.suspend_state);
    if (ret)
        DRM_ERROR("Failed to resume (%d)\n", ret);
    dev->mode_config.suspend_state = NULL;

    // 步骤 ②: 恢复 fbdev
    drm_fb_helper_set_suspend_unlocked(dev->fb_helper, 0);

    // 步骤 ③: 恢复输出轮询
    if (dev->mode_config.poll_enabled)
        drm_kms_helper_poll_enable(dev);

    return ret;
}

suspend 和 resume 的配对关系:

suspend 侧 resume 侧
drm_atomic_helper_suspend() 分配 state drm_atomic_helper_resume() 消费 + = NULL
drm_fb_helper_set_suspend(fb, 1) → SUSPENDED drm_fb_helper_set_suspend(fb, 0) → RUNNING
drm_kms_helper_poll_disable() drm_kms_helper_poll_enable()

ALARM 路径的 return 0 跳过了 resume 侧的全部三个配对。


rtk_drm_resume() 提前返回跳过的内容

rtk_drm_resume() 通过调用 drm_mode_config_helper_resume() 完成 resume。ALARM 路径直接 return 0跳过了以下三步全部操作

步骤 ① — drm_atomic_helper_resume() + suspend_state 清理

定义位置:drm_modeset_helper.c L240-263,drm_atomic_helper.c L3369-3385

drm_mode_config_helper_resume(dev)
├── drm_atomic_helper_resume(dev, suspend_state)
│   ├── drm_mode_config_reset(dev)          → 所有 connector→status = unknown
│   ├── drm_atomic_helper_commit(state)     → 提交原子状态到硬件
│   │   ├── CRTC → ON                       → 显示管线重新启用
│   │   ├── connector → connected            → connector 状态恢复
│   │   └── mode → 之前保存的分辨率           → 编码器 mode_set + enable
│   └── drm_atomic_state_put(state)         → 释放 suspend 时分配的内存
└── dev->mode_config.suspend_state = NULL    → 指针清零
跳过的具体操作 直接后果 跨周期后果
CRTC → ON 所有显示管线停留在 OFF 下次 suspend 保存 CRTC=OFF
connector → connected connector 停留在 disconnected 下次 suspend 保存 connector=disconnected
mode 恢复 + encoder enable 编码器不上电,无 TMDS 输出 状态被保存为"全禁用"快照
drm_atomic_state_put(state) 内存泄漏(每次 RTC 唤醒泄漏数 KB) 累积消耗
suspend_state = NULL 指针残留非 NULL 若加 guard 会被误触发

步骤 ② — drm_fb_helper_set_suspend_unlocked(fb, 0)

定义位置:drm_fb_helper.c L875-902

suspend 时:  fb_set_suspend(fbdev, 1)  → fbdev→state = FBINFO_STATE_SUSPENDED
resume 时:   fb_set_suspend(fbdev, 0)  → fbdev→state = FBINFO_STATE_RUNNING
                                           ↑ 被跳过!
跳过的具体操作 直接后果 跨周期后果
fbdev 恢复为 RUNNING fb 控制台不可用 下次正常 resume 时自动修复

注:console_lockdrm_fb_helper_set_suspend_unlocked() 内部正确配对释放(L888-L901),不存在锁泄漏。

步骤 ③ — drm_kms_helper_poll_enable(dev)

suspend 时:  drm_kms_helper_poll_disable(dev)  → 输出轮询停止
resume 时:   drm_kms_helper_poll_enable(dev)   → 输出轮询恢复
                                                  ↑ 被跳过!
跳过的具体操作 直接后果 跨周期后果
输出轮询恢复 HPD 轮询检测失效 热插拔需依赖 IRQ(IRQ 在 hdmi 侧已恢复)

rtk_hdmi_resume() 提前返回跳过的内容

注:enable_irq(hpd_irq) / enable_irq(rxsense_irq) / mod_timer(rxsense_timer) 已在 ALARM 检查之前执行,当前代码不跳过它们。以下仅列出 ALARM 检查之后被跳过的操作。

步骤 ① — gpiod_set_debounce(hpd_gpio, 30ms)

suspend 时:  未显式清除 debounce(GPIO 配置在 S3 期间保持)
resume 时:   gpiod_set_debounce(hpd_gpio, 30*1000) → 重新配置去抖
                                                      ↑ 被跳过!
跳过的具体操作 直接后果 跨周期后果
HPD GPIO 去抖恢复 debounce 沿用 suspend 前配置(通常已正确) 下次正常 resume 时恢复

步骤 ② — HDCP 状态恢复

resume 时:   检测 HPD 状态 → 判定 sink_hdcp_ver → 恢复 HDCP 状态
                                                  ↑ 被跳过!
跳过的具体操作 直接后果 跨周期后果
HDCP 版本协商 HDCP 不恢复 下次正常 resume 时完整恢复

步骤 ③ — update_hpd_state() → drm_helper_hpd_irq_event()

rtk_hdmi_update_hpd_state(hdmi)
├── gpiod_get_value(hpd_gpio)              → 读 HPD GPIO
├── rtk_hdmi_get_rxsense(hdmi)             → 读 RxSense 电气状态
├── hpd_state / rxsense_state 更新          → 内部状态同步
├── extcon_set_state_sync()                → 通知 extcon 框架
└── drm_helper_hpd_irq_event(dev)          → 通知 DRM 核心 hotplug 事件
                                               ↑ 全部被跳过!
跳过的具体操作 直接后果 跨周期后果
HPD 状态同步 hdmi->hpd_state 不更新 下次 resume update_hpd_state 完整执行
drm_helper_hpd_irq_event() DRM 不知道当前连接状态 connector 状态依赖 drm_atomic_helper_resume 的恢复(若该步也被跳过则状态停留在 disconnected)

跨周期污染:根因链

上述所有跳过中,致命的是 rtk_drm_resume() 跳过了步骤①(drm_atomic_helper_resume)

污染传播路径

╔══════════════════════════════════════════════════════════════════╗
║ 第一次 Suspend(RTC 唤醒之前,系统正常运行)                       ║
╠══════════════════════════════════════════════════════════════════╣
║ drm_mode_config_helper_suspend(dev)                              ║
║   ├── drm_atomic_helper_suspend(dev)                             ║
║   │   ├── duplicate 当前状态 → state A                           ║
║   │   │   {CRTC=ON, connector=connected, mode=1080p}             ║
║   │   └── drm_atomic_helper_disable_all(dev) → CRTC OFF, 全禁用  ║
║   ├── fb_set_suspend(fbdev, 1) → fbdev SUSPENDED                ║
║   └── suspend_state = A                                          ║
╚══════════════════════════════════════════════════════════════════╝
                              ↓
╔══════════════════════════════════════════════════════════════════╗
║ RTC 唤醒 → rtk_drm_resume(): ALARM_EVENT → return 0              ║
╠══════════════════════════════════════════════════════════════════╗
║ drm_atomic_helper_resume 跳过                                     ║
║   → CRTC 仍 OFF, connector 仍 disconnected                       ║
║   → suspend_state = A (未消费,残留)                              ║
║ fb_set_suspend(fbdev, 0) 跳过                                     ║
║   → fbdev 仍 SUSPENDED                                           ║
╚══════════════════════════════════════════════════════════════════╝
                              ↓
╔══════════════════════════════════════════════════════════════════╗
║ 第二次 Suspend(RTC 唤醒后系统自动重新待机)                        ║
╠══════════════════════════════════════════════════════════════════╣
║ drm_mode_config_helper_suspend(dev)                              ║
║   ├── drm_atomic_helper_suspend(dev)                             ║
║   │   ├── duplicate 当前状态 → state B                           ║
║   │   │   {CRTC=OFF, connector=disconnected}  ← 污染!           ║
║   │   └── drm_atomic_helper_disable_all(dev) → 已是 OFF,无操作   ║
║   ├── fb_set_suspend(fbdev, 1) → state != RUNNING → 直接返回     ║
║   └── suspend_state = B (覆盖 A,A 泄漏)                          ║
╚══════════════════════════════════════════════════════════════════╝
                              ↓
╔══════════════════════════════════════════════════════════════════╗
║ 第二次 Resume(用户手动 IR 唤醒)→ not ALARM → 全路径              ║
╠══════════════════════════════════════════════════════════════════╣
║ drm_mode_config_helper_resume(dev)                               ║
║   ├── drm_atomic_helper_resume(dev, B)                           ║
║   │   ├── drm_mode_config_reset(dev) → connector=unknown         ║
║   │   └── 提交状态 B: {CRTC=OFF, connector=disconnected}         ║
║   │       → CRTC 全部 OFF → 编码器不上电 → 无 TMDS 输出           ║
║   ├── suspend_state = NULL                                       ║
║   └── fb_set_suspend(fbdev, 0) → fbdev RUNNING                   ║
║                                                                  ║
║ rtk_hdmi_resume() → update_hpd_state()                           ║
║   → 读 HPD=1 → is_connected=1 → drm_helper_hpd_irq_event()      ║
║   → 但 CRTC 已 OFF,需要 userspace 响应 hotplug 才能恢复          ║
║                                                                  ║
║ 结果: ❌ HDMI 无输出(依赖 userspace 响应 hotplug 不稳定)         ║
╚══════════════════════════════════════════════════════════════════╝

日志证据

等待一次RTC唤醒,然后重新进入待机后,手动退出待机后HDMI无输出.txt

[324.893] rtk_cec_suspend              ← 第二次 suspend 开始
[324.990] [HDMI SUSPEND] hpd_irq=51    ← HDMI 进入 suspend
[325.301] [DRM_RESUME] not ALARM_EVENT go ahead  ← 手动唤醒,全路径 resume
                                                 ← 恢复状态 B {CRTC=OFF, disconnected}
// 此后没有 "Mode set" / "Enable encoder" / "Clear AVmute"
// → HDMI 无输出

对比正常输出日志(进入待机后,不等待RTC唤醒,直接唤醒系统,HDMI正常输出.txt):

[578.676] [DRM_RESUME] not ALARM_EVENT go ahead
[579.464] Mode set 1920x1080p           ← 仅 788ms 后!HDMI 正常初始化
[579.466] Enable encoder
[579.740] Clear AVmute

拔插恢复验证

HDMI无输出后拔插HDMI.txt 证明热插拔可以绕过污染状态恢复输出:

[475.640] HPD(0) RxSense(1) → is_connected=0    ← 拔掉
[494.243] HPD(1) RxSense(1) → is_connected=1    ← 插回
[494.280] Mode set 1920x1080p                    ← DRM 完整 modeset!
[494.282] Enable encoder                          ← PHY 上电
[494.555] Clear AVmute                            ← 输出恢复

修复方案

方向 A:DRM 完整恢复 + HDMI PHY 抑制(已实现)

核心思路:让 rtk_drm_resume() 始终走完整 drm_mode_config_helper_resume(),确保 CRTC/fbdev/connector 状态全部恢复。HDMI PHY 不上电由 rtk_hdmi_enc_enable() 中的 ALARM 检查保证。这样下次 suspend 保存的是正确状态。

修改点

rtk_drm_resume() — 删除 ALARM 提前返回

// 旧:ALARM_EVENT → return 0(跳过全部三步恢复)
// 新:ALARM_EVENT 也走完整路径
return drm_mode_config_helper_resume(drm);  // 始终执行

rtk_hdmi_enc_enable() — ALARM 时抑制 PHY 上电

static void rtk_hdmi_enc_enable(struct drm_encoder *encoder)
{
    struct rtk_hdmi *hdmi = to_rtk_hdmi(encoder);

    if (rtk_pm_get_wakeup_reason() == ALARM_EVENT) {
        dev_info(hdmi->dev, "Skip encoder enable for ALARM_EVENT");
        return;  // CRTC 状态已恢复,但 PHY 不上电
    }

    dev_info(hdmi->dev, "Enable encoder");
    rtk_hdmi_setup(hdmi, &hdmi->previous_mode);
}

rtk_hdmi_resume() — 维持现有逻辑不变

// irq/timer 已在 ALARM 检查之前恢复(避免嵌套计数泄漏)
// ALARM 时跳过 gpiod_set_debounce, HDCP, update_hpd_state

修复后的调用链

ALARM_EVENT 唤醒

rtk_drm_resume()
  └→ drm_mode_config_helper_resume()      ← 完整执行
       ├→ drm_atomic_helper_resume()       ← CRTC ON ✅
       ├→ fb_set_suspend(fb, 0)            ← fbdev RUNNING ✅
       └→ drm_kms_helper_poll_enable()     ← poll 恢复 ✅
       └→ 原子提交 → rtk_hdmi_enc_enable()
            └→ ALARM_EVENT? → return        ← PHY 不上电 ✅

rtk_hdmi_resume()
  ├→ enable_irq(hpd_irq)                   ← IRQ 恢复 ✅
  ├→ enable_irq(rxsense_irq)               ← RxSense 恢复 ✅
  └→ ALARM_EVENT? → return 0               ← 不调 update_hpd_state

下次 Suspend: 保存 {CRTC=ON, connector=connected}  ← 正确状态 ✅
下次 Resume:  恢复正确状态 → HDMI 正常输出 ✅

关键原则

任何函数内提前返回时,必须保证该函数跳过的所有操作都在 suspend 侧有对应的配对操作被执行。

suspend 侧操作 resume 侧配对 不配对的后果
drm_atomic_helper_suspend() 分配 suspend_state drm_atomic_helper_resume() 消费 + = NULL 内存泄漏 + 指针残留 + 状态污染
drm_atomic_helper_disable_all() → CRTC OFF 原子提交 → CRTC ON 硬件停留 OFF,下次 suspend 保存禁用状态
fb_set_suspend(fbdev, 1) → SUSPENDED fb_set_suspend(fbdev, 0) → RUNNING fbdev 不可用(下次 resume 自动修复)
disable_irq(hpd_irq) → depth++ enable_irq(hpd_irq) → depth– IRQ 嵌套计数泄漏,永久屏蔽
del_timer_sync(rxsense_timer) mod_timer(rxsense_timer) 定时器永久停止

其中 CRTC OFF → ON 的配对是跨周期最关键的一对——跳过它导致的状态污染是 HDMI 概率无输出的根因。