由于提前返回引起的待机状态下唤醒 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_lock在drm_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_statedrm_atomic_helper_resume()消费 += NULL内存泄漏 + 指针残留 + 状态污染 drm_atomic_helper_disable_all()→ CRTC OFF原子提交 → CRTC ON 硬件停留 OFF,下次 suspend 保存禁用状态 fb_set_suspend(fbdev, 1)→ SUSPENDEDfb_set_suspend(fbdev, 0)→ RUNNINGfbdev 不可用(下次 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 概率无输出的根因。