Files
ESP32_COMM/main/monitor/cpu_stats.cpp
T
小龙虾 51c89b3065 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背压机制的描述。
2026-07-16 01:24:12 +08:00

193 lines
5.9 KiB
C++

/*
* 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