添加了SPI链接V2支持文件,重构OTA协调器的存储准备流程,

统一WiFi/BLE传输路径的收尾处理逻辑。
feat(ota): 优化OTA性能并增强系统监控功能

- 修改RSSI警告阈值从-75dBm调整为-80dBm,更新相关文档说明
- 增加HTTP服务器任务栈大小至10240字节,解决栈溢出问题
- 将OTA块大小从4096字节降至2048字节以降低峰值堆内存占用
- 实现按需擦除机制替代预擦除,在GD32 OTA中按扇区动态擦除
- 添加任务栈水位主动巡检功能,监控栈使用情况并告警
- 集成FreeRTOS栈溢出钩子,提供栈溢出检测和告警
- 优化ESP32 OTA实现使用顺序写入模式,消除长时间擦除阻塞
- 改进堆内存不足告警机制,添加限流避免刷屏
- 移除不必要的调试日志输出,精简健康监控模块
This commit is contained in:
小龙虾
2026-07-13 03:12:21 +08:00
parent 0b363492ed
commit 76782349a9
32 changed files with 2245 additions and 788 deletions
+1
View File
@@ -150,6 +150,7 @@ bool startCanFeedbackTask()
void pauseCanFeedbackTask(bool pause)
{
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
s_paused = pause;
if (pause) {
DEBUG_INFO(kTag, "CAN 监听已暂停(OTA 进行中)");
+64 -5
View File
@@ -14,10 +14,25 @@
#include "esp_wifi.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "driver/temperature_sensor.h"
#include "health_monitor.h"
#include "debug_log.h"
#include "task_stack_check.h"
#include "stack_overflow_hook.h"
// ============================================================
// 栈水位主动巡检
// ============================================================
// 告警阈值:剩余高水位 < 栈大小 20% 即告警(复用 /system/status 的任务表)。
// 去重:每个任务只在"水位由安全跌入危险"时告警一次,恢复后再跌入才再次告警,
// 避免 5 秒巡检刷屏。health_mon 任务从不被 OTA 暂停,DEBUG_ERR 经 UDP/UART
// 直发,不受 OTA 门控抑制。
static constexpr float STACK_WARN_RATIO = 0.20f; ///< 剩余水位告警阈值(栈大小比例)
static constexpr size_t STACK_TASK_MAX = 8; ///< 受监控任务上限
static bool s_stackWarned[STACK_TASK_MAX] = {}; ///< 各任务是否已告警(去重)
// ============================================================
// 单例
@@ -190,7 +205,6 @@ void HealthMonitor::taskEntry(void *pvParameters)
DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
} else {
ESP_ERROR_CHECK(temperature_sensor_enable(self.m_tempHandle));
DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
}
// 定时器,每1秒读取一次温度
@@ -202,7 +216,6 @@ void HealthMonitor::taskEntry(void *pvParameters)
if (esp_timer_create(&timer_args, &self.m_tempTimer) == ESP_OK) {
esp_timer_start_periodic(self.m_tempTimer, 1000000); // 1秒
DEBUG_INFO("MON", "温度监控定时器已启动 (1秒间隔)");
} else {
self.m_tempTimer = nullptr;
DEBUG_ERR("MON", "创建温度监控定时器失败");
@@ -210,16 +223,62 @@ void HealthMonitor::taskEntry(void *pvParameters)
// 周期性巡检
while (1) {
// 栈溢出 canary 触发检测:vApplicationStackOverflowHook 置位后,
// 在 health_mon 完好栈上用 DEBUG_ERR 经 UDP/UART 发详细告警。
// 标志只在置位后告警一次,不自动清除(canary 触发即不可逆,需重启恢复)。
if (g_stackOverflowDetected) {
DEBUG_ERR("MON", "!!! 栈溢出已被内核 canary 检测到,请检查任务栈大小 !!!");
// 告警一次后清标志,避免 5 秒刷屏;后续若再次触发会重新置位
g_stackOverflowDetected = false;
}
uint32_t free_heap = esp_get_free_heap_size();
if (free_heap < HEAP_WARNING_MIN) {
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
free_heap, esp_get_minimum_free_heap_size(), largest_block);
/* OTA 期间堆会短暂变紧,限流避免串口/UDP 二次吃堆、刷屏 */
static TickType_t s_last_heap_warn = 0;
TickType_t now = xTaskGetTickCount();
if ((now - s_last_heap_warn) >= pdMS_TO_TICKS(2000)) {
s_last_heap_warn = now;
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
(unsigned long)free_heap,
(unsigned long)esp_get_minimum_free_heap_size(),
(unsigned long)largest_block);
}
}
health_state_t cur = self.getState();
if (cur >= SYS_CRITICAL) {
DEBUG_ERR("MON", "健康状态危险: %d", cur);
}
// 栈水位主动巡检:复用 /system/status 的任务表,低水位时 DEBUG_ERR 主动告警。
// health_mon 任务不被 OTA 暂停,告警经 UDP/UART 直发,穿透 OTA 门控。
TaskStackMetric metrics[STACK_TASK_MAX];
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
continue;
}
UBaseType_t hw = uxTaskGetStackHighWaterMark(metrics[i].handle);
size_t warn_bytes = (size_t)(metrics[i].stack_size_bytes * STACK_WARN_RATIO);
if (hw < (UBaseType_t)warn_bytes) {
if (!s_stackWarned[i]) {
DEBUG_ERR("MON", "!!! 栈水位过低 !!! 任务=%s 剩余=%luB 阈值=%luB(%.0f%%) 栈大小=%luB",
metrics[i].name,
(unsigned long)hw,
(unsigned long)warn_bytes,
STACK_WARN_RATIO * 100.0f,
(unsigned long)metrics[i].stack_size_bytes);
s_stackWarned[i] = true;
}
} else if (s_stackWarned[i]) {
// 水位恢复,清除告警标志,允许下次再次跌入时告警
s_stackWarned[i] = false;
DEBUG_INFO("MON", "栈水位恢复: 任务=%s 剩余=%luB",
metrics[i].name, (unsigned long)hw);
}
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* stack_overflow_hook.cpp - FreeRTOS 栈溢出钩子
*
* CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY 开启后,内核在任务切换时检查
* 栈 canary,若被踩则回调 vApplicationStackOverflowHook。
*
* 钩子在被踩栈的任务上下文中执行,此时该任务栈已损坏 —— 禁止任何依赖栈 /
* mutex / 网络栈的调用(printf 格式化、UDP sendto、xSemaphoreTake 等都可能
* 二次崩溃)。唯一安全做法:用 uart_write_bytes 往 UART0 硬写预写好的常量
* 字符串(物理外设直写,不需要栈空间),并置一个 volatile 标志,由
* HealthMonitor 巡检循环(运行在完好的 health_mon 栈上)后续用 DEBUG_ERR
* 经 UDP/UART 发详细告警。
*
* UART0 直写不受 OTA 门控抑制,是 OTA 末尾断网阶段唯一可靠的物理通道。
*/
#include <cstring>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_system.h"
// 供 HealthMonitor 巡检检测:canary 触发后置位,由巡检发详细告警。
volatile bool g_stackOverflowDetected = false;
extern "C" void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
(void)xTask;
g_stackOverflowDetected = true;
// 栈已损坏,只能做不依赖栈的最小操作。UART0 硬写常量字符串。
static const char prefix[] = "\r\n!!! STACK OVERFLOW in task: ";
uart_write_bytes(UART_NUM_0, prefix, sizeof(prefix) - 1);
// 任务名最多 configMAX_TASK_NAME_LEN,strlen 对小已知 buffer 安全
uart_write_bytes(UART_NUM_0, pcTaskName, strnlen(pcTaskName, configMAX_TASK_NAME_LEN));
static const char suffix[] = " !!!\r\n";
uart_write_bytes(UART_NUM_0, suffix, sizeof(suffix) - 1);
/* 立刻 abort:OTA 上传 100% 后若 httpd 栈溢出却不 abort,
* 前端会一直停在 100% 像“卡住”,且 TCB 已损坏无法恢复。 */
esp_system_abort("stack overflow");
}
+18
View File
@@ -0,0 +1,18 @@
/*
* stack_overflow_hook.h - FreeRTOS 栈溢出钩子标志声明
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* canary 触发时由 vApplicationStackOverflowHook 置位;
* HealthMonitor 巡检循环检测此标志,置位后用 DEBUG_ERR 发详细告警。 */
extern volatile bool g_stackOverflowDetected;
#ifdef __cplusplus
}
#endif
+46
View File
@@ -0,0 +1,46 @@
/*
* task_stack_check.cpp - 任务栈水位指标收集(共享)
*
* 实现 collectTaskStackMetrics():复用 http_handlers 旧版 task_metrics[] 任务表,
* 供 health_monitor 主动巡检与 /system/status 被动报告共用。
*/
#include "task_stack_check.h"
#include "app_config.h"
#include "health_monitor.h"
#include "ota_relay.h"
#include "wifi_ota.h"
#include "ble_ota.h"
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
{
if (!out || max == 0) return 0;
size_t n = 0;
// httpd 由 esp_http_server 创建,无公开句柄;按任务名取。
// httpd 任务在 httpd_start() 后才存在,未启动时 xTaskGetHandle 返回 NULL。
if (n < max) {
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
}
if (n < max) {
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 };
}
if (n < max) {
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), 6144 };
}
if (n < max) {
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 };
}
if (n < max) {
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 };
}
if (n < max) {
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), 4096 };
}
if (n < max) {
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 };
}
return n;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* task_stack_check.h - 任务栈水位指标收集(共享)
*
* 将受监控任务的任务名/句柄/栈大小统一收集,供:
* - health_monitor 巡检主动告警(低水位时 DEBUG_ERR,突破 OTA 门控)
* - http_handlers /system/status 被动报告(前端每秒轮询展示)
* 两处复用同一份任务表,避免散落维护。
*/
#pragma once
#include <stddef.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/** @brief 单个任务的栈指标描述。 */
struct TaskStackMetric {
const char* name; ///< 任务名(展示用)
TaskHandle_t handle; ///< FreeRTOS 任务句柄(可能为 NULL:任务尚未创建)
size_t stack_size_bytes; ///< 创建该任务时分配的栈大小(字节)
};
/**
* @brief 收集所有受监控任务的栈指标。
* @param out 输出数组。
* @param max out 数组容量。
* @return 实际写入条数(<= max)。
*
* 注意:部分任务句柄在任务创建前为 NULL,调用方应跳过 handle==NULL 的条目。
*/
size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max);