From 70daa4b0c6cd1ca45f73fd18e1cbb054bc58ae01 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sat, 5 Sep 2026 20:07:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(DCU=5FAPP):=20=E6=B7=BB=E5=8A=A0CPU?= =?UTF-8?q?=E8=B4=9F=E8=BD=BD=E7=9B=91=E6=8E=A7=E5=92=8CEtherCAT=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在DCU_APP项目中添加cpu_load.h和cpu_load.cpp模块用于CPU负载率监测 - 修改csolution配置将构建目标从Debug切换为Release - 在EtherCAT PDO协议栈中新增段5诊断区(632-639字节),包含负载率和序列号 - 在所有关键中断服务程序中添加CpuLoad::isrEnter()/isrExit()钩子用于准确统计中断占用 - 增强EtherCAT同步中断处理,改进AL事件掩码守护机制,修复OP状态下PDI中断门控问题 - 解决SSC源码中指针地址对齐警告和打包成员访问警告问题 - 在主循环中集成CPU负载率采样tick()调用,实现1秒滚动窗口统计 BREAKING CHANGE: 构建配置从Debug改为Release,EtherCAT协议栈增加新的诊断数据区 --- DCU_APP.csolution.yml | 2 +- .../GD32E508_DCU_APP.cproject.yml | 5 + GD32E508_DCU_APP/comm/ethercat_can_bridge.h | 15 +++ GD32E508_DCU_APP/comm/ethercat_pdo.cpp | 21 ++++ GD32E508_DCU_APP/comm/ethercat_pdo.h | 8 ++ GD32E508_DCU_APP/comm/ethercat_sync.cpp | 23 +++- GD32E508_DCU_APP/comm/ssc/port/dc_app.c | 47 ++++++- GD32E508_DCU_APP/comm/ssc/src/objdef.c | 5 +- GD32E508_DCU_APP/comm/ssc/src/sdoserv.c | 7 +- GD32E508_DCU_APP/device/CANFD.cpp | 19 ++- GD32E508_DCU_APP/device/Timer.cpp | 8 +- .../device/spi/app_spi_command.cpp | 5 + GD32E508_DCU_APP/device/usart0.cpp | 8 +- GD32E508_DCU_APP/main.cpp | 8 ++ GD32E508_DCU_APP/sys/config.h | 6 +- GD32E508_DCU_APP/sys/cpu_load.cpp | 117 ++++++++++++++++++ GD32E508_DCU_APP/sys/cpu_load.h | 97 +++++++++++++++ GD32E508_DCU_APP/sys/systick.cpp | 8 +- GD32E508_DCU_APP/sys/task_runner.cpp | 16 +++ 19 files changed, 398 insertions(+), 27 deletions(-) create mode 100644 GD32E508_DCU_APP/sys/cpu_load.cpp create mode 100644 GD32E508_DCU_APP/sys/cpu_load.h diff --git a/DCU_APP.csolution.yml b/DCU_APP.csolution.yml index 27f1bf4..e0dd50c 100644 --- a/DCU_APP.csolution.yml +++ b/DCU_APP.csolution.yml @@ -30,7 +30,7 @@ solution: target-set: - set: images: - - project-context: GD32E508_DCU_APP.Debug + - project-context: GD32E508_DCU_APP.Release debugger: name: ST-Link@pyOCD clock: 4000000 diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index eb45995..6e04674 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -16,6 +16,8 @@ project: - file: ./sys/task_manager.cpp - file: ./sys/task_runner.h - file: ./sys/task_runner.cpp + - file: ./sys/cpu_load.h + - file: ./sys/cpu_load.cpp - file: ./sys/ota_boot_request.h - file: ./sys/ota_boot_request.cpp - file: ./sys/aes128.h @@ -163,6 +165,9 @@ project: - -DHXTAL_VALUE=8000000U - -gdwarf-5 - -D__MICROLIB + # 启动文件经 legacy armasm 汇编:消 armclang 驱动"参数未使用"告警 + armasm 弃用告警(A1950W) + - -Wno-unused-command-line-argument + - -Wa,armasm,--diag_suppress=A1950W Link: - --library_type=microlib - --entry=Reset_Handler diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h index 10d3bd6..b345d5c 100644 --- a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h @@ -137,6 +137,18 @@ enum : uint16_t { ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */ }; +/** 输入 PDO 段 5(632..639):设备诊断区(1.5.61 新增,尾部追加)。 + * 批量接收区最坏情况(全 DLC64 满帧)只用到 364+3×72=580,580..639 永久空闲, + * 诊断区锚在 632 起的 8 字节,写侧是 EthercatPdo::process()。 */ +enum : uint16_t { + ECAT_DIAG_OFF_LOAD = 632U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。 + 中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比, + 不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */ + ECAT_DIAG_OFF_RSVD = 634U, /**< 保留 2B */ + ECAT_DIAG_OFF_SEQ = 636U, /**< 诊断区序号,小端 uint32,每完成一个统计窗口 +1, + 主站据此判断负载值是否仍在滚动刷新 */ +}; + /** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */ constexpr uint8_t kEcatCanTxDataMax = 32U; /** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */ @@ -183,6 +195,9 @@ static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越 /* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */ static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64"); static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64"); +/* 段 5 诊断区必须在批量接收区的最坏占用之外(改批量区布局时一起核对) */ +static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)kEcatRxBatchMax * (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax) + <= ECAT_DIAG_OFF_LOAD, "批量接收区最坏占用与段5诊断区重叠"); /** * @brief EtherCAT ↔ CAN 转发桥。 diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.cpp b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp index 47e53d9..d85b81c 100644 --- a/GD32E508_DCU_APP/comm/ethercat_pdo.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp @@ -4,6 +4,7 @@ */ #include "comm/ethercat_pdo.h" #include "comm/ethercat_can_bridge.h" +#include "sys/cpu_load.h" #include @@ -32,6 +33,26 @@ void EthercatPdo::process() * processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */ EthercatCanBridge::processOutput(s_out); EthercatCanBridge::buildInput(s_in); + fillDiagTail(s_in); +} + +/** + * @brief 填充输入 PDO 段 5 设备诊断区(632..639,1.5.61)。 + * @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。 + * @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。 + * 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。 + */ +void EthercatPdo::fillDiagTail(uint8_t *in) +{ + const uint16_t load = CpuLoad::loadPermille(); + in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU); + in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8); + + const uint32_t seq = CpuLoad::windowSeq(); + in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq & 0xFFU); + in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)((seq >> 8) & 0xFFU); + in[ECAT_DIAG_OFF_SEQ + 2] = (uint8_t)((seq >> 16) & 0xFFU); + in[ECAT_DIAG_OFF_SEQ + 3] = (uint8_t)((seq >> 24) & 0xFFU); } void EthercatPdo::writeInputs(uint8_t *esc) diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.h b/GD32E508_DCU_APP/comm/ethercat_pdo.h index 77d8111..9e54a6a 100644 --- a/GD32E508_DCU_APP/comm/ethercat_pdo.h +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.h @@ -77,6 +77,14 @@ public: static void snapshotOutput(uint8_t *dst); private: + /** + * @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节,1.5.61)。 + * @param[out] in 全长输入过程数据缓冲。 + * @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带 + * CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。 + */ + static void fillDiagTail(uint8_t *in); + /** @brief 最近一份主站下发的输出数据。 */ static uint8_t s_out[kPdoOutBytes]; /** @brief 待交回主站的输入数据。 */ diff --git a/GD32E508_DCU_APP/comm/ethercat_sync.cpp b/GD32E508_DCU_APP/comm/ethercat_sync.cpp index 13e2724..802096b 100644 --- a/GD32E508_DCU_APP/comm/ethercat_sync.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_sync.cpp @@ -9,6 +9,7 @@ #include "comm/ethercat_sync.h" #include "sys/config.h" #include "main.h" /* g_app_uptime_sec */ +#include "sys/cpu_load.h" #include extern "C" { @@ -168,13 +169,23 @@ extern "C" { #if (CONFIG_ETHERCAT_SYNC_MODE == 0) __attribute__((section(".ramfunc"), noinline)) -void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); } -void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); } -void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); } +void EXTI0_IRQHandler(void) +{ + CpuLoad::isrEnter(); + EthercatSync::onSync0Exti(); + CpuLoad::isrExit(); +} +void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); } +void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); } #else -void TIMER0_UP_TIMER9_IRQHandler(void) { EthercatSync::onTimerIrq(); } -void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); } -void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); } +void TIMER0_UP_TIMER9_IRQHandler(void) +{ + CpuLoad::isrEnter(); + EthercatSync::onTimerIrq(); + CpuLoad::isrExit(); +} +void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); } +void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); } #endif /* ---- SSC 诊断对象 0x2000 的 C 侧访问 (dc_app.c APPL_InputMapping 每周期拉取) ---- */ diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index d7ab8ce..3a5d941 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -389,8 +389,8 @@ UINT32 EcatDbg_ReadAlDiag(void) } /*--------------------------------------------------------------------------- - * AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展 - * (C 链接, task_runner 1s 节流调用) + * AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49/1.5.60 扩展 + * (C 链接, task_runner 250ms 节流调用) * * 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出 * 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直 @@ -401,6 +401,13 @@ UINT32 EcatDbg_ReadAlDiag(void) * * 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是 * "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。 + * 1.5.60 加固:期望值 u16ALEventMask 本身也可能缺 SM2 位——下行转换路径 + * (ecatslv.c AL_ControlInd 尾部)同时清 0x204 的 PDO 事件位、bEscIntEnabled、 + * bEcatFirstOutputsReceived,恢复全靠 SAFEOP 重入的 StartInputHandler + + * SetALEventMask(ecatslv.c:1506);重入没走全时 0x204 丢位。旧判定只比 + * "actual ⊇ 期望值",而期望值变量在下行路径并不清零——若错的是期望值本身, + * 比对恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的不变式是 + * u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086),先补期望再比对。 * * 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位 * = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位 @@ -408,6 +415,15 @@ UINT32 EcatDbg_ReadAlDiag(void) * 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个 * MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/ * 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。 + * + * 自愈(3)OP 态 PDI_Isr 门控(1.5.60):AL==OP 且 bEscIntEnabled==FALSE 不可 + * 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态; + * 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。 + * 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结 + * ——主站侧表现即楔死看门狗盯的窗口回显冻结(2026-09-05 实测 250µs 档每 + * 20~35s 触发一次重拉,wr 两小时涨 38)。修法:重开门 + 应答挂起的 SM2 事件 + * (掩码补位由守护(1)在同次调用完成),下一帧 LRW 的 SM2 中断即恢复整条 + * 过程数据链路,主站看门狗(2s 阈值)从此摸不到触发条件。 *---------------------------------------------------------------------------*/ UINT32 EcatDbg_GuardAleventMask(void) { @@ -474,7 +490,12 @@ UINT32 EcatDbg_GuardAleventMask(void) evt = HW_GetALEventRegister(); HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET); - /* (1) 0x204 缺位自愈 */ + /* (1) 0x204 缺位自愈(1.5.60 加固:先补期望值,再比对补实际值) */ + if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) { + u16ALEventMask |= PROCESS_OUTPUT_EVENT; + printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n", + (unsigned)u16ALEventMask); + } if ((UINT16)(actual & u16ALEventMask) != u16ALEventMask) { const UINT16 before = actual; HW_SetALEventMask(u16ALEventMask); @@ -498,5 +519,25 @@ UINT32 EcatDbg_GuardAleventMask(void) (unsigned)(bDcSyncActive ? 1U : 0U), (unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState); } + + /* (3) OP 态 PDI_Isr 门控自愈(1.5.60 新增):OP 下 bEscIntEnabled==FALSE + * 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 + * AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP + * 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282), + * PDO_OutputMapping 停调、输入冻结,只能靠主站看门狗重拉总线。 + * 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字); + * 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */ + if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) { + HW_EscReadWord(wdState, ESC_PD_WD_STATE); + bEscIntEnabled = TRUE; + HW_EscReadWord(dummy, nEscAddrOutputData); + HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U)); + printf("[ECAT][AEM] WARN: OP with PDI_Isr gated, evt=0x%04X " + "(bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) " + "-> gate reopened + event acked\r\n", + (unsigned)evt, + (unsigned)(bDcSyncActive ? 1U : 0U), + (unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState); + } return actual; } diff --git a/GD32E508_DCU_APP/comm/ssc/src/objdef.c b/GD32E508_DCU_APP/comm/ssc/src/objdef.c index 55789eb..fad2f38 100644 --- a/GD32E508_DCU_APP/comm/ssc/src/objdef.c +++ b/GD32E508_DCU_APP/comm/ssc/src/objdef.c @@ -887,14 +887,15 @@ UINT8 OBJ_Read( UINT16 index, UINT8 subindex, UINT32 objSize, OBJCONST TOBJECT O p = (CHAR **) pVarPtr; pVarPtr = (UINT16 MBXMEM *)p[subindex-1]; - if((((UINT16)pVarPtr) & 0x1) == 0x1) + if((((UINT32)pVarPtr) & 0x1) == 0x1) { /*enum is stored at an odd address*/ UINT16 cnt = 0; //get last even WORD address /*ECATCHANGE_START(V5.11) SDO3*/ - pVarPtr = (UINT16 MBXMEM *)(((MEM_ADDR)pVarPtr)& ~(MEM_ADDR)0x1); + /*MEM_ADDR=UINT16 会把 32 位指针截成 16 位,必须用完整位宽清 bit0*/ + pVarPtr = (UINT16 MBXMEM *)(((UINT32)pVarPtr) & ~(UINT32)0x1); /*ECATCHANGE_END(V5.11) SDO3*/ for(cnt = 0; cnt < (size / 2);cnt++) diff --git a/GD32E508_DCU_APP/comm/ssc/src/sdoserv.c b/GD32E508_DCU_APP/comm/ssc/src/sdoserv.c index d178a02..e9da339 100644 --- a/GD32E508_DCU_APP/comm/ssc/src/sdoserv.c +++ b/GD32E508_DCU_APP/comm/ssc/src/sdoserv.c @@ -1025,7 +1025,8 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd ) UINT16 n = OBJ_GetNoOfObjects(i); /* copy the number of objects of the list type in the SDO Information response */ - ((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n); + /*(UINT32) 中转:对 packed 成员直接取址强转 UINT16 指针会报 -Waddress-of-packed-member(邮箱缓冲实际偶对齐)*/ + ((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n); } /* size of the mailbox service response */ @@ -1045,7 +1046,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd ) size = u16SendMbxSize - SIZEOF_SDOINFO - MBX_HEADER_SIZE; } /* initialize pData with the pointer where the fragment has to be copied */ - pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1]; + pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1]; /* initialize index with the next index to be sent */ index = nSdoInfoIndex; /* decrement the number of fragments to be sent */ @@ -1063,7 +1064,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd ) size = u16SendMbxSize - SIZEOF_SDOINFOLISTSTRUCT - MBX_HEADER_SIZE; } /* initialize pData with the pointer where the fragment has to be copied */ - pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1]; + pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1]; /*Check if List need to be send in fragments*/ if( (n<<1) > size) { diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index a6f6186..81f4736 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -7,6 +7,7 @@ #include "device/canfd.h" #include "sys/config.h" #include "sys/critical.h" +#include "sys/cpu_load.h" #include "sys/systick.h" #include "sys/task_manager.h" #include @@ -1593,10 +1594,12 @@ bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out) * @retval 无返回值。 * @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(0U)`。 */ -extern "C" void CAN0_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(0U); } +extern "C" void CAN0_RX0_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(0U); CpuLoad::isrExit(); } /** @brief CAN0 TX 完成中断向量入口。 */ -extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); } +extern "C" void CAN0_TX_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(0U); CpuLoad::isrExit(); } /** * @brief CAN1 RX0 中断向量入口。 @@ -1605,10 +1608,12 @@ extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); } * @retval 无返回值。 * @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(1U)`。 */ -extern "C" void CAN1_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(1U); } +extern "C" void CAN1_RX0_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(1U); CpuLoad::isrExit(); } /** @brief CAN1 TX 完成中断向量入口。 */ -extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); } +extern "C" void CAN1_TX_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(1U); CpuLoad::isrExit(); } /** * @brief CAN2 RX0 中断向量入口。 @@ -1617,7 +1622,9 @@ extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); } * @retval 无返回值。 * @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(2U)`。 */ -extern "C" void CAN2_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(2U); } +extern "C" void CAN2_RX0_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(2U); CpuLoad::isrExit(); } /** @brief CAN2 TX 完成中断向量入口。 */ -extern "C" void CAN2_TX_IRQHandler(void) { CanFd::handleTxInterrupt(2U); } +extern "C" void CAN2_TX_IRQHandler(void) +{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(2U); CpuLoad::isrExit(); } diff --git a/GD32E508_DCU_APP/device/Timer.cpp b/GD32E508_DCU_APP/device/Timer.cpp index 9514d69..2d6fb60 100644 --- a/GD32E508_DCU_APP/device/Timer.cpp +++ b/GD32E508_DCU_APP/device/Timer.cpp @@ -7,6 +7,7 @@ #include "device/timer.h" #include "sys/task_manager.h" #include "sys/systick.h" +#include "sys/cpu_load.h" #include "sys/config.h" /** @@ -105,4 +106,9 @@ void SchedulerTimer::onInterrupt() * @retval 无返回值。 * @note C 向量包装函数,内部转调 `SchedulerTimer::onInterrupt()`。 */ -extern "C" void TIMER2_IRQHandler(void) { SchedulerTimer::onInterrupt(); } +extern "C" void TIMER2_IRQHandler(void) +{ + CpuLoad::isrEnter(); + SchedulerTimer::onInterrupt(); + CpuLoad::isrExit(); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index 12f20c4..a04cf35 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -37,6 +37,7 @@ extern "C" { #include "sys/config.h" #include "sys/critical.h" #include "sys/systick.h" +#include "sys/cpu_load.h" #include #include @@ -878,10 +879,14 @@ void AppSpiCommand::onCsExtiInterrupt() extern "C" void SPI0_IRQHandler(void) { + CpuLoad::isrEnter(); AppSpiCommandBus.onInterrupt(); + CpuLoad::isrExit(); } extern "C" void EXTI10_15_IRQHandler(void) { + CpuLoad::isrEnter(); AppSpiCommandBus.onCsExtiInterrupt(); + CpuLoad::isrExit(); } diff --git a/GD32E508_DCU_APP/device/usart0.cpp b/GD32E508_DCU_APP/device/usart0.cpp index ddcb094..d38209d 100644 --- a/GD32E508_DCU_APP/device/usart0.cpp +++ b/GD32E508_DCU_APP/device/usart0.cpp @@ -7,6 +7,7 @@ #include "device/usart0.h" #include "sys/config.h" #include "sys/critical.h" +#include "sys/cpu_load.h" uint8_t Usart0::s_txBuf[Usart0::kTxBufSize]; volatile uint16_t Usart0::s_txHead = 0U; @@ -182,4 +183,9 @@ extern "C" int fputc(int ch, FILE *f) { return Usart0::putChar(ch, f); } * @retval 无返回值。 * @note 该 C 包装函数内部转调 `Usart0::onInterrupt()`。 */ -extern "C" void USART0_IRQHandler(void) { Usart0::onInterrupt(); } +extern "C" void USART0_IRQHandler(void) +{ + CpuLoad::isrEnter(); + Usart0::onInterrupt(); + CpuLoad::isrExit(); +} diff --git a/GD32E508_DCU_APP/main.cpp b/GD32E508_DCU_APP/main.cpp index 06188f7..75ec1eb 100644 --- a/GD32E508_DCU_APP/main.cpp +++ b/GD32E508_DCU_APP/main.cpp @@ -5,6 +5,7 @@ #include "main.h" #include "sys/initial.h" +#include "sys/cpu_load.h" #include "sys/task_manager.h" #include "sys/task_runner.h" #include "sys/config.h" @@ -45,8 +46,15 @@ int App::run() APP_LOGI("[APP] >>> TaskRunner ready, entering main loop\r\n"); printBanner(); + CpuLoad::init(); + while(1) { + /* 负载率测量(1.5.63 定义收窄):runPending 内部按"非延迟工作"分段挂钩 + * (ISR 全量 + 任务派发 + 电机 OTA relay,见 sys/cpu_load.h); + * 常开轮询(SSC MainLoop / 各 poll)与 NOP 空转不计入。 + * tick() 每圈无条件做 1s 窗口滚动检查——滚动不依赖本圈是否有挂钩段。 */ TaskRunner::runPending(); + CpuLoad::tick(); #if (CONFIG_MAIN_LOOP_WFI_ENABLE != 0) uint32_t save = RT_CRITICAL_SAVE(); if(TaskManager::state().task_flags == 0U) { diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 0bfc663..afa0be0 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -120,7 +120,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 5U; -constexpr uint32_t APP_FW_VERSION_PATCH = 58U; /* 1.5.58:修复 EtherCAT 链路 CONFIG 回显目标值偶发离谱浮点(如 -3.2e26°)。根因:桥段 1/段 3"最近一帧快照"是多字段一组(id/dlc/data),生产者 onCanFrame(主循环 taskCanRxParse) 先写数据后写 id,而 s_rxValid 置位后永不清零,原"最后置位保证一致"注释不成立;DC 模式下 buildInput 在 Sync0_Isr(preempt=0,critical.h 的 BASEPRI 方案挡不住它)执行,ISR 抢占在数据 memcpy 与 id 赋值之间时,PDO 带出"新 STATUS 数据+旧 CONFIG ID",前端 dispatchFrame 按 CONFIG 布局解 STATUS 字节(电压 B11-12+角度低字节 B13-14 被当成 Target_Angle)。误标窗口只在 CONFIG 回显后的下一帧 STATUS 更新瞬间存在,故"调用时偶发";STATUS 走段 2 窗口按槽 seq 判活不受影响,与症状吻合。修法:快照组加 seqlock(s_xrxSeq 奇=正在写,前后 __DMB 防编译器把非 volatile 的 memcpy 挪出窗口),消费者判一次奇偶(单核 ISR 期间生产者必然冻结),奇数则本周期跳过段 1/段 3(开头已清零=无效),下周期自愈;零关中断、零 Sync0 抖动。配套:批量环 head/tail 发布前补 __DMB——同类编译器重排隐患,volatile 自序不约束相邻非 volatile memcpy。 1.5.57:EXMC 时序余量修复——rw/wr_timing.bus_latency 1→4。LAN9252 DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥13ns,该间隔由 EXMC 在两次访问间插入的总线空闲期承担 =(1 固有 + BUSLAT) HCLK:原 BUSLAT=1 只有 2 HCLK≈11.1ns,**低于 13ns 下限**,常温靠 AHB 取指间隙偶然兜底刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界——正是实测到的"温漂/压降时偶发读到 0xFFFF 或错值"。BUSLAT=4 → 5 HCLK≈27.8ns,余量 2.1 倍覆盖全温区。注意 2026-08-20 那轮"慢档 16/16/256 无效故排除速度因素"的结论只对当时的症状成立:那次读全 0/FFFF 的真因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),时序余量需按 DS 独立核算,不能沿用该结论认为时序无关。代价:每次 HBI 访问多 3 HCLK(16.7ns),满载 1024B 过程数据按每 DWORD 两次 16-bit 访问计增加约 8.6us(相对原 218us 增幅 3.9%),4ms 周期占用 5.4%→5.7%,无实质影响。配套:SNTCFG0 按位域解码回读打印(ASET/AHLD/DSET/BUSLAT),BUSLAT<4 时打 WARN,避免位域写错或库截断静默失效——烧录后串口应见 BUSLAT=4。 1.5.56:扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。 1.5.54:EtherCAT 收发模块类化——新增 comm/ethercat_pdo.h/.cpp 的 EthercatPdo 类,与 EthercatSlave / EthercatSync 同风格的全静态类。SSC 把一个 EtherCAT 周期拆成三个回调,类按这三步各给一个入口:APPL_OutputMapping→readOutputs()(取走主站下发的 32B)、APPL_Application→process()(跑应用逻辑,当前是 PDO↔CAN 转发)、APPL_InputMapping→writeInputs()(交回 32B)。过程数据缓冲区(s_out/s_in)与周期计数(outputCycles/inputCycles,可作"主站确实在发过程数据"的证据)归本类所有;dc_app.c 退回纯 SSC 移植层,只保留对象字典(0x7000/0x6000 的 packed 结构体是 SDO 存储必须留在 C 侧)与三行 extern "C" 转发(EcatPdo_ReadOutputs/Process/WriteInputs)。process() 内先 processOutput 后 buildInput,顺序不能反:buildInput 要把本周期刚执行的 seq 回显成 ack,反过来 ack 会恒滞后一拍。输入数据仍经 0x6000 对象存储中转再交给 ESC,保证主站 SDO 读 0x6000 与过程数据内容一致。不加临界区:DC 模式下 readOutputs/writeInputs 在 Sync0_Isr、process() 在主循环,但两者都是单写者整块读写,32B memcpy 撕裂最坏让某周期读到上/下半帧混合,而 seq 是单字节要么旧要么新不会产生第三个值,沿触发语义无害——在 ISR 里关中断反而拖累抖动。 1.5.53:新增 EtherCAT PDO ↔ CAN 转发桥(comm/ethercat_can_bridge.h/.cpp),主站可直接用过程数据收发 CAN 帧。布局:输出 PDO(0x7000,32B)=[0]seq [1]ch [2]dlc [3]flags [4..7]id(LE) [8..31]data(24B);输入 PDO(0x6000,32B)=[0]ack(回显 seq) [1]ch [2]dlc [3]flags(bit0=有效,bit1=截断) [4..7]id [8..15]data(8B) [16..19]rx计数 [20]最近发送结果 [21]TX队列积压 [22..23]tx计数 [24..25]丢帧数。关键设计:(1)**seq 变化沿触发发送**——PDO 是周期性刷新的,若按“内容非空即发”会把同一帧每周期重发;且上电/重连后首次只记基准 seq 不发送(s_seqPrimed),防主站残留的非零 seq 在进 OP 瞬间凭空发一帧。(2)**发送用 enqueueTx 而非 transmitFrame**:后者默认 allow_abort=true 会在邮箱满时abort 掉已装载的旧帧(历史上 7 帧突发只发出 3 帧的根因),PDO 周期性多帧正是该场景。(3)**RX 不自己 consumeRxFrame**:该队列已有唯一消费者 taskCanRxParse(),再加一个会与OTA relay/电机状态互抢帧;改为在 task_runner.cpp 分发链末尾**旁路**调 onCanFrame(),不 return 不 continue,不影响原有消费者。(4)dc_app.c 是 C 文件而 CanFd 是 C++ 类,经 extern "C" shim 调用。原 Phase 2 的 32B 回环被取代:回环的验证价值改由输入 PDO 的 seq 回显承担(写 seq=N 下周期应读回 ack=N,同样证明 输出→应用→输入 整条链路通)。配套:tools/ecat 新增 /api/can/send|/api/can/state 与网页 CAN 面板。 1.5.52:根治"反复启停后从站邮箱楔死"(主站侧任何手段都救不回、断电才恢复的硬楔死)——从站固件守护自愈。 +constexpr uint32_t APP_FW_VERSION_PATCH = 63U; /* 1.5.63:负载率定义收窄——1.5.61 实机实测恒 99.4%,根因是 runPending 每圈无条件跑 SSC MainLoop 轮询/各 poll() 检查,把常开轮询计入 busy 后指标饱和失去参考价值。改为只计「中断 + 非延迟处理段」占墙钟比:ISR 挂钩不变(TIMER2/SysTick/EXTI0/1/2/TIMER0/CAN0-2 RX+TX/SPI0+EXTI10_15/USART0,DC 模式 Sync0 的 EXMC 过程数据搬运仍计入),主循环侧撤销整圈 begin/end,改为 task_runner.cpp 三段(0xB0 进 BOOT 处理、MotorOtaRelay isActive() 门控的 relay 推进、takePending 任务派发段)挂 begin/end,常开轮询(Watchdog/AppSpiCommandBus.poll/MainLoop/ALE mask guard)与 NOP 空转不计入;1s 窗口滚动从 endWork 挪到主循环每圈无条件调用的 CpuLoad::tick(),空闲系统 seq 也按秒推进。PDO 段 5 布局/上报链路不变。 1.5.62:编译警告清理(固件行为零改动,生成代码与清理前一致)。(1) config.h 历史注释里嵌套的 1.5.47~1.5.51 块注释符使几乎每个编译单元报 -Wcomment,改用【1.5.4x】标注;(2) SSC sdoserv.c 3 处对 packed 成员 CoeHeader 取址强转 UINT16 指针报 -Waddress-of-packed-member,经 UINT32 整型中转消除(邮箱缓冲实际偶对齐,指令不变);(3) SSC objdef.c 2 处指针截断报 -Wpointer-to-int-cast——其中 MEM_ADDR 强转因 MEM_ADDR=UINT16(ecat_def.h:390) 会把 32 位指针截成 16 位,是 ENUM 奇地址路径的潜在 bug(本项目对象字典不含 ENUM 实际不执行),改用 UINT32 保真并顺带修正;(4) 启动文件 startup_gd32e50x_cl.s 走 legacy armasm:cproject.yml 的 ASM misc 增 -Wno-unused-command-line-argument(消 armclang 驱动 4 条参数未使用告警)与 -Wa,armasm,--diag_suppress=A1950W(消 armasm 弃用告警),已用真实工具链实测通过、对象文件仅内嵌命令行字符串有差异。 1.5.61:DCU_APP CPU 负载率经 EtherCAT 上报。新增 sys/cpu_load.{h,cpp}:DWT CYCCNT 计时、主循环任务与挂钩中断(TIMER2/SysTick/EXTI0/1/2/TIMER0/CAN0-2 RX+TX/SPI0/EXTI10_15/USART0)计入同一 busy 区间求并集(单核内任意优先级嵌套不重不漏,LDREX/STREX 原子协议,热路径 .ramfunc 与 onSync0Exti 同款,不给 Sync0 入口添 flash 抖动),1s 滚动窗口出 0.1% 分辨率负载率。数值写入输入 PDO 段 5 诊断区(632..639,批量接收区最坏占用 580 之后永久空闲的尾部,只追加不动既有偏移,PDO 长度/SII/ESI 均不变无需重刷 EEPROM):ECAT_DIAG_OFF_LOAD u16 LE(单位 0.1%) + ECAT_DIAG_OFF_SEQ u32 LE(窗口序号)。主站 tools/ecat ecat_native.c decode_input 解码后经 /api/app/load 暴露,tools/ESP32 OTA 页「运行指标」卡经 /ecat/api 代理 1Hz 轮询显示「GD32 负载」。1.5.60:SM2 事件路径自愈补全——SM2 事件路径自愈补全——2026-09-05 真机 250µs(4kHz) 档主站楔死看门狗每 20~35s 重拉一次(wr 两小时 +38)、从站槽位更新 4200/s 塌到 28/s 的根因加固。(a) dc_app.c 守护(1) 加固:期望值 u16ALEventMask 本身可能缺 PROCESS_OUTPUT_EVENT——下行转换(ecatslv.c AL_ControlInd 尾部)清 0x204/bEscIntEnabled/bEcatFirstOutputsReceived 后,SAFEOP 重入(SetALEventMask @:1506)没走全时 0x204 丢位,而旧判定 actual⊇期望 在错的是期望值时恒通过、缺位永远补不回;现先补期望再比对。(b) dc_app.c 新增自愈(3):OP 态 bEscIntEnabled==FALSE 不可合法出现(下行尾 :1273 清,重入漏恢复段即 OP 带门关)→ PDI_Isr 整体被门控(ecatappl.c:282)、PDO_OutputMapping 停调、输入冻结——正是主站楔死看门狗盯的窗口回显冻结;守护重开门 + 应答挂起 SM2 事件,250ms 节拍内自愈,主站 2s 阈值从此摸不到触发条件。本版首次携带 1.5.59 的 mailbox 构建期常量 + PDI 字节序修复出固件(该轮改动此前只落在 dc_app.c/mailbox.c,config.h 未记档)。 1.5.58:修复 EtherCAT 链路 CONFIG 回显目标值偶发离谱浮点(如 -3.2e26°)。根因:桥段 1/段 3"最近一帧快照"是多字段一组(id/dlc/data),生产者 onCanFrame(主循环 taskCanRxParse) 先写数据后写 id,而 s_rxValid 置位后永不清零,原"最后置位保证一致"注释不成立;DC 模式下 buildInput 在 Sync0_Isr(preempt=0,critical.h 的 BASEPRI 方案挡不住它)执行,ISR 抢占在数据 memcpy 与 id 赋值之间时,PDO 带出"新 STATUS 数据+旧 CONFIG ID",前端 dispatchFrame 按 CONFIG 布局解 STATUS 字节(电压 B11-12+角度低字节 B13-14 被当成 Target_Angle)。误标窗口只在 CONFIG 回显后的下一帧 STATUS 更新瞬间存在,故"调用时偶发";STATUS 走段 2 窗口按槽 seq 判活不受影响,与症状吻合。修法:快照组加 seqlock(s_xrxSeq 奇=正在写,前后 __DMB 防编译器把非 volatile 的 memcpy 挪出窗口),消费者判一次奇偶(单核 ISR 期间生产者必然冻结),奇数则本周期跳过段 1/段 3(开头已清零=无效),下周期自愈;零关中断、零 Sync0 抖动。配套:批量环 head/tail 发布前补 __DMB——同类编译器重排隐患,volatile 自序不约束相邻非 volatile memcpy。 1.5.57:EXMC 时序余量修复——rw/wr_timing.bus_latency 1→4。LAN9252 DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥13ns,该间隔由 EXMC 在两次访问间插入的总线空闲期承担 =(1 固有 + BUSLAT) HCLK:原 BUSLAT=1 只有 2 HCLK≈11.1ns,**低于 13ns 下限**,常温靠 AHB 取指间隙偶然兜底刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界——正是实测到的"温漂/压降时偶发读到 0xFFFF 或错值"。BUSLAT=4 → 5 HCLK≈27.8ns,余量 2.1 倍覆盖全温区。注意 2026-08-20 那轮"慢档 16/16/256 无效故排除速度因素"的结论只对当时的症状成立:那次读全 0/FFFF 的真因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),时序余量需按 DS 独立核算,不能沿用该结论认为时序无关。代价:每次 HBI 访问多 3 HCLK(16.7ns),满载 1024B 过程数据按每 DWORD 两次 16-bit 访问计增加约 8.6us(相对原 218us 增幅 3.9%),4ms 周期占用 5.4%→5.7%,无实质影响。配套:SNTCFG0 按位域解码回读打印(ASET/AHLD/DSET/BUSLAT),BUSLAT<4 时打 WARN,避免位域写错或库截断静默失效——烧录后串口应见 BUSLAT=4。 1.5.56:扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。 1.5.54:EtherCAT 收发模块类化——新增 comm/ethercat_pdo.h/.cpp 的 EthercatPdo 类,与 EthercatSlave / EthercatSync 同风格的全静态类。SSC 把一个 EtherCAT 周期拆成三个回调,类按这三步各给一个入口:APPL_OutputMapping→readOutputs()(取走主站下发的 32B)、APPL_Application→process()(跑应用逻辑,当前是 PDO↔CAN 转发)、APPL_InputMapping→writeInputs()(交回 32B)。过程数据缓冲区(s_out/s_in)与周期计数(outputCycles/inputCycles,可作"主站确实在发过程数据"的证据)归本类所有;dc_app.c 退回纯 SSC 移植层,只保留对象字典(0x7000/0x6000 的 packed 结构体是 SDO 存储必须留在 C 侧)与三行 extern "C" 转发(EcatPdo_ReadOutputs/Process/WriteInputs)。process() 内先 processOutput 后 buildInput,顺序不能反:buildInput 要把本周期刚执行的 seq 回显成 ack,反过来 ack 会恒滞后一拍。输入数据仍经 0x6000 对象存储中转再交给 ESC,保证主站 SDO 读 0x6000 与过程数据内容一致。不加临界区:DC 模式下 readOutputs/writeInputs 在 Sync0_Isr、process() 在主循环,但两者都是单写者整块读写,32B memcpy 撕裂最坏让某周期读到上/下半帧混合,而 seq 是单字节要么旧要么新不会产生第三个值,沿触发语义无害——在 ISR 里关中断反而拖累抖动。 1.5.53:新增 EtherCAT PDO ↔ CAN 转发桥(comm/ethercat_can_bridge.h/.cpp),主站可直接用过程数据收发 CAN 帧。布局:输出 PDO(0x7000,32B)=[0]seq [1]ch [2]dlc [3]flags [4..7]id(LE) [8..31]data(24B);输入 PDO(0x6000,32B)=[0]ack(回显 seq) [1]ch [2]dlc [3]flags(bit0=有效,bit1=截断) [4..7]id [8..15]data(8B) [16..19]rx计数 [20]最近发送结果 [21]TX队列积压 [22..23]tx计数 [24..25]丢帧数。关键设计:(1)**seq 变化沿触发发送**——PDO 是周期性刷新的,若按“内容非空即发”会把同一帧每周期重发;且上电/重连后首次只记基准 seq 不发送(s_seqPrimed),防主站残留的非零 seq 在进 OP 瞬间凭空发一帧。(2)**发送用 enqueueTx 而非 transmitFrame**:后者默认 allow_abort=true 会在邮箱满时abort 掉已装载的旧帧(历史上 7 帧突发只发出 3 帧的根因),PDO 周期性多帧正是该场景。(3)**RX 不自己 consumeRxFrame**:该队列已有唯一消费者 taskCanRxParse(),再加一个会与OTA relay/电机状态互抢帧;改为在 task_runner.cpp 分发链末尾**旁路**调 onCanFrame(),不 return 不 continue,不影响原有消费者。(4)dc_app.c 是 C 文件而 CanFd 是 C++ 类,经 extern "C" shim 调用。原 Phase 2 的 32B 回环被取代:回环的验证价值改由输入 PDO 的 seq 回显承担(写 seq=N 下周期应读回 ack=N,同样证明 输出→应用→输入 整条链路通)。配套:tools/ecat 新增 /api/can/send|/api/can/state 与网页 CAN 面板。 1.5.52:根治"反复启停后从站邮箱楔死"(主站侧任何手段都救不回、断电才恢复的硬楔死)——从站固件守护自愈。 03:00 主站侧探针钉死完整证据链(楔死态、未断电): (1) ESC/SSC 邮箱处理全程活着:手动注入合法 CoE 请求(写满 SM0 256B)后 200ms 内 SM1 出响应; (2) 但响应帧头出现在 SM1 start+8 而非 start(逐会话累积:第一轮 +4、后续 +8),前 8 字节全零; @@ -139,7 +139,7 @@ constexpr uint32_t APP_FW_VERSION_PATCH = 58U; /* 1.5.58:修复 EtherCAT 链 主站重试成功,彻底消除"断电才恢复"。 主站工具同步(tools/ecat/ecat_server.py):config_map 失败转储扩展为全部 4 个 SM 寄存器 (0x0800..0x081F;此前 0x0810/0x0818 曾误标为 SM0/SM1,实为 SM2/SM3——已修正), - 并注明"SM0/SM1 配置正常却持续失败=SSC 邮箱地址错位楔死"的判别法。 /* 1.5.51:主站工具修复"反复启停后卡住→闪退"(tools/ecat/ecat_server.py),GD32 功能代码零改动(无需重新烧录;bump 仅为变更记录与编译纪律)。 + 并注明"SM0/SM1 配置正常却持续失败=SSC 邮箱地址错位楔死"的判别法。 【1.5.51】主站工具修复"反复启停后卡住→闪退"(tools/ecat/ecat_server.py),GD32 功能代码零改动(无需重新烧录;bump 仅为变更记录与编译纪律)。 02:05 复现脚本(faulthandler)钉死闪退根因:同一连接内反复 op/start↔op/stop 不重枚举时,从站邮箱逐轮退化 (第 2 轮起 OP 态 SDO 读 0x2000 全 WkcError——即用户日志"报个读取失败"),第 4 轮 config_map() 内 SOEM CoE 分段读(ethercatcoe.c ecx_SDOread 的 memcpy 无边界检查,上游缺陷)吃到楔死邮箱的错位帧 → 原生访问违例 @@ -158,7 +158,7 @@ constexpr uint32_t APP_FW_VERSION_PATCH = 58U; /* 1.5.58:修复 EtherCAT 链 主站任何手段(含断开重连)都救不回,必须断电重启——从站根因待串口抓取(下次复现时串口 看主循环 5s 心跳 [ECAT][HB] 是否停跳 + AEM WARN 是否出现)。 30 轮启停压测:修复前第 4 轮必崩(0xC0000005);修复后 30 轮零崩溃零卡死 - (第 16 轮起从站进入上述硬楔死、主站侧全部为可恢复的干净报错,直至压测结束)。 /* 1.5.50:主站工具诊断盲区修复(tools/ecat 两文件),GD32 功能代码零改动(无需重新烧录,板上 1.5.49 即最新;bump 仅为变更记录与编译纪律)。01:31 复测确认 1.5.48 的自动重试机制工作正常:首败("进 PREOP 失败: AL=INIT code=0x0000")后自动恢复,OP 真实建立、UI 周期正确增长。首败根因定位:停止回 INIT 时 SSC MBX_StopMailboxHandler 禁用了邮箱 SM0/SM1(mailbox.c:359-361),而 CheckSmSettings 在 INIT→PREOP 要求邮箱 SM 已使能(ecatslv.c:416-418),拒绝迁移且从站自身无重新使能路径(死锁闭环)——自动重试里 config_init() 重写 SM0/SM1(ethercatconfig.c:279-280)正是解药,与"断开重连有效"同机理。修复:PREOP/SAFEOP 失败分支改用 read_state() 取真实 AL Status Code(旧报文的 code=0x0000 是 SOEM 广播 state_check 不刷新 al_status 的陈旧缓存,与 OP 循环同款盲区),下次首败将显示真实错误码(预期 0x0016/0x001D 类 INVALIDMBXCFGINPREOP)确凿验证根因。 /* 1.5.49:SM2 事件楔死自愈——01:03 串口日志钉死现场(evt=0x0400 在 0x220 挂起 10s 无人处理、escirq 仅 2.6/s 只随 AL Control 事件跳动、9s ESM 超时报 0x001B、[ECAT][AEM] 一次未打印=其前置 bEscIntEnabled==FALSE 未满足):PDI_Isr 被调用了 13 次却从未执行 SM2 分支(ecatappl.c:287 的 ALEvent&PROCESS_OUTPUT_EVENT 从未命中),bEcatFirstOutputsReceived 永不置位,SAFEOP→OP 挂到 ESM 超时。dc_app.c EcatDbg_GuardAleventMask() 扩展自愈(2):SAFEOP 下检测"0x220 bit10 挂起 + 标志未置位"即从主循环直接替 PDI_Isr 完成工作——置位 bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款);协议等价:bit10 挂起=主站已写过 SM2=WKC=3/3 佐证的"收到输出"成立,AL_ControlRes 下一个 MainLoop 周期即完成 OP。守护(1) 0x204 缺位自愈保留并解除 bEscIntEnabled前置(改判 AL>=SAFEOP+nPdOutputSize>0)。WARN 携带全部内部旗标(bEscInt/bDcSync/mask_exp/mask_act/wd 看门狗状态),下次复现一行日志即可定位楔死路径(嫌疑:bEscIntEnabled 被某路径意外清零、或 PDI_Isr 读 0x220 的 CSR 间接端口结果不含 bit10)。自愈仅在 SAFEOP+标志未置位时触发,OP 态下 LRW 每周期置位/清除 bit10 属正常不误报。1.5.48:修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 /* 1.5.48:修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 /* 1.5.47:修复 free-run 连接概率性卡死在"SAFEOP+0x0000 等待 OP"的根因——两个叠加的固件缺陷:(A) ECAT_CheckTimer() 在整个固件里没有任何调用者!ecat_def.h 曾设 ECAT_TIMER_INT=1(要求端口提供 1ms 定时器 ISR 调 ECAT_CheckTimer),但本工程从未实现该 ISR,而 MainLoop 的轮询调用路径被 #if !ECAT_TIMER_INT 编译排除;后果是 EsmTimeoutCounter 永不递减→任何 pending 状态迁移(SAFEOP→OP)永不超时→从站无限期忽略一切后续 AL Control(实测第二次点击报"进 PREOP 失败: AL=SAFEOP"即此),DC_CheckWatchdog/PLL 计数器也全部死掉。修复:ECAT_TIMER_INT 1→0 + 新增 ECAT_TIMER_INC_P_MS=180000(DWT CYCCNT@180MHz,hw_gd32exmc.cpp 的 HW_GetTimerValue/HW_ClearTimerValue 本就是为此写好的死代码),MainLoop 每 1ms 轮询调 ECAT_CheckTimer,ESM 超时(9s)/DC 看门狗/总线周期计算全部复活。(B) ESC 残留 DC 激活位导致从站误入 DC 同步模式:之前跑过 DC 模式(dc_sync(True,...))在 ESC 0x0981 留下激活位,跨"断开重连"不清(仅断电/主站主动清);PREOP→SAFEOP 的 UpdateSyncManagerParameter(ecatslv.c:754)读到 DC unit active 就按 DCSYNC0/1 推导 SyncType→bDcSyncActive=TRUE→SAFEOP→OP 改等 PLL 序列,而 free-run 主站不发 SYNC0、PLL 计数器又在死掉的 DC_CheckWatchdog 里——双重死锁,LRW 每 7ms 也救不回(00:27 复测:轨迹恒 SAFEOP+0x0000)。修复(主站侧,tools/ecat 两文件):free-run 连接时在 config_map 之前 dc_sync(False,0,0) 主动清残留激活;OP 等待 3s→12s 覆盖"ESM 超时(9s)+ACK 清错重试"完整序列;等待期记录 LRW WKC 采样+失败时报真实 AL code/轨迹/看门狗实际值(get_watchdog)作证据链。另:1.5.46 引入的 state_check→read_state 已修正 code=0x0000 假读(SOEM ecx_statecheck 广播模式不更新 ALstatuscode)。1.5.46:主站工具(tools/ecat ecat_server.py/ecat_tool.py)修复 50ms 周期重启后"进 OP 失败/进 PREOP 失败 code=0x0000"仍复现的问题,GD32 侧代码零改动(1.5.45 的 0x0420=1s 看门狗修复保持不变)——bump 版本号并重编译纯粹是为了让烧录生效可验证(串口 init 报告/版本号能对上)。22:15 复测日志钉死了旧修复无效的三处原因:(a) 1.5.45 注释"同态写即 ack"是错误前提——SSC AL_ControlInd(ecatslv.c:1372-1387)仅在 AL Control 带 ACK 位(bit4=0x10)时清 AL Status 错误位并继续执行迁移,错误位置位时普通同态写同样落入"先清错误位后直接 return"的忽略分支;上一轮看门狗 0x001B 拖回 SAFEOP 残留的错误位若不先带 ACK 清掉,重写 SAFEOP/OP 全部石沉大海,这就是第二次点击"进入 OP"报"进 PREOP 失败: AL=SAFEOP code=0x0000"的机制(错误位在 state_check 期间被某次写清掉后 code 显示 0,但状态从未迁移)。现 _require_op_ready/enter_op 所有 AL Control 写统一带 STATE_ACK(pysoem 已导出,=EC_STATE_ACK=0x10),与 SOEM 官方 ecatcheck 的恢复套路一致(写 当前态|ACK 清错再写目标态);无错误时 ACK 位被从站无害消费。(b) OP 等待循环里 state_check(OP, 200ms) 使 LRW 帧间隔 ~205ms,稳超固件<1.5.45 的 100ms SM 看门狗——CheckIfEcatError()(ecatslv.c:2137-2151) 在从站未到 OP 时(bEcatOutputUpdateRunning=FALSE)每 100ms 把 bEcatFirstOutputsReceived 清回 FALSE,而 SAFEOP→OP 的 AL_ControlRes 完成条件恰恰依赖该标志(ecatslv.c:1993),于是迁移挂起直到 ESM 超时回落;轮询收紧到 20ms(帧间隔 ~25ms)。注意:固件≥1.5.45(看门狗 1s)后 (b) 不再致命,但 20ms 轮询对任何看门狗配置都安全。(c) 回落判断须先 0x0F 掩掉错误位:PREOP+错误=0x12>0x04,旧代码 `state < SAFEOP_STATE` 漏判跌落,直接重写 OP 又被忽略,空转到 3s 超时——修正为 (state&0x0F)100ms 全 WKC 错的根治仍在从站固件 1.5.45 的 configPdWatchdog(0x0420=10000×100µs=1s),网页周期下拉最大 200ms 远小于 1s;若烧录 1.5.46 后 100/200ms 仍报 WKC 错误,抓 GD32 串口确认上电有无 "[ECAT][ESC] PD watchdog = 1s"(WARN=0x0420 写失败需查 HBI)。1.5.45:修复主站 100ms 周期全 WKC 错 + 重启有概率卡死。硬件实测(ecat_esc_regs.py RawBus 裸帧)钉死:0x0400=0x09C2(分频 100µs)/0x0420=0x03E8(1000),即 SM 看门狗正好 100ms,且 SII 不携带看门狗字段、这就是芯片复位默认值;探针实测 100ms 周期 wkc 19/20 错(wkc=1,espected=3)、10/50ms 全对、重启 5 轮 1 轮卡 PREOP code=0x0000,与用户现象完全一致。机制:主站写 SM2 后若 100ms 内没有新的写访问,ESC 看门狗到期(0x0440 bit0 清零),SSC CheckIfEcatError()(ecatslv.c:2133)读到后调 AL_ControlInd(STATE_SAFEOP, 0x001B) 拖回 SAFEOP,同时 DisableSyncManChannel(SM2)(ecatslv.c:1740-1746)——SM2 一旦被禁用,主站 LRW 不再写进 SM2、WKC 不累加,表现为 wkc=1 持续报错,直到重连走完整 INIT→PREOP→SAFEOP 才由 StartInputHandler 重新使能 SM2,这就是"WKC错误=周期数-1、断开重连后 WKC=0"的完整链条。修复(1):EscLan9252::configPdWatchdog() 在 init() 的 PDI 就绪分支里把 0x0420 写成 10000(1s),分频保持默认 100µs;正常 10/50ms 周期行为不变,100ms 手动调试周期不再被拖回。修复(2),主站工具(tools/ecat 两文件):重启卡死的根因是 SSC 对带 STATE_CHANGE 错误位的新 AL 请求会忽略(AL_ControlInd ecatslv.c:1376-1384 先清错误位后直接 return)——上一轮看门狗 0x001B/断开时留下的错误位没被 ack,直接 write_state(OP) 无人应答,断开重连之所以能恢复是因为 config_init 的状态轮询恰好完成了 ack。现在 OP 等待循环里若从站跌回 PREOP/INIT,先重写 SAFEOP(同态写即 ack)立起状态再重请 OP。【注:修复(2) 中"同态写即 ack"的前提已被 1.5.46 推翻并修正为带 ACK 位写,见上】 1.5.44:修复 OP 下输入 PDO 几乎每周期 WKC 不计数(主站实测已进 OP、输出 32B/输入 32B正常,但 89 个周期里 87 次 wkc < expected_wkc)——根因是 dc_app.c 的 APPL_StartInputHandler() 写死 *pIntMask = 0x0800,把 SSC 算好的 AL Event Mask 顶掉了。pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已根据同步模式推导它:本工程 free-run + 32B 输出 PDO 时,ecatslv.c:959 置 u16ALEventMask = PROCESS_OUTPUT_EVENT(0x0400),:1090 再次确保该位;而 ecatslv.c:1506 的 SetALEventMask() 只做“或”不做赋值,因此旧代码令寄存器 0x204 永远缺 SM2 位、只剩 SM3(0x0800)。后果:主站写输出到 SM2 不再产生 IRQ,PDI_Isr 只能被“主站读 SM3”事件带起来,于是 PDO_InputMapping() 永远发生在主站读取**之后**;SM3 是 3-buffer 模式,主站读到从未被从站重写的缓冲就不累加 WKC,恰好解释“能进 OP、长度对、但几乎每周期 WKC 少”。官方 sampleappl.c/el9800appl.c 的 APPL_StartInputHandler 均只 return、不碰 pIntMask,现修为 (void)pIntMask 并注明原因。同时撤销上一轮在主站工具里加的 0x1C32:01/0x1C33:01 SyncType SDO 写(tools/ecat 两个文件)——它既错也无效:实测返回 SdoError 0x08000022(当前状态不可写),因为 0x1C3x:01 的权限是 ACCESS_WRITE_PREOP 而 pysoem 的 config_map() 已把从站推到 SAFEOP;且本来就不需要写,SSC 在 PREOP→SAFEOP 的 StartInputHandler() 会自动按 DC 寄存器与 PDO 长度推导出 0x1C32=SM_SYNCHRON/0x1C33=SM2_SYNCHRON(ecatslv.c:849-880),coeappl.c:466/513 的 SYNCTYPE_FREERUN 仅是上电初值,bSyncSetByUser 也默认 FALSE。 1.5.43:修复 SAFEOP->OP 永远进不去(主站报 "进 OP 失败: AL=PREOP code=0x0000",且已边发 processdata 边等 3s 仍无效)——根因是 LAN9252 的 IRQ 引脚从未被使能,PDI_Isr 一次都没执行过。IRQ_CFG(0x054) 与 INT_EN(0x05C) 是 LAN9252 系统 CSR(HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即 IRQ_EN=0(引脚永久去断言)+ ECAT_INT_EN=0(EtherCAT 事件不汇入 IRQ,DS 8.2.6/8.3.1,ECAT_INT 为 INT_STS/INT_EN 的 bit0);固件此前只配了 EXTI/GPIO/NVIC 侧(PB1 上拉输入+下降沿+preempt=1)与 EEPROM 的 SYNC0/1(0x0151=0xCC),从来没打开芯片侧的中断输出总闸。后果链:PDI_Isr 从不触发 → SSC 收不到 PROCESS_OUTPUT_EVENT → bEcatFirstOutputsReceived 恒 FALSE(该标志只在 PDI_Isr(ecatappl.c:305) 或 free-run 的 MainLoop 轮询分支(ecatappl.c:551, 条件 !bEscIntEnabled)里置位)→ AL_ControlInd 的 SAFEOP_2_OP 分支(ecatslv.c:1993 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`) 因本工程有 32B 输出而永不成立 → 迁移被挂起(bApplEsmPending)后回落,AL Status Code 保持 0x0000(不是被拒绝,故不给 0x001B/0x001A)。注意本工程 SyncType 使 UpdateSyncManagerParameter 置 bEscIntEnabled=TRUE(ecatslv.c:951),因此 MainLoop 的 free-run 轮询兜底分支不执行,中断是唯一通路。EscLan9252::init() 在 DL_STATUS bit0 确认 PDI 就绪后新增 enableEcatIrqOutput():先写 INT_STS 清 ECAT_INT 悬留状态(R/WC),再置 INT_EN.ECAT_INT_EN,最后开 IRQ_CFG 总闸并回读校验,打印 [ECAT][ESC] IRQ output enabled(失败则 WARN 并指明 SAFEOP->OP 会卡)。极性与 ethercat_sync.cpp 保持一致:推挽 IRQ_TYPE=1 + 低有效 IRQ_POL=0,配对上拉输入的下降沿 EXTI;INT_DEAS 保持 0=禁用去断言定时器。AL Event Mask(0x204) 仍由 SSC 在状态迁移时自行维护,不在此处干预。配套修主站工具(tools/ecat):(a) _require_op_ready/enter_op 请求 OP 后改为边 send_processdata/receive_processdata 边 state_check 循环至多 3s(SSC 要求先收到首帧输出才 ack OP),中途回落则重发 OP 请求,失败信息按 AL code 分流排查方向;(b) 修正 state_check 超时单位——pysoem docstring 明确 "Timeout value in us",旧代码各处传 2000 实际只等 2ms(SOEM 默认 EC_TIMEOUTSTATE=2000000=2s),PREOP/SAFEOP 能过纯属从站 ack 快的侥幸,现统一用 EC_TIMEOUT_STATE_US=2_000_000 常量。1.5.42:修复主站 config_map 失败(SdoError "Unsupported access to an object" / "Attempt to read to a write only object",实测 0x1600/0x1A00 的 sub0/3/4 报错而 sub1/2 通过)——两个独立的对象字典 bug,均在从站侧,与主站工具无关。(1) REC 对象的 entry descriptor 数组给少了:OBJ_GetEntryDesc()(objdef.c:554) 与 OBJ_GetEntryOffset()(objdef.c:609) 只对 OBJCODE_ARR 复用 pEntryDesc[1] 描述所有子索引,对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex];而 dc_app.c 把 0x1600/0x1A00/0x7000/0x6000/0x2000 全声明为 REC(4|OBJCODE_REC<<8) 却只提供 2 元素描述数组,sub3/sub4 越界读到相邻常量数组的垃圾 DataType/ObjAccess,正好解释"sub0/3/4 报错、sub1/2 侥幸通过"的错误模式(sub1/2 恰好落在合法项上)。现为 0x1600/0x1A00 补满 Sub0..Sub4 共 5 项(asPDOMappingEntryDesc),并把原共用的 asDataEntryDesc 拆成 asOutputEntryDesc/asInputEntryDesc(各 5 项)、asSyncDiagEntryDesc 补到 5 项;同时给数据子条目补 OBJACCESS_RXPDOMAPPING/TXPDOMAPPING(官方 sampleappl.h 的 0x7010/0x6000 同此,缺这两个标志主站校验 0x1600/0x1A00 的映射目标时会拒绝),输入侧 0x6000 由 READWRITE 收紧为 ACCESS_READ。0x1C12/0x1C13 是 ARR 保持 2 项不变。(2) 结构体从未被紧排:ecat_def.h 的 STRUCT_PACKED_START/MBX_STRUCT_PACKED_START/OBJ_STRUCT_PACKED_START 三对宏全部留空,注释还误写"AC6 不需要"——实际这些宏就是 SSC 紧排结构体的唯一手段,SSC 的 SDO 读写按"si0 占 16bit、其后各条目按 BitLength 紧排"算字节偏移(OBJ_GetEntryOffset),留空则按编译器自然对齐。armclang 编译期断言实测:{UINT16;UINT64[4]} 的 UINT64 落在字节 8 而非 2,{UINT16;UINT32 x4} 落在字节 4 而非 2,故 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存,SSC 内部 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响(PDO 路径走 memcpy 未受影响,故此前回环测试无法暴露)。三对宏改为 __attribute__((packed)),并在 dc_app.c 加 _Static_assert 守卫 offsetof==2 与 sizeof==34 防回归。1.5.41:修正三处会误导诊断的日志缺陷(1.5.39 的 LAN9252 访问模型修复经实测已端到端成立:IDX=0x1234、BYTE_TEST=0x87654321、DL_STATUS bit0=1 且 waited 0ms、PDI_CTRL=0x8D、ESC_CFG bit0=0 Device Emulation 关闭、EEP_CSR 无错误位、CSR 写回环 0x0204 写 0x0A50 读回 PASS、SSC MainInit OK;waited 0ms 同时反证 1.5.37「EEPROM 加载竞态」确不存在,读全 0 纯因走错通路)。(1) [ECAT][DIAG] 的 EEP_CSR 位标签错且语义反:0x0502 的 bit11 是 Checksum Error 而非 loaded(真正的加载状态在 bit12,且 0=OK;见 ETG.1000.4 表 49 与 SSC esc.h ESC_EEPROM_ERROR_CRC/LOAD),旧版打印 "loaded=%u" 取 bit11,正常板子恒显示 loaded=0,字面像「EEPROM 未加载」,正是 1.5.37 那个已撤回结论的诱因;现改为 crc_err/load_err/ack_err/wr_err/busy 分位打印并注明全 0=正常。(2) [ECAT][SLAVE] init 终判在无主站时是假警报:s_escReady 要求 AL>=SAFEOP,而 AL 迁移只由主站写 AL Control(0x0120) 驱动、从站不自跳,故冷启动必然打 FAIL(partial),与紧随其后的 SSC MainInit OK 自相矛盾;现按 DL_STATUS 的 Physical Link 位 bit4/5/6(DS 12.14.19) 判定有无主站,空载报 WAIT_MASTER(实测空载 0x5503 三口 link/comm 全 0,接主站 0x5611),link up 却卡 AL 才报真问题并提示逐级 write_state+配 DC。(3) 串口日志丢字节:Usart0::putCharNonBlocking 在 1024B 环形缓冲满时静默丢弃且 putChar 忽略返回值,3M 波特率下 init 阶段诊断突发写入必溢出,实测掉字产生 "bus eaabled"/"pdi_type=0x8Dexect"/"[EAT][SLAVE]" 等伪影,直接污染判断依据;现 kTxBufSize 1024→4096,缓冲满时在临界区内轮询 TBE 就地发出最老字节腾位(drainOneByteLocked,走轮询不等中断,故 ISR 内 printf 亦不死锁;USART0 为 preempt=3 已被 BASEPRI 屏蔽,独占 s_txTail 无争用),仅 USART 外设不发送时才丢弃并累加 s_txDropped,新增 droppedBytes() 并在 init 报告尾部非 0 时打 WARN 自证日志完整性。1.5.40:清理上电 CAN 启动噪声——[CAN-DIAG] CH%u init_nom/init_fd/exit_ok 三行改为仅在本端控制器初始化失败(can_init/can_fd_init 返回失败,或退出 init mode 后 STAT.IWS 仍置位)时以 LOG_ERR 打印;正常上电(三通道 init_nom=1 init_fd=1 exit_ok=1)完全静默。判据只看 GD32 控制器自身,不含 TEC/REC——总线侧计数在未接从站时会因 RX 浮空噪声累积,与 1.5.36 移除 L0 自检 BUS_OFF/TEC/REC 检查同一原则。1.5.39:修复 LAN9252 访问模型根本性错误——EtherCAT 核心寄存器一直读 0/写丢弃(主站恒读 AL=INIT code=0 的真正根因,1.5.35/1.5.37 两次误判已撤回)。网络侧诊断(新增 tools/ecat/ecat_esc_regs.py,经 Npcap 裸 APRD 帧读 ESC + pysoem 读 SII)实测:EEPROM 加载正常(SII CRC=0x9B 相符)、DL_STATUS=0x5611 pdi_oper=1、ESC_CFG pdi_type=0x8D 已生效、AL Status=0x0001,即 ESC 侧一切正常,故障纯在 GD32 的 HBI 访问层。按 DS 12.11/表 12-13:HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口,EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、EEPROM 接口)必须经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接访问,过程 RAM(0x1000-0x1FFF:邮箱/PDO)必须经 PRAM FIFO(数据口 0x000-0x03C + 地址长度/命令 0x308-0x314);旧代码把 16 位索引当统一地址空间直读直写,只有 BYTE_TEST 这类系统 CSR 恰好可达,所以「探活全过、业务全死」,且写路径从未被端到端验证过。EscLan9252 新增 csrRead/csrWrite(BUSY 轮询+size 1/2/4)与 pramRead/pramWrite(ADDR_LEN→BUSY→按 avail 轮询读写 FIFO,首/末 DWORD 按 addr&3 取有效字节,别名口轮换),initOnlyReadReg/WriteReg 改走 CSR 端口;hw_gd32exmc.cpp 的 escRead/escWrite 按地址自动路由 CSR/PRAM,AL Event(0x220)/AL Event Mask(0x204)/LED override/HW_GetSyncMan 全部改经 CSR 端口。附带修复:HW_Enable/DisableSyncManChannel 的 Activate 偏移 +4→+6(+4 是 SM Control,Activate 在 0x0806+8n)。1.5.38:修复看门狗 init 失败(SPI 0xA3 实测 crit_fail=0x1000)——FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零),而 BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开,fwdgt_prescaler/reload_value_config 等 PUD/RUD 永远等不到清零、库超时返回 ERROR;该失败自始存在(旧代码忽略返回值时 FWDGT 实际按复位默认值 DIV4/0xFFF≈0.41s 运行,与配置 3.0s 不符,主循环喂狗快才未暴露)。Watchdog::init() 改为先 rcu_osci_on(RCU_IRC40K)+rcu_osci_stab_wait 再配置,新增故障码 kFaultLsiNotReady=4。1.5.37:修复 EEPROM 加载竞态致 EtherCAT 永不就绪(主站恒读 AL=INIT code=0 的根因)——LAN9252 RST# 释放后内部 EEPROM Loader 经 I2C 读 SII 约 30ms,加载完成前 EtherCAT 核心寄存器对 PDI(HBI) 读全 0、写静默丢弃;1.5.35 的 [ECAT][DIAG] 实测 RST# 释放 +3ms 时 DL_STATUS/ESC_CFG/EEP_CSR 全 0、写回环 FAIL,而 SSC 在 +4ms 就已完成 MainInit,AL Event Mask/邮箱 SM 配置全部落空。EscLan9252::init() 在 BYTE_TEST 通过后新增轮询 DL_STATUS(0x0110) bit0(PDI operational/EEPROM loaded,超时沿用 CONFIG_ESC_HBI_POLL_TIMEOUT_MS=300ms;成功打印等待时长,失败打印 DL_STATUS/EEP_CSR 原始值与排查提示)并新增 isCoreReady();initial.cpp 仅在 busReady&&coreReady 时放行 MainInit,总线通但核心未就绪报新故障码 kFaultEthercatCoreNotReady(=3,SPI 0xA3 fault_codes 同步可见)。订正 1.5.35 条目:写回环实际用 AL Event Mask(0x0204) 写 0x0A50 后还原,非 Station Alias(0x012)——后者在部分 ESC 上对 PDI 只读会造成假 FAIL。1.5.36:模块级故障码(fault_codes)——每模块 1B 原因码(0=成功),SPI 0xA3 INIT_STATUS 响应从 12B 扩到 25B([12..24] 按 ModuleBit 位序):canfd=原因码|(首个失败通道<<4)(1=控制器init失败 2=卡init mode 3=STB配置未生效)、ADC(1=RSTCLB超时 2=CLB超时 3=VDDA无效)、Scheduler(1=CEN 2=PSC 3=CAR 4=节拍未前进)、Watchdog(1=PSC 2=RLD 3=挂起)、EtherCAT(1=EXMC总线未就绪 2=SSC MainInit失败)、nvic/esp32_hold/usart0 各单码;init 报告失败模块名带 #XX 故障码后缀,码值定义集中在 sys/initial.h 注释。重要修正:CAN L0 自检移除 BUS_OFF 与 TEC/REC 检查——总线侧状态在通道未接从站(RX 浮空噪声累积 REC)时误报,「没接从站≠错误」;L0 现仅校验 GD32 控制器自身(init失败/卡init mode/STB输出配置),总线健康仍经 0xC1 反馈帧运行期上报(网页 CAN 页可见);CAN L0 按通道打 LOG_ERR(含 STAT/ERR 寄存器);无使能 CAN 通道(全关)属合法配置不再算失败。1.5.35:AL 恒卡 INIT 的判死诊断。主站侧根因已先修:tools/ecat/ecat_server.py 的 _require_op_ready 旧版只 state_check(SAFEOP) 而从不 write_state、且全程未请求过 PREOP——pysoem 的 state_check 只轮询等待、不发 AL Control,故从站从未被要求迁移(现象=AL INIT/code=0x0000 且无错误码);已改为 PREOP→config_map→[DC]→SAFEOP→OP 每步先 write_state 再 state_check。修完后主站确认真的发出 PREOP 请求,但从站仍恒 INIT/code=0x0000,故在 EthercatSlave::init() 增 [ECAT][DIAG] 一次性判死三点:DL_STATUS(0x110) bit0 PDI operational(为 0 则 ESC 静默丢弃 PDI 侧写入的 AL Status)、ESC_CFG(0x140) PDI 类型是否被 EEPROM 加载为 0x8D(HBI 16-bit)、EEP_CSR(0x502) EEPROM loaded/校验错(SII 校验失败会拒绝离开 INIT 且常不给 code);外加 HBI 写路径回环(0x012 Configured Station Alias 写 0xA55A 读回后还原)——此前所有上电探针验证的都是"读"(bit-bang IDX_0 / BYTE_TEST),而 esc_exmc_bus 的 EXMC 写回显测试只在 idx0_raw!=0x1234 的失败分支才跑、因 IDX 正确恒被跳过,故 HBI 写路径从未被端到端验证;若写坏,ECAT_Main 能读到 AL Control 事件却写不回 AL Status,与实测现象完全一致。注意主站读身份/AL 走 ESC 以太网侧+EEPROM(SII),不经过 GD32 的 HBI/PDI,"主站能读到身份"不能证明 PDI 写通。1.5.34:初始化状态检查补全覆盖——原"盲初始化"模块全部改为带验证:SysTick initMs 检查 SysTick_Config 返回值并验证 1ms 节拍真实前进(失败=FATAL 裸串口报告+停机,与 HXTAL FAIL 同级)、ADC 校准改带超时轮询(库函数 adc_calibration_enable 内部死等,硬件异常会永久挂住)并验证 VREFINT→VDDA 采样链、SchedulerTimer 回读 CEN/PSC/CAR 并验证 100us 中断节拍前进、USART0 回读 CTL0.UEN、Watchdog 检查 fwdgt PSC/RLD 的 ErrStatus 返回值+PUD/RUD 无挂起、NVIC 回读 AIRCR.PRIGROUP、ESP32_EN 回读门控置位;新增复位源采集(RCU_RSTSCK→ResetCauseBit 位图,init 报告打印);AppInitializer 导出 moduleMask/criticalFailMask/initState/resetCause getter;新增 SPI 0xA3 INIT_STATUS 命令(12B:模块位图+critical失败位图+复位源+init_state),身份块[15] 原 reserved 字节改填 init_state(旧 ESP32 忽略、旧 GD32 恒 0 与 OK 同义,不 bump feat_map_ver 前后兼容);CONFIG_SPI_MAX_COMMANDS 12→14。附带修复:system_gd32e50x.c 五处 HXTAL FAIL 裸串口诊断段寄存器 bug(CTL0 误写 STAT0 偏移 0x40013800 且漏开 APB2EN bit14 USART0EN,该诊断从未真正发出)——正确布局 CTL0 @ USART0+0x0C。1.5.33:HBI bring-up 诊断探针 EscExmcBus::diagDump 改由 CONFIG_ESC_EXMC_DIAG 门控(默认 0)——硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),清理上电 [EXMC-DIAG] 噪声并停止 task_runner 的 20s 周期复跑,不再每次上电把 EXMC 引脚切成 GPIO 手动拉一遍;换板/补焊排查浮空时置 1 即恢复。网页万用表通断测试 continuityDrive() 及其复用的 bit-bang 辅助函数不受影响,始终可用。1.5.32:AL 通路诊断打印(排查主站连接后 AL 卡 INIT)——ECAT_Main 收到主站 AL 控制请求打 [ECAT][AL] evt/req、SetALStatus 写回打 [ECAT][AL] -> 状态+错误码、task_runner 5s 心跳 [ECAT][HB] al/code/evt(仅同步 ISR 激活时,心跳停=主循环卡死);dc_app.c 导出 EcatDbg_ReadAlDiag(0x0130+0x0220 快照)。1.5.31:DC 通路观测同步到网页——SSC OD 新增只读记录 0x2000 SYNC Diag(sub1=sync0/sub2=sync1/sub3=escirq/sub4=uptime 秒,UINT32),APPL_InputMapping 每 PDO 周期从 EthercatSync 拉取快照(ethercat_sync.cpp 导出 EcatDbg_Get* extern C);配套 tools/ecat:/api/sync_diag 经 SDO 读 0x2000 并以主站时间戳算 sync0 速率,ecat.html 运行期 2s 轮询显示,串口 5s 低频打印保留为旁证。1.5.30:DC SYNC0 通路可观测性——EthercatSync 增加 s_sync0Count/s_sync1Count/s_escIrqCount 三个 volatile 计数器(ISR 内自增,SYNC0 路径实测仅 LDR/ADDS/STR 三条指令,仍在 .ramfunc);task_runner 新增 ethercatSyncCounterPrint(),5s 诊断按增量打印 sync0 速率(主站设 1ms 周期时应 ≈1000/s),无脉冲时静默。配套 tools/ecat 支持分布时钟:ecat_tool.py send/monitor 新增 --dc-cycle-us/--dc-shift-us(0=free-run 默认),enter_op() 在 config_map 后、进 SAFEOP 前调 config_dc()+dc_sync();ecat_server.py /api/op/start 接受 dc_cycle_us/dc_shift_us;ecat.html 加 DC SYNC0 周期下拉。此前工具从不调用 config_dc,PDO 恒为 free-run,SYNC0 引脚无脉冲、Sync0_Isr 从未触发,1.5.28 配的 word1=0xCC/word2=5000 一直未被实测覆盖。1.5.29:删除 GD32 侧 EEPROM 烧录路径(EscEeprom/esc_eeprom.h/.cpp、eeprom_image.h 及 task_runner 主循环烧录块)——ESI 镜像改由 EtherCAT 主站侧刷写(tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口,2026-08-31 实测 162 字全校验通过),固件不再负责烧录;gen_eeprom.py 生成器保留。HBI 复查修复:maskEnter/Exit 补屏蔽 EXTI2(SYNC1)——Sync1_Isr 的 PDO_InputMapping→HW_EscWriteIsr 两步事务(preempt=1)可打断主循环 HW_EscRead/Write 致错址;通断测试模式进入/退出改经 maskEnter/Exit 屏蔽三类 EXTI,防测试期 ISR 访问 GPIO 态总线读垃圾。1.5.28:对照 LAN9252 数据手册/AN1907/官方 EVB XML 修正——EEPROM word1 高字节 0x00→0xCC(Sync/Latch 0151h:SYNC0/1 Map+Output、推挽低有效配 EXTI 下降沿;原 0x00 时 SYNC0 引脚留在 LATCH 输入态,DC 模式过程数据死);word2 0→5000(Sync 脉宽 0982h,x10ns=50us,0=确认模式不可用);eeprom_image.h 尾部补真实 0xFF 字节(原注释行致 C 零填充,烧写全阵列);EXMC ADDSET 2→4 HCLK(DS 表9-4 trdrd/twrwr/twrrd≥13ns);XML ConfigData 扩到 14B 并加 CycleTimeSync1。1.5.27:LAN9252 使用修正——EXMC 16-bit 地址左移 1 位(HBI DATA 偏移 0x20000/0x30000→0x40000/0x60000);gen_eeprom.py 按 ETG.1000.6 重写(CRC-8/word7、身份字 word8-15、CoE=0x0004、分类区 byte128);DCU_ESI.xml ConfigData PDI 0x05→0x8D、RxPdo/TxPdo 互换并改 4×64bit 对齐 dc_app.c;HBI 两步事务互斥守卫(主循环 mask EXTI0/1 + PDI/Sync1 ISR 屏蔽 EXTI0);删除死代码 EthercatSlave::periodicIsr 三缓冲;20s HBI 诊断在同步 ISR 激活后自动停跑 */ + (第 16 轮起从站进入上述硬楔死、主站侧全部为可恢复的干净报错,直至压测结束)。 【1.5.50】主站工具诊断盲区修复(tools/ecat 两文件),GD32 功能代码零改动(无需重新烧录,板上 1.5.49 即最新;bump 仅为变更记录与编译纪律)。01:31 复测确认 1.5.48 的自动重试机制工作正常:首败("进 PREOP 失败: AL=INIT code=0x0000")后自动恢复,OP 真实建立、UI 周期正确增长。首败根因定位:停止回 INIT 时 SSC MBX_StopMailboxHandler 禁用了邮箱 SM0/SM1(mailbox.c:359-361),而 CheckSmSettings 在 INIT→PREOP 要求邮箱 SM 已使能(ecatslv.c:416-418),拒绝迁移且从站自身无重新使能路径(死锁闭环)——自动重试里 config_init() 重写 SM0/SM1(ethercatconfig.c:279-280)正是解药,与"断开重连有效"同机理。修复:PREOP/SAFEOP 失败分支改用 read_state() 取真实 AL Status Code(旧报文的 code=0x0000 是 SOEM 广播 state_check 不刷新 al_status 的陈旧缓存,与 OP 循环同款盲区),下次首败将显示真实错误码(预期 0x0016/0x001D 类 INVALIDMBXCFGINPREOP)确凿验证根因。 【1.5.49】SM2 事件楔死自愈——01:03 串口日志钉死现场(evt=0x0400 在 0x220 挂起 10s 无人处理、escirq 仅 2.6/s 只随 AL Control 事件跳动、9s ESM 超时报 0x001B、[ECAT][AEM] 一次未打印=其前置 bEscIntEnabled==FALSE 未满足):PDI_Isr 被调用了 13 次却从未执行 SM2 分支(ecatappl.c:287 的 ALEvent&PROCESS_OUTPUT_EVENT 从未命中),bEcatFirstOutputsReceived 永不置位,SAFEOP→OP 挂到 ESM 超时。dc_app.c EcatDbg_GuardAleventMask() 扩展自愈(2):SAFEOP 下检测"0x220 bit10 挂起 + 标志未置位"即从主循环直接替 PDI_Isr 完成工作——置位 bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款);协议等价:bit10 挂起=主站已写过 SM2=WKC=3/3 佐证的"收到输出"成立,AL_ControlRes 下一个 MainLoop 周期即完成 OP。守护(1) 0x204 缺位自愈保留并解除 bEscIntEnabled前置(改判 AL>=SAFEOP+nPdOutputSize>0)。WARN 携带全部内部旗标(bEscInt/bDcSync/mask_exp/mask_act/wd 看门狗状态),下次复现一行日志即可定位楔死路径(嫌疑:bEscIntEnabled 被某路径意外清零、或 PDI_Isr 读 0x220 的 CSR 间接端口结果不含 bit10)。自愈仅在 SAFEOP+标志未置位时触发,OP 态下 LRW 每周期置位/清除 bit10 属正常不误报。1.5.48:修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 【1.5.48】修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 【1.5.47】修复 free-run 连接概率性卡死在"SAFEOP+0x0000 等待 OP"的根因——两个叠加的固件缺陷:(A) ECAT_CheckTimer() 在整个固件里没有任何调用者!ecat_def.h 曾设 ECAT_TIMER_INT=1(要求端口提供 1ms 定时器 ISR 调 ECAT_CheckTimer),但本工程从未实现该 ISR,而 MainLoop 的轮询调用路径被 #if !ECAT_TIMER_INT 编译排除;后果是 EsmTimeoutCounter 永不递减→任何 pending 状态迁移(SAFEOP→OP)永不超时→从站无限期忽略一切后续 AL Control(实测第二次点击报"进 PREOP 失败: AL=SAFEOP"即此),DC_CheckWatchdog/PLL 计数器也全部死掉。修复:ECAT_TIMER_INT 1→0 + 新增 ECAT_TIMER_INC_P_MS=180000(DWT CYCCNT@180MHz,hw_gd32exmc.cpp 的 HW_GetTimerValue/HW_ClearTimerValue 本就是为此写好的死代码),MainLoop 每 1ms 轮询调 ECAT_CheckTimer,ESM 超时(9s)/DC 看门狗/总线周期计算全部复活。(B) ESC 残留 DC 激活位导致从站误入 DC 同步模式:之前跑过 DC 模式(dc_sync(True,...))在 ESC 0x0981 留下激活位,跨"断开重连"不清(仅断电/主站主动清);PREOP→SAFEOP 的 UpdateSyncManagerParameter(ecatslv.c:754)读到 DC unit active 就按 DCSYNC0/1 推导 SyncType→bDcSyncActive=TRUE→SAFEOP→OP 改等 PLL 序列,而 free-run 主站不发 SYNC0、PLL 计数器又在死掉的 DC_CheckWatchdog 里——双重死锁,LRW 每 7ms 也救不回(00:27 复测:轨迹恒 SAFEOP+0x0000)。修复(主站侧,tools/ecat 两文件):free-run 连接时在 config_map 之前 dc_sync(False,0,0) 主动清残留激活;OP 等待 3s→12s 覆盖"ESM 超时(9s)+ACK 清错重试"完整序列;等待期记录 LRW WKC 采样+失败时报真实 AL code/轨迹/看门狗实际值(get_watchdog)作证据链。另:1.5.46 引入的 state_check→read_state 已修正 code=0x0000 假读(SOEM ecx_statecheck 广播模式不更新 ALstatuscode)。1.5.46:主站工具(tools/ecat ecat_server.py/ecat_tool.py)修复 50ms 周期重启后"进 OP 失败/进 PREOP 失败 code=0x0000"仍复现的问题,GD32 侧代码零改动(1.5.45 的 0x0420=1s 看门狗修复保持不变)——bump 版本号并重编译纯粹是为了让烧录生效可验证(串口 init 报告/版本号能对上)。22:15 复测日志钉死了旧修复无效的三处原因:(a) 1.5.45 注释"同态写即 ack"是错误前提——SSC AL_ControlInd(ecatslv.c:1372-1387)仅在 AL Control 带 ACK 位(bit4=0x10)时清 AL Status 错误位并继续执行迁移,错误位置位时普通同态写同样落入"先清错误位后直接 return"的忽略分支;上一轮看门狗 0x001B 拖回 SAFEOP 残留的错误位若不先带 ACK 清掉,重写 SAFEOP/OP 全部石沉大海,这就是第二次点击"进入 OP"报"进 PREOP 失败: AL=SAFEOP code=0x0000"的机制(错误位在 state_check 期间被某次写清掉后 code 显示 0,但状态从未迁移)。现 _require_op_ready/enter_op 所有 AL Control 写统一带 STATE_ACK(pysoem 已导出,=EC_STATE_ACK=0x10),与 SOEM 官方 ecatcheck 的恢复套路一致(写 当前态|ACK 清错再写目标态);无错误时 ACK 位被从站无害消费。(b) OP 等待循环里 state_check(OP, 200ms) 使 LRW 帧间隔 ~205ms,稳超固件<1.5.45 的 100ms SM 看门狗——CheckIfEcatError()(ecatslv.c:2137-2151) 在从站未到 OP 时(bEcatOutputUpdateRunning=FALSE)每 100ms 把 bEcatFirstOutputsReceived 清回 FALSE,而 SAFEOP→OP 的 AL_ControlRes 完成条件恰恰依赖该标志(ecatslv.c:1993),于是迁移挂起直到 ESM 超时回落;轮询收紧到 20ms(帧间隔 ~25ms)。注意:固件≥1.5.45(看门狗 1s)后 (b) 不再致命,但 20ms 轮询对任何看门狗配置都安全。(c) 回落判断须先 0x0F 掩掉错误位:PREOP+错误=0x12>0x04,旧代码 `state < SAFEOP_STATE` 漏判跌落,直接重写 OP 又被忽略,空转到 3s 超时——修正为 (state&0x0F)100ms 全 WKC 错的根治仍在从站固件 1.5.45 的 configPdWatchdog(0x0420=10000×100µs=1s),网页周期下拉最大 200ms 远小于 1s;若烧录 1.5.46 后 100/200ms 仍报 WKC 错误,抓 GD32 串口确认上电有无 "[ECAT][ESC] PD watchdog = 1s"(WARN=0x0420 写失败需查 HBI)。1.5.45:修复主站 100ms 周期全 WKC 错 + 重启有概率卡死。硬件实测(ecat_esc_regs.py RawBus 裸帧)钉死:0x0400=0x09C2(分频 100µs)/0x0420=0x03E8(1000),即 SM 看门狗正好 100ms,且 SII 不携带看门狗字段、这就是芯片复位默认值;探针实测 100ms 周期 wkc 19/20 错(wkc=1,espected=3)、10/50ms 全对、重启 5 轮 1 轮卡 PREOP code=0x0000,与用户现象完全一致。机制:主站写 SM2 后若 100ms 内没有新的写访问,ESC 看门狗到期(0x0440 bit0 清零),SSC CheckIfEcatError()(ecatslv.c:2133)读到后调 AL_ControlInd(STATE_SAFEOP, 0x001B) 拖回 SAFEOP,同时 DisableSyncManChannel(SM2)(ecatslv.c:1740-1746)——SM2 一旦被禁用,主站 LRW 不再写进 SM2、WKC 不累加,表现为 wkc=1 持续报错,直到重连走完整 INIT→PREOP→SAFEOP 才由 StartInputHandler 重新使能 SM2,这就是"WKC错误=周期数-1、断开重连后 WKC=0"的完整链条。修复(1):EscLan9252::configPdWatchdog() 在 init() 的 PDI 就绪分支里把 0x0420 写成 10000(1s),分频保持默认 100µs;正常 10/50ms 周期行为不变,100ms 手动调试周期不再被拖回。修复(2),主站工具(tools/ecat 两文件):重启卡死的根因是 SSC 对带 STATE_CHANGE 错误位的新 AL 请求会忽略(AL_ControlInd ecatslv.c:1376-1384 先清错误位后直接 return)——上一轮看门狗 0x001B/断开时留下的错误位没被 ack,直接 write_state(OP) 无人应答,断开重连之所以能恢复是因为 config_init 的状态轮询恰好完成了 ack。现在 OP 等待循环里若从站跌回 PREOP/INIT,先重写 SAFEOP(同态写即 ack)立起状态再重请 OP。【注:修复(2) 中"同态写即 ack"的前提已被 1.5.46 推翻并修正为带 ACK 位写,见上】 1.5.44:修复 OP 下输入 PDO 几乎每周期 WKC 不计数(主站实测已进 OP、输出 32B/输入 32B正常,但 89 个周期里 87 次 wkc < expected_wkc)——根因是 dc_app.c 的 APPL_StartInputHandler() 写死 *pIntMask = 0x0800,把 SSC 算好的 AL Event Mask 顶掉了。pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已根据同步模式推导它:本工程 free-run + 32B 输出 PDO 时,ecatslv.c:959 置 u16ALEventMask = PROCESS_OUTPUT_EVENT(0x0400),:1090 再次确保该位;而 ecatslv.c:1506 的 SetALEventMask() 只做“或”不做赋值,因此旧代码令寄存器 0x204 永远缺 SM2 位、只剩 SM3(0x0800)。后果:主站写输出到 SM2 不再产生 IRQ,PDI_Isr 只能被“主站读 SM3”事件带起来,于是 PDO_InputMapping() 永远发生在主站读取**之后**;SM3 是 3-buffer 模式,主站读到从未被从站重写的缓冲就不累加 WKC,恰好解释“能进 OP、长度对、但几乎每周期 WKC 少”。官方 sampleappl.c/el9800appl.c 的 APPL_StartInputHandler 均只 return、不碰 pIntMask,现修为 (void)pIntMask 并注明原因。同时撤销上一轮在主站工具里加的 0x1C32:01/0x1C33:01 SyncType SDO 写(tools/ecat 两个文件)——它既错也无效:实测返回 SdoError 0x08000022(当前状态不可写),因为 0x1C3x:01 的权限是 ACCESS_WRITE_PREOP 而 pysoem 的 config_map() 已把从站推到 SAFEOP;且本来就不需要写,SSC 在 PREOP→SAFEOP 的 StartInputHandler() 会自动按 DC 寄存器与 PDO 长度推导出 0x1C32=SM_SYNCHRON/0x1C33=SM2_SYNCHRON(ecatslv.c:849-880),coeappl.c:466/513 的 SYNCTYPE_FREERUN 仅是上电初值,bSyncSetByUser 也默认 FALSE。 1.5.43:修复 SAFEOP->OP 永远进不去(主站报 "进 OP 失败: AL=PREOP code=0x0000",且已边发 processdata 边等 3s 仍无效)——根因是 LAN9252 的 IRQ 引脚从未被使能,PDI_Isr 一次都没执行过。IRQ_CFG(0x054) 与 INT_EN(0x05C) 是 LAN9252 系统 CSR(HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即 IRQ_EN=0(引脚永久去断言)+ ECAT_INT_EN=0(EtherCAT 事件不汇入 IRQ,DS 8.2.6/8.3.1,ECAT_INT 为 INT_STS/INT_EN 的 bit0);固件此前只配了 EXTI/GPIO/NVIC 侧(PB1 上拉输入+下降沿+preempt=1)与 EEPROM 的 SYNC0/1(0x0151=0xCC),从来没打开芯片侧的中断输出总闸。后果链:PDI_Isr 从不触发 → SSC 收不到 PROCESS_OUTPUT_EVENT → bEcatFirstOutputsReceived 恒 FALSE(该标志只在 PDI_Isr(ecatappl.c:305) 或 free-run 的 MainLoop 轮询分支(ecatappl.c:551, 条件 !bEscIntEnabled)里置位)→ AL_ControlInd 的 SAFEOP_2_OP 分支(ecatslv.c:1993 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`) 因本工程有 32B 输出而永不成立 → 迁移被挂起(bApplEsmPending)后回落,AL Status Code 保持 0x0000(不是被拒绝,故不给 0x001B/0x001A)。注意本工程 SyncType 使 UpdateSyncManagerParameter 置 bEscIntEnabled=TRUE(ecatslv.c:951),因此 MainLoop 的 free-run 轮询兜底分支不执行,中断是唯一通路。EscLan9252::init() 在 DL_STATUS bit0 确认 PDI 就绪后新增 enableEcatIrqOutput():先写 INT_STS 清 ECAT_INT 悬留状态(R/WC),再置 INT_EN.ECAT_INT_EN,最后开 IRQ_CFG 总闸并回读校验,打印 [ECAT][ESC] IRQ output enabled(失败则 WARN 并指明 SAFEOP->OP 会卡)。极性与 ethercat_sync.cpp 保持一致:推挽 IRQ_TYPE=1 + 低有效 IRQ_POL=0,配对上拉输入的下降沿 EXTI;INT_DEAS 保持 0=禁用去断言定时器。AL Event Mask(0x204) 仍由 SSC 在状态迁移时自行维护,不在此处干预。配套修主站工具(tools/ecat):(a) _require_op_ready/enter_op 请求 OP 后改为边 send_processdata/receive_processdata 边 state_check 循环至多 3s(SSC 要求先收到首帧输出才 ack OP),中途回落则重发 OP 请求,失败信息按 AL code 分流排查方向;(b) 修正 state_check 超时单位——pysoem docstring 明确 "Timeout value in us",旧代码各处传 2000 实际只等 2ms(SOEM 默认 EC_TIMEOUTSTATE=2000000=2s),PREOP/SAFEOP 能过纯属从站 ack 快的侥幸,现统一用 EC_TIMEOUT_STATE_US=2_000_000 常量。1.5.42:修复主站 config_map 失败(SdoError "Unsupported access to an object" / "Attempt to read to a write only object",实测 0x1600/0x1A00 的 sub0/3/4 报错而 sub1/2 通过)——两个独立的对象字典 bug,均在从站侧,与主站工具无关。(1) REC 对象的 entry descriptor 数组给少了:OBJ_GetEntryDesc()(objdef.c:554) 与 OBJ_GetEntryOffset()(objdef.c:609) 只对 OBJCODE_ARR 复用 pEntryDesc[1] 描述所有子索引,对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex];而 dc_app.c 把 0x1600/0x1A00/0x7000/0x6000/0x2000 全声明为 REC(4|OBJCODE_REC<<8) 却只提供 2 元素描述数组,sub3/sub4 越界读到相邻常量数组的垃圾 DataType/ObjAccess,正好解释"sub0/3/4 报错、sub1/2 侥幸通过"的错误模式(sub1/2 恰好落在合法项上)。现为 0x1600/0x1A00 补满 Sub0..Sub4 共 5 项(asPDOMappingEntryDesc),并把原共用的 asDataEntryDesc 拆成 asOutputEntryDesc/asInputEntryDesc(各 5 项)、asSyncDiagEntryDesc 补到 5 项;同时给数据子条目补 OBJACCESS_RXPDOMAPPING/TXPDOMAPPING(官方 sampleappl.h 的 0x7010/0x6000 同此,缺这两个标志主站校验 0x1600/0x1A00 的映射目标时会拒绝),输入侧 0x6000 由 READWRITE 收紧为 ACCESS_READ。0x1C12/0x1C13 是 ARR 保持 2 项不变。(2) 结构体从未被紧排:ecat_def.h 的 STRUCT_PACKED_START/MBX_STRUCT_PACKED_START/OBJ_STRUCT_PACKED_START 三对宏全部留空,注释还误写"AC6 不需要"——实际这些宏就是 SSC 紧排结构体的唯一手段,SSC 的 SDO 读写按"si0 占 16bit、其后各条目按 BitLength 紧排"算字节偏移(OBJ_GetEntryOffset),留空则按编译器自然对齐。armclang 编译期断言实测:{UINT16;UINT64[4]} 的 UINT64 落在字节 8 而非 2,{UINT16;UINT32 x4} 落在字节 4 而非 2,故 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存,SSC 内部 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响(PDO 路径走 memcpy 未受影响,故此前回环测试无法暴露)。三对宏改为 __attribute__((packed)),并在 dc_app.c 加 _Static_assert 守卫 offsetof==2 与 sizeof==34 防回归。1.5.41:修正三处会误导诊断的日志缺陷(1.5.39 的 LAN9252 访问模型修复经实测已端到端成立:IDX=0x1234、BYTE_TEST=0x87654321、DL_STATUS bit0=1 且 waited 0ms、PDI_CTRL=0x8D、ESC_CFG bit0=0 Device Emulation 关闭、EEP_CSR 无错误位、CSR 写回环 0x0204 写 0x0A50 读回 PASS、SSC MainInit OK;waited 0ms 同时反证 1.5.37「EEPROM 加载竞态」确不存在,读全 0 纯因走错通路)。(1) [ECAT][DIAG] 的 EEP_CSR 位标签错且语义反:0x0502 的 bit11 是 Checksum Error 而非 loaded(真正的加载状态在 bit12,且 0=OK;见 ETG.1000.4 表 49 与 SSC esc.h ESC_EEPROM_ERROR_CRC/LOAD),旧版打印 "loaded=%u" 取 bit11,正常板子恒显示 loaded=0,字面像「EEPROM 未加载」,正是 1.5.37 那个已撤回结论的诱因;现改为 crc_err/load_err/ack_err/wr_err/busy 分位打印并注明全 0=正常。(2) [ECAT][SLAVE] init 终判在无主站时是假警报:s_escReady 要求 AL>=SAFEOP,而 AL 迁移只由主站写 AL Control(0x0120) 驱动、从站不自跳,故冷启动必然打 FAIL(partial),与紧随其后的 SSC MainInit OK 自相矛盾;现按 DL_STATUS 的 Physical Link 位 bit4/5/6(DS 12.14.19) 判定有无主站,空载报 WAIT_MASTER(实测空载 0x5503 三口 link/comm 全 0,接主站 0x5611),link up 却卡 AL 才报真问题并提示逐级 write_state+配 DC。(3) 串口日志丢字节:Usart0::putCharNonBlocking 在 1024B 环形缓冲满时静默丢弃且 putChar 忽略返回值,3M 波特率下 init 阶段诊断突发写入必溢出,实测掉字产生 "bus eaabled"/"pdi_type=0x8Dexect"/"[EAT][SLAVE]" 等伪影,直接污染判断依据;现 kTxBufSize 1024→4096,缓冲满时在临界区内轮询 TBE 就地发出最老字节腾位(drainOneByteLocked,走轮询不等中断,故 ISR 内 printf 亦不死锁;USART0 为 preempt=3 已被 BASEPRI 屏蔽,独占 s_txTail 无争用),仅 USART 外设不发送时才丢弃并累加 s_txDropped,新增 droppedBytes() 并在 init 报告尾部非 0 时打 WARN 自证日志完整性。1.5.40:清理上电 CAN 启动噪声——[CAN-DIAG] CH%u init_nom/init_fd/exit_ok 三行改为仅在本端控制器初始化失败(can_init/can_fd_init 返回失败,或退出 init mode 后 STAT.IWS 仍置位)时以 LOG_ERR 打印;正常上电(三通道 init_nom=1 init_fd=1 exit_ok=1)完全静默。判据只看 GD32 控制器自身,不含 TEC/REC——总线侧计数在未接从站时会因 RX 浮空噪声累积,与 1.5.36 移除 L0 自检 BUS_OFF/TEC/REC 检查同一原则。1.5.39:修复 LAN9252 访问模型根本性错误——EtherCAT 核心寄存器一直读 0/写丢弃(主站恒读 AL=INIT code=0 的真正根因,1.5.35/1.5.37 两次误判已撤回)。网络侧诊断(新增 tools/ecat/ecat_esc_regs.py,经 Npcap 裸 APRD 帧读 ESC + pysoem 读 SII)实测:EEPROM 加载正常(SII CRC=0x9B 相符)、DL_STATUS=0x5611 pdi_oper=1、ESC_CFG pdi_type=0x8D 已生效、AL Status=0x0001,即 ESC 侧一切正常,故障纯在 GD32 的 HBI 访问层。按 DS 12.11/表 12-13:HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口,EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、EEPROM 接口)必须经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接访问,过程 RAM(0x1000-0x1FFF:邮箱/PDO)必须经 PRAM FIFO(数据口 0x000-0x03C + 地址长度/命令 0x308-0x314);旧代码把 16 位索引当统一地址空间直读直写,只有 BYTE_TEST 这类系统 CSR 恰好可达,所以「探活全过、业务全死」,且写路径从未被端到端验证过。EscLan9252 新增 csrRead/csrWrite(BUSY 轮询+size 1/2/4)与 pramRead/pramWrite(ADDR_LEN→BUSY→按 avail 轮询读写 FIFO,首/末 DWORD 按 addr&3 取有效字节,别名口轮换),initOnlyReadReg/WriteReg 改走 CSR 端口;hw_gd32exmc.cpp 的 escRead/escWrite 按地址自动路由 CSR/PRAM,AL Event(0x220)/AL Event Mask(0x204)/LED override/HW_GetSyncMan 全部改经 CSR 端口。附带修复:HW_Enable/DisableSyncManChannel 的 Activate 偏移 +4→+6(+4 是 SM Control,Activate 在 0x0806+8n)。1.5.38:修复看门狗 init 失败(SPI 0xA3 实测 crit_fail=0x1000)——FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零),而 BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开,fwdgt_prescaler/reload_value_config 等 PUD/RUD 永远等不到清零、库超时返回 ERROR;该失败自始存在(旧代码忽略返回值时 FWDGT 实际按复位默认值 DIV4/0xFFF≈0.41s 运行,与配置 3.0s 不符,主循环喂狗快才未暴露)。Watchdog::init() 改为先 rcu_osci_on(RCU_IRC40K)+rcu_osci_stab_wait 再配置,新增故障码 kFaultLsiNotReady=4。1.5.37:修复 EEPROM 加载竞态致 EtherCAT 永不就绪(主站恒读 AL=INIT code=0 的根因)——LAN9252 RST# 释放后内部 EEPROM Loader 经 I2C 读 SII 约 30ms,加载完成前 EtherCAT 核心寄存器对 PDI(HBI) 读全 0、写静默丢弃;1.5.35 的 [ECAT][DIAG] 实测 RST# 释放 +3ms 时 DL_STATUS/ESC_CFG/EEP_CSR 全 0、写回环 FAIL,而 SSC 在 +4ms 就已完成 MainInit,AL Event Mask/邮箱 SM 配置全部落空。EscLan9252::init() 在 BYTE_TEST 通过后新增轮询 DL_STATUS(0x0110) bit0(PDI operational/EEPROM loaded,超时沿用 CONFIG_ESC_HBI_POLL_TIMEOUT_MS=300ms;成功打印等待时长,失败打印 DL_STATUS/EEP_CSR 原始值与排查提示)并新增 isCoreReady();initial.cpp 仅在 busReady&&coreReady 时放行 MainInit,总线通但核心未就绪报新故障码 kFaultEthercatCoreNotReady(=3,SPI 0xA3 fault_codes 同步可见)。订正 1.5.35 条目:写回环实际用 AL Event Mask(0x0204) 写 0x0A50 后还原,非 Station Alias(0x012)——后者在部分 ESC 上对 PDI 只读会造成假 FAIL。1.5.36:模块级故障码(fault_codes)——每模块 1B 原因码(0=成功),SPI 0xA3 INIT_STATUS 响应从 12B 扩到 25B([12..24] 按 ModuleBit 位序):canfd=原因码|(首个失败通道<<4)(1=控制器init失败 2=卡init mode 3=STB配置未生效)、ADC(1=RSTCLB超时 2=CLB超时 3=VDDA无效)、Scheduler(1=CEN 2=PSC 3=CAR 4=节拍未前进)、Watchdog(1=PSC 2=RLD 3=挂起)、EtherCAT(1=EXMC总线未就绪 2=SSC MainInit失败)、nvic/esp32_hold/usart0 各单码;init 报告失败模块名带 #XX 故障码后缀,码值定义集中在 sys/initial.h 注释。重要修正:CAN L0 自检移除 BUS_OFF 与 TEC/REC 检查——总线侧状态在通道未接从站(RX 浮空噪声累积 REC)时误报,「没接从站≠错误」;L0 现仅校验 GD32 控制器自身(init失败/卡init mode/STB输出配置),总线健康仍经 0xC1 反馈帧运行期上报(网页 CAN 页可见);CAN L0 按通道打 LOG_ERR(含 STAT/ERR 寄存器);无使能 CAN 通道(全关)属合法配置不再算失败。1.5.35:AL 恒卡 INIT 的判死诊断。主站侧根因已先修:tools/ecat/ecat_server.py 的 _require_op_ready 旧版只 state_check(SAFEOP) 而从不 write_state、且全程未请求过 PREOP——pysoem 的 state_check 只轮询等待、不发 AL Control,故从站从未被要求迁移(现象=AL INIT/code=0x0000 且无错误码);已改为 PREOP→config_map→[DC]→SAFEOP→OP 每步先 write_state 再 state_check。修完后主站确认真的发出 PREOP 请求,但从站仍恒 INIT/code=0x0000,故在 EthercatSlave::init() 增 [ECAT][DIAG] 一次性判死三点:DL_STATUS(0x110) bit0 PDI operational(为 0 则 ESC 静默丢弃 PDI 侧写入的 AL Status)、ESC_CFG(0x140) PDI 类型是否被 EEPROM 加载为 0x8D(HBI 16-bit)、EEP_CSR(0x502) EEPROM loaded/校验错(SII 校验失败会拒绝离开 INIT 且常不给 code);外加 HBI 写路径回环(0x012 Configured Station Alias 写 0xA55A 读回后还原)——此前所有上电探针验证的都是"读"(bit-bang IDX_0 / BYTE_TEST),而 esc_exmc_bus 的 EXMC 写回显测试只在 idx0_raw!=0x1234 的失败分支才跑、因 IDX 正确恒被跳过,故 HBI 写路径从未被端到端验证;若写坏,ECAT_Main 能读到 AL Control 事件却写不回 AL Status,与实测现象完全一致。注意主站读身份/AL 走 ESC 以太网侧+EEPROM(SII),不经过 GD32 的 HBI/PDI,"主站能读到身份"不能证明 PDI 写通。1.5.34:初始化状态检查补全覆盖——原"盲初始化"模块全部改为带验证:SysTick initMs 检查 SysTick_Config 返回值并验证 1ms 节拍真实前进(失败=FATAL 裸串口报告+停机,与 HXTAL FAIL 同级)、ADC 校准改带超时轮询(库函数 adc_calibration_enable 内部死等,硬件异常会永久挂住)并验证 VREFINT→VDDA 采样链、SchedulerTimer 回读 CEN/PSC/CAR 并验证 100us 中断节拍前进、USART0 回读 CTL0.UEN、Watchdog 检查 fwdgt PSC/RLD 的 ErrStatus 返回值+PUD/RUD 无挂起、NVIC 回读 AIRCR.PRIGROUP、ESP32_EN 回读门控置位;新增复位源采集(RCU_RSTSCK→ResetCauseBit 位图,init 报告打印);AppInitializer 导出 moduleMask/criticalFailMask/initState/resetCause getter;新增 SPI 0xA3 INIT_STATUS 命令(12B:模块位图+critical失败位图+复位源+init_state),身份块[15] 原 reserved 字节改填 init_state(旧 ESP32 忽略、旧 GD32 恒 0 与 OK 同义,不 bump feat_map_ver 前后兼容);CONFIG_SPI_MAX_COMMANDS 12→14。附带修复:system_gd32e50x.c 五处 HXTAL FAIL 裸串口诊断段寄存器 bug(CTL0 误写 STAT0 偏移 0x40013800 且漏开 APB2EN bit14 USART0EN,该诊断从未真正发出)——正确布局 CTL0 @ USART0+0x0C。1.5.33:HBI bring-up 诊断探针 EscExmcBus::diagDump 改由 CONFIG_ESC_EXMC_DIAG 门控(默认 0)——硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),清理上电 [EXMC-DIAG] 噪声并停止 task_runner 的 20s 周期复跑,不再每次上电把 EXMC 引脚切成 GPIO 手动拉一遍;换板/补焊排查浮空时置 1 即恢复。网页万用表通断测试 continuityDrive() 及其复用的 bit-bang 辅助函数不受影响,始终可用。1.5.32:AL 通路诊断打印(排查主站连接后 AL 卡 INIT)——ECAT_Main 收到主站 AL 控制请求打 [ECAT][AL] evt/req、SetALStatus 写回打 [ECAT][AL] -> 状态+错误码、task_runner 5s 心跳 [ECAT][HB] al/code/evt(仅同步 ISR 激活时,心跳停=主循环卡死);dc_app.c 导出 EcatDbg_ReadAlDiag(0x0130+0x0220 快照)。1.5.31:DC 通路观测同步到网页——SSC OD 新增只读记录 0x2000 SYNC Diag(sub1=sync0/sub2=sync1/sub3=escirq/sub4=uptime 秒,UINT32),APPL_InputMapping 每 PDO 周期从 EthercatSync 拉取快照(ethercat_sync.cpp 导出 EcatDbg_Get* extern C);配套 tools/ecat:/api/sync_diag 经 SDO 读 0x2000 并以主站时间戳算 sync0 速率,ecat.html 运行期 2s 轮询显示,串口 5s 低频打印保留为旁证。1.5.30:DC SYNC0 通路可观测性——EthercatSync 增加 s_sync0Count/s_sync1Count/s_escIrqCount 三个 volatile 计数器(ISR 内自增,SYNC0 路径实测仅 LDR/ADDS/STR 三条指令,仍在 .ramfunc);task_runner 新增 ethercatSyncCounterPrint(),5s 诊断按增量打印 sync0 速率(主站设 1ms 周期时应 ≈1000/s),无脉冲时静默。配套 tools/ecat 支持分布时钟:ecat_tool.py send/monitor 新增 --dc-cycle-us/--dc-shift-us(0=free-run 默认),enter_op() 在 config_map 后、进 SAFEOP 前调 config_dc()+dc_sync();ecat_server.py /api/op/start 接受 dc_cycle_us/dc_shift_us;ecat.html 加 DC SYNC0 周期下拉。此前工具从不调用 config_dc,PDO 恒为 free-run,SYNC0 引脚无脉冲、Sync0_Isr 从未触发,1.5.28 配的 word1=0xCC/word2=5000 一直未被实测覆盖。1.5.29:删除 GD32 侧 EEPROM 烧录路径(EscEeprom/esc_eeprom.h/.cpp、eeprom_image.h 及 task_runner 主循环烧录块)——ESI 镜像改由 EtherCAT 主站侧刷写(tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口,2026-08-31 实测 162 字全校验通过),固件不再负责烧录;gen_eeprom.py 生成器保留。HBI 复查修复:maskEnter/Exit 补屏蔽 EXTI2(SYNC1)——Sync1_Isr 的 PDO_InputMapping→HW_EscWriteIsr 两步事务(preempt=1)可打断主循环 HW_EscRead/Write 致错址;通断测试模式进入/退出改经 maskEnter/Exit 屏蔽三类 EXTI,防测试期 ISR 访问 GPIO 态总线读垃圾。1.5.28:对照 LAN9252 数据手册/AN1907/官方 EVB XML 修正——EEPROM word1 高字节 0x00→0xCC(Sync/Latch 0151h:SYNC0/1 Map+Output、推挽低有效配 EXTI 下降沿;原 0x00 时 SYNC0 引脚留在 LATCH 输入态,DC 模式过程数据死);word2 0→5000(Sync 脉宽 0982h,x10ns=50us,0=确认模式不可用);eeprom_image.h 尾部补真实 0xFF 字节(原注释行致 C 零填充,烧写全阵列);EXMC ADDSET 2→4 HCLK(DS 表9-4 trdrd/twrwr/twrrd≥13ns);XML ConfigData 扩到 14B 并加 CycleTimeSync1。1.5.27:LAN9252 使用修正——EXMC 16-bit 地址左移 1 位(HBI DATA 偏移 0x20000/0x30000→0x40000/0x60000);gen_eeprom.py 按 ETG.1000.6 重写(CRC-8/word7、身份字 word8-15、CoE=0x0004、分类区 byte128);DCU_ESI.xml ConfigData PDI 0x05→0x8D、RxPdo/TxPdo 互换并改 4×64bit 对齐 dc_app.c;HBI 两步事务互斥守卫(主循环 mask EXTI0/1 + PDI/Sync1 ISR 屏蔽 EXTI0);删除死代码 EthercatSlave::periodicIsr 三缓冲;20s HBI 诊断在同步 ISR 激活后自动停跑 */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; diff --git a/GD32E508_DCU_APP/sys/cpu_load.cpp b/GD32E508_DCU_APP/sys/cpu_load.cpp new file mode 100644 index 0000000..04cd01f --- /dev/null +++ b/GD32E508_DCU_APP/sys/cpu_load.cpp @@ -0,0 +1,117 @@ +/** + * @file cpu_load.cpp + * @brief CPU 负载率测量实现(1.5.61,度量定义与并发协议见 cpu_load.h)。 + * + * 热路径(beginWork/endWork/isrEnter/isrExit/tick)放进 .ramfunc——它们会被包进 + * EXTI0(Sync0, preempt=0) 等延迟敏感的 ISR 包装,与 EthercatSync::onSync0Exti + * 同款 section 属性,避免 flash 等待态给 Sync0 入口添抖动。1s 一次的窗口 + * 结算(含 64 位除法)留在普通 flash 函数里。 + */ +#include "sys/cpu_load.h" + +#include "RTE_Components.h" +#include CMSIS_device_header + +volatile uint32_t CpuLoad::s_depth = 0U; +volatile uint32_t CpuLoad::s_stamp = 0U; +volatile uint32_t CpuLoad::s_busy = 0U; +volatile uint32_t CpuLoad::s_winStart = 0U; +volatile uint16_t CpuLoad::s_loadPerm = 0U; +volatile uint32_t CpuLoad::s_seq = 0U; + +/** @brief 区间并集的共享进入点。0→1 者负责盖时间戳;STREX 失败方重试时深度 + * 已被抢占者修正,不会再重复盖戳。 */ +__attribute__((always_inline)) static inline void cpuLoadEnter() +{ + uint32_t d; + do { + d = __LDREXW(&CpuLoad::s_depth); + if (d == 0U) { + CpuLoad::s_stamp = DWT->CYCCNT; + __DMB(); + } + } while (__STREXW(d + 1U, &CpuLoad::s_depth) != 0U); + __DMB(); +} + +/** @brief 区间并集的共享退出点。1→0 者把区间累进 s_busy(同样走独占访问, + * 防"ISR 恰好插在主循环退出的读-改-写之间"丢累加)。 */ +__attribute__((always_inline)) static inline void cpuLoadLeave() +{ + const uint32_t end = DWT->CYCCNT; + const uint32_t start = CpuLoad::s_stamp; + uint32_t d; + do { + d = __LDREXW(&CpuLoad::s_depth); + } while (__STREXW(d - 1U, &CpuLoad::s_depth) != 0U); + if (d == 1U) { + const uint32_t delta = end - start; + uint32_t b; + do { + b = __LDREXW(&CpuLoad::s_busy); + } while (__STREXW(b + delta, &CpuLoad::s_busy) != 0U); + } +} + +void CpuLoad::init() +{ + /* DWT 已由 EscLan9252::init 使能;这里幂等兜底(万一 EtherCAT 初始化被裁剪)。 */ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + s_depth = 0U; + s_busy = 0U; + s_winStart = DWT->CYCCNT; + s_loadPerm = 0U; + s_seq = 0U; +} + +__attribute__((section(".ramfunc"), noinline)) void CpuLoad::beginWork() +{ + cpuLoadEnter(); +} + +__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrEnter() +{ + cpuLoadEnter(); +} + +__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrExit() +{ + cpuLoadLeave(); +} + +/** + * @brief 窗口结算(普通 flash 函数,1s 才进一次)。 + * @note s_busy 用独占访问换出:窗口边界上 ISR 的在途累加要么算进本窗口、 + * 要么算进下窗口,不丢。 + */ +static void cpuLoadRollWindow(uint32_t now) +{ + const uint32_t elapsed = now - CpuLoad::s_winStart; /* 调用方已保证 >= 1s */ + uint32_t busy; + do { + busy = __LDREXW(&CpuLoad::s_busy); + } while (__STREXW(0U, &CpuLoad::s_busy) != 0U); + CpuLoad::s_winStart = now; + + uint32_t perm = (uint32_t)(((uint64_t)busy * 1000ULL) / (uint64_t)elapsed); + if (perm > 1000U) { + perm = 1000U; + } + CpuLoad::s_loadPerm = (uint16_t)perm; + CpuLoad::s_seq++; +} + +__attribute__((section(".ramfunc"), noinline)) void CpuLoad::endWork() +{ + cpuLoadLeave(); +} + +__attribute__((section(".ramfunc"), noinline)) void CpuLoad::tick() +{ + const uint32_t now = DWT->CYCCNT; + if ((uint32_t)(now - s_winStart) >= SystemCoreClock) { + cpuLoadRollWindow(now); + } +} diff --git a/GD32E508_DCU_APP/sys/cpu_load.h b/GD32E508_DCU_APP/sys/cpu_load.h new file mode 100644 index 0000000..a3b8ece --- /dev/null +++ b/GD32E508_DCU_APP/sys/cpu_load.h @@ -0,0 +1,97 @@ +/** + * @file cpu_load.h + * @brief CPU 负载率测量(1.5.63 定义收窄,机制沿用 1.5.61)。 + * + * 度量定义:**中断 + 非延迟处理段(任务派发 / OTA 中继等请求驱动工作)占墙钟 + * 时间的比值**,1s 滚动窗口,分辨率 0.1%(permille)。 + * + * 为什么不计入主循环常开轮询:本架构在 EtherCAT 实时模式(CONFIG_MAIN_LOOP_WFI_ENABLE=0) + * 下主循环空闲时是 NOP 忙等,且 runPending() 每圈无条件跑 SSC MainLoop 轮询与各 + * poll() 检查——把这部分计入"busy"的话负载率恒 ≈99%,失去参考价值(1.5.61 实机 + * 实测 99.4%,其中绝大部分是自由运转的轮询而非在处理事件)。收窄后的定义回答 + * 的问题变成"CPU 有多少时间在响应事件",随真实负载(CAN 流量、OTA 中继、 + * DC 模式 Sync0 过程数据搬运)增长;常开轮询成本与空转时间不计入。 + * + * 计入项(全走同一深度计数器求**区间并集**,嵌套不重不漏): + * - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/ + * Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。 + * DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。 + * - 主循环非延迟段:TaskManager 派发的周期任务、0xB0 进 BOOT 处理、 + * 电机 OTA relay 推进(isActive() 门控,空闲时零挂钩开销)。 + * 不计入项:SSC MainLoop 轮询、AppSpiCommandBus.poll()、看门狗喂狗等常开检查 + * (free-run 调试模式下 PDO 应用跑在主循环轮询路径,不计入;生产走 DC 模式, + * 等价工作在 Sync0 ISR 内已计入)。 + * + * 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度, + * 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的 + * 深度并补上时间戳。已知残余误差:exit 在读时间戳与递减深度之间被高优先级 + * 中断插入时,该次中断时长漏计(几 cycle 窗口,对 0.1% 分辨率可忽略)。 + * + * @note DWT 已由 EscLan9252::init 使能;init() 幂等兜底,在主循环启动前调用。 + */ +#ifndef SYS_CPU_LOAD_H +#define SYS_CPU_LOAD_H + +#include + +/** + * @brief CPU 负载率测量。 + * @note 全静态类,与 SysTickTimer 同风格。 + */ +class CpuLoad { +public: + /** + * @brief 使能 DWT CYCCNT 并复位统计状态。 + * @note 幂等;须在主循环启动前调用一次。 + */ + static void init(); + + /** @brief 主循环圈起点:进入"busy"区间。与 endWork() 成对,每圈各一次。 */ + static void beginWork(); + + /** + * @brief 主循环圈终点:退出"busy"区间。 + * @note 与 beginWork() 成对;窗口滚动不在这里(begin/end 可能整圈不执行), + * 见 tick()。 + */ + static void endWork(); + + /** + * @brief 主循环每圈无条件调用:检查 1s 窗口是否滚动。 + * @note 只做一次 CYCCNT 读 + 比较(命中才进 flash 侧结算函数),滚动不再 + * 依赖"本圈恰好有挂钩段执行",空闲系统 seq 也会按秒推进。 + */ + static void tick(); + + /** @brief 中断入口挂钩:与 isrExit() 成对,包住 ISR 主体。 */ + static void isrEnter(); + + /** @brief 中断出口挂钩。 */ + static void isrExit(); + + /** + * @brief 最近一个完整窗口的负载率。 + * @return 0..1000,单位 0.1%(1000=满载);窗口未滚动过时为 0。 + */ + static uint16_t loadPermille() { return s_loadPerm; } + + /** + * @brief 已完成的统计窗口数(自 init 起,单调递增回绕)。 + * @note 随负载率一起进 EtherCAT 输入 PDO 段 5,主站据此判断数据是否仍在刷新。 + */ + static uint32_t windowSeq() { return s_seq; } + +public: + /* 静态成员开成 public:cpu_load.cpp 里 .ramfunc 的文件级内联热路径 + * (cpuLoadEnter/cpuLoadLeave/cpuLoadRollWindow) 直接触碰它们, + * 经成员函数绕一道反而多一层调用。勿在模块外写这些状态。 */ + + static volatile uint32_t s_depth; /**< busy 区间嵌套深度 */ + static volatile uint32_t s_stamp; /**< 深度 0→1 时刻的 CYCCNT 快照 */ + static volatile uint32_t s_busy; /**< 当前窗口内累计 busy 周期数 */ + static volatile uint32_t s_winStart; /**< 当前窗口起点 CYCCNT */ + static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */ + static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */ +}; + +#endif /* SYS_CPU_LOAD_H */ diff --git a/GD32E508_DCU_APP/sys/systick.cpp b/GD32E508_DCU_APP/sys/systick.cpp index 61d60e1..f56c0cd 100644 --- a/GD32E508_DCU_APP/sys/systick.cpp +++ b/GD32E508_DCU_APP/sys/systick.cpp @@ -5,6 +5,7 @@ #include "main.h" #include "sys/systick.h" +#include "sys/cpu_load.h" volatile uint32_t SysTickTimer::s_ms = 0U; @@ -93,4 +94,9 @@ bool SysTickTimer::initMs() * @retval 无返回值。 * @note C 向量包装函数,内部转调 `SysTickTimer::onInterrupt()`。 */ -extern "C" void SysTick_Handler(void) { SysTickTimer::onInterrupt(); } +extern "C" void SysTick_Handler(void) +{ + CpuLoad::isrEnter(); + SysTickTimer::onInterrupt(); + CpuLoad::isrExit(); +} diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 66b276a..c6e8c1c 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -14,6 +14,7 @@ #include "sys/ota_boot_request.h" #include "sys/motor_ota_relay.h" #include "sys/task_manager.h" +#include "sys/cpu_load.h" #include "sys/task_runner.h" #include "comm/ethercat_slave.h" #include "comm/ethercat_sync.h" @@ -565,15 +566,26 @@ void TaskRunner::runPending() /* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。 * 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */ if (OtaBootRequest::isRebootPending()) { + CpuLoad::beginWork(); OtaBootRequest::process(); + CpuLoad::endWork(); } /* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。 * 非活跃时立即返回,开销可忽略。 */ + /* 负载率挂钩(1.5.63):relay 活跃时才计 busy——isActive() 是廉价枚举判断, + * 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */ + const bool motorOtaActive = MotorOtaRelay::isActive(); + if (motorOtaActive) { + CpuLoad::beginWork(); + } MotorOtaRelay::process(); /* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔 * (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */ MotorOtaRelay::onTxTick(); + if (motorOtaActive) { + CpuLoad::endWork(); + } /* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为 * ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。 @@ -593,6 +605,9 @@ void TaskRunner::runPending() const uint32_t pending = TaskManager::takePending(); if(pending == 0U) return; + /* 负载率挂钩(1.5.63):派发的周期任务属"非延迟工作",整段计入 busy; + * 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */ + CpuLoad::beginWork(); if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample(); if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample(); if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess(); @@ -600,4 +615,5 @@ void TaskRunner::runPending() if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend(); if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat(); if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint(); + CpuLoad::endWork(); }