附录05 上位机通信协议
约 1485 字大约 5 分钟
2026-05-12
1 协议性质
本协议为自定义设计,不基于蓝牙 HID 或其他标准通信协议。协议采用 Protocol Buffers v3 定义消息结构,使用 nanopb 轻量级编解码库。物理层通过 USB CDC ACM(串口,有线)和 BLE NUS(Nordic UART Service,无线)两条通道传输,协议层对通道无感知。
2 分层架构
┌──────────────────────────────────────────┐
│ 上位机 APP (Windows / iOS / Android) │
├──────────────────────────────────────────┤
│ DeviceMessage (protobuf, nanopb 编解码) │ ← 消息层
├──────────────────────────────────────────┤
│ 帧格式: Magic(2B) + Len(1B) + Payload │ ← 帧层
├──────────────────┬───────────────────────┤
│ USB CDC ACM │ BLE NUS │ ← 传输层
│ (cdc_acm_uart0) │ (Nordic UART Service) │
└──────────────────┴───────────────────────┘3 帧格式定义
帧结构:
┌──────────┬──────────┬──────────┬───────────────────┐
│ Byte 0 │ Byte 1 │ Byte 2 │ Byte 3..2+Len │
│ Magic[0] │ Magic[1] │ Len(N) │ Payload (N bytes) │
│ 0x55 │ 0xAA │ 0..64 │ protobuf 编码 │
└──────────┴──────────┴──────────┴───────────────────┘帧解析规则:
- 收到数据后扫描
0xAA55魔数 - 魔数不匹配 → 丢弃 1 字节,继续扫描
- 魔数匹配 → 读取 Length 字段,检查
Len ≤ 64 - 累积数据不足完整帧 → 等待更多数据
- 完整帧到达 → 提取 Payload,送入 protobuf 解码器
4 Protobuf 消息定义
4.1 顶层消息
message DeviceMessage {
uint32 msg_id = 10; // 消息 ID,用于请求-响应对应
uint32 reply_to = 11; // 回应哪条消息的 msg_id
oneof body {
HelloReq hello_req = 1;
HelloRsp hello_rsp = 2;
Bitmap bitmap = 3;
FunctionKeyEvent function_key_event = 4;
LedState led_state = 5;
TimeSync time_sync = 6;
ThemeRgb theme_rgb = 7;
Response response = 8;
}
}每条消息包含 msg_id(请求用)和 reply_to(回复用),实现请求-响应配对。
4.2 错误码
enum ResponseCode {
RESPONSE_CODE_OK = 0; // 成功
RESPONSE_CODE_UNKNOWN_TYPE = 1; // 不支持的消息类型
RESPONSE_CODE_INVALID_LENGTH = 2; // 字段长度不合法
RESPONSE_CODE_INVALID_PARAM = 3; // 参数值不合法
RESPONSE_CODE_NOT_READY = 4; // 会话未握手
}5 消息功能详述
5.1 HelloReq / HelloRsp — 会话握手
方向: 上位机 → HelloReq → 键盘 → HelloRsp
| 字段 | 类型 | 说明 |
|---|---|---|
HelloReq.protocol_version | uint32 | 上位机支持的协议版本 |
| 字段 | 类型 | 固定值 | 说明 |
|---|---|---|---|
HelloRsp.protocol_version | uint32 | 1 | 协议版本 |
HelloRsp.vendor_id | uint32 | 0x1915 | 厂商 ID |
HelloRsp.product_id | uint32 | 0x52F0 | 产品 ID |
HelloRsp.firmware_major | uint32 | 0 | 固件主版本 |
HelloRsp.firmware_minor | uint32 | 0 | 固件次版本 |
HelloRsp.capability_flags | uint32 | 0x1F | 能力位掩码 |
会话模型:
- 传输链路就绪后,协议会话进入
WAIT_HELLO状态 - 上位机必须在此时发送 HelloReq
- 键盘收到后回复 HelloRsp,会话进入
ACTIVE状态 ACTIVE之前收到的其他消息类型均返回NOT_READY
5.2 Bitmap — 按键位图下发
方向: 上位机 → 键盘 前置条件: 会话 ACTIVE
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
Bitmap.usage_bitmap | bytes | 29 | NKRO 按键位图 |
处理流程: 键盘将上位机下发的位图注入到当前 HID 按键报告中,效果等同于物理按键。位图长度必须等于 KEYBOARD_PROTOCOL_BITMAP_BYTES(29 字节),否则返回 INVALID_LENGTH。
5.3 FunctionKeyEvent — 功能键事件上报
方向: 键盘 → 上位机(主动推送) 前置条件: 会话 ACTIVE
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
FunctionKeyEvent.usage_bitmap | bytes | 29 | 功能键组合位图 |
触发条件: 用户按下 Fn 组合键(如 Fn+F1、Fn+F2 等)时触发。键盘向所有已 ACTIVE 的传输通道广播此事件。
5.4 LedState — LED 状态上报
方向: 键盘 → 上位机(主动推送) 前置条件: 会话 ACTIVE
| 字段 | 类型 | 说明 |
|---|---|---|
LedState.led_mask | uint32 | LED 状态位掩码 |
位掩码定义(标准 HID LED Usage):
| 位 | 含义 |
|---|---|
| bit 0 | Caps Lock |
| bit 1 | Num Lock |
| bit 2 | Scroll Lock |
| bit 3 | Compose |
| bit 4 | Kana |
触发条件: 键盘收到来自 USB HID 或 BLE HIDS 的 LED Output Report 后自动上报。发送优先级为与触发通道一致的传输通道(USB LED → USB CDC,BLE LED → BLE NUS)。
5.5 TimeSync — 时间同步
方向: 上位机 → 键盘 前置条件: 会话 ACTIVE
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
TimeSync.version | uint32 | 必须为 1 | 协议版本 |
TimeSync.flags | uint32 | — | 标志位(保留) |
TimeSync.timezone_min | sint32 | — | 时区偏移(分钟,有符号) |
TimeSync.utc_ms | fixed64 | — | UTC 时间戳(毫秒) |
TimeSync.accuracy_ms | fixed32 | — | 时间精度(毫秒) |
处理流程: version 不为 1 时返回 INVALID_PARAM。有效数据通过 time_sync_event 发布,由 time_sync_module 同步本地 RTC。
5.6 ThemeRgb — 主题颜色配置
方向: 上位机 → 键盘 前置条件: 会话 ACTIVE
| 字段 | 类型 | 范围 | 说明 |
|---|---|---|---|
ThemeRgb.red | uint32 | 0–255 | 红色分量 |
ThemeRgb.green | uint32 | 0–255 | 绿色分量 |
ThemeRgb.blue | uint32 | 0–255 | 蓝色分量 |
处理流程: 任一通道值 > 255 时返回 INVALID_PARAM。有效值通过 theme_rgb_update_event 发布,由 led_strip_module 更新 LED 灯效,同时 display_module 更新屏幕主题。
5.7 Response — 通用响应
方向: 键盘 → 上位机 协议处理每个请求后必须回复 Response,携带错误码。
| 字段 | 类型 | 说明 |
|---|---|---|
Response.error_code | ResponseCode | 操作结果 |
6 会话状态机
每个传输通道独立维护会话状态(protocol_module.c:41-45):
LINK_READY
PROTO_SESSION ──────────► PROTO_SESSION
_DOWN _WAIT_HELLO
▲ │
│ │ HelloReq
│ LINK_DOWN ▼
│ PROTO_SESSION
│◄───────────────────── _ACTIVE
│ LINK_DOWN │ │
│ │ │ 处理所有消息类型
└────────────────────────┘ │
│
消息交互 (Bitmap/TimeSync/ThemeRgb/
LedState/FunctionKeyEvent)状态行为:
| 状态 | 可处理的消息 | 其他消息返回 |
|---|---|---|
DOWN | 无 | 拒绝 |
WAIT_HELLO | HelloReq | 拒绝 |
ACTIVE | 全部 | UNKNOWN_TYPE 或具体错误 |
7 传输层实现
7.1 USB CDC ACM
| 项目 | 详情 |
|---|---|
| 设备节点 | cdc_acm_uart0 |
| 业务状态机 | BUS_OFFLINE → WAIT_DTR → SESSION_READY |
| 数据通路 | 中断驱动 UART → 环形缓冲区 → rx_work 帧提取 → proto_rx_event |
| DTR 检测 | 收到 DTR 后进入 SESSION_READY |
| Line Coding | 期望 115200 8N1 无流控 |
| 缓冲区 | RX ring 256B, TX ring 256B |
7.2 BLE NUS
| 项目 | 详情 |
|---|---|
| 业务状态机 | STACK_OFFLINE → IDLE → WAIT_NOTIFY → SESSION_READY |
| 数据通路 | 连接 → CCCD 启用 → bt_nus_send / received 回调 |
| 帧长度校验 | 3~67 字节(帧头 3 + 载荷 ≤64) |
8 上位机实现指南
上位机开发需遵循以下要点:
- 协议层:使用 protobuf 编译器 + nanopb 或标准 protobuf 库(Java/Go/Python)生成
DeviceMessage编解码代码 - 帧层:上层实现需按照
0xAA55 + Len + Payload格式组帧/拆帧 - 传输层: 通过系统串口 API(USB CDC ACM)或 BLE GATT NUS characteristic(BLE)收发原始字节流
- 握手时序:建立传输链路后必须在 5 秒内 发送 HelloReq,否则会话保持 WAIT_HELLO 不处理其他消息
- 请求-响应:每条请求设唯一
msg_id,键盘在 Response 的reply_to中回传该值 - 双通道互不干扰:USB 和 BLE 各有独立会话状态,可同时工作
