Files
ESP32_COMM/main/transport/canfd_bridge.cpp
T
小龙虾 6364eacfb1 feat(can): 优化批量发送性能并修复竞态问题
- 帧间添加5ms延时,暂停CAN反馈避免与批量SPI竞争导致偶发崩溃
- 单次批量容量从48帧提升至128帧,最大body大小从12KB增至48KB
- 单帧超时从400ms降至80ms,失败后不再拖慢整体速度
- 暂停0xC1反馈任务降低worker竞态和WDT风险
- 中途致命错误后跳过后续帧发送,避免每帧等待超时
- 添加详细的JSON错误响应格式
- 修复内存分配失败时的资源清理问题

refactor(wifi): 优化冷启动流程避免电源竞争

- 电源轨稳定等待策略调整:POWERON/BROWNOUT复位时等待200ms
- 仅BROWNOUT复位后清除PHY校准数据,避免每次上电全量校准
- 电源稳定等待期间等待400ms后再进行NVS/WiFi操作
- WiFi启动前延时从500ms调整为300ms
- BOD阈值适当放宽避免上电瞬间复位

perf(monitor): 调整堆内存监控策略

- 内存紧张时增大健康监控间隔:低于8KB时从3s增至10s
- 避免日志输出消耗更多堆内存造成雪崩效应

docs(readme): 更新冷启动优化说明

- 详细描述电源轨稳定等待机制
- 说明仅BROWNOUT后才强制PHY重校准的策略
- 解释避免与GD32抢电流的设计考虑

style(html): 更新前端资源版本号

- 更新CSS和JavaScript文件的版本参数
- 修复页面加载时的资源缓存问题

fix(ble): 修复阶段显示状态问题

- 终态阶段全部显示为完成状态(绿色)
- 优化状态栏样式更新逻辑

feat(ota): 优化SPI通信超时处理

- 将总超时拆分为发送和轮询两个阶段
- 禁止轮询单独占用500ms避免短超时批量卡顿
- 优化极短超时情况下的时间分配策略

refactor(can): 减少批量操作时的日志输出

- SPI失败和GD32拒绝时按类型聚合日志
- 每32次相同错误才输出一次完整日志
- 避免大量错误时UDP/格式化耗尽堆内存
2026-07-17 00:24:09 +08:00

290 lines
10 KiB
C++
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.
/*
* canfd_bridge.cpp - CANFD 3通道通信桥接实现
*
* SPI 业务类型:
* 0xC0 发 CAN 帧(canFdSend)
* 0xC1 反馈页解析(canFdParseFeedbackPage;由 GD32 主动推送,本文件不轮询)
* 0xC2 设 RX 模式(canFdSetRxMode)
*
* 0xC1 线格式与 GD32 app_spi_cmd_can_feedback.cpp 对齐:
* 固定头 28B + 条目 id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)
* 业务 payload 上限 256 - V2_OVERHEAD(20) = 236
*/
#include <string.h>
#include "canfd_bridge.h"
#include "spi_master.h"
#include "monitor/debug_log.h"
namespace {
constexpr uint8_t kSpiCanFrameType = 0xC0U;
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
constexpr uint8_t kFeedbackFixedHeader = 28U;
constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */
/**
* @brief 经 SPI 发送一帧 CAN,并解析 ACK/ERROR。
*
* 两步法:transact 发命令(丢弃延迟一拍陈旧响应)+ poll 真实响应。
* CAN 帧非幂等,命令只发一次,不重发。
*
* @return >=0 ACK 时响应负载长度;-2 SPI 失败;-3 GD32 ERROR。
*/
static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms,
uint8_t *gd32_err_out)
{
uint8_t tx_buf[72];
size_t tx_len = 0;
tx_buf[tx_len++] = static_cast<uint8_t>(channel & CAN_CHANNEL_MASK);
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 24) & 0xFFU);
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 16) & 0xFFU);
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 8) & 0xFFU);
tx_buf[tx_len++] = static_cast<uint8_t>(ext_id & 0xFFU);
if (dlc > 0U && data != nullptr) {
memcpy(&tx_buf[tx_len], data, dlc);
tx_len += dlc;
}
/* 独立缓冲收响应:调用方 recv_buf 为空时也能拿到 ERROR 细分码。 */
uint8_t local_resp[72];
uint8_t *spi_rx = (recv_buf != nullptr && recv_max > 0U) ? recv_buf : local_resp;
const size_t spi_rx_max = (recv_buf != nullptr && recv_max > 0U)
? static_cast<size_t>(recv_max)
: sizeof(local_resp);
uint8_t resp_type = 0U;
size_t resp_len = 0U;
esp_err_t ret = SpiMaster::instance().transact(
kSpiCanFrameType,
tx_buf, tx_len,
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
if (ret != ESP_OK) {
/* 批量失败时勿每帧打日志:UDP/格式化会把堆打穿 */
static uint32_t s_spi_fail_n = 0U;
static int s_last_spi_err = 0;
const int err_i = static_cast<int>(ret);
if (s_spi_fail_n == 0U || err_i != s_last_spi_err || (s_spi_fail_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s (x%lu)",
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret),
(unsigned long)(s_spi_fail_n + 1U));
}
s_last_spi_err = err_i;
++s_spi_fail_n;
if (gd32_err_out) {
*gd32_err_out = 0U;
}
return -2;
}
if (resp_type == SPI_OTA_RSP_ACK) {
if (gd32_err_out) {
*gd32_err_out = 0U;
}
if (recv_buf == nullptr || recv_max == 0U) {
return static_cast<int>(resp_len);
}
const size_t copied_len = (resp_len < recv_max) ? resp_len : recv_max;
return static_cast<int>(copied_len);
}
/* ERROR:细分码在 payload[0];不依赖调用方是否提供 recv_buf */
const uint8_t gd32_err = (resp_len > 0U) ? spi_rx[0] : 0xFFU;
if (gd32_err_out) {
*gd32_err_out = gd32_err;
}
{
static uint32_t s_rej_n = 0U;
static uint8_t s_last_rej = 0xFFU;
if (s_rej_n == 0U || gd32_err != s_last_rej || (s_rej_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X (x%lu)",
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err,
(unsigned long)(s_rej_n + 1U));
}
s_last_rej = gd32_err;
++s_rej_n;
}
return -3;
}
/** @brief 小端读 uint32(0xC1 固定头/条目 id)。 */
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
/**
* @brief 发送一帧 CANFD 数据,可选等待并读取响应。
*
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
* dlc==0 时允许 data 为 nullptr(仅发 ID 的远程帧/空数据帧)。
*/
int canFdSend(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms,
uint8_t *gd32_err_out)
{
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC) {
if (gd32_err_out) {
*gd32_err_out = 0U;
}
return -1;
}
/* dlc>0 时 data 必填;dlc==0 允许空指针 */
if (dlc > 0U && data == nullptr) {
if (gd32_err_out) {
*gd32_err_out = 0U;
}
return -1;
}
return sendCanFrameOverSpi(channel, ext_id, data, dlc,
recv_buf, recv_max, timeout_ms, gd32_err_out);
}
/**
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
*
* page=0 → 清空 entries、重置 entryIdx,填充 28 字节固定头 + 首批条目;
* page=1 → 校验 channel 一致后在已有条目后追加。
* 调用方按通道维护 entryIdx。
*
* @return 0 成功;-1 参数/页号/通道/截断导致无法解析。
*/
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
can_feedback_result_t *out, uint8_t *entryIdx)
{
if (payload == nullptr || out == nullptr || entryIdx == nullptr) {
return -1;
}
if (len < kFeedbackFixedHeader) {
return -1;
}
const uint8_t channel = payload[0];
const uint8_t page = payload[1];
if (channel >= CAN_CHANNEL_MAX) {
return -1;
}
/* GD32 仅 page 0/1;非法页拒绝,避免当追加污染缓冲 */
if (page > kFeedbackMaxPage) {
return -1;
}
if (page == 0U) {
/* 防 total 变少时 HTTP 读到上一轮残留 ID */
*entryIdx = 0U;
memset(out->entries, 0, sizeof(out->entries));
out->stats.channel = channel;
out->stats.mode = payload[4];
out->stats.snapshot_seq = readU32Le(&payload[5]);
out->stats.rx_count = readU32Le(&payload[9]);
out->stats.tx_count = readU32Le(&payload[13]);
out->stats.rx_drop_count = readU32Le(&payload[17]);
out->stats.error_count = readU32Le(&payload[21]);
out->stats.tec = payload[25];
out->stats.rec = payload[26];
out->stats.bus_state = payload[27];
out->total_count = payload[2];
} else {
/* page>=1:必须与 page0 已建立的通道一致 */
if (out->stats.channel != channel) {
return -1;
}
}
const uint8_t entryCount = payload[3];
const uint8_t idx_before = *entryIdx;
uint16_t offset = kFeedbackFixedHeader;
for (uint8_t i = 0U; i < entryCount && *entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
if (offset + 8U > len) {
break;
}
id_snapshot_entry_t &e = out->entries[*entryIdx];
e.id = readU32Le(&payload[offset]);
e.dlc = payload[offset + 4U];
const uint8_t flags = payload[offset + 5U];
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
e.brs = (flags & 0x02U) ? 1U : 0U;
e.esi = (flags & 0x04U) ? 1U : 0U;
/* 线协议不携带 per-ID seq/rx_count,显式置 0 避免脏读 */
e.seq = 0U;
e.rx_count = 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 > len) {
break;
}
memcpy(e.data, &payload[offset + 8U], dataLen);
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
e.data[d] = 0U;
}
offset += entrySize;
(*entryIdx)++;
}
/* 声明了条目但一个都没完整解析 → 截断/坏帧 */
if (entryCount > 0U && *entryIdx == idx_before) {
return -1;
}
out->valid = 1;
return 0;
}
/**
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
* @return 0 成功;-1 参数非法或 SPI/应答失败。
*/
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
&& mode != CAN_RX_MODE_BRIDGE_0_TO_12) {
DEBUG_WARN("CAN", "SET_MODE 非法 mode=%u", (unsigned)mode);
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;
/* 两步法:发命令一次 + poll 真实响应。0xC2 幂等,但避免无效重试。 */
esp_err_t ret = SpiMaster::instance().transact(
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) {
if (actual != nullptr) {
*actual = static_cast<can_rx_mode_t>(resp_buf[0]);
}
return 0;
}
DEBUG_WARN("CAN", "SET_MODE 失败: want=%u spi=%s resp=0x%02X len=%u",
(unsigned)mode,
esp_err_to_name(ret),
(unsigned)resp_type,
(unsigned)resp_len);
return -1;
}