Files
DCU_APP/GD32E508_DCU_APP/sys/ota_boot_request.cpp
T
cbb d8b3c9767c feat(ota): 电机 OTA 支持 EtherCAT 模式(CoE 邮箱进料 + 通道独占锁)
进料口从「只有 ESP32 SPI 一条」扩成两条,中继引擎不变:主站经 CoE 0x2511 数据窗
把电机固件明文灌进 GD32 的 48KB SRAM 暂存区,再由 comm/ecat_motor_ota 泵进原有
4KB 环缓;AES 握手、CAN 单发停等、重发、0AAC 运行确认全部沿用 MotorOtaRelay。
纯网线(ESP32 WiFi 关断、/motor/ota 打不通)场合因此也能刷电机。
偏移/幂等判据仍只有一份:feedData 拆出 feedAt,两个进料口共用。

同时把审计报告 VULN-12/13/14 落地(此前只存在于文档,一行未实现):start() 第一条
语句在 PRIMASK 内原子置位 s_channel_lock_mask,EtherCAT 桥接 sendBatch/processOutput
与 SPI 0xC0/0xC7 对锁定通道回新码 0xE7=「被电机 OTA 独占」,不冒充「邮箱满」0xE4。
反向互斥一并补上:电机 OTA 活跃时 requestEnterBoot() 直接拒绝——SPI 0xB0、CoE 0x2502、
CAN 0x320 三条交接入口共用这一个判据,避免复位把刷写腰斩在 erase 之后。

完整性口径按链路分开,不互相冒充:块级 CRC32 + 整镜像 CRC32 都在从站核,且核过才
让电机进 BOOT——失败时电机仍跑旧 APP、擦除没开始,不存在「先擦再验」的变砖路径。
CRC 为 CRC-32/B(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射无末尾异或),与 v2_crc32
同算法(用公开向量 123456789=0x0376E6E7 对过)。

块长取 1024 而不是 BOOT 用的 4096:SSC 的分段下载缓冲是全工程唯一的 malloc 用户,
而 APP 的 __HEAP_SIZE 只有 3KB,4096 会被从站当场回 abort 0x05040005 Out of memory
(BOOT 的堆是 16KB 才撑得住 4096——块长能抄,堆抄不了)。这是跨仓常量,须与
tools/ecat 的 MFO_CHUNK_OBJ 同步。

APP 1.7.4(RAM ZI 97156)。已实测:0x2510 参数写与一块 1016B 数据写均被从站接受
(staged=1016、phase bit4);端到端电机刷写待复测(native 块长修复后未跑完整一轮)。
2026-10-11 02:47:19 +08:00

273 lines
9.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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/motor_ota_relay.h"
#include "sys/systick.h"
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h),本文件原先没有 stdio */
namespace {
/** 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;
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 清零连续 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;
}
}
/**
* @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。
* @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。
*/
bool OtaBootRequest::requestEnterBoot()
{
/* 电机 OTA 正在跑(WiFi 与 EtherCAT 两条进料路都算)时绝不交回 BOOT:
* 复位会把电机的刷写腰斩在 erase 之后——旧 APP 已抹、新 APP 没收完,直接变砖。
* 判据只放这一个执行点:SPI 0xB0、EtherCAT CoE 0x2502、CAN 0x320 三条入口
* 最终都要过这里,不在调用方各抄一份。 */
if (MotorOtaRelay::isActive()) {
LOG_ERR("[OTA-BOOT] enter-boot refused: motor OTA active ch=%d\r\n",
(int)MotorOtaRelay::lockedChannel());
return false;
}
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); /* 与 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();
}