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_opsrtk_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 枚举