键盘无冲(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 源码
  • 《嵌入式系统设计与实践》
相关推荐
ACP广源盛139246256734 小时前
GSV2016@ACP#2016产品规格参数详解及产品应用场景分享
单片机·嵌入式硬件·计算机外设·音视频
ACP广源盛139246256734 小时前
GSV2501@ACP#2501产品规格参数详解及产品应用场景分享
单片机·嵌入式硬件·计算机外设·音视频
2501_9462429313 小时前
MPV-EASY Player (MPV播放器) v0.41.0.1
数据库·经验分享·云计算·计算机外设·github·电脑·csdn开发云
bkspiderx1 天前
详解Linux下xrandr工具:从基础配置到三显示器扩展桌面
linux·运维·计算机外设·显示器·分屏·xrandr·显示器扩展桌面
驱动开发0072 天前
成功将手机摄像头虚拟成电脑摄像头,实现Windows helo红外相机人脸识别登录
驱动开发·计算机外设·电脑·usb重定向·usb虚拟化
mastercoder--2 天前
速通51单片机————矩阵键盘及其应用
嵌入式硬件·计算机外设·51单片机
TESmart碲视2 天前
如何设置双屏KVM切换器(Win+Mac双屏双系统共享一套键鼠):手把手详细指南
macos·计算机外设·mst·kvm切换器·tesmart·双屏kvm切换器·tesmart碲视
legendary_1633 天前
Type-C 一拖二快充线:实用、便携的移动充电方式
计算机外设·电脑·音视频
爱喝矿泉水的猛男3 天前
鼠标堪比mac触控板(普通鼠标即可)
macos·计算机外设