Files
ESP32_COMM/docs/can_protocol.md
T
小龙虾 6335d05198 feat(main): 添加CAN 1Hz反馈轮询任务和HTTP接口
- 新增monitor/can_feedback_task.cpp组件,实现CAN 1Hz反馈轮询功能
- 在main.cpp中注册CAN反馈轮询任务,依赖SPI初始化完成后启动
- 在OTA协调器和中继模块中集成CAN反馈任务暂停/恢复机制
- 扩展CANFD桥接层,添加0xC1反馈查询、0xC2模式切换、0xC3ID快照协议支持
- 新增HTTP接口/can/feedback提供三路CAN反馈快照数据
- 新增HTTP接口/can/mode支持查询和切换CAN接收过滤模式
- 更新版本号从0.3.34到0.35
- 扩大SPI缓冲区以支持更大帧传输需求

BREAKING CHANGE: 新增CAN反馈监控功能需要GD32固件支持0xC1/0xC2/0xC3协议扩展
2026-06-29 01:51:44 +08:00

297 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CAN 通信协议文档
> 更新日期:2026-06-29
> 涉及固件:GD32 DCU_APP + ESP32 ESP32_COMM
> SPI 时钟:4 MHz | 应答帧最大 payload:123 字节(kTxBufSize=128 - overhead=5)
---
## 1. 概述
ESP32 通过 SPI 协议与 GD32 通信,实现三路 CAN/CANFD 的收发控制。CAN 相关 SPI 命令占用 `0xC0` ~ `0xC2` 帧类型。
| 帧类型 | 名称 | 方向 | 用途 |
|--------|------|------|------|
| `0xC0` | kTypeCanSend | ESP32 → GD32 | 发送一帧 CAN 数据 |
| `0xC1` | kTypeCanFeedback | ESP32 → GD32 | 拉取不重复 ID 快照表(分页) |
| `0xC2` | kTypeCanSetMode | ESP32 → GD32 | 切换三路 CAN 接收过滤模式 |
---
## 2. SPI 协议帧格式
所有 SPI 帧遵循统一格式:
```
[SOF:1B][TYPE:1B][LEN_LO:1B][LEN_HI:1B][PAYLOAD:NB][CRC8:1B]
```
- SOF = `0xA5`
- LEN = payload 长度(小端 16 位)
- CRC8 = 对 SOF+TYPE+LEN+PAYLOAD 的校验
应答帧 TYPE:
- `0x80` = ACK(成功)
- `0x81` = WAIT(OTA 中间态)
- `0x82` = ERROR
---
## 3. 0xC0 — CAN 发送
### 请求 payload
```
[0] channel 0/1/2
[1..4] ext_id 扩展帧 ID(Big-Endian,29 位)
[5..N] data CAN 数据负载(1~64 字节)
```
### 应答 payload
```
[0] 0x00(ACK)或错误码
```
---
## 4. 0xC1 — 不重复 ID 快照表(核心协议)
每秒 ESP32 对每通道发 2 次请求(page=0 和 page=1),拉取该通道所有**不重复 CAN ID** 的最新帧快照。
### 请求 payload
```
[0] channel 0/1/2
[1] page 页号:0 = 条目 0~7,1 = 条目 8~15
```
### 应答 payload(变长,最大 123 字节)
```
[0] channel 回显通道号
[1] page 回显页号
[2] total_count 该通道实际 ID 条目总数(0~16)
[3] entry_count 本页实际条目数
[4] mode 当前 CAN 接收模式(0=全收, 1=过滤)
[5..8] snapshot_seq 快照序号(LE,每秒 +1)
[9..12] rx_count 通道累计接收帧数(LE)
[13..16] tx_count 通道累计发送帧数(LE)
[17..20] rx_drop_count 通道累计丢帧(LE)
[21..24] error_count 通道累计错误(LE)
[25..] 条目逐条拼接(见下)
```
### 单条目格式(变长)
```
[+0..+3] id CAN 报文 ID(LE)
[+4] dlc 数据长度(1~64)
[+5] flags bit0=FD, bit1=BRS, bit2=ESI
[+6..+7] reserved 保留(0x00)
[+8..+N] data 报文数据(dlc 字节)
```
条目总大小 = 8 + dlc 字节。
- 标准 CAN(dlc=8):14 字节/条 → 每页最多 8 条
- CANFD(dlc=64):72 字节/条 → 每页最多 1 条
### 拉取时序
```
ESP32 GD32
|--- 0xC1 [ch=0, page=0] --->|
|<-- ACK [统计+条目0~7] -----|
|--- 0xC1 [ch=0, page=1] --->|
|<-- ACK [条目8~15] ---------|
|--- 0xC1 [ch=1, page=0] --->|
|<-- ACK [统计+条目0~7] -----|
|--- 0xC1 [ch=1, page=1] --->|
|<-- ACK [条目8~15] ---------|
|--- 0xC1 [ch=2, page=0] --->|
|<-- ACK [统计+条目0~7] -----|
|--- 0xC1 [ch=2, page=1] --->|
|<-- ACK [条目8~15] ---------|
| (1 秒后重复)
```
每轮 6 次 SPI 事务,SPI 总线占用 ~5ms(0.5%)。
### total_count 与 entry_count 的关系
- `total_count` = 该通道当前所有不重复 ID 的总数(0~16)
- `entry_count` = 本页实际返回的条目数(≤8)
- 当 `total_count ≤ 8` 时,page=1 的 `entry_count=0`,ESP32 不再拉第二页
- 当 `total_count = 0` 时,表示该通道未收到任何 CAN 帧
---
## 5. 0xC2 — 切换接收过滤模式
### 请求 payload
```
[0] mode 0 = 全部接收(kModeAllPass)
1 = 按 ID 过滤(kModeFilterId)
```
### 应答 payload
```
[0] actual_mode GD32 实际生效的模式
```
模式切换影响三路 CAN 通道(全局生效,不是单通道)。
---
## 6. GD32 端实现要点
### 不重复 ID 快照表(canfd.h)
```cpp
struct IdSnapshotEntry {
uint32_t id;
uint32_t seq; // 更新序号
uint32_t rx_count; // 该 ID 累计接收
uint8_t dlc, fd_flag, brs, esi;
uint8_t data[64];
bool occupied;
};
struct IdSnapshotTable {
IdSnapshotEntry entries[16]; // CANFD_ID_SNAPSHOT_MAX
uint8_t count;
uint32_t seq;
};
```
### 中断更新逻辑(canfd.cpp::handleRxInterrupt)
每收到一帧 CAN:
1. 遍历 `s_idSnapshot[channel].entries[0..count-1]`,查找 ID 匹配的条目
2. 找到 → 更新 data/dlc/flags/rx_count++
3. 未找到且 count < 16 → 新建条目
4. 未找到且 count = 16 → FIFO 淘汰最旧条目(index 0),整体左移,新条目插入末尾
### SPI 处理器(app_spi_cmd_can_feedback.cpp)
- `kFeedbackPayloadLen = 123`(kTxBufSize 128 - overhead 5)
- 固定头 25 字节 + 变长条目逐条拼接
- 条目不足时 `entry_count < kPageEntries`,ESP32 侧据此判断是否需要拉下一页
---
## 7. ESP32 端实现要点
### 数据流
```
GD32 CAN 中断
→ s_idSnapshot[ch].entries[匹配ID]
↓ (1Hz 快照刷新)
refreshFeedbackSnapshot → seq++
↓ (ESP32 1Hz 轮询)
0xC1 × 3通道 × 2页 = 6次 SPI/s
↓
can_feedback_task 缓存到 s_latest[ch]
↓
/can/feedback HTTP → JSON
↓
can_monitor.js 表格渲染
```
### SPI 轮询(can_feedback_task.cpp)
- 固定 1Hz 循环,每通道发 2 次 0xC1(page=0 和 page=1)
- OTA 进行中时暂停(`s_paused` 标志,由 OtaCoordinator 和 OtaRelay 设置)
- 结果缓存在 `s_latest[3]`(`can_feedback_result_t`),线程安全(互斥锁)
### HTTP 接口
| 端点 | 方法 | 用途 |
|------|------|------|
| `/can/feedback` | GET | 三路 CAN 通道统计 + ID 快照表 |
| `/can/mode` | GET | 查询当前接收模式 |
| `/can/mode` | POST | 切换模式 `{"mode":0}` 或 `{"mode":1}` |
| `/can/send` | POST | 发送一帧 CAN 数据 |
### /can/feedback 响应格式
```json
{
"channels": [
{
"ch": 0,
"valid": true,
"mode": 0,
"seq": 1234,
"rx": 5678,
"tx": 0,
"drop": 0,
"err": 0,
"count": 5,
"entries": [
{
"id": "0x18DA00F1",
"dlc": 8,
"fd": 0,
"brs": 0,
"esi": 0,
"data": "0102030405060708"
}
]
},
{ "ch": 1, "valid": false },
{ "ch": 2, "valid": false }
]
}
```
---
## 8. 前端(tools/can_monitor.html)
### 页面结构
1. **顶部状态栏**:连接状态、当前模式、轮询状态
2. **三路通道面板**(CH0/CH1/CH2):
- 状态(活跃/无帧)
- rx/tx/drop/err 统计
- seq(活性指示)
- 当前 ID 数量
3. **每 ID 快照表**:
- 通道下拉选择
- 每行一个不重复 ID
- 列:CAN ID | DLC | FD | BRS | ESI | 数据 (HEX)
- 1Hz 自动刷新
### 导航
- OTA 控制台 (`index.html`) → "CAN 接收监控" 按钮
- CAN 发送控制台 (`can.html`) → header "CAN 接收监控" 链接
- CAN 接收监控 (`can_monitor.html`) → header 可跳回 CAN 发送或 OTA 控制台
---
## 9. 资源消耗
| 资源 | 消耗 |
|------|------|
| GD32 SRAM(快照表) | 3.8KB / 128KB = 3% |
| GD32 中断开销(16 条遍历) | < 1μs |
| SPI 事务/秒 | 6 次(3 通道 × 2 页) |
| SPI 总线占用 | ~5ms/s = 0.5% |
| ESP32 RAM(缓存) | ~4KB |
| OTA 期间 CAN 轮询 | 自动暂停 |
---
## 10. 已废弃
| 项目 | 状态 | 说明 |
|------|------|------|
| `0xC3 kTypeIdSnapshot` | 保留定义但不使用 | 功能已合并到 0xC1 |
| `/can/idsnap` | 已移除 | 数据由 `/can/feedback` 统一提供 |
| `canFdPullIdSnapshot()` | 已移除 | 改用 `canFdPollFeedback()` 分页拉取 |