⌨️ 蓝牙键盘

基于 HID over GATT Profile 的无线键盘,支持多设备切换和按键无冲。

📐 键盘系统架构

flowchart TB subgraph Keyboard["蓝牙键盘"] MATRIX[按键矩阵] --> SCAN[扫描电路] SCAN --> MCU[BLE MCU] MCU --> ANT[天线] MCU --> LED[状态 LED] end subgraph Host["主机设备"] ANT2[天线] --> HOST[Host OS] HOST --> HID[HID 驱动] HID --> OS[操作系统] end ANT -.->|BLE 连接 | ANT2 style Keyboard fill:#e3f2fd,stroke:#007bff style Host fill:#d4edda,stroke:#28a745

📋 HID 键盘报告格式

// HID 键盘输入报告 (标准 8 键无冲)
// Report ID: 0x01
// 总长度:8 bytes

typedef struct {
    uint8_t  report_id;      // Report ID (0x01)
    uint8_t  modifier;       // 修饰键 (Ctrl/Shift/Alt 等)
    uint8_t  reserved;       // 保留
    uint8_t  keycode[6];     // 按键码 (最多 6 键同时按下)
} keyboard_input_report_t;

// 修饰键位定义
#define KB_MOD_LCTRL    0x01
#define KB_MOD_LSHIFT   0x02
#define KB_MOD_LALT     0x04
#define KB_MOD_LGUI     0x08  // Windows/Command
#define KB_MOD_RCTRL    0x10
#define KB_MOD_RSHIFT   0x20
#define KB_MOD_RALT     0x40
#define KB_MOD_RGUI     0x80

// 常用按键码 (Usage ID)
#define KB_KEY_A        0x04
#define KB_KEY_B        0x05
#define KB_KEY_C        0x06
#define KB_KEY_ENTER    0x28
#define KB_KEY_ESCAPE   0x29
#define KB_KEY_BACKSPACE 0x2A
#define KB_KEY_TAB      0x2B
#define KB_KEY_SPACE    0x2C
#define KB_KEY_F1       0x3A
#define KB_KEY_F12      0x45

// 示例:按下 Ctrl+A (全选)
uint8_t keyboard_report[] = {
    0x01,                   // Report ID
    KB_MOD_LCTRL,           // Modifier: Left Ctrl
    0x00,                   // Reserved
    KB_KEY_A, 0x00, 0x00,   // Keycode: A
    0x00, 0x00
};

// 示例:按下 Shift+1 (感叹号)
uint8_t keyboard_report_shift1[] = {
    0x01,
    KB_MOD_LSHIFT,          // Modifier: Left Shift
    0x00,
    0x1E, 0x00, 0x00,       // Keycode: 1
    0x00, 0x00
};

📊 HID 报告描述符

// 键盘 HID 报告描述符
const uint8_t keyboard_report_descriptor[] = {
    0x05, 0x01,        // Usage Page (Desktop)
    0x09, 0x06,        // Usage (Keyboard)
    0xA1, 0x01,        // Collection (Application)
    
    0x85, 0x01,        //   Report ID (1)
    
    // 修饰键 (8 个)
    0x05, 0x07,        //   Usage Page (Keyboard)
    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, Var, Abs)
    
    // 保留字节
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x08,        //   Report Size (8)
    0x81, 0x01,        //   Input (Cnst)
    
    // LED 输出 (5 个:NumLock/CapsLock/ScrollLock/Compose/Kana)
    0x05, 0x08,        //   Usage Page (LED)
    0x19, 0x01,        //   Usage Minimum (1)
    0x29, 0x05,        //   Usage Maximum (5)
    0x95, 0x05,        //   Report Count (5)
    0x75, 0x01,        //   Report Size (1)
    0x91, 0x02,        //   Output (Data, Var, Abs)
    
    // LED 填充
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x03,        //   Report Size (3)
    0x91, 0x01,        //   Output (Cnst)
    
    // 按键 (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 (Keyboard)
    0x19, 0x00,        //   Usage Minimum (0)
    0x29, 0x65,        //   Usage Maximum (101)
    0x81, 0x00,        //   Input (Data, Arr, Abs)
    
    0xC0               // End Collection
};

// LED 定义
#define LED_NUM_LOCK    0x01
#define LED_CAPS_LOCK   0x02
#define LED_SCROLL_LOCK 0x04
#define LED_COMPOSE     0x08
#define LED_KANA        0x10

📈 完整工作流程

sequenceDiagram participant KB as 蓝牙键盘 participant Host as 主机 Note over KB,Host: 1. 配对模式 KB->>KB: 长按配对键 KB->>Host: ADV_IND (键盘) Note over KB,Host: 2. 连接配对 Host->>KB: CONNECT_IND Host->>KB: 配对请求 KB-->>Host: 确认配对 Note over KB,Host: 3. 服务发现 Host->>KB: 发现 HID 服务 Host->>KB: 读取报告描述符 Note over KB,Host: 4. 启用通知 Host->>KB: 写入 CCCD Note over KB,Host: 5. 按键输入 loop 按键事件 KB->>Host: 按下通知 KB->>Host: 释放通知 (全 0) end Note over KB,Host: 6. LED 控制 Host->>KB: 输出报告 (CapsLock) KB->>KB: 点亮 LED Note over KB,Host: 7. 低功耗 KB->>KB: 无操作 10s KB->>Host: 进入休眠

💻 固件代码示例

#include "ble_hids.h"

// HID 服务句柄
BLE_HIDS_DEF(m_hids, 1);

// 键盘报告缓冲区
static uint8_t m_kb_report[8];
static uint8_t m_last_keys[6] = {0};

// 初始化键盘 HID 服务
void keyboard_hids_init(void) {
    ble_hids_init_t hids_init = {0};
    
    // 配置报告映射
    hids_init.rep_map.p_rep_map = (uint8_t *)keyboard_report_descriptor;
    hids_init.rep_map.rep_map_len = sizeof(keyboard_report_descriptor);
    
    // 配置输入报告
    ble_hids_inp_rep_t *p_inp_rep = &hids_init.inp_rep[0];
    p_inp_rep->max_len = 8;
    p_inp_rep->rep_ref.report_id = 1;
    p_inp_rep->rep_ref.report_type = BLE_HIDS_INPUT_REP;
    p_inp_rep->evt_handler = kb_input_evt_handler;
    
    // 配置输出报告 (LED 控制)
    ble_hids_outp_rep_t *p_outp_rep = &hids_init.outp_rep[0];
    p_outp_rep->max_len = 1;
    p_outp_rep->rep_ref.report_id = 1;
    p_outp_rep->rep_ref.report_type = BLE_HIDS_OUTPUT_REP;
    p_outp_rep->evt_handler = kb_output_evt_handler;
    p_outp_rep->security_mode.ccm.cps = 1;
    
    hids_init.inp_rep_count = 1;
    hids_init.outp_rep_count = 1;
    
    // HID 信息
    hids_init.hid_information.bcd_hid = 0x0111;
    hids_init.hid_information.b_country_code = 0;
    hids_init.hid_information.flags = 0x01;  // 远程唤醒
    
    ble_hids_init(&m_hids, &hids_init);
}

// 发送按键报告
void keyboard_send_keys(uint8_t modifier, uint8_t *keys, uint8_t count) {
    m_kb_report[0] = 0x01;      // Report ID
    m_kb_report[1] = modifier;  // Modifier
    m_kb_report[2] = 0x00;      // Reserved
    
    // 填充按键 (最多 6 个)
    for (int i = 0; i < 6; i++) {
        m_kb_report[3 + i] = (i < count) ? keys[i] : 0x00;
    }
    
    // 发送输入报告
    ble_hids_inp_rep_send(&m_hids, 0, 8, m_kb_report, m_conn_handle);
}

// 按键按下
void key_press(uint8_t keycode) {
    uint8_t keys[6] = {0};
    uint8_t count = 0;
    
    // 查找空位
    for (int i = 0; i < 6; i++) {
        if (m_last_keys[i] == keycode) {
            return;  // 已按下,忽略
        }
        if (m_last_keys[i] == 0x00) {
            m_last_keys[i] = keycode;
            count = i + 1;
            break;
        }
    }
    
    // 构建报告
    memcpy(keys, m_last_keys, 6);
    keyboard_send_keys(0, keys, count);
}

// 按键释放
void key_release(uint8_t keycode) {
    // 从数组中移除
    for (int i = 0; i < 6; i++) {
        if (m_last_keys[i] == keycode) {
            m_last_keys[i] = 0x00;
        }
    }
    
    // 发送空报告或剩余按键
    keyboard_send_keys(0, m_last_keys, 6);
}

// LED 输出报告处理
void kb_output_evt_handler(ble_hids_t * p_hids, 
                           ble_hids_evt_t * p_evt) {
    if (p_evt->evt_type == BLE_HIDS_EVT_OUTP_REP_WRITE) {
        uint8_t led_state = p_evt->params.outp_rep.data[0];
        
        // 控制 LED
        if (led_state & LED_CAPS_LOCK) {
            led_on(CAPS_LOCK_LED);
        } else {
            led_off(CAPS_LOCK_LED);
        }
        
        if (led_state & LED_NUM_LOCK) {
            led_on(NUM_LOCK_LED);
        } else {
            led_off(NUM_LOCK_LED);
        }
    }
}

⌨️ 多设备切换

flowchart TB subgraph Profiles["配对配置文件"] P1[设备 1
PC] P2[设备 2
手机] P3[设备 3
平板] end SWITCH[切换按键] --> SELECT{选择设备} SELECT -->|Fn+1| P1 SELECT -->|Fn+2| P2 SELECT -->|Fn+3| P3 P1 --> CONNECT1[连接设备 1] P2 --> CONNECT2[连接设备 2] P3 --> CONNECT3[连接设备 3] style Profiles fill:#e3f2fd,stroke:#007bff style SWITCH fill:#ffc107,color:#000
// 多设备切换实现
typedef struct {
    ble_gap_addr_t addr;      // 设备地址
    ble_gap_irk_t  irk;       // 身份解析密钥
    bool           bonded;    // 已配对
} device_profile_t;

// 存储 3 个设备配置
static device_profile_t m_profiles[3];
static uint8_t m_current_device = 0;

// 切换设备
void switch_device(uint8_t index) {
    if (index >= 3) return;
    
    // 断开当前连接
    sd_ble_gap_disconnect(m_conn_handle, 
                          BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
    
    // 更新当前设备
    m_current_device = index;
    
    // 使用对应设备的密钥广播
    if (m_profiles[index].bonded) {
        // 已配对,直接连接
        sd_ble_gap_connect(&m_profiles[index].addr, 
                           &m_scan_params,
                           &m_conn_params,
                           NULL);
    } else {
        // 未配对,进入广播模式
        start_advertising();
    }
    
    // 更新 LED 指示
    update_led_indicator(index);
}

⚡ 功耗优化

参数 配置值 说明
连接间隔 20ms - 50ms 键盘不需要超低延迟
从机延迟 4-9 允许跳过事件
监督超时 2s 平衡功耗与响应
广播间隔 30-60ms 快速重连
休眠电流 <5μA 深度睡眠

🔍 调试要点

常见问题

  • 按键重复:去抖时间不足
  • 多键无冲:检查报告格式
  • LED 不亮:输出报告未启用
  • 切换失败:清除配对信息

推荐工具

  • Device Manager: Windows 查看
  • Bluetooth Explorer: macOS 调试
  • nRF Sniffer: 空口抓包
  • Keyboard Tester: 在线测试

🔧 键盘数据包构建详解

从按键按下到蓝牙空中传输,每一层如何添加头部,组合成完整的数据包。

数据包组合流程

flowchart TB subgraph Step1["步骤 1: 按键输入"] A1["按键码 + 修饰键
8 bytes"] end subgraph Step2["步骤 2: HID 报告"] A2["Report ID + 数据
8 bytes"] end subgraph Step3["步骤 3: GATT 层"] A3["特征值句柄 + 报告"] end subgraph Step4["步骤 4: ATT 层"] A4["Opcode + Handle + Data"] end subgraph Step5["步骤 5: L2CAP 层"] A5["L2CAP Header
Length + CID"] end subgraph Step6["步骤 6: HCI 层"] A6["HCI ACL Header
Handle + Length"] end subgraph Step7["步骤 7: LL+PHY"] A7["LL Header + MIC + CRC
前导码 + 访问地址"] end A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> A6 A6 --> A7 style Step1 fill:#d4edda,stroke:#28a745 style Step2 fill:#e3f2fd,stroke:#007bff style Step3 fill:#e3f2fd,stroke:#007bff style Step4 fill:#e3f2fd,stroke:#007bff style Step5 fill:#fff3cd,stroke:#ffc107 style Step6 fill:#d4edda,stroke:#28a745 style Step7 fill:#d4edda,stroke:#28a745

逐步构建过程

// ========== 步骤 1: 按键输入 ==========
// 键盘矩阵扫描检测按键
uint8_t modifier = 0x02;    // Left Shift
uint8_t keycode = 0x1E;     // 数字键 1
// 组合后为 Shift+1 = !

// ========== 步骤 2: HID 报告封装 ==========
// 构建键盘输入报告 (8 bytes)
uint8_t keyboard_report[8] = {
    0x01,       // [0] Report ID: 1
    0x02,       // [1] Modifier: Left Shift
    0x00,       // [2] Reserved: 0
    0x1E,       // [3] Keycode 1: 1
    0x00,       // [4] Keycode 2: 0 (无)
    0x00,       // [5] Keycode 3: 0 (无)
    0x00,       // [6] Keycode 4: 0 (无)
    0x00        // [7] Keycode 5: 0 (无)
};
// HID 报告总长度:8 bytes

// 特征值句柄 (假设)
uint16_t report_handle = 0x002B;  // 输入报告特征值句柄

// ========== 步骤 3: GATT 层封装 ==========
// GATT 不添加额外头部,直接使用 ATT
// 但需要知道特征值句柄和 Report ID

// ========== 步骤 4: ATT 层封装 ==========
// 添加 ATT Header (Opcode + Handle)
uint8_t att_packet[] = {
    0x1B,                   // [0] Opcode: Handle Value Notification
    0x2B, 0x00,             // [1-2] Handle: 0x002B (小端序)
    // GATT 数据 (HID 报告)
    0x01, 0x02, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00
};
// ATT 层总长度:11 bytes

// ========== 步骤 5: L2CAP 层封装 ==========
// 添加 L2CAP Header (Length + CID)
uint8_t l2cap_packet[] = {
    0x0B, 0x00,             // [0-1] Length: 11 (ATT 包长度)
    0x04, 0x00,             // [2-3] CID: 0x0004 (ATT 信道)
    // ATT Packet (从步骤 4)
    0x1B, 0x2B, 0x00, 0x01, 0x02, 0x00, 0x1E, 0x00,
    0x00, 0x00, 0x00
};
// L2CAP 层总长度:15 bytes

// ========== 步骤 6: HCI ACL 层封装 ==========
// 添加 HCI ACL Header (Handle + Data Length)
uint8_t hci_packet[] = {
    0x01, 0x00,             // [0-1] Handle: 0x0001 (连接句柄)
    0x0F, 0x00,             // [2-3] Data Length: 15 (L2CAP 包长度)
    // L2CAP Packet (从步骤 5)
    0x0B, 0x00, 0x04, 0x00, 0x1B, 0x2B, 0x00, 0x01,
    0x02, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00
};
// HCI 层总长度:19 bytes

// ========== 步骤 7: Link Layer 封装 ==========
// LL 层添加:LL Header + MIC(加密) + CRC
// LL Data PDU 结构:
// +----------+------------+---------+-----+
// | LL Header| LL Payload | MIC(4B) | CRC |
// | (2B)     | (0-27B)    |         |(3B) |
// +----------+------------+---------+-----+

// LL Header (2 bytes)
uint8_t ll_header[] = {
    0x05,  // [0] LLID=01 (数据), NESN=0, SN=0, MD=0
    0x0F   // [1] Length: 15 bytes (HCI 数据长度)
};

// LL Payload = HCI Packet (19 bytes)
// 19 < 27-2-4-3 = 18, 需要分段发送!

// 分段 1 (LL PDU #1) - 前 23 bytes
uint8_t ll_pdu1[] = {
    0x05, 0x17,             // LL Header (Length=23)
    // HCI 数据前 19 bytes
    0x01, 0x00, 0x0F, 0x00, 0x0B, 0x00, 0x04, 0x00,
    0x1B, 0x2B, 0x00, 0x01, 0x02, 0x00, 0x1E, 0x00,
    0x00, 0x00, 0x00,
    // MIC (4 bytes, 如果启用加密)
    0xA1, 0xB2, 0xC3, 0xD4,
    // CRC (3 bytes)
    0x12, 0x34, 0x56
};

// ========== 步骤 8: PHY 层封装 ==========
// PHY 添加:前导码 + 访问地址
// PHY Packet 结构:
// +--------+----------+------------+-----+
// | Preamble| Access  |   Payload  | CRC |
// | (1B)   | Address  |  (LL PDU)  |     |
// |        | (4B)     |            |     |
// +--------+----------+------------+-----+

uint8_t preamble = 0x55;              // 前导码 (01010101)
uint32_t access_address = 0x8E89BED6; // 访问地址 (BLE 固定)
// Payload = LL PDU (从步骤 7)
// CRC 已在 LL 层计算

完整数据包结构图

flowchart LR subgraph PHY_Packet["PHY Packet (空中传输)"] P1["Preamble
1B"] P2["Access Addr
4B"] P3["LL PDU
可变"] P4["CRC
3B"] end subgraph LL_PDU["LL PDU"] L1["LL Header
2B"] L2["LL Payload
0-27B"] L3["MIC
4B"] end subgraph LL_Payload["LL Payload = HCI Packet"] H1["HCI ACL Header
4B"] H2["L2CAP Packet
可变"] end subgraph L2CAP["L2CAP Packet"] C1["L2CAP Header
4B"] C2["ATT PDU
可变"] end subgraph ATT["ATT PDU"] A1["ATT Header
3B"] A2["GATT Data
可变"] end subgraph GATT["GATT Data = HID Report"] G1["Report ID
1B"] G2["Modifier
1B"] G3["Reserved
1B"] G4["Keycode[0]
1B"] G5["Keycode[1-5]
5B"] end P3 --> LL_PDU L2 --> LL_Payload H2 --> L2CAP C2 --> ATT A2 --> GATT style PHY_Packet fill:#d4edda,stroke:#28a745 style LL_PDU fill:#e3f2fd,stroke:#007bff style LL_Payload fill:#fff3cd,stroke:#ffc107 style L2CAP fill:#e3f2fd,stroke:#007bff style ATT fill:#e3f2fd,stroke:#007bff style GATT fill:#d4edda,stroke:#28a745

数据包大小计算

头部大小 数据大小 本层总大小 累计大小
HID Report 0 B 8 B (键盘报告) 8 B 8 B
ATT 3 B (Opcode+Handle) 8 B (HID) 11 B 11 B
L2CAP 4 B (Length+CID) 11 B (ATT) 15 B 15 B
HCI ACL 4 B (Handle+Length) 15 B (L2CAP) 19 B 19 B
LL 2 B (Header) 19 B (HCI) 25 B (+MIC 4B +CRC 3B) 32 B
PHY 5 B (Preamble+Access) 32 B (LL) 37 B (+CRC 3B) 40 B

注意: 键盘报告 8 bytes,加上协议头部后约 40 bytes,通常可以一次 LL PDU 发送。

实际字节流 (十六进制)

# 最终在空中传输的完整字节流 (从内到外):

# 应用数据 (键盘报告 8 bytes):
01 02 00 1E 00 00 00 00

# + ATT Header (3 bytes):
1B 2B 00 01 02 00 1E 00 00 00 00

# + L2CAP Header (4 bytes):
0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00

# + HCI ACL Header (4 bytes):
0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00

# + LL Header + MIC + CRC:
05 13 0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00 A1 B2 C3 D4 12 34 56

# + PHY Preamble + Access Address:
55 8E 89 BE D6 05 13 0F 00 0B 00 04 00 1B 2B 00 01 02 00 1E 00 00 00 00 A1 B2 C3 D4 12 34 56

# 最终空中传输:约 40 bytes (单个包)

常见按键数据包

按键 Modifier Keycode[0] 完整报告
A 0x00 0x04 01 00 00 04 00 00 00 00
Ctrl+A 0x01 0x04 01 01 00 04 00 00 00 00
Enter 0x00 0x28 01 00 00 28 00 00 00 00
Escape 0x00 0x29 01 00 00 29 00 00 00 00
Space 0x00 0x2C 01 00 00 2C 00 00 00 00
F1 0x00 0x3A 01 00 00 3A 00 00 00 00
CapsLock 0x00 0x39 01 00 00 39 00 00 00 00
Shift+1 (!) 0x02 0x1E 01 02 00 1E 00 00 00 00
Ctrl+Alt+Del 0x05 0x4C 01 05 00 4C 00 00 00 00
多键 (ABC) 0x00 0x04,0x05,0x06 01 00 00 04 05 06 00 00
按键释放 0x00 0x00 01 00 00 00 00 00 00 00

按键按下与释放序列

# 按下并释放按键 "A" 的完整过程:

# 1. 初始状态 (无按键)
01 00 00 00 00 00 00 00

# 2. 按下 A (发送按键码)
01 00 00 04 00 00 00 00

# 3. 释放 A (发送空报告)
01 00 00 00 00 00 00 00

# 每个报告都会通过 BLE Notify 发送到主机
# 典型延迟:7.5ms - 20ms (取决于连接间隔)