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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 拉取)——静态,不占堆
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user