diff --git a/GD32E50X_BOOT/device/SpiOta.cpp b/GD32E50X_BOOT/device/SpiOta.cpp index 839b553..6805563 100644 --- a/GD32E50X_BOOT/device/SpiOta.cpp +++ b/GD32E50X_BOOT/device/SpiOta.cpp @@ -6,6 +6,7 @@ #include "gd32e50x_misc.h" #include "gd32e50x_spi.h" #include "gd32e50x_dma.h" +#include "sys/systick.h" #include #include @@ -23,6 +24,9 @@ */ /* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */ + +/* 主动按铃超时时间(毫秒) */ +static constexpr u32 kRingTimeoutMs = 500U; #define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_CH DMA_CH2 @@ -47,28 +51,70 @@ volatile u16 SpiOta::s_txLen = 0U; volatile bool SpiOta::s_txPending = false; volatile bool SpiOta::s_isrResponded = false; volatile bool SpiOta::s_otaSessionActive = false; +volatile bool SpiOta::s_ringPending = false; +volatile u16 SpiOta::s_ringTxLen = 0U; +volatile u32 SpiOta::s_ringDeadline = 0U; /* ============================================================ * CRC8 / 帧构建 / 帧解析 * ============================================================ */ -u8 SpiOta::crc8(const u8 *data, u16 len) { - u8 crc = 0x00U; - for (u16 i = 0; i < len; ++i) { crc ^= data[i]; } +static constexpr u8 kSyncSof0 = 0xA5U; +static constexpr u8 kSyncSof1 = 0x5AU; +static constexpr u16 kSyncHeaderLen = 6U; +static constexpr u16 kSyncCrcLen = 4U; +static constexpr u16 kSyncOverhead = (u16)(kSyncHeaderLen + kSyncCrcLen); + +/* BOOT 侧同步状态机(ISR 内只读/原子写)。 */ +volatile u8 g_localCredit = 8U; /* 非 static:供 OTA.cpp extern 引用 */ +static volatile u8 g_remoteCredit = 8U; + +static void putU16Le(u8 *pp, u16 v) { + pp[0] = (u8)(v & 0xFFU); + pp[1] = (u8)((v >> 8) & 0xFFU); +} +static u16 getU16Le(const u8 *pp) { + return (u16)(pp[0] | ((u16)pp[1] << 8)); +} +static void putU32Le(u8 *pp, u32 v) { + pp[0] = (u8)(v & 0xFFU); + pp[1] = (u8)((v >> 8) & 0xFFU); + pp[2] = (u8)((v >> 16) & 0xFFU); + pp[3] = (u8)((v >> 24) & 0xFFU); +} +static u32 getU32Le(const u8 *pp) { + return (u32)pp[0] | ((u32)pp[1] << 8) | + ((u32)pp[2] << 16) | ((u32)pp[3] << 24); +} + +static u32 spiCrc32(const u8 *data, u16 len) { + u32 crc = 0xFFFFFFFFU; + for (u16 i = 0U; i < len; ++i) { + crc ^= (u32)data[i] << 24; + for (int b = 0; b < 8; ++b) { + if (crc & 0x80000000U) { + crc = (crc << 1) ^ 0x04C11DB7U; + } else { + crc <<= 1; + } + } + } return crc; } bool SpiOta::buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len, u8 *out, u16 *out_len) { if (!out || !out_len) { return false; } - u16 total_len = (u16)(kFrameOverhead + payload_len); + u16 total_len = (u16)(kSyncOverhead + payload_len); if (total_len > kTxBufSize) { return false; } - out[0] = kSof; - out[1] = (u8)type; - out[2] = (u8)(payload_len & 0xFFU); - out[3] = (u8)((payload_len >> 8) & 0xFFU); - if (payload && payload_len > 0U) { memcpy(&out[4], payload, payload_len); } - out[4 + payload_len] = crc8(out, (u16)(4U + payload_len)); + out[0] = kSyncSof0; + out[1] = kSyncSof1; + out[2] = g_localCredit; + out[3] = (u8)type; + putU16Le(&out[4], payload_len); + if (payload && payload_len > 0U) { memcpy(&out[kSyncHeaderLen], payload, payload_len); } + u32 crc = spiCrc32(out, (u16)(kSyncHeaderLen + payload_len)); + putU32Le(&out[kSyncHeaderLen + payload_len], crc); *out_len = total_len; return true; } @@ -77,16 +123,28 @@ bool SpiOta::tryParseFrame(const u8 *frame, u16 frame_len, spi_ota_frame_type_t *type, const u8 **payload, u16 *payload_len) { - if (!frame || frame_len < kFrameOverhead) { return false; } - if (frame[0] != kSof) { return false; } - u16 len = (u16)(frame[2] | ((u16)frame[3] << 8)); - u16 expected = (u16)(kFrameOverhead + len); - if (expected != frame_len) { return false; } - if (crc8(frame, (u16)(4U + len)) != frame[4U + len]) { return false; } - if (type) { *type = (spi_ota_frame_type_t)frame[1]; } - if (payload) { *payload = &frame[4]; } - if (payload_len){ *payload_len = len; } - return true; + if (!frame || frame_len < kSyncOverhead) { + return false; + } + + /* 同步帧:允许首字节偏移,扫描 SOF0 SOF1。 */ + for (u16 s = 0U; s + kSyncOverhead <= frame_len; ++s) { + if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; } + u16 plen = getU16Le(&frame[s + 4U]); + u16 total = (u16)(kSyncOverhead + plen); + if ((u16)(frame_len - s) < total) { continue; } + u32 expect = getU32Le(&frame[s + kSyncHeaderLen + plen]); + u32 calc = spiCrc32(&frame[s], (u16)(kSyncHeaderLen + plen)); + if (expect != calc) { continue; } + + u8 credit = frame[s + 2U]; + if (type) { *type = (spi_ota_frame_type_t)frame[s + 3U]; } + if (payload) { *payload = (plen > 0U) ? &frame[s + kSyncHeaderLen] : nullptr; } + if (payload_len){ *payload_len = plen; } + g_remoteCredit = credit; + return true; + } + return false; } ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) { @@ -139,30 +197,18 @@ void SpiOta::queueTxFrame(const u8 *frame, u16 len) { s_txStreamPos = 0U; if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); } exitCritical(primask); - spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiTxDma(s_txBuf, s_txLen); -} - -/* ============================================================ - * RBNE 中断内即时帧检测与处理 - * ============================================================ */ - -void SpiOta::tryProcessCompleteFrame() { - u16 rx_len = s_rxStreamLen; - if (rx_len == 0U) { return; } - - /* 单字节 BOOT 命令留到事务结束后再判定。 */ - if (s_rxStream[0] == SPI_OTA_CMD_BOOT) { return; } - - if (s_rxStream[0] != kSof) { return; } - if (rx_len < kFrameOverhead) { return; } - - u16 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8)); - u16 expected = (u16)(kFrameOverhead + payload_len); - if (expected > kRxBufSize) { return; } - if (rx_len < expected) { return; } - - isrProcessFrame(); + /* ★ 不在此处启动 DMA。 + * + * 本函数可从 RBNE ISR 中调用(isrProcessFrame → handleLinkMessage + * → send_structured_ack → sender → queueTxFrame),此时 CS 仍为 LOW, + * DMA 正在发送上一次 onCsAssert() 装载的旧响应。 + * + * 若在此处 startSpiTxDma(),GD32 DMA 控制器会先 CHEN=0 再 CHEN=1, + * 导致 DMA 计数器清零并从 s_txBuf[0] 重新开始,MISO 输出被破坏 + * (旧响应尾部 + 新响应头部拼接),ESP32 的 otaFindFrame() 无法解析。 + * + * 正确做法:只缓冲响应到 s_txBuf,由下一次 onCsAssert() 检测 + * s_txPending==true 后统一装载 DMA。这遵循"延迟一拍"协议模型。 */ } /* ============================================================ @@ -177,12 +223,11 @@ void SpiOta::isrProcessFrame() { s_isrResponded = true; if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { - if (s_otaHandler != nullptr && s_otaHandler->isActive()) { - s_rxStreamLen = 0U; - return; - } - u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U}; - queueTxFrame(ack, sizeof(ack)); + /* 0xB0 始终回复 0xB5 并重置 OTA 状态机。 + * 旧代码在 isActive() 时静默忽略,导致上次 OTA 失败后 + * 所有后续 0xB0 都被吞掉,ESP32 永远收不到 0xB5。 */ + u8 ack = 0xB5U; + queueTxFrame(&ack, 1U); if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } s_otaSessionActive = false; s_rxStreamLen = 0U; @@ -190,23 +235,29 @@ void SpiOta::isrProcessFrame() { return; } - if (s_rxStream[0] != kSof) { + /* 新同步帧校验。 */ + if (rx_len < kSyncOverhead || + s_rxStream[0] != kSyncSof0 || s_rxStream[1] != kSyncSof1) { return; } - if (rx_len < kFrameOverhead) { return; } - u16 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8)); - u16 expected = (u16)(kFrameOverhead + payload_len); + u16 payload_len = getU16Le(&s_rxStream[4U]); + u16 expected = (u16)(kSyncOverhead + payload_len); if (expected > rx_len) { return; } - if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) { + /* 帧收齐日志:确认 BOOT 收到了完整帧 */ + isrLogFrameEx("[RX-FRM]", s_rxStream, (expected < 18U) ? expected : 18U); + + u32 expect = getU32Le(&s_rxStream[kSyncHeaderLen + payload_len]); + u32 calc = spiCrc32(s_rxStream, (u16)(kSyncHeaderLen + payload_len)); + if (expect != calc) { u8 err = 0xEEU; u8 err_frame[kTxBufSize] = {}; u16 built = 0U; if (buildFrame(SPI_OTA_RSP_ERROR, &err, 1U, err_frame, &built)) { queueTxFrame(err_frame, built); } - isrLogFrameEx("[CRC-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + isrLogFrameEx("[CRC-ERR]", s_rxStream, (rx_len < 18U) ? rx_len : 18U); return; } @@ -214,22 +265,26 @@ void SpiOta::isrProcessFrame() { const u8 *payload = nullptr; u16 p_len = 0U; if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) { +#if DEBUG_PRINT + static volatile u8 s_parseFailCnt = 0U; + if (s_parseFailCnt < 10U) { + s_parseFailCnt++; + printf("[SPI] parse fail rx=%u expected=%u sof=0x%02X type=0x%02X plen=%u\r\n", + rx_len, expected, s_rxStream[0], s_rxStream[1], + (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8))); + } +#endif isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); return; } if (frame_type == SPI_OTA_CMD_BOOT) { - if (s_otaHandler != nullptr && s_otaHandler->isActive()) { - s_rxStreamLen = 0U; - s_isrResponded = true; - return; - } - u8 ack_ok = 0xA5U; - u8 ack_frame[kTxBufSize] = {}; - u16 ack_len = 0U; - if (buildFrame(SPI_OTA_RSP_ACK, &ack_ok, 1U, ack_frame, &ack_len)) { - queueTxFrame(ack_frame, ack_len); - } + /* 帧式 0xB0 与裸字节 0xB0 行为一致:始终回裸字节 0xB5 + resetToIdle()。 + * 旧代码在 isActive() 时静默吞帧且回 0xA5 帧式 ACK,违反 + * "0xB0 始终回 0xB5" 约束,且与裸字节路径不一致(0xA5 vs 0xB5)。 + * ESP32 appRequestBootloader() 只读首字节期望 0xB5,帧式 ACK 永不匹配。 */ + u8 ack = 0xB5U; + queueTxFrame(&ack, 1U); if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } s_otaSessionActive = false; s_rxStreamLen = 0U; @@ -308,14 +363,21 @@ void SpiOta::configureGpio() { gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_7); gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); - /* PB6 复用为 SPI Ready 握手输出,默认 HIGH(就绪),允许主机立刻发起首帧。 - * 与 DCU_APP app_spi_command.cpp configureGpio 行为一致, - * 适配 ESP32 GPIO10 输入监测 PB6 电平的硬件握手时序。 */ - gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); + /* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。 + * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。 + * 默认释放(HIGH),允许主机立刻发起首帧。 + * 与 DCU_APP app_spi_command.cpp configureGpio 行为一致。 + * + * 方案A: 此处 PB6 从 main() 极早的推挽 LOW 切换为开漏 HIGH, + * 产生 LOW→HIGH 上升沿 = "Bootloader 就绪"信号, + * ESP32 waitForBootloaderReady() 据此唤醒并发起握手。 */ + gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); } void SpiOta::setSpiReady(bool ready) { + /* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高); + * ready=false 下拉(BRR,驱动低)。 */ if (ready) { gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); } else { @@ -323,6 +385,46 @@ void SpiOta::setSpiReady(bool ready) { } } +/* ============================================================ + * BOOT 主动按铃:下拉 SPIready 请求 ESP32 来读 + * ============================================================ */ + +bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len) { + if (s_ringPending) { + 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)) { + return false; + } + + uint32_t primask = enterCritical(); + queueTxFrame(frame_buf, built_len); + s_ringTxLen = built_len; + s_ringPending = true; + s_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + setSpiReady(false); + exitCritical(primask); + return true; +} + +void SpiOta::cancelRing() { + if (!s_ringPending) { + return; + } + uint32_t primask = enterCritical(); + s_ringPending = false; + s_ringTxLen = 0U; + s_txPending = false; + s_txLen = 0U; + s_txStreamPos = 0U; + memset(s_txBuf, 0xFF, sizeof(s_txBuf)); + setSpiReady(true); + exitCritical(primask); +} + void SpiOta::configureSpi0() { rcu_periph_clock_enable(RCU_SPI0); spi_i2s_deinit(SPI0); @@ -477,15 +579,33 @@ void SpiOta::onCsRelease() { } + + /* 如果这是 ESP32 响应 BOOT 主动按铃来读请求帧,直接清空按铃状态并释放 ready。 + * 按铃帧已在 CS 期间通过 DMA 输出到 MISO,不需要再解析本次接收到的 dummy 字节。 */ + if (s_ringPending) { + s_ringPending = false; + s_ringTxLen = 0U; + s_txPending = false; + s_txLen = 0U; + s_txStreamPos = 0U; + s_rxStreamLen = 0U; + s_isrResponded = true; + memset(s_txBuf, 0xFF, sizeof(s_txBuf)); + setSpiReady(true); + return; + } + if (s_rxStreamLen > 0U && !s_isrResponded) { isrProcessFrame(); } /* BOOT 在 ISR 内同步完成帧处理,响应帧已就绪(或为空轮询/残包无需响应), * 拉高 PB6 通知主机可发起下一笔事务。 - * 与 DCU_APP processCommand() 末尾 setSpiReady(true) 行为一致; - * 空轮询事务同样恢复 HIGH,避免 PB6 卡 LOW 导致 ESP32 waitSpiReady 超时。 */ - setSpiReady(true); + * flash_buf 满(g_localCredit==0)时不拉高,等 drainFlashBuffer 写完 Flash + * 再拉高,形成硬件级背压,避免 ESP32 疯狂重发导致 overrun。 */ + if (g_localCredit > 0U) { + setSpiReady(true); + } } /* ============================================================ @@ -503,12 +623,15 @@ void SpiOta::onSpiInterrupt() { if (s_rxStreamLen < kRxBufSize) { s_rxStream[s_rxStreamLen++] = byte; } - tryProcessCompleteFrame(); + /* 帧处理统一在 onCsRelease() (EXTI CS 释放中断) 里执行, + * 不在 RBNE 中断内调 isrProcessFrame(),避免 CRC32+AES+buildFrame + * 重操作(10-20us)期间 SPI 外设继续收字节导致 RXORERR。 */ } /* TBE 已由 DMA 替代,不再处理 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); s_rxStreamLen = 0U; + isrLogFrameEx("[RX-OVR]", NULL, 0U); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); @@ -537,7 +660,18 @@ void SpiOta::poll() { s_logReadIdx = (s_logReadIdx + 1U) % kLogSlotCount; s_logCount--; __enable_irq(); +#if DEBUG_PRINT + printf("[SPI] %s\r\n", (const char*)s_logSlots[s_logReadIdx]); +#endif } + /* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃, + * 防止永久占用 SPIready 线导致双方死锁。 */ + if (s_ringPending) { + if ((int32_t)(SysTickTimer::getMs() - s_ringDeadline) >= 0) { + cancelRing(); + } + } + if (s_otaHandler != nullptr) { s_otaHandler->process(); } } diff --git a/GD32E50X_BOOT/device/SpiOta.h b/GD32E50X_BOOT/device/SpiOta.h index fff793f..5aee9b9 100644 --- a/GD32E50X_BOOT/device/SpiOta.h +++ b/GD32E50X_BOOT/device/SpiOta.h @@ -24,10 +24,8 @@ typedef enum { class SpiOta { public: - static constexpr u8 kSof = 0xA5U; static constexpr u16 kRxBufSize = 256U; static constexpr u16 kTxBufSize = 128U; - static constexpr u16 kFrameOverhead = 5U; static constexpr u16 kIsrLogBufSize = 128U; static void init(); @@ -46,6 +44,18 @@ public: static bool hasActiveSession(); static u16 copyTxFrame(u8 *out, u16 max_len); + /** @brief BOOT 主动按铃:构建请求帧并拉低 SPIready,通知 ESP32 来读。 + * @param[in] frame_type 请求帧类型。 + * @param[in] payload 请求帧负载。 + * @param[in] payload_len 负载长度。 + * @return true 按铃成功,false 当前已有按铃未处理或帧构建失败。 + * @note 与 DCU_APP AppSpiCommand::requestEsp32Read 对齐。 + */ + static bool requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len); + + /** @brief 取消当前按铃请求,释放 SPIready。 */ + static void cancelRing(); + /** @brief 翻转 SPI Ready 握手引脚(PB6)电平。 * @param[in] ready true=就绪(HIGH), false=忙(LOW)。 * @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。 @@ -53,7 +63,6 @@ public: static void setSpiReady(bool ready); static void isrProcessFrame(); - static void tryProcessCompleteFrame(); static void isrLogFrame(const char *tag, const u8 *data, u16 len); static OTAHandler *s_otaHandler; @@ -66,11 +75,13 @@ public: static volatile bool s_txPending; static volatile bool s_isrResponded; static volatile bool s_otaSessionActive; + static volatile bool s_ringPending; ///< 是否有未读走的主动按铃请求 + static volatile u16 s_ringTxLen; ///< 按铃帧长度 + static volatile u32 s_ringDeadline; ///< 按铃超时截止时间(ms) static void sender(ota_link_tx_type_t type, u8 len, const u8 *data); private: - static u8 crc8(const u8 *data, u16 len); static bool buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len, u8 *out, u16 *out_len); static bool tryParseFrame(const u8 *frame, u16 frame_len, @@ -83,6 +94,9 @@ private: static void exitCritical(uint32_t primask); }; +/* OTA.cpp 通过此变量更新 SPI 响应帧的 CREDIT 字段 */ +extern volatile u8 g_localCredit; + #ifdef __cplusplus extern "C" { #endif diff --git a/GD32E50X_BOOT/main.cpp b/GD32E50X_BOOT/main.cpp index 5288482..3c63080 100644 --- a/GD32E50X_BOOT/main.cpp +++ b/GD32E50X_BOOT/main.cpp @@ -1,4 +1,4 @@ -#include "main.h" +#include "main.h" #include "device/Can.h" #include "device/SpiOta.h" #include "sys/config.h" @@ -333,6 +333,15 @@ void BootloaderSystem::run() { } int main(void) { + /* 方案A: 极早夺回 PB6 控制权,拉低 SPIready 线。 + * 复位后 GPIO 高阻,外部上拉会短暂拉高 PB6 (μs级); + * 此处尽快配为推挽输出 LOW,让 ESP32 能通过 LOW→HIGH 序列 + * 识别 "Bootloader 已接管" → "Bootloader 就绪"。 + * 后续 SpiOta::configureGpio() 会重配为开漏并释放 HIGH。 */ + rcu_periph_clock_enable(RCU_GPIOB); + gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6); + gpio_bit_reset(GPIOB, GPIO_PIN_6); + sys.initHardware(); sys.run(); diff --git a/GD32E50X_BOOT/sys/OTA.cpp b/GD32E50X_BOOT/sys/OTA.cpp index 9c920ad..9b90442 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -1,4 +1,4 @@ -#include "OTA.h" +#include "OTA.h" #include "Can.h" #include "SpiOta.h" #include "aes128.h" @@ -242,6 +242,11 @@ 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) { + printf("[OTA] send_structured_ack seq=%u\r\n", (unsigned)seq_or_flag); + } +#endif write_u32_to_4bytes(&send_data[2], seq_or_flag); send_ack_frame(6, send_data); } @@ -317,6 +322,39 @@ void OTAHandler::process() { pending_finish_ack = false; } + /** FINISH_OTA 已在 ISR 里回 ACK,等 Flash 缓冲 drain 完再做校验/CRC/写标志/跳转, + * 避免在 CS 释放 ISR 里长时间阻塞 SPIready。 */ + if (pending_finish_ota && flash_buf_cnt == 0U) { + pending_finish_ota = false; + + if (!validateApplication()) { + ota_error = OTA_ERROR_FLASH_ERROR; + ota_state = OTA_STATE_IDLE; + send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR); + } else { + /** 计算并写入 A区固件 CRC32,供后续启动时完整性校验。 */ + u32 latest_crc = calculateAppCrc(APPLATEST_START_ADDR, APP_FIRMWARE_SIZE / 4U); + writeAppCrc(APPLATEST_START_ADDR, latest_crc); + + OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_AVAL); + OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL); + OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL); + +#if DEBUG_PRINT + /* OTA 完成后立即 dump Reset_Handler 地址 */ + { + uint32_t rh_addr = *(__IO uint32_t *)(APPLATEST_START_ADDR + 4U) & ~1U; + printf("[OTA-DONE2] RH@0x%08lX: ", (unsigned long)rh_addr); + const uint8_t *p = (const uint8_t *)rh_addr; + for (int i = 0; i < 16; i++) printf("%02X ", p[i]); + printf("\r\n"); + } +#endif + + pending_app_jump = true; + } + } + if (pending_app_jump) { pending_app_jump = false; delay_1ms(20); @@ -580,6 +618,11 @@ void OTAHandler::drainFlashBuffer() { #if DEBUG_PRINT u32 drain_cnt = 0; #endif + if (flash_buf_cnt == 0U) { return; } + /* Flash 写入期间拉低 PB6,阻止 ESP32 发起新 SPI 事务。 + * fmc_word_program 会让 CPU 总线停顿,SPI RBNE ISR 来不及响应 → RX-OVR。 + * 写完 drain 后恢复 HIGH,ESP32 的 PB6 等待循环会自动放行。 */ + SpiOta::setSpiReady(false); while (1) { FlashSlot slot = {}; bool has_slot = false; @@ -596,6 +639,8 @@ void OTAHandler::drainFlashBuffer() { has_slot = true; } __enable_irq(); + /* 缓冲被消费,恢复 CREDIT 通知 ESP32 可继续发数据。 */ + g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt); if (!has_slot) { break; @@ -606,6 +651,10 @@ void OTAHandler::drainFlashBuffer() { drain_cnt++; #endif } + /* Flash 写入完成,恢复 CREDIT 并释放 PB6。 + * ESP32 检测到 PB6 拉高后会继续发数据包。 */ + g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt); + SpiOta::setSpiReady(true); #if DEBUG_PRINT if (drain_cnt > 0U) { printf("[FW] drain %u pkts\r\n", (unsigned)drain_cnt); @@ -694,15 +743,32 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { */ if (len == 64U && ota_state == OTA_STATE_RECEIVING_FIRMWARE) { u32 receive_session_num = extract_u32_from_4bytes(data); - (void)receive_session_num; + u32 expected_session_num = extract_u32_from_4bytes(nonce_server); + if (receive_session_num != expected_session_num) { + ota_error = OTA_ERROR_INVALID_PACKET; + send_error_frame(extract_u32_from_4bytes(data + 4), OTA_ERROR_INVALID_PACKET); + return; + } 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) { + 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); + } +#endif const u8 *receive_encrypted_data = data + 8; const u8 *receive_dataCRC32 = data + 60; /** 去重:跳过已经确认接收过的包号(ESP32 超时重传)。 */ if (receive_packet_num < next_expected_packet) { u32 request_packet = receive_packet_num + 1U; +#if DEBUG_PRINT + if (receive_packet_num <= 5U) { + printf("[OTA] DATA dup seq=%u (next=%u) -> ack %u\r\n", + (unsigned)receive_packet_num, (unsigned)next_expected_packet, (unsigned)request_packet); + } +#endif send_structured_ack(request_packet); return; } @@ -717,6 +783,9 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { /** 校验分包 CRC32(密钥参与 CRC,防止简单重放/篡改)。 */ calculate_crc32_with_key(current_K_session, decrypted_data, calculated_dataCRC32); if (memcmp(receive_dataCRC32, calculated_dataCRC32, SIZE_U32) != 0) { +#if DEBUG_PRINT + printf("[OTA] DATA crc err seq=%u\r\n", (unsigned)receive_packet_num); +#endif ota_error = OTA_ERROR_INVALID_CRC; send_error_frame(receive_packet_num, OTA_ERROR_INVALID_CRC); return; @@ -729,6 +798,7 @@ 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; } @@ -739,7 +809,17 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { __disable_irq(); flash_buf_cnt++; __enable_irq(); + /* 更新 CREDIT:剩余可接收槽位。缓冲将满时置 0, + * ESP32 收到 CREDIT=0 后会改为 POLL 等待而非盲目重发。 */ + g_localCredit = (flash_buf_cnt >= kFlashBufSlots) ? 0U + : (u8)(kFlashBufSlots - flash_buf_cnt); next_expected_packet = receive_packet_num + 1U; +#if DEBUG_PRINT + if (receive_packet_num <= 5U) { + printf("[OTA] DATA ok seq=%u -> ack %u\r\n", + (unsigned)receive_packet_num, (unsigned)next_expected_packet); + } +#endif } /** @@ -788,35 +868,13 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { /** 结束态下要求显式收到 0x0A AC,确认可以切换到 APP。 */ else if (len == 2U && ota_state == OTA_STATE_FINISHED) { if (data[0] == 0x0AU && data[1] == 0xACU) { - if (!validateApplication()) { - ota_error = OTA_ERROR_FLASH_ERROR; - ota_state = OTA_STATE_IDLE; - send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR); - return; - } - - /** 计算并写入 A区固件 CRC32,供后续启动时完整性校验。 */ - u32 latest_crc = calculateAppCrc(APPLATEST_START_ADDR, APP_FIRMWARE_SIZE / 4U); - writeAppCrc(APPLATEST_START_ADDR, latest_crc); - - OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_AVAL); - OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL); - OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL); - -#if DEBUG_PRINT - /* OTA 完成后立即 dump Reset_Handler 地址 */ - { - uint32_t rh_addr = *(__IO uint32_t *)(APPLATEST_START_ADDR + 4U) & ~1U; - printf("[OTA-DONE2] RH@0x%08lX: ", (unsigned long)rh_addr); - const uint8_t *p = (const uint8_t *)rh_addr; - for (int i = 0; i < 16; i++) printf("%02X ", p[i]); - printf("\r\n"); - } -#endif - u8 finish_ack[2] = {0x0A, 0xAC}; send_ack_frame(2, finish_ack); - pending_app_jump = true; + /** 重活放到主循环 process() 里做,ISR 只回 ACK 并置标志, + * 避免 SPIready 被拉低太久导致 ESP32 超时。 */ + if (!pending_finish_ota) { + pending_finish_ota = true; + } return; } @@ -836,8 +894,8 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { else { ota_error = OTA_ERROR_INVALID_PACKET; #if DEBUG_PRINT - printf("[OTA] INV_PKT: l=%u s=%u\r\n", - (unsigned)len, (unsigned)ota_state); + printf("[OTA] DATA ignored/INV_PKT: type=%u len=%u state=%u\r\n", + (unsigned)type, (unsigned)len, (unsigned)ota_state); #endif ota_state = OTA_STATE_IDLE; send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET); diff --git a/GD32E50X_BOOT/sys/initial.cpp b/GD32E50X_BOOT/sys/initial.cpp index 4bc0a70..34e1136 100644 --- a/GD32E50X_BOOT/sys/initial.cpp +++ b/GD32E50X_BOOT/sys/initial.cpp @@ -328,9 +328,13 @@ extern "C" void EXTI5_9_IRQHandler(void) /** * @brief External interrupt handler for EXTI line group 10 to 15. - * @return none + * + * PA15 (SPI CS from ESP32) maps to EXTI_15, which fires this handler. + * Must directly dispatch to the SPI OTA CS handler — NOT through + * BootSleepManager::onExtiInterrupt(), which only handles EXTI_8 + * (PB8 deep-sleep wake) and knows nothing about EXTI_15. */ extern "C" void EXTI10_15_IRQHandler(void) { - BootSleepManager::onExtiInterrupt(); + SpiOta::onCsExtiInterrupt(); } diff --git a/GD32E50X_BOOT/sys/systick.cpp b/GD32E50X_BOOT/sys/systick.cpp index 8b419cb..4c24962 100644 --- a/GD32E50X_BOOT/sys/systick.cpp +++ b/GD32E50X_BOOT/sys/systick.cpp @@ -11,6 +11,7 @@ */ volatile uint32_t SysTickTimer::s_delayCountdown = 0U; +volatile uint32_t SysTickTimer::s_msTicks = 0U; /** * @brief 配置 SysTick 为 1ms 周期中断。 @@ -87,6 +88,7 @@ void SysTickTimer::decrement() if (s_delayCountdown != 0U) { s_delayCountdown--; } + s_msTicks++; } /** @@ -115,6 +117,16 @@ void SysTickTimer::softReset() } } +/** + * @brief 获取上电以来的毫秒数。 + * + * @return 毫秒计数。 + */ +uint32_t SysTickTimer::getMs() +{ + return s_msTicks; +} + /** * @brief Configure SysTick via the timer helper. * @return none diff --git a/GD32E50X_BOOT/sys/systick.h b/GD32E50X_BOOT/sys/systick.h index 3fdf6b9..c399c2d 100644 --- a/GD32E50X_BOOT/sys/systick.h +++ b/GD32E50X_BOOT/sys/systick.h @@ -1,4 +1,4 @@ -#ifndef SYSTICK_H +#ifndef SYSTICK_H #define SYSTICK_H #ifdef __cplusplus @@ -28,9 +28,13 @@ public: static void onInterrupt(); /** @brief 触发软件复位。 */ static void softReset(); + /** @brief 获取上电以来的毫秒数。 + * @note 供按铃超时等非阻塞超时场景使用,不用于精确阻塞延时。 */ + static uint32_t getMs(); private: static volatile uint32_t s_delayCountdown; + static volatile uint32_t s_msTicks; }; /**