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

7.6 KiB
Raw Blame History

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)

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 响应格式

{
  "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() 分页拉取