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协议扩展
This commit is contained in:
小龙虾
2026-06-29 01:51:44 +08:00
parent a93dba60d2
commit 6335d05198
21 changed files with 1398 additions and 13 deletions
+192 -1
View File
@@ -1,4 +1,4 @@
/*
/*
* canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取)
*/
#include <string.h>
@@ -50,6 +50,18 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
return static_cast<int>(copied_len);
}
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
constexpr uint8_t kSpiIdSnapshotType = 0xC3U;
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
static inline uint32_t readU32Le(const uint8_t *p) {
return static_cast<uint32_t>(p[0])
| (static_cast<uint32_t>(p[1]) << 8)
| (static_cast<uint32_t>(p[2]) << 16)
| (static_cast<uint32_t>(p[3]) << 24);
}
} // namespace
/**
@@ -80,3 +92,182 @@ int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
return (sendCanFrameOverSpi(channel, ext_id, data, dlc, nullptr, 0U, 1000U) >= 0) ? 0 : -1;
}
/**
* @brief 拉取一路 CAN 通道的 1Hz 反馈 + ID 快照表(GD32 SPI 帧类型 0xC1,分页)。
* 连续拉取 page=0 和 page=1,合并为完整结果。
*/
int canFdPollFeedback(can_channel_t channel,
can_feedback_result_t *out,
uint32_t timeout_ms)
{
if (channel >= CAN_CHANNEL_MAX || out == nullptr) {
return -1;
}
out->valid = 0;
uint8_t entryIdx = 0U;
constexpr uint8_t kFixedHeader = 25U; /* ch+page+total+cnt+mode+seq+rx+tx+drop+err */
for (uint8_t page = 0U; page < 2U; ++page) {
uint8_t req[2] = { static_cast<uint8_t>(channel & CAN_CHANNEL_MASK), page };
uint8_t resp_buf[kFeedbackPayloadLen] = {};
uint8_t resp_type = 0U;
size_t resp_len = 0U;
esp_err_t ret = SpiMaster::instance().otaTransfer(
kSpiCanFeedbackType,
req, 2U,
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < kFixedHeader) {
return -1;
}
/* 第一页时解析固定头 */
if (page == 0U) {
out->stats.channel = resp_buf[0];
out->stats.mode = resp_buf[4];
out->stats.snapshot_seq = readU32Le(&resp_buf[5]);
out->stats.rx_count = readU32Le(&resp_buf[9]);
out->stats.tx_count = readU32Le(&resp_buf[13]);
out->stats.rx_drop_count = readU32Le(&resp_buf[17]);
out->stats.error_count = readU32Le(&resp_buf[21]);
out->total_count = resp_buf[2];
}
const uint8_t entryCount = resp_buf[3];
if (entryCount == 0U) break;
uint16_t offset = kFixedHeader;
for (uint8_t i = 0U; i < entryCount && entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
if (offset + 8U > resp_len) break;
id_snapshot_entry_t &e = out->entries[entryIdx];
e.id = readU32Le(&resp_buf[offset]);
e.dlc = resp_buf[offset + 4U];
const uint8_t flags = resp_buf[offset + 5U];
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
e.brs = (flags & 0x02U) ? 1U : 0U;
e.esi = (flags & 0x04U) ? 1U : 0U;
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
if (offset + entrySize > resp_len) break;
memcpy(e.data, &resp_buf[offset + 8U], dataLen);
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
e.data[d] = 0U;
}
offset += entrySize;
entryIdx++;
}
if (entryIdx >= out->total_count) break;
}
out->valid = 1;
return 0;
}
/**
* @brief 切换 GD32 三路 CAN 接收过滤模式(GD32 SPI 帧类型 0xC2)。
*/
/**
* @brief 拉取指定通道的不重复 ID 快照表(GD32 SPI 帧类型 0xC3)。
* 分页拉取,每页 8 条,最多拉 2 页(16 条)。
*/
int canFdPullIdSnapshot(can_channel_t channel,
id_snapshot_entry_t *out,
uint8_t *out_count,
uint32_t timeout_ms)
{
if (channel >= CAN_CHANNEL_MAX || out == nullptr || out_count == nullptr) {
return -1;
}
*out_count = 0U;
uint8_t total = 0U;
for (uint8_t page = 0U; page < 2U; ++page) {
uint8_t req[2] = { static_cast<uint8_t>(channel), page };
uint8_t resp_buf[kFeedbackPayloadLen] = {};
uint8_t resp_type = 0U;
size_t resp_len = 0U;
esp_err_t ret = SpiMaster::instance().otaTransfer(
kSpiIdSnapshotType,
req, 2U,
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 4U) {
return -1;
}
/* resp: [ch][page][total_count][entry_count][entries...] */
if (page == 0U) {
total = resp_buf[2];
}
const uint8_t entryCount = resp_buf[3];
if (entryCount == 0U) break;
uint16_t offset = 4U;
for (uint8_t i = 0U; i < entryCount && *out_count < CANFD_ID_SNAPSHOT_MAX; ++i) {
if (offset + 16U > resp_len) break;
id_snapshot_entry_t &e = out[*out_count];
e.id = readU32Le(&resp_buf[offset]);
e.seq = readU32Le(&resp_buf[offset + 4U]);
e.rx_count = readU32Le(&resp_buf[offset + 8U]);
e.dlc = resp_buf[offset + 12U];
const uint8_t flags = resp_buf[offset + 13U];
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
e.brs = (flags & 0x02U) ? 1U : 0U;
e.esi = (flags & 0x04U) ? 1U : 0U;
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
const uint16_t entrySize = 16U + dataLen;
if (offset + entrySize > resp_len) break;
memcpy(e.data, &resp_buf[offset + 16U], dataLen);
/* 清零剩余字节 */
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
e.data[d] = 0U;
}
offset += entrySize;
(*out_count)++;
}
if (*out_count >= total) break; /* 已拉完 */
}
return 0;
}
int canFdSetRxMode(can_rx_mode_t mode,
can_rx_mode_t *actual,
uint32_t timeout_ms)
{
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID) {
return -1;
}
uint8_t req = static_cast<uint8_t>(mode);
uint8_t resp_buf[1] = {0};
uint8_t resp_type = 0U;
size_t resp_len = 0U;
esp_err_t ret = SpiMaster::instance().otaTransfer(
kSpiCanSetModeType,
&req, 1U,
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 1U) {
return -1;
}
if (actual != nullptr) {
*actual = static_cast<can_rx_mode_t>(resp_buf[0]);
}
return 0;
}
+90 -1
View File
@@ -1,4 +1,4 @@
/*
/*
* canfd_bridge.h - CANFD 3通道通信桥接(M4: 从 SpiMaster 提取)
*
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责解耦。
@@ -61,4 +61,93 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc);
// ============================================================
// 不重复 ID 快照表(SPI 0xC3 协议配套)
// ============================================================
#define CANFD_ID_SNAPSHOT_MAX 16 ///< 每通道最大不重复 ID 条目数
#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数
/**
* @brief 单个不重复 ID 的最新帧快照条目。
*/
typedef struct {
uint32_t id;
uint32_t seq;
uint32_t rx_count;
uint8_t dlc;
uint8_t fd_flag;
uint8_t brs;
uint8_t esi;
uint8_t data[CANFD_ID_SNAPSHOT_DATA_MAX];
} id_snapshot_entry_t;
// ============================================================
// CAN 接收过滤模式 & 1Hz 反馈快照
// 与 GD32 DCU_APP SPI 协议帧 0xC1/0xC2 配套
// ============================================================
#define SPI_CAN_FEEDBACK_LAST_DATA_BYTES 93 ///< 反馈快照中最近一帧数据字节数(受 SPI 128B 应答帧约束(kTxBufSize=128 - overhead=5 - 前缀30 = 93))
typedef enum {
CAN_RX_MODE_ALL_PASS = 0, ///< 默认:禁用 ID 过滤,三路全部接收
CAN_RX_MODE_FILTER_ID = 1, ///< 按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧过滤
} can_rx_mode_t;
/**
* @brief 单通道 1Hz 反馈统计。
*/
typedef struct {
uint8_t channel;
uint8_t mode;
uint32_t snapshot_seq;
uint32_t rx_count;
uint32_t tx_count;
uint32_t rx_drop_count;
uint32_t error_count;
} can_channel_feedback_t;
/**
* @brief 单通道 0xC1 完整响应:统计 + ID 快照表。
*/
typedef struct {
can_channel_feedback_t stats;
id_snapshot_entry_t entries[CANFD_ID_SNAPSHOT_MAX];
uint8_t total_count; /**< 该通道实际 ID 条目总数 */
uint8_t valid; /**< 数据是否有效 */
} can_feedback_result_t;
/**
* @brief 拉取一路 CAN 通道的 1Hz 反馈快照。
* @param channel 目标通道 0/1/2。
* @param out 输出快照(不可为 nullptr)。
* @param timeout_ms SPI 事务超时。
* @return 0 成功,<0 失败(ERROR 应答、payload 长度不足或 SPI 错误)。
*/
int canFdPollFeedback(can_channel_t channel,
can_feedback_result_t *out,
uint32_t timeout_ms);
/**
* @brief 切换 GD32 三路 CAN 的接收过滤模式。
* @param mode 目标模式(0=全收,1=按 ID 过滤)。
* @param actual 可选输出,GD32 实际生效模式。
* @param timeout_ms SPI 事务超时。
* @return 0 成功,<0 失败。
*/
int canFdSetRxMode(can_rx_mode_t mode,
can_rx_mode_t *actual,
uint32_t timeout_ms);
/**
* @brief 拉取指定通道的不重复 ID 快照表(SPI 帧 0xC3,分页)。
* @param channel 目标通道 0/1/2。
* @param out 输出条目数组(至少 CANFD_ID_SNAPSHOT_MAX 个元素)。
* @param out_count 输出实际条目数。
* @param timeout_ms SPI 超时。
* @return 0 成功,<0 失败。
*/
int canFdPullIdSnapshot(can_channel_t channel,
id_snapshot_entry_t *out,
uint8_t *out_count,
uint32_t timeout_ms);
#endif // CANFD_BRIDGE_H
+1 -1
View File
@@ -452,7 +452,7 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
{
constexpr size_t kPollBytes = 96U;
constexpr size_t kPollBytes = 160U;
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
while (xTaskGetTickCount() <= deadline) {
+1 -1
View File
@@ -17,7 +17,7 @@
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
#define SPI_MODE (0) // CPOL=0, CPHA=0
#define SPI_QUEUE_SIZE (1) ///< SPI 事务队列深度
#define SPI_OTA_FRAME_MAX (96) ///< SPI OTA 单帧最大字节数
#define SPI_OTA_FRAME_MAX (133) ///< SPI OTA 单帧最大字节数
// GD32 SPI OTA 帧类型
#define SPI_OTA_SOF (0xA5)