diff --git a/GD32E50X_BOOT/device/SpiOta.cpp b/GD32E50X_BOOT/device/SpiOta.cpp index fc72c3f..e1dcfe5 100644 --- a/GD32E50X_BOOT/device/SpiOta.cpp +++ b/GD32E50X_BOOT/device/SpiOta.cpp @@ -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 风格兼容包装 * ============================================================ */ diff --git a/GD32E50X_BOOT/device/SpiOta.h b/GD32E50X_BOOT/device/SpiOta.h index 28b9133..b2345a5 100644 --- a/GD32E50X_BOOT/device/SpiOta.h +++ b/GD32E50X_BOOT/device/SpiOta.h @@ -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(); diff --git a/GD32E50X_BOOT/sys/OTA.cpp b/GD32E50X_BOOT/sys/OTA.cpp index 6fcefef..77c4551 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -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); } diff --git a/GD32E50X_BOOT/sys/OTA.h b/GD32E50X_BOOT/sys/OTA.h index 673f85b..659f88b 100644 --- a/GD32E50X_BOOT/sys/OTA.h +++ b/GD32E50X_BOOT/sys/OTA.h @@ -198,6 +198,7 @@ class OTAHandler { u32 next_expected_packet = 0U; /**< 下一个期望收到的包号,用于去重 */ void drainFlashBuffer(); + bool drainFlashBufferOne(); /**< 排空一个 flash_buf 槽位,返回 true 表示有写入 */ /** * @brief OTA 流程状态定义。 diff --git a/GD32E50X_BOOT/sys/config.h b/GD32E50X_BOOT/sys/config.h index 5c60f31..5f7dc35 100644 --- a/GD32E50X_BOOT/sys/config.h +++ b/GD32E50X_BOOT/sys/config.h @@ -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)