feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制

移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2
CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发
实现流量控制,提高OTA传输稳定性。

refactor(stack): 统一任务栈大小配置并添加CPU统计功能

- 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配
- 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样
- 更新main.cpp中的任务创建使用新的栈大小常量
- 调整health_monitor和ota_relay等任务的栈分配

perf(ota): 改进ESP32和GD32 OTA分区预擦策略

- ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞
- GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量
- 优化OTA协调器状态管理,修复FINALIZING状态残留问题

fix(log): 改进UDP日志发送的堆内存管理

- 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer
- 降低UDP发送跳过的内存阈值从10KB到6KB
- 降低日志排水最小内存阈值从8KB到4KB

docs(readme): 更新OTA配置参数文档说明

移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为
基于CREDIT背压机制的描述。
This commit is contained in:
小龙虾
2026-07-16 01:24:12 +08:00
parent a47a027178
commit 51c89b3065
52 changed files with 1006 additions and 628 deletions
+1
View File
@@ -10,6 +10,7 @@ idf_component_register(
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
"server/ble_ota.cpp" # BLE GATT OTA 服务器
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
"monitor/cpu_stats.cpp" # 1Hz FreeRTOS CPU 占用采样任务
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
+5 -13
View File
@@ -10,25 +10,17 @@ menu "DCU OTA Configuration"
validated against actual wiring length/quality and GD32
SPI slave timing.
config DCU_OTA_PACKET_INTERVAL_MS
int "Delay between GD32 OTA packets (ms)"
default 1
range 0 50
help
Idle delay inserted after each successfully acknowledged
firmware packet before sending the next one.
Lower value speeds up OTA, but if GD32 cannot finish
flash write / prepare next ACK in time, set it back.
config DCU_OTA_POLL_BACKOFF_MS
int "Polling backoff when GD32 is not ready (ms)"
default 2
range 0 50
help
Delay between SPI response polls when GD32 returns WAIT
or does not drive MISO yet. Lower value reduces per-packet
idle time and can significantly speed up OTA, but if GD32
cannot finish flash write before the next poll, increase it.
or does not drive MISO yet. Packet pacing uses SPI Link v2
CREDIT backpressure in SpiMaster (remoteCredit==0 → wait/
refuse TX), not a fixed inter-packet delay. Lower value
reduces poll idle time; increase if GD32 needs more time
between polls.
config DCU_OTA_FINISH_TIMEOUT_MS
int "Timeout for FINISH_OTA command (ms)"
+14 -11
View File
@@ -14,6 +14,7 @@
#include "esp_ota_ops.h"
#include "monitor/health_monitor.h"
#include "monitor/cpu_stats.h"
#include "relay/ota_relay.h"
#include "ota/ota_coordinator.h"
#include "transport/spi_master.h"
@@ -33,7 +34,7 @@ extern "C" void app_main(void)
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
// ★ M1: ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore(替代旧的全局变量)
// ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore
Esp32OtaCore::initMutex();
// 初始化网络接口 + 事件循环(必须最早)
@@ -43,7 +44,7 @@ extern "C" void app_main(void)
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
DebugLog::instance().init();
// ★ WiFi 提前初始化(调度器启动前)
// WiFi 提前初始化(调度器启动前)
bool wifi_sta_ok = WifiOta::instance().earlyInit();
HealthMonitor::instance().init();
@@ -61,7 +62,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
HealthMonitor::taskEntry,
"health_mon",
4096,
TASK_STACK_HEALTH_MON,
NULL,
configMAX_PRIORITIES - 4,
NULL,
@@ -76,12 +77,15 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "健康监控任务已创建");
}
/* CPU 占用 1Hz 采样(非致命;失败时 /system/status 的 cpu.valid 保持 false) */
CpuStats::instance().start();
Protocol::instance().init();
DEBUG_INFO("MAIN", "协议层初始化完成");
// ★ 全局 OTA 协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
// OTA 全局协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
OtaCoordinator::instance().init();
// ★ L1: 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
// 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance());
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
@@ -96,7 +100,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
OtaRelay::taskEntry,
"ota_relay",
6144,
TASK_STACK_OTA_RELAY,
NULL,
configMAX_PRIORITIES - 2,
NULL,
@@ -115,7 +119,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
WifiOta::taskEntry,
"wifi_ota",
8192,
TASK_STACK_WIFI_OTA,
NULL,
configMAX_PRIORITIES - 3,
NULL,
@@ -134,7 +138,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
BleOta::taskEntry,
"ble_ota",
4096,
TASK_STACK_BLE_OTA,
NULL,
configMAX_PRIORITIES - 3,
NULL,
@@ -158,7 +162,7 @@ extern "C" void app_main(void)
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
}
// ★ OTA 回滚保护:标记当前固件有效
// OTA 回滚保护:标记当前固件有效
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
@@ -171,8 +175,7 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "========================================");
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
// ★ 启动最后步骤:读取 GD32 APP 版本号
// 参考 OtaRelay::doWaitForAppAlive 刷写后读取逻辑:
// 启动最后步骤:读取 GD32 APP 版本号
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
{
+23 -10
View File
@@ -44,22 +44,35 @@ inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4; ///< AP 模
// HTTP 服务器配置
// ============================================================
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 10240; ///< HTTP 服务器任务栈大小(字节)
// ★ 历史:6144 栈溢出;8192 在上传收尾仍紧。曾提到 12288 但 ESP32-C3 堆极紧
// (OTA 后 free~5KB、largest~1KB)。SHA context 已改 static,httpd 栈 10KB 折中。
// 真·栈溢出钩子会 abort;卡 100% 更多是堆碎导致回包/后续分配失败。
// ============================================================
// 任务栈大小(字节)— 2026-07 按板端 task_stack 水位收敛
// 规则:peak_used 实测 + 至少 ~1.5–2KB 余量;httpd 路径最重单独放宽。
// 实测(空闲/常规):http peak~5KB, wifi_ota~1.4KB, ota_relay~1KB,
// health~1.2KB, cpu_stats~2KB, ble*~1KB, reconnect~1KB。
// ============================================================
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< httpd:原 10KB,peak~5KB → 8KB(余量~3KB)
inline constexpr uint32_t TASK_STACK_WIFI_OTA = 4096; ///< wifi_ota 主循环:原 8KB,peak~1.4KB
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(实测 peak~1KB;完整 GD32 中继后再确认水位)
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:无单独水位,6144→5120 小幅收
inline constexpr uint32_t TASK_STACK_BLE_OTA = 3072; ///< ble_ota 主任务:peak~1KB
inline constexpr uint32_t TASK_STACK_BLE_WORKER = 3072; ///< ble_ota_worker:peak~1.1KB(负载峰值曾~1.9KB)
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.2KB
inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~2KB
inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:peak~1KB
inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:1Hz 轮询
inline constexpr uint32_t TASK_STACK_BLE_FINISH = 4096; ///< 按需:SHA finalize(保持)
inline constexpr uint32_t TASK_STACK_BLE_GD32_WAIT = 3072; ///< 按需:GD32 中继等待
inline constexpr uint32_t TASK_STACK_BLE_RESTART = 2048; ///< 按需:延时重启
// ============================================================
// OTA 配置
// ============================================================
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值堆(原 4/8KB)
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值 BSS/堆(s_ota_upload_recv 静态占用)
// OTA_BLOCK_SIZE 演进:8KB(原)→2KB(压堆)→4KB(试吞吐)→2KB(2026-07 回退省 DRAM)。
// 4KB 时 s_ota_upload_recv BSS +2KB,开机 free/largest 更紧;回退 2KB 优先保内存。
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷
inline constexpr uint32_t OTA_POLL_RETRIES = 40; // 每包 ACK 轮询次数
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间
inline constexpr uint32_t OTA_POST_SEND_DELAY_MS = 3; // 发送请求后首次轮询前的延迟(ms),无 READY 拉低时防止读到 stale 数据
inline constexpr uint32_t OTA_POLL_NO_FRAME_DELAY_MS = 8; // 轮询未找到帧时的重试延迟(ms),无 READY 拉低时需更长
// ============================================================
// 健康监控阈值
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 46
#define FW_VERSION_STRING "0.3.46"
#define FW_VERSION_PATCH 47
#define FW_VERSION_STRING "0.3.47"
// 编译时间
#define BUILD_TIME "2026-07-15 12:08:48"
#define BUILD_TIME "2026-07-16 01:10:05"
// Git 信息
#define GIT_HASH "5a89cec"
#define GIT_HASH "a47a027"
+2 -1
View File
@@ -16,6 +16,7 @@
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "app_config.h"
#include "transport/canfd_bridge.h"
#include "transport/spi_master.h"
#include "monitor/debug_log.h"
@@ -130,7 +131,7 @@ bool startCanFeedbackTask()
BaseType_t xRet = xTaskCreatePinnedToCore(
taskLoop,
"can_fb",
4096,
TASK_STACK_CAN_FB,
nullptr,
4,
nullptr,
+192
View File
@@ -0,0 +1,192 @@
/*
* cpu_stats.cpp - 1Hz FreeRTOS CPU 占用采样任务
*/
#include "cpu_stats.h"
#include <algorithm>
#include <cstring>
#include <inttypes.h>
#include <stdlib.h>
#include "app_config.h"
#include "esp_timer.h"
#include "monitor/debug_log.h"
CpuStats& CpuStats::instance()
{
static CpuStats inst;
return inst;
}
void CpuStats::start()
{
if (m_started) {
return;
}
m_started = true;
#if !defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
portENTER_CRITICAL(&m_mux);
m_published = {};
m_published.available = false;
portEXIT_CRITICAL(&m_mux);
DEBUG_WARN("CPU", "未启用 CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS,跳过采样任务");
return;
#else
portENTER_CRITICAL(&m_mux);
m_published = {};
m_published.available = true;
m_published.valid = false;
portEXIT_CRITICAL(&m_mux);
BaseType_t ok = xTaskCreatePinnedToCore(
&CpuStats::taskEntry,
"cpu_stats",
TASK_STACK_CPU_STATS,
this,
tskIDLE_PRIORITY + 1, /* 低优先级,少搅动占用统计 */
&m_taskHandle,
0);
if (ok != pdPASS) {
m_taskHandle = nullptr;
m_started = false;
DEBUG_ERR("CPU", "创建 cpu_stats 任务失败");
return;
}
DEBUG_INFO("CPU", "cpu_stats 任务已启动,采样周期 %" PRIu32 " ms", CPU_STATS_SAMPLE_MS);
#endif
}
void CpuStats::getSnapshot(CpuUsageSnapshot& out) const
{
portENTER_CRITICAL(&m_mux);
out = m_published;
portEXIT_CRITICAL(&m_mux);
}
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
static uint64_t runtimeDelta(configRUN_TIME_COUNTER_TYPE cur, configRUN_TIME_COUNTER_TYPE prev)
{
return cur >= prev ? (uint64_t)(cur - prev) : 0U;
}
static bool taskNameIsIdle(const char* name)
{
return name && strncmp(name, "IDLE", 4) == 0;
}
void CpuStats::sampleOnce()
{
UBaseType_t task_count = uxTaskGetNumberOfTasks();
if (task_count == 0) {
return;
}
task_count = std::min<UBaseType_t>(task_count + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
if (!tasks) {
return;
}
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
UBaseType_t captured = uxTaskGetSystemState(tasks, task_count, &total_runtime);
const int64_t now_us = esp_timer_get_time();
if (captured == 0 || captured > task_count) {
DEBUG_WARN("CPU", "uxTaskGetSystemState 异常: captured=%lu buf=%lu",
(unsigned long)captured, (unsigned long)task_count);
free(tasks);
return;
}
CpuUsageSnapshot snap = {};
snap.available = true;
const bool had_previous = m_hasLast;
const configRUN_TIME_COUNTER_TYPE previous_total = m_lastTotal;
const int64_t previous_time_us = m_lastTimeUs;
const size_t previous_count = m_lastCount;
TaskSample previous_samples[CPU_TASK_SNAPSHOT_MAX];
for (size_t i = 0; i < previous_count && i < CPU_TASK_SNAPSHOT_MAX; ++i) {
previous_samples[i] = m_lastSamples[i];
}
/* 先更新基线,再算本窗口(与旧 HTTP 路径语义一致) */
size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
for (size_t i = 0; i < copy_count; ++i) {
m_lastSamples[i].task_number = tasks[i].xTaskNumber;
m_lastSamples[i].runtime = tasks[i].ulRunTimeCounter;
}
m_lastCount = copy_count;
m_lastTotal = total_runtime;
m_lastTimeUs = now_us;
m_hasLast = true;
const bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
const uint64_t total_delta = can_compute ? runtimeDelta(total_runtime, previous_total) : 0U;
uint64_t idle_delta = 0U;
if (can_compute && total_delta > 0U) {
for (UBaseType_t i = 0; i < captured; ++i) {
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
bool found = false;
for (size_t j = 0; j < previous_count; ++j) {
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
last_runtime = previous_samples[j].runtime;
found = true;
break;
}
}
if (!found) {
continue;
}
uint64_t delta = runtimeDelta(tasks[i].ulRunTimeCounter, last_runtime);
double pct = (double)delta * 100.0 / (double)total_delta;
if (taskNameIsIdle(tasks[i].pcTaskName)) {
idle_delta += delta;
}
if (snap.task_count < CPU_TASK_SNAPSHOT_MAX) {
CpuTaskMetric& m = snap.tasks[snap.task_count++];
const char* n = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
strncpy(m.name, n, sizeof(m.name) - 1);
m.name[sizeof(m.name) - 1] = '\0';
m.task_number = tasks[i].xTaskNumber;
m.runtime_delta = delta;
m.usage_pct = pct;
}
}
snap.valid = true;
snap.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
snap.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
snap.usage_pct = std::max(0.0, 100.0 - snap.idle_pct);
}
free(tasks);
portENTER_CRITICAL(&m_mux);
m_published = snap;
portEXIT_CRITICAL(&m_mux);
}
void CpuStats::taskEntry(void* arg)
{
auto* self = static_cast<CpuStats*>(arg);
DEBUG_INFO("CPU", "=== cpu_stats 采样任务启动 ===");
TickType_t last_wake = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(CPU_STATS_SAMPLE_MS);
/* 第一拍只建基线(valid=false);此后每拍窗口 ≈ 1000ms */
for (;;) {
self->sampleOnce();
vTaskDelayUntil(&last_wake, period);
}
}
#else /* !CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS */
void CpuStats::sampleOnce() {}
void CpuStats::taskEntry(void*) { vTaskDelete(nullptr); }
#endif
+73
View File
@@ -0,0 +1,73 @@
/*
* cpu_stats.h - FreeRTOS 运行时 CPU 占用采样
*
* 由独立低优先级任务按固定 1s 周期采样;/system/status 只读最新快照,
* 不再在 HTTP 线程里调用 uxTaskGetSystemState(避免窗口绑浏览器轮询、减 httpd 栈压)。
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/** 单次快照最多跟踪的任务数 */
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
/** 采样周期(ms)— 窗口 sample_ms 应对齐此值 */
static constexpr uint32_t CPU_STATS_SAMPLE_MS = 1000;
struct CpuTaskMetric {
char name[configMAX_TASK_NAME_LEN];
UBaseType_t task_number;
uint64_t runtime_delta;
double usage_pct;
};
struct CpuUsageSnapshot {
bool available; ///< 固件是否启用 RUN_TIME_STATS
bool valid; ///< 是否已有完整窗口(至少两拍)
uint32_t sample_ms; ///< 本窗口墙钟长度
double usage_pct; ///< 100 - idle
double idle_pct;
CpuTaskMetric tasks[CPU_TASK_SNAPSHOT_MAX];
size_t task_count;
};
/**
* @brief CPU 运行时统计(单例 + 1Hz 采样任务)
*/
class CpuStats {
public:
static CpuStats& instance();
/** 创建采样任务(幂等;失败仅打日志,不阻塞启动) */
void start();
/** 线程安全拷贝最新快照(HTTP 用) */
void getSnapshot(CpuUsageSnapshot& out) const;
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
private:
CpuStats() = default;
static void taskEntry(void* arg);
void sampleOnce();
mutable portMUX_TYPE m_mux = portMUX_INITIALIZER_UNLOCKED;
CpuUsageSnapshot m_published = {};
TaskHandle_t m_taskHandle = nullptr;
bool m_started = false;
/* 基线(仅采样任务写) */
struct TaskSample {
UBaseType_t task_number;
configRUN_TIME_COUNTER_TYPE runtime;
};
TaskSample m_lastSamples[CPU_TASK_SNAPSHOT_MAX] = {};
size_t m_lastCount = 0;
configRUN_TIME_COUNTER_TYPE m_lastTotal = 0;
int64_t m_lastTimeUs = 0;
bool m_hasLast = false;
};
+12 -1
View File
@@ -367,7 +367,8 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
}
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
* WiFi 未连接时 sendto 失败也静默跳过。 */
* WiFi 未连接时 sendto 失败也静默跳过。
* ★ 修复:堆不足跳过时写一条告警到 ring buffer,让前端 /logs 可见。 */
if (m_initialized && free_now >= kSkipUdpFree) {
if (m_udpSock < 0) {
ensureUdpSocket();
@@ -381,6 +382,16 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
udp_ret = ESP_OK;
}
}
} else if (m_initialized) {
/* 堆不足导致 UDP 被跳过——通过 ring buffer 记录,避免上位机无法感知 */
static uint32_t s_lastUdpSkipWarnMs = 0;
uint32_t now_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
/* 限制告警频率:每 10 秒最多一条,避免刷屏 */
if ((now_ms - s_lastUdpSkipWarnMs) > 10000) {
s_lastUdpSkipWarnMs = now_ms;
static const char kUdpSkipMsg[] = "[W][LOG] UDP 发送已暂停: 堆内存不足\n";
ringPush(kUdpSkipMsg, sizeof(kUdpSkipMsg) - 1);
}
}
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
+2 -2
View File
@@ -113,8 +113,8 @@ private:
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
static constexpr uint32_t kSkipUdpFree = 10U * 1024U;
static constexpr uint32_t kDrainMinFree = 8U * 1024U;
static constexpr uint32_t kSkipUdpFree = 6U * 1024U; // ★ 修复:从 10KB 降至 6KB,sendto 本身不需大块堆
static constexpr uint32_t kDrainMinFree = 4U * 1024U; // ★ 修复:从 8KB 降至 4KB,降低 /logs 返空的概率
esp_err_t ensureUdpSocket();
void ringPush(const char *data, int len);
+2 -2
View File
@@ -74,9 +74,9 @@ public:
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
/**
* @brief 由 WiFi 事件回调推送连接状态(解耦 HealthMonitor → WifiOta 依赖)。
* @brief 由 WiFi 事件回调推送连接状态(避免 HealthMonitor → WifiOta 依赖)。
* WiFi 连接/断开时由 wifi_ota 事件处理器主动调用,
* HealthMonitor 不再主动查询 WifiOta::isConnected()。
* HealthMonitor 无需主动查询 WifiOta::isConnected()。
* @param connected true=已连接,false=断开。
*/
void setWifiConnected(bool connected);
+10 -6
View File
@@ -8,6 +8,7 @@
#include "app_config.h"
#include "health_monitor.h"
#include "cpu_stats.h"
#include "ota_relay.h"
#include "wifi_ota.h"
#include "ble_ota.h"
@@ -24,22 +25,25 @@ size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
}
if (n < max) {
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 };
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), TASK_STACK_HEALTH_MON };
}
if (n < max) {
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), 6144 };
out[n++] = { "cpu_stats", CpuStats::instance().getTaskHandle(), TASK_STACK_CPU_STATS };
}
if (n < max) {
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 };
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), TASK_STACK_OTA_RELAY };
}
if (n < max) {
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 };
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), TASK_STACK_WIFI_OTA };
}
if (n < max) {
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), 4096 };
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, TASK_STACK_WIFI_RECONNECT };
}
if (n < max) {
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 };
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), TASK_STACK_BLE_OTA };
}
if (n < max) {
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), TASK_STACK_BLE_WORKER };
}
return n;
+48 -15
View File
@@ -25,7 +25,7 @@
#define TAG "OTA_CORE"
// ============================================================
// 分区互斥锁(M1: 内聚自 ota_mutex.h 全局变量)
// 分区互斥锁
// ============================================================
SemaphoreHandle_t Esp32OtaCore::s_mutex = NULL;
volatile bool Esp32OtaCore::s_occupied = false;
@@ -172,22 +172,57 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
return beginCleanupAndReturn(ESP_FAIL);
}
/* ★ 用 OTA_WITH_SEQUENTIAL_WRITES:边写边按扇区擦除,begin 几乎瞬时返回。
* 旧逻辑按 64KB 对齐预擦 ~1MB,会在 /ota/upload 开头阻塞几十秒~数分钟:
* 浏览器 upload 进度已到 100%,设备还在擦 Flash → 表现为“卡在 100% 很久才成功”。 */
DEBUG_INFO(TAG, "esp_ota_begin(SEQUENTIAL) 目标=%s size=%u free=%lu...",
m_partition->label, (unsigned)total_size,
/* ★ 整分区预擦(非 SEQUENTIAL 边写边擦):
* 1) 扇区循环 erase + 喂狗/yield,避免 TASK_WDT 与 WiFi 饿死
* 2) esp_ota_resume(erase_size≠SEQUENTIAL) → need_erase=false,写路径纯 write
* 3) 显式 invalidate inactive slot(resume 不像 begin 那样自动做)
* 预擦会在 begin 阶段占用数秒~数十秒,之后收流写 Flash 更快更稳。 */
const size_t part_size = m_partition->size;
const size_t sector = m_partition->erase_size ? m_partition->erase_size : 4096U;
DEBUG_INFO(TAG, "整分区预擦: %s size=%u sector=%u free=%lu...",
m_partition->label, (unsigned)part_size, (unsigned)sector,
(unsigned long)esp_get_free_heap_size());
const int64_t t0 = esp_timer_get_time();
esp_err_t err = esp_ota_begin(m_partition, OTA_WITH_SEQUENTIAL_WRITES, &m_handle);
for (size_t off = 0; off < part_size; ) {
size_t chunk = sector;
if (off + chunk > part_size) {
chunk = part_size - off;
}
esp_err_t erase_err = esp_partition_erase_range(m_partition, off, chunk);
if (erase_err != ESP_OK) {
DEBUG_ERR(TAG, "整分区预擦失败 @%u: %s",
(unsigned)off, esp_err_to_name(erase_err));
return beginCleanupAndReturn(erase_err);
}
off += chunk;
(void)esp_task_wdt_reset();
taskYIELD();
}
const int64_t t1 = esp_timer_get_time();
DEBUG_INFO(TAG, "整分区预擦完成 耗时 %lld ms",
(long long)((t1 - t0) / 1000));
#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) || defined(CONFIG_APP_ROLLBACK_ENABLE)
{
esp_err_t inv = esp_ota_invalidate_inactive_ota_data_slot();
if (inv != ESP_OK && inv != ESP_ERR_NOT_SUPPORTED) {
DEBUG_WARN(TAG, "invalidate inactive OTA slot: %s(继续)",
esp_err_to_name(inv));
}
}
#endif
/* erase_size 传 0(≠ OTA_WITH_SEQUENTIAL_WRITES)→ 写时不再擦扇区 */
esp_err_t err = esp_ota_resume(m_partition, /*erase_size=*/0,
/*image_offset=*/0, &m_handle);
const int64_t t2 = esp_timer_get_time();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s (耗时 %lld ms)",
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
DEBUG_ERR(TAG, "esp_ota_resume 失败: %s (耗时 %lld ms)",
esp_err_to_name(err), (long long)((t2 - t1) / 1000));
return beginCleanupAndReturn(err);
}
DEBUG_INFO(TAG, "esp_ota_begin 完成 耗时 %lld ms free=%lu",
(long long)((t1 - t0) / 1000),
DEBUG_INFO(TAG, "esp_ota_resume 完成 耗时 %lld ms free=%lu",
(long long)((t2 - t1) / 1000),
(unsigned long)esp_get_free_heap_size());
m_received = 0;
@@ -196,7 +231,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
m_ended = false;
memset(m_sha256, 0, sizeof(m_sha256));
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase)",
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (full-partition erase)",
m_partition->label, (unsigned)total_size);
return ESP_OK;
}
@@ -213,11 +248,9 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
return ESP_ERR_INVALID_STATE;
}
/* sequential 模式写时可能触发扇区擦除(禁 cache),期间 WiFi 易饿死。
* 写后 yield 一次即可;写前再 yield 会无谓拖慢吞吐(httpd 侧另有堆感知 pacing)。 */
/* 整分区已预擦:此处为纯 write,不再扇区擦除。 */
(void)esp_task_wdt_reset();
esp_err_t err = esp_ota_write(m_handle, data, len);
taskYIELD();
if (err == ESP_OK) {
m_received += len;
} else {
+3 -4
View File
@@ -5,7 +5,7 @@
* begin → write*(N) → finalize / abort
*
* 统一以 WiFi 流程为基准:
* - esp_ota_begin 第二参数传 total_size
* - begin:整分区预擦后 esp_ota_resume(写路径不再边写边擦)
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
* - finalize 顺序: 读回 SHA-256 → 比对前端 SHA → esp_ota_end → esp_ota_set_boot_partition → 可选重启
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
@@ -102,16 +102,15 @@ public:
/** @brief 获取已接收的字节数。 */
size_t getReceived() const { return m_received; }
// ── 分区互斥锁管理(M1: 从 ota_mutex.h 全局变量内聚到此类)──
// ── 分区互斥锁管理 ──
/**
* @brief 初始化 ESP32 OTA 分区互斥锁(在 app_main 中调用)。
* 替代旧的全局 g_esp32OtaMutex / g_esp32OtaOccupied。
*/
static void initMutex();
private:
Esp32OtaCore() = default;
// ── 分区互斥锁(M1: 内聚自 ota_mutex.h)──
// ── 分区互斥锁 ──
static SemaphoreHandle_t s_mutex; ///< ESP32 OTA 分区互斥锁
static volatile bool s_occupied; ///< 分区占用标志
static bool acquireMutex(uint32_t timeout_ms);
+75 -25
View File
@@ -98,13 +98,13 @@ void OtaCoordinator::watchdogCheckLocked()
return; // 未超时
}
// ★ 超时自愈:强制转 ERROR。
// ERROR 不在任何 beginXxx() 的拒绝列表中,下次请求会自动从 ERROR 恢复。
// 清理由各子系统自行负责(abortCurrent 中会按 ERROR 不做实际清理,仅复位状态)。
DEBUG_ERR(TAG, "忙状态 %d 卡死超过 %" PRIu32 " ms,强制自愈 → ERROR",
// ★ 超时自愈:完整中止到 IDLE(释放 Esp32OtaCore / 中继 / GD32 meta 等)。
// 旧逻辑只标 ERROR 不清理子系统:若卡在 FINALIZING 且 core 已半释放,
// 前端仍长时间判“占用”;若 core 仍持互斥,ERROR 后 begin 还要靠 forceCleanup。
// REBOOTING 本应已 esp_restart;超时则同样中止,避免永久卡死。
DEBUG_ERR(TAG, "忙状态 %d 卡死超过 %" PRIu32 " ms,强制中止 → IDLE",
(int)m_sysState, (uint32_t)pdTICKS_TO_MS(kBusyWatchdogTicks));
m_sysState = OTA_SYS_ERROR;
m_busySinceTick = 0;
doAbortLocked();
}
// ============================================================
@@ -124,7 +124,7 @@ void OtaCoordinator::init()
m_gd32StorageDone = false;
m_busySinceTick = 0;
// ★ H4 解耦:注册协调器为 OtaRelay 的中继事件监听器
// 注册协调器为 OtaRelay 的中继事件监听器
OtaRelay::setRelayListener(this);
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
@@ -221,12 +221,27 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
return err;
}
// 3. 复位按需擦除水位(不在此处整段预擦)
// 旧实现在 prepare 阶段 erase 整包固件对应分区:HTTP 主线程阻塞数秒~数十秒,
// 期间不 httpd_req_recv → TCP 窗口塞满后浏览器进度先到 100%,再空等擦+写+SHA。
// 现改为 flashWriteChunk 内按扇区 ensureErased,边收边擦写,进度与真实处理对齐。
// 3. 整分区预擦(不再边写边擦):prepare 阶段擦完 gd32_ota 分区,
// 收流时 flashWriteChunk 仅 write,吞吐更稳。会阻塞数秒~数十秒。
Gd32FlashStore::instance().beginWriteSession();
DEBUG_INFO(TAG, "prepareGd32Storage: 按需擦除会话已复位 (fw=%" PRIu32 ")", total_size);
{
const esp_partition_t *gd32_part = Gd32FlashStore::instance().getPartition();
const uint32_t erase_bytes = gd32_part ? (uint32_t)gd32_part->size : 0U;
if (erase_bytes == 0U) {
DEBUG_ERR(TAG, "prepareGd32Storage: 找不到 gd32_ota 分区");
abortCurrent();
return ESP_ERR_NOT_FOUND;
}
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦 %u 字节 (fw=%" PRIu32 ")...",
(unsigned)erase_bytes, total_size);
err = Gd32FlashStore::instance().erasePartition(erase_bytes);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "prepareGd32Storage: 整分区预擦失败: %s", esp_err_to_name(err));
abortCurrent();
return err;
}
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦完成");
}
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
clearGd32Meta();
@@ -270,7 +285,7 @@ esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
}
// ============================================================
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
// GD32 存储操作(传输层统一入口)
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
// ============================================================
@@ -394,7 +409,7 @@ void OtaCoordinator::notifyRelayComplete(bool success)
}
// ============================================================
// IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)
// IRelayListener 接口实现
// ============================================================
/** @brief IRelayListener 回调:中继已开始,委托给 notifyRelayStarted()。 */
void OtaCoordinator::onRelayStarted()
@@ -409,7 +424,7 @@ void OtaCoordinator::onRelayComplete(bool success)
}
// ============================================================
// BLE 射频共存管理(L1: 消除 ble_ota → wifi_ota 直接依赖)
// BLE 射频共存管理
// ============================================================
void OtaCoordinator::setBleCoexListener(IBleCoexListener *listener)
{
@@ -443,7 +458,6 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
// FINALIZING/REBOOTING 是不可逆阶段,也拒绝。
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
if (m_otaChannel != channel) {
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
@@ -458,14 +472,23 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
}
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 收尾/重启中,均拒绝。
// ★ 必须显式拒绝 ESP32_FINALIZING/REBOOTING:
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
// ★ FINALIZING 残留自愈:SHA 不匹配时 core 已 abort,但旧固件可能未把协调器
// 复位到 IDLE。core 已空闲 ⇒ 协调器状态是假占用,允许清掉后开新会话。
// core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。
if (h && m_sysState == OTA_SYS_ESP32_FINALIZING) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32_FINALIZING 残留(core 已空闲),强制复位后允许新会话");
doAbortLocked();
} else {
DEBUG_WARN(TAG, "系统忙 (ESP32_FINALIZING, core 活跃),拒绝 ESP32 OTA");
unlock(h);
return ESP_ERR_INVALID_STATE;
}
}
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 重启中,均拒绝。
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
(int)m_sysState, stateToString(m_sysState));
@@ -586,6 +609,13 @@ Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_h
r.device_sha_hex);
if (r.err == ESP_OK) {
notifyEsp32FinalizeComplete();
} else {
// ★ SHA 不匹配 / end 失败:Esp32OtaCore 已 abort+释放互斥,但协调器若仍停在
// ESP32_FINALIZING,WiFi 前端会一直被“设备 OTA 占用”拒绝(BLE 路径曾单独
// 补 abortCurrent;统一放到这里,WiFi/BLE 共用)。
DEBUG_ERR(TAG, "ESP32 finalize 失败: %s,复位协调器 → IDLE",
esp_err_to_name(r.err));
abortCurrent();
}
return r;
}
@@ -632,6 +662,26 @@ void OtaCoordinator::abortCurrent()
wifiOtaResumeBleIfNeeded();
}
void OtaCoordinator::healOrphanStates()
{
bool need_resume = false;
bool h = lock();
if (h) {
watchdogCheckLocked();
// finalize 失败后 core 已空、协调器仍 FINALIZING 的假占用:status 轮询即可自愈
if (m_sysState == OTA_SYS_ESP32_FINALIZING &&
!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "status 路径发现 ESP32_FINALIZING 残留,自愈 → IDLE");
doAbortLocked();
need_resume = true;
}
unlock(h);
}
if (need_resume) {
wifiOtaResumeBleIfNeeded();
}
}
uint32_t OtaCoordinator::getOtaEpoch() const
{
bool h = lock();
@@ -665,7 +715,7 @@ bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
ota_system_state_t OtaCoordinator::getSystemState() const
{
// 对于纯读取,短暂加锁保证一致性
// ★ M1/L3: m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
// m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
bool h = lock();
ota_system_state_t s = m_sysState;
unlock(h);
@@ -720,7 +770,7 @@ bool OtaCoordinator::getGd32AppAlive() const
}
// ============================================================
// OTA 状态快照(PR1: 收口状态读取)
// OTA 状态快照
// ============================================================
OtaSnapshot OtaCoordinator::getSnapshot() const
{
@@ -754,7 +804,7 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
}
// ============================================================
// GD32 APP 探测(PR2: 从 /system/status 移出)
// GD32 APP 探测
// ============================================================
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
+24 -14
View File
@@ -21,7 +21,7 @@
#include "esp_err.h"
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
#include "storage/gd32_flash_store.h"
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot(H4 解耦)
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot
// ============================================================
// 系统级 OTA 状态枚举
@@ -47,7 +47,7 @@ typedef enum {
} ota_channel_t;
// ============================================================
// OTA 系统全量只读快照(PR1: 收口状态读取)
// OTA 系统全量只读快照
// ============================================================
/**
* @brief OTA 系统全量只读快照。
@@ -64,11 +64,11 @@ struct OtaSnapshot {
};
// ============================================================
// IBleCoexListener - BLE 射频共存通知接口(解决 L1 横向耦合)
// IBleCoexListener - BLE 射频共存通知接口
// ============================================================
// WifiOta 实现此接口并注册到 OtaCoordinator,
// BLE OTA 开始/结束时通过协调器通知 WiFi 层,
// 消除 ble_ota → wifi_ota 的直接依赖。
// 避免 ble_ota → wifi_ota 的直接依赖。
/**
* @brief BLE 射频共存监听器接口。
* BLE OTA 开始/结束时,协调器通过此接口通知 WiFi 层暂停/恢复射频活动。
@@ -124,8 +124,8 @@ public:
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
/**
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 复位按需擦除水位 + 清空 meta + 写入初始 meta。
* 扇区擦除在 writeGd32Chunk/flashWriteChunk 内按写入范围推进(非整段预擦)。
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 整分区预擦 + 清空 meta + 写入初始 meta。
* 收流时 write 路径只写 Flash(ensureErased 仅作水位兜底)。
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE)。
@@ -146,7 +146,7 @@ public:
*/
esp_err_t finishGd32StorageAfterReceive();
// ── GD32 存储操作(传输层通过协调器操作 Flash,解决 H3 绕过问题)──
// ── GD32 存储操作(传输层通过协调器操作 Flash)──
/**
* @brief 向 GD32 Flash 分区写入固件数据块。
* 传输层(WiFi/BLE)通过此接口写入,不再直接调用 Gd32FlashStore。
@@ -312,7 +312,9 @@ public:
/**
* @brief ESP32 OTA 收尾(统一封装 BLE/WiFi 两端重复的收尾序列)。
* 步骤:notifyEsp32ReceiveComplete → Esp32OtaCore::finalize(reboot=false)
* → 成功则 notifyEsp32FinalizeComplete。
* → 成功则 notifyEsp32FinalizeComplete;
* → 失败(SHA 不匹配 / end 失败等)则 abortCurrent() 复位到 IDLE,
* 避免协调器卡在 ESP32_FINALIZING 导致后续上传被假拒绝。
* 不重启;调用方负责把 device_sha_hex 上报前端后再调 restartEsp32()。
* @param frontend_sha_hex 前端 64 字符 hex SHA-256,nullptr 表示跳过比对。
* @return Esp32FinalizeResult,err 为 ESP_OK/ESP_ERR_INVALID_VERSION/其他。
@@ -333,6 +335,14 @@ public:
*/
void abortCurrent();
/**
* @brief 自愈孤儿忙态(供 /system/status 等轮询路径调用)。
* - 跑忙状态 watchdog(超时完整 abort → IDLE)
* - ESP32_FINALIZING 但 Esp32OtaCore 已空闲:判定为 SHA 失败等残留,复位 IDLE
* 不影响真正正在 finalize(core.isActive())的会话。
*/
void healOrphanStates();
/**
* @brief 获取当前 OTA 会话的 epoch(代次)。
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
@@ -354,7 +364,7 @@ public:
// ── 状态查询 ──
/**
* @brief 一次性读取系统 OTA 全量状态快照(PR1: 收口状态读取)。
* @brief 一次性读取系统 OTA 全量状态快照。
* coordinator mutex + relay mutex 各取一次,
* 供 /system/status 等只读接口消费。
* @return 包含 sys_state、meta、relay 进度的完整快照。
@@ -362,7 +372,7 @@ public:
OtaSnapshot getSnapshot() const;
/**
* @brief 单次 SPI 探测 GD32 APP 是否存活(PR2: 从 status 移出)。
* @brief 单次 SPI 探测 GD32 APP 是否存活。
* 成功时更新 OtaRelay 缓存的 alive/version,
* 失败时清除 alive 标志。
* @param out_version 输出版本信息,可为 nullptr。
@@ -421,7 +431,7 @@ public:
*/
void reset();
// ── IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)──
// ── IRelayListener 接口实现 ──
/**
* @brief IRelayListener 回调:中继已开始。
* 由 OtaRelay task 在 forwardFirmware() 前通过 s_relayListener 调用。
@@ -435,7 +445,7 @@ public:
*/
void onRelayComplete(bool success) override;
// ── BLE 射频共存管理(L1: 替代 ble_ota → wifi_ota 直接调用)──
// ── BLE 射频共存管理 ──
/**
* @brief 注册 BLE 共存监听器(由 main 在初始化时调用)。
* @param listener WifiOta 实现的监听器指针,nullptr 取消注册。
@@ -498,10 +508,10 @@ private:
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
// 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast)
// 互斥锁(mutable 允许在 const 方法中加锁,无需 const_cast)
mutable SemaphoreHandle_t m_mutex = nullptr;
// BLE 共存监听器(L1: 由 WifiOta 实现并注册)
// BLE 共存监听器(由 WifiOta 实现并注册)
IBleCoexListener *m_bleCoexListener = nullptr;
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100);
+9 -10
View File
@@ -1,7 +1,7 @@
/*
* ota_relay.cpp - OTA 中继事件驱动状态机实现
*
* 将原 17 状态"声明式赋值"重构为 7 状态事件驱动架构:
* 7 状态事件驱动架构:
* - 每个协议阶段封装为一个状态 handler
* - processEvent() 根据当前状态分发事件
* - 状态转换显式通过 setState() 完成
@@ -24,8 +24,8 @@
#include "protocol.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
// OtaRelay 不再直接 #include "ota_coordinator.h",
// 通过 IRelayListener 接口回调通知协调器,依赖方向为单向。
// ============================================================
// 公共拒绝事件判断(文件内工具)
@@ -42,10 +42,9 @@ static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
}
// ============================================================
// doRelayPackets 重试阈值(与历史代码字面值保持一致,仅命名化)
// doRelayPackets 重试阈值(仅命名化字面值)
// ============================================================
// 原代码中 retry_count 阈值:行 1015/1075/1084 用 10U,行 1032/1050 用 3U。
// 含义:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
// retry_count 阈值:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
@@ -67,7 +66,7 @@ SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
TaskHandle_t OtaRelay::taskHandle = NULL;
IRelayListener *OtaRelay::s_relayListener = nullptr;
// ── 中继事件监听器(解决 H4 双向耦合)──
// ── 中继事件监听器 ──
void OtaRelay::setRelayListener(IRelayListener *listener)
{
s_relayListener = listener;
@@ -78,7 +77,7 @@ IRelayListener* OtaRelay::getRelayListener()
return s_relayListener;
}
// ── 队列访问器(M3: 封装 public static 成员)──
// ── 队列访问器 ──
QueueHandle_t OtaRelay::getDispatchQueue()
{
return dispatchQueue;
@@ -1415,7 +1414,7 @@ void OtaRelay::taskEntry(void *pvParameters)
}
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
// 通过 IRelayListener 接口回调通知协调器
IRelayListener *listener = getRelayListener();
if (listener) listener->onRelayStarted();
ota_result_t result = instance().forwardFirmware();
@@ -1432,7 +1431,7 @@ void OtaRelay::taskEntry(void *pvParameters)
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
}
instance().reset();
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
// 通过 IRelayListener 接口回调通知协调器中继结束
listener = getRelayListener();
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
}
+9 -11
View File
@@ -63,15 +63,15 @@ typedef enum {
} ota_result_t;
// ============================================================
// IRelayListener - 中继事件监听接口(解决 H4 双向耦合)
// IRelayListener - 中继事件监听接口
// ============================================================
// OtaCoordinator 实现此接口并注册到 OtaRelay,
// OtaRelay 通过接口回调通知协调器,不再直接 #include ota_coordinator.h。
// 依赖方向变为单向:OtaCoordinator → OtaRelay(通过接口)。
// 依赖方向为单向:OtaCoordinator → OtaRelay(通过接口)。
/**
* @brief 中继事件监听器接口。
* OtaCoordinator 实现此接口,OtaRelay 通过回调通知状态变更,
* 消除 OtaRelay → OtaCoordinator 的直接依赖(解决 H4 双向耦合)。
* 避免 OtaRelay → OtaCoordinator 的直接依赖。
*/
class IRelayListener {
public:
@@ -181,7 +181,7 @@ public:
*/
void reset();
// ── 中继事件监听器(解决 H4 双向耦合)──
// ── 中继事件监听器 ──
/**
* @brief 注册中继事件监听器(由 OtaCoordinator 在 init 时调用)。
* @param listener 监听器指针,传 nullptr 取消注册。
@@ -193,7 +193,7 @@ public:
*/
static IRelayListener* getRelayListener();
// ── 队列访问器(解决 M3: 封装 public static 成员)──
// ── 队列访问器 ──
/**
* @brief 获取事件分发队列句柄。
* 外部模块(OtaCoordinator / BLE)通过 getter 访问,不再直接引用静态成员。
@@ -207,8 +207,6 @@ public:
*/
static QueueHandle_t getResultQueue();
// ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)──
// ── 线程安全锁 ──
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
@@ -239,11 +237,11 @@ public:
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
private:
// ── 队列(M3: 由 getter 访问,外部不再直接引用)──
// ── 队列(由 getter 访问)──
static QueueHandle_t dispatchQueue; ///< 事件分发队列
static QueueHandle_t resultQueue; ///< 结果回传队列
// ── 状态成员(全部改 private,统一通过 public setter/getter 访问)──
// ── 状态成员(统一通过 public setter/getter 访问)──
// 外部代码使用 getState()/getRelaySeq()/... 读取,setState()/setRelaySeq()/... 写入。
// 内部代码:若已在 StateLockGuard 块内(reset/getSnapshot/启动探测),可直接访问;
// 否则必须使用 setter/getter,禁止裸读写(消除数据竞争)。
@@ -252,7 +250,7 @@ private:
static uint32_t relayTotalPackets; ///< 本次中继总包数
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
static gd32_version_t gd32Version; ///< GD32 固件版本信息
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
static IRelayListener *s_relayListener; ///< 中继事件监听器
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁(由 StateLockGuard 使用)
// taskHandle 为 write-once-at-init 字段:仅 taskEntry 启动首行写入一次,
@@ -331,7 +329,7 @@ private:
/**
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
*
* 内含重试循环,使用以下命名常量(与历史阈值保持一致):
* 内含重试循环,使用以下命名常量:
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
* kMaxResendOnStructError=3(结构化错误重发上限)
*
+32 -16
View File
@@ -66,7 +66,8 @@
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW + 4)
// 深度略大于窗口:seq=0 整分区预擦期间前端若仍误发,可多扛几批,减少“队列满丢包→重传风暴”。
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW * 2 + 8)
struct BleOtaDataBatch {
uint8_t *data;
@@ -83,7 +84,7 @@ struct BleEsp32FinishCtx {
uint16_t session_id;
};
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免历史多次漏 free。
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免漏 free。
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
// 但这是设备重启路径,资源由系统回收。
struct BleFinishCtxDeleter {
@@ -236,13 +237,10 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
// ============================================================
// WiFi 共存策略:BLE OTA 传输期间的处理
//
// 历史:曾在此处把 WiFi 切到 WIFI_PS_MAX_MODEM 以把空中时间让给 BLE。
// 但 MAX_MODEM 下 STA 监听间隔被拉到极大,HTTP server 的 TCP 握手/数据包
// 会大量丢失或延迟到超时 → BLE OTA 期间任何 WiFi 请求(包括互斥拒绝应返回的
// 403)都到不了设备,前端 XHR 长时间无响应,表现为"Wifi 上传既不成功也不报错"。
//
// 现策略:不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
// 不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
// 协调器照常以 403 拒绝并发 OTA。ESP32-C3 coex 默认调度已能保证 BLE OTA 可用。
// (曾尝试切到 WIFI_PS_MAX_MODEM 让出空中时间,但 STA 监听间隔被拉到极大,
// HTTP server 的 TCP 握手/数据包大量丢失或超时,前端 XHR 长时间无响应。)
// 仍保留 setBleOtaActive() 暂停 RSSI 轮询(避免 get_ap_info 的射频活动干扰 BLE)。
// ============================================================
static bool s_wifi_ps_boosted = false;
@@ -254,7 +252,7 @@ static void ble_ota_boost_wifi_ps()
{
if (s_wifi_ps_boosted) return;
s_wifi_ps_boosted = true;
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
// 通过协调器通知 WiFi 层
OtaCoordinator::instance().notifyBleCoexStart();
DEBUG_INFO(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
}
@@ -266,7 +264,7 @@ static void ble_ota_restore_wifi_ps()
{
if (!s_wifi_ps_boosted) return;
s_wifi_ps_boosted = false;
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
// 通过协调器通知 WiFi 层
OtaCoordinator::instance().notifyBleCoexEnd();
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
}
@@ -933,7 +931,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
ctx->session_id = session_id;
BaseType_t ok = xTaskCreate(ble_gd32_relay_wait_task,
"ble_gd32_wait",
3072,
TASK_STACK_BLE_GD32_WAIT,
ctx,
5,
NULL);
@@ -992,7 +990,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
TASK_STACK_BLE_FINISH,
finish_ctx, // heap context; task 负责 free
5,
NULL);
@@ -1015,7 +1013,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
"ble_ota_restart",
2048,
TASK_STACK_BLE_RESTART,
NULL,
5,
NULL);
@@ -1223,17 +1221,25 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
//
// ★ seq=0 强制 ACK:begin 含整分区预擦,可能耗时数秒~数十秒。
// 前端必须等“首包已落盘”ACK 后再发窗口内其余包,否则队列满丢包→重传极慢。
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
BLE_OTA_CHUNK_PAYLOAD;
bool reachedAckBoundary = completedPackets > 0U &&
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
bool firstPacketReady = (seq == 0U) && progressMade;
if (progressMade &&
(reachedAckBoundary ||
transferComplete ||
isLastPacket)) {
isLastPacket ||
firstPacketReady)) {
if (isLastPacket || transferComplete) {
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
seq, data_len, (unsigned int)afterReceived, total_size);
} else if (firstPacketReady) {
DEBUG_INFO(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
(unsigned)afterReceived, total_size);
}
uint16_t ackSeq = s_reorder.expected > 0U
? (uint16_t)(s_reorder.expected - 1U)
@@ -1384,7 +1390,9 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
meta.stored_size = stored_size;
meta.last_seq = seq;
const bool transfer_complete = stored_size >= meta.total_size;
// seq=0:prepare 含整分区预擦,写完首包必须 ACK,前端才能继续发后续窗口
const bool checkpoint =
(seq == 0U) ||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
transfer_complete ||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
@@ -1681,6 +1689,14 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
s_conn_fallback_requested = true;
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
}
/* 优先 LE 2M PHY(空中速率约翻倍);对端不支持则保持 1M,失败仅打日志 */
{
const uint8_t phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK;
int phy_rc = ble_gap_set_prefered_le_phy(
BleOta::instance().m_connHandle, phy_mask, phy_mask,
BLE_GAP_LE_PHY_CODED_ANY);
DEBUG_INFO(TAG, "请求 LE PHY 1M|2M: rc=%d", phy_rc);
}
} else {
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
BleOta::instance().m_bleConnected = false;
@@ -2040,7 +2056,7 @@ esp_err_t BleOta::resumeStack()
m_suspendedForWifiOta = false;
if (!m_stackRunning) {
/* 仅当历史上曾 hard-deinit 过才需要完整 serverStart */
/* 仅当曾 hard-deinit 过才需要完整 serverStart */
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
(unsigned long)esp_get_free_heap_size());
esp_err_t err = serverStart();
@@ -2125,7 +2141,7 @@ void BleOta::taskEntry(void *pvParameters)
if (s_otaDataQueue) {
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
"ble_ota_worker",
4096, // 峰值仅 ~1.9KB,4KB 留有充足余量
TASK_STACK_BLE_WORKER, // 实测 peak~1.1KB,负载峰值曾~1.9KB
NULL,
10, // 中等优先级,避免被 NimBLE host task 过度抢占
NULL);
+15 -181
View File
@@ -1,6 +1,5 @@
/*
* http_handlers.cpp - HTTP 请求处理器实现
* 从 wifi_ota.cpp 提取的 HTTP handler 函数
*/
#include <string.h>
#include <stdarg.h>
@@ -28,6 +27,7 @@
#include "storage/gd32_flash_store.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
#include "monitor/cpu_stats.h"
#include "spi_master.h"
#include "canfd_bridge.h"
#include "monitor/can_feedback_task.h"
@@ -208,7 +208,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
esp_wifi_set_ps(WIFI_PS_NONE);
// ★ L1: WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
// WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
OtaCoordinator::instance().notifyBleCoexEnd();
char peek;
@@ -307,7 +307,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
const uint32_t free_pre = esp_get_free_heap_size();
if (free_pre < 8192U) {
(void)esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 2U));
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1));
}
}
@@ -615,12 +615,10 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
valid[i] = getLatestCanFeedback(i, &results[i]);
}
/* 估算缓冲大小:每通道 ~2KB (统计+ID条目) */
char *resp = (char *)calloc(1, 8192);
if (!resp) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
return ESP_FAIL;
}
/* ★ 修复:改用静态缓冲区,避免每次请求 malloc 8KB 加剧堆碎片化 */
static char s_feedback_json[8192];
char *resp = s_feedback_json;
memset(resp, 0, sizeof(s_feedback_json));
char *p = resp;
p += sprintf(p, "{\"channels\":[");
@@ -670,7 +668,6 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, (int)(p - resp));
free(resp);
return ESP_OK;
}
@@ -882,7 +879,6 @@ esp_err_t spi_send_handler(httpd_req_t *req)
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
static SemaphoreHandle_t s_status_json_mux = nullptr;
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
@@ -899,167 +895,6 @@ static SemaphoreHandle_t statusJsonMutex(void)
return s_status_json_mux;
}
struct CpuTaskSample {
UBaseType_t task_number;
configRUN_TIME_COUNTER_TYPE runtime;
};
struct CpuTaskMetric {
const char* name;
UBaseType_t task_number;
uint64_t runtime_delta;
double usage_pct;
};
struct CpuUsageSnapshot {
bool available;
bool valid;
uint32_t sample_ms;
double usage_pct;
double idle_pct;
CpuTaskMetric tasks[CPU_TASK_SNAPSHOT_MAX];
size_t task_count;
};
static portMUX_TYPE s_cpu_stats_mux = portMUX_INITIALIZER_UNLOCKED;
static CpuTaskSample s_cpu_last_samples[CPU_TASK_SNAPSHOT_MAX];
static size_t s_cpu_last_count = 0;
static configRUN_TIME_COUNTER_TYPE s_cpu_last_total = 0;
static int64_t s_cpu_last_time_us = 0;
static bool s_cpu_has_last = false;
static uint64_t runtime_delta(configRUN_TIME_COUNTER_TYPE current, configRUN_TIME_COUNTER_TYPE previous)
{
return current >= previous ? (uint64_t)(current - previous) : 0U;
}
static bool task_name_is_idle(const char* name)
{
return name && strncmp(name, "IDLE", 4) == 0;
}
static void reset_cpu_usage_baseline_locked(const TaskStatus_t* tasks, size_t task_count,
configRUN_TIME_COUNTER_TYPE total_runtime,
int64_t now_us)
{
size_t copy_count = std::min(task_count, CPU_TASK_SNAPSHOT_MAX);
for (size_t i = 0; i < copy_count; ++i) {
s_cpu_last_samples[i].task_number = tasks[i].xTaskNumber;
s_cpu_last_samples[i].runtime = tasks[i].ulRunTimeCounter;
}
s_cpu_last_count = copy_count;
s_cpu_last_total = total_runtime;
s_cpu_last_time_us = now_us;
s_cpu_has_last = true;
}
// ★ 改为输出参数模式:CpuUsageSnapshot 608B + CpuTaskSample[24] 192B = 800B
// 之前全在栈上,system_status_handler 又拷一份 608B → 叠加 1.4KB,
// 加上 OtaSnapshot/TaskStackMetric/wifi_ap_record_t 约 2.3KB 栈消耗。
// HTTP 主线程上传 100% 后调用此 handler 时极易栈溢出。改堆分配。
static void sample_cpu_usage(CpuUsageSnapshot& result)
{
result = {};
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
result.available = true;
UBaseType_t task_count = uxTaskGetNumberOfTasks();
if (task_count == 0) {
return;
}
task_count = std::min<UBaseType_t>(task_count + 2U, CPU_TASK_SNAPSHOT_MAX);
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
if (!tasks) {
return;
}
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
UBaseType_t captured_count = uxTaskGetSystemState(tasks, task_count, &total_runtime);
int64_t now_us = esp_timer_get_time();
if (captured_count == 0 || captured_count > task_count) {
DEBUG_WARN("STATUS", "CPU stats: uxTaskGetSystemState 返回 0 (actual_tasks=%lu, buf_slots=%lu)",
(unsigned long)uxTaskGetNumberOfTasks(), (unsigned long)task_count);
free(tasks);
return;
}
// ★ previous_samples[24] = 192B,改堆分配减少栈压力
CpuTaskSample* previous_samples = (CpuTaskSample*)calloc(CPU_TASK_SNAPSHOT_MAX, sizeof(CpuTaskSample));
if (!previous_samples) {
free(tasks);
return;
}
size_t previous_count = 0;
configRUN_TIME_COUNTER_TYPE previous_total = 0;
int64_t previous_time_us = 0;
bool had_previous = false;
portENTER_CRITICAL(&s_cpu_stats_mux);
previous_count = s_cpu_last_count;
previous_total = s_cpu_last_total;
previous_time_us = s_cpu_last_time_us;
had_previous = s_cpu_has_last;
for (size_t i = 0; i < previous_count; ++i) {
previous_samples[i] = s_cpu_last_samples[i];
}
reset_cpu_usage_baseline_locked(tasks, captured_count, total_runtime, now_us);
portEXIT_CRITICAL(&s_cpu_stats_mux);
bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
uint64_t total_delta = can_compute ? runtime_delta(total_runtime, previous_total) : 0U;
uint64_t idle_delta = 0U;
if (can_compute && total_delta > 0U) {
for (UBaseType_t i = 0; i < captured_count; ++i) {
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
bool found_previous = false;
for (size_t j = 0; j < previous_count; ++j) {
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
last_runtime = previous_samples[j].runtime;
found_previous = true;
break;
}
}
if (!found_previous) {
continue;
}
uint64_t delta = runtime_delta(tasks[i].ulRunTimeCounter, last_runtime);
double pct = (double)delta * 100.0 / (double)total_delta;
if (task_name_is_idle(tasks[i].pcTaskName)) {
idle_delta += delta;
}
if (result.task_count < CPU_TASK_SNAPSHOT_MAX) {
CpuTaskMetric& metric = result.tasks[result.task_count++];
metric.name = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
metric.task_number = tasks[i].xTaskNumber;
metric.runtime_delta = delta;
metric.usage_pct = pct;
}
}
result.valid = true;
result.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
result.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
result.usage_pct = std::max(0.0, 100.0 - result.idle_pct);
}
if (!result.valid) {
DEBUG_DBG("STATUS", "CPU stats: valid=false had_prev=%d total=%lu prev_total=%lu "
"can_compute=%d delta=%lu captured=%lu",
(int)had_previous,
(unsigned long)total_runtime, (unsigned long)previous_total,
(int)can_compute, (unsigned long)total_delta,
(unsigned long)captured_count);
}
free(previous_samples);
free(tasks);
#else
result.available = false;
#endif
}
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
{
if (handle == NULL) return 0;
@@ -1089,6 +924,7 @@ static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
return offset + (int)written;
}
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样) */
static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu)
{
offset = append_jsonf(buf, cap, offset,
@@ -1168,7 +1004,9 @@ esp_err_t system_status_handler(httpd_req_t *req)
connected = true;
}
// ★ PR1: 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
// 先自愈孤儿忙态(如 SHA 失败后卡在 ESP32_FINALIZING),再取快照给 UI
OtaCoordinator::instance().healOrphanStates();
// 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
size_t free_heap = esp_get_free_heap_size();
@@ -1190,20 +1028,17 @@ esp_err_t system_status_handler(httpd_req_t *req)
}
}
/* CPU 采样在堆上;失败则跳过(available=false),不再 500 */
/* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */
CpuUsageSnapshot cpu_local = {};
CpuUsageSnapshot *cpu = &cpu_local;
CpuUsageSnapshot *cpu_heap = nullptr;
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 1024U)) {
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 512U)) {
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
if (cpu_heap) {
cpu = cpu_heap;
sample_cpu_usage(*cpu);
}
} else {
cpu_local.available = false;
cpu_local.valid = false;
}
CpuStats::instance().getSnapshot(*cpu);
char *resp = s_status_json;
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
@@ -1309,11 +1144,10 @@ esp_err_t system_status_handler(httpd_req_t *req)
}
// ============================================================
// GET /system/probe — GD32 APP 单次 SPI 探测(PR2)
// GET /system/probe — GD32 APP 单次 SPI 探测
// ============================================================
/**
* @brief 显式触发一次 GD32 APP 存活探测。
* 从 system_status_handler 移出的副作用,改为独立端点。
* 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。
*/
esp_err_t system_probe_handler(httpd_req_t *req)
+15 -13
View File
@@ -166,7 +166,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
inst.setStaRetryCount(0);
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
inst.stateUnlock();
// ★ M2: 推送 WiFi 连接状态给 HealthMonitor(解耦监控层主动查询)
// 推送 WiFi 连接状态给 HealthMonitor
HealthMonitor::instance().setWifiConnected(true);
break;
@@ -177,7 +177,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
bool exhausted = inst.getStaRetryExhausted();
int retryCount = inst.getStaRetryCount();
inst.stateUnlock();
// ★ M2: 推送 WiFi 断开状态给 HealthMonitor
// 推送 WiFi 断开状态给 HealthMonitor
HealthMonitor::instance().setWifiConnected(false);
if (!exhausted) {
@@ -401,15 +401,17 @@ esp_err_t WifiOta::httpServerStart()
config.stack_size = HTTP_SERVER_STACK_SIZE;
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
/* LWIP_MAX_SOCKETS=12;HTTP 内部约 3 + UDP 日志 1 → 业务 socket 不宜过大。
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。 */
config.max_open_sockets = 4;
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
config.lru_purge_enable = false;
/* 1MB sequential 擦写时,httpd 在 write 阶段可能 >60s 不调 recv;
* 超时后 handler 内会重试,但过短会放大“卡 100%”观感。 */
config.recv_wait_timeout = 120;
config.send_wait_timeout = 60;
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。
* ★ 修复:增加到 7,防止浏览器超时 abort 后旧连接占满所有槽位。 */
config.max_open_sockets = 7;
/* ★ 修复:启用 LRU 淘汰,当所有 socket 被占满时主动关闭最旧空闲连接。
* OTA 上传期间前端已暂停轮询(pausePollingForUpload),不会踢掉 upload 连接。
* 若仍担心极端情况,可在 ota_upload_handler 入口设置该 session 的更长超时。 */
config.lru_purge_enable = true;
/* ★ 修复:缩短超时,加速释放废弃连接。
* OTA 写 Flash 期间 handler 本身持有连接不会被超时踢(httpd 只在 idle 时检测)。 */
config.recv_wait_timeout = 10;
config.send_wait_timeout = 10;
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
@@ -508,7 +510,7 @@ bool WifiOta::isConnected()
}
// ============================================================
// IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)
// IBleCoexListener 接口实现
// ============================================================
void WifiOta::onBleOtaStart()
{
@@ -603,7 +605,7 @@ bool WifiOta::earlyInit()
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
if (WifiOta::instance().m_reconnectTask == nullptr) {
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
4096, NULL, 5,
TASK_STACK_WIFI_RECONNECT, NULL, 5,
&WifiOta::instance().m_reconnectTask);
if (ret != pdPASS) {
DEBUG_ERR("WIFI", "创建重连任务失败");
+1 -1
View File
@@ -90,7 +90,7 @@ public:
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
// ── IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)──
// ── IBleCoexListener 接口实现 ──
/** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询。 */
void onBleOtaStart() override;
/** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */
+2 -2
View File
@@ -136,7 +136,7 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
return ESP_ERR_INVALID_SIZE;
}
// 写入前按需擦除目标扇区(替代 prepare 阶段整段预擦)
// 正常路径:prepare 已整分区预擦;此处仅兜底(水位未覆盖时才擦)
esp_err_t erase_err = ensureErasedForWrite(aligned_offset, aligned_len);
if (erase_err != ESP_OK) {
return erase_err;
@@ -457,7 +457,7 @@ esp_err_t Gd32FlashStore::metaVerifyChecksum()
// 检查 checksum 是否全零(未计算过)
// 注意:返回 INVALID_STATE 而非 ESP_OK,由调用方决定是否容忍。
// 历史上这里直接返回 OK 跳过校验,但 meta 被意外清零时会掩盖固件损坏。
// 若直接返回 OK 跳过校验,meta 被意外清零时会掩盖固件损坏。
static const uint8_t kZeroSha[32] = {0};
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
+6 -8
View File
@@ -1,9 +1,8 @@
/*
* gd32_flash_store.h - GD32 OTA 固件 Flash 存储抽象层
*
* 从 OtaRelay 中剥离的 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验职责。
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储,
* 不再由 OtaRelay 承担存储职责(解决 H1 职责超载)。
* 封装 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验。
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储。
*
* 线程模型:
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
@@ -72,8 +71,8 @@ public:
esp_err_t erasePartition(uint32_t size);
/**
* @brief 新一轮 GD32 固件写入会话开始时调用:复位按需擦除水位。
* prepareGd32Storage 在开始收流前调用,避免整段预擦阻塞 HTTP/BLE 收包。
* @brief 新一轮 GD32 固件写入会话开始时调用:复位擦除水位。
* prepareGd32Storage 随后会 erasePartition(整分区)。
*/
void beginWriteSession();
@@ -153,9 +152,8 @@ private:
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
// ── 按需擦除水位:已保证 [0, m_erasedUpTo) 为擦除态(扇区对齐)──
// 收流时 flashWriteChunk 按写入范围推进擦除,替代上传前整段预擦。
// 这样 HTTP 主线程可边收边擦写,浏览器进度不会在“100% 发完”后空等整段擦除。
// ── 擦除水位:prepare 整分区预擦后 m_erasedUpTo==part->size;
// flashWriteChunk 内 ensureErased 为安全兜底(水位已覆盖则立即返回)。
static constexpr uint32_t kFlashSectorSize = 4096U;
uint32_t m_erasedUpTo = 0;
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取)
* canfd_bridge.cpp - CANFD 3通道通信桥接实现
*/
#include <string.h>
#include "canfd_bridge.h"
+2 -2
View File
@@ -1,7 +1,7 @@
/*
* canfd_bridge.h - CANFD 3通道通信桥接(M4: 从 SpiMaster 提取)
* canfd_bridge.h - CANFD 3通道通信桥接
*
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责解耦。
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责分离。
* 通过 SpiMaster 的公共 SPI 接口完成底层传输。
*/
#ifndef CANFD_BRIDGE_H
+25 -22
View File
@@ -23,6 +23,7 @@
#include "spi_master.h"
#include "spi_link_v2.h"
#include "app_config.h"
#include "monitor/debug_log.h"
@@ -80,7 +81,7 @@ SpiMaster& SpiMaster::instance()
}
// ============================================================
// L4 SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
// SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
// ============================================================
bool SpiMaster::onWorker() const
@@ -171,7 +172,7 @@ esp_err_t SpiMaster::serviceRingOnce()
m_gd32RequestPending = false;
m_linkState.enterBusy(HandshakeState::SERVING_GD32_RING);
if (waitSpiDataReady(5U) != ESP_OK) {
if (waitSpiDataReady(3U) != ESP_OK) {
m_linkState.exitBusy();
return ESP_ERR_NOT_FOUND;
}
@@ -295,7 +296,7 @@ void SpiMaster::workerLoop()
{
m_workerTask = xTaskGetCurrentTaskHandle();
m_spiTaskHandle = m_workerTask;
DEBUG_INFO("SPI", "SPI worker started (L4), handle=%p", (void *)m_workerTask);
DEBUG_INFO("SPI", "SPI worker started, handle=%p", (void *)m_workerTask);
for (;;) {
/* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。
@@ -309,7 +310,7 @@ void SpiMaster::workerLoop()
}
Job *job = nullptr;
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(20)) == pdTRUE && job != nullptr) {
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
executeJob(*job);
if (job->done) {
xSemaphoreGive(job->done);
@@ -317,9 +318,12 @@ void SpiMaster::workerLoop()
continue;
}
/* 无 job:等 GPIO / 提交唤醒 */
/* 无 job:等 GPIO / 提交唤醒。
* 超时只是无事件时的兜底回扫周期;ISR 的 xTaskNotifyFromISR 会立即唤醒
* 本任务,响应延迟不由这里决定。过短会令 spi_worker(优先级6) 忙转压住
* httpd(优先级5),导致 /system/status、/logs 响应不来 → 网页收不到消息。 */
uint32_t bits = 0U;
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(50));
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(25));
if (m_suppressRingService) {
m_gd32RequestPending = false;
} else if ((bits & 0x01U) || m_gd32RequestPending) {
@@ -386,7 +390,7 @@ esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
if (readReadyLevelRaw() == 1) { return ESP_OK; }
/* PB6 低:可能有未读响应。短等升高;BOOT 修好交付清 pending 后应很快。
* L3:超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
* 超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
while (xTaskGetTickCount() < deadline) {
if (readReadyLevelRaw() == 1) { return ESP_OK; }
@@ -800,7 +804,7 @@ esp_err_t SpiMaster::init()
m_stats = SpiLinkStats{};
m_eventHead = m_eventTail = m_eventCount = 0;
/* L4:job 队列 + 事件信号量 + SPI worker。
/* job 队列 + 事件信号量 + SPI worker。
* worker 启动后可能立刻抢 m_mutex;init 持锁时 worker 会阻塞在 LockGuard,
* init 返回释放锁后 worker 继续 —— 不会死锁。 */
if (m_jobQueue == nullptr) {
@@ -818,13 +822,13 @@ esp_err_t SpiMaster::init()
}
}
if (m_workerTask == nullptr) {
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", 6144, this, 6, &m_workerTask);
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 6, &m_workerTask);
if (ok != pdPASS || m_workerTask == nullptr) {
DEBUG_ERR("SPI", "SPI worker task create failed");
m_workerTask = nullptr;
return ESP_ERR_NO_MEM;
}
DEBUG_INFO("SPI", "SPI worker task created (L4)");
DEBUG_INFO("SPI", "SPI worker task created");
}
m_spiTaskHandle = m_workerTask;
@@ -855,14 +859,15 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
return ESP_ERR_INVALID_SIZE;
}
/* L5:remoteCredit==0 时禁止发新 REQUEST;等 CREDIT_UPDATE 或 poll 读到新 credit。
/* CREDIT 背压(包间节奏):
* remoteCredit==0 禁止发新 REQUEST;等 CREDIT_UPDATE / 帧头 credit。
* HELLO / 链路控制不受 credit 限制(session 建立前 credit 可能为 0)。 */
if (type != V2_TYPE_LINK_HELLO && m_remoteCredit == 0U) {
const TickType_t cred_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(
timeout_ms > 0U ? timeout_ms : 500U);
while (m_remoteCredit == 0U && xTaskGetTickCount() < cred_deadline) {
/* 有 DATA_READY 时变长读一帧(可能是 CREDIT_UPDATE 或陈旧响应) */
if (waitSpiDataReady(20U) == ESP_OK) {
if (waitSpiDataReady(5U) == ESP_OK) {
uint8_t drain[SPI_OTA_FRAME_MAX];
size_t drain_len = 0U;
if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) {
@@ -1195,7 +1200,7 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
return send_ret;
}
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(L3 不盲 CS)。 */
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(不盲 CS)。 */
{
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
vTaskDelay(gap);
@@ -1334,13 +1339,13 @@ esp_err_t SpiMaster::appRequestBootloader()
/* 先变长读最多 2 次,吞掉残留 0xC1(仅 DATA_READY 低时读,禁止固定长度盲读)。 */
for (uint32_t d = 0U; d < 2U; ++d) {
if (waitSpiDataReady(10U) != ESP_OK) {
if (waitSpiDataReady(5U) != ESP_OK) {
break;
}
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 50U);
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 20U);
m_linkState.exitBusy();
vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1);
}
@@ -1354,7 +1359,7 @@ esp_err_t SpiMaster::appRequestBootloader()
for (uint32_t retry = 0U; retry < kMaxSends; ++retry) {
if (retry > 0U) {
vTaskDelay(pdMS_TO_TICKS(50));
vTaskDelay(pdMS_TO_TICKS(2));
/* 序号/会话错乱时重同步一次再发 0xB0 */
if (retry == 1U) {
m_v2Active = false;
@@ -1412,11 +1417,11 @@ esp_err_t SpiMaster::appRequestBootloader()
while (xTaskGetTickCount() <= deadline) {
++poll_i;
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)waitSpiDataReady(poll_i <= 2U ? 40U : 10U);
(void)waitSpiDataReady(poll_i <= 2U ? 20U : 5U);
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 80U);
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 40U);
m_linkState.exitBusy();
if (ret != ESP_OK) {
vTaskDelay(gap);
@@ -1470,7 +1475,7 @@ esp_err_t SpiMaster::appRequestBootloader()
if (command_sent) {
/* 静默:APP 擦配置页(可达数十 ms) + 软复位 + BOOT 起机。
* 此间禁止 HELLO,否则 role 会一直像卡在 APP。 */
const uint32_t kQuietMs = got_ack ? 900U : 500U;
const uint32_t kQuietMs = got_ack ? 100U : 30U;
DEBUG_INFO("SPI", "ENTER_BOOT 已上总线 (ack=%s),静默 %lums 等待 APP 复位...",
got_ack ? "yes" : "no", (unsigned long)kQuietMs);
vTaskDelay(pdMS_TO_TICKS(kQuietMs));
@@ -1551,7 +1556,7 @@ esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms)
}
/**
* @brief 向 GD32 APP 发送存活探测(L3:帧长发送 + DATA_READY 变长读)。
* @brief 向 GD32 APP 发送存活探测(帧长发送 + DATA_READY 变长读)。
*
* 两步法(持锁):otaTransferUnlocked(expect_stale) + otaPollResponseUnlocked。
* GD32 App PING 响应 type=kRspAck(0x80),
@@ -1789,5 +1794,3 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
return ESP_ERR_INVALID_RESPONSE;
}
// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦)
+5 -9
View File
@@ -41,8 +41,6 @@
#define SPI_OTA_RSP_WAIT (0x81)
#define SPI_OTA_RSP_ERROR (0x82)
// CANFD 类型已迁移至 transport/canfd_bridge.h(M4 解耦)
// GD32 版本信息(从 APP PING 响应解析)
typedef struct {
uint16_t app_major;
@@ -121,14 +119,14 @@ public:
void confirmTxSeqAccepted();
/**
* @brief L4 统一请求入口:经 SPI worker 串行执行(send+poll)。
* @brief 统一请求入口:经 SPI worker 串行执行(send+poll)。
*/
esp_err_t transact(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/**
* @brief L4 取出 GD32 EVENT 帧(worker 已读入事件队列)。
* @brief 取出 GD32 EVENT 帧(worker 已读入事件队列)。
* @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。
* @return true 取到事件;false 队列空且超时未到达。
*/
@@ -196,8 +194,6 @@ public:
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
// ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)──
private:
/**
* @brief 私有默认构造函数。
@@ -271,7 +267,7 @@ private:
/** 事件计数信号量(worker push / takeEvent pop)。 */
SemaphoreHandle_t m_eventSem = NULL;
// ── L4 多槽事件队列(worker 写入,业务 takeEvent 读出)──
// ── 多槽事件队列(worker 写入,业务 takeEvent 读出)──
static constexpr uint8_t kEventSlots = 4U;
struct EventSlot {
uint8_t type;
@@ -283,7 +279,7 @@ private:
uint8_t m_eventTail = 0;
uint8_t m_eventCount = 0;
// ── L4 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
// ── 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
enum class JobKind : uint8_t {
Transact = 0,
Poll,
@@ -390,7 +386,7 @@ private:
uint32_t timeout_ms, uint32_t trans_flags);
/**
* @brief L3 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
* @brief 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
* @param out_frame 完整帧输出(含头+payload+CRC)
* @param out_max 输出缓冲容量