From ea7a885de412332f4c8d9fbfc431c438f6d73647 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sat, 30 May 2026 00:35:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(spi-ota):=20SPI=E5=88=B7=E5=86=99=E5=88=9D?= =?UTF-8?q?=E7=89=88=E5=B7=B2=E5=8F=AF=E5=AE=8C=E6=95=B4=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E5=88=B7=E5=86=99=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在SPI OTA ISR处理函数入口立即设置已响应标志,防止帧被重复处理,避免状态机错误推进 - 修正DMA启动前预填SPI发送缓冲区,覆盖旧字节,解决MISO线输出旧ACK字节导致序号错位问题 - 改为SPI从机内部NSS固定低电平,防止因EXTI延迟丢失首字节,提升帧接收稳定性 - 优化CS下降沿处理流程,确保DMA停止后重新配置并启动,避免数据丢失和标志位误触发 - CS上升沿停止DMA但保持内部NSS低电平,提高下一次事务的接收连续性 - SPI中断处理加入对上次事务提前检测和补充分支,增强系统鲁棒性 - 降低调试打印频率,减少ISR日志开销 - 修复看门狗复位检测代码,阻止BOOT跳转至卡死的APP,进入等待OTA修复模式 - 在工程配置文件中添加bin格式输出支持 --- GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml | 1 + GD32E50X_BOOT/device/SpiOta.cpp | 71 +- GD32E50X_BOOT/device/SpiOta.cpp.bak_20260528 | 608 +++++++++++++++++ .../device/SpiOta.cpp.bak_20260529_txdebug | 626 ++++++++++++++++++ GD32E50X_BOOT/device/SpiOta.h | 3 +- GD32E50X_BOOT/main.cpp | 23 + GD32E50X_BOOT/sys/OTA.cpp | 85 ++- GD32E50X_BOOT/sys/OTA.h | 13 + 8 files changed, 1406 insertions(+), 24 deletions(-) create mode 100644 GD32E50X_BOOT/device/SpiOta.cpp.bak_20260528 create mode 100644 GD32E50X_BOOT/device/SpiOta.cpp.bak_20260529_txdebug diff --git a/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml b/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml index 075bf93..d68b175 100644 --- a/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml +++ b/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml @@ -28,6 +28,7 @@ project: type: - elf - hex + - bin - map components: diff --git a/GD32E50X_BOOT/device/SpiOta.cpp b/GD32E50X_BOOT/device/SpiOta.cpp index a0b6753..bca63b2 100644 --- a/GD32E50X_BOOT/device/SpiOta.cpp +++ b/GD32E50X_BOOT/device/SpiOta.cpp @@ -179,17 +179,30 @@ void SpiOta::isrProcessFrame() { u16 rx_len = s_rxStreamLen; if (rx_len == 0U) { return; } + /* 关键修复:一旦开始处理本次 CS 周期中的完整帧,立刻标记“已响应/已处理”。 + * + * 否则会出现这样的竞态: + * 1) SPI0 RBNE IRQ 在收到最后一个字节后调用 isrProcessFrame() + * 2) 还没执行到函数尾部的 s_isrResponded = true + * 3) CS 已经上升,优先级更高的 EXTI IRQ 抢占进来执行 onCsRelease() + * 4) onCsRelease() 看到 s_isrResponded 仍为 false,于是再次调用 isrProcessFrame() + * 5) 同一帧被重复处理一次,状态机被错误推进(如 challenge 又被当 confirm) + * + * 所以必须在函数入口就先置位,防止 REQ/DATA 被双重分发。 */ + s_isrResponded = true; + if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U}; queueTxFrame(ack, sizeof(ack)); if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } + s_otaSessionActive = false; + s_rxStreamLen = 0U; isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen); s_isrResponded = true; return; } if (s_rxStream[0] != kSof) { - isrLogFrameEx("[NO-SOF]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); return; } if (rx_len < kFrameOverhead) { return; } @@ -225,7 +238,8 @@ void SpiOta::isrProcessFrame() { queueTxFrame(ack_frame, ack_len); } if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } - isrLogFrameEx("[BOOT-FRAME]", s_txBuf, s_txLen); + s_otaSessionActive = false; + s_rxStreamLen = 0U; s_isrResponded = true; return; } @@ -234,11 +248,22 @@ void SpiOta::isrProcessFrame() { s_otaSessionActive = true; s_otaHandler->setTransportSender(sender); ota_link_msg_type_t msg_type = mapMsgType(frame_type); - isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1); + /* 精简诊断: 只记录帧类型+状态, 减少ISR日志开销 */ + { + u8 diag[3] = {(u8)frame_type, (u8)(s_otaHandler ? s_otaHandler->getState() : 0xFFU), + (u8)p_len}; + isrLogFrameEx("[OT]", diag, 3); + } s_otaHandler->handleLinkMessage(msg_type, const_cast(payload), (u8)p_len); } - isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U); + /* 精简: 只在非DATA包时记录响应帧内容, DATA包只记录ACK的seq */ + if (frame_type != SPI_OTA_CMD_DATA) { + isrLogFrameEx("[RD]", s_txBuf, (s_txLen < 12U) ? s_txLen : 12U); + } else { + /* DATA包响应: 只记录payload前6字节(=BAC0+seq) */ + isrLogFrameEx("[RD]", s_txBuf + 4, (s_txLen > 10U) ? 6U : (s_txLen > 4U ? s_txLen - 4U : 0U)); + } s_isrResponded = true; } @@ -367,6 +392,7 @@ void SpiOta::init() { s_txLen = 0U; s_txPending = false; s_isrLogPending = false; + s_otaSessionActive = false; memset(s_rxStream, 0, sizeof(s_rxStream)); memset(s_txBuf, 0xFF, sizeof(s_txBuf)); configureGpio(); @@ -413,6 +439,30 @@ void SpiOta::onCsAssert() { DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + /* 1.5 覆盖 SPI 发送缓冲区 (TBE) 中的旧字节 + * + * 关键修复:CS 高期间,queueTxFrame() 会把新 ACK 帧写入 s_txBuf + * 并启动 DMA,DMA 把 s_txBuf[0] 写入 SPI_DATA (TBE 缓冲区)。 + * CS 高时没有外部 SCK,TBE 中的字节停留在发送缓冲区不会被移位输出。 + * + * 当 ESP32 下次 CS↓ 时,onCsAssert() 重启 DMA,但 TBE 中已经有 + * 旧字节。如果 ESP32 第一个 SCK 边沿在 DMA 写入新数据之前到达, + * MISO 上先输出 TBE 中的旧字节,然后 DMA 才开始推 s_txBuf[0]。 + * 这导致 MISO 上第一个字节是旧 ACK 帧的残留,ESP32 的 + * spi_ota_find_frame 先匹配到旧帧,拿到比期望少 1 的 seq 号 + * ("差一拍" 现象)。 + * + * 修复:在重启 DMA 前,写 0xFF 到 SPI_DATA 覆盖 TBE 中的旧字节。 + * 这样即使 ESP32 第一个 SCK 先到,MISO 输出的是 0xFF 而非旧 ACK 残留。 + * spi_ota_find_frame 会跳过 0xFF(不是 SOF=0xA5),从后面 DMA 推出的 + * 正确帧开始匹配。 + */ + spi_i2s_data_transmit(SPI0, 0xFFU); + /* 如果 TBE FIFO 有两层,再写一个 0xFF 确保覆盖 */ + if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) { + spi_i2s_data_transmit(SPI0, 0xFFU); + } + /* 2. 清残留错误标志 * * 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。 @@ -470,8 +520,9 @@ void SpiOta::onCsRelease() { * 这样能直接判断: * 1) 是不是完整收到了 ESP32 发来的整帧 * 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */ + /* 精简: CS-RX只记录前4字节, 减少ISR日志开销 */ if (s_rxStreamLen > 0U) { - isrLogFrame("[CS-RX]", s_rxStream, (s_rxStreamLen < 12U) ? s_rxStreamLen : 12U); + isrLogFrame("[CR]", s_rxStream, (s_rxStreamLen < 4U) ? s_rxStreamLen : 4U); } if (s_rxStreamLen > 0U && !s_isrResponded) { @@ -511,7 +562,6 @@ void SpiOta::onSpiInterrupt() { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); s_rxStreamLen = 0U; - isrLogFrame("[RXOR]", (const u8*)"OVR", 3); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); @@ -547,14 +597,13 @@ void SpiOta::poll() { } if (s_otaHandler != nullptr) { s_otaHandler->process(); } - /* 调试:定期打印 SPI 从机状态 */ + /* 调试:定期打印 SPI 从机状态 — 降低频率, 每20000次才打印一次 */ static u32 dbg_tick = 0; dbg_tick++; - if ((dbg_tick % 5000U) == 0U) { - printf("[SPI] csActive=%u rxLen=%u txPend=%u txLen=%u isrResp=%u otaState=%u\r\n", + if ((dbg_tick % 20000U) == 0U) { + printf("[SPI] st=%u rx=%u tx=%u ota=%u\r\n", (unsigned)s_csActive, (unsigned)s_rxStreamLen, - (unsigned)s_txPending, (unsigned)s_txLen, - (unsigned)s_isrResponded, + (unsigned)s_txLen, (unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF)); } } diff --git a/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260528 b/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260528 new file mode 100644 index 0000000..a0b6753 --- /dev/null +++ b/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260528 @@ -0,0 +1,608 @@ +#include "SpiOta.h" +#include "core_cm33.h" +#include "gd32e50x_gpio.h" +#include "gd32e50x_exti.h" +#include "gd32e50x_rcu.h" +#include "gd32e50x_misc.h" +#include "gd32e50x_spi.h" +#include "gd32e50x_dma.h" +#include +#include + +/** + * @file SpiOta.cpp + * @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。 + * + * CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备, + * 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。 + * 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。 + * + * GD32E50x DMA 通道映射(固定): + * DMA0_CH0: ADC0, DMA0_CH1: SPI0_RX, DMA0_CH2: SPI0_TX + * DMA0_CH3: SPI1_RX, DMA0_CH4: SPI1_TX + */ + +/* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */ +#define SPI_TX_DMA_PERIPH DMA0 +#define SPI_TX_DMA_CH DMA_CH2 + +/* 调试:多缓冲日志(8 条 ISR 日志缓冲区,避免关键事件丢失) */ +static constexpr u16 kLogSlotCount = 8U; +static constexpr u16 kLogSlotSize = 80U; +static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {}; +static u16 s_logSlotLens[kLogSlotCount] = {}; +static volatile u8 s_logWriteIdx = 0U; +static volatile u8 s_logReadIdx = 0U; +static volatile u8 s_logCount = 0U; + +static void isrLogFrameEx(const char *tag, const u8 *data, u16 len); + + +OTAHandler *SpiOta::s_otaHandler = nullptr; +volatile bool SpiOta::s_csActive = false; +volatile u16 SpiOta::s_rxStreamLen = 0U; +volatile u16 SpiOta::s_txStreamPos = 0U; +u8 SpiOta::s_rxStream[SpiOta::kRxBufSize] = {}; +u8 SpiOta::s_txBuf[SpiOta::kTxBufSize] = {}; +volatile u16 SpiOta::s_txLen = 0U; +volatile bool SpiOta::s_txPending = false; +volatile bool SpiOta::s_isrResponded = false; +volatile bool SpiOta::s_isrLogPending = false; +volatile bool SpiOta::s_otaSessionActive = false; +u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {}; +u16 SpiOta::s_isrLogLen = 0U; + +/* ============================================================ + * CRC8 / 帧构建 / 帧解析 + * ============================================================ */ + +u8 SpiOta::crc8(const u8 *data, u16 len) { + u8 crc = 0x00U; + for (u16 i = 0; i < len; ++i) { crc ^= data[i]; } + 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); + 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_len = total_len; + return true; +} + +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; +} + +ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) { + switch (type) { + case SPI_OTA_CMD_REQ: return OTA_LINK_MSG_REQ; + case SPI_OTA_CMD_DATA: return OTA_LINK_MSG_DATA; + default: return OTA_LINK_MSG_CANCEL; + } +} + +/* ============================================================ + * 回包发送适配器(ISR 安全) + * ============================================================ */ + +void SpiOta::sender(ota_link_tx_type_t type, u8 len, const u8 *data) { + spi_ota_frame_type_t rsp_type = SPI_OTA_RSP_ACK; + if (type == OTA_LINK_TX_WAIT) { rsp_type = SPI_OTA_RSP_WAIT; } + u8 frame_buf[kTxBufSize] = {}; + u16 built_len = 0U; + if (buildFrame(rsp_type, data, len, frame_buf, &built_len)) { + queueTxFrame(frame_buf, built_len); + } +} + +uint32_t SpiOta::enterCritical() { + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + return primask; +} + +void SpiOta::exitCritical(uint32_t primask) { + __set_PRIMASK(primask); +} + +/** @brief 启动 DMA 传输 s_txBuf → SPI0_DATA */ +static void startSpiTxDma(const u8 *buf, u16 len) { + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH); + DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; + DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len; + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +void SpiOta::queueTxFrame(const u8 *frame, u16 len) { + if (!frame || len == 0U || len > kTxBufSize) { return; } + uint32_t primask = enterCritical(); + memcpy(s_txBuf, frame, len); + s_txLen = len; + s_txPending = true; + 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; } + + if (s_rxStream[0] == SPI_OTA_CMD_BOOT) { + isrProcessFrame(); + 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(); +} + +/* ============================================================ + * CS 释放后 ISR 直接帧处理 + * ============================================================ */ + +void SpiOta::isrProcessFrame() { + u16 rx_len = s_rxStreamLen; + if (rx_len == 0U) { return; } + + if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { + u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U}; + queueTxFrame(ack, sizeof(ack)); + if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } + isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen); + s_isrResponded = true; + return; + } + + if (s_rxStream[0] != kSof) { + isrLogFrameEx("[NO-SOF]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + 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 > rx_len) { return; } + + if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) { + 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); + return; + } + + spi_ota_frame_type_t frame_type; + const u8 *payload = nullptr; + u16 p_len = 0U; + if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) { + isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + return; + } + + if (frame_type == SPI_OTA_CMD_BOOT) { + 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); + } + if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } + isrLogFrameEx("[BOOT-FRAME]", s_txBuf, s_txLen); + s_isrResponded = true; + return; + } + + if (s_otaHandler != nullptr) { + s_otaSessionActive = true; + s_otaHandler->setTransportSender(sender); + ota_link_msg_type_t msg_type = mapMsgType(frame_type); + isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1); + s_otaHandler->handleLinkMessage(msg_type, const_cast(payload), (u8)p_len); + } + + isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U); + s_isrResponded = true; +} + +/* ============================================================ + * ISR 内延迟日志 + * ============================================================ */ + +void SpiOta::isrLogFrame(const char *tag, const u8 *data, u16 len) { + isrLogFrameEx(tag, data, len); +} + +static void isrLogFrameEx(const char *tag, const u8 *data, u16 len) { + if (s_logCount >= kLogSlotCount) { return; } + u8 *buf = s_logSlots[s_logWriteIdx]; + u16 pos = 0U; + if (tag) { + while (*tag && pos < kLogSlotSize - 3U) { buf[pos++] = (u8)*tag++; } + buf[pos++] = ' '; + } + const u16 preview = (len < 24U) ? len : 24U; + for (u16 i = 0U; i < preview && pos + 2U < kLogSlotSize; ++i) { + static const char hx[] = "0123456789ABCDEF"; + buf[pos++] = (u8)hx[data[i] >> 4]; + buf[pos++] = (u8)hx[data[i] & 0x0FU]; + } + buf[pos] = 0U; + s_logSlotLens[s_logWriteIdx] = pos; + s_logWriteIdx = (s_logWriteIdx + 1U) % kLogSlotCount; + s_logCount++; +} + +/* ============================================================ + * 硬件初始化 + * ============================================================ */ + +void SpiOta::configureGpio() { + rcu_periph_clock_enable(RCU_GPIOA); + rcu_periph_clock_enable(RCU_GPIOB); + rcu_periph_clock_enable(RCU_AF); + gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE); + gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE); + gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5); + 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_6 | GPIO_PIN_7); + gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); +} + +void SpiOta::configureSpi0() { + rcu_periph_clock_enable(RCU_SPI0); + spi_i2s_deinit(SPI0); + (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ + spi_parameter_struct spi_init_struct; + spi_struct_para_init(&spi_init_struct); + spi_init_struct.device_mode = SPI_SLAVE; + spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; + spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; + spi_init_struct.nss = SPI_NSS_SOFT; + spi_init_struct.endian = SPI_ENDIAN_MSB; + /* SPI NSS 由 ESP32 的 CS 线 (GPIO4→PA15) 控制。\外部 CS 高时: + * spi_nss_internal_high(SPI0) 让 SPI 外设进入未选中状态, + * 这样 SPI0_DATA 输出 0xFF(而非 0x00), + * DMA 只在 CS 低时才被 SPI 外设允许推数据到 MISO。 */ + spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; + spi_init_struct.prescale = SPI_PSC_8; + spi_init(SPI0, &spi_init_struct); + /* 关键修正:内部 NSS 始终保持 LOW,使 SPI0 从机一直处于“已选中”状态。 + * + * APP 侧的稳定实现就是这样做的:PA15 只作为 EXTI 事务边界信号, + * 不再依赖 EXTI ISR 去“赶在首个 SCK 之前”切 internal NSS。 + * + * 之前 BOOT 的问题正出在这里: + * - 空闲时 spi_nss_internal_high(SPI0) + * - CS↓ 后靠 EXTI ISR 再切 internal_low + * - 但 ESP32 在 CS↓ 后几乎立刻出时钟 + * - 于是 GD32 常常在 internal NSS 真正拉低前丢掉首字节/首几位 + * → 请求帧首字节 0xA5 被吃掉,看到 0x80 / 0xFF / 随机 0x07 + * + * 现在改为 internal NSS 常低: + * - SPI 从机始终就绪,不会因为 EXTI 延迟丢首字节 + * - PA15 仍用于 onCsAssert/onCsRelease 做缓冲区清零和 DMA 重装 + */ + spi_nss_internal_low(SPI0); + /* 初始化时清空 SPI_DATA 寄存器,防止残留值影响首次传输 */ + (void)spi_i2s_data_receive(SPI0); + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR); + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); + /* SPI0 RBNE 中断优先级设为 (2, 0),低于 EXTI CS 中断 (1, 1)。 + * 这确保 CS 下降沿的 onCsAssert() 在任何 RBNE 之前执行, + * 否则 s_csActive 仍为 false,RBNE 中断丢弃接收字节。 */ + nvic_irq_enable(SPI0_IRQn, 2U, 0U); + spi_enable(SPI0); +} + +void SpiOta::configureDma() { + rcu_periph_clock_enable(RCU_DMA0); + dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_parameter_struct dma_init_struct; + dma_struct_para_init(&dma_init_struct); + dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU); + dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; + dma_init_struct.memory_addr = (uint32_t)s_txBuf; + dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_init_struct.number = 0U; + dma_init_struct.priority = DMA_PRIORITY_HIGH; + dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; + dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; + dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; + dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_init_struct); + dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); +} + +void SpiOta::configureExti() { + gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); + exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); + exti_interrupt_enable(EXTI_15); + exti_interrupt_flag_clear(EXTI_15); + nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); +} + +void SpiOta::init() { + s_csActive = false; + s_rxStreamLen = 0U; + s_txStreamPos = 0U; + s_txLen = 0U; + s_txPending = false; + s_isrLogPending = false; + memset(s_rxStream, 0, sizeof(s_rxStream)); + memset(s_txBuf, 0xFF, sizeof(s_txBuf)); + configureGpio(); + configureDma(); + configureSpi0(); + configureExti(); +} + +/* ============================================================ + * CS 边沿处理 + * ============================================================ */ + +void SpiOta::onCsAssert() { + if (s_csActive) { return; } + s_csActive = true; + s_rxStreamLen = 0U; + s_txStreamPos = 0U; + s_isrResponded = false; + + /* 内部 NSS 已在初始化时常置 LOW,这里不再依赖 EXTI 时序切 NSS。 + * PA15 仅作为“事务开始”的边界信号,用于清接收缓冲和重装 DMA。 */ + + /* 关键修复:先彻底停止 DMA,再重新配置并启动。 + * + * 上一版预填 SPI_DATA 方案失败原因: + * spi_i2s_data_transmit() 写入的字节进入 TBE 发送缓冲区。 + * 当 ESP32 第一个 SCK 边沿到来时,TBE 的预填字节被移入移位 + * 寄存器推到 MISO,同时移位寄存器中的 MOSI 数据被推入 RBNE。 + * 但 GD32E50x SPI 的 RBNE 标志在 SPI_DATA 写入后可能被误触发, + * 导致 isrLogFrame 报出预填值(如 0x80)而非 MOSI 真实数据。 + * + * 正确方案:不预填 SPI_DATA,而是确保 DMA 在 CS↓ 后、ESP32 + * 第一个 SCK 边沿之前就绪。DMA 第一拍自动把 s_txBuf[0] 写入 + * SPI_DATA,不会产生 RBNE 误触发。 + * + * 操作顺序: + * 1. 禁 DMA 通道 + 禁 SPI DMA 请求(彻底停止旧传输) + * 2. 清 RBNE / RXORERR / FERR 等残留标志 + * 3. 配置并启动 DMA(此时 RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) + * 4. 重新使能 RBNE 中断,开始接收 MOSI 数据 + */ + + /* 1. 停止旧 DMA */ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + + /* 2. 清残留错误标志 + * + * 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。 + * + * 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。 + * 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。 + * 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5 + * 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。 + * + * 所以这里仅清真正的错误标志,不主动消费 RBNE。 + * 已到达的首字节留给 onSpiInterrupt() 正常读走。 */ + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { + spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); + } + + /* 3. 配置并启动 DMA(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */ + if (s_txPending && s_txLen > 0U) { + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiTxDma(s_txBuf, s_txLen); + } else { + s_txPending = false; + s_txLen = kTxBufSize; + memset(s_txBuf, 0xFF, kTxBufSize); + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiTxDma(s_txBuf, s_txLen); + } + + /* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据 */ + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); +} +void SpiOta::onCsRelease() { + if (!s_csActive) { return; } + s_csActive = false; + + /* 内部 NSS 保持常 LOW,不在 CS↑ 时切回 HIGH。 + * 这样 SPI 从机始终处于就绪态,下一次 CS↓ 不会因 EXTI 延迟丢首字节。 + * + * 这里只停止 DMA,避免 CS 高期间继续空转消耗 s_txBuf 内容。 + * s_txPending 保持不变——如果 ISR 已准备好 ACK 帧, + * 下次 CS↓ 时 onCsAssert 会重启 DMA 推出。 */ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { + u8 last_byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU); + if (s_rxStreamLen < kRxBufSize) { + s_rxStream[s_rxStreamLen++] = last_byte; + } + } + + /* 关键调试日志:记录本次 CS 周期结束时到底收到了多少字节。 + * 这样能直接判断: + * 1) 是不是完整收到了 ESP32 发来的整帧 + * 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */ + if (s_rxStreamLen > 0U) { + isrLogFrame("[CS-RX]", s_rxStream, (s_rxStreamLen < 12U) ? s_rxStreamLen : 12U); + } + + if (s_rxStreamLen > 0U && !s_isrResponded) { + isrProcessFrame(); + } +} + +/* ============================================================ + * SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代 + * ============================================================ */ + +void SpiOta::onSpiInterrupt() { + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { + /* 与 APP 侧稳定实现保持一致: + * 如果 EXTI 下降沿因为中断时序略晚到达,但此时 PA15 已经为低, + * 则在首个 RBNE 到来时立即补做 onCsAssert()。 + * + * 关键限制:只能在“本事务尚未收过任何字节”时补做。 + * 否则若 EXTI / SPI 时序偶发抖动,重复 onCsAssert() 会把 + * s_rxStreamLen 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */ + if (!s_csActive && s_rxStreamLen == 0U && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { + onCsAssert(); + } + + u8 byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU); + if (s_rxStreamLen < kRxBufSize) { + s_rxStream[s_rxStreamLen++] = byte; + } + /* 调试阶段先关闭 RBNE 首字节日志。 + * 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位, + * onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。 + * 目前排查重点是“本次 CS 周期总共收到了哪些字节”, + * 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */ + tryProcessCompleteFrame(); + } + /* TBE 已由 DMA 替代,不再处理 */ + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + s_rxStreamLen = 0U; + isrLogFrame("[RXOR]", (const u8*)"OVR", 3); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { + spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); + } +} + +void SpiOta::onCsExtiInterrupt() { + if (exti_interrupt_flag_get(EXTI_15) != RESET) { + exti_interrupt_flag_clear(EXTI_15); + if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } + else { onCsRelease(); } + } +} + +/* ============================================================ + * 主循环接口 + * ============================================================ */ + +void SpiOta::poll() { + if (s_isrResponded) { + s_isrResponded = false; + } + /* 打印 ISR 日志缓冲区 */ + while (s_logCount > 0U) { + __disable_irq(); + if (s_logCount == 0U) { __enable_irq(); break; } + u8 *buf = s_logSlots[s_logReadIdx]; + u8 nextRead = (s_logReadIdx + 1U) % kLogSlotCount; + s_logReadIdx = nextRead; + s_logCount--; + __enable_irq(); + printf("[SPI] %s\r\n", buf); + } + if (s_otaHandler != nullptr) { s_otaHandler->process(); } + + /* 调试:定期打印 SPI 从机状态 */ + static u32 dbg_tick = 0; + dbg_tick++; + if ((dbg_tick % 5000U) == 0U) { + printf("[SPI] csActive=%u rxLen=%u txPend=%u txLen=%u isrResp=%u otaState=%u\r\n", + (unsigned)s_csActive, (unsigned)s_rxStreamLen, + (unsigned)s_txPending, (unsigned)s_txLen, + (unsigned)s_isrResponded, + (unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF)); + } +} + +void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; } + +void SpiOta::feedRxBytes(const u8 *data, u16 len) { + if (!data || len == 0U || !s_csActive) { return; } + if (len > kRxBufSize) { data += (len - kRxBufSize); len = kRxBufSize; } + memcpy(s_rxStream, data, len); + s_rxStreamLen = len; +} + +bool SpiOta::hasTxFrame() { return s_txPending; } + +bool SpiOta::hasActiveSession() { return s_otaSessionActive; } + +u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) { + if (!out || max_len == 0U) { return 0U; } + uint32_t primask = enterCritical(); + if (!s_txPending || s_txLen == 0U) { exitCritical(primask); return 0U; } + u16 copy_len = (s_txLen < max_len) ? s_txLen : max_len; + memcpy(out, s_txBuf, copy_len); + s_txPending = false; + s_txLen = 0U; + s_txStreamPos = 0U; + exitCritical(primask); + return copy_len; +} + +/* ============================================================ + * C 风格兼容包装 + * ============================================================ */ + +void spi_ota_init(void) { SpiOta::init(); } +void spi_ota_poll(void) { SpiOta::poll(); } +void spi_ota_set_sender(OTAHandler *handler) { SpiOta::setSender(handler); } +void spi_ota_on_cs_assert(void) { SpiOta::onCsAssert(); } +void spi_ota_on_cs_release(void) { SpiOta::onCsRelease(); } +void spi_ota_gpio_init(void) { SpiOta::configureGpio(); } +void spi_ota_spi0_init(void) { SpiOta::configureSpi0(); } +void spi_ota_exti_init(void) { SpiOta::configureExti(); } +void spi_ota_irq_handler(void) { SpiOta::onSpiInterrupt(); } +void spi_ota_cs_exti_handler(void) { SpiOta::onCsExtiInterrupt(); } +void spi_ota_feed_rx_bytes(const u8 *data, u16 len) { SpiOta::feedRxBytes(data, len); } +bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); } +u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); } + +extern "C" void SPI0_IRQHandler(void) { + SpiOta::onSpiInterrupt(); +} diff --git a/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260529_txdebug b/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260529_txdebug new file mode 100644 index 0000000..dc775a8 --- /dev/null +++ b/GD32E50X_BOOT/device/SpiOta.cpp.bak_20260529_txdebug @@ -0,0 +1,626 @@ +#include "SpiOta.h" +#include "core_cm33.h" +#include "gd32e50x_gpio.h" +#include "gd32e50x_exti.h" +#include "gd32e50x_rcu.h" +#include "gd32e50x_misc.h" +#include "gd32e50x_spi.h" +#include "gd32e50x_dma.h" +#include +#include + +/** + * @file SpiOta.cpp + * @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。 + * + * CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备, + * 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。 + * 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。 + * + * GD32E50x DMA 通道映射(固定): + * DMA0_CH0: ADC0, DMA0_CH1: SPI0_RX, DMA0_CH2: SPI0_TX + * DMA0_CH3: SPI1_RX, DMA0_CH4: SPI1_TX + */ + +/* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */ +#define SPI_TX_DMA_PERIPH DMA0 +#define SPI_TX_DMA_CH DMA_CH2 + +/* 调试:多缓冲日志(8 条 ISR 日志缓冲区,避免关键事件丢失) */ +static constexpr u16 kLogSlotCount = 8U; +static constexpr u16 kLogSlotSize = 80U; +static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {}; +static u16 s_logSlotLens[kLogSlotCount] = {}; +static volatile u8 s_logWriteIdx = 0U; +static volatile u8 s_logReadIdx = 0U; +static volatile u8 s_logCount = 0U; + +static void isrLogFrameEx(const char *tag, const u8 *data, u16 len); + + +OTAHandler *SpiOta::s_otaHandler = nullptr; +volatile bool SpiOta::s_csActive = false; +volatile u16 SpiOta::s_rxStreamLen = 0U; +volatile u16 SpiOta::s_txStreamPos = 0U; +u8 SpiOta::s_rxStream[SpiOta::kRxBufSize] = {}; +u8 SpiOta::s_txBuf[SpiOta::kTxBufSize] = {}; +volatile u16 SpiOta::s_txLen = 0U; +volatile bool SpiOta::s_txPending = false; +volatile bool SpiOta::s_isrResponded = false; +volatile bool SpiOta::s_isrLogPending = false; +volatile bool SpiOta::s_otaSessionActive = false; +u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {}; +u16 SpiOta::s_isrLogLen = 0U; + +/* ============================================================ + * CRC8 / 帧构建 / 帧解析 + * ============================================================ */ + +u8 SpiOta::crc8(const u8 *data, u16 len) { + u8 crc = 0x00U; + for (u16 i = 0; i < len; ++i) { crc ^= data[i]; } + 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); + 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_len = total_len; + return true; +} + +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; +} + +ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) { + switch (type) { + case SPI_OTA_CMD_REQ: return OTA_LINK_MSG_REQ; + case SPI_OTA_CMD_DATA: return OTA_LINK_MSG_DATA; + default: return OTA_LINK_MSG_CANCEL; + } +} + +/* ============================================================ + * 回包发送适配器(ISR 安全) + * ============================================================ */ + +void SpiOta::sender(ota_link_tx_type_t type, u8 len, const u8 *data) { + spi_ota_frame_type_t rsp_type = SPI_OTA_RSP_ACK; + if (type == OTA_LINK_TX_WAIT) { rsp_type = SPI_OTA_RSP_WAIT; } + u8 frame_buf[kTxBufSize] = {}; + u16 built_len = 0U; + if (buildFrame(rsp_type, data, len, frame_buf, &built_len)) { + queueTxFrame(frame_buf, built_len); + } +} + +uint32_t SpiOta::enterCritical() { + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + return primask; +} + +void SpiOta::exitCritical(uint32_t primask) { + __set_PRIMASK(primask); +} + +/** @brief 启动 DMA 传输 s_txBuf → SPI0_DATA */ +static void startSpiTxDma(const u8 *buf, u16 len) { + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH); + DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; + DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len; + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +void SpiOta::queueTxFrame(const u8 *frame, u16 len) { + if (!frame || len == 0U || len > kTxBufSize) { return; } + uint32_t primask = enterCritical(); + memcpy(s_txBuf, frame, len); + s_txLen = len; + s_txPending = true; + 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; } + + if (s_rxStream[0] == SPI_OTA_CMD_BOOT) { + isrProcessFrame(); + 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(); +} + +/* ============================================================ + * CS 释放后 ISR 直接帧处理 + * ============================================================ */ + +void SpiOta::isrProcessFrame() { + u16 rx_len = s_rxStreamLen; + if (rx_len == 0U) { return; } + + /* 关键修复:一旦开始处理本次 CS 周期中的完整帧,立刻标记“已响应/已处理”。 + * + * 否则会出现这样的竞态: + * 1) SPI0 RBNE IRQ 在收到最后一个字节后调用 isrProcessFrame() + * 2) 还没执行到函数尾部的 s_isrResponded = true + * 3) CS 已经上升,优先级更高的 EXTI IRQ 抢占进来执行 onCsRelease() + * 4) onCsRelease() 看到 s_isrResponded 仍为 false,于是再次调用 isrProcessFrame() + * 5) 同一帧被重复处理一次,状态机被错误推进(如 challenge 又被当 confirm) + * + * 所以必须在函数入口就先置位,防止 REQ/DATA 被双重分发。 */ + s_isrResponded = true; + + if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { + u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U}; + queueTxFrame(ack, sizeof(ack)); + if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } + isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen); + s_isrResponded = true; + return; + } + + if (s_rxStream[0] != kSof) { + isrLogFrameEx("[NO-SOF]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + 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 > rx_len) { return; } + + if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) { + 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); + return; + } + + spi_ota_frame_type_t frame_type; + const u8 *payload = nullptr; + u16 p_len = 0U; + if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) { + isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + return; + } + + if (frame_type == SPI_OTA_CMD_BOOT) { + 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); + } + if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } + isrLogFrameEx("[BOOT-FRAME]", s_txBuf, s_txLen); + s_isrResponded = true; + return; + } + + if (s_otaHandler != nullptr) { + s_otaSessionActive = true; + s_otaHandler->setTransportSender(sender); + ota_link_msg_type_t msg_type = mapMsgType(frame_type); + isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1); + /* 诊断: 记录 OTA 状态机当前状态和帧信息 */ + { + u8 diag[4] = {(u8)frame_type, (u8)p_len, (u8)(p_len >> 8), + (u8)(s_otaHandler ? s_otaHandler->getState() : 0xFFU)}; + isrLogFrameEx("[OTA-DIAG]", diag, 4); + } + s_otaHandler->handleLinkMessage(msg_type, const_cast(payload), (u8)p_len); + } + + isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U); + s_isrResponded = true; +} + +/* ============================================================ + * ISR 内延迟日志 + * ============================================================ */ + +void SpiOta::isrLogFrame(const char *tag, const u8 *data, u16 len) { + isrLogFrameEx(tag, data, len); +} + +static void isrLogFrameEx(const char *tag, const u8 *data, u16 len) { + if (s_logCount >= kLogSlotCount) { return; } + u8 *buf = s_logSlots[s_logWriteIdx]; + u16 pos = 0U; + if (tag) { + while (*tag && pos < kLogSlotSize - 3U) { buf[pos++] = (u8)*tag++; } + buf[pos++] = ' '; + } + const u16 preview = (len < 24U) ? len : 24U; + for (u16 i = 0U; i < preview && pos + 2U < kLogSlotSize; ++i) { + static const char hx[] = "0123456789ABCDEF"; + buf[pos++] = (u8)hx[data[i] >> 4]; + buf[pos++] = (u8)hx[data[i] & 0x0FU]; + } + buf[pos] = 0U; + s_logSlotLens[s_logWriteIdx] = pos; + s_logWriteIdx = (s_logWriteIdx + 1U) % kLogSlotCount; + s_logCount++; +} + +/* ============================================================ + * 硬件初始化 + * ============================================================ */ + +void SpiOta::configureGpio() { + rcu_periph_clock_enable(RCU_GPIOA); + rcu_periph_clock_enable(RCU_GPIOB); + rcu_periph_clock_enable(RCU_AF); + gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE); + gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE); + gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5); + 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_6 | GPIO_PIN_7); + gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); +} + +void SpiOta::configureSpi0() { + rcu_periph_clock_enable(RCU_SPI0); + spi_i2s_deinit(SPI0); + (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ + spi_parameter_struct spi_init_struct; + spi_struct_para_init(&spi_init_struct); + spi_init_struct.device_mode = SPI_SLAVE; + spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; + spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; + spi_init_struct.nss = SPI_NSS_SOFT; + spi_init_struct.endian = SPI_ENDIAN_MSB; + /* SPI NSS 由 ESP32 的 CS 线 (GPIO4→PA15) 控制。\外部 CS 高时: + * spi_nss_internal_high(SPI0) 让 SPI 外设进入未选中状态, + * 这样 SPI0_DATA 输出 0xFF(而非 0x00), + * DMA 只在 CS 低时才被 SPI 外设允许推数据到 MISO。 */ + spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; + spi_init_struct.prescale = SPI_PSC_8; + spi_init(SPI0, &spi_init_struct); + /* 关键修正:内部 NSS 始终保持 LOW,使 SPI0 从机一直处于“已选中”状态。 + * + * APP 侧的稳定实现就是这样做的:PA15 只作为 EXTI 事务边界信号, + * 不再依赖 EXTI ISR 去“赶在首个 SCK 之前”切 internal NSS。 + * + * 之前 BOOT 的问题正出在这里: + * - 空闲时 spi_nss_internal_high(SPI0) + * - CS↓ 后靠 EXTI ISR 再切 internal_low + * - 但 ESP32 在 CS↓ 后几乎立刻出时钟 + * - 于是 GD32 常常在 internal NSS 真正拉低前丢掉首字节/首几位 + * → 请求帧首字节 0xA5 被吃掉,看到 0x80 / 0xFF / 随机 0x07 + * + * 现在改为 internal NSS 常低: + * - SPI 从机始终就绪,不会因为 EXTI 延迟丢首字节 + * - PA15 仍用于 onCsAssert/onCsRelease 做缓冲区清零和 DMA 重装 + */ + spi_nss_internal_low(SPI0); + /* 初始化时清空 SPI_DATA 寄存器,防止残留值影响首次传输 */ + (void)spi_i2s_data_receive(SPI0); + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR); + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); + /* SPI0 RBNE 中断优先级设为 (2, 0),低于 EXTI CS 中断 (1, 1)。 + * 这确保 CS 下降沿的 onCsAssert() 在任何 RBNE 之前执行, + * 否则 s_csActive 仍为 false,RBNE 中断丢弃接收字节。 */ + nvic_irq_enable(SPI0_IRQn, 2U, 0U); + spi_enable(SPI0); +} + +void SpiOta::configureDma() { + rcu_periph_clock_enable(RCU_DMA0); + dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_parameter_struct dma_init_struct; + dma_struct_para_init(&dma_init_struct); + dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU); + dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; + dma_init_struct.memory_addr = (uint32_t)s_txBuf; + dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_init_struct.number = 0U; + dma_init_struct.priority = DMA_PRIORITY_HIGH; + dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; + dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; + dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; + dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_init_struct); + dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); +} + +void SpiOta::configureExti() { + gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); + exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); + exti_interrupt_enable(EXTI_15); + exti_interrupt_flag_clear(EXTI_15); + nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); +} + +void SpiOta::init() { + s_csActive = false; + s_rxStreamLen = 0U; + s_txStreamPos = 0U; + s_txLen = 0U; + s_txPending = false; + s_isrLogPending = false; + memset(s_rxStream, 0, sizeof(s_rxStream)); + memset(s_txBuf, 0xFF, sizeof(s_txBuf)); + configureGpio(); + configureDma(); + configureSpi0(); + configureExti(); +} + +/* ============================================================ + * CS 边沿处理 + * ============================================================ */ + +void SpiOta::onCsAssert() { + if (s_csActive) { return; } + s_csActive = true; + s_rxStreamLen = 0U; + s_txStreamPos = 0U; + s_isrResponded = false; + + /* 内部 NSS 已在初始化时常置 LOW,这里不再依赖 EXTI 时序切 NSS。 + * PA15 仅作为“事务开始”的边界信号,用于清接收缓冲和重装 DMA。 */ + + /* 关键修复:先彻底停止 DMA,再重新配置并启动。 + * + * 上一版预填 SPI_DATA 方案失败原因: + * spi_i2s_data_transmit() 写入的字节进入 TBE 发送缓冲区。 + * 当 ESP32 第一个 SCK 边沿到来时,TBE 的预填字节被移入移位 + * 寄存器推到 MISO,同时移位寄存器中的 MOSI 数据被推入 RBNE。 + * 但 GD32E50x SPI 的 RBNE 标志在 SPI_DATA 写入后可能被误触发, + * 导致 isrLogFrame 报出预填值(如 0x80)而非 MOSI 真实数据。 + * + * 正确方案:不预填 SPI_DATA,而是确保 DMA 在 CS↓ 后、ESP32 + * 第一个 SCK 边沿之前就绪。DMA 第一拍自动把 s_txBuf[0] 写入 + * SPI_DATA,不会产生 RBNE 误触发。 + * + * 操作顺序: + * 1. 禁 DMA 通道 + 禁 SPI DMA 请求(彻底停止旧传输) + * 2. 清 RBNE / RXORERR / FERR 等残留标志 + * 3. 配置并启动 DMA(此时 RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) + * 4. 重新使能 RBNE 中断,开始接收 MOSI 数据 + */ + + /* 1. 停止旧 DMA */ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + + /* 2. 清残留错误标志 + * + * 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。 + * + * 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。 + * 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。 + * 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5 + * 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。 + * + * 所以这里仅清真正的错误标志,不主动消费 RBNE。 + * 已到达的首字节留给 onSpiInterrupt() 正常读走。 */ + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { + spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); + } + + /* 3. 配置并启动 DMA(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */ + if (s_txPending && s_txLen > 0U) { + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiTxDma(s_txBuf, s_txLen); + } else { + s_txPending = false; + s_txLen = kTxBufSize; + memset(s_txBuf, 0xFF, kTxBufSize); + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiTxDma(s_txBuf, s_txLen); + } + + /* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据 */ + spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); +} +void SpiOta::onCsRelease() { + if (!s_csActive) { return; } + s_csActive = false; + + /* 内部 NSS 保持常 LOW,不在 CS↑ 时切回 HIGH。 + * 这样 SPI 从机始终处于就绪态,下一次 CS↓ 不会因 EXTI 延迟丢首字节。 + * + * 这里只停止 DMA,避免 CS 高期间继续空转消耗 s_txBuf 内容。 + * s_txPending 保持不变——如果 ISR 已准备好 ACK 帧, + * 下次 CS↓ 时 onCsAssert 会重启 DMA 推出。 */ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { + u8 last_byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU); + if (s_rxStreamLen < kRxBufSize) { + s_rxStream[s_rxStreamLen++] = last_byte; + } + } + + /* 关键调试日志:记录本次 CS 周期结束时到底收到了多少字节。 + * 这样能直接判断: + * 1) 是不是完整收到了 ESP32 发来的整帧 + * 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */ + if (s_rxStreamLen > 0U) { + isrLogFrame("[CS-RX]", s_rxStream, (s_rxStreamLen < 12U) ? s_rxStreamLen : 12U); + } + + if (s_rxStreamLen > 0U && !s_isrResponded) { + isrProcessFrame(); + } +} + +/* ============================================================ + * SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代 + * ============================================================ */ + +void SpiOta::onSpiInterrupt() { + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { + /* 与 APP 侧稳定实现保持一致: + * 如果 EXTI 下降沿因为中断时序略晚到达,但此时 PA15 已经为低, + * 则在首个 RBNE 到来时立即补做 onCsAssert()。 + * + * 关键限制:只能在“本事务尚未收过任何字节”时补做。 + * 否则若 EXTI / SPI 时序偶发抖动,重复 onCsAssert() 会把 + * s_rxStreamLen 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */ + if (!s_csActive && s_rxStreamLen == 0U && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { + onCsAssert(); + } + + u8 byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU); + if (s_rxStreamLen < kRxBufSize) { + s_rxStream[s_rxStreamLen++] = byte; + } + /* 调试阶段先关闭 RBNE 首字节日志。 + * 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位, + * onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。 + * 目前排查重点是“本次 CS 周期总共收到了哪些字节”, + * 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */ + tryProcessCompleteFrame(); + } + /* TBE 已由 DMA 替代,不再处理 */ + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + s_rxStreamLen = 0U; + isrLogFrame("[RXOR]", (const u8*)"OVR", 3); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { + spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); + } +} + +void SpiOta::onCsExtiInterrupt() { + if (exti_interrupt_flag_get(EXTI_15) != RESET) { + exti_interrupt_flag_clear(EXTI_15); + if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } + else { onCsRelease(); } + } +} + +/* ============================================================ + * 主循环接口 + * ============================================================ */ + +void SpiOta::poll() { + if (s_isrResponded) { + s_isrResponded = false; + } + /* 打印 ISR 日志缓冲区 */ + while (s_logCount > 0U) { + __disable_irq(); + if (s_logCount == 0U) { __enable_irq(); break; } + u8 *buf = s_logSlots[s_logReadIdx]; + u8 nextRead = (s_logReadIdx + 1U) % kLogSlotCount; + s_logReadIdx = nextRead; + s_logCount--; + __enable_irq(); + printf("[SPI] %s\r\n", buf); + } + if (s_otaHandler != nullptr) { s_otaHandler->process(); } + + /* 调试:定期打印 SPI 从机状态 */ + static u32 dbg_tick = 0; + dbg_tick++; + if ((dbg_tick % 5000U) == 0U) { + printf("[SPI] csActive=%u rxLen=%u txPend=%u txLen=%u isrResp=%u otaState=%u\r\n", + (unsigned)s_csActive, (unsigned)s_rxStreamLen, + (unsigned)s_txPending, (unsigned)s_txLen, + (unsigned)s_isrResponded, + (unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF)); + } +} + +void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; } + +void SpiOta::feedRxBytes(const u8 *data, u16 len) { + if (!data || len == 0U || !s_csActive) { return; } + if (len > kRxBufSize) { data += (len - kRxBufSize); len = kRxBufSize; } + memcpy(s_rxStream, data, len); + s_rxStreamLen = len; +} + +bool SpiOta::hasTxFrame() { return s_txPending; } + +bool SpiOta::hasActiveSession() { return s_otaSessionActive; } + +u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) { + if (!out || max_len == 0U) { return 0U; } + uint32_t primask = enterCritical(); + if (!s_txPending || s_txLen == 0U) { exitCritical(primask); return 0U; } + u16 copy_len = (s_txLen < max_len) ? s_txLen : max_len; + memcpy(out, s_txBuf, copy_len); + s_txPending = false; + s_txLen = 0U; + s_txStreamPos = 0U; + exitCritical(primask); + return copy_len; +} + +/* ============================================================ + * C 风格兼容包装 + * ============================================================ */ + +void spi_ota_init(void) { SpiOta::init(); } +void spi_ota_poll(void) { SpiOta::poll(); } +void spi_ota_set_sender(OTAHandler *handler) { SpiOta::setSender(handler); } +void spi_ota_on_cs_assert(void) { SpiOta::onCsAssert(); } +void spi_ota_on_cs_release(void) { SpiOta::onCsRelease(); } +void spi_ota_gpio_init(void) { SpiOta::configureGpio(); } +void spi_ota_spi0_init(void) { SpiOta::configureSpi0(); } +void spi_ota_exti_init(void) { SpiOta::configureExti(); } +void spi_ota_irq_handler(void) { SpiOta::onSpiInterrupt(); } +void spi_ota_cs_exti_handler(void) { SpiOta::onCsExtiInterrupt(); } +void spi_ota_feed_rx_bytes(const u8 *data, u16 len) { SpiOta::feedRxBytes(data, len); } +bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); } +u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); } + +extern "C" void SPI0_IRQHandler(void) { + SpiOta::onSpiInterrupt(); +} diff --git a/GD32E50X_BOOT/device/SpiOta.h b/GD32E50X_BOOT/device/SpiOta.h index a32d4bc..5de3722 100644 --- a/GD32E50X_BOOT/device/SpiOta.h +++ b/GD32E50X_BOOT/device/SpiOta.h @@ -64,6 +64,8 @@ public: static u8 s_isrLogBuf[kIsrLogBufSize]; static u16 s_isrLogLen; + 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, @@ -73,7 +75,6 @@ private: const u8 **payload, u16 *payload_len); static ota_link_msg_type_t mapMsgType(spi_ota_frame_type_t type); - static void sender(ota_link_tx_type_t type, u8 len, const u8 *data); static void queueTxFrame(const u8 *frame, u16 len); static uint32_t enterCritical(); static void exitCritical(uint32_t primask); diff --git a/GD32E50X_BOOT/main.cpp b/GD32E50X_BOOT/main.cpp index fef620d..88d8f60 100644 --- a/GD32E50X_BOOT/main.cpp +++ b/GD32E50X_BOOT/main.cpp @@ -114,6 +114,29 @@ void BootloaderSystem::run() { volatile uint32_t *flash_ptr; uint32_t app_enable, app_check; + /** 检测 FWDGT 看门狗复位:APP 卡死导致看门狗超时。 + * 将 app_enable 标记为 NOT_AVAL,阻止 BOOT 再次跳入卡死的 APP, + * 改为停留在 BOOT 等待 OTA 修复。 */ + if (rcu_flag_get(RCU_FLAG_FWDGTRST) != RESET) { + rcu_all_reset_flag_clear(); +#if DEBUG_PRINT + printf("[BOOT] FWDGT reset detected, blocking app jump\r\n"); +#endif + OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL); + } + + /** 检测看门狗复位:如果上次复位是由 APP 的 FWDGT 超时触发, + * 则将 app_enable 标记为 NOT_AVAL,阻止 BOOT 再次跳入卡死的 APP, + * 让 BOOT 停留在 OTA 稡式等待远程修复。 */ + if (rcu_flag_get(RCU_FLAG_FWDGTRST) != RESET) { +#if DEBUG_PRINT + printf("[BOOT] FWDGT reset detected, blocking app jump\r\n"); +#endif + OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL); + } + + rcu_all_reset_flag_clear(); + flash_ptr = (volatile uint32_t *)n_APP_LATEST_EN_ADDR; app_enable = *flash_ptr; flash_ptr = (volatile uint32_t *)n_APP_LATEST_CHECK_ADDR; diff --git a/GD32E50X_BOOT/sys/OTA.cpp b/GD32E50X_BOOT/sys/OTA.cpp index c13d64c..5a87134 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -1,5 +1,6 @@ #include "OTA.h" #include "Can.h" +#include "SpiOta.h" #include "aes128.h" #include "config.h" #include "core_cm33.h" @@ -56,7 +57,7 @@ static void ota_can_sender(ota_link_tx_type_t type, u8 len, const u8 *data) { } /** - * @brief SPI 链路回包发送占位适配器。 + * @brief SPI 链路回包发送适配器。 * * @param type OTA 回包类型。 * @param len 有效载荷长度。 @@ -64,14 +65,19 @@ static void ota_can_sender(ota_link_tx_type_t type, u8 len, const u8 *data) { * @return 无。 */ static void ota_spi_sender(ota_link_tx_type_t type, u8 len, const u8 *data) { - (void)type; - (void)len; - (void)data; - /** - * 这里仍保留占位实现。 - * 当前 SPI OTA 的真正回包发送,是由 device/SpiOta.cpp 内的 spi_ota_sender() 接管的。 + * 关键修复:这里不能是空函数! + * + * 之前是占位实现,因为 ISR 内的回包由 SpiOta::isrProcessFrame() 直接调用 + * SpiOta::sender() → queueTxFrame() → DMA 推送。 + * + * 但 process() 主循环中也会发送 ACK(如擦除完成后的 ACK(seq=0)), + * 这些调用走的是 transport_send = ota_spi_sender。 + * 如果这里是空的,ACK 就被吞掉了,ESP32 永远等不到擦除完成的准入信号。 + * + * 所以必须委托给 SpiOta::sender(),它会构建帧并推 DMA。 */ + SpiOta::sender(type, len, data); } /** @@ -244,11 +250,31 @@ void OTAHandler::process() { /** * 把真正耗时的 Flash 擦除动作推迟到主循环做, * 避免在接收消息的处理路径里执行过重操作。 + * + * 协议关键点:SIZE 阶段不能在“刚收到大小”时立刻 ACK(seq=0), + * 否则 ESP32 会马上开始发数据包,而此时 BOOT 可能仍在整片擦 Flash。 + * 擦除期间本函数会关中断,SPI 无法及时收包/回 ACK,导致包0超时。 + * + * 因此改为: + * 1. ISR 收到 SIZE 后只回 WAIT,并置 pending_flash_erase=true + * 2. 这里真正擦除完成后,再切到 RECEIVING_FIRMWARE 并发送 ACK(seq=0) */ if (pending_flash_erase) { pending_flash_erase = false; eraseApplicationArea(); ota_state = OTA_STATE_RECEIVING_FIRMWARE; + + /* 关键修复:SIZE 请求来自 SPI 时,擦除完成后的“准入 ACK(seq=0)” + * 必须强制走 SPI 回包。 + * + * 否则如果主循环里恰好有 CAN communication_check() 把 transport_send + * 切回了 CAN,process() 这里发出的 ACK 会跑到 CAN 通道,ESP32 SPI 侧就 + * 只能一直读到上一次 WAIT 帧残留(例如 0x27 CRC 尾字节)。 */ + ota_bind_spi_transport(); +#if DEBUG_PRINT + printf("[OTA] erase done, send SPI ACK seq0\r\n"); +#endif + send_structured_ack(0U); } if (pending_app_jump) { @@ -343,6 +369,18 @@ bool validateApplication() { bool stack_valid = (stack_ptr >= 0x20000000U && stack_ptr < 0x20020000U); bool reset_valid = (reset_handler >= APPLATEST_START_ADDR && reset_handler < (APPLATEST_START_ADDR + APP_MAX_SIZE)); + +#if DEBUG_PRINT + printf("[BOOT] validateApp: MSP=0x%08lX (%s) Reset=0x%08lX (%s) @0x%08lX\r\n", + (unsigned long)stack_ptr, stack_valid ? "OK" : "INVALID", + (unsigned long)reset_handler, reset_valid ? "OK" : "INVALID", + (unsigned long)APPLATEST_START_ADDR); + /* 额外打印前 4 个 word 帮助诊断固件内容是否正确 */ + printf("[BOOT] vector[0..3]=%08lX %08lX %08lX %08lX\r\n", + (unsigned long)app_vector[0], (unsigned long)app_vector[1], + (unsigned long)app_vector[2], (unsigned long)app_vector[3]); +#endif + return stack_valid && reset_valid; } @@ -376,6 +414,11 @@ void OTAHandler::jumpToApplication() { /** 关闭 Bootloader 运行过程中启用过的外设。 */ can_deinit(CAN0); + spi_i2s_deinit(SPI0); + exti_interrupt_disable(EXTI_15); + exti_interrupt_flag_clear(EXTI_15); + DMA_CHCTL(DMA0, DMA_CH1) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(DMA0, DMA_CH2) &= ~DMA_CHXCTL_CHEN; rcu_system_clock_source_config(RCU_CKSYSSRC_IRC8M); rcu_periph_clock_disable(RCU_GPIOA); @@ -383,6 +426,8 @@ void OTAHandler::jumpToApplication() { rcu_periph_clock_disable(RCU_GPIOC); rcu_periph_clock_disable(RCU_USART0); rcu_periph_clock_disable(RCU_CAN0); + rcu_periph_clock_disable(RCU_SPI0); + rcu_periph_clock_disable(RCU_AF); rcu_periph_clock_disable(RCU_TIMER0); rcu_periph_clock_disable(RCU_DMA0); @@ -398,6 +443,9 @@ void OTAHandler::jumpToApplication() { /** 切栈后直接跳转执行主应用。 */ __set_MSP(msp_value); + __set_CONTROL(0); + __DSB(); + __ISB(); rcu_all_reset_flag_clear(); app_entry(); } @@ -469,6 +517,13 @@ void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) { * @return 无。 */ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { +#if DEBUG_PRINT + /* 精简: 只打印关键帧(非DATA包或状态变化时), DATA包省略 */ + if (type != OTA_LINK_MSG_DATA || ota_state != OTA_STATE_RECEIVING_FIRMWARE) { + printf("[OTA] hl: t=%u l=%u s=%u\r\n", + (unsigned)type, (unsigned)len, (unsigned)ota_state); + } +#endif /** * CANCEL:无条件终止当前 OTA 会话,回到空闲态。 */ @@ -577,11 +632,14 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { return; } - /** 把擦除动作延后到 process() 中执行。 */ + /** + * 不要立刻 ACK(seq=0)。 + * 先告诉 ESP32“我在忙(WAIT)”,等 process() 真正擦除完 Flash 后, + * 再发送 ACK(seq=0) 作为“现在可以开始发包”的准入信号。 + */ pending_flash_erase = true; - - /** 固件大小合法,返回严格 seq 语义 ACK:阶段起始包号 0。 */ - send_structured_ack(0U); + u8 wait_hint[1] = {0x02}; + send_wait_frame(1, wait_hint); } /** 结束态下要求显式收到 0x0A AC,确认可以切换到 APP。 */ else if (len == 2U && ota_state == OTA_STATE_FINISHED) { @@ -608,9 +666,12 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET); return; } - /** 其它长度 / 状态组合都视作非法包。 */ 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); +#endif ota_state = OTA_STATE_IDLE; send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET); } diff --git a/GD32E50X_BOOT/sys/OTA.h b/GD32E50X_BOOT/sys/OTA.h index e88fc24..fa4c644 100644 --- a/GD32E50X_BOOT/sys/OTA.h +++ b/GD32E50X_BOOT/sys/OTA.h @@ -5,7 +5,10 @@ #include "aes128.h" #include "config.h" #include "gd32e50x.h" + + #include +#include /** * @brief OTA 统一链路消息类型。 @@ -88,6 +91,16 @@ class OTAHandler { ota_state = OTA_STATE_IDLE; ota_time = 0; ota_error = OTA_ERROR_NONE; + received_size = 0; + expected_size = 0; + memset(current_K_session, 0, sizeof(current_K_session)); + memset(nonce_server, 0, sizeof(nonce_server)); + memset(&aes_ctx, 0, sizeof(aes_ctx)); + pending_flash_erase = false; + pending_app_jump = false; + pending_flash_offset = 0; + memset(pending_flash_data, 0, sizeof(pending_flash_data)); + pending_flash_size = 0; } /**