From 77354fd2a84dab403869dd88d773913886475ad7 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sun, 20 Sep 2026 22:58:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(boot):=201.1.0.22=20CoE=20=E8=B7=B3?= =?UTF-8?q?=E5=9B=9E=20APP=20+=20=E8=BF=9E=E7=BB=AD=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=E9=97=A8=20+=20warm=20jump=20=E6=A0=B9=E6=B2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更点: 1. CoE 0x2503「GD32 jump to app」 - 主站写 1 → requestJumpToApp:向量/CRC 校验后置 CHECK/AVAL/EN,process() 在 SDO 响应后跳 - 与 APP 0x2502 对称;crash_gate 只禁自动跳,人工跳仍允许 2. 连续 FWDGT 崩溃门(配置页 word9) - 复位 crash_count+1;达 OTA_LAYOUT_CRASH_STAY_MAX(3) 后关闭全部自动跳 APP - APP 喂狗稳定后再清 0(不在 init 立刻清) 3. warm jump 卡死 - bootEcatDeinit 去掉 LAN9252 writeReg32(INT_EN/IRQ_CFG):HBI 不返回会锁死 AHB - 跳转前有界等 USART TC,避免 handoff 半截 + APP 乱码 4. EtherCAT 邮箱空闲限速 5ms;delayMs 改 WFI,BOOT 驻留不空转烧 CPU 5. ota_layout.h vendored 进仓库(GD32E50X_BOOT/shared/),include 改 ../shared/ 6. .gitignore:firmware_keep/、评审 HTML、IDE 过程目录 --- .gitignore | 14 ++++- GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp | 66 ++++++++++++++++++++-- GD32E50X_BOOT/comm/ecat/boot_ecat_hw.cpp | 22 ++++---- GD32E50X_BOOT/main.cpp | 63 +++++++++++++++++---- GD32E50X_BOOT/shared/ota_layout.h | 59 +++++++++++++++++++ GD32E50X_BOOT/sys/OTA.cpp | 43 ++++++++++++++ GD32E50X_BOOT/sys/OTA.h | 9 +++ GD32E50X_BOOT/sys/config.h | 69 +++++++++++++++-------- GD32E50X_BOOT/sys/systick.cpp | 8 ++- 9 files changed, 300 insertions(+), 53 deletions(-) create mode 100644 GD32E50X_BOOT/shared/ota_layout.h diff --git a/.gitignore b/.gitignore index 672b633..ba98cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Prerequisites +# Prerequisites *.d # Object files @@ -85,3 +85,15 @@ uv2csolution.log # legacy intermediate project file *.cprj +# 本机固件留存与评审过程稿 +firmware_keep/ +SECURITY_AUDIT_REPORT.* +*.html + +# IDE / 过程产物 +.trae/ +.agents/ +.worktrees/ +.qoder/ +.zcode/ + diff --git a/GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp b/GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp index 9bda698..e504311 100644 --- a/GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp +++ b/GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp @@ -20,6 +20,7 @@ * 所以主站**只能靠 0x1008 的 "GD32E508 DCU BOOT" 字符串**辨认这是引导程序。 * 0x2500 会话控制(REC of UINT32) * 0x2501 数据通道(ARR of UINT8,BitSize = 4096*8,写回调落 flash) + * 0x2503 跳回 APP(VAR UINT32,写 1 触发;与 0x2502 对称,方向相反) */ #include "ecat_def.h" @@ -118,6 +119,23 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asDlChunkEntryDesc[] = { }; static OBJCONST UCHAR aName0x2501[] = "GD32 firmware image chunk"; +/*============================================================================ + * 0x2503 跳回 APP(主站 → BOOT:「让你跳回应用」) + * + * 与 APP 的 0x2502 对称:写 1 只置 pending,真正跳转由 process() 在 + * SDO 响应发出之后做,不会把 SM1 半包留在总线上。 + * VAR 对象只能写 :00;条目必须 ACCESS_READWRITE 才过 objdef 写权限门。 + *==========================================================================*/ + +static UINT32 s_jumpAppReq = 0U; + +OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2503[] = { + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, +}; +static OBJCONST UCHAR aName0x2503[] = "GD32 jump to app"; + +/* 回调实现在 dlLoadU32 之后(见下方 ObjJumpAppWrite)。 */ + /*============================================================================ * SDO 回调 *==========================================================================*/ @@ -177,6 +195,27 @@ static inline UINT32 dlLoadU32(const UINT16 MBXMEM *p) return v; } +/** @brief 0x2503 写 1 = 请求跳回 APP(挂 pending_app_jump,process() 排空)。 */ +static UINT8 ObjJumpAppWrite(UINT16 index, UINT8 subindex, UINT32 dataSize, + UINT16 MBXMEM *pData, UINT8 bCompleteAccess) +{ + (void)index; + (void)dataSize; + (void)bCompleteAccess; + + if (subindex != 0U) { + return ABORTIDX_SUBINDEX_NOT_EXISTING; + } + if (dlLoadU32(pData) != 1U) { + return ABORTIDX_GENERAL_ERROR; + } + if (!ota.requestJumpToApp()) { + return ABORTIDX_GENERAL_ERROR; + } + s_jumpAppReq = 1U; + return 0U; +} + /** @brief 0x2500 写入:sub1 = 命令,sub2 = 镜像总长;sub3/sub4 只读。 */ static UINT8 ObjFoeDlCtlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize, UINT16 MBXMEM *pData, UINT8 bCompleteAccess) @@ -283,6 +322,10 @@ TOBJECT OBJMEM ApplicationObjDicList[] = { asDlChunkEntryDesc, aName0x2501, &s_dlChunk, NULL, ObjFoeDlWrite, 0x0000}, + {NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)}, + sEntryDesc0x2503, aName0x2503, &s_jumpAppReq, + NULL, ObjJumpAppWrite, 0x0000}, + /* 终止条目 */ {NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000} }; @@ -363,7 +406,10 @@ static bool s_hwOk = false; namespace { /** @brief 与 main.cpp 的 1ms 主循环节拍对齐的时基判定(ECAT_TIMER_INT=0)。 */ constexpr uint32_t kTickCycles = ECAT_TIMER_INC_P_MS; /* 180000 @180MHz = 1ms */ -uint32_t s_lastCheck = 0U; +/** @brief 空闲时 MainLoop 最小间隔。FreeRun 每圈 csrRead 双读 = EXMC 20+ 线翻转, + * 邮箱-only 在 5ms 节拍下仍够用;OTA/ECAT 会话期间不限速。 */ +constexpr uint32_t kEcatIdlePollMinMs = 5U; +uint32_t s_lastPollCyc = 0U; } /** @brief 上电调一次。硬件不通就整个 EtherCAT 栈不启,SPI OTA 照旧。 */ @@ -383,19 +429,27 @@ extern "C" void ecatTaskStart(void) * APP 侧就是因为零调用者才让 s_alEventMask 影子停在 0xFFFF。 */ (void)HW_Init(); (void)MainInit(); - s_lastCheck = DWT->CYCCNT; + s_lastPollCyc = DWT->CYCCNT; } -/** @brief 主循环每圈调。邮箱服务、AL 状态机、SDO 分段都在这条路上推进。 */ +/** @brief 主循环每圈调。邮箱服务、AL 状态机、SDO 分段都在这条路上推进。 + * 空闲时按 kEcatIdlePollMinMs 限速,降低 FreeRun 对 LAN9252 的 EXMC 翻转。 */ extern "C" void ecatTaskPoll(void) { if (!s_started || !s_hwOk) { return; } + + const bool busy = ota.isActive() || ota.ecatSessionActive(); + const uint32_t now = DWT->CYCCNT; + if (!busy) { + const uint32_t elapsed_ms = (now - s_lastPollCyc) / ECAT_TIMER_INC_P_MS; + if (elapsed_ms < kEcatIdlePollMinMs) { + return; + } + } + s_lastPollCyc = now; MainLoop(); - /* ECAT_TIMER_INT=0:SSC 的超时计数靠 MainLoop 内部自己按 HW_GetTimerValue() - * 判 1ms,这里不需要额外节拍。保留函数以便 Phase 2 挂进度上报。 */ (void)kTickCycles; - (void)s_lastCheck; } diff --git a/GD32E50X_BOOT/comm/ecat/boot_ecat_hw.cpp b/GD32E50X_BOOT/comm/ecat/boot_ecat_hw.cpp index c2492da..4e6266e 100644 --- a/GD32E50X_BOOT/comm/ecat/boot_ecat_hw.cpp +++ b/GD32E50X_BOOT/comm/ecat/boot_ecat_hw.cpp @@ -495,24 +495,22 @@ void escResetForJump() gpio_input_bit_get(ESC_NRST_PORT, ESC_NRST_PIN) == SET ? 1 : 0); } -/** @brief 跳转 APP 前拆掉 BOOT 留下的 HBI 用户与引脚占用。 */ +/** @brief 跳转 APP 前拆掉 BOOT 留下的 HBI 用户与引脚占用。 + * ⚠ 跳转路径上**禁止**再对 LAN9252 做 writeReg32:HBI 若不返回会锁死 AHB, + * 主循环卡在 deinit,SPI ISR 仍应答 HELLO(表现为 role=boot)而邮箱全死。 + * APP 1.6.40+ 放行门是 BYTE_TEST 往返,不再依赖「读复位默认 IDX_0」, + * 清 INT_EN/IRQ_CFG 残值不是硬需求;宁可交给 APP 自己 init/RST#。 */ extern "C" void bootEcatDeinit(void) { NVIC_DisableIRQ(ESC_IRQ_IRQn); NVIC_ClearPendingIRQ(ESC_IRQ_IRQn); exti_interrupt_disable(ESC_IRQ_EXTI); exti_flag_clear(ESC_IRQ_EXTI); - writeReg32(LAN9252_REG_INT_EN, 0U); /* 免得 APP 起来后 PB1 还挂着 ESC 驱动的电平 */ - writeReg32(LAN9252_REG_IRQ_CFG, 0U); - /** [WJ-TEST 1.1.0.18] 单变量裁决:暂时短路跳转前这发 RST#。 - * 板上实测(2026-09-16):刷完 warm jump 进不去 APP,但按一下 reset 走 - * cold jump 就正常起 —— 两条路唯一差异是 s_escBusUp 为真时下面这些真跑了, - * 而 escResetForJump 释放 RST# 后只忙等 2ms 就交 CPU(对照 BOOT 自己起板 - * exmcInit 轮询 HBI_IDX_0==0x1234 上限 300ms)。 - * 预期代价=1.1.0.13 的原病回来(APP 自检「EXMC 未就绪/降级 ethercat」、 - * IDX_0 读到 0x0054);那是"能起但降级",与"起不来"形态不同,可分开判读。 - * 起来 ⇒ 元凶=这发复位的时序;仍起不来 ⇒ 转查 PD/PE 留在 AF 的 MCU 侧残留。 */ - /* escResetForJump(); */ /* 1.1.0.13: 交还总线前把芯片复位到上电默认态 */ + /** [WJ-TEST 1.1.0.18] escResetForJump 仍短路;1.1.0.21 再去掉两笔 HBI 写, + * 根治 warm jump 时 jumpToApplication 卡死(SPI 活、ECAT 死、role=boot)。 */ + /* writeReg32(LAN9252_REG_INT_EN, 0U); */ + /* writeReg32(LAN9252_REG_IRQ_CFG, 0U); */ + /* escResetForJump(); */ exmc_norsram_disable(EXMC_BANK0_NORSRAM_REGION0); rcu_periph_clock_disable(RCU_EXMC); /* PD/PE 回浮空,交还 APP 自己初始化 */ DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; diff --git a/GD32E50X_BOOT/main.cpp b/GD32E50X_BOOT/main.cpp index 1c9dc0b..cb67194 100644 --- a/GD32E50X_BOOT/main.cpp +++ b/GD32E50X_BOOT/main.cpp @@ -1,4 +1,4 @@ -#include "main.h" +#include "main.h" #include "device/Can.h" #include "device/SpiOta.h" #include "sys/config.h" @@ -80,7 +80,7 @@ void BootloaderSystem::initHardware() { 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 个配置字,再擦除整页后一次性回写。 + /** 版本变更:先读取整页全部配置字(含 arm/crash),再擦除整页后一次性回写。 * 读取逻辑与 app_spi_command.cpp 中 BOOT 版本读取方式一致。 */ u32 temp_val[OTA_CONFIG_NUM]; volatile u32 *flash_ptr; @@ -139,6 +139,27 @@ void BootloaderSystem::initHardware() { /** @brief 心跳打印间隔(毫秒)。 */ static constexpr uint32_t kHeartbeatPrintIntervalMs = 2000U; +/** + * @brief 读连续 FWDGT 崩溃计数。 + * @note 擦除态 0xFFFFFFFF 视为 0(旧板/未写过)。 + */ +static uint32_t otaCrashCount(void) +{ + const uint32_t raw = *(volatile uint32_t *)n_BOOT_CRASH_COUNT_ADDR; + return (raw == OTA_NOT_AVAL) ? 0U : raw; +} + +/** + * @brief FWDGT 复位时把崩溃计数 +1(Flash 友好:OTA_SET 内含擦页回写)。 + */ +static void otaCrashBump(void) +{ + const uint32_t cur = otaCrashCount(); + if (cur < 0xFFFFFFFFU) { + (void)OTA_SET(n_BOOT_CRASH_COUNT_ADDR, cur + 1U); + } +} + /** * @brief EtherCAT 邮箱栈:首次调用拉起,之后每圈推一次 SSC 状态机。 * @@ -214,13 +235,14 @@ void BootloaderSystem::run() { uint32_t app_enable, app_check; /** 检测 FWDGT 看门狗复位:APP 卡死导致看门狗超时。 - * 将 app_enable 标记为 NOT_AVAL,阻止 BOOT 再次跳入卡死的 APP, - * 改为停留在 BOOT 等待 OTA 修复。 */ + * ① 崩溃计数 +1;② EN 标 NOT_AVAL 走 OTA 窗口。 + * 计数达 OTA_CRASH_STAY_MAX 后由下方 crash_gate 彻底禁止自动跳 APP。 */ 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"); + printf("[BOOT] FWDGT reset detected, bump crash_count\r\n"); #endif + otaCrashBump(); OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL); } @@ -231,17 +253,33 @@ void BootloaderSystem::run() { flash_ptr = (volatile uint32_t *)n_APP_LATEST_CHECK_ADDR; app_check = *flash_ptr; + /** 连续崩溃门:达阈值后强制走 wait_ota_window,并在超时/恢复路径上禁止跳 APP。 + * APP 起板成功会把计数清 0,故这不是永久砖,是「必须外部灌写才能再试」。 */ + const uint32_t crash_count = otaCrashCount(); + const bool crash_gate = (crash_count >= OTA_CRASH_STAY_MAX); + if (crash_gate) { #if DEBUG_PRINT - printf("[BOOT] app_enable=0x%08lX app_check=0x%08lX\r\n", + printf("[BOOT] crash_count=%lu >= %lu, auto-jump DISABLED\r\n", + (unsigned long)crash_count, (unsigned long)OTA_CRASH_STAY_MAX); +#endif + if (app_enable != OTA_NOT_AVAL) { + OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL); + app_enable = OTA_NOT_AVAL; + } + } + +#if DEBUG_PRINT + printf("[BOOT] app_enable=0x%08lX app_check=0x%08lX crash=%lu\r\n", (unsigned long)app_enable, - (unsigned long)app_check); + (unsigned long)app_check, + (unsigned long)crash_count); #endif /** * 情况 1:应用已校验完成,且允许启动。 * 增加 CRC32 完整性校验;若校验失败则标记 A区无效,fall through 到恢复逻辑。 */ - if (app_check == OTA_AVAL && app_enable == OTA_AVAL) { + if (app_check == OTA_AVAL && app_enable == OTA_AVAL && !crash_gate) { #if DEBUG_PRINT printf("[BOOT] path=jump_latest_now\r\n"); #endif @@ -336,6 +374,7 @@ void BootloaderSystem::run() { (unsigned long)ended_check, (unsigned long)ended_aval); #endif if (ended_check == OTA_AVAL && ended_aval == OTA_AVAL && + !crash_gate && verifyAppCrc(APPLATEST_START_ADDR)) { ota.jumpToApplication(); } @@ -354,7 +393,11 @@ void BootloaderSystem::run() { #if DEBUG_PRINT printf("[BOOT] ota_window_timeout_then_jump\r\n"); #endif - if (verifyAppCrc(APPLATEST_START_ADDR)) { + if (crash_gate) { +#if DEBUG_PRINT + printf("[BOOT] crash_gate: timeout jump suppressed\r\n"); +#endif + } else if (verifyAppCrc(APPLATEST_START_ADDR)) { ota.jumpToApplication(); /** jumpToApplication() 正常不返回;若返回则继续 fall through。 */ } else { @@ -384,7 +427,7 @@ void BootloaderSystem::run() { (unsigned long)back_check); #endif - if (back_check == OTA_AVAL && back_aval == OTA_AVAL) { + if (back_check == OTA_AVAL && back_aval == OTA_AVAL && !crash_gate) { #if DEBUG_PRINT printf("[BOOT] path=restore_backup_then_jump\r\n"); #endif diff --git a/GD32E50X_BOOT/shared/ota_layout.h b/GD32E50X_BOOT/shared/ota_layout.h new file mode 100644 index 0000000..5866a42 --- /dev/null +++ b/GD32E50X_BOOT/shared/ota_layout.h @@ -0,0 +1,59 @@ +/** + * @file ota_layout.h + * @brief APP/BOOT 共用 OTA Flash 布局(唯一真源)。 + * @note 两侧 sys/config.h 都必须 include 本头,禁止再抄地址常量。 + * 相对路径:BOOT/APP 的 sys/config.h → ../../../shared/ota_layout.h + * + * 约定:0x00000000=AVAL(有效),0xFFFFFFFF=NOT_AVAL(擦除态/无效)。 + */ +#ifndef SHARED_OTA_LAYOUT_H +#define SHARED_OTA_LAYOUT_H + +#include + +/* ---- 整片 Flash 区段(GD32E508VE 512KB) ---- */ +#define OTA_LAYOUT_BOOT_START 0x08000000U +#define OTA_LAYOUT_BOOT_SIZE 0x00012000U /* 72KB */ +#define OTA_LAYOUT_APP_BACK_START 0x08012000U /* 216KB 备份区 */ +#define OTA_LAYOUT_APP_MAIN_START 0x08048000U /* 216KB 主应用区 = APP VTOR */ +#define OTA_LAYOUT_APP_MAX_SIZE 0x00035FFCU /* 216KB-4B,末尾 CRC32 */ +#define OTA_LAYOUT_CONFIG_PAGE 0x0807E000U /* OTA 状态配置页 2KB */ +#define OTA_LAYOUT_SLEEP_PAGE 0x0807E800U /* 睡眠配置页 2KB */ +#define OTA_LAYOUT_PAGE_SIZE 0x00000800U /* FMC 单页 2KB */ + +/* ---- OTA 配置页 word 布局(word N = PAGE + 4*N) ---- */ +#define OTA_LAYOUT_WORD_LATEST_AVAL 0U +#define OTA_LAYOUT_WORD_BACKUP_AVAL 1U +#define OTA_LAYOUT_WORD_LATEST_EN 2U +#define OTA_LAYOUT_WORD_BACKUP_EN 3U +#define OTA_LAYOUT_WORD_LATEST_CHECK 4U +#define OTA_LAYOUT_WORD_BACKUP_CHECK 5U +#define OTA_LAYOUT_WORD_BOOT_VER0 6U /* major|minor */ +#define OTA_LAYOUT_WORD_BOOT_VER1 7U /* patch|build */ +#define OTA_LAYOUT_WORD_ECAT_ARM 8U /* 0xACA70001 才允许 ECAT SDO 刷 APP */ +#define OTA_LAYOUT_WORD_CRASH_COUNT 9U /* 连续 FWDGT 崩溃次数;APP 起板成功清 0 */ + +#define OTA_LAYOUT_ADDR_LATEST_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_AVAL) +#define OTA_LAYOUT_ADDR_BACKUP_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_AVAL) +#define OTA_LAYOUT_ADDR_LATEST_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_EN) +#define OTA_LAYOUT_ADDR_BACKUP_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_EN) +#define OTA_LAYOUT_ADDR_LATEST_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_CHECK) +#define OTA_LAYOUT_ADDR_BACKUP_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_CHECK) +#define OTA_LAYOUT_ADDR_BOOT_VER0 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER0) +#define OTA_LAYOUT_ADDR_BOOT_VER1 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER1) +#define OTA_LAYOUT_ADDR_ECAT_ARM (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_ECAT_ARM) +#define OTA_LAYOUT_ADDR_CRASH_COUNT (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_CRASH_COUNT) + +/** 配置页有效 word 数(擦页回写必须覆盖到此);页 2KB,余量充足。 */ +#define OTA_LAYOUT_WORD_COUNT 10U + +/** ECAT 下载 arm 令牌。避开擦除态与 AVAL。 */ +#define OTA_LAYOUT_ECAT_ARM_KEY 0xACA70001U + +/** 连续 FWDGT 达到该次数后 BOOT 停止自动跳 APP,只接受外部灌写。 */ +#define OTA_LAYOUT_CRASH_STAY_MAX 3U + +#define OTA_LAYOUT_AVAL 0x00000000U +#define OTA_LAYOUT_NOT_AVAL 0xFFFFFFFFU + +#endif /* SHARED_OTA_LAYOUT_H */ diff --git a/GD32E50X_BOOT/sys/OTA.cpp b/GD32E50X_BOOT/sys/OTA.cpp index bdcc6b4..78a0397 100644 --- a/GD32E50X_BOOT/sys/OTA.cpp +++ b/GD32E50X_BOOT/sys/OTA.cpp @@ -177,6 +177,9 @@ u8 OTA_SET(u32 addr, u32 data) { * 表现为"刚 arm 完、一次标志更新就又不让刷了"。 */ flash_ptr = (volatile u32 *)n_ECAT_OTA_ARM_ADDR; temp_val[8] = *flash_ptr; + /** word 9 = 连续 FWDGT 崩溃计数。漏读会让「APP 刚清零 / BOOT 刚 +1」被误抹。 */ + flash_ptr = (volatile u32 *)n_BOOT_CRASH_COUNT_ADDR; + temp_val[9] = *flash_ptr; /** 整页擦除后重建配置页。 */ fmc_unlock(); @@ -609,6 +612,21 @@ return; (unsigned)BOOT_FW_VERSION_PATCH, (unsigned)BOOT_FW_VERSION_BUILD); /* 无缓冲的 microlib 下 fflush 是空操作,留着只为换回标准库时语义不变。 */ fflush(stdout); + /* putChar 只等 TBE(数据寄存器空),最后一字节可能还在移位寄存器。 + * 不等 TC 就 BX:APP SystemInit 会重配 USART/时钟,线上出现 + * "handoff" 后半截 + APP 乱码。DWT 已在 bootEcatDeinit 关掉, + * 这里用有界轮询(~2ms @3Mbps 约需 60 bit 时间,留足余量)。 */ + { + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + const uint32_t t0 = DWT->CYCCNT; + const uint32_t tmax = 2U * (SystemCoreClock / 1000U); + while ((USART_STAT0(USART0) & USART_FLAG_TC) == 0U) { + if ((DWT->CYCCNT - t0) > tmax) { + break; + } + } + } #endif /* @@ -635,6 +653,31 @@ return; __builtin_unreachable(); } +/** + * @brief 主站写 0x2503 请求跳回 APP。 + * @note 与 ecatCommit 成功尾巴逐字对齐:先置 CHECK/AVAL/EN 三面标志, + * 再 pending_app_jump,由 process() 走同一套 delay+jumpToApplication。 + * 不要另起跳转实现。 + */ +bool OTAHandler::requestJumpToApp() +{ + if (ecat_session || ota_state != OTA_STATE_IDLE) { + return false; + } + if (!validateApplication()) { + return false; + } + if (!verifyAppCrc(APPLATEST_START_ADDR)) { + return false; + } + /* 与 ecatCommit OTA_SET 三连相同;EN 原先可能是 NOT_AVAL(0x2502 进来的)。 */ + OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_AVAL); + OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL); + OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL); + pending_app_jump = true; + return true; +} + /** * @brief 擦除当前主应用区。 * diff --git a/GD32E50X_BOOT/sys/OTA.h b/GD32E50X_BOOT/sys/OTA.h index c2fc72c..243de9e 100644 --- a/GD32E50X_BOOT/sys/OTA.h +++ b/GD32E50X_BOOT/sys/OTA.h @@ -165,6 +165,15 @@ class OTAHandler { /** @brief 放弃会话。已擦/已写的 A 区不恢复,靠 AVAL 未置守住。 */ void ecatAbort(); + /** + * @brief 主站显式请求跳回 APP(CoE 0x2503:00=1)。 + * @note 不在 SDO 回调里跳:置 pending_app_jump,由 process() 排空。 + * 与 crash_gate 正交——门只禁自动跳,人工跳仍允许(否则 UI 无法退出 BOOT)。 + * @retval true 已挂起跳转(向量+CRC 均合法) + * @retval false 会话中/镜像非法,未挂起 + */ + bool requestJumpToApp(); + /** @brief 已落字节数(0x2500:03 进度)。 */ u32 ecatReceived() const { return ecat_recv; } /** @brief 会话是否活跃。 */ diff --git a/GD32E50X_BOOT/sys/config.h b/GD32E50X_BOOT/sys/config.h index 2dfd10c..7d15b91 100644 --- a/GD32E50X_BOOT/sys/config.h +++ b/GD32E50X_BOOT/sys/config.h @@ -1,8 +1,9 @@ -#ifndef __CONFIG_H__ +#ifndef __CONFIG_H__ #define __CONFIG_H__ #include #include "gd32e50x.h" +#include "../shared/ota_layout.h" /** 项目内部统一使用的简写类型。 */ typedef uint8_t u8; @@ -193,14 +194,34 @@ typedef uint32_t u32; * 注:本版与 1.1.0.15~17(关中断跳转实验、BKP 面包屑、EXTI15 清理)无关, * 那批改动未提交且已整体回退到 46adcb7,补丁存于 * G:/Robot_v2/DCU/_revert_backup_20260916/。 + * v1.1.0.19: 连续 FWDGT 崩溃计数 + 共享 OTA 布局头。 + * ① 配置页新增 word9=crash_count:FWDGT 复位时 +1;APP 喂狗稳定满 30s 后清 0 + * (task_runner::maybeClearCrashCount,**不在 init 里立刻清**,否则主循环崩溃 + * 永远攒不到阈值)。达到 OTA_CRASH_STAY_MAX(3) 后 BOOT 停止一切自动跳 APP + * (jump_latest_now / 窗口超时 / stay_in_ota 结束 / 备份恢复均被门控), + * 只留在 BOOT 等外部灌写。 + * 动机:旧逻辑一次 FWDGT 就 EN=NOT_AVAL,10s 后 CRC 过仍自动再跳——崩溃镜像会 + * 以 10s 周期打转;同时“一次误触发就永远留守”过严。N 次阈值两边都收住。 + * ② 地址/字布局抽到 G:/Robot_v2/DCU/shared/ota_layout.h,APP/BOOT 共用,禁止再抄。 + * v1.1.0.20: CoE 0x2503「GD32 jump to app」——主站写 1 让 BOOT 跳回 APP。 + * 与 APP 0x2502 对称;回调只置 pending_app_jump,process() 在 SDO 响应后跳。 + * 主站 ecat op=jump_app / 前端「跳回应用」按钮配套。 + * v1.1.0.21: 修 warm jump 卡死。现象:写 0x2503 成功(ok=true)后 SPI 仍 role=boot、 + * EtherCAT 邮箱全死。根因:jumpToApplication → bootEcatDeinit 里两笔 writeReg32 + * (INT_EN/IRQ_CFG) 走 EXMC HBI,LAN9252 不返回时锁死 AHB,主循环卡住;SPI 由 ISR + * 应答故看起来还活着。去掉这两笔(APP 1.6.40+ 用 BYTE_TEST,不依赖清残值)。 + * v1.1.0.22: [J6] 后串口乱码。putChar 只等 TBE,末字节仍在移位寄存器就 BX, + * APP 重配 USART 后线上混成 "handoff"+乱码。跳转前有界等 USART TC(~2ms)。 + * requestJumpToApp 改为与 ecatCommit 成功尾巴一致:先置 CHECK/AVAL/EN 再 pending, + * 仍走 process() 同一条 delay+jumpToApplication,不另起跳转。 */ #define BOOT_FW_VERSION_MAJOR 1U #define BOOT_FW_VERSION_MINOR 1U #define BOOT_FW_VERSION_PATCH 0U -#define BOOT_FW_VERSION_BUILD 18U /* 变更来由见上方注释块 */ +#define BOOT_FW_VERSION_BUILD 22U /* 变更来由见上方注释块 */ /** - * Flash 布局说明(GD32E508VE,512KB) + * Flash 布局说明(GD32E508VE,512KB)——唯一真源 shared/ota_layout.h * ----------------------------------- * 0x08000000 ~ 0x08011FFF : Bootloader 区(72KB) * 0x08012000 ~ 0x08047FFF : 备份应用区(216KB) @@ -208,13 +229,13 @@ typedef uint32_t u32; * 0x0807E000 ~ 0x0807E7FF : OTA 状态配置页 * 0x0807E800 ~ 0x0807EFFF : 睡眠配置页 */ -inline constexpr u32 BOOTLOADER_START_ADDR = 0x08000000U; -inline constexpr u32 BOOTLOADER_SIZE = 0x00012000U; /**< 72KB */ -inline constexpr u32 APPBACK_START_ADDR = 0x08012000U; /**< 备份应用起始地址 */ -inline constexpr u32 APPLATEST_START_ADDR = 0x08048000U; /**< 当前有效应用起始地址 */ -inline constexpr u32 APP_MAX_SIZE = 0x00035FFCU; /**< 216KB - 4B,末尾预留 CRC32 */ -inline constexpr u32 FMC_PAGE_SIZE = 0x00000800U; /**< 2KB,GD32E50x 单页大小 */ -inline constexpr u32 OTA_CONFIG_PAGE_ADDR = 0x0807E000U; /**< OTA 元数据页 */ +inline constexpr u32 BOOTLOADER_START_ADDR = OTA_LAYOUT_BOOT_START; +inline constexpr u32 BOOTLOADER_SIZE = OTA_LAYOUT_BOOT_SIZE; +inline constexpr u32 APPBACK_START_ADDR = OTA_LAYOUT_APP_BACK_START; +inline constexpr u32 APPLATEST_START_ADDR = OTA_LAYOUT_APP_MAIN_START; +inline constexpr u32 APP_MAX_SIZE = OTA_LAYOUT_APP_MAX_SIZE; +inline constexpr u32 FMC_PAGE_SIZE = OTA_LAYOUT_PAGE_SIZE; +inline constexpr u32 OTA_CONFIG_PAGE_ADDR = OTA_LAYOUT_CONFIG_PAGE; inline constexpr u32 APP_CRC_SIZE = 4U; /**< 固件区末尾 CRC32 占用字节数 */ inline constexpr u32 APP_FIRMWARE_SIZE = APP_MAX_SIZE - APP_CRC_SIZE; /**< 实际固件大小(不含 CRC) */ inline constexpr u32 n_APP_LATEST_CRC_ADDR = APPLATEST_START_ADDR + APP_FIRMWARE_SIZE; /**< A区 CRC 存储地址 */ @@ -228,24 +249,28 @@ inline constexpr u32 SLEEP_ENABLE = 0x11111111U; inline constexpr u32 SLEEP_DISABLE = 0x00000000U; /** - * OTA 配置页一共使用 9 个 word(页大小 2KB,余量充足): + * OTA 配置页一共使用 10 个 word(页大小 2KB,余量充足): * word 0~5: latest_aval / backup_aval / latest_en / backup_en / latest_check / backup_check * word 6~7: BOOT 版本号(major|minor / patch|build) * word 8 : EtherCAT 下载 arm 令牌,值 == OTA_ECAT_ARM_KEY 才允许 SDO 刷 APP + * word 9 : 连续 FWDGT 崩溃计数;APP 喂狗满 30s 清 0;达 OTA_CRASH_STAY_MAX 禁止自动跳 */ -inline constexpr u32 OTA_CONFIG_NUM = 9U; -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 */ -inline constexpr u32 n_ECAT_OTA_ARM_ADDR = OTA_CONFIG_PAGE_ADDR + 0x20U; /**< EtherCAT 下载 arm 令牌 word8 */ +inline constexpr u32 OTA_CONFIG_NUM = OTA_LAYOUT_WORD_COUNT; +inline constexpr u32 n_APP_LATEST_AVAL_ADDR = OTA_LAYOUT_ADDR_LATEST_AVAL; +inline constexpr u32 n_APP_BACKUP_AVAL_ADDR = OTA_LAYOUT_ADDR_BACKUP_AVAL; +inline constexpr u32 n_APP_LATEST_EN_ADDR = OTA_LAYOUT_ADDR_LATEST_EN; +inline constexpr u32 n_APP_BACKUP_EN_ADDR = OTA_LAYOUT_ADDR_BACKUP_EN; +inline constexpr u32 n_APP_LATEST_CHECK_ADDR = OTA_LAYOUT_ADDR_LATEST_CHECK; +inline constexpr u32 n_APP_BACKUP_CHECK_ADDR = OTA_LAYOUT_ADDR_BACKUP_CHECK; +inline constexpr u32 n_BOOT_VERSION_ADDR0 = OTA_LAYOUT_ADDR_BOOT_VER0; +inline constexpr u32 n_BOOT_VERSION_ADDR1 = OTA_LAYOUT_ADDR_BOOT_VER1; +inline constexpr u32 n_ECAT_OTA_ARM_ADDR = OTA_LAYOUT_ADDR_ECAT_ARM; +inline constexpr u32 n_BOOT_CRASH_COUNT_ADDR = OTA_LAYOUT_ADDR_CRASH_COUNT; /** @brief arm 令牌值。刻意避开擦除态 0xFFFFFFFF 与 OTA_AVAL 0x00000000, * 所以没写过这一字的旧板子/新板子都天然读成「未 arm」。 */ -inline constexpr u32 OTA_ECAT_ARM_KEY = 0xACA70001U; +inline constexpr u32 OTA_ECAT_ARM_KEY = OTA_LAYOUT_ECAT_ARM_KEY; +/** @brief 连续崩溃达到该次数后,BOOT 停止自动跳 APP。 */ +inline constexpr u32 OTA_CRASH_STAY_MAX = OTA_LAYOUT_CRASH_STAY_MAX; /** * 本项目把 0x00000000 视为“有效/可用”, diff --git a/GD32E50X_BOOT/sys/systick.cpp b/GD32E50X_BOOT/sys/systick.cpp index 94e3d73..b704a8b 100644 --- a/GD32E50X_BOOT/sys/systick.cpp +++ b/GD32E50X_BOOT/sys/systick.cpp @@ -50,16 +50,20 @@ void SysTickTimer::delayUs(uint32_t count) } /** - * @brief 执行毫秒级阻塞延时。 + * @brief 执行毫秒级阻塞延时(WFI 等 SysTick,不空转烧 CPU)。 * * @param count 延时毫秒数。 * @return 无。 + * @note 180MHz 下空 while 会让 die 迅速升温(BOOT 驻留实测 ~80°C)。 + * SysTick 已配 1ms 且优先级 0,WFI 睡到下一次滴答即可。 + * 禁止在 __disable_irq() 之后调用——滴答不进则永久睡死; + * 那类路径(bootEcatDeinit 等)必须继续用 DWT 忙等。 */ void SysTickTimer::delayMs(uint32_t count) { - /** 设置倒计时,由 SysTick_Handler 每毫秒递减。 */ s_delayCountdown = count; while (s_delayCountdown != 0U) { + __WFI(); } }