RTC 唤醒注入 KEY_POWER — 代码路径分析

背景

rtk-parker-aosp-squash-25q2.02-mirakulo项目

概述

STB 待机时 RTC 定时唤醒,IR 驱动在 Resume 时从寄存器读到上一个按键(POWER)的 NEC 帧残留,因 KEY_E 不在键值表中导致查表失败,旧代码 fallback 注入 KEY_POWER,经 Android → CEC 触发电视开机。

根因是两层问题叠加: ① 键值表缺少 KEY_E 映射;② 旧代码在 KEY_E 查不到时无条件使用寄存器残留值调用 rc_keydown()


一、待机前:POWER 键的 NEC 帧锁存在寄存器中

用户按遥控器 POWER 让 STB 待机。IR 硬件收到 NEC 帧后存入 ISO 寄存器 0x65c

正常 ISR 路径(rtk-ir-hw.c:100-139)读出并解码,发送按键。Suspend 时 ISR 已不再触发,但硬件寄存器继续锁存上次收到的数据。

rtk-ir-hw.c:76-86 的 suspend 路径只 drain FIFO,不清零数据寄存器,残留值保留。


二、RTC 唤醒:PM notifier 读取寄存器

RTC 到期 → SoC 唤醒 → PM notifier 回调 rtk_ir_key_event()

rtk-ir-core.c:108-112

regmap_read(ir->iso, 0x65c, &val);       // 读 RX FIFO → 残留值 0xf50a4987
regmap_read(ir->iso, 0x650, &nf_code);   // 噪声检测 → 0(无真实 IR 信号)

pr_err("Wakeup entire android src %d, 0x%x, %d\n", wakeup_event, val, nf_code);
// 输出: Wakeup entire android src 3, 0xf50a4987, 0

dec->scancode(&request, val);             // 解码

唤醒事件类型定义在 rtk_pm_pcpu.h:25-31

enum rtk_wakeup_event {
    LAN_EVENT = 0,
    IR_EVENT,        // 1 — 遥控器按键唤醒
    GPIO_EVENT,      // 2 — GPIO 唤醒
    ALARM_EVENT,     // 3 — RTC 定时器唤醒 ← 本次
    // ...
};

三、NEC 解码:0xf50a4987 → 0x87490a

rtk-ir-nec.c:11-37

static int rtk_ir_nec_scancode(struct rtk_ir_scancode_req *request, unsigned int raw)
{
    addr     = (raw >>  0) & 0xff;   // 0x87
    addr_inv = (raw >>  8) & 0xff;   // 0x49
    data     = (raw >> 16) & 0xff;   // 0x0a
    data_inv = (raw >> 24) & 0xff;   // 0xf5

    // (data_inv ^ data) = 0xff → 不走 NEC32
    // (addr_inv ^ addr) = 0xCE ≠ 0xff → 走 NECX
    request->scancode = addr << 16 | addr_inv << 8 | data;
                     = 0x87 << 16 | 0x49 << 8 | 0x0a;
                     = 0x87490a;    // ← 恰是 KEY_POWER 的 scancode

    request->protocol = RC_PROTO_NECX;
}

解码出的 0x87490a 就是用户在待机前按下 POWER 键的 scancode。


四、查找 KEY_E 失败 — scancode 未被覆盖

rtk-ir-core.c:114-141 根据唤醒类型决定使用什么 scancode:

if (wakeup_event == IR_EVENT) {
    // 在 map 中匹配当前 scancode
} else if (wakeup_event == GPIO_EVENT) {
    // 查 KEY_PROG1(Netflix 按钮)
} else if (wakeup_event == ALARM_EVENT) {
    // ★ 走这里 — 查找 KEY_E
    for (k = 0; k < map->len; k++)
        if (KEY_E == (map->scan + k)->keycode) {
            request.scancode = (map->scan + k)->scancode;  // 用 KEY_E 的 scancode 覆盖
            break;
        }
    // GIEC 的 rc-realtek-dhc 中没有 KEY_E → k >= map->len
    // request.scancode 未被覆盖 → 仍是解码出来的 0x87490a
} else {
    // 其他事件 — 查 KEY_POWER
}

GIEC 的键值表 rc-realtek-dhc.c 中没有 KEY_E 条目:

static struct rc_map_table realtek_dhc[] = {
    // ...
    { 0x87490A, KEY_POWER },    // ← KEY_POWER 在表中
    { 0x8869, KEY_Y },
    { 0x8870, KEY_U },
    // ... 没有 KEY_E
};

五、旧代码无条件注入 → 命中 KEY_POWER

修复前的 rtk-ir-core.c(注入阶段):

} else if (wakeup_event == ALARM_EVENT) {
    pr_err("[ALARM_RESUME][%s] Check Wi-Fi connection, scan code: 0x%x\n",
           __func__, request.scancode);
    // 输出: [ALARM_RESUME][rtk_ir_key_event] Check Wi-Fi connection, scan code: 0x87490a

    rc_keydown(rc, map->rc_proto, request.scancode, 0);  // 无条件注入
    // → rc_keydown(NECX, 0x87490a, 0)
}

rc-main.c:848-855rc_keydown()

void rc_keydown(struct rc_dev *dev, enum rc_proto protocol, u64 scancode, u8 toggle)
{
    u32 keycode = rc_g_keycode_from_table(dev, scancode);  // 查表
    ir_do_keydown(dev, protocol, scancode, keycode, toggle);
}

rc_g_keycode_from_table()rc-main.c:605-625)对 0x87490a 二分查找 → 命中 rc-realtek-dhc 中的 { 0x87490A, KEY_POWER } → 返回 KEY_POWER (116) → Android 收到电源键事件。


六、Android 到 CEC 到电视开机

input_report_key(KEY_POWER, 1)
  → /vendor/usr/keylayout/rtk-ir-hw.kl → key 116 POWER WAKE
  → PhoneWindowManager → 检测系统从待机恢复 → 触发 HDMI-CEC
  → HdmiControlService → <Active Source> + <One Touch Play>
  → 电视收到 CEC → 开机

七、完整链路

待机前用户按 POWER
  → IR 硬件捕获 NEC 帧 → ISO 0x65c = 0xf50a4987
  → STB 进入 Suspend(寄存器不清零)
  │
RTC 到期
  → SoC 唤醒
  → PM notifier: rtk_ir_key_event()
    → regmap_read(0x65c) → val = 0xf50a4987(残留值)
    → NECX 解码 → scancode = 0x87490a
    → ALARM_EVENT → 查 map 找 KEY_E → 找不到
    → request.scancode 未被覆盖 → 仍是 0x87490a
    → 旧代码无条件 rc_keydown(0x87490a)
      → rc_g_keycode_from_table() → 命中 KEY_POWER
      → input_report_key(KEY_POWER, 1)
        → Android → CEC One Touch Play → TV ON

八、修复

ps: 最终采用了直接将

switch (wakeup_event) {
	case ALARM_EVENT:
		sendKeyEvent = false;

ALARM_EVENT的sendKeyEvent置为false,直接不注入按键到上层

8.1 解析层 — KEY_E 找不到时 scancode 置 0

rtk-ir-core.c:127-133

} else if (wakeup_event == ALARM_EVENT) {
    for (k = 0; k < map->len; k++)
        if (KEY_E == (map->scan + k)->keycode) {
            request.scancode = (map->scan + k)->scancode;
            break;
        }
    if (k == map->len)
        request.scancode = 0;      // KEY_E 找不到 → 显式清零
}

scancode = 0 永远不会命中键值表中的任何条目,确保 rc_keydown() 即使被调用也不会注入有效按键。

8.2 注入层 — KEY_E 找不到时跳过注入

rtk-ir-core.c:152-160

} else if (wakeup_event == ALARM_EVENT) {
    if (k < map->len) {
        pr_err("[ALARM_RESUME]... scan code: 0x%x\n", __func__, request.scancode);
        rc_keydown(rc, map->rc_proto, request.scancode, 0);
    } else {
        pr_err("KEY_E not in rc_map, skip ALARM inject\n");  // 找不到 → 跳过
    }
}

8.3 键值表 — 添加 KEY_E 映射(可选)

rc-realtek-dhc.c

{ 0x8749FF, KEY_E },   // KEY_E for wifi connected

添加后,RTC 唤醒时 KEY_E 被注入到 Android → GlobalKeyReceiver 收到 BUTTON_8 → 后台 WiFi 重连 → 释放 WakeLock → 系统睡回。


涉及文件

文件 改动
drivers/media/rc/rtk-ir/rtk-ir-core.c 解析层 scancode 置 0 + 注入层 skip
drivers/media/rc/rtk-ir/rtk-ir-nec.c NEC 解码器(未改,链路中说明)
drivers/media/rc/keymaps/rc-realtek-dhc.c 添加 KEY_E 映射(可选)
drivers/media/rc/rc-main.c rc_keydown + 查表逻辑(未改,链路中说明)
include/soc/realtek/uapi/rtk_pm_pcpu.h 唤醒事件枚举