diff --git a/.gitignore b/.gitignore index 672b633..9af94e8 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,11 @@ uv2csolution.log # legacy intermediate project file *.cprj + +# Verification & Test suite process artifacts +/tests/ +/.agents/ +TEST_READY.md +TEST_READY.html +convert_md_to_html.py +*.html diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp index 2757e12..ed628cb 100644 --- a/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp @@ -13,6 +13,7 @@ volatile uint8_t EthercatCanBridge::s_lastSeq = 0U; volatile bool EthercatCanBridge::s_seqPrimed = false; +volatile bool EthercatCanBridge::s_coldStartPending = false; volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U; volatile uint16_t EthercatCanBridge::s_txCount = 0U; volatile uint16_t EthercatCanBridge::s_dropCount = 0U; @@ -223,9 +224,10 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head) /* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */ if (!s_seqPrimed) { - s_seqPrimed = true; - s_lastSeq = seq; - return false; + s_seqPrimed = true; + s_lastSeq = seq; + s_coldStartPending = true; + return true; /* 冷启动首包强制全量读,避免漏掉首帧命令 */ } if (seq != s_lastSeq) { @@ -234,6 +236,13 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head) return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U; } +void EthercatCanBridge::resetSequenceState() +{ + s_seqPrimed = false; + s_lastSeq = 0U; + s_coldStartPending = false; +} + void EthercatCanBridge::processOutput(const uint8_t *out) { if (out == nullptr) { @@ -243,19 +252,33 @@ void EthercatCanBridge::processOutput(const uint8_t *out) const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ]; if (!s_seqPrimed) { - s_seqPrimed = true; - s_lastSeq = seq; - return; + s_seqPrimed = true; + s_lastSeq = seq; + s_coldStartPending = true; } + /* 无论 seq 是否变化,优先处理非 seq 驱动的命令(FLAGS bit0 清统计) */ if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) { - s_txCount = 0U; - s_dropCount = 0U; - s_rxCount = 0U; + s_txCount = 0U; + s_dropCount = 0U; + s_rxCount = 0U; s_rxPublished = true; } - if (seq == s_lastSeq) { + bool executeCommand = false; + if (s_coldStartPending) { + s_coldStartPending = false; + /* 冷启动首包:若包含有效单帧命令或批量帧,立即执行 */ + const uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC]; + const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT]; + if (dlc > 0U || batchCount > 0U) { + executeCommand = true; + } + } else if (seq != s_lastSeq) { + executeCommand = true; + } + + if (!executeCommand) { return; } s_lastSeq = seq; @@ -384,4 +407,9 @@ void EcatCan_BuildInput(uint8_t *in) EthercatCanBridge::buildInput(in); } +void EcatCan_ResetSequenceState(void) +{ + EthercatCanBridge::resetSequenceState(); +} + } /* extern "C" */ diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h index 088c830..7c7537e 100644 --- a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h @@ -199,6 +199,9 @@ public: */ static bool peekOutput(const uint8_t *head); + /** @brief 复位输出 sequence 与冷启动同步状态。 */ + static void resetSequenceState(); + static void processOutput(const uint8_t *out); /** @@ -227,6 +230,7 @@ private: static volatile uint8_t s_lastSeq; static volatile bool s_seqPrimed; + static volatile bool s_coldStartPending; static volatile uint8_t s_lastTxResult; static volatile uint16_t s_txCount; static volatile uint16_t s_dropCount; @@ -259,6 +263,8 @@ extern "C" { void EcatCan_ProcessOutput(const uint8_t *out); /** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */ void EcatCan_BuildInput(uint8_t *in); +/** @brief 供 dc_app.c 调用的 shim:复位输出 sequence 状态机。 */ +void EcatCan_ResetSequenceState(void); } #endif /* COMM_ETHERCAT_CAN_BRIDGE_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.cpp b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp index ba83c53..c4afaa5 100644 --- a/GD32E508_DCU_APP/comm/ethercat_pdo.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp @@ -4,9 +4,12 @@ */ #include "comm/ethercat_pdo.h" #include "comm/ethercat_can_bridge.h" +#include "comm/esc_exmc_bus.h" #include "device/canfd.h" #include "sys/config.h" #include "sys/cpu_load.h" +#include "RTE_Components.h" +#include CMSIS_device_header /* __DMB() */ /* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。 * 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值, * 开关就会被悄悄当成"关掉"。 */ @@ -23,6 +26,12 @@ alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0}; /* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。 * 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */ alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0}; + +namespace { +/* 待交回主站的输入数据双缓冲(ping-pong,配合 __DMB() 保证原子发布与 tear-free 读写) */ +alignas(4) uint8_t s_inBuf[2][EthercatPdo::kPdoInBytes] = {{0}, {0}}; +volatile uint8_t s_inActiveIdx = 0U; +} alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0}; alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0}; alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0}; @@ -90,16 +99,24 @@ void EthercatPdo::process() * 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput * 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后, * 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */ + /* 双缓冲发布:填入非活跃槽 nextIdx,完成后通过 __DMB() 与 s_inActiveIdx 切换 + * 实现原子发布,彻底避免中断读侧(PDI_Isr / Sync0_Isr)与主循环之间的读写撕裂。 */ + const uint8_t nextIdx = 1U - s_inActiveIdx; + uint8_t *build = s_inBuf[nextIdx]; + EthercatCanBridge::processOutput(s_out); - EthercatCanBridge::buildInput(s_build); - fillDiagTail(s_build); - memcpy(s_in, s_build, kPdoInBytes); + EthercatCanBridge::buildInput(build); + fillDiagTail(build); + + __DMB(); + s_inActiveIdx = nextIdx; + memcpy(s_in, build, kPdoInBytes); #if CONFIG_ECAT_DIFF_STATS /* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000 * sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16 * 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */ - EthercatDiffStats::observe(s_in); + EthercatDiffStats::observe(s_inBuf[nextIdx]); #endif s_building = false; @@ -159,7 +176,9 @@ void EthercatPdo::writeInputs(uint8_t *esc) if (esc == nullptr) { return; } - memcpy(esc, s_in, kPdoInBytes); + const uint8_t activeIdx = s_inActiveIdx; + __DMB(); + memcpy(esc, s_inBuf[activeIdx], kPdoInBytes); ++s_inputCycles; } @@ -174,10 +193,13 @@ bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0, #else /* 仅 1.6.21 密排布局:count 有效且两段之间确有空洞才分段。 * magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */ - if (s_in[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) { + const uint8_t activeIdx = s_inActiveIdx; + __DMB(); + const uint8_t *cur = s_inBuf[activeIdx]; + if (cur[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) { return false; } - const uint8_t count = s_in[ECAT_MOTW_OFF_COUNT]; + const uint8_t count = cur[ECAT_MOTW_OFF_COUNT]; if (count > kEcatMotSlotsPerPdo) { return false; } @@ -253,19 +275,25 @@ uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens) return n; #else /* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。 - * 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。 */ + * 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。 + * 无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes - 1), + * 以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。 */ + const uint8_t activeIdx = s_inActiveIdx; + __DMB(); + const uint8_t *cur = s_inBuf[activeIdx]; + uint8_t n = 0U; - if (diffNeedWrite(s_in, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) { + if (diffNeedWrite(cur, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) { offs[n] = 0U; lens[n] = 32U; ++n; } - if (diffNeedWrite(s_in, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U), + if (diffNeedWrite(cur, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U), s_escSh2, s_escSh2Valid, s_escFlush2)) { offs[n] = 32U; /* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。 * 空洞不写入 ESC 无妨:主站只按 count 迭代。 */ - const uint16_t wlen = escSeg2WriteLen(s_in); + const uint16_t wlen = escSeg2WriteLen(cur); lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U; if (lens[n] == 0U) { /* count=0:段2 头 8 字节仍要写(count/active/magic) */ @@ -273,16 +301,19 @@ uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens) } ++n; } - if (diffNeedWrite(s_in, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH), + bool seg3Included = false; + if (diffNeedWrite(cur, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH), s_escSh3, s_escSh3Valid, s_escFlush3)) { offs[n] = ECAT_XRX_OFF_CH; lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH); ++n; + seg3Included = true; } - if (n == 0U) { - offs[0] = (uint16_t)(kPdoInBytes - 2U); - lens[0] = 2U; - n = 1U; + if (!seg3Included) { + /* 段 3 未包含时,补写末 2 字节以保证触碰 SM3 结束地址触发硬件提交 */ + offs[n] = (uint16_t)(kPdoInBytes - 2U); + lens[n] = 2U; + ++n; } return n; #endif @@ -348,4 +379,25 @@ uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs) return n; } +/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */ +uint32_t EscExmcBus_MaskEnter(void) +{ + return EscExmcBus::maskEnter(); +} + +void EscExmcBus_MaskExit(uint32_t prev) +{ + EscExmcBus::maskExit(prev); +} + +uint32_t HW_EscExmcMaskEnter(void) +{ + return EscExmcBus::maskEnter(); +} + +void HW_EscExmcMaskExit(uint32_t prev) +{ + EscExmcBus::maskExit(prev); +} + } /* extern "C" */ diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index 574dbfa..95cef91 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -166,6 +166,10 @@ extern void EcatPdo_ReadOutputs(const UINT8 *esc); extern void EcatPdo_Process(void); extern void EcatPdo_WriteInputs(UINT8 *esc); +/* 0x2502 用:进 BOOT 的异步请求(sys/ota_boot_request.cpp 的 extern "C" shim)。 + * 只置 pending 标志,真正的配置页写入 + 复位在主循环下一圈,见 ObjBootReqWrite 注。 */ +extern void EcatEnterBootRequestAsync(void); + /*--------------------------------------------------------------------------- * PDO Assign 对象 (0x1C12 / 0x1C13) *---------------------------------------------------------------------------*/ @@ -298,6 +302,57 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = { {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */ }; +/*--------------------------------------------------------------------------- + * 0x2502 引导程序交接请求(主站 → APP:「让你回 BOOT」) + * + * 为什么要有它:EtherCAT 直灌的会话对象在 **BOOT** 里(0x2500/0x2501),APP 的字典 + * 根本没有这两个对象 —— 实测对着 APP 写 0x2500 回 abort=0x06020000「对象不存在」。 + * 所以一台正在跑 APP 的机器必须先交接,才能走总线刷写。量产那条交接只有 + * SPI 0xB0 和 CAN 0x320+"OTA!",没有总线内的入口;常驻刷写必须补上。 + * + * 语义:写 1 = 请求进 BOOT;写 0 是无操作(不做"取消",避免半路误清)。 + * 动作复用现成的 OtaBootRequest 异步链:这里**只置 pending 标志**,配置页三字的 + * Flash 写入 + NVIC_SystemReset 由主循环 OtaBootRequest::process() 下一圈执行 + * (sys/initial.cpp 每圈调;与 SPI 0xB0 在 ISR 里只置标志同一个形状), + * 所以 SDO 响应先发出去、复位后到,不会把 SM1 半包留在总线上。 + * + * 交接后板子会稳定停在 BOOT:APP 写的是 EN=NOT_AVAL,而 BOOT 的救砖门 + * ecatArmed() = !(CHECK==AVAL && EN==AVAL) ⇒ 顺手就把 0x2500 直灌解锁了, + * 不需要 word8 令牌(也别依赖它:APP 的 otaConfigSet 只保全 8 个字,一擦页就没了)。 + *---------------------------------------------------------------------------*/ +OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2502[] = { + /* VAR 只有 :00 一个条目,且必须带写权限位(SSC 的通用写权限门在 objdef.c:1325, + * 早于 1352 的回调分发 —— BOOT 的 0x2500 就是栽在只给 ACCESS_READ 上) */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, +}; +static OBJCONST UCHAR aName0x2502[] = "GD32 boot request"; + +static UINT32 s_bootReq = 0U; /**< 最近一次写入值,读回用于主站自证 */ + +static UINT8 ObjBootReqWrite(UINT16 index, UINT8 subindex, UINT32 dataSize, + UINT16 MBXMEM *pData, UINT8 bCompleteAccess) +{ + const UINT8 *p8 = (const UINT8 *)pData; + UINT32 v; + + (void)index; + (void)bCompleteAccess; + + if (subindex != 0U) { + return ABORTIDX_SUBINDEX_NOT_EXISTING; + } + if (pData == NULL || dataSize < 4U) { + return ABORTIDX_PARAM_LENGTH_TOO_SHORT; + } + v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24); + s_bootReq = v; + + if (v == 1U) { + EcatEnterBootRequestAsync(); + } + return 0U; +} + /*--------------------------------------------------------------------------- * Object Names *---------------------------------------------------------------------------*/ @@ -343,6 +398,10 @@ TOBJECT OBJMEM ApplicationObjDicList[] = { {NULL, NULL, 0x2000, {DEFTYPE_RECORD, 23 | (OBJCODE_REC << 8)}, asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000}, + /* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */ + {NULL, NULL, 0x2502, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)}, + sEntryDesc0x2502, aName0x2502, &s_bootReq, NULL, ObjBootReqWrite, 0x0000}, + /* 终止条目 */ {NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000} }; @@ -391,8 +450,17 @@ UINT16 APPL_StopInputHandler(void) { return 0; } /*--------------------------------------------------------------------------- * Output Handler (SAFEOP→OP) *---------------------------------------------------------------------------*/ -UINT16 APPL_StartOutputHandler(void) { return 0; } -UINT16 APPL_StopOutputHandler(void) { return 0; } +UINT16 APPL_StartOutputHandler(void) +{ + EcatCan_ResetSequenceState(); + return 0; +} + +UINT16 APPL_StopOutputHandler(void) +{ + EcatCan_ResetSequenceState(); + return 0; +} /*--------------------------------------------------------------------------- * APPL_AckErrorInd @@ -691,7 +759,7 @@ void EcatDbg_OpInputFreshness(void) } /* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来), - * 再在关中断窗口内做一次输入映射 —— 此路径极低频,关中断可接受。 */ + * 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。 */ { UINT16 dummy = 0U; const UINT16 evt = HW_GetALEventRegister(); @@ -703,9 +771,9 @@ void EcatDbg_OpInputFreshness(void) HW_EscReadWord(dummy, nEscAddrOutputData); HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U)); } - __asm volatile ("cpsid i" ::: "memory"); + const uint32_t prevMask = EscExmcBus_MaskEnter(); PDO_InputMapping(); - __asm volatile ("cpsie i" ::: "memory"); + EscExmcBus_MaskExit(prevMask); ++s_opFreshRescues; s_inMapSeqSeen = s_inMapSeq; s_inStaleBaseCyc = EcatDbg_Dwt(); diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.h b/GD32E508_DCU_APP/comm/ssc/port/dc_app.h index b89c859..8361763 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.h +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.h @@ -12,6 +12,7 @@ #include "ecat_def.h" #include "esc.h" #include "ecatslv.h" +#include #ifdef __cplusplus extern "C" { @@ -36,6 +37,15 @@ void APPL_Application(void); void EcatDbg_OpInputFreshness(void); UINT32 EcatDbg_OpFreshRescues(void); +/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */ +uint32_t EscExmcBus_MaskEnter(void); +void EscExmcBus_MaskExit(uint32_t prev); +uint32_t HW_EscExmcMaskEnter(void); +void HW_EscExmcMaskExit(uint32_t prev); + +/* 1.6.34: 复位输出 sequence 状态机 */ +void EcatCan_ResetSequenceState(void); + #ifdef __cplusplus } #endif diff --git a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h index f34c59c..ae5bff5 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h +++ b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h @@ -666,8 +666,10 @@ #define DEVICE_SW_VERSION "1.0" #endif +/* 0x100A 的位长。SSC 默认 3 字符只装得下占位 "1.0";这里抬到 16,真实值由 + * sys/initial.cpp 起板时写进 acSoftwareversion(与 BOOT 侧同款改动)。 */ #ifndef DEVICE_SW_VERSION_LEN -#define DEVICE_SW_VERSION_LEN 0x3 +#define DEVICE_SW_VERSION_LEN 0x10 #endif /*----------------------------------------------------------------------------------------- diff --git a/GD32E508_DCU_APP/comm/ssc/src/coeappl.c b/GD32E508_DCU_APP/comm/ssc/src/coeappl.c index 5c6ea06..1a15571 100644 --- a/GD32E508_DCU_APP/comm/ssc/src/coeappl.c +++ b/GD32E508_DCU_APP/comm/ssc/src/coeappl.c @@ -209,8 +209,27 @@ OBJCONST UCHAR OBJMEM aName0x1009[] = "Hardware version"; -----------------------------------------------*/ /** * \brief 0x100A (Software version) variable to handle the object data + * + * 必须显式给到 DEVICE_SW_VERSION_LEN+1:条目描述符按 LEN 声明位长,SSC 会照长度 + * 整块回给主站;用 =DEVICE_SW_VERSION 推出来只有 4 字节,读到的是越界内容。 + * 真实版本号由 sys/initial.cpp 起板时调 AppSetSoftwareVersion() 覆盖。 */ -CHAR acSoftwareversion[] = DEVICE_SW_VERSION; +CHAR acSoftwareversion[DEVICE_SW_VERSION_LEN + 1] = DEVICE_SW_VERSION; + +/** @brief 覆盖 0x100A 上报的软件版本串(容量只有本文件知道,故 setter 放这儿)。 */ +void AppSetSoftwareVersion(const char *version) +{ + unsigned int i = 0U; + + if (version == NULL) { + return; + } + /* 本文件没 include stdio,手写有界拷贝:超长就截断,末尾必带 NUL。 */ + for (; i + 1U < sizeof(acSoftwareversion) && version[i] != '\0'; i++) { + acSoftwareversion[i] = version[i]; + } + acSoftwareversion[i] = '\0'; +} /** * \brief 0x1009 (Hardware version) entry description diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index 1eeb37f..ff32d38 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -255,6 +255,49 @@ static void hardResetCan(uint32_t canPeriph, rcu_periph_enum canRcu) } } +/** + * @brief CAN TX 并发保护 RAII 守卫。 + * + * 屏蔽 EXTI10_15_IRQn(SPI CS Assert/Release, Preempt 0)与该通道的 txIrq(Preempt 2), + * 防止 EtherCAT PDI_Isr(Preempt 1)与 SPI CS EXTI ISR(Preempt 0)在操作 s_txQueues + * 或硬件发送邮箱寄存器时产生并发撕裂与重入竞争。 + * 具备重入安全性:若某中断当前未使能(例如已被外层守卫屏蔽),退出时不会误使能。 + */ +class CanTxConcurrencyGuard { +public: + explicit CanTxConcurrencyGuard(uint8_t channelIndex) + : m_txIrq(kChannels[channelIndex].txIrq) + { + m_exti15_enabled = (NVIC_GetEnableIRQ(EXTI10_15_IRQn) != 0U); + m_tx_enabled = (NVIC_GetEnableIRQ(m_txIrq) != 0U); + + if (m_exti15_enabled) { + NVIC_DisableIRQ(EXTI10_15_IRQn); + } + if (m_tx_enabled) { + NVIC_DisableIRQ(m_txIrq); + } + __DSB(); + __ISB(); + } + + ~CanTxConcurrencyGuard() { + if (m_tx_enabled) { + NVIC_EnableIRQ(m_txIrq); + } + if (m_exti15_enabled) { + NVIC_EnableIRQ(EXTI10_15_IRQn); + } + __DSB(); + __ISB(); + } + +private: + IRQn_Type m_txIrq; + bool m_exti15_enabled; + bool m_tx_enabled; +}; + } // namespace CanFd::Diag CanFd::s_diag{}; @@ -541,7 +584,6 @@ void CanFd::initChannelHardware(uint8_t i) s_rxQueues[i].head = 0U; s_rxQueues[i].tail = 0U; - s_rxQueues[i].count = 0U; s_txQueues[i].head = 0U; s_txQueues[i].tail = 0U; s_txQueues[i].count = 0U; @@ -982,23 +1024,20 @@ CanFd::Mode CanFd::mode() * @param[in] 无。 * @param[out] 无。 * @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。 + * @note 无锁检查 head != tail,免关中断。 */ bool CanFd::hasRxFrame() { - bool found = false; - uint32_t save = RT_CRITICAL_SAVE(); for(size_t i = 0; i < kChannelCount; ++i) { - if(s_rxQueues[i].count != 0U) { - found = true; - break; + if(s_rxQueues[i].head != s_rxQueues[i].tail) { + return true; } } - RT_CRITICAL_RESTORE(save); - return found; + return false; } /** - * @brief 轮转读取并消费一帧接收报文。 + * @brief 轮转读取并消费一帧接收报文(Lock-Free SPSC 消费者端)。 * @param[in] 无。 * @param[out] id 输出报文标识符;允许为空。 * @param[out] data 输出报文数据缓存;允许为空。 @@ -1006,42 +1045,37 @@ bool CanFd::hasRxFrame() * @param[out] channel 输出报文来源通道号;允许为空。 * @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。 * @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。 + * 基于 SPSC 无锁环形缓冲区,消费端读取 payload 并执行 __DMB() 后推进 head, + * 无需关中断或依赖 BASEPRI,彻底消除与 Preempt 0 RX ISR 的并发撕裂。 */ bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel) { - RxFrame localFrame{}; - bool found = false; - - uint32_t save = RT_CRITICAL_SAVE(); for(size_t n = 0; n < kChannelCount; ++n) { const uint8_t idx = static_cast((s_nextConsumeChannel + n) % kChannelCount); auto &queue = s_rxQueues[idx]; - if(queue.count == 0U) { + const uint8_t head = queue.head; + const uint8_t tail = queue.tail; + + if(head == tail) { continue; } - localFrame = queue.frames[queue.head]; - queue.head = static_cast((queue.head + 1U) % kRxQueueDepth); - queue.count--; - s_nextConsumeChannel = static_cast((idx + 1U) % kChannelCount); - found = true; - break; - } - RT_CRITICAL_RESTORE(save); - - if(!found) { - return false; - } - - if(channel) *channel = localFrame.channel; - if(id) *id = localFrame.id; - if(length) *length = localFrame.length; - if(data) { - for(uint8_t i = 0; i < localFrame.length; ++i) { - data[i] = localFrame.data[i]; + const auto &src = queue.frames[head]; + if(channel) *channel = src.channel; + if(id) *id = src.id; + if(length) *length = src.length; + if(data && src.length > 0U) { + for(uint8_t i = 0; i < src.length; ++i) { + data[i] = src.data[i]; + } } + + __DMB(); + queue.head = static_cast((head + 1U) & kRxQueueMask); + s_nextConsumeChannel = static_cast((idx + 1U) % kChannelCount); + return true; } - return true; + return false; } /** @@ -1068,6 +1102,8 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, return CanTxResult::kErrInvalid; } + CanTxConcurrencyGuard guard(channelIndex); + /* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */ if (ext_id == 0x00000100U && dlc == 16U) { CAN_LOGI("[CAN-DIAG] TX 0x100 DLC=16 via transmitFrame ch=%u\r\n", @@ -1181,6 +1217,10 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id, TxQueue &q = s_txQueues[channelIndex]; + /* 关中断并发保护:屏蔽 EXTI10_15_IRQn(SPI burst, Preempt 0)与 txIrq(Preempt 2), + * 解决 PDI_Isr(Preempt 1)与 SPI CS release(Preempt 0)并发访问 s_txQueues 的竞态。 */ + CanTxConcurrencyGuard guard(channelIndex); + /* 快速路径:队列空 → 直通硬件邮箱。装得下立即返回,不用等待 TX 完成中断。 * ★ allow_abort=false:邮箱满时禁止 abort 旧帧顶替(那会把同通道已装载的 * 旧帧静默丢弃,>3 帧同通道突发只剩最后 3 帧上线),一律落入软件队列, @@ -1193,16 +1233,9 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id, /* 邮箱满:落入下面的入队路径 */ } - /* 关中段:EXTI15(SPI burst) 与 CANx TX ISR 并发,保护 head/tail/count。 - * 用 CMSIS NVIC 接口(SPL 的 nvic_irq_enable 是 3 参版)。 - * 关的是该通道自身的 txIrq,避免关全局影响其他通道的实时性。 */ - const IRQn_Type irqn = kChannels[channelIndex].txIrq; - NVIC_DisableIRQ(irqn); - if (q.count >= kTxQueueDepth) { /* 队列也满了:彻底装不下。此时不能再返回 kOk, * 否则前端会误判成功;只能丢弃并返回 kErrMailbox。 */ - NVIC_EnableIRQ(irqn); s_diag.error_count++; s_channelStats[channelIndex].error_count++; return CanTxResult::kErrMailbox; @@ -1217,14 +1250,13 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id, q.tail = static_cast((q.tail + 1U) % kTxQueueDepth); q.count++; - NVIC_EnableIRQ(irqn); return CanTxResult::kOk; } /** * @brief CAN TX 完成中断末尾调用:用硬件邮箱空闲出的位置,把队列里的帧装回去。 - * @note 只在 ISR 内调用;关中断保护已在 transmitFrame 返回值判定外层完成, - * 本函数本身不关中(邮箱空位是"单调减少再单调增加",多读无害)。 + * @note 由 CAN TX 完成 ISR 调用;使用 CanTxConcurrencyGuard 屏蔽 EXTI10_15 与 txIrq, + * 防止出队期间被高优先级的 SPI CS release 抢占导致 head/count 撕裂。 */ void CanFd::drainTxQueueIsr(uint8_t channelIndex) { @@ -1232,6 +1264,8 @@ void CanFd::drainTxQueueIsr(uint8_t channelIndex) return; } TxQueue &q = s_txQueues[channelIndex]; + CanTxConcurrencyGuard guard(channelIndex); + while (q.count > 0U) { TxQueuedFrame &f = q.frames[q.head]; /* 复用 transmitFrame 的装载逻辑;allow_abort=false:这里邮箱刚空出来, @@ -1261,6 +1295,16 @@ uint32_t CanFd::txQueueBacklog(uint8_t channelIndex) return s_txQueues[channelIndex].count; } +uint32_t CanFd::rxQueueCount(uint8_t channelIndex) +{ + if (channelIndex >= kChannelCount) { + return 0U; + } + const uint8_t head = s_rxQueues[channelIndex].head; + const uint8_t tail = s_rxQueues[channelIndex].tail; + return static_cast((tail - head) & kRxQueueMask); +} + void CanFd::abortPendingTx(uint8_t channelIndex) { if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) { @@ -1612,13 +1656,17 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex) } auto &queue = s_rxQueues[channelIndex]; - if(queue.count >= kRxQueueDepth) { + const uint8_t head = queue.head; + const uint8_t tail = queue.tail; + const uint8_t next_tail = static_cast((tail + 1U) & kRxQueueMask); + + if(next_tail == head) { s_diag.rx_drop_count++; stats.rx_drop_count++; continue; } - auto &frame = queue.frames[queue.tail]; + auto &frame = queue.frames[tail]; frame.channel = channelIndex; frame.id = rxId; frame.length = rx_message.rx_dlen; @@ -1626,8 +1674,8 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex) frame.data[i] = rx_message.rx_data[i]; } - queue.tail = static_cast((queue.tail + 1U) % kRxQueueDepth); - queue.count++; + __DMB(); + queue.tail = next_tail; } if(fifoFull) { diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index e15c204..352c801 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -246,8 +246,15 @@ public: /** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */ static uint32_t txQueueBacklog(uint8_t channelIndex); + /** @brief 接收队列容量深度(固定为 kRxQueueDepth = 16)。 */ + static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; } + + /** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */ + static uint32_t rxQueueCount(uint8_t channelIndex); + private: static constexpr uint8_t kRxQueueDepth = 16U; + static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U; /** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的 * 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */ static constexpr uint8_t kTxQueueDepth = 13U; @@ -259,11 +266,15 @@ private: uint8_t length; }; + /** + * @brief Lock-Free 单生产者单消费者 (SPSC) 环形接收队列。 + * @note 生产者为 CAN RX ISR (Preempt 0) 写入 tail,消费者为 consumeRxFrame (Thread) 推进 head。 + * 移除共享 count 变量,消除 BASEPRI=0x10 临界区对 Preempt 0 无效导致的并发撕裂与计数下溢风险。 + */ struct RxQueue { RxFrame frames[kRxQueueDepth]; volatile uint8_t head; volatile uint8_t tail; - volatile uint8_t count; }; /** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */ diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index 5da8b40..6a816c0 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -370,8 +370,12 @@ void AppSpiCommand::configureExti() { gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); - exti_interrupt_enable(EXTI_15); + /* 1.6.35:先清 STAT 再开 IMR。原 enable→clear 顺序在 warm jump 下有害—— + * BOOT 关中断跳转(v1.1.0.15)前 ESP32 持续拉 CS,EXTI15 STAT 大概率残留; + * 本函数在 AppSpiCommandBus.init() 里跑,此刻 PRIMASK 已被 init 开头清零, + * 若先 enable IMR,那个残留位会立即回放成一次假 CS 沿进 NVIC。 */ exti_interrupt_flag_clear(EXTI_15); + exti_interrupt_enable(EXTI_15); /* 1.6.32:SPI CS EXTI 抢占级 1→0。原 1 与 PDI/SYNC1/SCHED 同级不抢占 * (1.6.13 实测 PDI 窗口 ~357µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽; * release ISR 一旦拖过 ESP32 的 CS 间隔(0.4.4 起 1ms),re-arm 落空, diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index d99390e..fb9efe7 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -149,7 +149,25 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 6U; -constexpr uint32_t APP_FW_VERSION_PATCH = 33U; /* 1.6.33: OP 输入新鲜度不变量(EcatDbg_OpInputFreshness)——SM-Synchron 下输入 PDO 只靠 PDI_Isr,通路一停主站读到冻缓冲(WKC/AL 仍正常=假连接);现 OP 内输入停更>5ms 由主循环强制 PDO_InputMapping+重开门。根治"连接看起来正常但 CPU/CAN 不更新、复位 GD32 才好"。 */ /* 1.6.32: SPI CS EXTI 提到抢占级0(见 app_spi_command configureExti),根治 CS release 被 PDI/RT_CRITICAL 延迟导致的 RX 撕裂(0xE8) *//* /* 1.6.28:ESC 睡眠挂起门控(EscLan9252::pmSuspended)。pmDrive 进 D1-D3 置位后全固件 HBI 消费者静默——task_runner 停调 MainLoop/AL 守护/AL 心跳(MainLoop 每圈 HBI 读 0x220,睡眠中任何 HBI 访问含读都把芯片打回 D0,2026-09-12 实测休眠 ~20s 即被主站"复活":芯片毫秒级被 GD32 自己叫醒、SM 失效后主站 wedge 重建 OP);ethercat_sync 三 EXTI ISR 与 timer Sync0 路径清 pending 即返。解除:pmDrive 唤醒成功/op0 读到 D0 自愈/nrstDrive 释放复位/hardwareReset。配套主站侧 ecat_native /api/pm/hold 楔死看门狗豁免。 */ /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */ +/* 1.6.34:EtherCAT 侧补「APP→BOOT 交接」+ 版本自证。 + * (1) 新增 CoE 0x2502「GD32 boot request」(VAR UINT32, ACCESS_READWRITE, + * comm/ssc/port/dc_app.c):主站写 1 就请求本 APP 回引导程序。动作复用量产 + * SPI 0xB0 那条异步链(回调只置 pending,配置页 CHECK/AVAL/EN 三字的 Flash 写 + * + NVIC_SystemReset 在主循环 OtaBootRequest::process() 下一圈做),所以 SDO + * 响应先发、复位后到,不会把 SM1 半包留在总线上。APP 写的 EN=NOT_AVAL 正好命中 + * BOOT 救砖门 ecatArmed()=!(CHECK==AVAL&&EN==AVAL) ⇒ 直灌自动解锁,不需 word8 令牌。 + * 动因:0x2500/0x2501 只在 BOOT,实测对跑着的 APP 写 0x2500 回 + * abort=0x06020000「对象不存在」,跑着的机器必须先交接才能走总线。 + * (2) CoE 0x100A 从 SSC 占位 "1.0" 改成真版本串(DEVICE_SW_VERSION_LEN 0x3→0x10、 + * acSoftwareversion 显式 LEN+1 防越界读、initial.cpp 起板时写),主站刷完可以在 + * 总线上自证"新 APP 起来了、是哪一版",不必再靠串口或 SPI PING 身份块。 */ + /* 1.6.39:按用户裁定,跳转/启动链回退到 git 已提交基线(对齐 BOOT 46adcb7 + * = 1.1.0.14)——startup_gd32e50x_cl.s / system_gd32e50x.c / initial.cpp+h / + * systick.cpp / hardfault.cpp 全部回到 HEAD:裸机诊断回到 115200、无 + * __enable_irq、无 BKP 面包屑、无 haltWithWatchdog 兜底、无 3M 化。 + * 业务改动全部保留(CAN/PDO/桥/睡眠/task_manager/0x100A 版本上报)。 + * 未提交的 1.6.35~38 实验备份在 G:/Robot_v2/DCU/_revert_backup_20260916/。 */ +constexpr uint32_t APP_FW_VERSION_PATCH = 39U; /* 1.6.33: OP 输入新鲜度不变量(EcatDbg_OpInputFreshness)——SM-Synchron 下输入 PDO 只靠 PDI_Isr,通路一停主站读到冻缓冲(WKC/AL 仍正常=假连接);现 OP 内输入停更>5ms 由主循环强制 PDO_InputMapping+重开门。根治"连接看起来正常但 CPU/CAN 不更新、复位 GD32 才好"。 */ /* 1.6.32: SPI CS EXTI 提到抢占级0(见 app_spi_command configureExti),根治 CS release 被 PDI/RT_CRITICAL 延迟导致的 RX 撕裂(0xE8) *//* /* 1.6.28:ESC 睡眠挂起门控(EscLan9252::pmSuspended)。pmDrive 进 D1-D3 置位后全固件 HBI 消费者静默——task_runner 停调 MainLoop/AL 守护/AL 心跳(MainLoop 每圈 HBI 读 0x220,睡眠中任何 HBI 访问含读都把芯片打回 D0,2026-09-12 实测休眠 ~20s 即被主站"复活":芯片毫秒级被 GD32 自己叫醒、SM 失效后主站 wedge 重建 OP);ethercat_sync 三 EXTI ISR 与 timer Sync0 路径清 pending 即返。解除:pmDrive 唤醒成功/op0 读到 D0 自愈/nrstDrive 释放复位/hardwareReset。配套主站侧 ecat_native /api/pm/hold 楔死看门狗豁免。 */ /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */ /* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */ /* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */ /* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与旧基线对拍,真实写侧基准须置 0 重测。 */ diff --git a/GD32E508_DCU_APP/sys/initial.cpp b/GD32E508_DCU_APP/sys/initial.cpp index 1dfb818..82b0c92 100644 --- a/GD32E508_DCU_APP/sys/initial.cpp +++ b/GD32E508_DCU_APP/sys/initial.cpp @@ -37,6 +37,11 @@ uint8_t AppInitializer::s_initState = AppInitializer::kInitOk; uint8_t AppInitializer::s_resetCause = 0U; uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U}; +/* CoE 0x100A「Software version」写入口,实现在 comm/ssc/src/coeappl.c(C 链接)。 + * APP 侧原本一直回 SSC 占位 "1.0",主站在总线上读不到版本,只能靠 SPI PING 的 + * 4 字节二进制身份块;这条总线读数是刷写后「新 APP 真起来了」的自证。 */ +extern "C" void AppSetSoftwareVersion(const char *version); + namespace { struct InitEntry { @@ -279,6 +284,16 @@ void AppInitializer::init() OtaBootRequest::init(); mark(kOtaBootReq); + /* 版本号写进 CoE 0x100A:唯一真源是 sys/config.h 的 APP_FW_VERSION_*, + * 不在别处再抄一遍字符串。 */ + { + char ver[20]; + snprintf(ver, sizeof ver, "%u.%u.%u", + (unsigned)APP_FW_VERSION_MAJOR, (unsigned)APP_FW_VERSION_MINOR, + (unsigned)APP_FW_VERSION_PATCH); + AppSetSoftwareVersion(ver); + } + TaskManager::init(); mark(kTaskManager); diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.cpp b/GD32E508_DCU_APP/sys/ota_boot_request.cpp index 4123277..c618b2f 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.cpp +++ b/GD32E508_DCU_APP/sys/ota_boot_request.cpp @@ -223,3 +223,15 @@ void OtaBootRequest::process() while(1) { } } + +/*-------------------------------------------------------------------------- + * EtherCAT CoE 0x2502 的入口(comm/ssc/port/dc_app.c 是 C 文件,调不到类静态方法, + * 与 EcatPdo_* 同一形状)。写 1 走的是和 SPI 0xB0 完全相同的那条异步链: + * 这里只置标志,配置页三字的 Flash 写入 + NVIC_SystemReset 在主循环 + * OtaBootRequest::process() 下一圈做 —— SDO 响应先发、复位后到, + * 不会把 SM1 半包留在总线上。 + *-------------------------------------------------------------------------*/ +extern "C" void EcatEnterBootRequestAsync(void) +{ + OtaBootRequest::requestEnterBootAsync(); +} diff --git a/GD32E508_DCU_APP/sys/task_manager.cpp b/GD32E508_DCU_APP/sys/task_manager.cpp index e5dc3f7..5c7f926 100644 --- a/GD32E508_DCU_APP/sys/task_manager.cpp +++ b/GD32E508_DCU_APP/sys/task_manager.cpp @@ -40,13 +40,15 @@ void TaskManager::init() * @param[in] taskMask 待置位的任务位图掩码。 * @param[out] 无。 * @retval 无返回值。 - * @note 该操作不清除其他已存在的任务位。 + * @note 该操作不清除其他已存在的任务位;通过 __LDREXW/__STREXW 独占访问循环实现无锁原子置位。 */ void TaskManager::set(uint32_t taskMask) { - uint32_t save = RT_CRITICAL_SAVE(); - g_scheduler.task_flags |= taskMask; - RT_CRITICAL_RESTORE(save); + uint32_t val; + do { + val = __LDREXW(&g_scheduler.task_flags); + } while (__STREXW(val | taskMask, &g_scheduler.task_flags) != 0U); + __DMB(); } /** @@ -54,15 +56,15 @@ void TaskManager::set(uint32_t taskMask) * @param[in] 无。 * @param[out] 无。 * @retval uint32_t 返回当前待执行任务位图。 - * @note 内部通过关中断实现对 `task_flags` 的原子交换。 + * @note 通过 __LDREXW/__STREXW 独占访问循环实现对 `task_flags` 的原子读取与清零,彻底避免被 Preempt 0 ISR 抢占丢失标志。 */ uint32_t TaskManager::takePending() { uint32_t pending; - uint32_t save = RT_CRITICAL_SAVE(); - pending = g_scheduler.task_flags; - g_scheduler.task_flags &= ~pending; - RT_CRITICAL_RESTORE(save); + do { + pending = __LDREXW(&g_scheduler.task_flags); + } while (__STREXW(0U, &g_scheduler.task_flags) != 0U); + __DMB(); return pending; } diff --git a/GD32E508_DCU_APP/sys/task_manager.h b/GD32E508_DCU_APP/sys/task_manager.h index 46478a1..4c09703 100644 --- a/GD32E508_DCU_APP/sys/task_manager.h +++ b/GD32E508_DCU_APP/sys/task_manager.h @@ -33,7 +33,7 @@ public: * @param[in] 无。 * @param[out] 无。 * @retval uint32_t 返回当前待执行任务位图。 - * @note 内部通过关中断保证原子性,供 TaskRunner 消费。 + * @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证原子性,供 TaskRunner 消费。 */ static uint32_t takePending(); @@ -42,7 +42,7 @@ public: * @param[in] taskMask 待置位的任务位图掩码。 * @param[out] 无。 * @retval 无返回值。 - * @note 内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用。 + * @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证可由主循环与任意抢占优先级 ISR 共享调用。 */ static void set(uint32_t taskMask); diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index baab39e..5d96b02 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -11,6 +11,7 @@ #include "device/motor_state.h" #include "device/motor_version.h" #include "sys/config.h" +#include "sys/initial.h" #include "sys/ota_boot_request.h" #include "sys/motor_ota_relay.h" #include "sys/task_manager.h"