Initial import of firmware and host projects
This commit is contained in:
305
KeyBorad/PROTOBUF_ALIGN_STEPS.md
Normal file
305
KeyBorad/PROTOBUF_ALIGN_STEPS.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# KeyBorad Protobuf 对齐步骤
|
||||
|
||||
这份步骤是按当前工程目录写的,目标是:
|
||||
|
||||
1. 保留当前 CDC 传输帧: `AA 55 + len + type + data + checksum`
|
||||
2. 把 `data` 改成 protobuf 二进制载荷
|
||||
3. 上位机使用 protobuf C++
|
||||
4. 下位机使用 nanopb
|
||||
|
||||
## 1. 先确定边界
|
||||
|
||||
当前工程里三层职责不要打乱:
|
||||
|
||||
- `KeyBorad\KeyBorad\DRI\Dri_Cdc.*`
|
||||
作用: 串口枚举、握手、收发字节流
|
||||
- `KeyBorad\KeyBorad\COM\Com_CdcEncode.*`
|
||||
作用: 把一帧 CDC 包组出来
|
||||
- `KeyBorad\KeyBorad\COM\Com_CdcDecode.*`
|
||||
作用: 从 CDC 字节流缓存里拆出一帧
|
||||
|
||||
引入 protobuf 以后,建议职责变成:
|
||||
|
||||
- `DRI`
|
||||
只管串口
|
||||
- `COM`
|
||||
只管外层帧
|
||||
- `proto`
|
||||
只管 `data` 的结构定义
|
||||
|
||||
也就是说:
|
||||
|
||||
- `type` 继续保留在外层帧里
|
||||
- `data` 改为 protobuf 序列化结果
|
||||
- `len` 改为 protobuf 载荷实际长度
|
||||
|
||||
## 2. 当前已经准备好的文件
|
||||
|
||||
本次已经加了两份协议源文件:
|
||||
|
||||
- `KeyBorad\proto\keyboard.proto`
|
||||
- `KeyBorad\proto\keyboard.options`
|
||||
|
||||
其中:
|
||||
|
||||
- `keyboard.proto`
|
||||
定义了 `HelloReqPayload / HelloRspPayload / BitmapPayload / FunctionKeyEventPayload / TimeSyncPayload` 等消息
|
||||
- `keyboard.options`
|
||||
给 `nanopb` 的 `bytes usage_bitmap` 指定了 `max_size:29`
|
||||
|
||||
## 3. 下载工具
|
||||
|
||||
### 上位机 protobuf
|
||||
|
||||
按 protobuf 官方文档,`protoc` 用 `--cpp_out` 生成 C++ 代码。
|
||||
|
||||
你需要准备:
|
||||
|
||||
- `protoc.exe`
|
||||
- protobuf C++ runtime 头文件和库
|
||||
|
||||
官方参考:
|
||||
|
||||
- protobuf C++ generated code guide
|
||||
- protobuf releases / protoc binary
|
||||
|
||||
### 下位机 nanopb
|
||||
|
||||
你需要准备:
|
||||
|
||||
- `nanopb` runtime
|
||||
`pb.h`, `pb_common.c/h`, `pb_encode.c/h`, `pb_decode.c/h`
|
||||
- `nanopb_generator.py`
|
||||
|
||||
官方参考:
|
||||
|
||||
- nanopb overview
|
||||
- nanopb generator reference
|
||||
|
||||
## 4. 生成代码
|
||||
|
||||
### 4.1 生成上位机 C++ 代码
|
||||
|
||||
在工程根目录 `C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard` 下执行:
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null
|
||||
protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto
|
||||
```
|
||||
|
||||
预期生成:
|
||||
|
||||
- `KeyBorad\generated\cpp\keyboard.pb.h`
|
||||
- `KeyBorad\generated\cpp\keyboard.pb.cc`
|
||||
|
||||
### 4.2 生成下位机 nanopb 代码
|
||||
|
||||
如果你是 nanopb 源码目录:
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null
|
||||
python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto
|
||||
```
|
||||
|
||||
如果你用的是 nanopb 二进制包,自带 generator,也可以直接换成对应可执行文件。
|
||||
|
||||
预期生成:
|
||||
|
||||
- `KeyBorad\generated\nanopb\keyboard.pb.h`
|
||||
- `KeyBorad\generated\nanopb\keyboard.pb.c`
|
||||
|
||||
## 5. 先做一处关键改造
|
||||
|
||||
### 必改: 不再把 payload 长度当成固定值
|
||||
|
||||
一旦 `data` 改成 protobuf,`HelloRsp / TimeSync / Ack` 的 `data` 长度就不一定再等于今天的固定字节数。
|
||||
|
||||
所以迁移时要先把 `COM` 层规则改成:
|
||||
|
||||
- `len` 表示 protobuf 载荷实际字节数
|
||||
- `Com_CdcDecode` 不再用 `type -> 固定 len` 做强校验
|
||||
- 改成:
|
||||
- 校验帧头
|
||||
- 校验 `len`
|
||||
- 校验 checksum
|
||||
- 可选校验某个 `type` 的最大长度,不再校验固定长度
|
||||
|
||||
建议做法:
|
||||
|
||||
- 先保留 `Packet_Type`
|
||||
- 删除或弱化 `Packet_len` 在解码期的“硬匹配”
|
||||
- `Packet_len` 只保留给旧协议兼容或做上限参考
|
||||
|
||||
## 6. 上位机接入方式
|
||||
|
||||
### 6.1 在 VCXPROJ 里加入生成文件
|
||||
|
||||
把下面文件加入 `KeyBorad\KeyBorad\KeyBorad.vcxproj`:
|
||||
|
||||
- `..\generated\cpp\keyboard.pb.cc`
|
||||
- `..\generated\cpp\keyboard.pb.h`
|
||||
|
||||
并加 include 目录:
|
||||
|
||||
- `KeyBorad\generated\cpp`
|
||||
- protobuf runtime include 目录
|
||||
|
||||
再链接 protobuf C++ runtime 库。
|
||||
|
||||
### 6.2 改造原则
|
||||
|
||||
不要再在 `Com_Cdc.h` 里维护两份字段和 `data`。
|
||||
|
||||
建议改成两层:
|
||||
|
||||
- 原始帧:
|
||||
`Packet`
|
||||
- protobuf payload:
|
||||
`keyboard::cdc::HelloReqPayload` 等生成类
|
||||
|
||||
### 6.3 发送流程
|
||||
|
||||
以 `HelloReq` 为例:
|
||||
|
||||
1. 创建 `keyboard::cdc::HelloReqPayload`
|
||||
2. 填字段
|
||||
3. `SerializeToString()` / `SerializeToArray()`
|
||||
4. 把序列化结果塞进 `Packet.data`
|
||||
5. `Packet.type = Com_Type_HelloReq`
|
||||
6. 用 `Com_Cdc_Func_BuildFrame()` 打外层包
|
||||
|
||||
伪代码:
|
||||
|
||||
```cpp
|
||||
keyboard::cdc::HelloReqPayload payload;
|
||||
payload.set_protocol_version(1);
|
||||
|
||||
std::string bytes;
|
||||
payload.SerializeToString(&bytes);
|
||||
|
||||
Packet frame;
|
||||
frame.type = Com_Type_HelloReq;
|
||||
frame.data = QByteArray(bytes.data(), static_cast<int>(bytes.size()));
|
||||
|
||||
QByteArray tx = Com_Cdc_Func_BuildFrame(frame);
|
||||
```
|
||||
|
||||
### 6.4 接收流程
|
||||
|
||||
1. `Dri_Cdc_Read()` 读到完整 CDC 帧
|
||||
2. `Com_Cdc_Func_ParseFrame()` 拿到 `Packet`
|
||||
3. 根据 `Packet.type` 选择 protobuf 消息类型
|
||||
4. 对 `Packet.data` 做 `ParseFromArray()`
|
||||
|
||||
伪代码:
|
||||
|
||||
```cpp
|
||||
Packet frame;
|
||||
if (!Com_Cdc_Func_ParseFrame(frameBytes, &frame))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame.type == Com_Type_HelloRsp)
|
||||
{
|
||||
keyboard::cdc::HelloRspPayload payload;
|
||||
if (!payload.ParseFromArray(frame.data.constData(), frame.data.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 下位机接入方式
|
||||
|
||||
下位机不要碰外层 CDC 帧格式,直接沿用:
|
||||
|
||||
- 帧头
|
||||
- 长度
|
||||
- type
|
||||
- checksum
|
||||
|
||||
只把 `type` 对应的 `data` 改成 nanopb 编解码。
|
||||
|
||||
建议流程:
|
||||
|
||||
1. 串口 ISR / DMA 收满一帧
|
||||
2. 校验 `AA 55 / len / checksum`
|
||||
3. 根据 `type` 选 `keyboard.pb.h` 里的消息描述符
|
||||
4. `pb_decode()` 解 protobuf 载荷
|
||||
5. 业务处理
|
||||
6. 回复时 `pb_encode()` 出 payload,再挂回外层 CDC 帧
|
||||
|
||||
## 8. 推荐迁移顺序
|
||||
|
||||
建议按下面顺序,不要一次全换:
|
||||
|
||||
1. 只提交 `.proto` 和 `.options`
|
||||
2. 上位机先接 `HelloReq / HelloRsp`
|
||||
3. 下位机把 `HelloReq / HelloRsp` 改成 nanopb
|
||||
4. 确认握手通了,再迁移 `Ack / Error`
|
||||
5. 然后迁移 `TimeSync / ThemeRgb`
|
||||
6. 最后迁移 `Bitmap / FunctionKeyEvent`
|
||||
|
||||
原因:
|
||||
|
||||
- `HelloReq / HelloRsp` 字段简单,最适合先打通生成链路
|
||||
- `Bitmap` 有 `bytes[29]`,更适合在 `nanopb` 选项验证完后再切
|
||||
|
||||
## 9. 你当前 COM 文件还能起什么作用
|
||||
|
||||
即使引入 protobuf,当前 `COM` 层仍然非常有价值:
|
||||
|
||||
- `Com_CdcEncode`
|
||||
继续负责外层帧打包
|
||||
- `Com_CdcDecode`
|
||||
继续负责流式拆包
|
||||
- `Packet_Type`
|
||||
继续作为外层快速分发 ID
|
||||
|
||||
真正被 protobuf 替代的是:
|
||||
|
||||
- 旧的 `data` 手写字段布局
|
||||
- 手工小端拼 payload 的代码
|
||||
|
||||
一句话:
|
||||
|
||||
- `COM` 继续保留
|
||||
- 但 `COM` 不再定义业务 payload 字段
|
||||
- payload 交给 `.proto`
|
||||
|
||||
## 10. 一套最小可复现命令
|
||||
|
||||
假设你已经装好 `protoc` 和 `nanopb`,最小步骤就是:
|
||||
|
||||
```powershell
|
||||
Set-Location C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard
|
||||
|
||||
New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null
|
||||
protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto
|
||||
|
||||
New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null
|
||||
python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto
|
||||
```
|
||||
|
||||
然后做两边接入:
|
||||
|
||||
1. 上位机 `vcxproj` 加入 `keyboard.pb.cc`
|
||||
2. 下位机工程加入 `keyboard.pb.c`
|
||||
3. 把 `HelloReq / HelloRsp` 的 `data` 改为 protobuf payload
|
||||
4. 把 `Com_CdcDecode` 的固定长度校验去掉
|
||||
|
||||
## 11. 最后一个关键建议
|
||||
|
||||
如果你只是想“共享结构定义”,那就:
|
||||
|
||||
- 保留外层 `type`
|
||||
- 每个 `type` 对应一个 protobuf message
|
||||
|
||||
不要一开始就把外层 `type` 也塞进 protobuf `oneof` 里。
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 外层 `type` 对调试串口抓包很直观
|
||||
- 下位机分发也更省
|
||||
- 迁移成本最小
|
||||
Reference in New Issue
Block a user