feat(spi): 增强 SPI master:性能剖析与执行耗时跟踪
- 新增 `lastWorkerExecUs()`:获取最近一次 worker 任务执行耗时(µs)。 - 引入 `SpiProfile` 结构体:事务级详细剖析,记录 SPI 事务各阶段的时间戳。 - 实现 `lastProfile()`:诊断用,获取最近一次事务剖析快照。 - SPI 时钟配置提升至 13.3 MHz,改善性能。 - 修改前端 JavaScript 轮询逻辑:新增 uptime 检查,更好地处理 ESP32 重启状态。 - SPI 测速 HTML 的计时单位由毫秒改为微秒显示。 - 新建 `spi_ring_cache.h`:共享 SPI ring push 缓存工具。 - 新增 `http_util.h`:共享 HTTP 处理器工具,含 JSON 拼接与查询参数解析。 - 引入 `json_parse.h`:轻量级 JSON 解析器,针对扁平 JSON 对象设计。 - 新增 Windows 环境下的项目构建批处理脚本。 - 重构 CAN 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* can_feedback_task.cpp - 被动监听 GD32 主动推送的 CAN 反馈快照
|
||||
* can_feedback_task.cpp - 被动监听 GD32 主动推送的事件
|
||||
*
|
||||
* 架构(GD32 主动推送,ESP32 被动接收):
|
||||
* GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页,
|
||||
* 通过 requestEsp32Read(0xC1) 下拉 DATA_READY 主动按铃,将每页反馈 payload
|
||||
* 推送给 ESP32。ESP32 本任务 SpiMaster::takeEvent() 阻塞等待按铃/事件,
|
||||
* 读到一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再
|
||||
* publish() 到 s_latest[channel] 供 Web 读取。
|
||||
*
|
||||
* 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。
|
||||
* 架构(2026-08 改造后):
|
||||
* 0xC1 CAN 反馈已改为请求驱动(前端请求时 ESP32 主动向 GD32 查询),
|
||||
* 不再被动缓存。本任务仅保留 0xC4/0xC5/0xC6/0xD2 事件分发:
|
||||
* 0xC4 ADC 调试反馈、0xC5 多电机状态、0xC6 多电机版本、0xD2 电机 OTA 进度
|
||||
* 这些仍由 GD32 主动推送,ESP32 被动接收。
|
||||
*
|
||||
* OTA 期间通过 pauseCanFeedbackTask(true) 暂停本任务,避免与 OTA 抢占 SPI。
|
||||
*/
|
||||
@@ -17,7 +14,6 @@
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "transport/canfd_bridge.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
@@ -25,7 +21,6 @@
|
||||
#include "monitor/motor_state.h"
|
||||
#include "monitor/motor_version.h"
|
||||
#include "monitor/motor_ota_relay.h"
|
||||
#include "app_config.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include <string.h>
|
||||
@@ -34,7 +29,6 @@ namespace {
|
||||
|
||||
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
|
||||
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiAdcFeedbackType = 0xC4U; ///< GD32 ADC 调试反馈
|
||||
constexpr uint8_t kSpiMotorStateType = 0xC5U; ///< GD32 多电机状态推送
|
||||
constexpr uint8_t kSpiMotorVersionType = 0xC6U; ///< GD32 多电机版本推送(事件驱动)
|
||||
@@ -42,44 +36,22 @@ constexpr uint8_t kSpiMotorOtaStatusType = 0xD2U; ///< GD32 电机 OTA relay
|
||||
constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断
|
||||
constexpr const char *kTag = "CAN_FB";
|
||||
|
||||
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
||||
static bool s_valid [CAN_CHANNEL_MAX];
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static volatile bool s_paused = false;
|
||||
|
||||
/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */
|
||||
static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
|
||||
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
|
||||
static uint32_t s_lastLoggedError[CAN_CHANNEL_MAX];
|
||||
static volatile bool s_paused = false;
|
||||
|
||||
/**
|
||||
* @brief 将某通道完整反馈发布到 s_latest(线程安全)。
|
||||
* @param ch 通道 0..2
|
||||
* @param fb 解析后的快照
|
||||
*/
|
||||
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||
{
|
||||
if (s_mutex == nullptr) return;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_latest[ch] = fb;
|
||||
s_valid[ch] = true;
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief can_fb 任务主循环:takeEvent 分发 0xC1 CAN / 0xC4 ADC。
|
||||
* @brief can_fb 任务主循环:takeEvent 分发 0xC4/0xC5/0xC6/0xD2 推送事件。
|
||||
* 0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback),不再在此处理。
|
||||
*/
|
||||
static void taskLoop(void *)
|
||||
{
|
||||
DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)");
|
||||
DEBUG_INFO(kTag, "事件监听任务启动 (0xC4 ADC / 0xC5 motor / 0xC6 ver / 0xD2 ota)");
|
||||
|
||||
/* 冷启动延后开始监听,错开上电初始化窗口 */
|
||||
{
|
||||
esp_reset_reason_t rr = esp_reset_reason();
|
||||
if (rr == ESP_RST_POWERON || rr == ESP_RST_BROWNOUT) {
|
||||
constexpr uint32_t kCanFbDeferMs = 1500U;
|
||||
DEBUG_INFO(kTag, "冷启动: %lu ms 后开始 CAN 反馈监听",
|
||||
DEBUG_INFO(kTag, "冷启动: %lu ms 后开始事件监听",
|
||||
(unsigned long)kCanFbDeferMs);
|
||||
vTaskDelay(pdMS_TO_TICKS(kCanFbDeferMs));
|
||||
}
|
||||
@@ -126,39 +98,14 @@ static void taskLoop(void *)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ring_type != kSpiCanFeedbackType || ring_len < 28U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t ch = ring_payload[0];
|
||||
if (ch >= CAN_CHANNEL_MAX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 解析本页并累积到工作缓冲,page=0 会自动重置 */
|
||||
if (canFdParseFeedbackPage(ring_payload, ring_len,
|
||||
&s_work[ch], &s_workIdx[ch]) == 0) {
|
||||
publish(ch, s_work[ch]);
|
||||
|
||||
/* 累计错误发生变化时才告警一次;P0/P1 携带相同统计,不能重复打印。 */
|
||||
if (s_work[ch].stats.error_count != 0U
|
||||
&& s_work[ch].stats.error_count != s_lastLoggedError[ch]) {
|
||||
DEBUG_WARN(kTag,
|
||||
"CH%u err=%lu TEC=%u REC=%u",
|
||||
static_cast<unsigned>(ch),
|
||||
static_cast<unsigned long>(s_work[ch].stats.error_count),
|
||||
static_cast<unsigned>(s_work[ch].stats.tec),
|
||||
static_cast<unsigned>(s_work[ch].stats.rec));
|
||||
s_lastLoggedError[ch] = s_work[ch].stats.error_count;
|
||||
}
|
||||
}
|
||||
/* 0xC1 已改为请求驱动,不再在此处理;未知类型静默跳过 */
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @brief 启动事件监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask()
|
||||
@@ -166,19 +113,6 @@ bool startCanFeedbackTask()
|
||||
static bool s_started = false;
|
||||
if (s_started) return true;
|
||||
|
||||
if (s_mutex == nullptr) {
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
if (s_mutex == nullptr) {
|
||||
DEBUG_ERR(kTag, "互斥量创建失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
memset(s_latest, 0, sizeof(s_latest));
|
||||
memset(s_lastLoggedError, 0, sizeof(s_lastLoggedError));
|
||||
memset(s_valid, 0, sizeof(s_valid));
|
||||
memset(s_work, 0, sizeof(s_work));
|
||||
memset(s_workIdx, 0, sizeof(s_workIdx));
|
||||
|
||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||
taskLoop,
|
||||
"can_fb",
|
||||
@@ -196,7 +130,7 @@ bool startCanFeedbackTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 暂停/恢复 CAN(及同任务分发的 ADC)事件监听。
|
||||
* @brief 暂停/恢复事件监听。
|
||||
* @param pause true=OTA 期间暂停;false=恢复
|
||||
*/
|
||||
void pauseCanFeedbackTask(bool pause)
|
||||
@@ -204,28 +138,17 @@ void pauseCanFeedbackTask(bool pause)
|
||||
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_DBG(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
DEBUG_DBG(kTag, "事件监听已暂停(OTA 进行中)");
|
||||
} else {
|
||||
DEBUG_DBG(kTag, "CAN 监听已恢复");
|
||||
DEBUG_DBG(kTag, "事件监听已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新快照(线程安全)。
|
||||
* @return true 表示拿到一份有效数据。
|
||||
* @brief 查询是否处于暂停(OTA / send_batch 期间为 true)。
|
||||
* 暂停期间 HTTP handler 应跳过 0xC1 查询,避免与 OTA 抢占 SPI。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out)
|
||||
bool canFeedbackPaused()
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
if (s_valid[channel]) {
|
||||
*out = s_latest[channel];
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return ok;
|
||||
return s_paused;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
/*
|
||||
* can_feedback_task.h - 被动接收 GD32 主动推送的 CAN 反馈快照
|
||||
* can_feedback_task.h - 事件监听任务(0xC4/0xC5/0xC6/0xD2 推送)
|
||||
*
|
||||
* ESP32 侧不主动 1Hz 轮询 0xC1;由 GD32 按铃推送,本任务 takeEvent 接收。
|
||||
* 2026-08 架构改造:0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback),
|
||||
* 不再被动缓存。本任务仅分发 GD32 主动推送的 ADC/电机状态/版本/OTA 进度事件。
|
||||
*/
|
||||
#ifndef CAN_FEEDBACK_TASK_H
|
||||
#define CAN_FEEDBACK_TASK_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "transport/canfd_bridge.h"
|
||||
|
||||
/** @brief 暂停/恢复 CAN 反馈监听(OTA 期间调用 pause=true)。 */
|
||||
/** @brief 暂停/恢复事件监听(OTA 期间调用 pause=true)。 */
|
||||
void pauseCanFeedbackTask(bool pause);
|
||||
|
||||
/** @brief 查询监听是否处于暂停(OTA / send_batch 期间为 true)。
|
||||
* 请求驱动的 SPI 查询(canFdQueryFeedback)在暂停期间应跳过。 */
|
||||
bool canFeedbackPaused();
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @brief 启动事件监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask();
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新反馈快照(线程安全)。
|
||||
* @param channel 0/1/2。
|
||||
* @param out 输出缓冲。
|
||||
* @return true 表示拿到一份有效数据,false 表示通道无效或尚未拿到第一份。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CAN_FEEDBACK_TASK_H
|
||||
#endif // CAN_FEEDBACK_TASK_H
|
||||
@@ -11,8 +11,12 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
/** 单次快照最多跟踪的任务数 */
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
|
||||
/** 单次快照最多跟踪的任务数。
|
||||
* 系统常驻任务 ~14(app)+ ~5(IDF: tiT/wifi/nimble/hci/ble)= ~19,
|
||||
* 按需短暂任务(ble_finish/restart/cancel/gd32_wait/ota_feed)不常驻。
|
||||
* 20 覆盖常驻集 + 余量 1;偶发按需任务多出时 uxTaskGetSystemState 截断,
|
||||
* 仅影响该拍 CPU 统计显示,不影响功能。原 32 浪费 ~1KB BSS。 */
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 20;
|
||||
|
||||
/** 采样周期(ms)— 窗口 sample_ms 应对齐此值 */
|
||||
static constexpr uint32_t CPU_STATS_SAMPLE_MS = 1000;
|
||||
|
||||
@@ -509,7 +509,12 @@ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
const int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||||
/* 非阻塞直写 FIFO:满了丢弃(日志必进 ring,web /logs 仍完整)。
|
||||
* 旧实现 uart_write_bytes 在 TX buffer=0 时同步阻塞到整行发完
|
||||
* (115200 下 ~5-8ms/条),OTA 期间日志密集会把 ota_relay(prio 23)
|
||||
* / spi_worker(prio 6) 阻塞数 ms~数十 ms——50ms ACK 窗口被拖爆,
|
||||
* 表现为改短重试后超时重发频繁(线上 12/777)。 */
|
||||
const int ret = uart_tx_chars(UART_NUM_0, (const char *)data, (int)len);
|
||||
if (ret < 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -646,7 +651,10 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
|
||||
if (sock >= 0) {
|
||||
const size_t send_len = len > 1024 ? 1024 : len;
|
||||
const int ret = ::sendto(sock, data, send_len, 0,
|
||||
/* MSG_DONTWAIT:非阻塞发送。lwIP 在 ARP 未解析/路由未就绪时
|
||||
* sendto 会排队等待(可达 SO_SNDTIMEO 200ms),会阻塞打日志的
|
||||
* 业务任务(OTA relay/worker)——日志转发必须尽力而为。 */
|
||||
const int ret = ::sendto(sock, data, send_len, MSG_DONTWAIT,
|
||||
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
|
||||
if (ret >= 0) {
|
||||
udp_ret = ESP_OK;
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* gd32_adc_feedback.cpp - GD32 ADC 调试反馈解析与缓存
|
||||
*/
|
||||
#include "monitor/gd32_adc_feedback.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -17,29 +15,6 @@ static SemaphoreHandle_t s_mux = nullptr;
|
||||
static gd32_adc_feedback_t s_latest = {};
|
||||
static bool s_have = false;
|
||||
|
||||
/** @brief 读小端 u16。 */
|
||||
static uint16_t rdU16Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint16_t>(p[0] | (static_cast<uint16_t>(p[1]) << 8));
|
||||
}
|
||||
|
||||
/** @brief 读小端 u32。 */
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
/** @brief 懒创建快照互斥量。 */
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@@ -80,7 +55,7 @@ bool onGd32AdcFeedbackEvent(const uint8_t *payload, size_t len)
|
||||
fb.uptime_sec = uptime;
|
||||
fb.recv_ms = esp_timer_get_time() / 1000;
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -103,7 +78,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out)
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -124,7 +99,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out)
|
||||
*/
|
||||
uint32_t gd32AdcFeedbackAgeMs(void)
|
||||
{
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
@@ -135,16 +110,5 @@ uint32_t gd32AdcFeedbackAgeMs(void)
|
||||
recv = s_latest.recv_ms;
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
if (!have || recv <= 0) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv) {
|
||||
return 0U;
|
||||
}
|
||||
const int64_t age = now - recv;
|
||||
if (age > static_cast<int64_t>(UINT32_MAX)) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
return static_cast<uint32_t>(age);
|
||||
}
|
||||
return ringCacheAgeMs(have, recv);
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "spi_master.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif_types.h"
|
||||
#include "server/http_util.h"
|
||||
|
||||
// ============================================================
|
||||
// heap hook:捕获"破新低时的最后一次 alloc"(最后一击,未必是主因)
|
||||
@@ -399,8 +400,13 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
* 必须在 maybeRecord 之前,否则 est 长期偏低会误触发 hook 快照。 */
|
||||
hookResyncEstFree(free_heap);
|
||||
|
||||
// 任务栈指标:本周期只采集一次,堆低水位快照和栈水位巡检共用。
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
|
||||
// 堆低水位快照:仅在"最低空闲"创新低时记录一条上下文(5s 检测足够)
|
||||
self.maybeRecordHeapLowWater();
|
||||
// 传入已采集的 metrics,避免 maybeRecordHeapLowWater 内部二次遍历任务表。
|
||||
self.maybeRecordHeapLowWater(metrics, count);
|
||||
|
||||
if (free_heap < HEAP_WARNING_MIN) {
|
||||
/* 堆告警节流:OTA 期间阈值更低、间隔更长 */
|
||||
@@ -426,10 +432,8 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
DEBUG_ERR("MON", "健康状态危险: %d", cur);
|
||||
}
|
||||
|
||||
// 栈水位主动巡检:复用 /system/status 的任务表,低水位时 DEBUG_ERR 主动告警。
|
||||
// 栈水位主动巡检:复用已采集的任务表,低水位时 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;
|
||||
@@ -604,23 +608,6 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
|
||||
// ============================================================
|
||||
// 堆低水位快照("最低空闲"创新低时记录上下文)
|
||||
// ============================================================
|
||||
namespace {
|
||||
/** @brief snprintf 追加到 buf 的安全封装:截断但不越界,返回新 offset。 */
|
||||
int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...)
|
||||
{
|
||||
if (offset < 0 || (size_t)offset >= cap) return offset;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) return offset;
|
||||
if ((size_t)n >= cap - (size_t)offset) {
|
||||
// 被截断:vsnprintf 返回的是"本应写入"的长度,按截断后修正 offset
|
||||
return (int)cap - 1;
|
||||
}
|
||||
return offset + n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 检测"内部堆最低空闲"是否创新低,是则覆盖写入唯一快照槽。
|
||||
@@ -630,7 +617,7 @@ int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...)
|
||||
* 所有外部读取(OtaCoordinator::getSnapshot / collectTaskStackMetrics / rssi / temp)
|
||||
* 都先在锁外取,避免持锁期间嵌套调用其它模块的锁;持锁后只做字段拷贝。
|
||||
*/
|
||||
void HealthMonitor::maybeRecordHeapLowWater()
|
||||
void HealthMonitor::maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics, size_t pre_count)
|
||||
{
|
||||
// 与 http_handlers.cpp 同口径:内部 DRAM 数据堆
|
||||
static constexpr uint32_t CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
@@ -691,8 +678,17 @@ void HealthMonitor::maybeRecordHeapLowWater()
|
||||
HeapLowTaskStack stacks[8];
|
||||
uint8_t stack_count = 0;
|
||||
{
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
// 优先复用巡检循环已采集的指标,避免二次遍历
|
||||
TaskStackMetric local_metrics[STACK_TASK_MAX];
|
||||
const TaskStackMetric *metrics;
|
||||
size_t count;
|
||||
if (pre_metrics && pre_count > 0) {
|
||||
metrics = pre_metrics;
|
||||
count = pre_count;
|
||||
} else {
|
||||
count = collectTaskStackMetrics(local_metrics, STACK_TASK_MAX);
|
||||
metrics = local_metrics;
|
||||
}
|
||||
// 先全部采集
|
||||
struct Item { char name[16]; uint32_t rem; };
|
||||
Item items[STACK_TASK_MAX];
|
||||
@@ -816,7 +812,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
LockGuard lock(m_mutex);
|
||||
|
||||
if (!m_heapLowWaterValid) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
|
||||
const HeapLowWaterRecord &r = m_heapLowWater;
|
||||
@@ -824,7 +820,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
* hook_confidence 告知前端置信度:hook 命中=est_low_watermark,未命中=none。
|
||||
* 前端应据此区分 estimated 与 measured(free_heap/min_free_heap 为实测)。 */
|
||||
const char *hook_confidence = r.hook_fired ? "est_low_watermark" : "none";
|
||||
int pos = appendJsonfSafe(buf, cap, offset,
|
||||
int pos = http_json_append(buf, cap, offset,
|
||||
"{\"count\":1,\"records\":[{"
|
||||
"\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,"
|
||||
"\"largest_free_block\":%lu,\"sys_state\":%u,\"ota_channel\":%u,"
|
||||
@@ -863,24 +859,24 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
(unsigned long)r.hook_fired,
|
||||
(unsigned long)r.ip_event_count);
|
||||
if (pos == offset) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
|
||||
for (uint8_t k = 0; k < r.task_stack_count && k < 8; ++k) {
|
||||
int after = appendJsonfSafe(buf, cap, pos,
|
||||
int after = http_json_append(buf, cap, pos,
|
||||
"%s{\"name\":\"%s\",\"remaining\":%lu}",
|
||||
k == 0 ? "" : ",",
|
||||
r.task_stacks[k].name,
|
||||
(unsigned long)r.task_stacks[k].remaining);
|
||||
if (after == pos) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
pos = after;
|
||||
}
|
||||
|
||||
int closed = appendJsonfSafe(buf, cap, pos, "]}]}");
|
||||
int closed = http_json_append(buf, cap, pos, "]}]}");
|
||||
if (closed == pos) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
return closed;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "esp_timer.h"
|
||||
#include "app_config.h"
|
||||
#include "task_stack.h" // TaskStackMetric(maybeRecordHeapLowWater 可选参数)
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
typedef enum {
|
||||
@@ -133,7 +134,11 @@ public:
|
||||
/** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。
|
||||
* 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。
|
||||
* 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。 */
|
||||
void maybeRecordHeapLowWater();
|
||||
/** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。
|
||||
* 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。
|
||||
* 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。
|
||||
* @param pre_metrics 巡检循环已采集的任务栈指标(避免二次遍历);null 时内部自采 */
|
||||
void maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics = nullptr, size_t pre_count = 0);
|
||||
|
||||
/** @brief 将唯一快照序列化为 JSON:{"count":0|1,"records":[...]}。线程安全。 */
|
||||
int dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const;
|
||||
|
||||
+109
-111
@@ -35,6 +35,59 @@ static int64_t nowMs() {
|
||||
return esp_timer_get_time() / 1000;
|
||||
}
|
||||
|
||||
/* ── 共享辅助:终态判断 + QUERY SPI 事务 ── */
|
||||
|
||||
/** @brief GD32 侧已进终态(IDLE/FINISHED/ERROR),喂数据应停止。 */
|
||||
static inline bool isTerminalState(motor_ota_state_t s)
|
||||
{
|
||||
return s == MOTOR_OTA_IDLE || s == MOTOR_OTA_FINISHED || s == MOTOR_OTA_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发 SPI 0xD0 QUERY 查询 GD32 真实状态。
|
||||
* @param out_state 非 null 时写入 GD32 返回的 state
|
||||
* @param out_err 非 null 时写入 error_code
|
||||
* @param update_status true 时把结果同步写回 s_status(持锁)
|
||||
* @return true=QUERY 成功且 ACK 有有效数据;false=SPI 失败或无 ACK
|
||||
*
|
||||
* 消除 feedTask 中 4 处重复的 qcmd[1]={2} + transact + parse qp[0..5] 块。
|
||||
*/
|
||||
static bool queryMotorOtaStatus(motor_ota_state_t *out_state = nullptr,
|
||||
uint8_t *out_err = nullptr,
|
||||
bool update_status = false)
|
||||
{
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) != ESP_OK
|
||||
|| qt != kRspAck || ql < 1U) {
|
||||
return false;
|
||||
}
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (out_state) *out_state = qst;
|
||||
if (out_err) *out_err = (ql >= 2U) ? qp[1] : 0U;
|
||||
if (update_status && s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = (ql >= 2U) ? qp[1] : 0U;
|
||||
if (ql >= 6U) {
|
||||
s_status.packets_acked = static_cast<uint16_t>(qp[2])
|
||||
| (static_cast<uint16_t>(qp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qp[4])
|
||||
| (static_cast<uint16_t>(qp[5]) << 8);
|
||||
}
|
||||
if (ql >= 10U) {
|
||||
s_status.fw_size = static_cast<uint32_t>(qp[6])
|
||||
| (static_cast<uint32_t>(qp[7]) << 8)
|
||||
| (static_cast<uint32_t>(qp[8]) << 16)
|
||||
| (static_cast<uint32_t>(qp[9]) << 24);
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ═══════════════════ 喂数据 task ═══════════════════ */
|
||||
|
||||
static void feedTask(void *arg)
|
||||
@@ -87,8 +140,7 @@ static void feedTask(void *arg)
|
||||
* (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。 */
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (nowMs() >= grace_end_ms) {
|
||||
if (st.state == MOTOR_OTA_IDLE || st.state == MOTOR_OTA_FINISHED ||
|
||||
st.state == MOTOR_OTA_ERROR) {
|
||||
if (isTerminalState(st.state)) {
|
||||
ESP_LOGW(TAG, "GD32 终态 0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)st.state, (unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
break;
|
||||
@@ -127,36 +179,17 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); /* 快速重试(原 100ms 太慢导致吐量低) */
|
||||
/* suppress 期间 s_status 不更新,连续失败时主动 QUERY GD32 真实状态,
|
||||
* 避免 GD32 已进 ERROR 终态但 feedTask 不知道、盲重试空转到 120s 超时。 */
|
||||
if (++consec_fail >= kQueryAfterFails) {
|
||||
consec_fail = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)qst, (unsigned)qp[1],
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qp[1];
|
||||
if (ql >= 6U) {
|
||||
s_status.packets_acked = static_cast<uint16_t>(qp[2])
|
||||
| (static_cast<uint16_t>(qp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qp[4])
|
||||
| (static_cast<uint16_t>(qp[5]) << 8);
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (++consec_fail >= kQueryAfterFails) {
|
||||
consec_fail = 0U;
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, true) && isTerminalState(qst)) {
|
||||
ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)qst, (unsigned)qerr,
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue; /* 不推进 offset,同 offset 重发 */
|
||||
}
|
||||
consec_fail = 0U;
|
||||
@@ -177,13 +210,8 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
|
||||
/* 握手未确认时定期 QUERY:确认 GD32 已过握手阶段才标 TRANSFER */
|
||||
if (!handshake_confirmed && (ack_n & 7U) == 0U) {
|
||||
uint8_t qcmd[1] = {2U};
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
motor_ota_state_t qst;
|
||||
if (queryMotorOtaStatus(&qst, nullptr, false)) {
|
||||
if (qst == MOTOR_OTA_ERASE || qst == MOTOR_OTA_TRANSFER ||
|
||||
qst == MOTOR_OTA_FINISH_OTA || qst == MOTOR_OTA_WAIT_APP_RUN) {
|
||||
handshake_confirmed = true;
|
||||
@@ -216,16 +244,11 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
/* 连续 WAIT 超限:可能 GD32 auto-retry 重置了 s_fw_fed=0,
|
||||
* 当前 offset 被 gap 拒收(永远不会变 ACK)。QUERY 确认真实状态。 */
|
||||
if (++consec_wait >= kQueryAfterWaits) {
|
||||
consec_wait = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 2U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
const uint8_t qerr = qp[1];
|
||||
if (++consec_wait >= kQueryAfterWaits) {
|
||||
consec_wait = 0U;
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, false)) {
|
||||
if (qst != MOTOR_OTA_TRANSFER) {
|
||||
/* GD32 不在传输态(auto-retry 回了 kPreCancel/握手):
|
||||
* 重置 offset 从头喂,配合 GD32 重新擦除+握手。 */
|
||||
@@ -237,21 +260,20 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
} else {
|
||||
handshake_confirmed = true; /* GD32 在 transfer,确认握手完成 */
|
||||
}
|
||||
/* 终态仍 break */
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止",
|
||||
(unsigned)qst, (unsigned)qerr);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qerr;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 终态仍 break */
|
||||
if (isTerminalState(qst)) {
|
||||
ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止",
|
||||
(unsigned)qst, (unsigned)qerr);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qerr;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 非 ACK/WAIT(含 ERROR/陈旧 type):不 break 整次 OTA。
|
||||
* 曾因 poll 读到 EVENT/错 type 直接 break → fed 卡首 chunk。 */
|
||||
@@ -273,53 +295,29 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
const uint32_t exit_deadline = nowMs() + kExitPollMs;
|
||||
bool got_terminal = false;
|
||||
while (nowMs() < exit_deadline) {
|
||||
/* 优先吃 ring 推送(已恢复 suppress) */
|
||||
{
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (st.state == MOTOR_OTA_FINISHED || st.state == MOTOR_OTA_ERROR ||
|
||||
st.state == MOTOR_OTA_IDLE) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)st.state, (unsigned)st.error_code,
|
||||
(unsigned)st.packets_acked, (unsigned)st.total_packets);
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qresp_type = 0;
|
||||
uint8_t qresp[16] = {0};
|
||||
size_t qresp_len = 0;
|
||||
esp_err_t qr = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1,
|
||||
&qresp_type, qresp, sizeof(qresp), &qresp_len, 300U);
|
||||
if (qr == ESP_OK && qresp_type == kRspAck && qresp_len >= 10U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qresp[0]);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qresp[1];
|
||||
s_status.packets_acked = static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8);
|
||||
s_status.fw_size = static_cast<uint32_t>(qresp[6])
|
||||
| (static_cast<uint32_t>(qresp[7]) << 8)
|
||||
| (static_cast<uint32_t>(qresp[8]) << 16)
|
||||
| (static_cast<uint32_t>(qresp[9]) << 24);
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
if (qst == MOTOR_OTA_FINISHED || qst == MOTOR_OTA_ERROR ||
|
||||
qst == MOTOR_OTA_IDLE) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)qst, (unsigned)qresp[1],
|
||||
(unsigned)(static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8)),
|
||||
(unsigned)(static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8)));
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* 优先吃 ring 推送(已恢复 suppress) */
|
||||
{
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (isTerminalState(st.state)) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)st.state, (unsigned)st.error_code,
|
||||
(unsigned)st.packets_acked, (unsigned)st.total_packets);
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, true)) {
|
||||
if (isTerminalState(qst)) {
|
||||
const motor_ota_status_t snap = motorOtaGetStatus();
|
||||
ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)qst, (unsigned)qerr,
|
||||
(unsigned)snap.packets_acked, (unsigned)snap.total_packets);
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(kExitGapMs));
|
||||
}
|
||||
if (!got_terminal) {
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* motor_state.cpp - GD32 多电机状态推送解析与缓存 (SPI TYPE 0xC5)
|
||||
*/
|
||||
#include "monitor/motor_state.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -13,21 +11,6 @@ namespace {
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_state_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
@@ -51,7 +34,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
@@ -102,7 +85,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
bool getLatestMotorState(motor_state_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
@@ -118,7 +101,7 @@ bool getLatestMotorState(motor_state_table_t *out)
|
||||
|
||||
uint32_t motorStateAgeMs(void)
|
||||
{
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return UINT32_MAX;
|
||||
|
||||
int64_t recv = 0;
|
||||
@@ -128,11 +111,5 @@ uint32_t motorStateAgeMs(void)
|
||||
recv = s_table.recv_ms;
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
if (!have || recv <= 0) return UINT32_MAX;
|
||||
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv) return 0U;
|
||||
const int64_t age = now - recv;
|
||||
return (age > static_cast<int64_t>(UINT32_MAX)) ? UINT32_MAX
|
||||
: static_cast<uint32_t>(age);
|
||||
}
|
||||
return ringCacheAgeMs(have, recv);
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
* 镜像 motor_state.cpp 的互斥保护缓存模型。
|
||||
*/
|
||||
#include "monitor/motor_version.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -15,21 +13,6 @@ namespace {
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_version_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
@@ -40,7 +23,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
|
||||
const uint8_t count = payload[0];
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
@@ -69,7 +52,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
bool getLatestMotorVersion(motor_version_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
@@ -81,4 +64,4 @@ bool getLatestMotorVersion(motor_version_table_t *out)
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* spi_ring_cache.h - SPI ring push 缓存共享辅助
|
||||
*
|
||||
* gd32_adc_feedback / motor_state / motor_version 三个模块共用相同的
|
||||
* 互斥懒创建 + 小端读 + ageMs 计算样板,提取到此处消除三份拷贝。
|
||||
*/
|
||||
#ifndef SPI_RING_CACHE_H
|
||||
#define SPI_RING_CACHE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── 小端读 ── */
|
||||
|
||||
static inline uint16_t rdU16Le(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static inline uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0]
|
||||
| ((uint32_t)p[1] << 8)
|
||||
| ((uint32_t)p[2] << 16)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
/* ── 互斥懒创建 ── */
|
||||
|
||||
static inline void ringCacheEnsureMux(SemaphoreHandle_t *mux)
|
||||
{
|
||||
if (*mux == nullptr) {
|
||||
*mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 通用 ageMs ──
|
||||
* 传入缓存的 have 标志 + recv_ms 时间戳,返回距 now 的毫秒数。
|
||||
* 无数据或 recv_ms<=0 返回 UINT32_MAX。
|
||||
*/
|
||||
static inline uint32_t ringCacheAgeMs(bool have, int64_t recv_ms)
|
||||
{
|
||||
if (!have || recv_ms <= 0) return UINT32_MAX;
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv_ms) return 0U;
|
||||
const int64_t age = now - recv_ms;
|
||||
return (age > (int64_t)UINT32_MAX) ? UINT32_MAX : (uint32_t)age;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // SPI_RING_CACHE_H
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "esp_timer.h"
|
||||
#include "spi_master.h"
|
||||
#include "http_handlers.h" /* http_set_cors_headers */
|
||||
#include "server/http_util.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
namespace {
|
||||
@@ -37,30 +38,14 @@ constexpr size_t kPayloadDefault = 236U; /* 满帧:等效 0xD1 电机 OTA
|
||||
constexpr size_t kPayloadMax = 236U; /* APP kTxBufSize=256 - V2_OVERHEAD */
|
||||
constexpr uint32_t kFrameTimeoutMs = 500U;
|
||||
|
||||
/** @brief 从 query 里取无符号整型参数,缺失/非法时返回默认值。 */
|
||||
uint32_t queryU32(httpd_req_t *req, const char *key, uint32_t defval)
|
||||
{
|
||||
char qbuf[64];
|
||||
char vbuf[16];
|
||||
if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
if (httpd_query_key_value(qbuf, key, vbuf, sizeof(vbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
char *end = nullptr;
|
||||
const unsigned long v = strtoul(vbuf, &end, 10);
|
||||
return (end && *end == '\0') ? (uint32_t)v : defval;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
{
|
||||
uint32_t frames = queryU32(req, "frames", kFramesDefault);
|
||||
uint32_t frames = http_query_u32(req, "frames", kFramesDefault);
|
||||
if (frames < 1U) { frames = 1U; }
|
||||
if (frames > kFramesMax) { frames = kFramesMax; }
|
||||
size_t payload_len = (size_t)queryU32(req, "payload", (uint32_t)kPayloadDefault);
|
||||
size_t payload_len = (size_t)http_query_u32(req, "payload", (uint32_t)kPayloadDefault);
|
||||
if (payload_len > kPayloadMax) { payload_len = kPayloadMax; }
|
||||
|
||||
/* 探针 payload:递增填充(CRC32 覆盖,顺带校验数据路径完整性) */
|
||||
@@ -74,9 +59,18 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
uint64_t rtt_sum_us = 0U;
|
||||
uint64_t rtt_min_us = UINT64_MAX;
|
||||
uint64_t rtt_max_us = 0U;
|
||||
/* RTT 分布直方图:<2 / 2-3 / 3-4 / 4-5 / >=5 ms,用于判断抖动形态
|
||||
* (双峰=离散 +1ms 碰撞;均匀=tick 相位量化) */
|
||||
uint32_t histo[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
/* RTT 分布直方图(µs 计时,<2ms 区间细化为 0.5ms 粒度):
|
||||
* <0.5 / 0.5-1 / 1-1.5 / 1.5-2 / 2-3 / 3-5 / >=5 ms,
|
||||
* 用于判断抖动形态(<2ms 内的尖峰/双峰 vs 离散 +1ms 碰撞) */
|
||||
uint32_t histo[7] = {0U, 0U, 0U, 0U, 0U, 0U, 0U};
|
||||
/* 事务耗时(worker 内执行段,SpiMaster::lastWorkerExecUs)与
|
||||
* 调度/唤醒开销(RTT − 事务)拆解:事务慢=GD32 应答/SPI 时序;
|
||||
* 调度慢=ESP32 任务抢占(WiFi/esp_timer 等)。 */
|
||||
uint64_t w_sum_us = 0U, w_min_us = UINT64_MAX, w_max_us = 0U;
|
||||
uint64_t s_sum_us = 0U, s_min_us = UINT64_MAX, s_max_us = 0U;
|
||||
/* 5 段细分(SpiMaster::SpiProfile):提交/调度、发送帧、等应答、读响应、唤醒 */
|
||||
uint64_t seg_sum[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint64_t seg_max[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] start frames=%u payload=%u",
|
||||
(unsigned)frames, (unsigned)payload_len);
|
||||
@@ -100,12 +94,34 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
rtt_sum_us += rtt;
|
||||
if (rtt < rtt_min_us) { rtt_min_us = rtt; }
|
||||
if (rtt > rtt_max_us) { rtt_max_us = rtt; }
|
||||
const uint32_t ms_bucket = (uint32_t)(rtt / 1000U);
|
||||
if (ms_bucket < 2U) { ++histo[0]; }
|
||||
else if (ms_bucket < 3U) { ++histo[1]; }
|
||||
else if (ms_bucket < 4U) { ++histo[2]; }
|
||||
else if (ms_bucket < 5U) { ++histo[3]; }
|
||||
else { ++histo[4]; }
|
||||
const uint64_t wus = SpiMaster::instance().lastWorkerExecUs();
|
||||
const uint64_t sus = (rtt > wus) ? (rtt - wus) : 0U;
|
||||
w_sum_us += wus;
|
||||
if (wus < w_min_us) { w_min_us = wus; }
|
||||
if (wus > w_max_us) { w_max_us = wus; }
|
||||
s_sum_us += sus;
|
||||
if (sus < s_min_us) { s_min_us = sus; }
|
||||
if (sus > s_max_us) { s_max_us = sus; }
|
||||
/* 5 段细分(防回绕钳 0) */
|
||||
const SpiMaster::SpiProfile &pf = SpiMaster::instance().lastProfile();
|
||||
const uint64_t seg[5] = {
|
||||
(pf.t_w0 > pf.t_submit0) ? (pf.t_w0 - pf.t_submit0) : 0U,
|
||||
(pf.t_send1 > pf.t_send0) ? (pf.t_send1 - pf.t_send0) : 0U,
|
||||
(pf.t_wait1 > pf.t_wait0) ? (pf.t_wait1 - pf.t_wait0) : 0U,
|
||||
(pf.t_read1 > pf.t_wait1) ? (pf.t_read1 - pf.t_wait1) : 0U,
|
||||
(pf.t_submit1 > pf.t_w1) ? (pf.t_submit1 - pf.t_w1) : 0U,
|
||||
};
|
||||
for (int k = 0; k < 5; ++k) {
|
||||
seg_sum[k] += seg[k];
|
||||
if (seg[k] > seg_max[k]) { seg_max[k] = seg[k]; }
|
||||
}
|
||||
if (rtt < 500U) { ++histo[0]; }
|
||||
else if (rtt < 1000U) { ++histo[1]; }
|
||||
else if (rtt < 1500U) { ++histo[2]; }
|
||||
else if (rtt < 2000U) { ++histo[3]; }
|
||||
else if (rtt < 3000U) { ++histo[4]; }
|
||||
else if (rtt < 5000U) { ++histo[5]; }
|
||||
else { ++histo[6]; }
|
||||
} else {
|
||||
++n_fail;
|
||||
}
|
||||
@@ -120,25 +136,44 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
tput_kbps = ((double)n_ok * (double)payload_len) / ((double)elapsed_us / 1000000.0) / 1024.0;
|
||||
}
|
||||
if (rtt_min_us == UINT64_MAX) { rtt_min_us = 0U; }
|
||||
if (w_min_us == UINT64_MAX) { w_min_us = 0U; }
|
||||
if (s_min_us == UINT64_MAX) { s_min_us = 0U; }
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] done ok=%u fail=%u avg=%uus tput=%.1fKB/s",
|
||||
(unsigned)n_ok, (unsigned)n_fail, (unsigned)rtt_avg_us, tput_kbps);
|
||||
|
||||
char text[480];
|
||||
char text[640];
|
||||
const int len = snprintf(text, sizeof(text),
|
||||
"SPI 测速结果\n"
|
||||
"帧数: %u 成功: %u 失败: %u%s\n"
|
||||
"载荷: %u B/帧 总耗时: %u ms\n"
|
||||
"往返延迟: 平均 %.1f ms 最快 %.1f ms 最慢 %.1f ms\n"
|
||||
"延迟分布: <2ms:%u 2-3ms:%u 3-4ms:%u 4-5ms:%u >=5ms:%u\n"
|
||||
"往返延迟: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"SPI 事务: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"调度开销: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"单帧分段 平均: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n"
|
||||
"单帧分段 最慢: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n"
|
||||
"延迟分布: <0.5ms:%u 0.5-1ms:%u 1-1.5ms:%u 1.5-2ms:%u 2-3ms:%u 3-5ms:%u >=5ms:%u\n"
|
||||
"等效吞吐: %.1f KB/s\n",
|
||||
(unsigned)frames, (unsigned)n_ok, (unsigned)n_fail,
|
||||
(n_fail > 0U) ? " <-- 存在失败帧, 链路异常!" : "",
|
||||
(unsigned)payload_len, (unsigned)elapsed_ms,
|
||||
(double)rtt_avg_us / 1000.0, (double)rtt_min_us / 1000.0,
|
||||
(double)rtt_max_us / 1000.0,
|
||||
(unsigned long long)rtt_avg_us, (unsigned long long)rtt_min_us,
|
||||
(unsigned long long)rtt_max_us,
|
||||
(unsigned long long)(w_sum_us / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)w_min_us, (unsigned long long)w_max_us,
|
||||
(unsigned long long)(s_sum_us / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)s_min_us, (unsigned long long)s_max_us,
|
||||
(unsigned long long)(seg_sum[0] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[1] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[2] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[3] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[4] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)seg_max[0], (unsigned long long)seg_max[1],
|
||||
(unsigned long long)seg_max[2], (unsigned long long)seg_max[3],
|
||||
(unsigned long long)seg_max[4],
|
||||
(unsigned)histo[0], (unsigned)histo[1], (unsigned)histo[2],
|
||||
(unsigned)histo[3], (unsigned)histo[4], tput_kbps);
|
||||
(unsigned)histo[3], (unsigned)histo[4],
|
||||
(unsigned)histo[5], (unsigned)histo[6], tput_kbps);
|
||||
|
||||
http_set_cors_headers(req);
|
||||
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
||||
|
||||
Reference in New Issue
Block a user