From c644c29a4be08d5fe56da4892ce0cefb68f24b20 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 13 Jul 2026 03:10:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(GD32E50X=5FBOOT):=20=E9=9B=86=E6=88=90SPIv?= =?UTF-8?q?2=E5=91=BD=E4=BB=A4=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=B4=E6=96=87=E4=BB=B6=E8=B7=AF=E5=BE=84=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加spi_link_v2.cpp到项目中,分组为spi_link - 新增G:/Robot_v2/DCU/spi_link目录到编译器包含路径 - 优化项目结构以支持SPI通信功能 --- GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml | 4 + GD32E50X_BOOT/device/SpiOta.cpp | 689 +++++++++++++++-------- GD32E50X_BOOT/device/SpiOta.h | 15 +- GD32E50X_BOOT/main.cpp | 10 +- GD32E50X_BOOT/sys/OTA.cpp | 64 ++- GD32E50X_BOOT/sys/initial.cpp | 6 +- 6 files changed, 534 insertions(+), 254 deletions(-) diff --git a/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml b/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml index d68b175..5b9411e 100644 --- a/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml +++ b/GD32E50X_BOOT/GD32E50X_BOOT.cproject.yml @@ -23,6 +23,9 @@ project: - file: ./device/Can.cpp - file: ./device/SpiOta.h - file: ./device/SpiOta.cpp + - group: spi_link + files: + - file: ../../spi_link/spi_link_v2.cpp output: type: @@ -62,6 +65,7 @@ project: - -fshort-enums - -fshort-wchar - -Wno-register + - -IG:/Robot_v2/DCU/spi_link C: - -std=gnu11 CPP: diff --git a/GD32E50X_BOOT/device/SpiOta.cpp b/GD32E50X_BOOT/device/SpiOta.cpp index ae2c6de..fc72c3f 100644 --- a/GD32E50X_BOOT/device/SpiOta.cpp +++ b/GD32E50X_BOOT/device/SpiOta.cpp @@ -7,28 +7,30 @@ #include "gd32e50x_spi.h" #include "gd32e50x_dma.h" #include "sys/systick.h" +#include "spi_link_v2.h" #include #include /** * @file SpiOta.cpp - * @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。 + * @brief Bootloader SPI OTA 从机模块实现(DMA 全双工版)。 * * CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备, * 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。 - * 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。 + * 接收走 DMA0_CH1(SPI0_RX),不再用 RBNE 逐字节 ISR。 + * ESP32 固定打 256B 时钟时,RBNE 路径必现 RX-OVR/SOF 丢失/PARSE-ERR。 * * 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 固定映射) */ - /* 主动按铃超时时间(毫秒) */ static constexpr u32 kRingTimeoutMs = 500U; #define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_CH DMA_CH2 +#define SPI_RX_DMA_PERIPH DMA0 +#define SPI_RX_DMA_CH DMA_CH1 /* 调试日志缓冲 */ static constexpr u16 kLogSlotCount = 8U; @@ -57,66 +59,93 @@ volatile u32 SpiOta::s_ringDeadline = 0U; SpiLinkState SpiOta::s_linkState; /* ============================================================ - * CRC8 / 帧构建 / 帧解析 + * SPI Link v2 SESSION/SEQ 状态 + 响应缓存(ISR 内读写) * ============================================================ */ -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); +static volatile u32 s_sessionId = 0U; +static volatile u8 s_txSeq = 0U; +static volatile u8 s_expectedRxSeq = 0U; +static volatile bool s_v2Active = false; + +/* 响应缓存:用于重复帧重发(dist 128~255 分支)。 + * Boot 的 TX buffer 最多 128 字节,缓存长度对齐 kTxBufSize。 */ +static u8 s_lastReqSeq = 0xFFU; +static u8 s_lastRespBuf[128] = {}; /* kTxBufSize=128 */ +static u16 s_lastRespLen = 0U; +static bool s_hasCachedResp = false; + +/* ============================================================ + * 帧构建 / 帧解析(v2 委托 spi_link_v2.cpp) + * ============================================================ */ /* 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); +/* Boot TX buffer 仅 128 字节,最大 payload = 128 - V2_OVERHEAD(20) = 108 字节。 + * v2_build_frame 只检查 payload_len > V2_MAX_PAYLOAD(236),Boot 侧需额外检查 + * total <= kTxBufSize。 */ +static constexpr u16 kBootMaxPayload = (u16)(SpiOta::kTxBufSize - V2_OVERHEAD); + +/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */ +static volatile u8 s_pendingAckSeq = 0U; + +/* + * OTA 固件分包(64B DATA)延后到主循环处理。 + * AES-128 + CRC + 入 flash 队列若在 CS EXTI ISR 内执行,会长时间关抢占: + * ESP32 下一笔 CS 到达时 DMA 尚未 re-arm → 中后期(约几十~上百包后)卡死。 + * ISR 只入队;主循环 SpiOta::poll() 调 handleLinkMessage 再回 ACK。 + */ +static constexpr u8 kOtaDataSlots = 4U; +static u8 s_otaDataSlots[kOtaDataSlots][64]; +static volatile u8 s_otaDataWi = 0U; +static volatile u8 s_otaDataRi = 0U; +static volatile u8 s_otaDataCnt = 0U; + +static bool enqueueOtaDataIsr(const u8 *payload) +{ + if (payload == nullptr || s_otaDataCnt >= kOtaDataSlots) { + return false; + } + memcpy(s_otaDataSlots[s_otaDataWi], payload, 64U); + s_otaDataWi = (u8)((s_otaDataWi + 1U) % kOtaDataSlots); + s_otaDataCnt++; + return true; } -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; - } - } +static bool dequeueOtaData(u8 out[64]) +{ + if (out == nullptr || s_otaDataCnt == 0U) { + return false; } - return crc; + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + if (s_otaDataCnt == 0U) { + __set_PRIMASK(primask); + return false; + } + memcpy(out, s_otaDataSlots[s_otaDataRi], 64U); + s_otaDataRi = (u8)((s_otaDataRi + 1U) % kOtaDataSlots); + s_otaDataCnt--; + __set_PRIMASK(primask); + return true; } 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)(kSyncOverhead + payload_len); - if (total_len > kTxBufSize) { return false; } - 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; + if (payload_len > kBootMaxPayload) { return false; } + /* FLAGS=RESPONSE:Boot 作为从机只发响应帧;填入当前 session/seq/credit。 + * ACK_SEQ = 对端请求 TX_SEQ,不能写死 0,否则 ESP32 otaFindFrame 会丢弃合法响应。 */ + size_t built = 0U; + bool ok = v2_build_frame((uint8_t)type, V2_FLAG_RESPONSE, + (uint32_t)s_sessionId, + (uint8_t)s_txSeq, (uint8_t)s_pendingAckSeq, (uint8_t)g_localCredit, + (const uint8_t *)payload, (size_t)payload_len, + (uint8_t *)out, &built); + if (!ok) { return false; } + if (built > kTxBufSize) { return false; } + *out_len = (u16)built; return true; } @@ -124,28 +153,19 @@ 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 < kSyncOverhead) { + if (!frame || frame_len < V2_OVERHEAD) { 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; + v2_header_t hdr; + const uint8_t *pload = nullptr; + if (!v2_parse_frame((const uint8_t *)frame, (size_t)frame_len, &hdr, &pload)) { + return false; } - return false; + if (type) { *type = (spi_ota_frame_type_t)hdr.type; } + if (payload) { *payload = (const u8 *)pload; } + if (payload_len) { *payload_len = (u16)hdr.payload_len; } + g_remoteCredit = hdr.credit; + return true; } ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) { @@ -189,6 +209,82 @@ static void startSpiTxDma(const u8 *buf, u16 len) { DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; } +/** @brief 启动 DMA 接收 SPI0_DATA → s_rxStream(容量 kRxBufSize)。 */ +static void startSpiRxDma(void) { + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_RX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_RX_DMA_CH); + DMA_CHMADDR(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = (uint32_t)SpiOta::s_rxStream; + DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = SpiOta::kRxBufSize; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +/** @brief 清空 SPI 接收通路残留(含 OVR)。 */ +static void drainSpiRxHw(void) { + while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) { + (void)spi_i2s_data_receive(SPI0); + } + if (spi_i2s_flag_get(SPI0, SPI_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + (void)SPI_STAT(SPI0); + } +} + +/** + * @brief 在 CS 空闲期预装下一笔全双工 DMA。 + * + * 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚, + * 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。 + * + * TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。 + * 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。 + */ +static void armSpiDmasForNextCs(void) { + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + drainSpiRxHw(); + + /* 预填 TX:有挂起响应用响应帧(头部)+ 0xFF 填充到满缓冲 */ + if (SpiOta::s_txPending && SpiOta::s_txLen > 0U && SpiOta::s_txLen <= SpiOta::kTxBufSize) { + if (SpiOta::s_txLen < SpiOta::kTxBufSize) { + memset(&SpiOta::s_txBuf[SpiOta::s_txLen], 0xFF, + (size_t)(SpiOta::kTxBufSize - SpiOta::s_txLen)); + } + } else { + SpiOta::s_txPending = false; + SpiOta::s_txLen = 0U; + memset(SpiOta::s_txBuf, 0xFF, SpiOta::kTxBufSize); + } + + memset(SpiOta::s_rxStream, 0x00, sizeof(SpiOta::s_rxStream)); + SpiOta::s_rxStreamLen = 0U; + + /* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */ + spi_i2s_data_transmit(SPI0, SpiOta::s_txBuf[0]); + + /* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */ + spi_dma_enable(SPI0, SPI_DMA_RECEIVE); + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiRxDma(); + /* TX 永远满缓冲长度,与主机 96~128B 事务匹配 */ + startSpiTxDma(SpiOta::s_txBuf, SpiOta::kTxBufSize); +} + +/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 */ +static u16 stopSpiDmas(void) { + const u16 remaining = (u16)DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + u16 got = 0U; + if (remaining <= SpiOta::kRxBufSize) { + got = (u16)(SpiOta::kRxBufSize - remaining); + } + return got; +} + void SpiOta::queueTxFrame(const u8 *frame, u16 len) { if (!frame || len == 0U || len > kTxBufSize) { return; } uint32_t primask = enterCritical(); @@ -223,76 +319,130 @@ void SpiOta::isrProcessFrame() { /* 提前置位,避免同一事务被重复处理。 */ s_isrResponded = true; - if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { - /* 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; - s_isrResponded = true; - return; - } + /* 裸字节 0xB0/0xB5 路径已删除:v2 协议下 ENTER_BOOT 走标准帧。 + * 短于 v2 帧头的输入视为残包/空轮询,直接丢弃。 */ + if (rx_len < V2_OVERHEAD) { return; } - /* 新同步帧校验。 */ - if (rx_len < kSyncOverhead || - s_rxStream[0] != kSyncSof0 || s_rxStream[1] != kSyncSof1) { - return; - } - - u16 payload_len = getU16Le(&s_rxStream[4U]); - u16 expected = (u16)(kSyncOverhead + payload_len); - if (expected > rx_len) { return; } - - /* 帧收齐日志:确认 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 < 18U) ? rx_len : 18U); - return; - } - - spi_ota_frame_type_t frame_type; + /* ---- 1. v2 帧解析(内部扫描 SOF、校验 VERSION/CRC32)---- */ + v2_header_t hdr; 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))); + if (!v2_parse_frame((const uint8_t *)s_rxStream, (size_t)rx_len, &hdr, + (const uint8_t **)&payload)) { + /* ESP32 otaPoll 发全 0xFF dummy 属延迟一拍常态,不刷 PARSE-ERR。 */ + bool all_ff = (rx_len > 0U); + const u16 chk = (rx_len < 8U) ? rx_len : 8U; + for (u16 i = 0U; i < chk; ++i) { + if (s_rxStream[i] != 0xFFU) { all_ff = false; break; } + } + if (!all_ff) { + isrLogFrameEx("[PARSE-ERR]", s_rxStream, chk); } -#endif - isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); return; } - if (frame_type == SPI_OTA_CMD_BOOT) { - /* 帧式 0xB0 与裸字节 0xB0 行为一致:始终回裸字节 0xB5 + resetToIdle()。 - * 旧代码在 isActive() 时静默吞帧且回 0xA5 帧式 ACK,违反 - * "0xB0 始终回 0xB5" 约束,且与裸字节路径不一致(0xA5 vs 0xB5)。 - * ESP32 appRequestBootloader() 只读首字节期望 0xB5,帧式 ACK 永不匹配。 */ - u8 ack = 0xB5U; - queueTxFrame(&ack, 1U); + u16 p_len = (u16)hdr.payload_len; + /* 正常收帧不打 [RX-FRM]:OTA 千包级会刷爆串口;异常路径仍有 PARSE-ERR/SEQ-JMP 等。 */ + g_remoteCredit = hdr.credit; + /* 响应归属:ACK_SEQ 回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */ + s_pendingAckSeq = hdr.tx_seq; + + /* ---- 2. TYPE=0x00 HELLO:建立/重置 session ---- */ + if (hdr.type == V2_TYPE_LINK_HELLO) { + s_sessionId = (u32)hdr.session_id; + s_txSeq = 0U; + s_expectedRxSeq = 0U; + s_v2Active = true; + u8 hello_ack[kTxBufSize] = {}; + size_t built = 0U; + if (v2_build_hello_ack_frame((uint32_t)s_sessionId, V2_ROLE_BOOT, + (uint8_t)g_localCredit, + (uint8_t)s_txSeq, hdr.tx_seq, + hello_ack, &built)) { + queueTxFrame(hello_ack, (u16)built); + /* 缓存 HELLO_ACK 以应对 ESP32 重试同一 HELLO。 */ + memcpy(s_lastRespBuf, hello_ack, built); + s_lastRespLen = (u16)built; + s_lastReqSeq = hdr.tx_seq; + s_hasCachedResp = true; + } + isrLogFrameEx("[HELLO]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + s_rxStreamLen = 0U; + return; + } + + /* ---- 3. v2 已激活:SESSION/SEQ 校验 ---- */ + if (s_v2Active) { + /* 3a. session 校验:不匹配回 LINK_ERROR(0x02)。 */ + if ((u32)hdr.session_id != s_sessionId) { + u8 err_frame[kTxBufSize] = {}; + size_t built = 0U; + if (v2_build_error_frame((uint32_t)s_sessionId, (uint8_t)s_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SESSION, + (uint8_t)s_expectedRxSeq, + err_frame, &built)) { + queueTxFrame(err_frame, (u16)built); + } + isrLogFrameEx("[SES-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + s_rxStreamLen = 0U; + return; + } + + /* 3b. SEQ 距离判定。 */ + u8 dist = v2_seq_distance(hdr.tx_seq, (uint8_t)s_expectedRxSeq); + if (dist == 0U) { + /* 正常:序号匹配,推进期望序号(volatile 原子写)。 */ + s_expectedRxSeq = (u8)(s_expectedRxSeq + 1U); + } else if (dist >= 128U) { + /* 重复请求(rx_seq 落后于 expected):重发上一条缓存响应, + * 不推进序号。若 DATA 仍在主循环排队,回 WAIT 让 ESP32 继续 poll。 */ + 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); + } + isrLogFrameEx("[DUP-SEQ]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + s_rxStreamLen = 0U; + return; + } else { + /* 1..127:跳序(丢失了 dist 帧),回 LINK_ERROR(0x04)。 */ + u8 err_frame[kTxBufSize] = {}; + size_t built = 0U; + if (v2_build_error_frame((uint32_t)s_sessionId, (uint8_t)s_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SEQ_JUMP, + (uint8_t)s_expectedRxSeq, + err_frame, &built)) { + queueTxFrame(err_frame, (u16)built); + } + isrLogFrameEx("[SEQ-JMP]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); + s_rxStreamLen = 0U; + return; + } + } + + /* ---- 4. TYPE=0xB0 ENTER_BOOT:标准帧 ACK + resetToIdle ---- */ + if (hdr.type == SPI_OTA_CMD_BOOT) { + u8 ack_frame[kTxBufSize] = {}; + u16 built = 0U; + if (buildFrame(SPI_OTA_RSP_ACK, nullptr, 0U, ack_frame, &built)) { + queueTxFrame(ack_frame, built); + /* 缓存 ACK 响应以应对重复的 0xB0。 */ + memcpy(s_lastRespBuf, ack_frame, built); + s_lastRespLen = built; + s_lastReqSeq = hdr.tx_seq; + s_hasCachedResp = true; + } if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } s_otaSessionActive = false; + s_otaDataWi = 0U; + s_otaDataRi = 0U; + s_otaDataCnt = 0U; s_rxStreamLen = 0U; - s_isrResponded = true; return; } + /* ---- 5. 走原有 OTA 状态机分发 ---- */ + spi_ota_frame_type_t frame_type = (spi_ota_frame_type_t)hdr.type; /* 仅转发 BOOT 实际支持的 OTA 协议帧(REQ/DATA/CANCEL)给 OTA 状态机。 * 其它帧类型(如 0xC0~0xC3 CAN bridge 命令,由 DCU_APP 注册处理器处理, * BOOT 无此功能)直接返回 ERROR,避免 mapMsgType 的 default 分支把 @@ -309,7 +459,6 @@ void SpiOta::isrProcessFrame() { } isrLogFrameEx("[UNK-TYPE]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); s_rxStreamLen = 0U; - s_isrResponded = true; return; } @@ -317,7 +466,31 @@ void SpiOta::isrProcessFrame() { s_otaSessionActive = true; s_otaHandler->setTransportSender(sender); ota_link_msg_type_t msg_type = mapMsgType(frame_type); - s_otaHandler->handleLinkMessage(msg_type, const_cast(payload), (u8)p_len); + + /* 64B 固件分包:只入队,AES/Flash 放到主循环,避免 ISR 过长。 */ + if (frame_type == SPI_OTA_CMD_DATA && p_len == 64U && payload != nullptr) { + if (!enqueueOtaDataIsr(payload)) { + g_localCredit = 0U; + u8 wait_hint = 0x03U; + sender(OTA_LINK_TX_WAIT, 1U, &wait_hint); + isrLogFrameEx("[DATA-QFULL]", s_rxStream, 8U); + } else { + /* 立即 WAIT:告诉 ESP32“已收包、处理中”,主循环完成后再 ACK。 + * 若主循环在 ESP32 首 poll 前已写好 ACK,会覆盖本 WAIT。 */ + u8 wait_hint = 0x03U; + sender(OTA_LINK_TX_WAIT, 1U, &wait_hint); + } + } else { + s_otaHandler->handleLinkMessage(msg_type, const_cast(payload), (u8)p_len); + } + } + + /* ---- 6. 分发后缓存响应到 s_lastRespBuf ---- */ + if (s_txPending && s_txLen > 0U && s_txLen <= sizeof(s_lastRespBuf)) { + memcpy(s_lastRespBuf, s_txBuf, s_txLen); + s_lastRespLen = s_txLen; + s_lastReqSeq = hdr.tx_seq; + s_hasCachedResp = true; } s_isrResponded = true; @@ -386,6 +559,14 @@ void SpiOta::setSpiReady(bool ready) { } } +/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。 + * 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */ +static void updateDataReady() +{ + const bool has_data = (SpiOta::s_txPending && SpiOta::s_txLen > 0U) || SpiOta::s_ringPending; + SpiOta::setSpiReady(!has_data); +} + /* ============================================================ * BOOT 主动按铃:下拉 SPIready 请求 ESP32 来读 * ============================================================ */ @@ -412,7 +593,7 @@ bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len) s_ringPending = true; s_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; s_linkState.onRingStart(); - setSpiReady(false); + updateDataReady(); exitCritical(primask); return true; } @@ -429,7 +610,7 @@ void SpiOta::cancelRing() { s_txStreamPos = 0U; memset(s_txBuf, 0xFF, sizeof(s_txBuf)); s_linkState.onRingTimeout(); - setSpiReady(true); + updateDataReady(); exitCritical(primask); } @@ -470,18 +651,17 @@ void SpiOta::configureSpi0() { 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); + /* 接收改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR。 */ + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_RBNE); + spi_i2s_interrupt_disable(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); + + /* TX: SPI0 → memory? no, memory → SPI0 */ dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); dma_parameter_struct dma_init_struct; dma_struct_para_init(&dma_init_struct); @@ -497,6 +677,22 @@ void SpiOta::configureDma() { 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); + + /* RX: SPI0 → s_rxStream */ + dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + 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_rxStream; + dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_init_struct.number = kRxBufSize; + 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_PERIPHERAL_TO_MEMORY; + dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_init_struct); + dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); } void SpiOta::configureExti() { @@ -509,18 +705,22 @@ void SpiOta::configureExti() { void SpiOta::configureSpiReadyExti() { /* EXTI6 上升沿:ESP32 releaseReady 通知 BOOT "读取完成"。 - * EXTI5_9_IRQn 已被 BootSleepManager 以 (0,0) 占用(PB8 深睡唤醒), - * 这里仅使能 EXTI_6 line,中断入口在 EXTI5_9_IRQHandler 中分发。 */ + * EXTI5_9_IRQn 已被 BootSleepManager 以 (0,0) 占用(PB8 深睡唤醒)。 + * 此处显式使能 EXTI5_9 NVIC 并设置抢占优先级 (3,1),低于 CS 中断 (1,1) + * 与 SPI0 RBNE (2,0),确保 Ready 上升沿不会打断正在进行的帧处理。 + * 中断入口在 EXTI5_9_IRQHandler 中分发到 onSpiReadyExti()。 */ gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6); exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING); exti_interrupt_enable(EXTI_6); exti_interrupt_flag_clear(EXTI_6); + nvic_irq_enable(EXTI5_9_IRQn, 3U, 1U); } void SpiOta::onSpiReadyExti() { + /* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。 + * 仅清理可能残留的挂起标志,避免 NVIC 重复进入。 */ if (exti_interrupt_flag_get(EXTI_6) != RESET) { exti_interrupt_flag_clear(EXTI_6); - s_linkState.onEsp32Complete(); } } @@ -533,12 +733,26 @@ void SpiOta::init() { s_otaSessionActive = false; memset(s_rxStream, 0, sizeof(s_rxStream)); memset(s_txBuf, 0xFF, sizeof(s_txBuf)); + /* v2 链路状态复位。 */ + s_sessionId = 0U; + s_txSeq = 0U; + s_expectedRxSeq = 0U; + s_v2Active = false; + s_lastReqSeq = 0xFFU; + s_lastRespLen = 0U; + s_hasCachedResp = false; + memset(s_lastRespBuf, 0, sizeof(s_lastRespBuf)); + s_otaDataWi = 0U; + s_otaDataRi = 0U; + s_otaDataCnt = 0U; configureGpio(); configureDma(); configureSpi0(); configureExti(); configureSpiReadyExti(); s_linkState.reset(); + /* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK */ + armSpiDmasForNextCs(); } /* ============================================================ @@ -548,117 +762,113 @@ void SpiOta::init() { void SpiOta::onCsAssert() { if (s_csActive) { return; } s_csActive = true; - /* CS 下降沿立即拉低 PB6:表示从机即将忙碌, - * 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。 - * (若此帧为空轮询/残包,onCsRelease 会恢复 HIGH) - * 与 DCU_APP AppSpiCommand::onCsAssert 行为一致。 */ - setSpiReady(false); - s_rxStreamLen = 0U; + /* CS 下降:DMA 已在空闲期预装,此处只标记忙碌。 + * v2 DATA_READY: 不在此处操作 PB6,CS 本身即互斥, + * PB6 电平完全由队列状态(updateDataReady)决定。 + * 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK。 */ s_txStreamPos = 0U; s_isrResponded = false; - - /* PA15 仅作为事务边界信号;这里重装 DMA 并清理旧状态。 */ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); - - /* 覆盖 TBE 里的旧字节,避免下一次事务先吐出残留 ACK。 */ - spi_i2s_data_transmit(SPI0, 0xFFU); - if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) { - spi_i2s_data_transmit(SPI0, 0xFFU); - } - - /* 只清错误标志,不主动消费 RBNE。 */ - 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 */ - 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; } + if (!s_csActive) { + /* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步 */ + s_rxStreamLen = stopSpiDmas(); + drainSpiRxHw(); + /* 本 CS 已输出预装 TX:先标记交付,避免 pending 永久为 true。 */ + if (s_txPending && s_txLen > 0U) { + s_txPending = false; + s_txLen = 0U; + s_txStreamPos = 0U; + } + if (s_rxStreamLen >= V2_OVERHEAD && !s_isrResponded) { + isrProcessFrame(); + } + armSpiDmasForNextCs(); + updateDataReady(); + return; + } s_csActive = false; - /* 内部 NSS 保持常 LOW;这里只停止 DMA。 */ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + /* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable) */ + s_rxStreamLen = stopSpiDmas(); + drainSpiRxHw(); - 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; - } - } - - - - /* 如果这是 ESP32 响应 BOOT 主动按铃来读请求帧,直接清空按铃状态并释放 ready。 - * 按铃帧已在 CS 期间通过 DMA 输出到 MISO,不需要再解析本次接收到的 dummy 字节。 */ - if (s_ringPending) { - s_ringPending = false; - s_ringTxLen = 0U; + /* ★ 关键:本 CS 已时钟输出预装的 TX 帧(延迟一拍响应)。 + * armSpiDmasForNextCs() 装载后并不清除 s_txPending,若此处不标记交付, + * pending 会永远为 true → updateDataReady() 把 PB6 永久拉低 → + * ESP32 waitSpiReady(等 HIGH) 每包空等 SPI_READY_TIMEOUT_MS, + * 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_rxStreamLen = 0U; - s_isrResponded = true; - memset(s_txBuf, 0xFF, sizeof(s_txBuf)); - setSpiReady(true); - return; + } + + /* 按铃中若 ESP32 发的是 v2 协议帧(HELLO/OTA 等),不能当 dummy 读吞掉 */ + if (s_ringPending) { + bool is_v2_frame = false; + if (s_rxStreamLen >= 2U) { + const u16 last = (s_rxStreamLen < 16U) ? (u16)(s_rxStreamLen - 1U) : 16U; + for (u16 i = 0U; i < last; ++i) { + if (s_rxStream[i] == V2_SOF0 && s_rxStream[i + 1U] == V2_SOF1) { + is_v2_frame = true; + break; + } + } + } + if (is_v2_frame) { + s_ringPending = false; + s_ringTxLen = 0U; + /* 保留 s_txBuf 由 process 覆盖;pending 已在上方清掉 */ + s_txStreamPos = 0U; + /* v2: ESP32 CS 完成即确认读走,补充 onEsp32Complete 信号 + * (ESP32 不再操作 PB6,EXTI6 上升沿信号源消失)。 */ + s_linkState.onEsp32Complete(); + /* 继续 isrProcessFrame */ + } else { + 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)); + armSpiDmasForNextCs(); + updateDataReady(); + return; + } } if (s_rxStreamLen > 0U && !s_isrResponded) { isrProcessFrame(); + } else if (s_rxStreamLen == 0U) { + /* 诊断:预装 DMA 仍收 0 字节时打点(日志槽满则静默) */ + isrLogFrameEx("[RX0]", NULL, 0U); } - /* BOOT 在 ISR 内同步完成帧处理,响应帧已就绪(或为空轮询/残包无需响应), - * 拉高 PB6 通知主机可发起下一笔事务。 - * flash_buf 满(g_localCredit==0)时不拉高,等 drainFlashBuffer 写完 Flash - * 再拉高,形成硬件级背压,避免 ESP32 疯狂重发导致 overrun。 */ - if (g_localCredit > 0U) { - setSpiReady(true); - } + /* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */ + armSpiDmasForNextCs(); + + /* v2 DATA_READY: PB6 电平由队列状态决定。 + * flash_buf 满时 CREDIT=0 已通知 ESP32 停止发送,不再用 PB6 背压。 */ + updateDataReady(); } /* ============================================================ - * SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代 + * SPI0 中断处理 — RX 已改 DMA,保留空壳兼容旧 IRQ 向量 * ============================================================ */ void SpiOta::onSpiInterrupt() { + /* 遗留 ERR 标志清理(RBNE 已禁用) */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { - /* EXTI 略晚时,在首个 RBNE 到来时补做 onCsAssert()。 */ - 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; - } - /* 帧处理统一在 onCsRelease() (EXTI CS 释放中断) 里执行, - * 不在 RBNE 中断内调 isrProcessFrame(),避免 CRC32+AES+buildFrame - * 重操作(10-20us)期间 SPI 外设继续收字节导致 RXORERR。 */ + (void)spi_i2s_data_receive(SPI0); } - /* 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); + (void)SPI_STAT(SPI0); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); @@ -677,6 +887,13 @@ void SpiOta::onCsExtiInterrupt() { * 主循环接口 * ============================================================ */ +void SpiOta::servicePendingTx() { + if (!s_csActive && s_txPending && s_txLen > 0U) { + armSpiDmasForNextCs(); + updateDataReady(); + } +} + void SpiOta::poll() { if (s_isrResponded) { s_isrResponded = false; @@ -699,7 +916,28 @@ void SpiOta::poll() { } } - if (s_otaHandler != nullptr) { s_otaHandler->process(); } + if (s_otaHandler != nullptr) { + /* 先消化 ISR 入队的 64B DATA(AES+入 flash 槽),再 process 擦除/drain。 */ + u8 data_pkt[64]; + u8 drained = 0U; + while (drained < kOtaDataSlots && dequeueOtaData(data_pkt)) { + s_otaHandler->setTransportSender(sender); + s_otaHandler->handleLinkMessage(OTA_LINK_MSG_DATA, data_pkt, 64U); + if (s_txPending && s_txLen > 0U && s_txLen <= sizeof(s_lastRespBuf)) { + memcpy(s_lastRespBuf, s_txBuf, s_txLen); + s_lastRespLen = s_txLen; + s_hasCachedResp = true; + } + drained++; + } + s_otaHandler->process(); + } + + /* process()/DATA 处理可能在主循环排队 ACK;CS 空闲时 re-arm + 拉低 PB6。 */ + if (!s_csActive && s_txPending && s_txLen > 0U) { + armSpiDmasForNextCs(); + updateDataReady(); + } } void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; } @@ -734,6 +972,7 @@ u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) { void spi_ota_init(void) { SpiOta::init(); } void spi_ota_poll(void) { SpiOta::poll(); } +void spi_ota_service_pending_tx(void) { SpiOta::servicePendingTx(); } 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(); } diff --git a/GD32E50X_BOOT/device/SpiOta.h b/GD32E50X_BOOT/device/SpiOta.h index 2830d2b..28b9133 100644 --- a/GD32E50X_BOOT/device/SpiOta.h +++ b/GD32E50X_BOOT/device/SpiOta.h @@ -25,15 +25,16 @@ typedef enum { /** * @brief SPI 链路节奏控制状态机(与 DCU_APP AppSpiCommand::SpiLinkState 对齐)。 * - * BOOT 主动按铃(requestEsp32Read)后,必须等待 ESP32 读取完成 - * (SPIready/PB6 上升沿,由 EXTI6 捕获)后才允许发起下一帧, - * 避免下降沿被 ESP32 ISR 在非 IDLE 态丢弃。 + * v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿信号源消失。 + * canRingNow 总是返回 true,靠 requestEsp32Read 内部 s_ringPending + * 检查防止重复按铃;onEsp32Complete 改由 onCsRelease 的 ring pending + * 分支补充调用(ESP32 CS 完成即确认读走)。 */ class SpiLinkState { public: - bool canRingNow() const { return m_esp32Ready; } ///< 是否允许发起新按铃 + bool canRingNow() const { return true; } ///< v2: 总是允许(s_ringPending 已防重复) void onRingStart() { m_esp32Ready = false; } ///< 发起按铃时消费完成 flag - void onEsp32Complete() { m_esp32Ready = true; } ///< ESP32 releaseReady 上升沿触发(EXTI6 ISR 调用) + void onEsp32Complete() { m_esp32Ready = true; } ///< ESP32 CS 完成即确认读走(onCsRelease 调用) void onRingTimeout() { m_esp32Ready = true; } ///< 按铃超时取消时允许重试 void reset() { m_esp32Ready = true; } ///< 初始化(首帧允许) private: @@ -81,6 +82,9 @@ public: /** @brief 取消当前按铃请求,释放 SPIready。 */ static void cancelRing(); + /** @brief 主循环入队 TX 后装载 DMA + 更新 DATA_READY(擦除中途刷 WAIT 用)。 */ + static void servicePendingTx(); + /** @brief 翻转 SPI Ready 握手引脚(PB6)电平。 * @param[in] ready true=就绪(HIGH), false=忙(LOW)。 * @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。 @@ -130,6 +134,7 @@ extern "C" { void spi_ota_init(void); void spi_ota_poll(void); +void spi_ota_service_pending_tx(void); void spi_ota_set_sender(OTAHandler *handler); void spi_ota_on_cs_assert(void); void spi_ota_on_cs_release(void); diff --git a/GD32E50X_BOOT/main.cpp b/GD32E50X_BOOT/main.cpp index 3c63080..4c43809 100644 --- a/GD32E50X_BOOT/main.cpp +++ b/GD32E50X_BOOT/main.cpp @@ -333,14 +333,10 @@ void BootloaderSystem::run() { } int main(void) { - /* 方案A: 极早夺回 PB6 控制权,拉低 SPIready 线。 - * 复位后 GPIO 高阻,外部上拉会短暂拉高 PB6 (μs级); - * 此处尽快配为推挽输出 LOW,让 ESP32 能通过 LOW→HIGH 序列 - * 识别 "Bootloader 已接管" → "Bootloader 就绪"。 - * 后续 SpiOta::configureGpio() 会重配为开漏并释放 HIGH。 */ + /* v2 DATA_READY: 仅启用 GPIOB 时钟,不配 PB6 为推挽低。 + * PB6 由 SpiOta::configureGpio() 初始化为开漏高(释放态)。 + * 复位后 GPIO 高阻,靠外部上拉保持 HIGH,不再制造 LOW→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 9b90442..6fcefef 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -315,6 +315,7 @@ void OTAHandler::process() { printf("[OTA] erase done, send SPI ACK seq0\r\n"); #endif send_structured_ack(0U); + SpiOta::servicePendingTx(); } drainFlashBuffer(); @@ -582,26 +583,55 @@ void OTAHandler::eraseApplicationArea() { OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL); OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_NOT_AVAL); - __disable_irq(); + /* 只擦「本轮固件需要」的页,而不是整片 APP_MAX_SIZE(~108 页)。 + * 旧实现整片擦 + 全程关中断可达数秒:SPI 无响应,ESP32 卡在 SEND_SIZE WAIT。 + * 53KB 固件约 27 页,体感从数秒降到约 1 秒内。 */ + u32 erase_bytes = expected_size; + if (erase_bytes == 0U || erase_bytes > APP_MAX_SIZE) { + erase_bytes = APP_MAX_SIZE; + } + /* 向上对齐页;额外 +1 页余量,避免边界少擦 */ + u32 erase_pages = (erase_bytes + FMC_PAGE_SIZE - 1U) / FMC_PAGE_SIZE; + if (erase_pages < (APP_MAX_SIZE / FMC_PAGE_SIZE)) { + erase_pages += 1U; + } + u32 erase_end = APPLATEST_START_ADDR + erase_pages * FMC_PAGE_SIZE; + if (erase_end > (APPLATEST_START_ADDR + APP_MAX_SIZE)) { + erase_end = APPLATEST_START_ADDR + APP_MAX_SIZE; + } + fmc_unlock(); #if DEBUG_PRINT - printf("[FW] E app area 0x%08lX ~ 0x%08lX (%lu pages)\r\n", + printf("[FW] E app 0x%08lX ~ 0x%08lX (%lu pages, fw=%lu)\r\n", (unsigned long)APPLATEST_START_ADDR, - (unsigned long)(APPLATEST_START_ADDR + APP_MAX_SIZE), - (unsigned long)((APP_MAX_SIZE + FMC_PAGE_SIZE - 1U) / FMC_PAGE_SIZE)); + (unsigned long)erase_end, + (unsigned long)((erase_end - APPLATEST_START_ADDR) / FMC_PAGE_SIZE), + (unsigned long)expected_size); #endif - for (u32 addr = APPLATEST_START_ADDR; - addr < (APPLATEST_START_ADDR + APP_MAX_SIZE); addr += FMC_PAGE_SIZE) { + u32 page_i = 0U; + for (u32 addr = APPLATEST_START_ADDR; addr < erase_end; addr += FMC_PAGE_SIZE) { + /* 仅在单页擦除窗口关中断,避免整片擦时 SPI/SysTick 假死 */ + __disable_irq(); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGAERR | FMC_FLAG_PGERR); fmc_state_enum status = fmc_page_erase(addr); + __enable_irq(); if (status != FMC_READY) { ota_error = OTA_ERROR_FLASH_ERROR; + ota_bind_spi_transport(); send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR); break; } + ++page_i; + /* 每 8 页刷一次 WAIT,让 ESP32 SEND_SIZE 轮询看到“仍在忙”而非全 0xFF */ + if ((page_i & 7U) == 0U) { + ota_bind_spi_transport(); + u8 wait_hint[1] = {0x02}; + send_wait_frame(1, wait_hint); + /* process() 内嵌擦除时 poll 尚未 re-arm,这里立刻装 DMA */ + SpiOta::servicePendingTx(); + } } fmc_lock(); - __enable_irq(); } /** @@ -619,10 +649,9 @@ void OTAHandler::drainFlashBuffer() { 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); + /* 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; @@ -651,13 +680,18 @@ void OTAHandler::drainFlashBuffer() { drain_cnt++; #endif } - /* Flash 写入完成,恢复 CREDIT 并释放 PB6。 - * ESP32 检测到 PB6 拉高后会继续发数据包。 */ + /* Flash 写入完成,恢复 CREDIT 通知 ESP32 可继续发数据。 + * v2 DATA_READY: 不再操作 PB6,PB6 由队列状态自动维护。 */ g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt); - SpiOta::setSpiReady(true); #if DEBUG_PRINT + /* 每累计 10 包打一次,避免 OTA 过程刷屏。 */ + static u32 s_drain_since_log = 0U; if (drain_cnt > 0U) { - printf("[FW] drain %u pkts\r\n", (unsigned)drain_cnt); + 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; + } } #endif } diff --git a/GD32E50X_BOOT/sys/initial.cpp b/GD32E50X_BOOT/sys/initial.cpp index 0c194c8..f54fae4 100644 --- a/GD32E50X_BOOT/sys/initial.cpp +++ b/GD32E50X_BOOT/sys/initial.cpp @@ -270,8 +270,10 @@ void BootSleepManager::onExtiInterrupt(void) /** EXTI_6 (PB6 SPIready) 上升沿:通知 SpiOta 状态机 ESP32 已完成读取。 */ SpiOta::onSpiReadyExti(); - /** 继续分发给 SPI OTA 的 CS 边沿处理逻辑。 */ - spi_ota_cs_exti_handler(); + /* 注意:SPI OTA 的 CS 边沿信号 PA15 映射到 EXTI_15,属于 EXTI10_15_IRQn, + * 由 EXTI10_15_IRQHandler 直接分发到 SpiOta::onCsExtiInterrupt(), + * 不应在此处(EXTI5_9_Handler)调用 spi_ota_cs_exti_handler()。 + * 该调用属于死代码,会导致 PA15 边沿在错误的 ISR 中被重复处理。 */ } /**