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-45 和 board/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 = 3由CONFIG_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 寄存器地址 0x400A3004(chip/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_INVERTED(baseboard/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_DRIVE(chip/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 一般鬼键条件
在三键场景中,鬼键需要满足两个条件:
- 列条件:三个键分布在两列上,其中一列上有两键(col 0)
- 行条件:两列上的键共享行
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 两键 + 另一列同行键 → 鬼键/无响应
不可修复的原因
- 硬件决定:KBS 外设的输出级是固定推挽,PTYPE 在 ALT 模式下无效
- 无法用 GPIO 模式替代:键盘扫描需要 KBS 外设的专用功能(中断唤醒、自动扫描可选)
- 大改驱动不现实:在每次列切换时手动切换 GPIO 模式 → 额外 2µs+ 切换时间,破坏 50µs 扫描时序,且无法从睡眠中唤醒
- 全开漏改造不可行:外部上拉电阻和准双向缓冲器都需要硬件修改
芯片级缓解可能性
- 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 宏定义 |
浙公网安备 33010602011771号