键盘无冲(NKRO)的嵌入式实现原理——从鬼键问题到全键无冲

前言

最近在做一个机械键盘的项目,客户要求必须支持全键无冲。说实话,之前只知道无冲是个卖点,真正动手做才发现这里面水挺深的------涉及到矩阵扫描原理、硬件电路设计、USB协议限制等一系列问题。今天把整个研究过程整理出来,希望能帮到同样在做键盘或者矩阵按键项目的朋友。

什么是键盘冲突

先搞清楚一个概念:键盘冲突到底是什么?

简单说就是:同时按下多个键时,键盘无法正确识别所有按键,甚至会误报没按下的键

这个问题在游戏玩家群体里特别敏感。比如玩FPS游戏,你同时按住W(前进)+ Shift(加速)+ 空格(跳跃),结果键盘告诉电脑你只按了W和Shift,跳跃没触发,角色就跳不起来------这就是键盘冲突造成的。

常见的描述方式:

  • 2KRO:最多同时识别2个按键
  • 6KRO:最多同时识别6个按键(USB HID标准)
  • NKRO:N-Key Rollover,全键无冲,理论上可以同时按下所有键都能正确识别

键盘矩阵扫描原理

要理解冲突问题,先得搞懂键盘是怎么检测按键的。

为什么用矩阵结构

一个标准104键键盘,如果每个按键单独接一根线到MCU,需要104个IO口------这显然不现实。所以键盘普遍采用矩阵扫描结构。

矩阵的核心思想是把按键排列成行和列的交叉点,比如8行×16列的矩阵可以容纳128个按键,但只需要8+16=24个IO口。

复制代码
        COL0   COL1   COL2   COL3  ... COL15
         |      |      |      |         |
ROW0 ----+------+------+------+----...--+
         |      |      |      |         |
ROW1 ----+------+------+------+----...--+
         |      |      |      |         |
ROW2 ----+------+------+------+----...--+
         .      .      .      .         .
         .      .      .      .         .
ROW7 ----+------+------+------+----...--+

每个交叉点放一个按键开关

扫描过程

矩阵扫描的基本流程:

  1. 所有列线配置为输入,开启上拉电阻(默认高电平)
  2. 逐行输出低电平进行扫描
  3. 读取所有列的状态,低电平表示该位置按键被按下
  4. 切换到下一行,重复扫描
c 复制代码
// 简化的矩阵扫描代码
#define ROW_NUM 8
#define COL_NUM 16

// 行引脚(输出)
const uint8_t row_pins[ROW_NUM] = {PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7};
// 列引脚(输入,上拉)
const uint8_t col_pins[COL_NUM] = {PB0, PB1, PB2, PB3, PB4, PB5, PB6, PB7,
                                   PC0, PC1, PC2, PC3, PC4, PC5, PC6, PC7};

// 按键状态矩阵
uint8_t key_matrix[ROW_NUM][COL_NUM];

void matrix_scan(void)
{
    for (int row = 0; row < ROW_NUM; row++) {
        // 1. 当前行输出低电平
        gpio_write(row_pins[row], 0);
        
        // 2. 短暂延时,等待电平稳定
        delay_us(5);
        
        // 3. 读取所有列
        for (int col = 0; col < COL_NUM; col++) {
            // 低电平表示按下
            key_matrix[row][col] = (gpio_read(col_pins[col]) == 0) ? 1 : 0;
        }
        
        // 4. 恢复当前行为高电平
        gpio_write(row_pins[row], 1);
    }
}

看起来很简单对吧?问题来了。

鬼键(Ghost Key)问题

问题复现

假设有个简化的3×3矩阵:

复制代码
        COL0   COL1   COL2
         |      |      |
ROW0 ---[A]----[B]----[C]---
         |      |      |
ROW1 ---[D]----[E]----[F]---
         |      |      |
ROW2 ---[G]----[H]----[I]---

现在同时按下A、B、D三个键,分析一下扫描过程会发生什么:

扫描ROW0时

  • ROW0输出低电平
  • A被按下 → COL0读到低电平 ✓
  • B被按下 → COL1读到低电平 ✓
  • 到这里都正常

扫描ROW1时

  • ROW1输出低电平
  • D被按下 → COL0读到低电平 ✓

但问题来了,看这条电流路径:

复制代码
ROW1(低) → D(按下) → COL0 → A(按下) → ROW0(此时是高阻态)
                                ↓
                    然后通过B(按下) → COL1

由于A、B、D都被按下,形成了一条电流泄漏路径。扫描ROW1时,电流可以从ROW1经过D到COL0,再经过A到ROW0,然后经过B到COL1。这导致COL1也读到了低电平!

MCU会误认为E键也被按下了------但实际上E根本没按。这个凭空出现的E键就叫鬼键(Ghost Key)

鬼键产生的条件

分析上面的例子,鬼键产生需要满足:

  1. 至少同时按下3个键
  2. 这3个键构成一个"L"形或矩形的三个角
  3. 此时第四个角就会产生鬼键

用矩阵坐标来说:如果(r1,c1)、(r1,c2)、(r2,c1)三个位置都被按下,那么(r2,c2)就会产生鬼键。

复制代码
        c1     c2
         |      |
    r1 --*------*--   * = 按下的键
         |      |
    r2 --*-----[?]--  [?] = 鬼键位置

这是矩阵结构的固有缺陷,纯软件层面无法解决。

硬件解决方案:二极管隔离

原理分析

既然问题出在电流可以反向流动形成回路,那解决方案就很直接:加二极管,让电流只能单向流动

改造后的电路:

复制代码
        COL0    COL1    COL2
         |       |       |
ROW0 --[A]>----[B]>----[C]>---
         |       |       |
ROW1 --[D]>----[E]>----[F]>---
         |       |       |
ROW2 --[G]>----[H]>----[I]>---

[X]> 表示按键串联一个二极管,阳极接行,阴极接列

每个按键位置的详细电路:

复制代码
    ROW ────┬──── 按键开关 ────┬──── 二极管(阳极) ──>|── COL
            │                  │
           行线               二极管阴极接列线

或者更常见的画法:

复制代码
ROW ──────○/ ○──────|>|────── COL
         按键开关    二极管

为什么二极管能解决问题

还是刚才的例子,同时按下A、B、D:

扫描ROW1时

  • ROW1输出低电平
  • D的二极管:阳极(ROW1)为低,阴极(COL0)通过上拉为高 → 二极管导通 → COL0被拉低 ✓

那之前的泄漏路径呢?

复制代码
ROW1(低) → D → COL0 → [试图经过A反向流向ROW0]
                       ↓
                  A的二极管反向截止!电流过不去

二极管阻止了电流从COL0反向流回ROW0,泄漏路径被切断,鬼键问题解决。

二极管选型

实际设计中,二极管的选型需要注意:

  1. 正向压降要小:压降太大会影响低电平的判断阈值。一般选肖特基二极管(压降约0.3V)或普通开关二极管1N4148(压降约0.7V)

  2. 封装要小:104键要焊104个二极管,封装太大PCB放不下。常用SOD-323、SOD-123封装

  3. 响应速度:扫描频率通常1-10kHz,普通二极管都能满足

常用型号:

  • 1N4148W(SOD-123封装,便宜)
  • BAV70(SOT-23双二极管,省空间)
  • BAT54S(肖特基,低压降)

硬件设计实例

这是一个4×4矩阵的原理图片段:

复制代码
                VCC
                 |
                [R] 10K上拉电阻
                 |
COL0 ───────────┴─────────────────────────────────
                |         |         |         |
               1N4148    1N4148    1N4148    1N4148
                |>        |>        |>        |>
                |         |         |         |
ROW0 ─────────[SW]──────[SW]──────[SW]──────[SW]───
                |         |         |         |
                |         |         |         |
ROW1 ─────────[SW]──────[SW]──────[SW]──────[SW]───
                |         |         |         |
               ...

PCB布局时,二极管通常放在按键旁边,阴极统一朝向列线方向。

软件层面的优化

硬件加了二极管后,矩阵本身不会产生鬼键了。但要实现完整的NKRO,软件层面还有工作要做。

去抖动处理

机械开关在按下/松开瞬间会有抖动,表现为电平快速跳变:

复制代码
理想波形:     ______|‾‾‾‾‾‾‾‾‾‾
实际波形:     ______|||‾‾‾‾‾‾‾‾
                   ↑
                 抖动区域(约5-20ms)

不处理抖动会导致一次按键被识别成多次。常用的去抖方法:

c 复制代码
#define DEBOUNCE_COUNT 5  // 去抖计数阈值

typedef struct {
    uint8_t current;      // 当前稳定状态
    uint8_t raw;          // 原始读取值
    uint8_t counter;      // 计数器
} key_state_t;

key_state_t key_states[ROW_NUM][COL_NUM];

void debounce_update(int row, int col, uint8_t raw_value)
{
    key_state_t *state = &key_states[row][col];
    
    if (raw_value != state->current) {
        // 状态不一致,累加计数
        state->counter++;
        if (state->counter >= DEBOUNCE_COUNT) {
            // 连续多次读到新状态,确认切换
            state->current = raw_value;
            state->counter = 0;
            
            // 触发按键事件
            if (raw_value) {
                key_pressed(row, col);
            } else {
                key_released(row, col);
            }
        }
    } else {
        // 状态一致,清零计数
        state->counter = 0;
    }
    
    state->raw = raw_value;
}

void matrix_scan_with_debounce(void)
{
    for (int row = 0; row < ROW_NUM; row++) {
        gpio_write(row_pins[row], 0);
        delay_us(5);
        
        for (int col = 0; col < COL_NUM; col++) {
            uint8_t raw = (gpio_read(col_pins[col]) == 0) ? 1 : 0;
            debounce_update(row, col, raw);
        }
        
        gpio_write(row_pins[row], 1);
    }
}

扫描频率设计

扫描太慢会有延迟感,太快浪费CPU。一般原则:

  • 扫描周期 < 去抖时间/去抖计数,确保去抖算法正常工作
  • 总延迟(扫描周期×去抖计数)控制在10-20ms内,人感觉不到

假设去抖计数为5,希望总延迟在10ms:

  • 扫描周期 = 10ms / 5 = 2ms
  • 扫描频率 = 500Hz

实际项目中1000Hz扫描频率比较常见,配合5次去抖,响应延迟5ms。

c 复制代码
// 定时器中断,1ms触发一次
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        matrix_scan_with_debounce();
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

void scan_timer_init(void)
{
    // 配置TIM2,1kHz中断
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    TIM_TimeBaseStructure.TIM_Period = 1000 - 1;     // 1ms
    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;    // 72MHz/72 = 1MHz
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    NVIC_EnableIRQ(TIM2_IRQn);
    TIM_Cmd(TIM2, ENABLE);
}

USB HID协议与NKRO

硬件和扫描都搞定了,还有最后一道坎:USB协议限制

标准Boot Protocol的6KRO限制

USB HID键盘有个Boot Protocol,是为了兼容BIOS等不支持复杂HID报告的场景。它的报告格式是固定的8字节:

复制代码
Byte 0: 修饰键 (Ctrl, Shift, Alt, GUI)
Byte 1: 保留 (0x00)
Byte 2-7: 最多6个普通按键的键码

这就是为什么很多键盘标称6KRO------不是硬件不行,是USB报告格式只能装6个键码。

标准Boot Protocol报告描述符:

c 复制代码
const uint8_t hid_report_desc_boot[] = {
    0x05, 0x01,        // Usage Page (Generic Desktop)
    0x09, 0x06,        // Usage (Keyboard)
    0xA1, 0x01,        // Collection (Application)
    
    // 修饰键 (8个bit)
    0x05, 0x07,        //   Usage Page (Key Codes)
    0x19, 0xE0,        //   Usage Minimum (224) - Left Ctrl
    0x29, 0xE7,        //   Usage Maximum (231) - Right GUI
    0x15, 0x00,        //   Logical Minimum (0)
    0x25, 0x01,        //   Logical Maximum (1)
    0x75, 0x01,        //   Report Size (1)
    0x95, 0x08,        //   Report Count (8)
    0x81, 0x02,        //   Input (Data, Variable, Absolute)
    
    // 保留字节
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x08,        //   Report Size (8)
    0x81, 0x01,        //   Input (Constant)
    
    // 6个按键码
    0x95, 0x06,        //   Report Count (6)
    0x75, 0x08,        //   Report Size (8)
    0x15, 0x00,        //   Logical Minimum (0)
    0x25, 0x65,        //   Logical Maximum (101)
    0x05, 0x07,        //   Usage Page (Key Codes)
    0x19, 0x00,        //   Usage Minimum (0)
    0x29, 0x65,        //   Usage Maximum (101)
    0x81, 0x00,        //   Input (Data, Array)
    
    0xC0               // End Collection
};

NKRO报告格式

要实现全键无冲,需要换一种报告格式。思路是:用bitmap代替键码数组,每个键占1bit。

c 复制代码
// NKRO报告结构
// Byte 0: 修饰键 (8 bits)
// Byte 1-15: 120个按键的bitmap (实际用不了这么多)

const uint8_t hid_report_desc_nkro[] = {
    0x05, 0x01,        // Usage Page (Generic Desktop)
    0x09, 0x06,        // Usage (Keyboard)
    0xA1, 0x01,        // Collection (Application)
    
    // Report ID (区分多个报告)
    0x85, 0x01,        //   Report ID (1)
    
    // 修饰键 (8 bits)
    0x05, 0x07,        //   Usage Page (Key Codes)
    0x19, 0xE0,        //   Usage Minimum (224)
    0x29, 0xE7,        //   Usage Maximum (231)
    0x15, 0x00,        //   Logical Minimum (0)
    0x25, 0x01,        //   Logical Maximum (1)
    0x75, 0x01,        //   Report Size (1)
    0x95, 0x08,        //   Report Count (8)
    0x81, 0x02,        //   Input (Data, Variable, Absolute)
    
    // 普通按键 bitmap (120 bits = 15 bytes, 覆盖键码0-119)
    0x05, 0x07,        //   Usage Page (Key Codes)
    0x19, 0x00,        //   Usage Minimum (0)
    0x29, 0x77,        //   Usage Maximum (119)
    0x15, 0x00,        //   Logical Minimum (0)
    0x25, 0x01,        //   Logical Maximum (1)
    0x75, 0x01,        //   Report Size (1)
    0x95, 0x78,        //   Report Count (120)
    0x81, 0x02,        //   Input (Data, Variable, Absolute)
    
    0xC0               // End Collection
};

这样一来,理论上可以同时报告120个按键------远超实际键盘的按键数。

USB报告发送

c 复制代码
// NKRO报告缓冲区
typedef struct {
    uint8_t report_id;           // Report ID = 0x01
    uint8_t modifiers;           // 修饰键
    uint8_t keys[15];            // 按键bitmap
} nkro_report_t;

nkro_report_t nkro_report = {.report_id = 0x01};

// 键码到bitmap位置的转换
void set_key_in_report(uint8_t keycode, uint8_t pressed)
{
    if (keycode >= 0xE0 && keycode <= 0xE7) {
        // 修饰键
        uint8_t bit = keycode - 0xE0;
        if (pressed) {
            nkro_report.modifiers |= (1 << bit);
        } else {
            nkro_report.modifiers &= ~(1 << bit);
        }
    } else if (keycode < 120) {
        // 普通按键
        uint8_t byte_index = keycode / 8;
        uint8_t bit_index = keycode % 8;
        if (pressed) {
            nkro_report.keys[byte_index] |= (1 << bit_index);
        } else {
            nkro_report.keys[byte_index] &= ~(1 << bit_index);
        }
    }
}

// 按键事件处理
void key_pressed(int row, int col)
{
    uint8_t keycode = keymap[row][col];  // 从键位映射表获取键码
    set_key_in_report(keycode, 1);
    usb_hid_send_report(&nkro_report, sizeof(nkro_report));
}

void key_released(int row, int col)
{
    uint8_t keycode = keymap[row][col];
    set_key_in_report(keycode, 0);
    usb_hid_send_report(&nkro_report, sizeof(nkro_report));
}

6KRO与NKRO的兼容

实际产品通常同时支持两种模式,因为有些老系统不认NKRO报告。可以通过组合键切换,或者自动检测。

c 复制代码
typedef enum {
    MODE_6KRO,
    MODE_NKRO
} keyboard_mode_t;

keyboard_mode_t current_mode = MODE_6KRO;  // 默认6KRO保证兼容

void toggle_nkro_mode(void)
{
    if (current_mode == MODE_6KRO) {
        current_mode = MODE_NKRO;
        // 切换到NKRO报告描述符
        usb_hid_set_report_desc(hid_report_desc_nkro, sizeof(hid_report_desc_nkro));
    } else {
        current_mode = MODE_6KRO;
        usb_hid_set_report_desc(hid_report_desc_boot, sizeof(hid_report_desc_boot));
    }
    
    // 重新枚举USB设备
    usb_reconnect();
}

完整实现示例

把上面的模块整合起来,给一个相对完整的实现框架:

c 复制代码
/**
 * keyboard_matrix.h - 键盘矩阵扫描与NKRO实现
 */

#ifndef __KEYBOARD_MATRIX_H
#define __KEYBOARD_MATRIX_H

#include <stdint.h>

#define ROW_NUM     8
#define COL_NUM     16
#define KEY_COUNT   (ROW_NUM * COL_NUM)

#define DEBOUNCE_THRESHOLD  5
#define SCAN_INTERVAL_MS    1

// 按键状态
typedef struct {
    uint8_t pressed;        // 当前稳定状态
    uint8_t debounce_cnt;   // 去抖计数
} key_state_t;

// 初始化
void keyboard_init(void);

// 矩阵扫描(由定时器中断调用)
void keyboard_scan(void);

// 获取按键状态
uint8_t keyboard_is_pressed(uint8_t row, uint8_t col);

// 切换NKRO模式
void keyboard_toggle_nkro(void);

#endif
c 复制代码
/**
 * keyboard_matrix.c - 实现
 */

#include "keyboard_matrix.h"
#include "gpio.h"
#include "usb_hid.h"
#include "keymap.h"

// 引脚定义
static const uint8_t row_pins[ROW_NUM] = {PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7};
static const uint8_t col_pins[COL_NUM] = {PB0, PB1, PB2, PB3, PB4, PB5, PB6, PB7,
                                          PC0, PC1, PC2, PC3, PC4, PC5, PC6, PC7};

// 按键状态矩阵
static key_state_t key_states[ROW_NUM][COL_NUM];

// 上次的报告,用于检测变化
static uint8_t last_report[17];

// NKRO报告
static struct {
    uint8_t report_id;
    uint8_t modifiers;
    uint8_t keys[15];
} nkro_report = {.report_id = 0x01};

// 6KRO报告
static struct {
    uint8_t modifiers;
    uint8_t reserved;
    uint8_t keys[6];
} boot_report;

static uint8_t nkro_enabled = 0;

// ============ 硬件初始化 ============

static void gpio_init_matrix(void)
{
    // 行:推挽输出,默认高电平
    for (int i = 0; i < ROW_NUM; i++) {
        gpio_config(row_pins[i], GPIO_MODE_OUTPUT_PP);
        gpio_write(row_pins[i], 1);
    }
    
    // 列:输入,内部上拉
    for (int i = 0; i < COL_NUM; i++) {
        gpio_config(col_pins[i], GPIO_MODE_INPUT_PULLUP);
    }
}

void keyboard_init(void)
{
    gpio_init_matrix();
    
    // 清空状态
    for (int r = 0; r < ROW_NUM; r++) {
        for (int c = 0; c < COL_NUM; c++) {
            key_states[r][c].pressed = 0;
            key_states[r][c].debounce_cnt = 0;
        }
    }
}

// ============ 矩阵扫描 ============

static void update_key_state(uint8_t row, uint8_t col, uint8_t raw)
{
    key_state_t *state = &key_states[row][col];
    
    if (raw != state->pressed) {
        state->debounce_cnt++;
        if (state->debounce_cnt >= DEBOUNCE_THRESHOLD) {
            state->pressed = raw;
            state->debounce_cnt = 0;
            
            // 更新报告
            uint8_t keycode = keymap_get(row, col);
            update_report(keycode, raw);
        }
    } else {
        state->debounce_cnt = 0;
    }
}

void keyboard_scan(void)
{
    for (int row = 0; row < ROW_NUM; row++) {
        // 拉低当前行
        gpio_write(row_pins[row], 0);
        
        // 等待电平稳定(关键!太短会读错)
        delay_us(5);
        
        // 读取所有列
        for (int col = 0; col < COL_NUM; col++) {
            uint8_t raw = (gpio_read(col_pins[col]) == 0) ? 1 : 0;
            update_key_state(row, col, raw);
        }
        
        // 恢复当前行
        gpio_write(row_pins[row], 1);
    }
    
    // 发送报告(如果有变化)
    send_report_if_changed();
}

// ============ 报告生成与发送 ============

static void update_report(uint8_t keycode, uint8_t pressed)
{
    // 修饰键处理
    if (keycode >= 0xE0 && keycode <= 0xE7) {
        uint8_t bit = keycode - 0xE0;
        if (pressed) {
            nkro_report.modifiers |= (1 << bit);
            boot_report.modifiers |= (1 << bit);
        } else {
            nkro_report.modifiers &= ~(1 << bit);
            boot_report.modifiers &= ~(1 << bit);
        }
        return;
    }
    
    // NKRO模式:bitmap
    if (keycode < 120) {
        uint8_t byte_idx = keycode / 8;
        uint8_t bit_idx = keycode % 8;
        if (pressed) {
            nkro_report.keys[byte_idx] |= (1 << bit_idx);
        } else {
            nkro_report.keys[byte_idx] &= ~(1 << bit_idx);
        }
    }
    
    // 6KRO模式:键码数组
    if (pressed) {
        // 找空位放入
        for (int i = 0; i < 6; i++) {
            if (boot_report.keys[i] == 0) {
                boot_report.keys[i] = keycode;
                break;
            }
        }
    } else {
        // 找到并移除
        for (int i = 0; i < 6; i++) {
            if (boot_report.keys[i] == keycode) {
                boot_report.keys[i] = 0;
                break;
            }
        }
    }
}

static void send_report_if_changed(void)
{
    uint8_t *report;
    uint8_t size;
    
    if (nkro_enabled) {
        report = (uint8_t *)&nkro_report;
        size = sizeof(nkro_report);
    } else {
        report = (uint8_t *)&boot_report;
        size = sizeof(boot_report);
    }
    
    // 对比是否有变化
    if (memcmp(last_report, report, size) != 0) {
        usb_hid_send(report, size);
        memcpy(last_report, report, size);
    }
}

// ============ 模式切换 ============

void keyboard_toggle_nkro(void)
{
    nkro_enabled = !nkro_enabled;
    
    // 清空报告
    memset(&nkro_report.keys, 0, sizeof(nkro_report.keys));
    memset(&boot_report.keys, 0, sizeof(boot_report.keys));
    nkro_report.modifiers = 0;
    boot_report.modifiers = 0;
    
    // 实际产品可能需要重新枚举USB
    // usb_reconnect();
}

uint8_t keyboard_is_pressed(uint8_t row, uint8_t col)
{
    if (row >= ROW_NUM || col >= COL_NUM) return 0;
    return key_states[row][col].pressed;
}

调试技巧

做键盘项目时踩过的坑,分享几个调试经验:

1. 扫描延时不能省

行切换后必须加延时再读列,否则会读到上一行的残留电平。5-10us通常够用,如果矩阵大或者走线长,可能需要更长。

c 复制代码
// 错误示例
gpio_write(row_pins[row], 0);
uint8_t val = gpio_read(col_pins[col]);  // 可能读错!

// 正确示例
gpio_write(row_pins[row], 0);
delay_us(5);  // 等电平稳定
uint8_t val = gpio_read(col_pins[col]);

2. 二极管方向别装反

装反了会导致整行或整列不工作。调试时可以用万用表二极管档测一下通断方向。

3. NKRO在BIOS中可能不工作

很多BIOS只支持Boot Protocol,进BIOS设置时发现键盘失灵,多半是这个原因。所以默认模式最好是6KRO。

4. 按键抖动比想象的严重

机械轴的抖动有时能持续10-20ms,去抖参数设太小会有连击问题。用示波器实测一下自己用的轴体最保险。

总结

实现一个全键无冲的键盘,需要软硬件配合:

层面 问题 解决方案
硬件矩阵 鬼键 每个按键串联二极管
扫描算法 抖动 软件去抖(计数器法)
USB协议 6键限制 NKRO报告格式(bitmap)

这套方案在我的项目上已经稳定跑了几个月,应付日常使用和游戏都没问题。代码可以直接拿去改,有问题评论区见。


参考资料:

  • USB HID Usage Tables (usb.org)
  • QMK Firmware 源码
  • 《嵌入式系统设计与实践》
相关推荐
PHOSKEY1 小时前
光子精密QM系列闪测仪在鼠标电路板部件质量控制中的核心应用
计算机外设
墩墩冰2 小时前
计算机图形学 分析选择缓冲区中的数字
计算机外设
UI设计兰亭妙微7 小时前
中车株州所显示器界面设计
计算机外设·界面设计
墩墩冰8 小时前
计算机图形学 多视区的显示
计算机外设
墩墩冰8 小时前
计算机图形学 GLU库中的二次曲面函数
计算机外设
墩墩冰9 小时前
计算机图形学 利用鼠标实现橡皮筋技术
计算机外设
企鹅侠客2 天前
鼠标键盘按键统计工具
计算机外设·键盘·鼠标
华一精品Adreamer3 天前
便携式显示器供应链与成本结构:挑战与机遇
计算机外设
开开心心就好3 天前
图片校正漂白工具永久免费,矫正实时预览
网络·人工智能·windows·计算机视觉·计算机外设·电脑·excel
开开心心就好3 天前
免费批量抠图软件大模型,复杂倒影精准去除
网络·windows·pdf·计算机外设·电脑·硬件架构·材料工程