AIGC标识 Redrix (NPCX9) 键盘鬼键问题分析

Redrix (NPCX9) 键盘鬼键问题分析

基于 Chrome EC 固件 board/redrix/ (NPCX9M3F, Brya baseboard) 分析
代码版本: Chromium EC main branch


1. 键盘矩阵原理

1.1 矩阵拓扑结构

键盘矩阵是一个 M×N 的交叉开关网络,EC 通过逐列扫描的方式检测按键:

                EC (NPCX9M3F)
          ┌─────────────────────────┐
          │   KBS 外设              │
          │                         │
 KSO00 ───┤ Col 0  ←───→ [Ctrl]     │
          │                ↓        │
 KSO01 ───┤ Col 1  ←───→ [`~`]      │
          │                ↓        │
 KSO02 ───┤ Col 2  ←───→ [Refr] ───→ H1 (反相)
          │                ↓        │
 KSO03~12─┤ Col 3~12     [键]       │
          │                ↓        │
          │           KSI0~7 (行)   │
          │            ↑            │
          │       内部上拉 0xFF     │
          └─────────────────────────┘

关键电气特性

  • KSO (列):EC 输出,驱动列电平
  • KSI (行):EC 输入,内部上拉 (NPCX_KBSINPU = 0xFF)
  • 二极管:每个按键串联一个二极管,从 KSO 指向 KSI (阳极→阴极),防止反向电流
  • 按键按下 = 对应 KSO 与 KSI 通过二极管连通

当某列被选中 (LOW) 且该列上的键被按下时,该键所在行线被拉低;未被选中的列处于 HIGH 状态。

1.2 完整键盘矩阵 (Redrix, 13列 × 8行)

以下为 Redrix 的标准 Chrome OS 键盘矩阵布局。每个单元格标注为 键名 (col, row)

Row0 Row1 Row2 Row3 Row4 Row5 Row6 Row7
Col 0 L_Ctrl 🔴Search 🔴R_Ctrl
Col 1 Mute① ESC Tab 🔴~ a z 1 q
Col 2 T1(Bk) T4(Ov) T3(Full) 🔴T2(Rfr) d c 3 e
Col 3 b g t 🔴5 f v 4 r
Col 4 T10(Mic) T7(Br+) T6(Br-) 🔴T5(Snap) s x 2 w
Col 5 T12(Vd-) ] 🔴T13(Vu+) k , 8 i
Col 6 n h y 🔴6 j m 7 u
Col 7 L_Sft R_Sft
Col 8 = ' [ 🔴- ; / 0 p
Col 9 T14① T9(P/P) T8(KBB) | . 9 o
Col 10 R_Alt L_Alt
Col 11 T15① B_Sp 🔴\ Enter Space Down Up
Col 12 Right Left

图例:🔴 = 受鬼键影响(这些键与 col 0 的键共享行,且所在的列与 col 0 非同列)
① = Vivaldi 功能键

col 0 的三键——鬼键根源

Row 功能 KSO 引脚 KSI 引脚
2 L_Ctrl 左 Ctrl KSO00 (GPIO2.1) KSI2 (GPIO2.7)
3 Search 搜索/Super KSO00 (GPIO2.1) KSI3 (GPIO2.6)
4 R_Ctrl 右 Ctrl KSO00 (GPIO2.1) KSI4 (GPIO2.5)

三个键共享同一列 (col 0) 但分布在连续的三行 (row 2/3/4)——这是鬼键问题的矩阵拓扑根源。

1.3 Vivaldi 顶层键行映射

Redrix 的 keyboard.c 通过 Vivaldi 覆盖顶层功能键的位置。来源:common/keyboard_vivaldi.c:30-45board/redrix/keyboard.c:7-36

功能 Row Col 标准键覆盖
T1 Back 0 2 F1
T2 (🔴) Refresh 3 2 F2
T3 Fullscreen 2 2 F3
T4 Overview 1 2 F4
T5 (🔴) Snapshot 3 4 F5
T6 Brightness- 2 4 F6
T7 Brightness+ 1 4 F7
T8 KbdBklTgl 2 9 F8
T9 PlayPause 1 9 F9
T10 MicMute 0 4 F10
T11 VolMute 0 1 F11
T12 VolDown 1 5 F12
T13 (🔴) VolUp 3 5 F13

T2(Refresh) 的 KEYBOARD_ROW_REFRESH = 3CONFIG_KEYBOARD_REFRESH_ROW3 定义(board/redrix/board.h:221)。


2. EC 扫描机制

2.1 初始化和扫描循环

扫描由 keyboard_scan_task()common/keyboard_scan.c:1083)驱动。

初始化阶段 (keyboard_raw_init(), chip/npcx/keyboard_raw.c:13-60):

1. 使能 KBS 外设时钟 (CGC_OFFSET_KBS)
2. NPCX_KBSINPU = 0xFF        (行输入内部上拉, 8行全部使能)
3. CLEAR_BIT(NPCX_KBSCTL, KBSMODE)  (禁用自动扫描, 使用纯软件扫描)
4. NPCX_KBSOUT0 = 0x0000      (所有列初始为 LOW)
   NPCX_KBSOUT1 = 0x0000
5. gpio_config_module(MODULE_KEYBOARD_SCAN, 1)
   ├── gpio_set_flags_by_mask()     → 设置 PTYPE=开漏, PULL_UP
   └── gpio_set_alternate_function() → 设置 DEVALT = KBS ALT 功能
6. 配置 MIWU 中断 (下降沿触发, 行从HIGH→LOW时唤醒)

主扫描循环 (每 keyscan_config.scan_period_us = 3ms):

for each column c = 0 .. (keyboard_cols - 1):

  1. keyboard_raw_drive_column(c)
     → 选中 KSO c = LOW (0)
     → 非选中 KSO = HIGH (1)

  2. udelay(output_settle_us)     // 默认 50µs, 等待电平稳定
  
  3. if c == 2 or c == (2+1):      // COL2 经 H1 反相, 有额外延迟
       udelay(COL2_DELAY_US = 30)

  4. keyboard_raw_read_rows()
     → state[c] = (~NPCX_KBSIN) & 0xFF
     → 读回 8 个 KSI 引脚电平, 取反
     → bit N = 1 表示 row N 有键按下

后处理:
  5. Transitional ghost 合并
  6. 叠加 simulated_key
  7. actual_key_mask 过滤
  8. has_ghosting() 检测
  9. keyboard_raw_drive_column(COLUMN_NONE)  // 所有列 tri-state

2.2 keyboard_raw_drive_column() 的寄存器操作

这是鬼键问题的核心函数chip/npcx/keyboard_raw.c:82-119):

void keyboard_raw_drive_column(int col)
{
    uint32_t col_out = col + CONFIG_KEYBOARD_KSO_BASE;  // 通常 col_out = col

    if (col == KEYBOARD_COLUMN_NONE) {
        mask = ~0;                         // 所有位 = 1
    } else if (col == KEYBOARD_COLUMN_ALL) {
        mask = ~(BIT(keyboard_cols) - 1);  // 前 N 位 = 0
    } else {
        mask = ~BIT(col_out);              // 仅选中的位 = 0
    }

    // 写入 KBS 输出寄存器
    NPCX_KBSOUT0 = (mask & 0xFFFF);        // KSO 0~15
    NPCX_KBSOUT1 = ((mask >> 16) & 0x03);  // KSO 16~17
}

写入效果(以扫描 col 1 为例):

扫描 col 1 时: mask = ~BIT(1) = 0xFFFF_FFFD

NPCX_KBSOUT0 = 0xFFFD
→ KSO00 bit 0 = 1 → KSO00 = HIGH (非选中)
→ KSO01 bit 1 = 0 → KSO01 = LOW  (选中扫描)
→ KSO02~15 bit 2~15 = 1 → HIGH (非选中)

2.3 keyboard_raw_read_rows() 的寄存器操作

chip/npcx/keyboard_raw.c:125-127

int keyboard_raw_read_rows(void)
{
    return (~NPCX_KBSIN) & KB_ROW_MASK;  // KB_ROW_MASK = 0xFF
}

NPCX_KBSIN 寄存器地址 0x400A3004chip/npcx/registers.h:221),直接反映 8 个 KSI 引脚的输入电平:

  • bit N = 0 → KSI_N 引脚为 LOW(有键按下,或被外部拉低)
  • bit N = 1 → KSI_N 引脚为 HIGH(内部上拉生效,无键按下)

函数返回取反值,所以返回值中 bit N = 1 表示 row N 有键按下。

2.4 Col 2 的特殊路径

板级配置 CONFIG_KEYBOARD_COL2_INVERTEDbaseboard/brya/baseboard.h:137)意味着:

EC KSO02 ──→ H1 (Google Security Chip) ──→ 键盘 KSO02 (反相)

H1 芯片对 KSO02 信号进行反相并增加延迟。因此扫描代码中(common/keyboard_scan.c:319-322):

if (c == COL2 || c == (COL2 + 1))
    udelay(COL2_DELAY_US);  // 额外 30µs 等待 H1 延迟

gpio.inc 中对应的引脚配置(board/redrix/gpio.inc:121):

GPIO(EC_KSO_02_INV, PIN(1, 7), GPIO_OUT_LOW)
// KSO02 (物理 pin GPIO17) 未在 KBS ALT 中使能
// 改用独立的 GPIO 通过 H1 驱动

3. 输出驱动模式:开漏 vs 推挽

3.1 开漏 (Open-Drain) — 正确的键盘扫描模式

电气结构

           内部数据 = 1          内部数据 = 0
             (非选中)               (选中扫描)
                │                      │
          ┌─────┘                      │
          │                             │
         Vcc                            │
          │                             │
         [R_pull]                       │
       (弱上拉,10kΩ)                   │
          │                             │
          ├────── KSO 引脚 ─────────────┤
          │                             │
       ┌──┴──┐                      ┌──┴──┐
       │ NMOS │                      │ NMOS │
       │ 关断 │                      │ 导通 │
       └──┬──┘                      └──┬──┘
          │                             │
         GND                           GND

关键行为

  • 输出 1 (非选中):NMOS 关断,引脚高阻态 (Hi-Z),仅通过 R_pull (10kΩ~100kΩ) 弱上拉到 Vcc
  • 输出 0 (选中扫描):NMOS 导通,引脚通过 NMOS (<10Ω) 强下拉到 GND

防鬼键原理:非选中列的 "HIGH" 是弱上拉,驱动能力极弱(μA 级)。当鬼路径形成时:

  • 选中列的 NMOS 强下拉 (mA 级) → 压倒非选中列的弱上拉电流
  • 共享行线的电平被拉到接近 GND → KSI 读到正确电平
  • 鬼键不产生

3.2 推挽 (Push-Pull) — NPCX KBS 的实际行为

电气结构

         内部数据 = 1         内部数据 = 0
           (非选中)              (选中扫描)
              │                     │
        ┌─────┘                     │
        │                            │
       Vcc                           │
        │                            │
     ┌──┴──┐                         │
     │ PMOS│                         │
     │ 导通 │                         │
     └──┬──┘                         │
        ├───── KSO 引脚 ─────────────┤
        │                         ┌──┴──┐
        │                         │ NMOS│
        │                         │ 导通 │
        │                         └──┬──┘
        │                             │
       GND                            │
                                       │
                                      GND

关键行为

  • 输出 1 (非选中):PMOS 导通,引脚通过 PMOS (<50Ω) 强拉到 Vcc
  • 输出 0 (选中扫描):NMOS 导通,引脚通过 NMOS (<50Ω) 强拉到 GND

鬼键成因:非选中列的 "HIGH" 是强推挽,驱动能力极强(几十 mA)。当鬼路径形成时:

  • 选中列的 NMOS 和非选中列的 PMOS 直接打架
  • 共享行线电平取决于哪个驱动更强 → 无法保证读到正确电平
  • KSI 输入是 CMOS 逻辑门,约 Vcc/2 跳变阈值 → 可能读到错误的电平

3.3 NPCX9 为何忽略 PTYPE 寄存器

关键代码路径chip/npcx/keyboard_raw.c:42-48):

void keyboard_raw_init(void)
{
    // ...
    gpio_config_module(MODULE_KEYBOARD_SCAN, 1);
}

gpio_config_module()common/gpio.c:94-98)调用 gpio_config_pins()common/gpio.c:74-80):

gpio_set_flags_by_mask(af->port, (af->mask & pin_mask),
                       enable ? af->flags : GPIO_INPUT);
gpio_set_alternate_function(af->port, (af->mask & pin_mask),
                            enable ? af->func : GPIO_ALT_FUNC_NONE);

第 1 步gpio_set_flags_by_mask()chip/npcx/gpio.c:407

if (flags & GPIO_OPEN_DRAIN)
    NPCX_PTYPE(port) |= mask;   // ← 设置 PTYPE=开漏

NPCX_PTYPE 寄存器chip/npcx/registers.h:339):

#define NPCX_PTYPE(n) REG8(NPCX_GPIO_BASE_ADDR(n) + 0x006)
// 每 bit 控制一个引脚:0=推挽, 1=开漏

第 2 步gpio_alt_sel()chip/npcx/gpio.c:67-80

NPCX_DEVALT(map->alt.group) &= ~alt_mask;
// `NPCX_ALT_INV` 中 inverted=1 → 清除 DEVALT 位 = 使能 KBS ALT 功能

NPCX_DEVALT 寄存器控制引脚的功能复用。当 KSO 引脚切换到 KBS 功能后:

PTYPE 寄存器只在 GPIO 模式下有效。当 DEVALT 将引脚切换到 KBS 外设功能时,KBS 内部的硬件输出级接管引脚控制——这个输出级是固定的推挽结构,不受 PTYPE 影响。chip/npcx/gpio_chip-npcx9.h:282-293

更精确地说,KBS 外设的 KBSOUT 寄存器直接写入的是引脚电平控制寄存器(pin's data register),跳过了 GPIO 模块的 PTYPE 控制逻辑。这是 NPCX 硬件设计上的固定行为,在 NPCX5、NPCX7 和 NPCX9 全系列中都存在。

3.4 软件缓解措施及其局限性

CONFIG_KEYBOARD_KSO_HIGH_DRIVEchip/npcx/keyboard_raw.c:29-33):

/*
 * Select quasi-bidirectional buffers for KSO pins. It reduces the
 * low-to-high transition time. This feature only supports in npcx7.
 */
#ifdef CONFIG_KEYBOARD_KSO_HIGH_DRIVE
    SET_FIELD(NPCX_KBSCTL, NPCX_KBHDRV_FIELD, 0x01);
#endif

NPCX7 引入的准双向缓冲器模式(include/config.h:3219-3223):

"Enable quasi-bidirectional buffers for KSO pins. It has an open-drain output and a low-impedance pull-up. The low-impedance pull-up is active when ec changes the output data buffers from 0 to 1, thereby reducing the low-to-high transition time."

这种模式在开漏基础上增加一个瞬态强上拉(低→高跳变时短暂使能,然后恢复弱上拉)。但代码注释指出只在 npcx7 支持,且本质上仍是开漏输出——不能解决推挽的问题。


4. 鬼键产生机理

4.1 一般鬼键条件

在三键场景中,鬼键需要满足两个条件:

  1. 列条件:三个键分布在两列上,其中一列上有两键(col 0)
  2. 行条件:两列上的键共享行
      Col X (LOW, 扫描)      Col 0 (HIGH, 非选中)
             │                         │
             │                         │
  ┌─ Row N ──┼─────[A键]───────────────┼─────[B键]──┐
  │          │                         │            │
  │     (按下A) ───→ 电流 ──→ (按下B)               │
  │                                                 │
  │    Row N 通过 B 键被 Col 0(HIGH) 上拉           │
  │    同时通过 A 键被 Col X(LOW) 下拉              │
  └─────────────────────────────────────────────────┘

4.2 具体电流路径分析

Ctrl(R2,C0) + Search(R3,C0) + ~(R3,C1) 为例:

扫描 col 1 时(KSO01 = LOW, KSO00 = HIGH 推挽):

       ┌─ KSO00 (col 0) ── 推挽 HIGH ────────────────┐
       │                                             │
       │  ┌──[Ctrl 闭合]──┐                          │
       │  │   R2↔C0       │                          │
       │  └──────┬────────┘                          │
       │         │                                   │
       │         ├── Row 2 ── (无到 col 1 的连接)    │
       │         │                                   │
       │  ┌──[Search 闭合]─┐                         │
       │  │   R3↔C0        │                         │
       │  └──────┬─────────┘                         │
       │         │                                   │
       │         ├── Row 3 ──┬──[`~`闭合]─┐          │
       │         │           │   R3↔C1    │          │
       └─────────┼───────────┘            │          │
                 │                        │          │
              Vcc/2(GHOST!)         ┌─────┴──────────┴── KSO01 (col 1) ── 推挽 LOW
                                    │
                                    └── Row 3 电平 = 推挽打架结果

电流路径

KSO00(HIGH, 强推挽 PMOS)
  → Search 闭合 → Row 3
  → `~` 闭合 → KSO01(LOW, 强推挽 NMOS)

推挽打架

  • PMOS 导通阻抗 ≈ 30Ω,电流 = (3.3V - V_f_diode - V_f_diode) / (30Ω + R_switch + 30Ω) ≈ 几十 mA
  • Row 3 电压取决于两推挽级的相对强度,处于 Vcc/2 附近
  • KSI 输入 CMOS 门限 ≈ 0.7×Vcc (HIGH) ~ 0.3×Vcc (LOW)
  • Row 3 电压不确定 → EC 可能误读

4.3 如果有开漏 (期望行为)

KSO00(HIGH, NMOS 关断, 仅弱上拉)
  → Search 闭合 → Row 3
  → `~` 闭合 → KSO01(LOW, NMOS 导通)

弱上拉电流 ≈ 3.3V / 10kΩ = 0.33mA
NMOS 下拉电流 ≈ 3.3V / 30Ω = 110mA (实际受 NMOS 限流)

→ 110mA ≫ 0.33mA → Row 3 被强拉到接近 GND
→ KSI 读到 Row 3 = LOW → 正确识别 `~` 按下
→ Search 的鬼路径无效 → 无鬼键

关键数值对比

模式 非选中列阻抗 驱动电流 防鬼效果
开漏 (期望) ~10kΩ (弱上拉) ~0.3mA ✅ 选中列轻松压倒
推挽 (实际) ~30Ω (PMOS) ~50mA ❌ 与选中列打架

4.4 防鬼二极管的作用与局限性

键盘矩阵中每个按键串联一个二极管,方向为 KSO(阳极) → 按键触点 → 二极管阴极 → KSI(阴极)

KSO ──┬───[二极管 >| ]───[按键触点]───┬─── KSI
      │                               │
      └────────── (PCB 走线) ─────────┘

二极管防鬼原理

  • 正常情况下:KSO(LOW) → 二极管正向 → 按键 → KSI,电流从 KSO 流向 KSI
  • 鬼路径中:KSI → 按键 → 二极管反向 → KSO(HIGH) → 被二极管阻断

但在本问题中:

  • 鬼路径是 KSO(HIGH)→按键二极管正向→KSI→另一按键正向→KSO(LOW)
  • 所有二极管的极性都是正向 → 无法阻挡鬼电流

这正是一般鬼键和本问题的根本区别——按键在相邻列上,鬼路径不经过反向二极管


5. 软件防鬼键逻辑及其失效分析

5.1 has_ghosting() — 经典检测

common/keyboard_scan.c:588-605

static int has_ghosting(const uint8_t *state)
{
    for (c = 0; c < keyboard_cols; c++) {
        if (!state[c]) continue;
        for (c2 = c + 1; c2 < keyboard_cols; c2++) {
            uint8_t common = state[c] & state[c2];
            // x & (x-1) 在 x 仅有 1 个 bit 时为 0
            if (common & (common - 1))
                return 1;   // ≥2 bits set → ghosting
        }
    }
    return 0;
}

判定条件:任意两列的状态共享 ≥2 个行位 → 判定为鬼键。

本问题场景(Ctrl + Search + ~):

扫描 col 0: KSI 读到 row2(LOW) + row3(LOW) → state[0] = 0x0C (bit2+bit3)
扫描 col 1: 鬼电流导致 row2 和 row3 同时被影响 → state[1] = 0x0C (bit2+bit3)

has_ghosting:
  state[0] & state[1] = 0x0C & 0x0C = 0x0C
  0x0C & (0x0C - 1) = 0x0C & 0x0B = 0x08 ≠ 0
  → has_ghosting 返回 1

在这种情况下,has_ghosting 确实触发。但当 check_keys_changed() 检测到鬼键时(common/keyboard_scan.c:684-687):

if (has_ghosting(new_state))
    return any_pressed;  // 丢弃 new_state, 保留旧 state

效果:当前扫描周期的 new_state 被丢弃 → 下个 3ms 周期重试 → 鬼态持续 → 无限重试 → 按键无响应

5.2 transitional ghost 合并 — 反效果

common/keyboard_scan.c:413-425

/* 3. Detect transitional ghost */
for (c = 0; c < keyboard_cols; c++) {
    for (c2 = 0; c2 < c; c2++) {
        if ((state[c] & state[c2]) && (state[c] != state[c2])) {
            uint8_t merged = state[c] | state[c2];
            state[c] = state[c2] = merged;
        }
    }
}

原本目的:补偿列间扫描的时间差——如果用户在扫描过程中按下键,导致不同列读到不同状态,取并集避免错误。

对本问题的影响:在部分鬼键场景中,如果某列只鬼到一个行位,另一列有真键也有鬼键,合并后两列状态完全相同,导致 has_ghosting 检测到多个 common bits(反而被检测到丢弃)。但在更微妙的场景中(如 col 0 状态传播不均匀),合并逻辑可能扩大鬼键范围。

5.3 软件防护的边界条件

Case 1: has_ghosting 触发(≥2 行受影响)

  • 鬼电流同时影响 row 2 和 row 3 → state 中有两个 bit 置位
  • result: 鬼键被检测到,但扫描结果丢弃,键无响应

Case 2: has_ghosting 未触发(仅 1 行受影响)

  • 理论上 col 0 和 col 1 在共享行上需要有 ≥2 个 true bits
  • 本场景中鬼电流路径通过 row 3 → row 2 也会被影响(通过 Ctrl+Search 的导通路径)
  • 因此 Case 2 在本特定矩阵中不太可能发生

实际用户体验

  • 按下 Ctrl + Search + ~ → 系统无响应(鬼检测循环)
  • 释放任意一个键 → 恢复正常
  • 用户感知为"键按下没反应"

6. 受影响组合完整列表

6.1 [A] Ctrl(R2,C0) + Search(R3,C0) + 其他列上的键

Row 3 上的键(与 Search 共享 row 3)

Col KSO 引脚 确认状态
`~ 1 KSO01 ✅ 确认
F2 (T2 Refresh) 2 KSO02(H1) ✅ 确认
5 3 KSO03 ✅ 确认
F5 (T5 Snapshot) 4 KSO04 ✅ 确认
F13 (T13 VolUp) 5 KSO05 ✅ 确认
6 6 KSO06 ✅ 确认
- 8 KSO08 ✅ 确认
\ 11 KSO11 ✅ 确认
keypad / (若有) 12 KSO12 ❓ 待确认

Row 2 上的键(与 Ctrl 共享 row 2)

Col KSO 引脚 确认状态
Tab 1 KSO01 ✅ 确认
F3 (T3 Fullscreen) 2 KSO02(H1) ❓ 待确认
t 3 KSO03 ✅ 确认
F6 (T6 Brightness-) 4 KSO04 ❓ 待确认
] 5 KSO05 ✅ 确认
y 6 KSO06 ✅ 确认
[ 8 KSO08 ✅ 确认
F8 (T8 KbdBklTgl) 9 KSO09 ❓ 待确认

6.2 [B] Search(R3,C0) + R_Ctrl(R4,C0) + 其他列上的键

Row 4 上的键(与 R_Ctrl 共享 row 4)

Col KSO 引脚 确认状态
a 1 KSO01 ✅ 确认
d 2 KSO02(H1) ✅ 确认
f 3 KSO03 ✅ 确认
s 4 KSO04 ✅ 确认
k 5 KSO05 ✅ 确认
j 6 KSO06 ✅ 确认
; 8 KSO08 ✅ 确认
| 9 KSO09 ❎ 未鬼掉
Enter 11 KSO11 ❎ 未鬼掉

Row 3 上的键(与 Search 共享 row 3)

同 6.1 的 Row 3 表格(需按 Search + R_Ctrl 而不按 Ctrl)。


6.3 [C] Ctrl(R2,C0) + R_Ctrl(R4,C0) + 其他列上的键

  • Row 2 上的键 → 同 6.1 Row 2 表格
  • Row 4 上的键 → 同 6.2 Row 4 表格

6.4 同时按三个 col 0 键

Ctrl(R2,C0) + Search(R3,C0) + R_Ctrl(R4,C0)

此时 col 0 上 三行全部置位,col 0 的状态为 0x1C (bit 2+3+4)。鬼电流通过全部三行传播到其他列。

影响范围

  • Row 2 上任何键 → 鬼键
  • Row 3 上任何键 → 鬼键
  • Row 4 上任何键 → 鬼键
  • 同时按下三个 col 0 键中的任意两个 → 影响对应的那两行

7. 与标准鬼键的区别

维度 标准鬼键 本问题
原因 多按导致二极管路径形成环形电流 非选中列驱动类型错误(推挽代替开漏)
检测条件 两列共享 ≥2 行 → has_ghosting 可以检测 两列只共享 1 行 → 或鬼态被合并逻辑掩盖
防护手段 二极管 + has_ghosting() 二极管无效;has_ghosting() 有限
物理根源 用户操作组合 硬件设计:NPCX KBS 固定推挽
是否可修复 软件可处理 需硬件修改(新芯片/KBS 版本)

8. 结论

根因总结

矩阵拓扑: col 0 上 L_Ctrl(R2) + Search(R3) + R_Ctrl(R4)

硬件缺陷: NPCX9 KBS 外设在 ALT 模式下
          - PTYPE(open-drain) 被旁路
          - 非选中列使用推挽强输出 HIGH
          - 与选中列的 LOW 推挽直接打架

软件失效: has_ghosting() 条件不匹配
          transitional ghost 合并扩大错误
          防鬼二极管极性无法阻断此路径

结果: 按 col 0 两键 + 另一列同行键 → 鬼键/无响应

不可修复的原因

  1. 硬件决定:KBS 外设的输出级是固定推挽,PTYPE 在 ALT 模式下无效
  2. 无法用 GPIO 模式替代:键盘扫描需要 KBS 外设的专用功能(中断唤醒、自动扫描可选)
  3. 大改驱动不现实:在每次列切换时手动切换 GPIO 模式 → 额外 2µs+ 切换时间,破坏 50µs 扫描时序,且无法从睡眠中唤醒
  4. 全开漏改造不可行:外部上拉电阻和准双向缓冲器都需要硬件修改

芯片级缓解可能性

  • NPCX7+ 准双向缓冲器 (CONFIG_KEYBOARD_KSO_HIGH_DRIVE):瞬态强上拉后恢复弱上拉,但仍为开漏本质,不能解决推挽问题
  • 未来 NPCX 芯片:需要 KBS 外设硬件增加 PTYPE 遵从性,或引入 open-drain only 模式
  • 替代方案:使用有独立开漏控制的键盘扫描控制器,或将 KBS 替换为 GPIO 逐列驱动(牺牲功耗和中断唤醒能力)

附录 A: 关键代码引用

文件 内容
board/redrix/build.mk 1-8 Redrix 硬件配置:CHIP=npcx, CHIP_FAMILY=npcx9
board/redrix/gpio.inc 82-85 KSO 引脚 ALT 配置为 GPIO_ODR_HIGH
board/redrix/keyboard.c 7-36 Vivaldi 功能键配置 (13 键)
baseboard/brya/baseboard.h 137 CONFIG_KEYBOARD_COL2_INVERTED 定义
chip/npcx/keyboard_raw.c 13-60 keyboard_raw_init(): KBS 初始化和 GPIO ALT 配置
chip/npcx/keyboard_raw.c 82-119 keyboard_raw_drive_column(): KBSOUT 寄存器操作
chip/npcx/keyboard_raw.c 125-127 keyboard_raw_read_rows(): KBSIN 读取
chip/npcx/gpio.c 67-80 gpio_alt_sel(): DEVALT 设置, ALT 功能切换
chip/npcx/gpio.c 407 gpio_set_flags_by_mask(): PTYPE 设置
chip/npcx/registers.h 221-248 KBSCAN 寄存器定义
chip/npcx/registers-npcx9.h 187 NPCX9 KBHDRV 寄存器字段
chip/npcx/gpio_chip-npcx9.h 273-301 KSO/KSI ALT 引脚映射表 (NPCX_ALT_INV)
common/keyboard_scan.c 238-283 read_matrix(): 逐列扫描 + transitional ghost 合并
common/keyboard_scan.c 319-322 COL2 额外 30µs 延时
common/keyboard_scan.c 588-605 has_ghosting(): 鬼键检测
common/keyboard_scan.c 668-784 check_keys_changed(): 扫描主循环
common/keyboard_8042_sharedlib.c 22-43 标准键盘矩阵扫描码表
common/keyboard_vivaldi.c 30-45 Vivaldi 顶层键行列映射
common/gpio.c 74-80 gpio_config_pins(): ALT 功能使能 + 标志设置
include/keyboard_config.h 77-81 REFRESH_ROW3 宏定义

posted on 2026-08-14 13:23  acd407  阅读(15)  评论(0)    收藏  举报

导航