fix(boot): 修复SPI OTA协议中的ACK丢失死锁和缓冲区管理问题

- 修复ACK丢失导致的死锁问题:修改onCsRelease()逻辑,只有在ESP32发送
  有效v2请求帧时才清除s_txPending,避免dummy poll误清待发ACK

- 优化缓冲区管理和信用机制:实现增量式flash缓冲区排空(drainFlashBufferOne),
  避免一次性排空阻塞主循环;CREDIT计算同时考虑ISR DATA队列待处理数量

- 添加重复序列处理增强:当DATA仍在主循环排队时优先回复ACK(type=0x80),
  避免ESP32长时间接收WAIT导致连接中断

- 修复缓冲区满时的数据包丢失:在handleLinkMessage中同步排空已写入槽位,
  腾出空间后继续处理当前包,防止永久DUP-SEQ错误

- 优化主循环处理顺序:先处理ISR DATA队列并及时回复ACK,再进行flash写入,
  减少ESP32超时重发导致的断续问题

- 增加FLASH写入错误检查:验证fmc_word_program返回状态,写入失败时及时
  返回避免数据损坏
This commit is contained in:
cbb
2026-07-14 12:10:25 +08:00
parent c644c29a4b
commit 1223be9ca9
5 changed files with 176 additions and 68 deletions
+96 -12
View File
@@ -394,12 +394,19 @@ void SpiOta::isrProcessFrame() {
s_expectedRxSeq = (u8)(s_expectedRxSeq + 1U);
} else if (dist >= 128U) {
/* 重复请求(rx_seq 落后于 expected):重发上一条缓存响应,
* 不推进序号。若 DATA 仍在主循环排队,回 WAIT 让 ESP32 继续 poll。 */
* 不推进序号。若 DATA 仍在主循环排队,回 WAIT 让 ESP32 继续 poll。
*
* 注意:缓存若仍是 ISR 即时 WAIT,而主循环已处理完并写过 ACK,
* 应优先回放 ACK(type=0x80),避免 ESP32 长时间只见 WAIT → 断续。 */
if (s_hasCachedResp && s_lastRespLen > 0U) {
queueTxFrame(s_lastRespBuf, s_lastRespLen);
} else if (s_otaDataCnt > 0U) {
u8 wait_hint = 0x03U;
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
} else if (s_otaHandler != nullptr && s_otaHandler->isActive()) {
/* 无缓存但会话仍活跃:回 WAIT 促使 ESP32 继续 poll,勿静默丢弃 */
u8 wait_hint = 0x03U;
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
}
isrLogFrameEx("[DUP-SEQ]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
s_rxStreamLen = 0U;
@@ -575,18 +582,39 @@ bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len)
if (s_ringPending) {
return false;
}
/* 已有 OTA ACK/WAIT 待发时禁止 CREDIT_UPDATE 等按铃覆盖 s_txBuf,
* 否则 ESP32 读到 EVENT 而非 ACK → 超时重发 → DUP-SEQ 断续。
* CREDIT 仍随下一笔 RESPONSE 帧头更新。 */
if (s_txPending && s_txLen > 0U) {
return false;
}
if (!s_linkState.canRingNow()) {
/* ESP32 尚未完成上一帧读取(PB6 未出现上升沿),拒绝发起按铃,
* 避免下降沿被 ESP32 ISR 在非 IDLE 态丢弃。 */
return false;
}
u8 frame_buf[kTxBufSize];
u16 built_len = 0U;
if (!buildFrame((spi_ota_frame_type_t)frame_type, payload, payload_len, frame_buf, &built_len)) {
if (payload_len > kBootMaxPayload) {
return false;
}
/* L5:主动上报必须用 EVENT(非 RESPONSE),与 App requestEsp32Read 对齐。 */
u8 frame_buf[kTxBufSize];
size_t built = 0U;
if (!v2_build_frame(frame_type, V2_FLAG_EVENT,
(uint32_t)s_sessionId,
(uint8_t)s_txSeq, 0U, (uint8_t)g_localCredit,
payload, (size_t)payload_len,
frame_buf, &built)) {
return false;
}
if (built > kTxBufSize) {
return false;
}
const u16 built_len = (u16)built;
if (s_v2Active) {
s_txSeq = (u8)(s_txSeq + 1U);
}
uint32_t primask = enterCritical();
queueTxFrame(frame_buf, built_len);
s_ringTxLen = built_len;
@@ -598,6 +626,17 @@ bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len)
return true;
}
void SpiOta::notifyCreditRestore() {
static u8 s_prevCredit = 8U;
const u8 cur = g_localCredit;
if (s_prevCredit == 0U && cur > 0U) {
const u8 pl[1] = { cur };
/* 队列忙时失败可接受:下一笔响应帧头仍带 CREDIT,ESP32 会更新 */
(void)requestEsp32Read(V2_TYPE_CREDIT_UPDATE, pl, 1U);
}
s_prevCredit = cur;
}
void SpiOta::cancelRing() {
if (!s_ringPending) {
return;
@@ -797,14 +836,35 @@ void SpiOta::onCsRelease() {
* armSpiDmasForNextCs() 装载后并不清除 s_txPending,若此处不标记交付,
* pending 会永远为 true → updateDataReady() 把 PB6 永久拉低 →
* ESP32 waitSpiReady(等 HIGH) 每包空等 SPI_READY_TIMEOUT_MS,
* 1116 包 OTA 表现为“卡在 1/1116 十几分钟”。
* 1116 包 OTA 表现为"卡在 1/1116 十几分钟"。
* 先清交付态;随后 isrProcessFrame/queueTxFrame 可再置 pending。
* 重复帧缓存在 s_lastRespBuf,不依赖 s_txPending 保活。 */
if (s_txPending && s_txLen > 0U) {
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
* 重复帧缓存在 s_lastRespBuf,不依赖 s_txPending 保活。
*
* ★ 修复 ACK 丢失死锁(2026-07-14):
* 旧代码无条件清 s_txPending,ESP32 poll 盲读(0xFF dummy)也会清掉
* GD32 待发 ACK。如果 ESP32 读到 ACK 但 CRC 解析失败,ACK 永久丢失,
* ESP32 永远等不到该 ACK → 卡死。
* 修复:只在 ESP32 发送了有效 v2 请求帧(含 SOF0/SOF1)时才清交付态;
* 纯 dummy poll(全 0xFF / 无 SOF)保留 s_txPending,让 ESP32 下次
* poll 能重新读到同一个 ACK。 */
bool has_v2_request = false;
if (s_rxStreamLen >= 2U) {
const u16 chk = (s_rxStreamLen < 16U) ? (u16)(s_rxStreamLen - 1U) : 16U;
for (u16 i = 0U; i < chk; ++i) {
if (s_rxStream[i] == V2_SOF0 && s_rxStream[i + 1U] == V2_SOF1) {
has_v2_request = true;
break;
}
}
}
if (has_v2_request) {
if (s_txPending && s_txLen > 0U) {
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
}
}
/* dummy poll:保留 s_txPending,ACK 可被下次 CS 重新读走 */
/* 按铃中若 ESP32 发的是 v2 协议帧(HELLO/OTA 等),不能当 dummy 读吞掉 */
if (s_ringPending) {
@@ -917,9 +977,24 @@ void SpiOta::poll() {
}
if (s_otaHandler != nullptr) {
/* 先消化 ISR 入队的 64B DATA(AES+入 flash 槽),再 process 擦除/drain。 */
/* 先 process 排空 flash_buf,再消化 ISR 入队的 64B DATA。
* 旧顺序先 handleLinkMessage 再 process,当 flash_buf 满时
* handleLinkMessage 会丢弃当前包(ISR 已推进 seq),导致永久 DUP-SEQ。 */
/* 先消化 ISR DATA 队列并尽快回 ACK,再 process 写 Flash。
* 旧顺序先 drainFlash 会把 ACK 推迟到整批 fmc_word_program 之后,
* ESP32 侧易超时重发 → DUP-SEQ 断续。flash 满时 handleLinkMessage
* 内部仍会 drainFlashBuffer 腾槽,不会永久丢包。 */
u8 data_pkt[64];
u8 drained = 0U;
#if DEBUG_PRINT
/* 限频:满速 OTA 时每包 printf 会拖慢主循环、放大断续 */
static u32 s_poll_log_n = 0U;
if (s_otaDataCnt > 0U && ((++s_poll_log_n % 32U) == 1U)) {
printf("[POLL] otaData=%u txPending=%d txLen=%u credit=%u\r\n",
(unsigned)s_otaDataCnt, (int)s_txPending, (unsigned)s_txLen,
(unsigned)g_localCredit);
}
#endif
while (drained < kOtaDataSlots && dequeueOtaData(data_pkt)) {
s_otaHandler->setTransportSender(sender);
s_otaHandler->handleLinkMessage(OTA_LINK_MSG_DATA, data_pkt, 64U);
@@ -930,6 +1005,11 @@ void SpiOta::poll() {
}
drained++;
}
/* ACK 已入队:CS 空闲则立刻 re-arm + 拉低 PB6,再去做慢速 Flash */
if (!s_csActive && s_txPending && s_txLen > 0U) {
armSpiDmasForNextCs();
updateDataReady();
}
s_otaHandler->process();
}
@@ -966,6 +1046,10 @@ u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) {
return copy_len;
}
u8 SpiOta::pendingDataCount() {
return s_otaDataCnt;
}
/* ============================================================
* C 风格兼容包装
* ============================================================ */
+6
View File
@@ -82,6 +82,12 @@ public:
/** @brief 取消当前按铃请求,释放 SPIready。 */
static void cancelRing();
/** @brief CREDIT 从 0 恢复为 >0 时发送 CREDIT_UPDATE 事件并拉低 DATA_READY。 */
static void notifyCreditRestore();
/** @brief ISR DATA 队列中待处理包数(供 OTA.cpp 计算 CREDIT 用)。 */
static u8 pendingDataCount();
/** @brief 主循环入队 TX 后装载 DMA + 更新 DATA_READY(擦除中途刷 WAIT 用)。 */
static void servicePendingTx();
+72 -55
View File
@@ -243,7 +243,7 @@ static inline void write_u32_to_4bytes(uint8_t *array, uint32_t value) {
static inline void send_structured_ack(uint32_t seq_or_flag) {
u8 send_data[6] = {0xBA, 0xC0, 0x00, 0x00, 0x00, 0x00};
#if DEBUG_PRINT
if (seq_or_flag <= 5U || seq_or_flag == 0xF0F0F0F0U) {
if (seq_or_flag <= 20U || seq_or_flag == 0xF0F0F0F0U) {
printf("[OTA] send_structured_ack seq=%u\r\n", (unsigned)seq_or_flag);
}
#endif
@@ -317,7 +317,11 @@ void OTAHandler::process() {
send_structured_ack(0U);
SpiOta::servicePendingTx();
}
drainFlashBuffer();
if (pending_finish_ack || pending_finish_ota) {
drainFlashBuffer();
} else {
drainFlashBufferOne();
}
if (pending_finish_ack && flash_buf_cnt == 0U) {
pending_finish_ack = false;
@@ -644,56 +648,42 @@ void OTAHandler::eraseApplicationArea() {
*/
void OTAHandler::drainFlashBuffer() {
#if DEBUG_PRINT
u32 drain_cnt = 0;
#endif
if (flash_buf_cnt == 0U) { return; }
/* v2 DATA_READY: Flash 写入不再用 PB6 背压,改由 CREDIT=0 通知 ESP32 停止发送。
* fmc_word_program 会让 CPU 总线停顿,但 ESP32 在 CREDIT=0 后改为 POLL 等待,
* 不会盲目发新事务导致 RX-OVR。 */
while (1) {
FlashSlot slot = {};
bool has_slot = false;
bool OTAHandler::drainFlashBufferOne() {
if (flash_buf_cnt == 0U) { return false; }
/* 先原子出队,再慢速写 Flash。
* 这样即使 ISR 中途 resetToIdle() 清空了环形缓冲计数,
* 这里也不会在写完后再把 0 继续 -- 成 255。 */
__disable_irq();
if (flash_buf_cnt > 0U) {
u8 ri = flash_buf_ri;
slot = flash_buf[ri];
flash_buf_ri = (ri + 1U) % kFlashBufSlots;
flash_buf_cnt--;
has_slot = true;
}
FlashSlot slot = {};
__disable_irq();
if (flash_buf_cnt > 0U) {
u8 ri = flash_buf_ri;
slot = flash_buf[ri];
flash_buf_ri = (ri + 1U) % kFlashBufSlots;
flash_buf_cnt--;
} else {
__enable_irq();
/* 缓冲被消费,恢复 CREDIT 通知 ESP32 可继续发数据。 */
g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt);
if (!has_slot) {
break;
}
writeApplicationData(slot.offset, slot.data, OTA_PACKET_SIZE);
#if DEBUG_PRINT
drain_cnt++;
#endif
return false;
}
/* Flash 写入完成,恢复 CREDIT 通知 ESP32 可继续发数据。
* v2 DATA_READY: 不再操作 PB6,PB6 由队列状态自动维护。 */
g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt);
__enable_irq();
writeApplicationData(slot.offset, slot.data, OTA_PACKET_SIZE);
{ u8 pending = SpiOta::pendingDataCount();
g_localCredit = (flash_buf_cnt + pending >= kFlashBufSlots) ? 0U
: (u8)(kFlashBufSlots - flash_buf_cnt - pending); }
SpiOta::notifyCreditRestore();
#if DEBUG_PRINT
/* 每累计 10 包打一次,避免 OTA 过程刷屏。 */
static u32 s_drain_since_log = 0U;
if (drain_cnt > 0U) {
s_drain_since_log += drain_cnt;
if (s_drain_since_log >= 10U) {
printf("[FW] drain %u pkts\r\n", (unsigned)s_drain_since_log);
s_drain_since_log = 0U;
}
s_drain_since_log++;
if (s_drain_since_log >= 10U) {
printf("[FW] drain %u pkts\r\n", (unsigned)s_drain_since_log);
s_drain_since_log = 0U;
}
#endif
return true;
}
void OTAHandler::drainFlashBuffer() {
while (drainFlashBufferOne()) {}
}
void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) {
/**
@@ -705,12 +695,26 @@ void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) {
fmc_unlock();
u32 i = 0;
for (; i + 4U <= size; i += 4U) {
(void)fmc_word_program(write_addr + i, *(u32 *)&data[i]);
fmc_state_enum st = fmc_word_program(write_addr + i, *(u32 *)&data[i]);
if (st != FMC_READY) {
#if DEBUG_PRINT
printf("[FW] write err 0x%08lX st=%u\r\n", (unsigned long)(write_addr + i), (unsigned)st);
#endif
fmc_lock();
return;
}
}
if (i < size) {
u32 word = 0xFFFFFFFFU;
memcpy(&word, &data[i], size - i);
(void)fmc_word_program(write_addr + i, word);
fmc_state_enum st = fmc_word_program(write_addr + i, word);
if (st != FMC_READY) {
#if DEBUG_PRINT
printf("[FW] write tail err 0x%08lX st=%u\r\n", (unsigned long)(write_addr + i), (unsigned)st);
#endif
fmc_lock();
return;
}
}
fmc_lock();
}
@@ -786,7 +790,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
u32 receive_packet_num = extract_u32_from_4bytes(data + 4);
u32 receive_IFEND = extract_u32_from_4bytes(data + 56);
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
if (receive_packet_num <= 20U) {
printf("[OTA] DATA in: seq=%u next=%u flash_cnt=%u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet, (unsigned)flash_buf_cnt);
}
@@ -798,7 +802,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
if (receive_packet_num < next_expected_packet) {
u32 request_packet = receive_packet_num + 1U;
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
if (receive_packet_num <= 20U) {
printf("[OTA] DATA dup seq=%u (next=%u) -> ack %u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet, (unsigned)request_packet);
}
@@ -832,9 +836,18 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
return;
}
if (flash_buf_cnt >= kFlashBufSlots) {
g_localCredit = 0U;
send_wait_frame(1, (const u8 *)"\x03");
return;
/* 缓冲满:同步排空已写入的槽位,腾出空间后再入队。
* 旧实现在此直接 return 丢弃当前包,但 ISR 已推进
* s_expectedRxSeq,导致 ESP32 重发被判为 DUP-SEQ,
* 永远只能读到缓存的 WAIT,包永久丢失。 */
/* 只排空一个包腾槽,避免一次 drain 8 包阻塞主循环。
* 剩余包由 process() 的增量 drain 逐步消费。 */
drainFlashBufferOne();
if (flash_buf_cnt >= kFlashBufSlots) {
g_localCredit = 0U;
send_wait_frame(1, (const u8 *)"\x03");
return;
}
}
u8 wi = flash_buf_wi;
flash_buf[wi].offset = offset;
@@ -845,11 +858,15 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
__enable_irq();
/* 更新 CREDIT:剩余可接收槽位。缓冲将满时置 0,
* ESP32 收到 CREDIT=0 后会改为 POLL 等待而非盲目重发。 */
g_localCredit = (flash_buf_cnt >= kFlashBufSlots) ? 0U
: (u8)(kFlashBufSlots - flash_buf_cnt);
/* CREDIT 需同时减去 ISR DATA 队列待处理数,
* 否则 ESP32 看到 credit>0 但 flash_buf 即将满。 */
{ u8 pending = SpiOta::pendingDataCount();
u8 avail = (flash_buf_cnt + pending >= kFlashBufSlots) ? 0U
: (u8)(kFlashBufSlots - flash_buf_cnt - pending);
g_localCredit = avail; }
next_expected_packet = receive_packet_num + 1U;
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
if (receive_packet_num <= 20U) {
printf("[OTA] DATA ok seq=%u -> ack %u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet);
}
+1
View File
@@ -198,6 +198,7 @@ class OTAHandler {
u32 next_expected_packet = 0U; /**< 下一个期望收到的包号,用于去重 */
void drainFlashBuffer();
bool drainFlashBufferOne(); /**< 排空一个 flash_buf 槽位,返回 true 表示有写入 */
/**
* @brief OTA 流程状态定义。
+1 -1
View File
@@ -16,7 +16,7 @@ typedef uint32_t u32;
#define BOOT_FW_VERSION_MAJOR 1U
#define BOOT_FW_VERSION_MINOR 0U
#define BOOT_FW_VERSION_PATCH 1U
#define BOOT_FW_VERSION_BUILD 1U
#define BOOT_FW_VERSION_BUILD 2U
/**
* Flash 布局说明(GD32E508VE,512KB)