feat(gml670x4): 所有协议帧添加 CRC-8/MAXIM 校验

使用 Zephyr crc8() 对帧头之后的载荷计算 CRC,上行发送前填充、
下行接收时校验不通过则丢弃并告警。同步更新协议文档。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 14:38:28 +08:00
parent 0d2fd30b3a
commit dc525ea4e7
6 changed files with 61 additions and 29 deletions
+25 -16
View File
@@ -27,19 +27,21 @@
当前协议使用双帧头 `0xAA 0x55` 作为包起始标记。所有多字节字段按 Little Endian 编码。 当前协议使用双帧头 `0xAA 0x55` 作为包起始标记。所有多字节字段按 Little Endian 编码。
每帧末尾附加 1 字节 CRC-8/MAXIM 校验(poly=`0x31`, init=`0x00`, refin=true, refout=true, xorout=`0x00`)。校验范围为帧头之后、CRC 字节之前的所有字段(即跳过 `sync0`/`sync1`)。调用方式:`crc8(data + 2, len - 3, 0x31, 0x00, true)`
| 类型名 | 值 | 方向 | 长度 | 频率 | 说明 | | 类型名 | 值 | 方向 | 长度 | 频率 | 说明 |
|--------|----|------|------|------|------| |--------|----|------|------|------|------|
| `PROTO_TYPE_COP` | `0x01` | 平衡板 -> 基站 | `17` | 50 Hz | 压力中心帧 | | `PROTO_TYPE_COP` | `0x01` | 平衡板 -> 基站 | `18` | 50 Hz | 压力中心帧 |
| `PROTO_TYPE_ENCODER` | `0x02` | 平衡板 -> 基站 | `13` | TBD | 电机编码器帧(预留) | | `PROTO_TYPE_ENCODER` | `0x02` | 平衡板 -> 基站 | `14` | TBD | 电机编码器帧(预留) |
| `PROTO_TYPE_RESISTANCE` | `0x10` | 基站 -> 平衡板 | `15` | 按需 | 阻力参数帧 | | `PROTO_TYPE_RESISTANCE` | `0x10` | 基站 -> 平衡板 | `16` | 按需 | 阻力参数帧 |
| `PROTO_TYPE_SPOTTER` | `0x11` | 基站 -> 平衡板 | `8` | 按需 | 保护模式帧 | | `PROTO_TYPE_SPOTTER` | `0x11` | 基站 -> 平衡板 | `9` | 按需 | 保护模式帧 |
| `PROTO_TYPE_HEARTBEAT` | `0x20` | 基站 -> 平衡板 | `4` | ~1 Hz | 心跳帧 | | `PROTO_TYPE_HEARTBEAT` | `0x20` | 基站 -> 平衡板 | `5` | ~1 Hz | 心跳帧 |
## 3. 上行数据包 ## 3. 上行数据包
### 3.1 压力中心帧 (CoP) ### 3.1 压力中心帧 (CoP)
CoP 帧固定 17 字节,结构如下: CoP 帧固定 18 字节,结构如下:
| 字节偏移 | 字段 | 类型 | 值/范围 | 说明 | | 字节偏移 | 字段 | 类型 | 值/范围 | 说明 |
|----------|------|------|---------|------| |----------|------|------|---------|------|
@@ -51,6 +53,7 @@ CoP 帧固定 17 字节,结构如下:
| `5-8` | `cop_x` | `float32 LE` | - | 压力中心 X 坐标 (mm) | | `5-8` | `cop_x` | `float32 LE` | - | 压力中心 X 坐标 (mm) |
| `9-12` | `cop_y` | `float32 LE` | - | 压力中心 Y 坐标 (mm) | | `9-12` | `cop_y` | `float32 LE` | - | 压力中心 Y 坐标 (mm) |
| `13-16` | `force` | `float32 LE` | - | 总压力 (N) | | `13-16` | `force` | `float32 LE` | - | 总压力 (N) |
| `17` | `crc` | `uint8` | - | CRC-8/MAXIM 校验 |
#### flags 位定义 #### flags 位定义
@@ -94,7 +97,7 @@ CoP_Y = (F0 * y0 + F1 * y1 + F2 * y2 + F3 * y3) / F_total
下面是一个 CoP 帧示例(CoP 有效): 下面是一个 CoP 帧示例(CoP 有效):
```text ```text
AA 55 01 2A 01 00 00 F0 41 00 00 48 42 00 80 BB 44 AA 55 01 2A 01 00 00 F0 41 00 00 48 42 00 80 BB 44 XX
``` ```
含义如下: 含义如下:
@@ -106,10 +109,11 @@ AA 55 01 2A 01 00 00 F0 41 00 00 48 42 00 80 BB 44
- `00 00 F0 41``cop_x = 30.0` mmLE float32 - `00 00 F0 41``cop_x = 30.0` mmLE float32
- `00 00 48 42``cop_y = 50.0` mmLE float32 - `00 00 48 42``cop_y = 50.0` mmLE float32
- `00 80 BB 44``force = 1500.0` NLE float32 - `00 80 BB 44``force = 1500.0` NLE float32
- `XX`CRC-8/MAXIM(对字节 `[2..16]` 计算)
### 3.2 电机编码器帧(预留) ### 3.2 电机编码器帧(预留)
编码器帧固定 13 字节,结构如下。该帧目前仅在协议中预留定义,固件暂不实现。 编码器帧固定 14 字节,结构如下。该帧目前仅在协议中预留定义,固件暂不实现。
| 字节偏移 | 字段 | 类型 | 值/范围 | 说明 | | 字节偏移 | 字段 | 类型 | 值/范围 | 说明 |
|----------|------|------|---------|------| |----------|------|------|---------|------|
@@ -120,12 +124,13 @@ AA 55 01 2A 01 00 00 F0 41 00 00 48 42 00 80 BB 44
| `4` | `flags` | `uint8` | `0` | 预留 | | `4` | `flags` | `uint8` | `0` | 预留 |
| `5-8` | `cable_length` | `float32 LE` | - | 拉索长度 (mm) | | `5-8` | `cable_length` | `float32 LE` | - | 拉索长度 (mm) |
| `9-12` | `velocity` | `float32 LE` | - | 拉索速度 (mm/s) | | `9-12` | `velocity` | `float32 LE` | - | 拉索速度 (mm/s) |
| `13` | `crc` | `uint8` | - | CRC-8/MAXIM 校验 |
## 4. 下行数据包 ## 4. 下行数据包
### 4.1 阻力参数帧 ### 4.1 阻力参数帧
阻力参数帧固定 15 字节,用于基站向平衡板下发电机阻力控制参数。 阻力参数帧固定 16 字节,用于基站向平衡板下发电机阻力控制参数。
| 字节偏移 | 字段 | 类型 | 值/范围 | 说明 | | 字节偏移 | 字段 | 类型 | 值/范围 | 说明 |
|----------|------|------|---------|------| |----------|------|------|---------|------|
@@ -135,11 +140,12 @@ AA 55 01 2A 01 00 00 F0 41 00 00 48 42 00 80 BB 44
| `3-6` | `K` | `float32 LE` | - | 刚度 (N/m) | | `3-6` | `K` | `float32 LE` | - | 刚度 (N/m) |
| `7-10` | `B` | `float32 LE` | - | 阻尼 (Ns/m) | | `7-10` | `B` | `float32 LE` | - | 阻尼 (Ns/m) |
| `11-14` | `Tau` | `float32 LE` | - | 时间常数 (s) | | `11-14` | `Tau` | `float32 LE` | - | 时间常数 (s) |
| `15` | `crc` | `uint8` | - | CRC-8/MAXIM 校验 |
#### 示例 #### 示例
```text ```text
AA 55 10 00 00 C8 42 00 00 48 41 CD CC 4C 3E AA 55 10 00 00 C8 42 00 00 48 41 CD CC 4C 3E XX
``` ```
含义如下: 含义如下:
@@ -148,10 +154,11 @@ AA 55 10 00 00 C8 42 00 00 48 41 CD CC 4C 3E
- `00 00 C8 42``K = 100.0` N/m - `00 00 C8 42``K = 100.0` N/m
- `00 00 48 41``B = 12.5` Ns/m - `00 00 48 41``B = 12.5` Ns/m
- `CD CC 4C 3E``Tau = 0.2` s - `CD CC 4C 3E``Tau = 0.2` s
- `XX`CRC-8/MAXIM
### 4.2 Spotter Mode 帧 ### 4.2 Spotter Mode 帧
Spotter 帧固定 8 字节,用于启用或关闭保护模式。 Spotter 帧固定 9 字节,用于启用或关闭保护模式。
| 字节偏移 | 字段 | 类型 | 值/范围 | 说明 | | 字节偏移 | 字段 | 类型 | 值/范围 | 说明 |
|----------|------|------|---------|------| |----------|------|------|---------|------|
@@ -160,18 +167,19 @@ Spotter 帧固定 8 字节,用于启用或关闭保护模式。
| `2` | `type` | `uint8` | `0x11` | Spotter 包类型 | | `2` | `type` | `uint8` | `0x11` | Spotter 包类型 |
| `3-6` | `threshold` | `float32 LE` | - | 保护力阈值 (N) | | `3-6` | `threshold` | `float32 LE` | - | 保护力阈值 (N) |
| `7` | `enable` | `uint8` | `0``1` | `0` = 关闭, `1` = 启用 | | `7` | `enable` | `uint8` | `0``1` | `0` = 关闭, `1` = 启用 |
| `8` | `crc` | `uint8` | - | CRC-8/MAXIM 校验 |
#### 示例 #### 示例
启用 Spotter Mode,阈值 500 N 启用 Spotter Mode,阈值 500 N
```text ```text
AA 55 11 00 00 FA 43 01 AA 55 11 00 00 FA 43 01 XX
``` ```
### 4.3 心跳帧 ### 4.3 心跳帧
心跳帧固定 4 字节,基站以约 1 Hz 频率发送,用于活性检测。 心跳帧固定 5 字节,基站以约 1 Hz 频率发送,用于活性检测。
| 字节偏移 | 字段 | 类型 | 值/范围 | 说明 | | 字节偏移 | 字段 | 类型 | 值/范围 | 说明 |
|----------|------|------|---------|------| |----------|------|------|---------|------|
@@ -179,18 +187,19 @@ AA 55 11 00 00 FA 43 01
| `1` | `sync1` | `uint8` | `0x55` | 帧头第 2 字节 | | `1` | `sync1` | `uint8` | `0x55` | 帧头第 2 字节 |
| `2` | `type` | `uint8` | `0x20` | 心跳包类型 | | `2` | `type` | `uint8` | `0x20` | 心跳包类型 |
| `3` | `counter` | `uint8` | `0-255` | 滚动计数 | | `3` | `counter` | `uint8` | `0-255` | 滚动计数 |
| `4` | `crc` | `uint8` | - | CRC-8/MAXIM 校验 |
#### 示例 #### 示例
```text ```text
AA 55 20 05 AA 55 20 05 XX
``` ```
含义:心跳包,计数 `5` 含义:心跳包,计数 `5``XX` 为 CRC
## 5. 当前实现约束 ## 5. 当前实现约束
- CoP 帧固定 17 字节,可在最小 ATT MTU 23(NUS 有效负载 20 字节)下传输。 - CoP 帧固定 18 字节,可在最小 ATT MTU 23(NUS 有效负载 20 字节)下传输。
- 50 Hz CoP 上行流隐式充当平衡板到基站的活性检测,因此心跳仅为下行。 - 50 Hz CoP 上行流隐式充当平衡板到基站的活性检测,因此心跳仅为下行。
- 传感器通道到物理位置的映射:MUX `{0x01, 0x23, 0x45, 0x67}` → FL/FR/BR/BL。 - 传感器通道到物理位置的映射:MUX `{0x01, 0x23, 0x45, 0x67}` → FL/FR/BR/BL。
- `BOARD_HALF_WIDTH_MM = 200``BOARD_HALF_LENGTH_MM = 200`(后续可能调整)。 - `BOARD_HALF_WIDTH_MM = 200``BOARD_HALF_LENGTH_MM = 200`(后续可能调整)。
+16 -10
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <zephyr/sys/crc.h>
/* ─── 帧常量 ─── */ /* ─── 帧常量 ─── */
/* 双帧头比单字节魔数更容易在连续字节流里完成包起始判定。 */ /* 双帧头比单字节魔数更容易在连续字节流里完成包起始判定。 */
@@ -41,9 +42,9 @@
/* 总重低于此值时 CoP 无意义(除零保护),单位 kg */ /* 总重低于此值时 CoP 无意义(除零保护),单位 kg */
#define COP_MIN_FORCE_THRESHOLD 0.5f #define COP_MIN_FORCE_THRESHOLD 0.5f
/* ─── 上行: CoP 帧 (17 bytes, 50 Hz) ─── /* ─── 上行: CoP 帧 (18 bytes, 50 Hz) ───
* *
* [AA 55 01 seq flags cop_x(4) cop_y(4) force(4)] * [AA 55 01 seq flags cop_x(4) cop_y(4) force(4) crc]
*/ */
struct cop_frame_t { struct cop_frame_t {
uint8_t sync0; /* = PROTO_SYNC0 */ uint8_t sync0; /* = PROTO_SYNC0 */
@@ -54,6 +55,7 @@ struct cop_frame_t {
float cop_x; /* 压力中心 X (mm),板面坐标系 */ float cop_x; /* 压力中心 X (mm),板面坐标系 */
float cop_y; /* 压力中心 Y (mm),板面坐标系 */ float cop_y; /* 压力中心 Y (mm),板面坐标系 */
float force; /* 总重量 (kg) */ float force; /* 总重量 (kg) */
uint8_t crc; /* CRC-8/MAXIM, 校验 type~force */
} __attribute__((packed)); } __attribute__((packed));
union cop_pkt_t { union cop_pkt_t {
@@ -61,9 +63,9 @@ union cop_pkt_t {
uint8_t bytes[sizeof(struct cop_frame_t)]; uint8_t bytes[sizeof(struct cop_frame_t)];
}; };
/* ─── 上行: 电机编码器帧 (13 bytes, 预留) ─── /* ─── 上行: 电机编码器帧 (14 bytes, 预留) ───
* *
* [AA 55 02 seq flags cable_length(4) velocity(4)] * [AA 55 02 seq flags cable_length(4) velocity(4) crc]
*/ */
struct encoder_frame_t { struct encoder_frame_t {
uint8_t sync0; /* = PROTO_SYNC0 */ uint8_t sync0; /* = PROTO_SYNC0 */
@@ -73,6 +75,7 @@ struct encoder_frame_t {
uint8_t flags; /* 预留,填 0 */ uint8_t flags; /* 预留,填 0 */
float cable_length; /* 拉索长度 (mm) */ float cable_length; /* 拉索长度 (mm) */
float velocity; /* 拉索速度 (mm/s) */ float velocity; /* 拉索速度 (mm/s) */
uint8_t crc; /* CRC-8/MAXIM */
} __attribute__((packed)); } __attribute__((packed));
union encoder_pkt_t { union encoder_pkt_t {
@@ -80,9 +83,9 @@ union encoder_pkt_t {
uint8_t bytes[sizeof(struct encoder_frame_t)]; uint8_t bytes[sizeof(struct encoder_frame_t)];
}; };
/* ─── 下行: 阻力参数帧 (15 bytes) ─── /* ─── 下行: 阻力参数帧 (16 bytes) ───
* *
* [AA 55 10 K(4) B(4) Tau(4)] * [AA 55 10 K(4) B(4) Tau(4) crc]
*/ */
struct resistance_frame_t { struct resistance_frame_t {
uint8_t sync0; /* = PROTO_SYNC0 */ uint8_t sync0; /* = PROTO_SYNC0 */
@@ -91,6 +94,7 @@ struct resistance_frame_t {
float K; /* 刚度 (N/m) */ float K; /* 刚度 (N/m) */
float B; /* 阻尼 (Ns/m) */ float B; /* 阻尼 (Ns/m) */
float Tau; /* 时间常数 (s) */ float Tau; /* 时间常数 (s) */
uint8_t crc; /* CRC-8/MAXIM */
} __attribute__((packed)); } __attribute__((packed));
union resistance_pkt_t { union resistance_pkt_t {
@@ -98,9 +102,9 @@ union resistance_pkt_t {
uint8_t bytes[sizeof(struct resistance_frame_t)]; uint8_t bytes[sizeof(struct resistance_frame_t)];
}; };
/* ─── 下行: Spotter Mode 帧 (8 bytes) ─── /* ─── 下行: Spotter Mode 帧 (9 bytes) ───
* *
* [AA 55 11 threshold(4) enable] * [AA 55 11 threshold(4) enable crc]
*/ */
struct spotter_frame_t { struct spotter_frame_t {
uint8_t sync0; /* = PROTO_SYNC0 */ uint8_t sync0; /* = PROTO_SYNC0 */
@@ -108,6 +112,7 @@ struct spotter_frame_t {
uint8_t type; /* = PROTO_TYPE_SPOTTER */ uint8_t type; /* = PROTO_TYPE_SPOTTER */
float threshold; /* 保护力阈值 (N) */ float threshold; /* 保护力阈值 (N) */
uint8_t enable; /* 0 = 关闭, 1 = 启用 */ uint8_t enable; /* 0 = 关闭, 1 = 启用 */
uint8_t crc; /* CRC-8/MAXIM */
} __attribute__((packed)); } __attribute__((packed));
union spotter_pkt_t { union spotter_pkt_t {
@@ -115,15 +120,16 @@ union spotter_pkt_t {
uint8_t bytes[sizeof(struct spotter_frame_t)]; uint8_t bytes[sizeof(struct spotter_frame_t)];
}; };
/* ─── 下行: 心跳帧 (4 bytes, ~1 Hz) ─── /* ─── 下行: 心跳帧 (5 bytes, ~1 Hz) ───
* *
* [AA 55 20 counter] * [AA 55 20 counter crc]
*/ */
struct heartbeat_frame_t { struct heartbeat_frame_t {
uint8_t sync0; /* = PROTO_SYNC0 */ uint8_t sync0; /* = PROTO_SYNC0 */
uint8_t sync1; /* = PROTO_SYNC1 */ uint8_t sync1; /* = PROTO_SYNC1 */
uint8_t type; /* = PROTO_TYPE_HEARTBEAT */ uint8_t type; /* = PROTO_TYPE_HEARTBEAT */
uint8_t counter; /* 滚动计数,用于活性检测 */ uint8_t counter; /* 滚动计数,用于活性检测 */
uint8_t crc; /* CRC-8/MAXIM */
} __attribute__((packed)); } __attribute__((packed));
union heartbeat_pkt_t { union heartbeat_pkt_t {
+2
View File
@@ -27,3 +27,5 @@ CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_BACKEND_UART=n CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_MODE_IMMEDIATE=y CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_MAIN_STACK_SIZE=4096 CONFIG_MAIN_STACK_SIZE=4096
CONFIG_CRC=y
+15
View File
@@ -78,6 +78,11 @@ static void nus_received_cb(struct bt_conn *conn, const uint8_t *data, uint16_t
switch (data[2]) { switch (data[2]) {
case PROTO_TYPE_RESISTANCE: case PROTO_TYPE_RESISTANCE:
if (len >= sizeof(struct resistance_frame_t)) { if (len >= sizeof(struct resistance_frame_t)) {
uint8_t crc = crc8(data + 2, sizeof(struct resistance_frame_t) - 3, 0x31, 0x00, true);
if (data[sizeof(struct resistance_frame_t) - 1] != crc) {
LOG_WRN("Resistance frame CRC mismatch");
break;
}
union resistance_pkt_t pkt; union resistance_pkt_t pkt;
memcpy(pkt.bytes, data, sizeof(struct resistance_frame_t)); memcpy(pkt.bytes, data, sizeof(struct resistance_frame_t));
resistance_K = pkt.frame.K; resistance_K = pkt.frame.K;
@@ -90,6 +95,11 @@ static void nus_received_cb(struct bt_conn *conn, const uint8_t *data, uint16_t
break; break;
case PROTO_TYPE_SPOTTER: case PROTO_TYPE_SPOTTER:
if (len >= sizeof(struct spotter_frame_t)) { if (len >= sizeof(struct spotter_frame_t)) {
uint8_t crc = crc8(data + 2, sizeof(struct spotter_frame_t) - 3, 0x31, 0x00, true);
if (data[sizeof(struct spotter_frame_t) - 1] != crc) {
LOG_WRN("Spotter frame CRC mismatch");
break;
}
union spotter_pkt_t pkt; union spotter_pkt_t pkt;
memcpy(pkt.bytes, data, sizeof(struct spotter_frame_t)); memcpy(pkt.bytes, data, sizeof(struct spotter_frame_t));
spotter_threshold = pkt.frame.threshold; spotter_threshold = pkt.frame.threshold;
@@ -99,6 +109,11 @@ static void nus_received_cb(struct bt_conn *conn, const uint8_t *data, uint16_t
break; break;
case PROTO_TYPE_HEARTBEAT: case PROTO_TYPE_HEARTBEAT:
if (len >= sizeof(struct heartbeat_frame_t)) { if (len >= sizeof(struct heartbeat_frame_t)) {
uint8_t crc = crc8(data + 2, sizeof(struct heartbeat_frame_t) - 3, 0x31, 0x00, true);
if (data[sizeof(struct heartbeat_frame_t) - 1] != crc) {
LOG_WRN("Heartbeat frame CRC mismatch");
break;
}
last_heartbeat_ms = k_uptime_get(); last_heartbeat_ms = k_uptime_get();
LOG_DBG("RX Heartbeat"); LOG_DBG("RX Heartbeat");
} }
+2 -3
View File
@@ -23,9 +23,8 @@ int main(void) {
ble_transport_adv_start(); ble_transport_adv_start();
if (sensor_init()) return 0; if (sensor_init()) return 0;
while (1) {
k_sleep(K_SECONDS(1)); k_sleep(K_FOREVER);
}
return 0; return 0;
} }
+1
View File
@@ -163,6 +163,7 @@ static void sensor_thread_fn(void *p1, void *p2, void *p3) {
pkt.frame.cop_x = cop_x; pkt.frame.cop_x = cop_x;
pkt.frame.cop_y = cop_y; pkt.frame.cop_y = cop_y;
pkt.frame.force = force; pkt.frame.force = force;
pkt.frame.crc = crc8(pkt.bytes + 2, sizeof(pkt.bytes) - 3, 0x31, 0x00, true);
/* 发送 */ /* 发送 */
ble_transport_send(pkt.bytes, sizeof(pkt.bytes)); ble_transport_send(pkt.bytes, sizeof(pkt.bytes));