From 0b086d4b8629a1b93464bc3d1fe82567cf649ffc Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 3 Jun 2026 03:13:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor(spi):=20=E4=BC=98=E5=8C=96SPI=20OTA?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的调试相关变量和函数,包括s_isrLogPending、s_isrLogBuf等 - 简化注释内容,保留核心功能说明 - 优化CS信号处理逻辑,提高代码可读性 - 修正帧处理中的竞态条件问题,提前设置响应标志避免重复处理 feat(boot): 添加BOOT固件版本号管理功能 - 在main.cpp中添加版本号写入逻辑,将BOOT版本号存储到OTA配置页 - 更新OTA配置页结构,从6个word扩展到8个word,增加版本号存储空间 - 实现版本变更检测机制,仅在版本更新时重写配置页 - 优化SpiOta模块的日志系统,移除多余的调试日志和缓冲区管理代码 --- GD32E50X_BOOT/device/SpiOta.cpp | 125 +++----------------------------- GD32E50X_BOOT/device/SpiOta.h | 3 - GD32E50X_BOOT/main.cpp | 56 ++++++++++++++ GD32E50X_BOOT/sys/OTA.cpp | 6 +- GD32E50X_BOOT/sys/config.h | 15 +++- 5 files changed, 83 insertions(+), 122 deletions(-) diff --git a/GD32E50X_BOOT/device/SpiOta.cpp b/GD32E50X_BOOT/device/SpiOta.cpp index 42ee1cc..94ddb26 100644 --- a/GD32E50X_BOOT/device/SpiOta.cpp +++ b/GD32E50X_BOOT/device/SpiOta.cpp @@ -26,11 +26,10 @@ #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; @@ -47,10 +46,7 @@ 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 / 帧构建 / 帧解析 @@ -155,12 +151,7 @@ void SpiOta::tryProcessCompleteFrame() { u16 rx_len = s_rxStreamLen; if (rx_len == 0U) { return; } - /* Raw 0xB0 BOOT 命令是单字节事务。 - * 不要在 CS 仍为低时仅凭“首字节 == 0xB0”就立刻处理, - * 否则一旦事务错位/丢首字节,普通 OTA 数据流里的某个随机字节也可能 - * 被误判成 BOOT 命令,直接把会话打回 IDLE。 - * - * 交给 onCsRelease() 在事务结束后再按“完整单字节事务”判定。 */ + /* 单字节 BOOT 命令留到事务结束后再判定。 */ if (s_rxStream[0] == SPI_OTA_CMD_BOOT) { return; } if (s_rxStream[0] != kSof) { return; } @@ -182,22 +173,11 @@ 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) - * - isrLogFrameEx("[RX-RAW]", s_rxStream, (rx_len < 16U) ? rx_len : 16U); - * 所以必须在函数入口就先置位,防止 REQ/DATA 被双重分发。 */ + /* 提前置位,避免同一事务被重复处理。 */ s_isrResponded = true; if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { if (s_otaHandler != nullptr && s_otaHandler->isActive()) { - isrLogFrameEx("[BOOT-CMD-IGN]", s_rxStream, rx_len); s_rxStreamLen = 0U; return; } @@ -206,7 +186,6 @@ void SpiOta::isrProcessFrame() { if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } s_otaSessionActive = false; s_rxStreamLen = 0U; - isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen); s_isrResponded = true; return; } @@ -241,7 +220,6 @@ void SpiOta::isrProcessFrame() { if (frame_type == SPI_OTA_CMD_BOOT) { if (s_otaHandler != nullptr && s_otaHandler->isActive()) { - isrLogFrameEx("[BOOT-FR-IGN]", s_rxStream, (expected < 24U) ? expected : 24U); s_rxStreamLen = 0U; s_isrResponded = true; return; @@ -292,7 +270,6 @@ static void isrLogFrameEx(const char *tag, const u8 *data, u16 len) { buf[pos++] = (u8)hx[data[i] & 0x0FU]; } buf[pos] = 0U; - s_logSlotLens[s_logWriteIdx] = pos; s_logWriteIdx = (s_logWriteIdx + 1U) % kLogSlotCount; s_logCount++; } @@ -393,7 +370,6 @@ void SpiOta::init() { s_txStreamPos = 0U; 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)); @@ -414,68 +390,17 @@ void SpiOta::onCsAssert() { 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 */ + /* PA15 仅作为事务边界信号;这里重装 DMA 并清理旧状态。 */ 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 推出的 - * 正确帧开始匹配。 - */ + /* 覆盖 TBE 里的旧字节,避免下一次事务先吐出残留 ACK。 */ 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”。 - * - * 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。 - * 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。 - * 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5 - * 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。 - * - * 所以这里仅清真正的错误标志,不主动消费 RBNE。 - * 已到达的首字节留给 onSpiInterrupt() 正常读走。 */ + /* 只清错误标志,不主动消费 RBNE。 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); } @@ -483,7 +408,7 @@ void SpiOta::onCsAssert() { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); } - /* 3. 配置并启动 DMA(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */ + /* 3. 配置并启动 DMA */ if (s_txPending && s_txLen > 0U) { spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); startSpiTxDma(s_txBuf, s_txLen); @@ -502,12 +427,7 @@ 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 推出。 */ + /* 内部 NSS 保持常 LOW;这里只停止 DMA。 */ DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); @@ -530,13 +450,7 @@ void SpiOta::onCsRelease() { 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 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */ + /* EXTI 略晚时,在首个 RBNE 到来时补做 onCsAssert()。 */ if (!s_csActive && s_rxStreamLen == 0U && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } @@ -545,11 +459,6 @@ void SpiOta::onSpiInterrupt() { if (s_rxStreamLen < kRxBufSize) { s_rxStream[s_rxStreamLen++] = byte; } - /* 调试阶段先关闭 RBNE 首字节日志。 - * 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位, - * onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。 - * 目前排查重点是“本次 CS 周期总共收到了哪些字节”, - * 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */ tryProcessCompleteFrame(); } /* TBE 已由 DMA 替代,不再处理 */ @@ -578,28 +487,14 @@ 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_logReadIdx = (s_logReadIdx + 1U) % kLogSlotCount; s_logCount--; __enable_irq(); - printf("[SPI] %s\r\n", buf); } if (s_otaHandler != nullptr) { s_otaHandler->process(); } - - /* 调试:定期打印 SPI 从机状态 — 降低频率, 每20000次才打印一次 */ - static u32 dbg_tick = 0; - dbg_tick++; - 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_txLen, - (unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF)); - } } void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; } diff --git a/GD32E50X_BOOT/device/SpiOta.h b/GD32E50X_BOOT/device/SpiOta.h index 5de3722..f65e905 100644 --- a/GD32E50X_BOOT/device/SpiOta.h +++ b/GD32E50X_BOOT/device/SpiOta.h @@ -59,10 +59,7 @@ public: static volatile u16 s_txLen; static volatile bool s_txPending; static volatile bool s_isrResponded; - static volatile bool s_isrLogPending; static volatile bool s_otaSessionActive; - static u8 s_isrLogBuf[kIsrLogBufSize]; - static u16 s_isrLogLen; static void sender(ota_link_tx_type_t type, u8 len, const u8 *data); diff --git a/GD32E50X_BOOT/main.cpp b/GD32E50X_BOOT/main.cpp index e305f34..5288482 100644 --- a/GD32E50X_BOOT/main.cpp +++ b/GD32E50X_BOOT/main.cpp @@ -51,6 +51,62 @@ void BootloaderSystem::initHardware() { /** 让 SPI OTA 模块在收到完整帧后,转交给 ota 实例处理。 */ SpiOta::setSender(&ota); + /** 写入 BOOT 版本号到 OTA 配置页(仅当版本变更时重写)。 + * 采用与 OTA_SET 一致的读-擦-写策略,但将 word6/word7 合并为单次页面擦除, + * 避免两次 OTA_SET 调用导致的双重擦除。 */ + { + u32 ver_word0 = (u32)((BOOT_FW_VERSION_MAJOR & 0xFFFFU) | ((BOOT_FW_VERSION_MINOR & 0xFFFFU) << 16)); + u32 ver_word1 = (u32)((BOOT_FW_VERSION_PATCH & 0xFFFFU) | ((BOOT_FW_VERSION_BUILD & 0xFFFFU) << 16)); + volatile u32 *cur0 = (volatile u32 *)n_BOOT_VERSION_ADDR0; + volatile u32 *cur1 = (volatile u32 *)n_BOOT_VERSION_ADDR1; + if (*cur0 != ver_word0 || *cur1 != ver_word1) { + /** 版本变更:先读取整页 8 个配置字,再擦除整页后一次性回写。 + * 读取逻辑与 app_spi_command.cpp 中 BOOT 版本读取方式一致。 */ + u32 temp_val[OTA_CONFIG_NUM]; + volatile u32 *flash_ptr; + for (int i = 0; i < OTA_CONFIG_NUM; ++i) { + flash_ptr = (volatile u32 *)(OTA_CONFIG_PAGE_ADDR + 4U * (u32)i); + temp_val[i] = *flash_ptr; + } + /** 用新版本号替换 word6/word7。 */ + temp_val[6] = ver_word0; + temp_val[7] = ver_word1; + + fmc_unlock(); + fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGAERR | FMC_FLAG_PGERR); + fmc_state_enum fmc_status = fmc_page_erase(OTA_CONFIG_PAGE_ADDR); +#if DEBUG_PRINT + printf("[FW] E page=0x%08lX\r\n", (unsigned long)OTA_CONFIG_PAGE_ADDR); +#endif + if (fmc_status == FMC_READY) { + for (int i = 0; i < OTA_CONFIG_NUM; ++i) { + if (temp_val[i] != OTA_NOT_AVAL) { + u32 current_addr = OTA_CONFIG_PAGE_ADDR + 4U * (u32)i; + fmc_status = fmc_word_program(current_addr, temp_val[i]); +#if DEBUG_PRINT + printf("[FW] W addr=0x%08lX data=0x%08lX\r\n", + (unsigned long)current_addr, (unsigned long)temp_val[i]); +#endif + if (fmc_status != FMC_READY) { +#if DEBUG_PRINT + printf("[BOOT] version write FAILED at word %d\r\n", i); +#endif + break; + } + } + } + } + fmc_lock(); +#if DEBUG_PRINT + if (fmc_status == FMC_READY) { + printf("[BOOT] version written: v%u.%u.%u.%u\r\n", + BOOT_FW_VERSION_MAJOR, BOOT_FW_VERSION_MINOR, + BOOT_FW_VERSION_PATCH, BOOT_FW_VERSION_BUILD); + } +#endif + } + } + /** 检查是否需要进入深度睡眠模式。 */ { const int sleep_state = BootSleepManager::check(); diff --git a/GD32E50X_BOOT/sys/OTA.cpp b/GD32E50X_BOOT/sys/OTA.cpp index b5aed5a..9c920ad 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -152,7 +152,7 @@ u8 OTA_SET(u32 addr, u32 data) { } } - /** 读取当前配置页中的全部状态,供擦除后重写。 */ + /** 读取当前配置页中的全部状态(含 BOOT 版本 word 6/7),供擦除后重写。 */ flash_ptr = (volatile u32 *)n_APP_LATEST_AVAL_ADDR; temp_val[0] = *flash_ptr; flash_ptr = (volatile u32 *)n_APP_BACKUP_AVAL_ADDR; @@ -165,6 +165,10 @@ u8 OTA_SET(u32 addr, u32 data) { temp_val[4] = *flash_ptr; flash_ptr = (volatile u32 *)n_APP_BACKUP_CHECK_ADDR; temp_val[5] = *flash_ptr; + flash_ptr = (volatile u32 *)n_BOOT_VERSION_ADDR0; + temp_val[6] = *flash_ptr; + flash_ptr = (volatile u32 *)n_BOOT_VERSION_ADDR1; + temp_val[7] = *flash_ptr; /** 整页擦除后重建配置页。 */ fmc_unlock(); diff --git a/GD32E50X_BOOT/sys/config.h b/GD32E50X_BOOT/sys/config.h index bb7f4e0..f89d4a1 100644 --- a/GD32E50X_BOOT/sys/config.h +++ b/GD32E50X_BOOT/sys/config.h @@ -11,6 +11,12 @@ typedef uint32_t u32; /** 调试打印总开关。置 1 时所有 printf 调试输出编译进去;置 0 时完全剔除,零开销。 */ #define DEBUG_PRINT 1 +/* BOOT 固件版本号 */ +#define BOOT_FW_VERSION_MAJOR 1U +#define BOOT_FW_VERSION_MINOR 0U +#define BOOT_FW_VERSION_PATCH 0U +#define BOOT_FW_VERSION_BUILD 1U + /** * Flash 布局说明(GD32E508VE,512KB) * ----------------------------------- @@ -40,16 +46,19 @@ inline constexpr u32 SLEEP_ENABLE = 0x11111111U; inline constexpr u32 SLEEP_DISABLE = 0x00000000U; /** - * OTA 配置页一共使用 6 个 word,分别保存: - * latest_aval / backup_aval / latest_en / backup_en / latest_check / backup_check + * OTA 配置页一共使用 8 个 word: + * word 0~5: latest_aval / backup_aval / latest_en / backup_en / latest_check / backup_check + * word 6~7: BOOT 版本号(major|minor / patch|build) */ -inline constexpr u32 OTA_CONFIG_NUM = 6U; +inline constexpr u32 OTA_CONFIG_NUM = 8U; inline constexpr u32 n_APP_LATEST_AVAL_ADDR = OTA_CONFIG_PAGE_ADDR + 0x00U; inline constexpr u32 n_APP_BACKUP_AVAL_ADDR = OTA_CONFIG_PAGE_ADDR + 0x04U; inline constexpr u32 n_APP_LATEST_EN_ADDR = OTA_CONFIG_PAGE_ADDR + 0x08U; inline constexpr u32 n_APP_BACKUP_EN_ADDR = OTA_CONFIG_PAGE_ADDR + 0x0CU; inline constexpr u32 n_APP_LATEST_CHECK_ADDR = OTA_CONFIG_PAGE_ADDR + 0x10U; inline constexpr u32 n_APP_BACKUP_CHECK_ADDR = OTA_CONFIG_PAGE_ADDR + 0x14U; +inline constexpr u32 n_BOOT_VERSION_ADDR0 = OTA_CONFIG_PAGE_ADDR + 0x18U; /**< BOOT版本 word0: major|minor */ +inline constexpr u32 n_BOOT_VERSION_ADDR1 = OTA_CONFIG_PAGE_ADDR + 0x1CU; /**< BOOT版本 word1: patch|build */ /** * 本项目把 0x00000000 视为“有效/可用”,