feat(spi): 增强 SPI master:性能剖析与执行耗时跟踪

- 新增 `lastWorkerExecUs()`:获取最近一次 worker 任务执行耗时(µs)。
- 引入 `SpiProfile` 结构体:事务级详细剖析,记录 SPI 事务各阶段的时间戳。
- 实现 `lastProfile()`:诊断用,获取最近一次事务剖析快照。
- SPI 时钟配置提升至 13.3 MHz,改善性能。
- 修改前端 JavaScript 轮询逻辑:新增 uptime 检查,更好地处理 ESP32 重启状态。
- SPI 测速 HTML 的计时单位由毫秒改为微秒显示。
- 新建 `spi_ring_cache.h`:共享 SPI ring push 缓存工具。
- 新增 `http_util.h`:共享 HTTP 处理器工具,含 JSON 拼接与查询参数解析。
- 引入 `json_parse.h`:轻量级 JSON 解析器,针对扁平 JSON 对象设计。
- 新增 Windows 环境下的项目构建批处理脚本。
- 重构 CAN 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。
This commit is contained in:
小龙虾
2026-08-02 17:37:19 +08:00
parent 04a118219f
commit f9cbcc396b
42 changed files with 1957 additions and 4537 deletions
+75 -71
View File
@@ -349,10 +349,14 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
/* 转发预算:自本阶段起算。正常 ~15ms/包,预算 40ms/包 + 20s 底(上限 120s)。 */
constexpr uint32_t kXferMinMs = 20U * 1000U;
/* 转发预算:自本阶段起算。正常 ~2ms/包(12MHz+ 读预算收紧后实测),
* 单包失败重发最坏 ~95ms/轮 × 3 轮 ≈ 285ms(底层 3 次尝试 + relay 50ms 窗口)。
* 预算按"正常 + 重发余量":25ms/包 ≈ 正常 2ms + 约 10% 包重发 1 次的平均,
* 底 10s 覆盖慢启动/握手余量(777 包正常仅 ~1.6s);上限 120s 兜极端慢速。
* 跨包卡死(10s 无成功帧)由 kStallTimeoutMs 单独检测,总预算不重复兜底。 */
constexpr uint32_t kXferMinMs = 10U * 1000U;
constexpr uint32_t kXferMaxMs = 120U * 1000U;
constexpr uint32_t kXferPerPktMs = 40U;
constexpr uint32_t kXferPerPktMs = 25U;
uint32_t xfer_ms = kXferMinMs + m_transferTotalPackets * kXferPerPktMs;
if (xfer_ms > kXferMaxMs) {
xfer_ms = kXferMaxMs;
@@ -448,9 +452,6 @@ ota_result_t OtaRelay::forwardFirmware()
DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size);
/* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */
SpiMaster::instance().setSuppressRingService(true);
/* 完整性门控(避免与 /ota/relay·BLE 触发前的 verifyGd32Firmware 二次全盘 SHA):
* - meta 必须完整 stored≥total
* - checksum 非全零:触发路径已 finalize+比对,此处信任 meta,跳过再扫 Flash
@@ -500,10 +501,8 @@ ota_result_t OtaRelay::forwardFirmware()
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
/* 必须通知 GD32 CANCEL,否则远端停在 WAITING_* 半会话 */
abortRemoteAndReset(true);
} else {
/* 成功路径:解除按铃抑制(reset 也会做,成功时状态机可能停在 DONE) */
SpiMaster::instance().setSuppressRingService(false);
}
/* 成功/失败统一由 reset() 解除 ring 抑制(relay task 侧持有 ref) */
return res;
}
@@ -1174,7 +1173,10 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
* - 等 ACK 期间可预组下一包(与从机处理重叠)。
*/
constexpr uint32_t kMaxDataSends = 3U;
constexpr uint32_t kAckWaitMs = 6000U; /* 单次发送后等 ACK 上限(覆盖 GD32 增量 drain) */
/* 单次发送后等 ACK 上限:GD32 正常 ms 级回 ACK(ISR 回 WAIT、主循环回 ACK),
* 50ms 未到即重发(DUP-SEQ 回放捞回),不再干等 6s(用户指定 50ms)。
* credit==0 的排空积压由 transact 内部 credit 背压消化,此处无需长等。 */
constexpr uint32_t kAckWaitMs = 50U;
constexpr uint32_t kPollSliceMs = 50U; /* 略紧:DATA_READY 驱动,缩短空切片 */
TickType_t pkt_start = xTaskGetTickCount();
@@ -1204,9 +1206,11 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
{
resp_len = 0;
resp_type = 0;
/* 等首响应上限 10ms:一次 SPI 事务最多 5ms(用户指定),
* 超时立即进入下方 50ms poll 窗口,不再干等 200ms。 */
esp_err_t err = SpiMaster::instance().transact(
SPI_OTA_CMD_DATA, tx_buf, 64,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200);
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 10);
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
saw_wait = true;
@@ -1412,6 +1416,58 @@ bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
return false;
}
/**
* @brief 统一 "HELLO(role=APP) → 可选 PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。
*
* 两处原循环差异仅:taskEntry 在 HELLO 已带身份(gd32IdKnown)时跳过 PING 直接成功。
* 用 allow_hello_shortcut 参数统一:true 时启用 HELLO 身份快路径。
*
* @param timeout_ms PING 超时
* @param ping_interval_ms 每轮间隔
* @param max_attempts 最大轮数
* @param allow_hello_shortcut true=HELLO 带身份即成功(taskEntry 场景)
* @return true=APP 存活已确认
*/
bool OtaRelay::helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms,
uint32_t max_attempts, bool allow_hello_shortcut)
{
bool session_ok = false;
for (uint32_t attempt = 0; attempt < max_attempts; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(ping_interval_ms));
if (!session_ok) {
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK &&
SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
session_ok = true;
/* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */
if (allow_hello_shortcut && SpiMaster::instance().gd32IdKnown()) {
StateLockGuard lock;
gd32Version = SpiMaster::instance().gd32VersionCached();
gd32AppAlive = true;
DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d",
gd32Version.app_major, gd32Version.app_minor,
gd32Version.app_patch, gd32Version.app_build);
DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d",
gd32Version.boot_major, gd32Version.boot_minor,
gd32Version.boot_patch, gd32Version.boot_build);
return true;
}
} else {
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts);
continue;
}
}
/* HELLO 成功但无身份(旧固件),或 doWaitForAppAlive 路径:回退 APP PING */
if (probeAndCacheAppAlive(timeout_ms)) {
return true;
}
/* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */
session_ok = false;
DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts);
}
return false;
}
/** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */
ota_result_t OtaRelay::doSendFinishOta()
{
@@ -1485,30 +1541,8 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
/* FINISH 后短暂安静,避免 jump/APP SPI arm 窗口被 HELLO 打乱(对齐既有 300ms 节拍)。 */
vTaskDelay(pdMS_TO_TICKS(500));
/* APP 起机约数秒;HELLO 须 role=APP 再建 session,再 appPing。 */
bool session_established = false;
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(300));
if (!session_established) {
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
if (hr == ESP_OK && SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
session_established = true;
} else {
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
continue;
}
}
if (probeAndCacheAppAlive(timeout_ms)) {
break;
}
/* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */
session_established = false;
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
}
if (!getGd32AppAlive()) {
/* doWaitForAppAlive 不启用 HELLO 身份快路径:OTA 完成后必须 PING 确认 APP 真正运行 */
if (!helloAndProbeApp(timeout_ms, 300U, 20U, /*allow_hello_shortcut=*/false)) {
DEBUG_ERR("OTA", "GD32 APP 启动确认超时,OTA 失败");
/* APP 未起来时 CANCEL 可能无效,仍尽力清理 BOOT 半会话 */
abortRemoteAndReset(true);
@@ -1562,42 +1596,7 @@ void OtaRelay::taskEntry(void *pvParameters)
/* 启动 GD32 探测:先 hello 建 v2 session,再 appPing。 */
{
DEBUG_INFO("OTA", "启动初始 GD32 探测 (hello+probe)...");
bool session_ok = false;
bool alive = false;
for (uint32_t attempt = 0; attempt < 20U; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(300));
if (!session_ok) {
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK &&
SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
session_ok = true;
/* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */
if (SpiMaster::instance().gd32IdKnown()) {
StateLockGuard lock;
gd32Version = SpiMaster::instance().gd32VersionCached();
gd32AppAlive = true;
alive = true;
DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d",
gd32Version.app_major, gd32Version.app_minor,
gd32Version.app_patch, gd32Version.app_build);
DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d",
gd32Version.boot_major, gd32Version.boot_minor,
gd32Version.boot_patch, gd32Version.boot_build);
break;
}
} else {
DEBUG_INFO("OTA", "等待 GD32 APP... (%" PRIu32 "/20)", attempt + 1U);
continue;
}
}
/* 旧固件无 HELLO 身份:回退 APP PING */
if (probeAndCacheAppAlive(2000)) {
alive = true;
break;
}
session_ok = false;
DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/20)", attempt + 1U);
}
if (!alive) {
if (!helloAndProbeApp(2000U, 300U, 20U, /*allow_hello_shortcut=*/true)) {
DEBUG_WARN("OTA", "启动 GD32 探测未成功(中继前仍可再 hello)");
}
}
@@ -1610,6 +1609,11 @@ void OtaRelay::taskEntry(void *pvParameters)
}
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
/* 尽早抑制 ring service:onRelayStarted 可能因协调器锁(/system/status
* 等争锁)阻塞,此窗口内 worker 会把上一阶段残留 RESPONSE 当 EVENT 偷读
* (线上 log#1:TRANSFERRING 起始瞬间 serviceRing 读到 type=0x80)。
* forwardFirmware 不再重复设置;释放统一走 reset()。 */
SpiMaster::instance().setSuppressRingService(true);
// 通过 IRelayListener 接口回调通知协调器
IRelayListener *listener = getRelayListener();
if (listener) listener->onRelayStarted();
+13 -40
View File
@@ -118,16 +118,6 @@ public:
*/
ota_result_t forwardFirmware();
// ── 事件驱动接口 ──
/**
* @brief 向状态机投递事件,驱动状态转换.
* @param event 事件类型.
* @param payload 关联数据(响应载荷等),可为nullptr.
* @param len 数据长度.
* @return 当前操作结果码(OTA_RESULT_OK 表示事件已处理且可继续).
*/
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
// ── 状态查询(线程安全)──
/**
* @brief 获取当前中继状态.
@@ -145,31 +135,6 @@ public:
* @return 中继进度快照。
*/
static RelaySnapshot getSnapshot();
/**
* @brief 设置中继状态.
* @param s 目标状态。
*/
static void setState(relay_state_t s);
/**
* @brief 设置中继当前序号.
* @param seq 序号值。
*/
static void setRelaySeq(uint32_t seq);
/**
* @brief 设置中继总包数.
* @param total 总包数。
*/
static void setRelayTotalPackets(uint32_t total);
/**
* @brief 设置GD32 APP存活标志位.
* @param alive true 表示存活。
*/
static void setGd32AppAlive(bool alive);
/**
* @brief 设置GD32固件版本.
* @param ver 版本信息结构体引用。
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 获取 OTA 中继任务句柄。
* @return FreeRTOS 任务句柄。
@@ -195,11 +160,6 @@ public:
* @param listener 监听器指针,传 nullptr 取消注册。
*/
static void setRelayListener(IRelayListener *listener);
/**
* @brief 获取当前注册的监听器。
* @return 监听器指针,未注册时为 nullptr。
*/
static IRelayListener* getRelayListener();
// ── 队列访问器 ──
/**
@@ -223,6 +183,19 @@ public:
*/
static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr);
/** @brief 统一 "HELLO(role=APP) → PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。 */
static bool helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms,
uint32_t max_attempts, bool allow_hello_shortcut);
// ── 内部状态写入(仅 ota_relay.cpp 使用,无外部调用)──
static void setState(relay_state_t s);
static void setRelaySeq(uint32_t seq);
static void setRelayTotalPackets(uint32_t total);
static void setGd32AppAlive(bool alive);
static void setGd32Version(const gd32_version_t &ver);
static IRelayListener* getRelayListener();
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
// ── 线程安全锁 ──
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间