🖱️ 蓝牙鼠标
基于 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
移动/按键] 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
按键状态"] 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
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 |