feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
SPI Link v2 Self-Test / selftest (push) Has been cancelled
SPI Link v2 Self-Test / selftest (push) Has been cancelled
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等 多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量 的撕裂读问题,并增强了BLE OTA的数据传输可靠性。 refactor(health_monitor): 修复heap hook多字段撕裂读问题 采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook 之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免 跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。 fix(ble_ota): 实现DATA_IN丢包负反馈机制 当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免 静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆 NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。 refactor(spi_master): 从堆分配改为固定Job池管理 将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的 calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制 管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 52
|
||||
#define FW_VERSION_STRING "0.3.52"
|
||||
#define FW_VERSION_PATCH 53
|
||||
#define FW_VERSION_STRING "0.3.53"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-20 02:50:38"
|
||||
#define BUILD_TIME "2026-07-21 00:23:44"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "b807645"
|
||||
#define GIT_HASH "00b716c"
|
||||
|
||||
@@ -46,16 +46,15 @@
|
||||
// ★ 记录策略:health_mon 周期把真实 free 同步进 g_est_free;
|
||||
// hook 内仅做 g_est_free -= size(C3 单核,无锁可接受漂移)。
|
||||
// 当 est 创新低时更新 hook 快照。free hook 不知 size,靠周期 resync 纠偏。
|
||||
//
|
||||
// ★ 采样协议(消除多字段撕裂读):
|
||||
// hook 写「最后一击」快照的多个字段(min_est/alloc_size/caller/free_after/fired
|
||||
// + recent_allocs[])虽各自是字宽 volatile,但 reader 跨字段组合可撕裂
|
||||
// (fired=1 配上上一次的 alloc_size)。reader(maybeRecordHeapLowWater)在
|
||||
// portENTER_CRITICAL(&s_hookMux) 内一次性拷出全部字段——单核禁中断即与 hook
|
||||
// (在 heap 临界区内写)互斥。m_mutex 不参与此互斥(hook 不获取它)。
|
||||
// reader 临界区仅做 volatile 拷贝,不分配、不调 FreeRTOS/flash,保持极短。
|
||||
namespace {
|
||||
volatile uint32_t g_hook_min_est = 0xFFFFFFFFu; ///< hook 见过的最低 est free
|
||||
volatile uint32_t g_hook_alloc_size = 0; ///< 触发 est 新低的那次 alloc 大小
|
||||
volatile uint32_t g_hook_caller_addr = 0; ///< 调用者返回地址
|
||||
volatile uint32_t g_hook_free_after = 0; ///< 那次后的 est free(非精确实测)
|
||||
volatile uint32_t g_hook_fired = 0;
|
||||
volatile uint32_t g_hook_armed = 0;
|
||||
volatile uint32_t g_est_free = 0; ///< 近似空闲,任务上下文 resync
|
||||
volatile uint32_t g_ip_event_count = 0;
|
||||
|
||||
/** 最近若干次「值得关注」的 alloc(size≥阈值),供破新低时对照。 */
|
||||
struct RecentAllocSnap {
|
||||
uint32_t size;
|
||||
@@ -63,17 +62,82 @@ struct RecentAllocSnap {
|
||||
};
|
||||
static constexpr uint32_t kHookInterestingMin = 48; ///< 忽略极小碎片分配
|
||||
static constexpr size_t kRecentAllocCap = 4;
|
||||
|
||||
volatile uint32_t g_hook_min_est = 0xFFFFFFFFu; ///< hook 见过的最低 est free
|
||||
volatile uint32_t g_hook_alloc_size = 0; ///< 触发 est 新低的那次 alloc 大小
|
||||
volatile uint32_t g_hook_caller_addr = 0; ///< 调用者返回地址
|
||||
volatile uint32_t g_hook_free_after = 0; ///< 那次后的 est free(estimated,非精确实测)
|
||||
volatile uint32_t g_hook_fired = 0;
|
||||
volatile uint32_t g_hook_armed = 0;
|
||||
volatile uint32_t g_est_free = 0; ///< estimated 近似空闲(只减不增),health_mon 周期用 esp_get_free_heap_size() 实测 resync 纠偏;误差上界 ≈ resync 周期(5s)内总 alloc 字节
|
||||
volatile uint32_t g_ip_event_count = 0;
|
||||
|
||||
RecentAllocSnap g_recent_allocs[kRecentAllocCap] = {};
|
||||
volatile uint32_t g_recent_alloc_w = 0;
|
||||
volatile uint32_t g_recent_alloc_n = 0;
|
||||
|
||||
/* s_hookMux:reader(maybeRecordHeapLowWater,任务上下文)取多字段一致快照用的
|
||||
* portMUX(单核 = 禁中断)。hook 端不加锁——它在 heap 临界区内写,与 reader 的
|
||||
* 禁中断互斥。仅用于消除「最后一击」快照多字段撕裂读,不保护 m_heapLowWater
|
||||
* (后者由 m_mutex 保护)。 */
|
||||
static portMUX_TYPE s_hookMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
void hookResyncEstFree(uint32_t real_free)
|
||||
{
|
||||
g_est_free = real_free;
|
||||
}
|
||||
|
||||
/** @brief reader 一次性拷出的 hook 共享状态一致快照(任务上下文,禁中断极短临界区)。 */
|
||||
struct HookSnap {
|
||||
uint32_t alloc_size;
|
||||
uint32_t caller_addr;
|
||||
uint32_t free_after; ///< estimated(非实测)
|
||||
uint32_t fired;
|
||||
uint32_t est_free;
|
||||
uint32_t min_est;
|
||||
uint32_t recent_n;
|
||||
RecentAllocSnap recent[kRecentAllocCap];
|
||||
};
|
||||
|
||||
/** @brief 任务上下文用:禁中断拷出 hook 共享状态一致快照。仅 volatile 拷贝,不分配/不调 FreeRTOS。 */
|
||||
HookSnap captureHookSnapshot()
|
||||
{
|
||||
HookSnap s = {};
|
||||
portENTER_CRITICAL(&s_hookMux);
|
||||
s.alloc_size = g_hook_alloc_size;
|
||||
s.caller_addr = g_hook_caller_addr;
|
||||
s.free_after = g_hook_free_after;
|
||||
s.fired = g_hook_fired;
|
||||
s.est_free = g_est_free;
|
||||
s.min_est = g_hook_min_est;
|
||||
s.recent_n = g_recent_alloc_n;
|
||||
for (uint32_t i = 0; i < s.recent_n && i < (uint32_t)kRecentAllocCap; ++i) {
|
||||
s.recent[i] = g_recent_allocs[i];
|
||||
}
|
||||
portEXIT_CRITICAL(&s_hookMux);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** @brief 在 s_hookMux 临界区内调用:消费已记录的 hook 命中,重置 min_est/fired。
|
||||
* 必须由 reader 在写完 HeapLowWaterRecord 后调用,避免同一窗口重复记。 */
|
||||
void consumeHookFiredInLock()
|
||||
{
|
||||
/* min_est 抬到当前 est:本次已记下低谷,下次需更低才会再次触发。
|
||||
* g_est_free 在此读以与 hook 写一致(s_hookMux 内)。 */
|
||||
g_hook_min_est = g_est_free;
|
||||
g_hook_fired = 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ESP-IDF heap alloc hook:仅 DRAM 标量 + 内联 return address,禁止 FreeRTOS/flash 长函数。
|
||||
//
|
||||
// ★ hook 可调用约束(IRAM 安全契约):
|
||||
// - 禁止:FreeRTOS API、flash/rodata 访问、mutex/semaphore、长函数。
|
||||
// - 允许:DRAM volatile 标量读写、__builtin_return_address、纯算术。
|
||||
// - 写共享状态天然原子:hook 已在 heap 临界区内(禁中断),reader 用 s_hookMux
|
||||
// 禁中断取一致快照,二者互斥;hook 端不加锁、保持 IRAM 安全。
|
||||
// - 嵌套分配:hook 内禁止任何可能触发 malloc 的调用(否则递归进 hook,armed
|
||||
// 标志不防递归——当前 hook 体内无分配调用,保持之)。
|
||||
extern "C"
|
||||
void esp_heap_trace_alloc_hook(void *ptr, size_t size, uint32_t caps)
|
||||
{
|
||||
@@ -582,6 +646,10 @@ void HealthMonitor::maybeRecordHeapLowWater()
|
||||
const uint32_t free_heap = (uint32_t)esp_get_free_heap_size();
|
||||
const uint32_t largest_block = (uint32_t)heap_caps_get_largest_free_block(CAPS);
|
||||
|
||||
/* hook 共享状态一致快照:禁中断一次性拷出,避免多字段撕裂读。
|
||||
* 必须在 m_mutex 外取——s_hookMux 与 m_mutex 不可嵌套持锁。 */
|
||||
const HookSnap hs = captureHookSnapshot();
|
||||
|
||||
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
||||
const uint8_t sys_state = (uint8_t)snap.sys_state;
|
||||
const uint8_t ota_channel = (uint8_t)snap.channel;
|
||||
@@ -686,20 +754,22 @@ void HealthMonitor::maybeRecordHeapLowWater()
|
||||
rec.default_free_heap = default_free;
|
||||
rec.alloc_count = alloc_count;
|
||||
// heap hook:「最后一击」alloc(无任务名;见文件头注释)
|
||||
// 字段取自锁外已取的一致快照 hs(captureHookSnapshot),不再裸读 volatile,
|
||||
// 消除 fired/size/caller/free_after 多字段撕裂读。
|
||||
{
|
||||
uint32_t hook_size = g_hook_alloc_size;
|
||||
uint32_t hook_caller = g_hook_caller_addr;
|
||||
uint32_t hook_after = g_hook_free_after;
|
||||
uint32_t hook_fired = g_hook_fired;
|
||||
const uint32_t hook_size = hs.alloc_size;
|
||||
const uint32_t hook_caller = hs.caller_addr;
|
||||
const uint32_t hook_after = hs.free_after;
|
||||
const uint32_t hook_fired = hs.fired;
|
||||
|
||||
/* 窗口内更大块(若存在):发送前小 pbuf 压低水位时,主因更可能是此前 bulk */
|
||||
uint32_t big_size = 0;
|
||||
uint32_t big_caller = 0;
|
||||
const uint32_t n = g_recent_alloc_n;
|
||||
const uint32_t n = hs.recent_n;
|
||||
for (uint32_t i = 0; i < n && i < (uint32_t)kRecentAllocCap; ++i) {
|
||||
if (g_recent_allocs[i].size > big_size) {
|
||||
big_size = g_recent_allocs[i].size;
|
||||
big_caller = g_recent_allocs[i].caller;
|
||||
if (hs.recent[i].size > big_size) {
|
||||
big_size = hs.recent[i].size;
|
||||
big_caller = hs.recent[i].caller;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,8 +795,11 @@ void HealthMonitor::maybeRecordHeapLowWater()
|
||||
rec.hook_task[sizeof(rec.hook_task) - 1] = '\0';
|
||||
}
|
||||
|
||||
g_hook_min_est = g_est_free;
|
||||
g_hook_fired = 0;
|
||||
/* 消费已记录的 hook 命中:在 s_hookMux 内重置 min_est/fired,避免同一窗口
|
||||
* 重复记。与 hook 写互斥(禁中断),不持 m_mutex 同时持 s_hookMux。 */
|
||||
portENTER_CRITICAL(&s_hookMux);
|
||||
consumeHookFiredInLock();
|
||||
portEXIT_CRITICAL(&s_hookMux);
|
||||
}
|
||||
rec.ip_event_count = g_ip_event_count;
|
||||
|
||||
@@ -747,6 +820,10 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
}
|
||||
|
||||
const HeapLowWaterRecord &r = m_heapLowWater;
|
||||
/* hook_free_after 是 estimated(hook 只减不增近似值),JSON 字段名显式标注 est,
|
||||
* 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,
|
||||
"{\"count\":1,\"records\":[{"
|
||||
"\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,"
|
||||
@@ -757,7 +834,8 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
"\"ble_data_queue_depth\":%u,\"ble_reorder_cached\":%u,"
|
||||
"\"spi_pending_jobs\":%u,\"default_free_heap\":%lu,\"alloc_count\":%lu,"
|
||||
"\"hook_task\":\"%s\",\"hook_alloc_size\":%lu,\"hook_caller_addr\":%lu,"
|
||||
"\"hook_free_after\":%lu,\"hook_fired\":%lu,\"ip_event_count\":%lu,"
|
||||
"\"hook_est_free_after\":%lu,\"hook_confidence\":\"%s\",\"hook_fired\":%lu,"
|
||||
"\"ip_event_count\":%lu,"
|
||||
"\"task_stacks\":[",
|
||||
(unsigned long long)r.ts_ms,
|
||||
(unsigned long)r.free_heap,
|
||||
@@ -781,6 +859,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
(unsigned long)r.hook_alloc_size,
|
||||
(unsigned long)r.hook_caller_addr,
|
||||
(unsigned long)r.hook_free_after,
|
||||
hook_confidence,
|
||||
(unsigned long)r.hook_fired,
|
||||
(unsigned long)r.ip_event_count);
|
||||
if (pos == offset) {
|
||||
|
||||
@@ -81,7 +81,7 @@ struct HeapLowWaterRecord {
|
||||
char hook_task[16]; ///< 分配类别 net?/bulk/tiny,不是任务名
|
||||
uint32_t hook_alloc_size; ///< 最后一击 alloc 字节数
|
||||
uint32_t hook_caller_addr; ///< 调用者返回地址(addr2line)
|
||||
uint32_t hook_free_after; ///< 该次后的近似 free(est,非精确实测)
|
||||
uint32_t hook_free_after; ///< 该次后的近似 free(**estimated 低水位近似值**,非精确实测;JSON 输出为 hook_est_free_after + hook_confidence,前端应标注 estimated)
|
||||
uint32_t hook_fired; ///< 0=本窗口 hook 未命中
|
||||
/* DHCP/IP 事件计数:每次 STA GOT_IP(含续约)+1。
|
||||
* 对比两次低谷记录的计数差,确认低谷是不是 DHCP 续约引起。 */
|
||||
|
||||
@@ -54,6 +54,33 @@ ota_meta_t s_ble_gd32_meta = {};
|
||||
bool s_ble_gd32_meta_active = false;
|
||||
bool s_conn_fallback_requested = false;
|
||||
|
||||
// ============================================================
|
||||
// DATA_IN 丢包负反馈:把 malloc 失败 / 队列满从"静默丢包"变成
|
||||
// "主动发缺口 ACK 让前端按 completed_bytes 续传退避"。
|
||||
//
|
||||
// 现状:writeValueWithoutResponse 窗口内丢包时,NimBLE 不会向对端
|
||||
// 回 ATT Error Response(即发即忘),返回非零 ATT 错误对窗口内
|
||||
// 包无效,只能走 DATA_OUT 通知通道发窗口 ACK。
|
||||
// 静默丢包会让前端等满 8s 窗口超时后整窗口重发 → 重传风暴叠加
|
||||
// 低堆时可能二次失败。这里在丢包路径补发一次缺口 ACK(seq 取
|
||||
// s_reorder.expected-1,completed 取实际落盘字节),让前端立刻
|
||||
// 从 lastAckBytes 续传,客户端已有续传语义无需改动。
|
||||
//
|
||||
// 节流:丢包路径无"进度/窗口边界"可复用,用"丢包计数 ≥ ACK_WINDOW
|
||||
// 或距上次 ≥ NEG_ACK_MIN_INTERVAL_US"双约束,防止重传叠加时
|
||||
// ACK 通知打爆 NimBLE mbuf 池(与现有正常路径节流同动机)。
|
||||
// 状态仅在 NimBLE host task 回调中读写(单线程),无需锁。
|
||||
// ============================================================
|
||||
static uint32_t s_ota_drop_malloc_fail = 0; // DATA_IN malloc 失败累计
|
||||
static uint32_t s_ota_drop_queue_full = 0; // DATA_IN 队列满累计
|
||||
static uint32_t s_neg_ack_sent = 0; // 负 ACK 发送累计
|
||||
static uint32_t s_neg_ack_drop_count = 0; // 自上次发负 ACK 以来累计丢包数
|
||||
static int64_t s_last_neg_ack_us = 0; // 上次负 ACK 时刻(esp_timer_get_time)
|
||||
// 节流常量:与正常 ACK 路径同窗口量级(BLE_OTA_ACK_WINDOW=64),
|
||||
// 最小间隔 200ms 兜底,避免极端低堆时负 ACK 抖动。
|
||||
static constexpr uint32_t NEG_ACK_DROP_WINDOW = 64; // 丢包数阈值
|
||||
static constexpr int64_t NEG_ACK_MIN_INTERVAL_US = 200 * 1000; // 200ms
|
||||
|
||||
// 前向声明(alloc/free 互相引用)
|
||||
void esp32_reorder_free();
|
||||
|
||||
@@ -416,6 +443,12 @@ void ble_ota_session_reset()
|
||||
s_gd32_ooo_count = 0;
|
||||
s_ble_gd32_meta = {};
|
||||
s_ble_gd32_meta_active = false;
|
||||
// 复位 DATA_IN 丢包负反馈计数/节流状态,下次会话从干净状态开始
|
||||
s_ota_drop_malloc_fail = 0;
|
||||
s_ota_drop_queue_full = 0;
|
||||
s_neg_ack_sent = 0;
|
||||
s_neg_ack_drop_count = 0;
|
||||
s_last_neg_ack_us = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,6 +492,79 @@ void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 DATA_IN batch 头部解析 cmd 字节。
|
||||
*
|
||||
* DATA_IN 回调只把 mbuf 拷到 buf,未解析协议头;负 ACK 需要 cmd 字节
|
||||
* 区分 ESP32/GD32 续传分支,避免误报让前端走错路径。与 worker 中
|
||||
* ble_ota_process_data_batch 的包头解析保持一致(buf[0] = cmd)。
|
||||
*
|
||||
* @param buf 数据缓冲区。
|
||||
* @param recv_len 数据长度。
|
||||
* @return cmd 字节;非法长度返回 0(GD32 命令),调用方据此不发误导性 ACK。
|
||||
*/
|
||||
static uint8_t parse_cmd_from_batch(const uint8_t *buf, size_t recv_len)
|
||||
{
|
||||
// 与 ble_ota_process_data_batch 一致:至少 7 字节头才算合法包
|
||||
if (recv_len < 7 || buf == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return buf[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 丢包路径负 ACK:发缺口 ACK 让前端按 completed_bytes 续传退避。
|
||||
*
|
||||
* 触发于 DATA_IN 回调中 malloc 失败 / 队列满。窗口内包用
|
||||
* writeValueWithoutResponse(即发即忘),收不到 ATT Error Response,
|
||||
* 只能走 DATA_OUT 通知通道。负 ACK 复用 sendWindowAck,seq 取
|
||||
* s_reorder.expected-1(最近连续落盘序号),completed 取实际落盘字节,
|
||||
* 前端 resumeSeq = ceil(bytes/payloadSize) 续传,无需改客户端。
|
||||
*
|
||||
* 节流:丢包数 ≥ NEG_ACK_DROP_WINDOW 或距上次 ≥ NEG_ACK_MIN_INTERVAL_US
|
||||
* 才发一次,防止重传风暴期间 ACK 通知打爆 NimBLE mbuf 池。
|
||||
* 状态仅在 NimBLE host task 回调中读写,无需锁。
|
||||
*
|
||||
* 仅 ESP32 路径发负 ACK(s_reorder 状态在此文件内可见);GD32 中继转发
|
||||
* 的缺口反馈由 GD32 侧自行处理,本函数对 GD32 cmd 仅累计不发送。
|
||||
*/
|
||||
static void sendNegativeAckIfDue(uint8_t cmd)
|
||||
{
|
||||
// 累计丢包(无论是否发送,计数都增长,供可观测性)
|
||||
s_neg_ack_drop_count += 1;
|
||||
|
||||
// 节流判定:丢包数达阈值 或 距上次发送超过最小间隔
|
||||
const int64_t now_us = esp_timer_get_time();
|
||||
bool by_count = (s_neg_ack_drop_count >= NEG_ACK_DROP_WINDOW);
|
||||
bool by_time = ((now_us - s_last_neg_ack_us) >= NEG_ACK_MIN_INTERVAL_US);
|
||||
// 首次(s_last_neg_ack_us==0)也允许立即发,避免低堆首包丢就憋住
|
||||
bool first_time = (s_last_neg_ack_us == 0);
|
||||
if (!(by_count || by_time || first_time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅 ESP32 路径发负 ACK;GD32 中继缺口由 GD32 侧反馈,这里只计不发
|
||||
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
|
||||
// expected>0:已落盘过包,ackSeq=expected-1,completed=实际落盘字节
|
||||
// expected==0:首包都没落盘,发 ackSeq=0/completed=0,等价"从头续发"
|
||||
// (客户端 seq=0 用 write-with-response,本就等首包 ACK,不会误导)
|
||||
uint16_t ackSeq = (s_reorder.expected > 0U)
|
||||
? (uint16_t)(s_reorder.expected - 1U)
|
||||
: 0U;
|
||||
uint32_t completed = (s_reorder.expected > 0U)
|
||||
? (uint32_t)Esp32OtaCore::instance().getReceived()
|
||||
: 0U;
|
||||
sendWindowAck(cmd, ackSeq, completed);
|
||||
s_neg_ack_sent += 1;
|
||||
DEBUG_WARN(TAG, "DATA_IN 负ACK cmd=0x%02X seq=%u bytes=%lu (drop:malloc=%u qfull=%u)",
|
||||
cmd, ackSeq, (unsigned long)completed,
|
||||
(unsigned)s_ota_drop_malloc_fail, (unsigned)s_ota_drop_queue_full);
|
||||
}
|
||||
// 复位节流状态
|
||||
s_neg_ack_drop_count = 0;
|
||||
s_last_neg_ack_us = now_us;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送收尾阶段事件通知(开始/完成/失败/重启等)。
|
||||
* @param stage 阶段标识码。
|
||||
@@ -594,16 +700,25 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
// 将数据拷贝到堆上并送入工作队列,避免在 NimBLE host task 中执行
|
||||
// 耗时的 flash 操作(esp_ota_begin / esp_ota_write)导致 BLE 连接断开。
|
||||
if (s_otaDataQueue) {
|
||||
// 解析 cmd 字节供负 ACK 区分 ESP32/GD32 续传分支(与 worker 解析一致)
|
||||
uint8_t cmd = parse_cmd_from_batch(buf, (size_t)recv_len);
|
||||
uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len);
|
||||
if (heap_buf) {
|
||||
memcpy(heap_buf, buf, (size_t)recv_len);
|
||||
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
|
||||
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
|
||||
// 队列满:发缺口 ACK 让前端按 completed_bytes 续传退避,
|
||||
// 而非等满 8s 窗口超时后整窗口重发 → 重传风暴叠加低堆二次失败
|
||||
s_ota_drop_queue_full += 1;
|
||||
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
|
||||
free(heap_buf);
|
||||
sendNegativeAckIfDue(cmd);
|
||||
}
|
||||
} else {
|
||||
// malloc 失败:同上,发缺口 ACK 形成可恢复协议反馈,避免静默丢包
|
||||
s_ota_drop_malloc_fail += 1;
|
||||
DEBUG_ERR(TAG, "DATA_IN 堆内存不足 (%d 字节),丢弃", recv_len);
|
||||
sendNegativeAckIfDue(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+150
-54
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -30,7 +29,7 @@
|
||||
|
||||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
|
||||
/** @brief submitJob 堆 Job 与 worker 之间的 finished/abandoned 交接。 */
|
||||
/** @brief submitJob 池 Job 与 worker 之间的 finished/abandoned/in_use 交接。 */
|
||||
static portMUX_TYPE s_spiJobMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
/**
|
||||
@@ -296,38 +295,105 @@ void SpiMaster::executeJob(Job &job)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief worker 完成堆上 Job 后的收尾:释放完成信号量,超时放弃时由 worker free。
|
||||
* @param job 堆分配的 Job 指针
|
||||
* @brief 从固定 Job 池取空闲槽;池耗尽返回 nullptr。
|
||||
* @note in_use 在 critical 内置 true 并保持,禁止整体 Job{} 赋值清掉占用标志。
|
||||
*/
|
||||
void SpiMaster::finishHeapJob(Job *job)
|
||||
SpiMaster::Job *SpiMaster::acquireJobSlot()
|
||||
{
|
||||
if (!m_jobPoolReady) {
|
||||
return nullptr;
|
||||
}
|
||||
Job *slot = nullptr;
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
for (UBaseType_t i = 0; i < kJobPoolSize; ++i) {
|
||||
if (!m_jobPool[i].in_use) {
|
||||
slot = &m_jobPool[i];
|
||||
/* 清业务字段但 in_use 全程 true,避免与其它 acquire 竞态 */
|
||||
SemaphoreHandle_t done = slot->done;
|
||||
slot->kind = JobKind::Transact;
|
||||
slot->timeout_ms = 0U;
|
||||
slot->type = 0U;
|
||||
slot->expected_type = 0xFFU;
|
||||
slot->role_hint = 0U;
|
||||
slot->payload_len = 0U;
|
||||
slot->out_type = nullptr;
|
||||
slot->out_payload = nullptr;
|
||||
slot->out_max = 0U;
|
||||
slot->out_len = nullptr;
|
||||
slot->out_uptime = nullptr;
|
||||
slot->out_version = nullptr;
|
||||
slot->result = ESP_FAIL;
|
||||
slot->done = done;
|
||||
slot->finished = false;
|
||||
slot->abandoned = false;
|
||||
slot->in_use = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (slot == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
/* 仅本槽所有者 drain;不可在 critical 内 take */
|
||||
if (slot->done != nullptr) {
|
||||
while (xSemaphoreTake(slot->done, 0) == pdTRUE) {
|
||||
}
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 归还池槽(调用方已独占该 Job,不再与 worker 共享)。
|
||||
*/
|
||||
void SpiMaster::releaseJobSlot(Job *job)
|
||||
{
|
||||
if (job == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 先 give,再标 finished;提交方 take 成功后会 spin 等 finished 再 free。 */
|
||||
if (job->done != nullptr) {
|
||||
while (xSemaphoreTake(job->done, 0) == pdTRUE) {
|
||||
}
|
||||
}
|
||||
job->out_type = nullptr;
|
||||
job->out_payload = nullptr;
|
||||
job->out_len = nullptr;
|
||||
job->out_uptime = nullptr;
|
||||
job->out_version = nullptr;
|
||||
job->payload_len = 0U;
|
||||
job->result = ESP_FAIL;
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
job->finished = false;
|
||||
job->abandoned = false;
|
||||
job->in_use = false;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief worker 完成池 Job 后的收尾:give done;abandoned 时由 worker release 槽。
|
||||
* @param job 池内 Job 指针
|
||||
*/
|
||||
void SpiMaster::finishPoolJob(Job *job)
|
||||
{
|
||||
if (job == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 先 give,再标 finished;提交方 take 成功后会 spin 等 finished 再 release。 */
|
||||
if (job->done) {
|
||||
(void)xSemaphoreGive(job->done);
|
||||
}
|
||||
if (!job->heap_allocated) {
|
||||
return;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
job->finished = true;
|
||||
const bool free_it = job->abandoned;
|
||||
const bool release_it = job->abandoned;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (free_it) {
|
||||
if (job->done) {
|
||||
vSemaphoreDelete(job->done);
|
||||
job->done = nullptr;
|
||||
}
|
||||
free(job);
|
||||
if (release_it) {
|
||||
releaseJobSlot(job);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将 Job 提交给 SPI worker 串行执行(非 worker 线程走队列)。
|
||||
* @param job 任务描述(栈上 Job 会拷到堆上再排队)
|
||||
* @return 业务结果码;队列满/超时返回 ESP_ERR_TIMEOUT
|
||||
* @param job 任务描述(栈上字段拷入固定池槽再排队)
|
||||
* @return 业务结果码;池耗尽 ESP_ERR_NO_MEM;队列满/超时 ESP_ERR_TIMEOUT
|
||||
*/
|
||||
esp_err_t SpiMaster::submitJob(Job &job)
|
||||
{
|
||||
@@ -336,70 +402,81 @@ esp_err_t SpiMaster::submitJob(Job &job)
|
||||
return job.result;
|
||||
}
|
||||
|
||||
/* 跨任务作业:堆上 Job + binary 完成信号量 */
|
||||
Job *heap_job = static_cast<Job *>(calloc(1, sizeof(Job)));
|
||||
if (heap_job == nullptr) {
|
||||
DEBUG_ERR("SPI", "submitJob: calloc failed kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
*heap_job = job;
|
||||
heap_job->heap_allocated = true;
|
||||
heap_job->finished = false;
|
||||
heap_job->abandoned = false;
|
||||
heap_job->result = ESP_FAIL;
|
||||
heap_job->done = xSemaphoreCreateBinary();
|
||||
if (heap_job->done == nullptr) {
|
||||
free(heap_job);
|
||||
DEBUG_ERR("SPI", "submitJob: done sem create failed");
|
||||
/* 跨任务:固定池 Job + 静态 binary 完成信号量(无 calloc/xSemaphoreCreate) */
|
||||
Job *pool_job = acquireJobSlot();
|
||||
if (pool_job == nullptr) {
|
||||
DEBUG_ERR("SPI", "submitJob: job pool exhausted kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* 逐字段拷贝业务参数;禁止整体赋值覆盖 done/in_use */
|
||||
pool_job->kind = job.kind;
|
||||
pool_job->timeout_ms = job.timeout_ms;
|
||||
pool_job->type = job.type;
|
||||
pool_job->expected_type = job.expected_type;
|
||||
pool_job->role_hint = job.role_hint;
|
||||
pool_job->payload_len = job.payload_len;
|
||||
if (job.payload_len > 0U) {
|
||||
const size_t n = (job.payload_len > sizeof(pool_job->payload))
|
||||
? sizeof(pool_job->payload)
|
||||
: job.payload_len;
|
||||
memcpy(pool_job->payload, job.payload, n);
|
||||
pool_job->payload_len = n;
|
||||
} else {
|
||||
pool_job->payload_len = 0U;
|
||||
}
|
||||
pool_job->out_type = job.out_type;
|
||||
pool_job->out_payload = job.out_payload;
|
||||
pool_job->out_max = job.out_max;
|
||||
pool_job->out_len = job.out_len;
|
||||
pool_job->out_uptime = job.out_uptime;
|
||||
pool_job->out_version = job.out_version;
|
||||
pool_job->result = ESP_FAIL;
|
||||
pool_job->finished = false;
|
||||
pool_job->abandoned = false;
|
||||
/* done / in_use 保持 acquire 时状态 */
|
||||
|
||||
const TickType_t qwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 2000U);
|
||||
if (xQueueSend(m_jobQueue, &heap_job, qwait) != pdTRUE) {
|
||||
if (xQueueSend(m_jobQueue, &pool_job, qwait) != pdTRUE) {
|
||||
DEBUG_WARN("SPI", "worker job queue full/timeout kind=%u", (unsigned)job.kind);
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
free(heap_job);
|
||||
releaseJobSlot(pool_job);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
xTaskNotify(m_workerTask, 0x10U, eSetBits);
|
||||
|
||||
const TickType_t dwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U);
|
||||
if (xSemaphoreTake(heap_job->done, dwait) == pdTRUE) {
|
||||
const esp_err_t result = heap_job->result;
|
||||
/* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 free。 */
|
||||
if (xSemaphoreTake(pool_job->done, dwait) == pdTRUE) {
|
||||
const esp_err_t result = pool_job->result;
|
||||
/* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 release。 */
|
||||
for (;;) {
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
const bool fin = heap_job->finished;
|
||||
const bool fin = pool_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (fin) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
releaseJobSlot(pool_job);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 超时:标记 abandoned;若 worker 已跑完则本侧 free,否则 worker finishHeapJob free。 */
|
||||
/* 超时:标记 abandoned;若 worker 已跑完则本侧 release,否则 finishPoolJob release。 */
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
heap_job->abandoned = true;
|
||||
const bool already_done = heap_job->finished;
|
||||
pool_job->abandoned = true;
|
||||
const bool already_done = pool_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
|
||||
if (already_done) {
|
||||
(void)xSemaphoreTake(heap_job->done, 0);
|
||||
const esp_err_t result = heap_job->result;
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
(void)xSemaphoreTake(pool_job->done, 0);
|
||||
const esp_err_t result = pool_job->result;
|
||||
releaseJobSlot(pool_job);
|
||||
return result;
|
||||
}
|
||||
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u(堆 Job 交 worker 释放)",
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u(池槽交 worker 释放)",
|
||||
(unsigned)job.kind);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
@@ -434,7 +511,7 @@ void SpiMaster::workerLoop()
|
||||
Job *job = nullptr;
|
||||
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
|
||||
executeJob(*job);
|
||||
finishHeapJob(job);
|
||||
finishPoolJob(job);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -890,7 +967,7 @@ esp_err_t SpiMaster::init()
|
||||
m_stats = SpiLinkStats{};
|
||||
m_eventHead = m_eventTail = m_eventCount = 0;
|
||||
|
||||
/* 创建 job 队列、事件信号量与 SPI worker(init 仍持 m_mutex) */
|
||||
/* 创建 job 队列、固定 Job pool(静态 done 信号量)、事件信号量与 SPI worker */
|
||||
if (m_jobQueue == nullptr) {
|
||||
m_jobQueue = xQueueCreate(kJobQueueLen, sizeof(Job *));
|
||||
if (m_jobQueue == nullptr) {
|
||||
@@ -898,6 +975,25 @@ esp_err_t SpiMaster::init()
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
if (!m_jobPoolReady) {
|
||||
for (UBaseType_t i = 0; i < kJobPoolSize; ++i) {
|
||||
m_jobPool[i] = Job{};
|
||||
m_jobPool[i].done = xSemaphoreCreateBinary();
|
||||
if (m_jobPool[i].done == nullptr) {
|
||||
DEBUG_ERR("SPI", "SPI job pool done sem create failed idx=%u",
|
||||
(unsigned)i);
|
||||
for (UBaseType_t j = 0; j < i; ++j) {
|
||||
vSemaphoreDelete(m_jobPool[j].done);
|
||||
m_jobPool[j].done = nullptr;
|
||||
}
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
m_jobPool[i].in_use = false;
|
||||
}
|
||||
m_jobPoolReady = true;
|
||||
DEBUG_INFO("SPI", "SPI job pool ready size=%u queue=%u",
|
||||
(unsigned)kJobPoolSize, (unsigned)kJobQueueLen);
|
||||
}
|
||||
if (m_eventSem == nullptr) {
|
||||
m_eventSem = xSemaphoreCreateCounting(kEventSlots, 0);
|
||||
if (m_eventSem == nullptr) {
|
||||
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
|
||||
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
|
||||
|
||||
/** @brief SPI 任务队列当前待处理 job 数(每个堆 Job 持有 calloc 的 Job 结构)。
|
||||
/** @brief SPI 任务队列当前待处理 job 数(固定 Job pool 槽,无堆分配)。
|
||||
* 供 /system/heap_low_log 堆低水位快照采样,定位"低谷是不是 SPI 队列堆积引起"。 */
|
||||
UBaseType_t getPendingJobCount() const { return m_jobQueue ? uxQueueMessagesWaiting(m_jobQueue) : 0; }
|
||||
|
||||
@@ -302,7 +302,7 @@ private:
|
||||
uint8_t m_eventTail = 0;
|
||||
uint8_t m_eventCount = 0;
|
||||
|
||||
// ── 作业提交(跨任务:堆 Job + binary done;worker 串行执行)──
|
||||
// ── 作业提交(跨任务:固定 Job pool + 静态 binary done;worker 串行执行)──
|
||||
enum class JobKind : uint8_t {
|
||||
Transact = 0,
|
||||
Poll,
|
||||
@@ -326,19 +326,27 @@ private:
|
||||
uint32_t *out_uptime = nullptr;
|
||||
gd32_version_t *out_version = nullptr;
|
||||
esp_err_t result = ESP_FAIL;
|
||||
SemaphoreHandle_t done = nullptr;
|
||||
/** submitJob 堆分配的 Job:超时放弃后由 worker 释放,成功路径由提交方释放。 */
|
||||
bool heap_allocated = false;
|
||||
SemaphoreHandle_t done = nullptr; ///< 池槽静态 binary,init 创建,永不 delete
|
||||
volatile bool finished = false; ///< worker 已 execute + give
|
||||
volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 free
|
||||
volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 release 槽
|
||||
bool in_use = false; ///< 池槽占用(critical 保护)
|
||||
};
|
||||
/** 队列深度;池 = 队列 + 1(在途执行槽),覆盖 queued + executing。 */
|
||||
static constexpr UBaseType_t kJobQueueLen = 6;
|
||||
static constexpr UBaseType_t kJobPoolSize = kJobQueueLen + 1U;
|
||||
|
||||
Job m_jobPool[kJobPoolSize] = {};
|
||||
bool m_jobPoolReady = false;
|
||||
|
||||
bool onWorker() const;
|
||||
/** @brief 从固定池取空闲 Job;池耗尽返回 nullptr(ESP_ERR_NO_MEM)。 */
|
||||
Job *acquireJobSlot();
|
||||
/** @brief 归还池槽(清空业务字段/标志,drain done 信号量)。 */
|
||||
void releaseJobSlot(Job *job);
|
||||
esp_err_t submitJob(Job &job);
|
||||
void executeJob(Job &job);
|
||||
/** @brief worker 完成堆 Job 后的生命周期收尾(give done + 可能 free)。 */
|
||||
void finishHeapJob(Job *job);
|
||||
/** @brief worker 完成池 Job 后的收尾(give done;abandoned 时 release 槽)。 */
|
||||
void finishPoolJob(Job *job);
|
||||
/** @brief FreeRTOS 任务入口,转到 workerLoop。 */
|
||||
static void workerEntry(void *arg);
|
||||
/** @brief SPI worker 主循环:EVENT 按铃 + Job 队列。 */
|
||||
|
||||
Reference in New Issue
Block a user