HDMI 被 RTC 唤醒 — 代码路径分析
背景
rtk-parker-aosp-squash-25q2.02-mirakulo项目
前置问题见:系统待机状态被异常唤醒.md
概述
RTC 定时唤醒 STB 时,HDMI 会短暂输出信号导致电视黑屏,然后恢复无信号。
根因:内核 PM 框架的 Resume 不判断唤醒源,无条件恢复所有设备,HDMI 驱动在 resume 时无条件打开 PHY。
修复方向:在 rtk_drm_resume() 和 rtk_hdmi_resume() 中判断唤醒源,RTC 后台唤醒时跳过显示/PHY 初始化。
一、系统唤醒总入口
kernel/power/suspend.c:490-529
int suspend_devices_and_enter(suspend_state_t state)
{
// ① suspend 所有设备
dpm_suspend_start(PMSG_SUSPEND);
// ② SoC 深度睡眠
suspend_enter(state, &wakeup);
Resume_devices: // ③ 被 RTC/IR/GPIO 唤醒后从这里开始
dpm_resume_end(PMSG_RESUME); // 恢复所有设备
}
醒来后 dpm_resume_end() 是唯一出口,不管谁唤醒的都走这里。
二、dpm_resume — 无差别恢复所有设备
drivers/base/power/main.c:1001-1040
void dpm_resume(pm_message_t state)
{
while (!list_empty(&dpm_suspended_list)) { // 遍历所有已 suspend 的设备
dev = to_device(dpm_suspended_list.next);
device_resume(dev); // 逐个恢复
}
}
dpm_suspended_list 是内核 PM 框架维护的全局链表,所有被 suspend 的设备都在上面。HDMI 设备在 suspend 时被加入链表,resume 时必然被取出恢复。
这个 while 循环没有任何 if (wakeup_source == RTC) skip 的判断。
三、device_resume 如何找到 HDMI 的回调
drivers/base/power/main.c:888-958
static void __device_resume(struct device *dev, pm_message_t state)
{
// 按优先级找回调:pm_domain → type → class → bus → driver
if (!callback && dev->driver && dev->driver->pm)
callback = pm_op(dev->driver->pm, state); // PMSG_RESUME → .resume 回调
dpm_run_callback(callback, dev, state, info); // 调用回调
}
pm_op() 根据 state.event == PM_EVENT_RESUME 返回 ops->resume。
HDMI 驱动注册了 dev_pm_ops,rtk_hdmi.c:2383-2402:
static const struct dev_pm_ops rtk_hdmi_pm_ops = {
.resume = rtk_hdmi_resume, // ← 被 pm_op() 返回
};
struct platform_driver rtk_hdmi_driver = {
.driver = {
.name = "rtk-hdmi",
.pm = &rtk_hdmi_pm_ops, // ← dev->driver->pm 指向这里
},
};
DRM 驱动也注册了自己的 resume:
rtk_drm_drv.c:244-256:
static int rtk_drm_resume(struct device *dev)
{
return drm_mode_config_helper_resume(drm); // 恢复 CRTC/Plane/Encoder
}
四、rtk_hdmi_resume — 无条件打开 PHY
rtk_hdmi.c:2294-2352
static int rtk_hdmi_resume(struct device *dev)
{
hdmi->in_suspend = false;
gpiod_set_debounce(hdmi->hpd_gpio, 30*1000);
// HDCP 重新协商 ...
hdmi->hdmi_ops->update_hpd_state(hdmi); // ← 关键调用
enable_irq(hdmi->hpd_irq);
return 0;
}
update_hpd_state() 是主动调用,不是硬件自动行为。 这里不判断唤醒源,任何 resume 都会触发后续的热插拔链路。
五、update_hpd_state — 判定为"新连接"
rtk_hdmi.c:1342-1414
static bool rtk_hdmi_update_hpd_state(struct rtk_hdmi *hdmi)
{
hpd = gpiod_get_value(hdmi->hpd_gpio); // TV 连着 → 1
if ((hdmi->hpd_state == 0) && (hpd == 1)) { // suspend 后 hpd_state 被清零
hdmi->edid_cache = drm_get_edid(&hdmi->connector, hdmi->ddc); // 读 EDID
}
hdmi->hpd_state = hpd;
is_connected = hpd && rxsense;
if (is_connected != pre_state) {
drm_helper_hpd_irq_event(hdmi->connector.dev); // ★ 触发 DRM 热插拔
extcon_set_state_sync(hdmi->edev, EXTCON_DISP_HDMI, true);
}
}
Suspend 时 PHY 断电 → hpd_state 复位为 0。Resume 后读 GPIO 得到 1 → (hpd_state == 0) && (hpd == 1) 成立 → 当作"新连接"处理:读 EDID、通知 DRM。
六、drm_helper_hpd_irq_event — 触发 modeset
drivers/gpu/drm/drm_probe_helper.c:831-881
bool drm_helper_hpd_irq_event(struct drm_device *dev)
{
drm_kms_helper_hotplug_event(dev);
// → drm_client_dev_hotplug(dev)
// → connector->detect() → connector_status_connected
// → drm_fb_helper_hotplug_event()
// → drm_fb_helper_force_modeset() // fb_helper 自动设置显示模式
// → drm_atomic_commit()
// → encoder->enable()
}
drm_fb_helper 是内核自带的 framebuffer 辅助层。当检测到 connector 变为 connected,它自动配置显示模式——不需要用户空间参与。
七、encoder enable — 真正打开 PHY
rtk_hdmi.c:1692-1663
// DRM 框架调用 encoder 的 enable 回调
static void rtk_hdmi_enc_enable(struct drm_encoder *encoder)
{
rtk_hdmi_setup(hdmi, &hdmi->previous_mode);
}
// rtk_hdmi_setup 内部:
static void rtk_hdmi_setup(struct rtk_hdmi *hdmi, struct drm_display_mode *mode)
{
hdmitx_set_video_timing(hdmi, mode); // 配置 video timing → PHY 上电
rtk_hdmi_send_avmute(hdmi, 0); // 取消 AVMUTE → 开始输出画面
hdmi->is_hdmi_on = true;
}
到这一步,TMDS 信号开始输出,电视检测到信号 → 黑屏。
八、Suspend 时如何关闭
rtk_hdmi.c:2250-2291 + rtk_hdmi.c:1701-1708
// PM suspend 回调
static int rtk_hdmi_suspend(struct device *dev)
{
disable_irq(hdmi->hpd_irq);
rtk_hdmi_off(hdmi, OFF_FLAG_HDCP | OFF_FLAG_DIS_AUDIO); // 关 PHY
return 0;
}
// DRM encoder disable 回调
static void rtk_hdmi_enc_disable(struct drm_encoder *encoder)
{
rtk_hdmi_off(hdmi, OFF_FLAG_NORMAL); // 关 PHY
}
九、Suspend 为什么会失败一次
日志中常出现 PM: Some devices failed to suspend, or early wake event detected。
drivers/base/power/main.c:1633-1637:
static int __device_suspend(struct device *dev, ...)
{
if (pm_wakeup_pending()) { // 检测到未处理的唤醒事件
async_error = -EBUSY; // 不打印设备名,直接中止
goto Complete;
}
// ...
}
原因是 RTC 唤醒后在 re-suspend 过程中 PCPU 发出了一个无效的唤醒通知(wakeup_event = 10 = MAX_EVENT),pm_wakeup_pending() 检测到后中止了第一次 suspend。恢复后重试,第二次成功。
根因在 rtk_pm.c:157:
dev_pm->wakeup_reason = MAX_EVENT; // suspend 时初始化为 MAX_EVENT
如果 PCPU 在 re-suspend 前来不及更新正确的唤醒源,IR 驱动就会读到 10,打印 Bad wakeup event value 10。
十、完整链路(一张图)
RTC 到期
→ PCPU 唤醒 SoC
→ suspend_devices_and_enter() → Resume_devices
→ dpm_resume_end(PMSG_RESUME)
→ dpm_resume() 遍历 dpm_suspended_list
→ device_resume(HDMI_DEV)
→ rtk_hdmi_pm_ops.resume = rtk_hdmi_resume()
→ update_hpd_state(hdmi)
→ hpd=1, hpd_state=0 → 读 EDID
→ drm_helper_hpd_irq_event()
→ drm_kms_helper_hotplug_event()
→ fb_helper 自动 modeset
→ rtk_hdmi_enc_enable()
→ rtk_hdmi_setup()
→ PHY 上电 → TMDS 输出 → 电视黑屏
这条链路上没有任何一步判断唤醒源。只要 SoC resume,HDMI 必然亮屏。
十一、修复方案
在这条链路的两个入口处加唤醒源判断:
11.1 DRM 层 — rtk_drm_resume
rtk_drm_drv.c:251-255
static int rtk_drm_resume(struct device *dev)
{
struct drm_device *drm = dev_get_drvdata(dev);
if (rtk_pm_get_wakeup_reason() == ALARM_EVENT)
return 0; // RTC 后台唤醒 → 不恢复显示管线
return drm_mode_config_helper_resume(drm);
}
11.2 HDMI 层 — rtk_hdmi_resume(兜底)
rtk_hdmi.c:2296-2315
static int rtk_hdmi_resume(struct device *dev)
{
hdmi->in_suspend = false;
if (rtk_pm_get_wakeup_reason() == ALARM_EVENT)
return 0; // RTC 后台唤醒 → 不初始化 PHY
gpiod_set_debounce(hdmi->hpd_gpio, 30*1000);
// ...
hdmi->hdmi_ops->update_hpd_state(hdmi);
enable_irq(hdmi->hpd_irq);
return 0;
}
11.3 Suspend 防重入
rtk_drm_drv.c:244-249
static int rtk_drm_suspend(struct device *dev)
{
struct drm_device *drm = dev_get_drvdata(dev);
if (drm->mode_config.suspend_state) // 已 suspend 过,跳过
return 0;
return drm_mode_config_helper_suspend(drm);
}
11.4 效果
| 唤醒类型 | 修复前 | 修复后 |
|---|---|---|
| RTC 定时唤醒 | PHY 上电 → TV 黑屏 | PHY 保持关闭 → TV 无感知 |
| 遥控器 IR 唤醒 | 正常亮屏 | 正常亮屏 |
| GPIO 唤醒 | 正常亮屏 | 正常亮屏 |
| CEC 唤醒 | 正常亮屏 | 正常亮屏 |
涉及文件
| 文件 | 改动 |
|---|---|
drivers/gpu/drm/realtek/rtk_drm_drv.c |
rtk_drm_resume + rtk_drm_suspend 加判断 |
drivers/gpu/drm/realtek/rtk_hdmi.c |
rtk_hdmi_resume 加判断 |
drivers/soc/realtek/common/rtk_pm_common.c |
rtk_pm_get_wakeup_reason() 接口 |
include/soc/realtek/rtk_pm.h |
接口声明 + ALARM_EVENT 枚举 |