feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
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:
小龙虾
2026-07-21 01:16:43 +08:00
parent 00b716c8db
commit 770ae14156
29 changed files with 1712 additions and 2428 deletions
+115
View File
@@ -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);
}
}