🖱️ 蓝牙鼠标

基于 HID over GATT Profile 的无线鼠标,低延迟、低功耗设计。

📐 HID over GATT 架构

flowchart TB subgraph Mouse["蓝牙鼠标"] SENSOR[光学传感器] --> MCU[BLE MCU] BUTTON[按键扫描] --> MCU MCU --> ANT[天线] end subgraph Host["主机 (PC/手机)"] ANT2[天线] --> HOST[Host OS] HOST --> HID[HID 驱动] HID --> CURSOR[光标控制] end ANT -.->|BLE 低延迟连接 | ANT2 style Mouse fill:#e3f2fd,stroke:#007bff style Host fill:#d4edda,stroke:#28a745

📋 HID 服务结构

flowchart TB subgraph HID["HID 服务 0x1812"] INFO[HID 信息 0x2A4A] REPORT_MAP[报告映射 0x2A4B] CTRL_PT[控制点 0x2A4C] REPORT[报告特征值 0x2A4D] end REPORT --> CCCD[CCCD 0x2902] subgraph Report["鼠标报告类型"] INPUT[输入报告
移动/按键] OUTPUT[输出报告
LED 控制] FEATURE[特征报告
DPI 设置] end style HID fill:#e3f2fd,stroke:#007bff style INPUT fill:#fff3cd,stroke:#ffc107

📊 鼠标输入报告格式

// HID 输入报告格式 (标准鼠标)
// Report ID: 0x01
// 总长度:5 bytes

typedef struct {
    uint8_t  report_id;    // Report ID (0x01)
    uint8_t  buttons;      // 按键状态
    int8_t   x_axis;       // X 轴移动 (-127~127)
    int8_t   y_axis;       // Y 轴移动 (-127~127)
    int8_t   wheel;        // 滚轮 (-127~127)
} mouse_input_report_t;

// 按键位定义
#define MOUSE_BTN_LEFT    0x01
#define MOUSE_BTN_RIGHT   0x02
#define MOUSE_BTN_MIDDLE  0x04
#define MOUSE_BTN_BACK    0x08
#define MOUSE_BTN_FORWARD 0x10

// 示例:左键按下,向右移动 10 像素,向上移动 5 像素
uint8_t mouse_report[] = {
    0x01,  // Report ID
    0x01,  // Buttons: Left clicked
    0x0A,  // X: +10
    0xFB,  // Y: -5 (向上,补码)
    0x00   // Wheel: 无滚动
};

// HID 报告描述符 (Report Descriptor)
const uint8_t report_descriptor[] = {
    0x05, 0x01,        // Usage Page (Desktop)
    0x09, 0x02,        // Usage (Mouse)
    0xA1, 0x01,        // Collection (Application)
    
    0x85, 0x01,        //   Report ID (1)
    0x09, 0x01,        //   Usage (Pointer)
    0xA1, 0x00,        //   Collection (Physical)
    
    0x05, 0x09,        //     Usage Page (Button)
    0x19, 0x01,        //     Usage Minimum (1)
    0x29, 0x05,        //     Usage Maximum (5)
    0x15, 0x00,        //     Logical Minimum (0)
    0x25, 0x01,        //     Logical Maximum (1)
    0x95, 0x05,        //     Report Count (5)
    0x75, 0x01,        //     Report Size (1)
    0x81, 0x02,        //     Input (Data, Var, Abs)
    
    0x95, 0x01,        //     Report Count (1)
    0x75, 0x03,        //     Report Size (3)
    0x81, 0x01,        //     Input (Cnst)
    
    0x05, 0x01,        //     Usage Page (Desktop)
    0x09, 0x30,        //     Usage (X)
    0x09, 0x31,        //     Usage (Y)
    0x15, 0x81,        //     Logical Minimum (-127)
    0x25, 0x7F,        //     Logical Maximum (127)
    0x75, 0x08,        //     Report Size (8)
    0x95, 0x02,        //     Report Count (2)
    0x81, 0x06,        //     Input (Data, Var, Rel)
    
    0x09, 0x38,        //     Usage (Wheel)
    0x81, 0x06,        //     Input (Data, Var, Rel)
    
    0xC0,              //   End Collection
    0xC0               // End Collection
};

📈 完整工作流程

sequenceDiagram participant Mouse as 蓝牙鼠标 participant Host as 主机 (PC) Note over Mouse,Host: 1. 配对模式 Mouse->>Mouse: 进入配对 (长按) Mouse->>Host: ADV_IND (可配对) Note over Mouse,Host: 2. 配对连接 Host->>Mouse: CONNECT_IND Host->>Mouse: 配对请求 Mouse-->>Host: 配对确认 Host->>Mouse: 加密建立 Note over Mouse,Host: 3. 服务发现 Host->>Mouse: 发现 HID 服务 Host->>Mouse: 读取报告描述符 Note over Mouse,Host: 4. 启用通知 Host->>Mouse: 写入 CCCD (启用) Note over Mouse,Host: 5. 数据传输 loop 移动/点击 Mouse->>Host: 输入报告通知 end Note over Mouse,Host: 6. 低功耗 Mouse->>Mouse: 无操作 5s Mouse->>Host: 进入休眠

💻 固件代码示例

#include "ble_hids.h"

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

// 鼠标输入报告
static uint8_t m_mouse_report[MOUSE_REPORT_MAX_LEN];

// 初始化 HID 服务
void hids_mouse_init(void) {
    uint32_t err_code;
    
    // HID 服务初始化配置
    ble_hids_init_t hids_init = {0};
    
    // 配置报告映射
    hids_init.rep_map.p_rep_map = (uint8_t *)report_descriptor;
    hids_init.rep_map.rep_map_len = sizeof(report_descriptor);
    
    // 配置输入报告
    ble_hids_inp_rep_t *p_inp_rep = &hids_init.inp_rep[0];
    p_inp_rep->max_len = MOUSE_REPORT_MAX_LEN;
    p_inp_rep->rep_ref.report_id = 1;
    p_inp_rep->rep_ref.report_type = BLE_HIDS_INPUT_REP;
    p_inp_rep->rep_ref.uuid = BLE_UUID_HID_REPORT_TYPE;
    p_inp_rep->evt_handler = input_report_evt_handler;
    p_inp_rep->security_mode.ccm.cps = 1;  // 需要加密
    
    hids_init.inp_rep_count = 1;
    
    // 配置 HID 信息
    hids_init.hid_information.bcd_hid = 0x0111;  // HID 1.11
    hids_init.hid_information.b_country_code = 0;
    hids_init.hid_information.flags = 0x01;      // 远程唤醒
    
    // 初始化服务
    err_code = ble_hids_init(&m_hids, &hids_init);
    APP_ERROR_CHECK(err_code);
}

// 发送鼠标报告
void mouse_send_report(int8_t x, int8_t y, uint8_t buttons) {
    uint32_t err_code;
    
    m_mouse_report[0] = 0x01;  // Report ID
    m_mouse_report[1] = buttons;
    m_mouse_report[2] = x;
    m_mouse_report[3] = y;
    m_mouse_report[4] = 0;     // Wheel
    
    // 发送通知
    err_code = ble_hids_inp_rep_send(&m_hids,
                                      0,  // 输入报告索引
                                      MOUSE_REPORT_MAX_LEN,
                                      m_mouse_report,
                                      m_conn_handle);
    
    if (err_code != NRF_SUCCESS) {
        // 缓冲区满,稍后重试
    }
}

// 按键扫描任务
void mouse_scan_task(void) {
    static uint8_t last_buttons = 0;
    static int16_t last_x = 0, last_y = 0;
    
    // 读取传感器数据
    int16_t delta_x, delta_y;
    optical_sensor_read(&delta_x, &delta_y);
    
    // 读取按键状态
    uint8_t buttons = read_buttons();
    
    // 有变化才发送 (降低功耗)
    if (delta_x != 0 || delta_y != 0 || buttons != last_buttons) {
        // 限制范围 (-127 ~ 127)
        int8_t send_x = CLAMP(delta_x, -127, 127);
        int8_t send_y = CLAMP(delta_y, -127, 127);
        
        mouse_send_report(send_x, send_y, buttons);
        
        last_buttons = buttons;
        last_x = delta_x;
        last_y = delta_y;
        
        // 重置活动定时器
        activity_timer_restart();
    }
}

⚡ 低延迟优化

参数 配置值 说明
连接间隔 7.5ms - 11.25ms 最低延迟 7.5ms
从机延迟 0 不允许跳过事件
监督超时 500ms 快速检测断开
MTU 23 bytes (默认) 报告足够小
PHY 2M PHY BLE 5.0+ 高速模式

🔐 配对与加密

sequenceDiagram participant Mouse as 鼠标 participant Host as 主机 Note over Mouse,Host: 1. 进入配对模式 Mouse->>Mouse: 长按配对键 3s Mouse->>Host: 广播 (可发现) Note over Mouse,Host: 2. 发起配对 Host->>Mouse: 连接请求 Host->>Mouse: 配对请求 (Just Works) Mouse-->>Host: 配对响应 Note over Mouse,Host: 3. 密钥交换 Mouse->>Host: 公钥交换 Host->>Mouse: 公钥 Note over Mouse,Host: 4. 加密建立 Mouse->>Host: 加密请求 Host->>Mouse: 加密完成 Mouse->>Mouse: 存储 LTK Note over Mouse,Host: 5. 重连 (下次) Mouse->>Host: 直接加密连接
// 配对配置
void ble_gap_sec_params_init(void) {
    ble_gap_sec_params_t sec_params = {
        .bond = 1,           // 允许绑定
        .mitm = 0,           // 不需要 MITM 保护
        .lesc = 0,           // 不使用 LE Secure Connections
        .keypress = 0,       // 不支持按键确认
        .io_caps = BLE_GAP_IO_CAPS_NONE,  // 无 IO 能力
        .oob = 0,            // 无 OOB 数据
        .min_key_size = 7,
        .max_key_size = 16,
        .kdist_own.enc = 1,  // 分发加密密钥
        .kdist_own.id = 1,   // 分发身份密钥
        .kdist_peer.enc = 1,
        .kdist_peer.id = 1,
    };
    
    sd_ble_gap_sec_params_reply(m_conn_handle, 
                                 BLE_GAP_SEC_STATUS_SUCCESS, 
                                 &sec_params, 
                                 NULL);
}

🔍 调试要点

常见问题

  • 光标跳动:检查传感器 DPI 设置
  • 延迟高:确认连接间隔 7.5ms
  • 配对失败:清除主机缓存
  • 唤醒慢:调整广播间隔

推荐工具

  • Wireshark: HCI 抓包分析
  • nRF Sniffer: BLE 空口抓包
  • BlueZ: Linux 调试
  • Device Manager: Windows 查看

🔧 鼠标数据包构建详解

从鼠标移动/点击到蓝牙空中传输,每一层如何添加头部,组合成完整的数据包。

数据包组合流程

flowchart TB subgraph Step1["步骤 1: 鼠标输入"] A1["移动 ΔX,ΔY
按键状态"] end subgraph Step2["步骤 2: HID 报告"] A2["Report ID + 数据
5 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: 鼠标输入 ==========
// 光学传感器检测移动,按键扫描检测点击
int8_t delta_x = 10;        // 向右移动 10 像素
int8_t delta_y = -5;        // 向上移动 5 像素
uint8_t buttons = 0x01;     // 左键按下
int8_t wheel = 0;           // 无滚动

// ========== 步骤 2: HID 报告封装 ==========
// 构建鼠标输入报告 (5 bytes)
uint8_t mouse_report[5] = {
    0x01,       // [0] Report ID: 1
    0x01,       // [1] Buttons: Left clicked
    0x0A,       // [2] X: +10
    0xFB,       // [3] Y: -5 (补码 0xFB = -5)
    0x00        // [4] Wheel: 0
};
// HID 报告总长度:5 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, 0x01, 0x0A, 0xFB, 0x00
};
// ATT 层总长度:8 bytes

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

// ========== 步骤 6: HCI ACL 层封装 ==========
// 添加 HCI ACL Header (Handle + Data Length)
uint8_t hci_packet[] = {
    0x01, 0x00,             // [0-1] Handle: 0x0001 (连接句柄)
    0x0C, 0x00,             // [2-3] Data Length: 12 (L2CAP 包长度)
    // L2CAP Packet (从步骤 5)
    0x08, 0x00, 0x04, 0x00, 0x1B, 0x2B, 0x00, 0x01,
    0x01, 0x0A, 0xFB, 0x00
};
// HCI 层总长度:16 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
    0x0C   // [1] Length: 12 bytes (HCI 数据长度)
};

// LL Payload = HCI Packet (16 bytes)
// 16 < 27-2-4-3 = 18,可以一次发送,无需分段

// MIC (4 bytes, 如果启用加密)
uint8_t mic[] = {0xA1, 0xB2, 0xC3, 0xD4};  // 示例

// CRC (3 bytes)
uint8_t crc[] = {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["Buttons
1B"] G3["X Axis
1B"] G4["Y Axis
1B"] G5["Wheel
1B"] 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 5 B (鼠标报告) 5 B 5 B
ATT 3 B (Opcode+Handle) 5 B (HID) 8 B 8 B
L2CAP 4 B (Length+CID) 8 B (ATT) 12 B 12 B
HCI ACL 4 B (Handle+Length) 12 B (L2CAP) 16 B 16 B
LL 2 B (Header) 16 B (HCI) 22 B (+MIC 4B +CRC 3B) 29 B
PHY 5 B (Preamble+Access) 29 B (LL) 34 B (+CRC 3B) 37 B

优势: 鼠标报告很小 (5 bytes),可以一次 LL PDU 发送,无需分段,延迟最低。

实际字节流 (十六进制)

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

# 应用数据 (鼠标报告 5 bytes):
01 01 0A FB 00

# + ATT Header (3 bytes):
1B 2B 00 01 01 0A FB 00

# + L2CAP Header (4 bytes):
08 00 04 00 1B 2B 00 01 01 0A FB 00

# + HCI ACL Header (4 bytes):
0C 00 08 00 04 00 1B 2B 00 01 01 0A FB 00

# + LL Header + MIC + CRC:
05 0C 0C 00 08 00 04 00 1B 2B 00 01 01 0A FB 00 A1 B2 C3 D4 12 34 56

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

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

鼠标移动示例

动作 Report ID Buttons X Y Wheel 十六进制
静止 0x01 0x00 0x00 0x00 0x00 01 00 00 00 00
左键单击 0x01 0x01 0x00 0x00 0x00 01 01 00 00 00
向右移动 0x01 0x00 0x0A 0x00 0x00 01 00 0A 00 00
向上移动 0x01 0x00 0x00 0xFB 0x00 01 00 00 FB 00
右键 + 移动 0x01 0x02 0x05 0xF8 0x00 01 02 05 F8 00
滚轮向下 0x01 0x00 0x00 0x00 0xFF 01 00 00 00 FF