变更点: 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 过程目录
456 lines
19 KiB
C++
456 lines
19 KiB
C++
/**
|
||
* @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;
|
||
}
|