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:
小龙虾
2026-08-02 17:37:19 +08:00
parent 04a118219f
commit f9cbcc396b
42 changed files with 1957 additions and 4537 deletions
+20 -97
View File
@@ -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;
}
+10 -16
View File
@@ -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
+6 -2
View File
@@ -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;
+10 -2
View File
@@ -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;
+6 -42
View File
@@ -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);
}
+27 -31
View File
@@ -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;
}
+6 -1
View File
@@ -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
View File
@@ -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) {
+6 -29
View File
@@ -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 -21
View File
@@ -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;
}
}
+62
View File
@@ -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
+68 -33
View File
@@ -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");