feat(can): 新增CAN通道收发器Silent模式功能
- 新增SPI 0xC8命令用于切换CAN通道收发器Silent模式(只听不发) - 在canFdSetTxSilent函数中实现静默模式控制逻辑 - 在HTTP API中添加POST /can/silent端点 - 更新CAN协议文档,记录新命令和错误码E6 - 修改bus_state位图,添加bit5表示静默状态 fix(can): 优化CAN反馈任务内存使用和链路离线处理 - 将任务栈上的临时缓冲区改为静态分配,防止栈溢出 - 添加链路在线检测,在离线状态下进入休眠退避 - 修复queryMotorStatePages和queryAdcFeedback函数返回值类型 - 优化UDP日志发送的冷却退避机制 chore(config): 更新调试日志配置和版本信息 - 在VSCode设置中添加ESP-IDF串口配置 - 添加SPI调试日志级别编译期开关CONFIG_SPI_DEBUG_LOG_LEVEL - 更新固件版本至0.5.2,记录相关变更内容 - 调整SPI和CAN反馈任务的栈大小配置
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* 0xC0 发 CAN 帧(canFdSend)
|
||||
* 0xC1 反馈页解析(canFdParseFeedbackPage;由 GD32 主动推送,本文件不轮询)
|
||||
* 0xC2 设 RX 模式(canFdSetRxMode)
|
||||
* 0xC8 设通道收发器 Silent(canFdSetTxSilent,只听不发)
|
||||
*
|
||||
* 0xC1 线格式与 GD32 app_spi_cmd_can_feedback.cpp 对齐:
|
||||
* 固定头 28B + 条目 id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)
|
||||
@@ -22,6 +23,7 @@ namespace {
|
||||
constexpr uint8_t kSpiCanFrameType = 0xC0U;
|
||||
constexpr uint8_t kSpiCanBurstType = 0xC7U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr uint8_t kSpiCanSetSilentType = 0xC8U;
|
||||
constexpr uint8_t kFeedbackFixedHeader = 28U;
|
||||
constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */
|
||||
|
||||
@@ -403,8 +405,13 @@ int canFdQueryFeedback(uint8_t channel, uint8_t page,
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s",
|
||||
(unsigned)channel, (unsigned)page, esp_err_to_name(ret));
|
||||
static TickType_t s_lastFailWarnTick = 0;
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (s_lastFailWarnTick == 0 || (now_tick - s_lastFailWarnTick) >= pdMS_TO_TICKS(5000)) {
|
||||
s_lastFailWarnTick = now_tick;
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s (限频5s)",
|
||||
(unsigned)channel, (unsigned)page, esp_err_to_name(ret));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (resp_type == SPI_OTA_RSP_ERROR) {
|
||||
@@ -489,3 +496,52 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
(unsigned)resp_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换指定 CAN 通道收发器 Silent(SPI 0xC8):只听不发。
|
||||
* @return 0 成功;-1 参数非法或 SPI/应答失败;-2 GD32 返回 ERROR。
|
||||
* @note 与 canFdSetRxMode 同款单步事务:GD32 在 CS EXTI ISR 内直接回 ACK/ERROR,
|
||||
* 不会回 WAIT(区别于 0xC0 那条要排空 OTA 残留 WAIT 的路径)。
|
||||
* ACK payload = [0]channel [1]实际 silent [2]通道使能位。
|
||||
*/
|
||||
int canFdSetTxSilent(uint8_t channel,
|
||||
bool silent,
|
||||
uint8_t *actual,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (channel > 2U) {
|
||||
DEBUG_WARN("CAN", "SET_SILENT 非法 ch=%u", (unsigned)channel);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t req[2] = { channel, static_cast<uint8_t>(silent ? 1U : 0U) };
|
||||
uint8_t resp_buf[3] = {0};
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiCanSetSilentType,
|
||||
req, sizeof(req),
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||
|
||||
if (ret == ESP_OK && resp_type == SPI_OTA_RSP_ACK && resp_len >= 2U) {
|
||||
if (actual != nullptr) {
|
||||
*actual = resp_buf[1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ret == ESP_OK && resp_type == SPI_OTA_RSP_ERROR) {
|
||||
DEBUG_WARN("CAN", "SET_SILENT ch=%u want=%u GD32 err=0x%02X",
|
||||
(unsigned)channel, (unsigned)(silent ? 1U : 0U),
|
||||
(resp_len >= 1U) ? (unsigned)resp_buf[0] : 0U);
|
||||
return -2;
|
||||
}
|
||||
|
||||
DEBUG_WARN("CAN", "SET_SILENT ch=%u want=%u %s r=0x%02X n=%u",
|
||||
(unsigned)channel, (unsigned)(silent ? 1U : 0U),
|
||||
esp_err_to_name(ret),
|
||||
(unsigned)resp_type,
|
||||
(unsigned)resp_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ typedef struct {
|
||||
* @param recv_buf 接收数据缓冲区(可为 nullptr)。
|
||||
* @param recv_max 接收缓冲区最大长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5);
|
||||
* @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5/0xE6=通道静默拒发);
|
||||
* 仅在返回 -3 时有效,其它情况清零。可为 nullptr。
|
||||
* @return 成功时返回接收数据长度,失败返回 -1/-2/-3。
|
||||
*/
|
||||
@@ -137,7 +137,7 @@ typedef struct {
|
||||
uint32_t error_count;
|
||||
uint8_t tec; /**< 硬件发送错误计数器 (0~255) */
|
||||
uint8_t rec; /**< 硬件接收错误计数器 (0~255) */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init,bit1=sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init,bit1=控制器sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning,bit5=tx_silent(收发器Silent,只听不发) */
|
||||
} can_channel_feedback_t;
|
||||
|
||||
/**
|
||||
@@ -196,4 +196,20 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 切换指定 CAN 通道收发器的 Silent 模式(SPI 0xC8):只听不发。
|
||||
* GD32 侧把该通道 TCAN1057AV 的 S 脚置高 → 驱动器关断、**接收机照常工作**
|
||||
* (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与 0xC1 上报不受影响。
|
||||
* 静默期间该通道所有发送(0xC0/0xC7/桥接/relay)回 0xE6=kErrSilent,不计错误。
|
||||
* @param channel 目标通道 0/1/2。
|
||||
* @param silent true=进 Silent,false=回 Normal。
|
||||
* @param actual 可选输出,GD32 实际生效状态(1=Silent,与其发送门控同一个值)。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败(-1 参数/SPI 失败,-2 GD32 返回 ERROR)。
|
||||
*/
|
||||
int canFdSetTxSilent(uint8_t channel,
|
||||
bool silent,
|
||||
uint8_t *actual,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
#endif // CANFD_BRIDGE_H
|
||||
|
||||
@@ -28,6 +28,32 @@
|
||||
#include "app_config.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
/* ============================================================
|
||||
* SPI 调试日志编译期门(app_config.h: CONFIG_SPI_DEBUG_LOG_LEVEL)
|
||||
*
|
||||
* 本文件内 DEBUG_xxx("SPI", ...) 的级别若高于门限,展开为不执行的空语句:
|
||||
* 用 `if (false)` 保留对实参的引用,避免"变量只用于日志"触发
|
||||
* -Wunused/-Werror,同时运行时不格式化、不写 UART/UDP/环。
|
||||
* 恢复 SPI 联调明细(与改动前完全一致):把 CONFIG_SPI_DEBUG_LOG_LEVEL 改回 3。
|
||||
* ============================================================ */
|
||||
#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 4)
|
||||
#define SPI_LOG_MUTED(tag, fmt, ...) \
|
||||
do { if (false) DebugLog::instance().log(DEBUG_LOG_DBG, (tag), (fmt), ##__VA_ARGS__); } while (false)
|
||||
#undef DEBUG_DBG
|
||||
#define DEBUG_DBG(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__)
|
||||
#endif
|
||||
#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 3)
|
||||
#undef DEBUG_INFO
|
||||
#define DEBUG_INFO(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__)
|
||||
#endif
|
||||
#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 2)
|
||||
#undef DEBUG_WARN
|
||||
#define DEBUG_WARN(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__)
|
||||
#endif
|
||||
#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 1)
|
||||
#undef DEBUG_ERR
|
||||
#define DEBUG_ERR(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
|
||||
@@ -391,6 +417,8 @@ esp_err_t SpiMaster::serviceRingOnce()
|
||||
m_stats.ring_steal_last_p1 = (found_len > 1U) ? found_payload[1] : 0U; /* 0.4.8 */
|
||||
m_stats.ring_steal_last_p2 = (found_len > 2U) ? found_payload[2] : 0U;
|
||||
m_stats.ring_steal_last_p3 = (found_len > 3U) ? found_payload[3] : 0U;
|
||||
m_stats.ring_steal_last_p4 = (found_len > 4U) ? found_payload[4] : 0U;
|
||||
m_stats.ring_steal_last_p5 = (found_len > 5U) ? found_payload[5] : 0U;
|
||||
static uint32_t s_steal_logs = 0U;
|
||||
if ((++s_steal_logs % 16U) == 1U) {
|
||||
DEBUG_WARN("SPI", "serviceRing 偷读迟到 RESPONSE t=0x%02X p0=0x%02X "
|
||||
@@ -641,6 +669,13 @@ esp_err_t SpiMaster::submitJob(Job &job)
|
||||
/* 尚未执行:标 abandoned,worker dequeue 时跳过 executeJob——
|
||||
* 本函数即将返回,out_* 将指向已消亡栈帧,再执行即 use-after-return。 */
|
||||
pool_job->abandoned = true;
|
||||
/* ★ 立即清除输出缓冲指针,彻底杜绝 worker 迟到执行时踩坏已消亡栈帧 */
|
||||
pool_job->out_type = nullptr;
|
||||
pool_job->out_payload = nullptr;
|
||||
pool_job->out_len = nullptr;
|
||||
pool_job->out_uptime = nullptr;
|
||||
pool_job->out_version = nullptr;
|
||||
pool_job->out_init_status = nullptr;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
|
||||
@@ -676,6 +711,13 @@ esp_err_t SpiMaster::submitJob(Job &job)
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
if (!pool_job->finished) {
|
||||
pool_job->abandoned = true;
|
||||
/* ★ 立即清除输出缓冲指针,杜绝写入消亡栈帧 */
|
||||
pool_job->out_type = nullptr;
|
||||
pool_job->out_payload = nullptr;
|
||||
pool_job->out_len = nullptr;
|
||||
pool_job->out_uptime = nullptr;
|
||||
pool_job->out_version = nullptr;
|
||||
pool_job->out_init_status = nullptr;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
@@ -1651,6 +1693,8 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
m_stats.rsp_err_last_p1 = (*resp_len > 1U) ? resp_payload[1] : 0U;
|
||||
m_stats.rsp_err_last_p2 = (*resp_len > 2U) ? resp_payload[2] : 0U;
|
||||
m_stats.rsp_err_last_p3 = (*resp_len > 3U) ? resp_payload[3] : 0U;
|
||||
m_stats.rsp_err_last_p4 = (*resp_len > 4U) ? resp_payload[4] : 0U;
|
||||
m_stats.rsp_err_last_p5 = (*resp_len > 5U) ? resp_payload[5] : 0U;
|
||||
}
|
||||
m_linkState.exitBusy();
|
||||
return ESP_OK;
|
||||
@@ -1661,15 +1705,65 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
|
||||
m_linkState.exitBusy();
|
||||
m_stats.ready_timeouts++;
|
||||
/* 短切片超时是 OTA 正常紧循环,勿 WARN 刷爆 ring/网页 */
|
||||
/* 短切片超时是 OTA 正常紧循环,勿 WARN 刷爆 ring/网页;限频 5 秒避免 SPI 不通时日志风暴耗尽堆栈 */
|
||||
if (timeout_ms >= 200U) {
|
||||
DEBUG_WARN("SPI", "OTA response timeout: %lu ms", (unsigned long)timeout_ms);
|
||||
static TickType_t s_lastTimeoutWarnTick = 0;
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (s_lastTimeoutWarnTick == 0 ||
|
||||
(now_tick - s_lastTimeoutWarnTick) >= pdMS_TO_TICKS(5000)) {
|
||||
s_lastTimeoutWarnTick = now_tick;
|
||||
DEBUG_WARN("SPI", "OTA response timeout: %lu ms (限频5s)", (unsigned long)timeout_ms);
|
||||
}
|
||||
} else {
|
||||
DEBUG_DBG("SPI", "OTA poll slice timeout: %lu ms", (unsigned long)timeout_ms);
|
||||
}
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查当前是否允许发起常规业务 SPI 事务。
|
||||
* 离线熔断机制:SPI 连续失败达到阈值后进入冷却期,冷却期内快速失败,
|
||||
* 避免周期性业务查询(ADC/电机/CAN反馈)死循环重试阻塞 CPU 与排队破坏堆栈。
|
||||
*/
|
||||
bool SpiMaster::allowRegularTransact(uint8_t type) const
|
||||
{
|
||||
/* 关键链路/握手命令(HELLO、ENTER_BOOT、OTA 等)不受熔断限制 */
|
||||
if (type == V2_TYPE_LINK_HELLO || type == SPI_OTA_CMD_REQ ||
|
||||
type == SPI_OTA_CMD_DATA || type == SPI_OTA_CMD_CANCEL ||
|
||||
type == SPI_APP_ENTER_BOOT_CMD) {
|
||||
return true;
|
||||
}
|
||||
if (m_linkOnline) {
|
||||
return true;
|
||||
}
|
||||
/* 处于离线熔断状态:若已过冷却期,放行一笔作为探针尝试恢复 */
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if ((now - m_lastFailureTick) >= pdMS_TO_TICKS(kCircuitBreakerCooloffMs)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SpiMaster::recordSpiSuccess()
|
||||
{
|
||||
m_consecutiveFailures = 0;
|
||||
if (!m_linkOnline) {
|
||||
m_linkOnline = true;
|
||||
DEBUG_INFO("SPI", "SPI 链路恢复在线 (circuit breaker reset)");
|
||||
}
|
||||
}
|
||||
|
||||
void SpiMaster::recordSpiFailure()
|
||||
{
|
||||
m_consecutiveFailures = m_consecutiveFailures + 1;
|
||||
m_lastFailureTick = xTaskGetTickCount();
|
||||
if (m_consecutiveFailures >= kCircuitBreakerFailThreshold && m_linkOnline) {
|
||||
m_linkOnline = false;
|
||||
DEBUG_WARN("SPI", "SPI 连续失败 %lu 次,触发熔断降频 (circuit breaker trip, 冷却 %lums)",
|
||||
(unsigned long)m_consecutiveFailures, (unsigned long)kCircuitBreakerCooloffMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。
|
||||
*
|
||||
@@ -1684,6 +1778,10 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo
|
||||
if (payload_len > SPI_OTA_FRAME_MAX) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
if (!allowRegularTransact(type)) {
|
||||
/* 离线熔断保护:快速返回超时,避免排队阻塞与无休止重试 */
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
if (!onWorker() && m_workerTask != nullptr) {
|
||||
Job job{};
|
||||
job.kind = JobKind::Transact;
|
||||
@@ -1805,14 +1903,20 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
|
||||
if (attempt + 1U < kTxRetryMax) {
|
||||
m_stats.tx_retry++; /* 0.4.9:发送级失败→重发(原静默) */
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(send_ret));
|
||||
static TickType_t s_lastSendRetryTick = 0;
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (s_lastSendRetryTick == 0 || (now_tick - s_lastSendRetryTick) >= pdMS_TO_TICKS(5000)) {
|
||||
s_lastSendRetryTick = now_tick;
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s (限频5s)",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(send_ret));
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(3));
|
||||
continue;
|
||||
}
|
||||
m_stats.tx_fail++; /* 0.4.9:耗尽重试仍失败 */
|
||||
m_stats.tx_fail_last_type = type;
|
||||
recordSpiFailure();
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
@@ -1831,20 +1935,28 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
esp_err_t poll_ret = otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len,
|
||||
poll_timeout, /*expected_type=*/0xFFU);
|
||||
if (poll_ret == ESP_OK) {
|
||||
recordSpiSuccess();
|
||||
return ESP_OK; /* 已读到有效响应帧(ACK/ERROR/陈旧) */
|
||||
}
|
||||
if (attempt + 1U < kTxRetryMax) {
|
||||
m_stats.tx_retry++; /* 0.4.9:poll 无响应→重发(原静默) */
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(poll_ret));
|
||||
static TickType_t s_lastPollRetryTick = 0;
|
||||
const TickType_t now_tick = xTaskGetTickCount();
|
||||
if (s_lastPollRetryTick == 0 || (now_tick - s_lastPollRetryTick) >= pdMS_TO_TICKS(5000)) {
|
||||
s_lastPollRetryTick = now_tick;
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s (限频5s)",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(poll_ret));
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(3));
|
||||
continue;
|
||||
}
|
||||
m_stats.tx_fail++; /* 0.4.9:耗尽重试仍失败 */
|
||||
m_stats.tx_fail_last_type = type;
|
||||
recordSpiFailure();
|
||||
return poll_ret;
|
||||
}
|
||||
recordSpiFailure();
|
||||
return ESP_ERR_TIMEOUT; /* 不可达:循环必有返回 */
|
||||
}
|
||||
|
||||
|
||||
@@ -210,8 +210,10 @@ public:
|
||||
uint8_t rsp_err_last_req; ///< 最近一笔 0x82 对应的主动请求 TYPE
|
||||
uint8_t rsp_err_last_p0; ///< 最近一笔 0x82 的 payload[0](GD32 错误码)
|
||||
uint8_t rsp_err_last_p1; ///< 0xE8 时=GD32 收到的 rxLen
|
||||
uint8_t rsp_err_last_p2; ///< 0xE8 时=GD32 rxBuf[0](期望 SOF0=0xE2?)
|
||||
uint8_t rsp_err_last_p3; ///< 0xE8 时=GD32 rxBuf[1](期望 SOF1)
|
||||
uint8_t rsp_err_last_p2; ///< 0xE8 时=GD32 rxBuf[0](期望 SOF0=0xA5)
|
||||
uint8_t rsp_err_last_p3; ///< 0xE8 时=GD32 rxBuf[1](期望 SOF1=0x5A)
|
||||
uint8_t rsp_err_last_p4; ///< 0xE8 时=GD32 SpiParseResult 解析失败原因枚举
|
||||
uint8_t rsp_err_last_p5; ///< 0xE8 时=GD32 SOF offset (0=正常, >0=漂移, 255=未找到)
|
||||
uint32_t ring_steals; ///< serviceRing 偷读迟到 RESPONSE 总数
|
||||
uint8_t ring_steal_last_type; ///< 最近一笔偷读帧的 TYPE(0.4.5 归因)
|
||||
uint8_t ring_steal_last_ack; ///< 最近一笔偷读帧 ack_seq 是否匹配 lastSent
|
||||
@@ -220,6 +222,8 @@ public:
|
||||
uint8_t ring_steal_last_p1; ///< 0xE8 时=GD32 诊断 rxLen(0.4.8)
|
||||
uint8_t ring_steal_last_p2; ///< 0xE8 时=GD32 诊断 rxBuf[0](0.4.8)
|
||||
uint8_t ring_steal_last_p3; ///< 0xE8 时=GD32 诊断 rxBuf[1](0.4.8)
|
||||
uint8_t ring_steal_last_p4; ///< 0xE8 时=GD32 诊断 parse_res
|
||||
uint8_t ring_steal_last_p5; ///< 0xE8 时=GD32 诊断 sof_offset
|
||||
/* 0.4.9 诊断:透明重试会掩盖真实失败率(DBG 被编译裁掉、无计数)。
|
||||
* tx_total 进入事务;tx_retry 走到重发;tx_fail 耗尽重试仍失败返回。 */
|
||||
uint32_t tx_total; ///< otaSendAndPoll 事务总数
|
||||
@@ -244,6 +248,15 @@ public:
|
||||
/** @brief v2 session 是否已激活。 */
|
||||
bool v2Active() const { return m_v2Active; }
|
||||
|
||||
/** @brief SPI 链路是否判定在线(连续失败未触发熔断,防 SPI 不通时死循环阻塞与堆栈耗尽)。 */
|
||||
bool isLinkOnline() const { return m_linkOnline; }
|
||||
|
||||
/** @brief 当前连续失败次数。 */
|
||||
uint32_t consecutiveFailures() const { return m_consecutiveFailures; }
|
||||
|
||||
/** @brief 检查当前是否允许发起常规业务 SPI 事务(离线熔断冷却期内快速失败)。 */
|
||||
bool allowRegularTransact(uint8_t type) const;
|
||||
|
||||
/** @brief 最近一次 HELLO 得到的 GD32 角色(0=unknown, 1=app, 2=boot)。 */
|
||||
uint8_t gd32Role() const { return m_gd32Role; }
|
||||
|
||||
@@ -384,6 +397,16 @@ private:
|
||||
volatile bool m_resyncPending = false;
|
||||
/** 上次 HELLO 重同步时刻(冷却 5s,防 GD32 不响应时反复 HELLO)。 */
|
||||
volatile uint32_t m_lastResyncTick = 0;
|
||||
/** 链路在线状态及熔断状态:防 SPI 不通时死循环阻塞与堆栈耗尽 */
|
||||
volatile bool m_linkOnline = true;
|
||||
volatile uint32_t m_consecutiveFailures = 0;
|
||||
volatile uint32_t m_lastFailureTick = 0;
|
||||
static constexpr uint32_t kCircuitBreakerFailThreshold = 3U; ///< 连续 3 次失败触发熔断
|
||||
static constexpr uint32_t kCircuitBreakerCooloffMs = 2000U; ///< 熔断后冷却 2 秒
|
||||
|
||||
void recordSpiSuccess();
|
||||
void recordSpiFailure();
|
||||
|
||||
/** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */
|
||||
volatile uint8_t m_lastReqType = 0xFFU;
|
||||
/** SPI worker 任务句柄(ISR 唯一通知目标)。 */
|
||||
|
||||
Reference in New Issue
Block a user