2026-05-30 00:24:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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 {
|
2026-09-20 22:58:09 +08:00
|
|
|
|
/** Bootloader OTA 配置页地址与 word 布局:唯一真源 shared/ota_layout.h。 */
|
|
|
|
|
|
constexpr uint32_t kOtaConfigPageAddr = OTA_LAYOUT_CONFIG_PAGE;
|
|
|
|
|
|
constexpr uint32_t kAppLatestAvalAddr = OTA_LAYOUT_ADDR_LATEST_AVAL;
|
|
|
|
|
|
constexpr uint32_t kAppLatestEnAddr = OTA_LAYOUT_ADDR_LATEST_EN;
|
|
|
|
|
|
constexpr uint32_t kAppLatestCheckAddr = OTA_LAYOUT_ADDR_LATEST_CHECK;
|
|
|
|
|
|
constexpr uint32_t kBootVersionAddr0 = OTA_LAYOUT_ADDR_BOOT_VER0;
|
|
|
|
|
|
constexpr uint32_t kBootVersionAddr1 = OTA_LAYOUT_ADDR_BOOT_VER1;
|
|
|
|
|
|
constexpr uint32_t kBootCrashCountAddr = OTA_LAYOUT_ADDR_CRASH_COUNT;
|
|
|
|
|
|
constexpr uint32_t kOtaAval = OTA_LAYOUT_AVAL;
|
|
|
|
|
|
constexpr uint32_t kOtaNotAval = OTA_LAYOUT_NOT_AVAL;
|
|
|
|
|
|
constexpr uint32_t kFmcPageSize = OTA_LAYOUT_PAGE_SIZE;
|
|
|
|
|
|
constexpr uint32_t kRebootDelayMs = 20U;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 03:13:49 +08:00
|
|
|
|
uint32_t words[CONFIG_OTA_CONFIG_WORD_COUNT];
|
|
|
|
|
|
for(uint32_t i = 0U; i < CONFIG_OTA_CONFIG_WORD_COUNT; ++i) {
|
2026-05-30 00:24:31 +08:00
|
|
|
|
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;
|
2026-06-03 03:13:49 +08:00
|
|
|
|
for(uint32_t i = 0U; i < CONFIG_OTA_CONFIG_WORD_COUNT; ++i) {
|
2026-05-30 00:24:31 +08:00
|
|
|
|
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;
|
2026-07-08 02:51:24 +08:00
|
|
|
|
volatile bool OtaBootRequest::s_bootRequestPending = false;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
volatile bool OtaBootRequest::s_spiFrozenForBoot = false;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 初始化进入 Bootloader 请求模块的运行态。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void OtaBootRequest::init()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_rebootPending = false;
|
2026-07-08 02:51:24 +08:00
|
|
|
|
s_bootRequestPending = false;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
s_spiFrozenForBoot = false;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-15 00:34:56 +08:00
|
|
|
|
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)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 22:58:09 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 清零连续 FWDGT 崩溃计数。
|
|
|
|
|
|
* @note 与 BOOT OTA_CRASH_STAY_MAX 门配套。写 0(AVAL)可对 1→0 直接编程。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool OtaBootRequest::clearCrashCount()
|
|
|
|
|
|
{
|
|
|
|
|
|
return otaConfigSet(kBootCrashCountAddr, kOtaAval);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 喂狗运行满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清崩溃计数(每上电至多一次)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void OtaBootRequest::maybeClearCrashCount(uint32_t uptime_sec)
|
|
|
|
|
|
{
|
|
|
|
|
|
static bool s_cleared = false;
|
|
|
|
|
|
if (s_cleared || uptime_sec < CONFIG_CRASH_CLEAR_AFTER_SEC) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (clearCrashCount()) {
|
|
|
|
|
|
s_cleared = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
/* 写配置页失败则永远停在 APP,ESP32 会看到持续 role=1 */
|
|
|
|
|
|
/* 无 printf 依赖:由调用方 process() 恢复 SPI 后继续运行 */
|
2026-05-30 00:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 异步请求进入 Bootloader(ISR 安全)。
|
|
|
|
|
|
* @note 仅设置 pending 标志,Flash 操作和复位由 process() 在主循环执行。
|
|
|
|
|
|
* 用于 SPI EXTI ISR 内的 0xB0 命令处理,避免 Flash 擦除阻塞 SPI 中断。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void OtaBootRequest::requestEnterBootAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_bootRequestPending = true;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/* 立即冻结 SPI 业务:不必等 process()(原依赖 1Hz 心跳,最坏拖 ~1s)。
|
|
|
|
|
|
* 否则 ESP32 紧跟着 HELLO/0xC1 会继续进 processCommand,干扰擦页/复位。 */
|
|
|
|
|
|
s_spiFrozenForBoot = true;
|
2026-07-08 02:51:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-08 02:51:24 +08:00
|
|
|
|
* @brief 在主循环任务中执行异步 Flash 操作和短延迟软复位。
|
|
|
|
|
|
* @note 不在 SPI/CAN 中断里直接复位,避免应答还没有发出就重启。
|
|
|
|
|
|
* 异步路径:SPI ISR 收到 0xB0 后只设标志,Flash 操作(可能擦页 20-30ms)
|
|
|
|
|
|
* 在此主循环执行,避免阻塞 SPI 中断导致 ESP32 ACK 轮询丢字节。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void OtaBootRequest::process()
|
|
|
|
|
|
{
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/* 异步处理 0xB0:主循环写 Flash(可能擦页 20–30ms)。
|
|
|
|
|
|
* 期间冻结 SPI 业务处理,避免 ESP32 紧跟着 HELLO/按铃读 打断擦写。 */
|
2026-07-08 02:51:24 +08:00
|
|
|
|
if(s_bootRequestPending) {
|
|
|
|
|
|
s_bootRequestPending = false;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
s_spiFrozenForBoot = true;
|
|
|
|
|
|
nvic_irq_disable(SPI0_IRQn);
|
|
|
|
|
|
nvic_irq_disable(EXTI10_15_IRQn);
|
|
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
const bool ok = requestEnterBoot();
|
2026-07-13 03:10:37 +08:00
|
|
|
|
if(!ok) {
|
|
|
|
|
|
s_spiFrozenForBoot = false;
|
|
|
|
|
|
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
|
2026-09-20 22:58:09 +08:00
|
|
|
|
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 与 configureExti 一致:抢占级 0 */
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */
|
|
|
|
|
|
return;
|
2026-07-08 02:51:24 +08:00
|
|
|
|
}
|
2026-07-13 03:10:37 +08:00
|
|
|
|
s_rebootPending = true;
|
2026-07-08 02:51:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 00:24:31 +08:00
|
|
|
|
if(!s_rebootPending) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 20:03:31 +08:00
|
|
|
|
s_rebootPending = false;
|
2026-07-13 03:10:37 +08:00
|
|
|
|
s_spiFrozenForBoot = true;
|
|
|
|
|
|
nvic_irq_disable(SPI0_IRQn);
|
|
|
|
|
|
nvic_irq_disable(EXTI10_15_IRQn);
|
2026-07-08 02:51:24 +08:00
|
|
|
|
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/* v2 DATA_READY: Boot 切换不再用 PB6 通知对端,
|
|
|
|
|
|
* BOOT 起机后再由 HELLO role=boot 确认。 */
|
|
|
|
|
|
/* SysTick 中断仍开,delayMs 可用;SPI 相关 IRQ 已关。 */
|
2026-05-30 20:03:31 +08:00
|
|
|
|
SysTickTimer::delayMs(kRebootDelayMs);
|
2026-07-15 02:49:37 +08:00
|
|
|
|
/* 复位前最终关中断:必须用 PRIMASK 全屏蔽(非 BASEPRI),确保 NVIC_SystemReset 过程中无任何中断嵌套 */
|
2026-05-30 00:24:31 +08:00
|
|
|
|
__disable_irq();
|
|
|
|
|
|
NVIC_SystemReset();
|
|
|
|
|
|
while(1) {
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-16 00:48:34 +08:00
|
|
|
|
|
|
|
|
|
|
/*--------------------------------------------------------------------------
|
|
|
|
|
|
* 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();
|
|
|
|
|
|
}
|