/** * @file ota_boot_request.cpp * @brief DCU_APP 进入 Bootloader OTA 窗口控制实现。 */ #include "sys/ota_boot_request.h" #include "gd32e50x.h" #include "sys/config.h" #include "sys/systick.h" namespace { /** Bootloader OTA 配置页地址,与 BOOT 工程 sys/config.h 保持一致。 */ constexpr uint32_t kOtaConfigPageAddr = 0x0807E000U; constexpr uint32_t kAppLatestAvalAddr = kOtaConfigPageAddr + 0x00U; constexpr uint32_t kAppLatestEnAddr = kOtaConfigPageAddr + 0x08U; constexpr uint32_t kAppLatestCheckAddr = kOtaConfigPageAddr + 0x10U; /** BOOT 版本号在 OTA 配置页中的存储地址:word6=major|minor, word7=patch|build。 */ constexpr uint32_t kBootVersionAddr0 = kOtaConfigPageAddr + 0x18U; constexpr uint32_t kBootVersionAddr1 = kOtaConfigPageAddr + 0x1CU; constexpr uint32_t kOtaAval = 0x00000000U; constexpr uint32_t kOtaNotAval = 0xFFFFFFFFU; constexpr uint32_t kFmcPageSize = 0x00000800U; constexpr uint32_t kRebootDelayMs = 20U; static bool writeWord(uint32_t addr, uint32_t value) { fmc_unlock(); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGAERR | FMC_FLAG_PGERR); fmc_state_enum status = fmc_word_program(addr, value); fmc_lock(); return status == FMC_READY; } /** * @brief 按 Bootloader OTA_SET 语义更新配置页中单个 word。 * @note Flash 只能 1->0;需要写回 0xFFFFFFFF 时必须擦整页再恢复其他字段。 */ static bool otaConfigSet(uint32_t targetAddr, uint32_t value) { if(targetAddr < kOtaConfigPageAddr || targetAddr >= (kOtaConfigPageAddr + kFmcPageSize)) { return false; } const uint32_t current = *((volatile uint32_t *)targetAddr); if(current == value) { return true; } if(value != kOtaNotAval) { return writeWord(targetAddr, value); } uint32_t words[CONFIG_OTA_CONFIG_WORD_COUNT]; for(uint32_t i = 0U; i < CONFIG_OTA_CONFIG_WORD_COUNT; ++i) { words[i] = *((volatile uint32_t *)(kOtaConfigPageAddr + i * 4U)); } words[(targetAddr - kOtaConfigPageAddr) / 4U] = value; fmc_unlock(); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGAERR | FMC_FLAG_PGERR); fmc_state_enum status = fmc_page_erase(kOtaConfigPageAddr); if(status != FMC_READY) { fmc_lock(); return false; } bool ok = true; for(uint32_t i = 0U; i < CONFIG_OTA_CONFIG_WORD_COUNT; ++i) { if(words[i] == kOtaNotAval) { continue; } status = fmc_word_program(kOtaConfigPageAddr + i * 4U, words[i]); if(status != FMC_READY) { ok = false; break; } } fmc_lock(); return ok; } static bool requestMagicValid(const uint8_t *data, uint8_t length) { return data != nullptr && length >= 4U && data[0] == CONFIG_CAN_OTA_BOOT_MAGIC0 && data[1] == CONFIG_CAN_OTA_BOOT_MAGIC1 && data[2] == CONFIG_CAN_OTA_BOOT_MAGIC2 && data[3] == CONFIG_CAN_OTA_BOOT_MAGIC3; } } // namespace /** @brief 已写入 OTA 标志并等待软复位的挂起标志。 */ volatile bool OtaBootRequest::s_rebootPending = false; volatile bool OtaBootRequest::s_bootRequestPending = false; volatile bool OtaBootRequest::s_spiFrozenForBoot = false; /** * @brief 初始化进入 Bootloader 请求模块的运行态。 */ void OtaBootRequest::init() { s_rebootPending = false; s_bootRequestPending = false; s_spiFrozenForBoot = false; } BootVersion OtaBootRequest::readBootVersion() { /* BOOT 启动时将自身版本写入 OTA 配置页 word6/word7, * 字布局:word6=[15:0]major|[31:16]minor, word7=[15:0]patch|[31:16]build。 * Flash 未写入(擦除态 0xFFFFFFFF)时各字段返回 0xFF,调用方可据此判断有效性。 */ const volatile uint32_t *bv0 = (const volatile uint32_t *)kBootVersionAddr0; const volatile uint32_t *bv1 = (const volatile uint32_t *)kBootVersionAddr1; const uint32_t w0 = *bv0; const uint32_t w1 = *bv1; return BootVersion{ (uint8_t)(w0 & 0xFFU), (uint8_t)((w0 >> 16) & 0xFFU), (uint8_t)(w1 & 0xFFU), (uint8_t)((w1 >> 16) & 0xFFU) }; } /** * @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。 * @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。 */ bool OtaBootRequest::requestEnterBoot() { bool ok = true; ok = otaConfigSet(kAppLatestCheckAddr, kOtaAval) && ok; ok = otaConfigSet(kAppLatestAvalAddr, kOtaAval) && ok; if(ok) { ok = otaConfigSet(kAppLatestEnAddr, kOtaNotAval); } if(ok) { s_rebootPending = true; } else { /* 写配置页失败则永远停在 APP,ESP32 会看到持续 role=1 */ /* 无 printf 依赖:由调用方 process() 恢复 SPI 后继续运行 */ } return ok; } /** * @brief 异步请求进入 Bootloader(ISR 安全)。 * @note 仅设置 pending 标志,Flash 操作和复位由 process() 在主循环执行。 * 用于 SPI EXTI ISR 内的 0xB0 命令处理,避免 Flash 擦除阻塞 SPI 中断。 */ void OtaBootRequest::requestEnterBootAsync() { s_bootRequestPending = true; /* 立即冻结 SPI 业务:不必等 process()(原依赖 1Hz 心跳,最坏拖 ~1s)。 * 否则 ESP32 紧跟着 HELLO/0xC1 会继续进 processCommand,干扰擦页/复位。 */ s_spiFrozenForBoot = true; } /** * @brief 处理备用 CAN 入口的进入 Bootloader 请求。 * @note 当前主路径是 SPI 命令;CAN 入口只负责识别魔术字并触发同一套标志写入逻辑。 */ bool OtaBootRequest::handleCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t length) { if(id != CONFIG_CAN_OTA_BOOT_REQ_ID) { return false; } if(!requestMagicValid(data, length)) { return true; } (void)channel; const bool ok = requestEnterBoot(); (void)ok; return true; } /** * @brief 在主循环任务中执行异步 Flash 操作和短延迟软复位。 * @note 不在 SPI/CAN 中断里直接复位,避免应答还没有发出就重启。 * 异步路径:SPI ISR 收到 0xB0 后只设标志,Flash 操作(可能擦页 20-30ms) * 在此主循环执行,避免阻塞 SPI 中断导致 ESP32 ACK 轮询丢字节。 */ void OtaBootRequest::process() { /* 异步处理 0xB0:主循环写 Flash(可能擦页 20–30ms)。 * 期间冻结 SPI 业务处理,避免 ESP32 紧跟着 HELLO/按铃读 打断擦写。 */ if(s_bootRequestPending) { s_bootRequestPending = false; s_spiFrozenForBoot = true; nvic_irq_disable(SPI0_IRQn); nvic_irq_disable(EXTI10_15_IRQn); const bool ok = requestEnterBoot(); if(!ok) { s_spiFrozenForBoot = false; nvic_irq_enable(SPI0_IRQn, 2U, 0U); nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 1.6.32: 与 configureExti 一致提到抢占级 0 */ /* requestEnterBoot 失败时 s_rebootPending 仍为 false */ return; } s_rebootPending = true; } if(!s_rebootPending) { return; } s_rebootPending = false; s_spiFrozenForBoot = true; nvic_irq_disable(SPI0_IRQn); nvic_irq_disable(EXTI10_15_IRQn); /* v2 DATA_READY: Boot 切换不再用 PB6 通知对端, * BOOT 起机后再由 HELLO role=boot 确认。 */ /* SysTick 中断仍开,delayMs 可用;SPI 相关 IRQ 已关。 */ SysTickTimer::delayMs(kRebootDelayMs); /* 复位前最终关中断:必须用 PRIMASK 全屏蔽(非 BASEPRI),确保 NVIC_SystemReset 过程中无任何中断嵌套 */ __disable_irq(); NVIC_SystemReset(); while(1) { } } /*-------------------------------------------------------------------------- * EtherCAT CoE 0x2502 的入口(comm/ssc/port/dc_app.c 是 C 文件,调不到类静态方法, * 与 EcatPdo_* 同一形状)。写 1 走的是和 SPI 0xB0 完全相同的那条异步链: * 这里只置标志,配置页三字的 Flash 写入 + NVIC_SystemReset 在主循环 * OtaBootRequest::process() 下一圈做 —— SDO 响应先发、复位后到, * 不会把 SM1 半包留在总线上。 *-------------------------------------------------------------------------*/ extern "C" void EcatEnterBootRequestAsync(void) { OtaBootRequest::requestEnterBootAsync(); }