Files
GD32E50X_BOOT/GD32E50X_BOOT/comm/ecat/boot_ecat_app.cpp
T
cbb 77354fd2a8 feat(boot): 1.1.0.22 CoE 跳回 APP + 连续崩溃门 + warm jump 根治
变更点:
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 过程目录
2026-09-20 22:58:14 +08:00

456 lines
19 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 boot_ecat_app.cpp
* @brief BOOT 邮箱-only SSC 的应用层:最小对象字典 + EtherCAT 下载会话入口。
*
* 角色分工(刻意这么切的):
* - 硬件/寄存器 → boot_ecat_hw.cpp
* - flash 与提交 → sys/OTA.cpp(BOOT 唯一的 flash writer,EtherCAT 不另起写者)
* - 本文件 → 对象字典 + APPL 钩子 + 把 SDO 写转成对 OTAHandler 的调用
*
* 为什么不需要 FoE:comm/ecat 这份 SSC 的 CoE/SDO 服务是完整的(mailbox.c:453 的
* case MBX_TYPE_COE 原样可用),而 SOEM 的 ecx_SDOwrite 会自动分段。SSC 把整段
* 分段下载先攒进 pSdoSegData(sdoserv.c:214-216 注明是 SDOS_SdoInd 里分配的),
* 最后一个分段到达时才调 OBJ_Write,并在 objdef.c:1352 早分发给对象的 Write 回调
* —— 所以**一次 SDO 传输 = 回调一次、拿到整块**,从站侧不需要 begin/data/commit
* 状态机,也不需要自己拼包。
*
* 对象:
* 0x1000/0x1008/0x1009/0x100A/0x1018 由 coeappl.c 的标准表自动挂上;
* BOOT 与 APP 共用同一份 SII(vendor/product 完全相同,本轮刻意不动 SII),
* 所以主站**只能靠 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"
#include "esc.h"
#include "el9800appl.h"
#include "objdef.h"
#include "sdoserv.h"
#include "ecatslv.h"
#include "dc_app.h"
#include "sys/OTA.h"
#include "sys/config.h"
#include <cstring>
/** 全局实例定义在 sys/OTA.cpp:18(main.h 也这么声明)。 */
extern OTAHandler ota;
/*============================================================================
* 下载参数
*==========================================================================*/
/** @brief 一次 SDO 传输的镜像块大小。
* @note 上限来自 TSDOINFOENTRYDESC.BitSize 是 UINT16 —— 单个条目最大
* 65535 bit = 8191 字节。取 4096:实测 DCU_APP 的 .hex 转出来 124,992 B
* ⇒ 31 块。每块 = 1 个 initiate(带 240 B,SOEM maxdata = mbx_l-0x10)
* + 16 个段(3856 = 15×247 + 151;SSC 侧段上限 = 256-6-3 = 247,取小者)
* = **17 个邮箱帧搬 4096 B**,即每帧净落 flash 241 B。
* 从站侧只需 4KB 攒包缓冲(sdoserv.c 在最后一帧才调 OBJ_Write)。 */
#define ECAT_DL_CHUNK 4096U
/*============================================================================
* 0x2500 会话控制(REC,条目全用 UINT32,偏移才和 SSC 的
* "si0 占 16bit + 按 BitLength 紧排" 计算一致;见 APP dc_app.c:72-77 的同款断言)
*==========================================================================*/
typedef struct OBJ_STRUCT_PACKED_START {
UINT16 bSubIndex; /**< sub0:条目数,SSC 按 UINT16 读 */
UINT32 u32Cmd; /**< sub1:写 1=begin 2=commit 3=abort;读回最近一次结果 */
UINT32 u32Expected; /**< sub2:镜像总字节数(begin 时写) */
UINT32 u32Received; /**< sub3:已落 flash 字节数(只读,进度) */
UINT32 u32Phase; /**< sub4:bit0=已 arm,bit1=会话活跃,高 16 位=错误码 */
}
OBJ_STRUCT_PACKED_END
TOBJ2500;
_Static_assert(offsetof(TOBJ2500, u32Cmd) == 2, "TOBJ2500 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ2500) == 2 + 4 * 4, "TOBJ2500 尺寸应为 2+16");
static TOBJ2500 s_dlCtl = {4, 0U, 0U, 0U, 0U};
OBJCONST TSDOINFOENTRYDESC OBJMEM asDlCtlEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
/* sub1 cmd / sub2 expected 必须带写权限位:SSC 的 OBJ_Write 在 objdef.c:1325 就做
* 通用写权限检查,**只有过了它才会在 1352 分发到本对象的 ObjFoeDlCtlWrite**。
* 之前两条都写成 ACCESS_READ,主站声明总长那一步直接吃到
* abort=0x06010002 "Attempt to write to a read only object"(实测),回调一次没进过,
* 整个 SDO 写半边等于不存在。sub3/sub4 是真只读,保持不动。 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub1 cmd: R(结果)/ W(触发,见 Write) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub2 expected(begin 前声明总长) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 received: 只读 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub4 phase: 只读 */
};
static OBJCONST UCHAR aName0x2500[] = "GD32 self-update control";
/*============================================================================
* 0x2501 数据通道
*
* 建成 VAR(不是 ARR):VAR 没有 sub0 计数字段,pVarPtr 就是值本身,主站按
* `0x2501:00` 普通访问写一整块即可。ARR 的话 ObjDesc 的元素类型会和条目类型
* 打架,还要在缓冲前面多塞一个没用的 sub0。
*
* 两条主站必须守的约束(objdef.c 实测,改协议前先回来看):
* - VAR 的 maxSubindex 恒为 0(objdef.c:1278 只对非 VAR 才从 sub0 取),
* 所以只能写 `:00`,写 `:1` 直接 ABORTIDX_SUBINDEX_NOT_EXISTING;
* - complete access 对 VAR 一律 UNSUPPORTED_ACCESS(objdef.c:1295),
* 所以 SOEM 那侧必须传 CA=FALSE —— ecat_native 就是这样调的。
* 另外 OBJ_Write 在 objdef.c:1352 就早分发给 Write 回调,前面没有任何通用**长度**
* 校验(sdoserv.c:714 注明「由对象自己的函数查」),所以最后一块短收能原样传进来。
* ⚠ 但**写权限门在前面**(objdef.c:1325):条目描述符没给写位就到不了回调,且报的是
* SSC 通用 abort 码而不是本对象的错误 —— 0x2500 那次就这么静默躺平的,见上面注。
*==========================================================================*/
static UINT8 s_dlChunk[ECAT_DL_CHUNK];
/** @brief 落 flash 前的对齐缓冲(文件级 static,不能开在栈上:BOOT 栈仅 1KB)。
* 回调只在 MainLoop 里跑,无重入。 */
static UINT8 s_dlStage[ECAT_DL_CHUNK];
/** 单条目 BitSize 是 UINT16 ⇒ 8191 字节是天花板,这就是块长取 4096 的原因。 */
_Static_assert(ECAT_DL_CHUNK * 8U <= 0xFFFFU, "0x2501 块长超出 BitSize 上限");
OBJCONST TSDOINFOENTRYDESC OBJMEM asDlChunkEntryDesc[] = {
/* 唯一条目 = 整块镜像。写回调在 objdef.c:1352 早分发,默认拷贝路径不会执行。 */
{DEFTYPE_OCTETSTRING, (UINT16)(ECAT_DL_CHUNK * 8U), ACCESS_READWRITE},
};
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 回调
*==========================================================================*/
/** @brief 0x2501 写入 = 收到一块镜像。
*
* SSC 已把整段分段下载攒完才调这里,dataSize 就是本块字节数(最后一块可短)。
* 落 flash 走 OTAHandler 现成的那条路,本文件不碰 fmc_*。
*/
static UINT8 ObjFoeDlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (dataSize == 0U || dataSize > ECAT_DL_CHUNK) {
return ABORTIDX_PARAM_LENGTH_TOO_LONG;
}
/* 邮箱载荷按原样字节搬出即可。字节序已从两端源码对过(**还没上板实测**):
* - SOEM `ecx_SDOwrite`:`memcpy(&SDOp->ldata[1], hp, framedatasize)`,
* 镜像字节原样上线,总长单独放 CompleteSize(不占载荷);
* - SSC `SdoDownloadSegmentInd`:攒包时把线节拍重建成长字节序内存字
* (sdoserv.c:282/287 `(Data[i-1]>>8)&~MASK | (Data[i]<<8)&MASK`),
* 即 `pSdoSegData` 里的字节顺序 == 线上顺序。
* ⇒ 主站镜像字节 N 落到 flash 的 offset N。上板第一件事仍是拿现成
* /api/sdo 往 0x2501 写 00 01 02 ... 0F 再看回读/串口,不对就只在
* 这一处加转换,绝不在 ecatFeed 之后补偿。 */
/* 必须拷一份再落 flash:writeApplicationData 按 `*(u32*)&data[i]` 整字写,
* 邮箱载荷不保证 4 字节对齐,直接喂进去在 Cortex-M33 上是未对齐 32bit 读。
* ⚠ 但不能开成局部数组 —— BOOT 栈只有 1KB(regions_GD32E508VE.h:96),
* 4KB 局部必爆栈,所以是文件级 static。回调运行在 MainLoop 里,
* 不存在重入。 */
memcpy(s_dlStage, (const void *)pData, dataSize);
if (!ota.ecatFeed(s_dlStage, dataSize)) {
/* 会话没 arm / 超界 / 与 SPI OTA 抢写:报 SDO abort,主站侧能看到 */
return ABORTIDX_GENERAL_ERROR;
}
s_dlCtl.u32Received = ota.ecatReceived();
return 0U;
}
/** @brief 从 SDO 载荷里取一个 UINT32。
*
* 和 0x2501 一样按原样字节取,不做手工字序拼装。
* ⚠ 字节序与载荷布局是上板第一件要验的事:拿现成 /api/sdo 往 0x2500:02 写
* 0x11223344 再读回,不对就在这一处加转换,别在下游各处补偿。
*/
static inline UINT32 dlLoadU32(const UINT16 MBXMEM *p)
{
UINT32 v = 0U;
memcpy(&v, (const void *)p, sizeof(v));
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)
{
(void)index;
(void)dataSize;
(void)bCompleteAccess;
if (subindex == 2U) {
/* 直接存:不能再"转交 OBJ_Write",那会绕回本回调无限递归。 */
s_dlCtl.u32Expected = dlLoadU32(pData);
return 0U;
}
if (subindex != 1U) {
/* sub0 是条目数,sub3/sub4 只读 */
return ABORTIDX_WRITE_ONLY_ENTRY;
}
const UINT32 cmd = dlLoadU32(pData);
switch (cmd) {
case 1U: {
const UINT32 total = s_dlCtl.u32Expected;
if (!ota.ecatBegin(total)) {
s_dlCtl.u32Phase |= 0x00010000U; /* 高 16 位错误码:拒绝 */
return ABORTIDX_GENERAL_ERROR;
}
s_dlCtl.u32Phase = (s_dlCtl.u32Phase & ~0xFFFF0000U) | 0x0002U; /* 会话活跃 */
s_dlCtl.u32Received = 0U;
break;
}
case 2U:
if (!ota.ecatCommit()) {
s_dlCtl.u32Phase = (s_dlCtl.u32Phase & ~0xFFFF0000U) | 0x00030000U;
return ABORTIDX_GENERAL_ERROR;
}
s_dlCtl.u32Phase = (s_dlCtl.u32Phase & ~0xFFFF0000U) & ~0x0002U;
break;
case 3U:
ota.ecatAbort();
s_dlCtl.u32Phase &= ~0x0002U;
s_dlCtl.u32Received = 0U;
break;
default:
return ABORTIDX_GENERAL_ERROR;
}
s_dlCtl.u32Cmd = cmd;
return 0U;
}
/** @brief 把 UINT32 原样字节写回 SDO 载荷(与 dlLoadU32 对称)。 */
static inline void dlStoreU32(UINT16 MBXMEM *p, UINT32 v)
{
memcpy((void *)p, &v, sizeof(v));
}
/** @brief 0x2500 读取:把实时状态并进去,主站轮询进度不必区分对象。
*
* 自己搬字节,不调 objdef.c 的 OBJ_Read —— 那个头的 PROTO 声明没有 extern "C"
* 包裹(这套 SSC 里只有 el9800hw.h 有),从 C++ 调会拿到修饰名变未定义符号。
* 0x2500 是我们自己的定长 record,逐 subindex 取值比重造 SSC 的偏移计算更直白。
*/
static UINT8 ObjFoeDlCtlRead(UINT16 index, UINT8 subindex, UINT32 objSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
(void)index;
(void)objSize;
(void)bCompleteAccess;
s_dlCtl.u32Received = ota.ecatReceived();
s_dlCtl.u32Phase = (s_dlCtl.u32Phase & 0xFFFF0000U)
| (ota.ecatArmed() ? 0x0001U : 0x0000U)
| (ota.ecatSessionActive() ? 0x0002U : 0x0000U);
UINT32 v;
switch (subindex) {
case 0U: v = s_dlCtl.bSubIndex; break;
case 1U: v = s_dlCtl.u32Cmd; break;
case 2U: v = s_dlCtl.u32Expected; break;
case 3U: v = s_dlCtl.u32Received; break;
case 4U: v = s_dlCtl.u32Phase; break;
default: return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (subindex == 0U) {
/* sub0 是 UINT8(条目数),只占载荷一个字节 */
*(UINT8 *)pData = (UINT8)v;
} else {
dlStoreU32(pData, v);
}
return 0U;
}
/*============================================================================
* 对象字典
*==========================================================================*/
TOBJECT OBJMEM ApplicationObjDicList[] = {
{NULL, NULL, 0x2500, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)},
asDlCtlEntryDesc, aName0x2500, &s_dlCtl,
ObjFoeDlCtlRead, ObjFoeDlCtlWrite, 0x0000},
{NULL, NULL, 0x2501, {DEFTYPE_OCTETSTRING, 0 | (OBJCODE_VAR << 8)},
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}
};
TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
/*============================================================================
* APP 埋点/PDO 计划符号的 BOOT 本地实现
*
* comm/ecat/ecatappl.c 与 APP 那份逐字节相同,因此仍引用 1.6.0~1.6.25 期间加的
* EcatDbg_Dwt / EcatPdo_OutputProbe / EcatPdo_SparseEscPlan / EcatPdo_EscWritePlan
* (实体在 APP 的 comm/ethercat_sync.cpp、comm/ethercat_pdo.cpp 里)。
* 这里补齐而**不改 ecatappl.c**,以保持两份 SSC 可 diff。
* BOOT 无过程数据,取中性实现:分段计划恒 0(⇒ 整包写)、输出探针恒 1(⇒ 全量读)。
*==========================================================================*/
extern "C" UINT32 EcatDbg_Dwt(void) { return DWT->CYCCNT; }
extern "C" UINT8 EcatPdo_OutputProbe(UINT8 const *head) { (void)head; return 1U; }
extern "C" UINT8 EcatPdo_SparseEscPlan(UINT16 *o0, UINT16 *l0, UINT16 *o1, UINT16 *l1)
{
(void)o0; (void)l0; (void)o1; (void)l1;
return 0U;
}
extern "C" UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs)
{
(void)offs; (void)lens; (void)maxSegs;
return 0U;
}
/*============================================================================
* APPL_* —— ecatslv.c / ecatappl.c 调用的 11 个钩子
*
* BOOT 无过程数据:Input/Output handler 一律拒绝,SAFEOP 进不去,
* 从站稳定停在 PREOP —— 正是 SDO 下载需要的状态,也让 0x0420 过程数据
* 看门狗天然不适用(秒级上传不会被踢出 OP)。
*==========================================================================*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
{
*pInputSize = 0U;
*pOutputSize = 0U;
return 0U;
}
UINT16 APPL_StartMailboxHandler(void) { return 0U; }
UINT16 APPL_StopMailboxHandler(void) { return 0U; }
UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
{
(void)pIntMask;
return ALSTATUSCODE_INVALIDALCONTROL; /* 非 0 = 拒绝该迁移 */
}
UINT16 APPL_StopInputHandler(void) { return 0U; }
UINT16 APPL_StartOutputHandler(void) { return ALSTATUSCODE_INVALIDALCONTROL; }
UINT16 APPL_StopOutputHandler(void) { return 0U; }
void APPL_AckErrorInd(UINT16 stateTrans) { (void)stateTrans; }
void APPL_InputMapping(UINT16 *pInputData) { (void)pInputData; }
void APPL_OutputMapping(UINT16 *pOutputData) { (void)pOutputData; }
void APPL_Application(void) { }
/*============================================================================
* EcatTask
*==========================================================================*/
extern "C" UINT16 MainInit(void);
extern "C" void MainLoop(void);
extern "C" bool bootEcatBringup(void);
static bool s_started = false;
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 */
/** @brief 空闲时 MainLoop 最小间隔。FreeRun 每圈 csrRead 双读 = EXMC 20+ 线翻转,
* 邮箱-only 在 5ms 节拍下仍够用;OTA/ECAT 会话期间不限速。 */
constexpr uint32_t kEcatIdlePollMinMs = 5U;
uint32_t s_lastPollCyc = 0U;
}
/** @brief 上电调一次。硬件不通就整个 EtherCAT 栈不启,SPI OTA 照旧。 */
extern "C" void ecatTaskStart(void)
{
if (s_started) {
return;
}
s_started = true;
s_hwOk = bootEcatBringup();
if (!s_hwOk) {
return;
}
/* HW_Init() 必须显式调:MainInit() 不调它(见 ecatappl.c:571 注释),
* APP 侧就是因为零调用者才让 s_alEventMask 影子停在 0xFFFF。 */
(void)HW_Init();
(void)MainInit();
s_lastPollCyc = DWT->CYCCNT;
}
/** @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();
(void)kTickCycles;
}