diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index f51633d..eb45995 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -79,6 +79,10 @@ project: - file: ./comm/ethercat_slave.cpp - file: ./comm/ethercat_sync.h - file: ./comm/ethercat_sync.cpp + - file: ./comm/ethercat_can_bridge.h + - file: ./comm/ethercat_can_bridge.cpp + - file: ./comm/ethercat_pdo.h + - file: ./comm/ethercat_pdo.cpp - group: comm/ssc files: - file: ./comm/ssc/inc/ecatslv.h diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp index 779f41c..2672edd 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp @@ -198,11 +198,22 @@ bool EscExmcBus::init() { /* 读/写共用时序(非 extended mode) * 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何 - * 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。 */ + * 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。 + * 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复), + * 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */ rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A; rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK; rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE; - rw_timing.bus_latency = 1U; + /* BUSLAT: 1→4 (1.5.57)。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 倍,覆盖全温区与电压波动。 + * 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD + * 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%, + * 4ms 周期占用从 5.4% 升至 5.7%,无实质影响。 */ + rw_timing.bus_latency = 4U; rw_timing.asyn_address_setuptime = 4U; /* ~22ns。DS 表9-4/9-5: trdrd/twrwr/twrrd ≥13ns——选通间隔=ADDSET 相,2 HCLK≈11.1ns 不达标(仅靠 AHB 间隙兜底),4 HCLK 稳过 */ @@ -212,8 +223,11 @@ bool EscExmcBus::init() { wr_timing = rw_timing; ECAT_LOGI("[ECAT][EXMC] init: applying NOR/SRAM Region0 params (16b, async, " - "t_data=%u HCLK)\r\n", - (unsigned)rw_timing.asyn_data_setuptime); + "ADDSET=%u AHLD=%u DSET=%u BUSLAT=%u HCLK)\r\n", + (unsigned)rw_timing.asyn_address_setuptime, + (unsigned)rw_timing.asyn_address_holdtime, + (unsigned)rw_timing.asyn_data_setuptime, + (unsigned)rw_timing.bus_latency); exmc_norsram_init(&norsram_init); exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0); @@ -229,6 +243,25 @@ bool EscExmcBus::init() { ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n", (unsigned long)snctl_before, (unsigned long)snctl_after); + /* 时序寄存器实测回读:确认写入的 BUSLAT 真正落到硬件(1.5.57)。 + * SNTCFG0 位域: [3:0]=ASET-1 [7:4]=AHLD-1 [15:8]=DSET-1 [19:16]=BUSLAT-1。 + * 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印, + * 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */ + { + const uint32_t tcfg = EXMC_SNTCFG0; + const unsigned aset = (unsigned)((tcfg & EXMC_SNTCFG_ASET) >> 0) + 1U; + const unsigned ahld = (unsigned)((tcfg & EXMC_SNTCFG_AHLD) >> 4) + 1U; + const unsigned dset = (unsigned)((tcfg & EXMC_SNTCFG_DSET) >> 8) + 1U; + const unsigned buslat = (unsigned)((tcfg & EXMC_SNTCFG_BUSLAT) >> 16) + 1U; + ECAT_LOGI("[ECAT][EXMC] init: SNTCFG0=0x%08lX -> ASET=%u AHLD=%u DSET=%u BUSLAT=%u\r\n", + (unsigned long)tcfg, aset, ahld, dset, buslat); + /* 5 HCLK @180MHz = 27.8ns,DS trdrd 下限 13ns → 需 BUSLAT>=2 才达标,取 4 留余量 */ + if (buslat < 4U) { + ECAT_LOGI("[ECAT][EXMC] WARN: BUSLAT=%u < 4, trdrd 余量不足," + " 温漂/压降时会偶发读到 0xFFFF 或错值\r\n", buslat); + } + } + /* 总线探活:LAN9252 处于 16-bit HBI Indexed Mode,无法通过固定偏移直接读取。 * 真实的芯片 ID / AL 状态校验由 EscLan9252::init() 通过 HBI_IDX_0/HBI_DATA_0 * 间接访问完成。 注意:LAN9252 上电后第一次 Host Bus diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.cpp b/GD32E508_DCU_APP/comm/esc_lan9252.cpp index 5c16cca..b501c3b 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.cpp +++ b/GD32E508_DCU_APP/comm/esc_lan9252.cpp @@ -58,6 +58,13 @@ constexpr uint32_t IRQ_CFG_IRQ_POL = 1UL << 4; /**< 1=高有效,0=低有效( constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏强制低有效)。 */ constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */ +/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认 + * divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——低于实测可用的 + * 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */ +constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */ +constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */ +constexpr uint16_t ESC_WD_TIME_PD_STEPS = 10000U; /**< 10000 × 100µs = 1s。 */ + /* LAN9252 直接映射的 ESC 访问端口(DS 表 12-13;经 HBI 索引直接读写) */ constexpr uint16_t ECAT_PRAM_RD_DATA = 0x0000U; /**< PRAM 读 FIFO(0x00-0x1C 别名,功能相同,突发用) */ constexpr uint16_t ECAT_PRAM_WR_DATA = 0x0020U; /**< PRAM 写 FIFO(0x20-0x3C 别名) */ @@ -257,6 +264,7 @@ bool EscLan9252::init() printf("[ECAT][ESC] PDI operational, EtherCAT core ready (DL_STATUS=0x%04X, waited %ums)\r\n", (unsigned)dlStatus, (unsigned)waited); enableEcatIrqOutput(); + configPdWatchdog(); } else { printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums " "(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n", @@ -279,6 +287,24 @@ bool EscLan9252::init() * 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。 * 注意 AL Event Mask(0x204) 由 SSC 在状态迁移时自行维护,不在此处设置。 */ +void EscLan9252::configPdWatchdog() +{ + /* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。 + * 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError() + * 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP, + * 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数; + * 正常周期(≤50ms)远小于 1s,行为不变。 */ + csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U); + const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U); + if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) { + printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n", + (unsigned)ESC_WD_TIME_PD_STEPS); + } else { + printf("[ECAT][ESC] WARN: PD watchdog writeback 0x%04lX != %u, 100ms 调试周期会被 0x001B 拖回 SAFEOP\r\n", + (unsigned long)(rd & 0xFFFFU), (unsigned)ESC_WD_TIME_PD_STEPS); + } +} + void EscLan9252::enableEcatIrqOutput() { /* 先清掉可能悬留的状态位(R/WC:写 1 清零),避免使能瞬间就有一个陈旧电平。 */ diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.h b/GD32E508_DCU_APP/comm/esc_lan9252.h index 0c5fb42..9c44b15 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.h +++ b/GD32E508_DCU_APP/comm/esc_lan9252.h @@ -93,6 +93,12 @@ private: * bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */ static void enableEcatIrqOutput(); + /** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。 + * @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000 + * (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗, + * SSC 以 0x001B 拖回 SAFEOP。 */ + static void configPdWatchdog(); + static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */ static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */ static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */ diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp new file mode 100644 index 0000000..f697dd9 --- /dev/null +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp @@ -0,0 +1,390 @@ +/** + * @file ethercat_can_bridge.cpp + * @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。 + */ +#include "comm/ethercat_can_bridge.h" +#include "device/canfd.h" +#include "device/motor_state.h" + +#include "RTE_Components.h" +#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */ + +#include + +volatile uint8_t EthercatCanBridge::s_lastSeq = 0U; +volatile bool EthercatCanBridge::s_seqPrimed = false; +volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U; +volatile uint16_t EthercatCanBridge::s_txCount = 0U; +volatile uint16_t EthercatCanBridge::s_dropCount = 0U; +volatile uint8_t EthercatCanBridge::s_winBase = 0U; + +volatile bool EthercatCanBridge::s_rxValid = false; +volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U; +volatile uint8_t EthercatCanBridge::s_rxCh = 0U; +volatile uint8_t EthercatCanBridge::s_rxDlc = 0U; +volatile bool EthercatCanBridge::s_rxTruncated = false; +volatile uint32_t EthercatCanBridge::s_rxId = 0U; +volatile uint32_t EthercatCanBridge::s_rxCount = 0U; +uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0}; + +EthercatCanBridge::RxRingItem + EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {}; +volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U; +volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U; +volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U; + +namespace { +/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */ +inline uint32_t rd32le(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} +inline void wr32le(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); +} +inline void wr16le(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8); +} +} // namespace + +uint8_t EthercatCanBridge::sendBatch(const uint8_t *out) +{ + uint8_t want = out[ECAT_TXB_OFF_COUNT]; + if (want == 0U) { + return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */ + } + if (want > kEcatTxBatchMax) { + want = kEcatTxBatchMax; + } + const bool stopOnError = (out[ECAT_TXB_OFF_FLAGS] & 0x01U) != 0U; + + uint8_t sent = 0U; + uint16_t off = ECAT_TXB_OFF_ITEMS; + + for (uint8_t i = 0U; i < want; ++i) { + /* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。 + * 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */ + if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) { + break; + } + const uint8_t *f = &out[off]; + uint8_t dlc = f[ECAT_FRM_OFF_DLC]; + if (dlc > kEcatCanFdDataMax) { + dlc = kEcatCanFdDataMax; + } + if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoOutBytes) { + break; + } + + const uint8_t ch = f[ECAT_FRM_OFF_CH]; + CanFd::CanTxResult r; + if (!CanFd::isChannelEnabled(ch)) { + r = CanFd::CanTxResult::kErrInvalid; + } else { + /* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道, + * transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧 + * 只发出 3 帧的根因),队列语义才不静默丢帧。 */ + r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]), + &f[ECAT_FRM_HDR_SIZE], dlc); + } + + if (r == CanFd::CanTxResult::kOk) { + ++s_txCount; + ++sent; + } else { + s_lastTxResult = (uint8_t)r; + if (stopOnError) { + break; + } + } + off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc); + } + return sent; +} + +void EthercatCanBridge::fillRxBatch(uint8_t *in) +{ + uint8_t count = 0U; + uint16_t off = ECAT_RXB_OFF_ITEMS; + uint8_t tail = s_rxRingTail; + const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */ + + while (tail != head && count < kEcatRxBatchMax) { + const RxRingItem &it = s_rxRing[tail]; + uint8_t dlc = it.dlc; + if (dlc > kEcatCanFdDataMax) { + dlc = kEcatCanFdDataMax; + } + /* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */ + if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoInBytes) { + break; + } + + uint8_t *p = &in[off]; + p[ECAT_FRM_OFF_CH] = it.ch; + p[ECAT_FRM_OFF_DLC] = dlc; + p[ECAT_FRM_OFF_FLAGS] = 0x01U; + wr32le(&p[ECAT_FRM_OFF_ID], it.id); + memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlc); + + off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc); + tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots); + ++count; + } + __DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */ + s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */ + + const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots); + in[ECAT_RXB_OFF_COUNT] = count; + in[ECAT_RXB_OFF_LOST] = s_rxRingLost; + in[ECAT_RXB_OFF_PENDING] = pending; + s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */ +} + +void EthercatCanBridge::processOutput(const uint8_t *out) +{ + if (out == nullptr) { + return; + } + + const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ]; + + /* 电机状态窗口下标每周期都取(与 seq 沿无关):主站靠移动这个值滚动扫表, + * 不该被「有没有新 CAN 帧要发」耦合。越界值钳到 0,避免读到槽数组外。 */ + const uint8_t win = out[ECAT_CAN_TX_OFF_WIN]; + s_winBase = (win < kMotorSlotCount) ? win : 0U; + + /* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的 + * 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */ + if (!s_seqPrimed) { + s_seqPrimed = true; + s_lastSeq = seq; + return; + } + + /* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */ + if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) { + s_txCount = 0U; + s_dropCount = 0U; + s_rxCount = 0U; + } + + if (seq == s_lastSeq) { + return; /* 没有新的发送请求 */ + } + s_lastSeq = seq; + + const uint8_t ch = out[ECAT_CAN_TX_OFF_CH]; + uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC]; + if (dlc > kEcatCanTxDataMax) { + dlc = kEcatCanTxDataMax; + } + + /* 单帧区先发(保持 1.5.55 语义),再发批量区。 + * dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */ + if (dlc > 0U) { + /* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */ + if (!CanFd::isChannelEnabled(ch)) { + s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid; + } else { + /* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到 + * 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载 + * 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、 + * 满时排队,不静默丢帧。 */ + const CanFd::CanTxResult r = + CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]), + &out[ECAT_CAN_TX_OFF_DATA], dlc); + s_lastTxResult = (uint8_t)r; + if (r == CanFd::CanTxResult::kOk) { + ++s_txCount; + } + } + } + + /* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */ + (void)sendBatch(out); +} + +void EthercatCanBridge::buildInput(uint8_t *in) +{ + if (in == nullptr) { + return; + } + /* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后 + * 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */ + memset(in, 0, kEcatPdoInBytes); + + /* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */ + in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq; + in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult; + wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount); + wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount); + wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount); + + /* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR + * 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零, + * 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */ + const bool xrxStable = ((s_xrxSeq & 1U) == 0U); + + if (xrxStable && s_rxValid) { + uint8_t flags = 0x01U; + if (s_rxTruncated) { + flags |= 0x02U; + } + in[ECAT_CAN_RX_OFF_CH] = s_rxCh; + in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc; + in[ECAT_CAN_RX_OFF_FLAGS] = flags; + wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId); + /* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */ + memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax); + } + + const uint8_t ch = s_rxValid ? s_rxCh : 0U; + const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U; + in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog; + + /* ── 段 2(32..319):电机状态窗口 ── + * 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过 + * 输出 PDO 的 WIN 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。 + * + * 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由 + * 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍, + * 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */ + const MotorStateTable &mt = MotorState::table(); + const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U; + + in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count; + in[ECAT_MOTW_OFF_BOOT] = mt.boot_active; + in[ECAT_MOTW_OFF_WIN_BASE] = base; + wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq); + + uint8_t count = 0U; + for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) { + const uint8_t idx = (uint8_t)(base + i); + if (idx >= kMotorSlotCount) { + break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */ + } + const MotorSlot &s = mt.slots[idx]; + uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize]; + + p[ECAT_MOT_OFF_NODE] = s.node_id; + p[ECAT_MOT_OFF_CH] = s.channel; + p[ECAT_MOT_OFF_ONLINE] = s.online; + p[ECAT_MOT_OFF_DLC] = s.dlc; + wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq); + memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc); + ++count; + } + in[ECAT_MOTW_OFF_WIN_COUNT] = count; + + /* ── 段 3(320..359):最近一帧完整快照 ── + * 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。 + * 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照 + * 和电机页的长快照会各自解出半新半旧的 ID/数据。 */ + if (xrxStable && s_rxValid) { + in[ECAT_XRX_OFF_CH] = s_rxCh; + in[ECAT_XRX_OFF_DLC] = s_rxDlc; + in[ECAT_XRX_OFF_FLAGS] = 0x01U; + wr32le(&in[ECAT_XRX_OFF_ID], s_rxId); + memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax); + } + + /* ── 段 4(360..639):CANFD 批量接收区 ── + * 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖 + * (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧, + * 让主站在一个周期内拿到完整的帧序列而不是采样点。 */ + fillRxBatch(in); +} + +void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id, + const uint8_t *data, uint8_t length) +{ + if (data == nullptr) { + return; + } + + /* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧, + * 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */ + if (s_rxValid) { + ++s_dropCount; + } + + /* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报, + * 这样 ecat.html 的 CAN 调试页语义不变。 */ + uint8_t copyLen = length; + if (copyLen > kEcatCanXRxDataMax) { + copyLen = kEcatCanXRxDataMax; + } + + /* ── 快照 seqlock(1.5.58)── + * s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体 + * 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会 + * 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致" + * 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没 + * 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG + * (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。 + * 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶 + * 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。 + * 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0; + * 而 seqlock 零抖动,与本文件既有无锁约定一致。 + * __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */ + ++s_xrxSeq; /* → 奇数:快照进入"正在写" */ + __DMB(); + + memcpy(s_rxData, data, copyLen); + if (copyLen < kEcatCanXRxDataMax) { + memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen)); + } + + s_rxCh = channel; + s_rxId = id; + s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */ + s_rxTruncated = (length > kEcatCanRxDataMax); + ++s_rxCount; + s_rxValid = true; + + __DMB(); + ++s_xrxSeq; /* → 偶数:快照整体一致 */ + + /* ── 推入批量接收环形队列(1.5.56)── + * 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义), + * 队列则保序累积,供 fillRxBatch 一次回收多帧。 + * 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整, + * 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */ + const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots); + if (next == s_rxRingTail) { + if (s_rxRingLost < 0xFFU) { + ++s_rxRingLost; + } + } else { + RxRingItem &it = s_rxRing[s_rxRingHead]; + uint8_t n = length; + if (n > kEcatCanFdDataMax) { + n = kEcatCanFdDataMax; + } + it.id = id; + it.ch = channel; + it.dlc = n; + memcpy(it.data, data, n); + __DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */ + s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */ + } +} + +extern "C" { + +void EcatCan_ProcessOutput(const uint8_t *out) +{ + EthercatCanBridge::processOutput(out); +} + +void EcatCan_BuildInput(uint8_t *in) +{ + EthercatCanBridge::buildInput(in); +} + +} /* extern "C" */ diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h new file mode 100644 index 0000000..10d3bd6 --- /dev/null +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h @@ -0,0 +1,278 @@ +/** + * @file ethercat_can_bridge.h + * @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。 + * + * 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B) + * 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照 + * + * 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的, + * 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达 + * 「一次性事件」的标准做法,主站只需每次发送前把 seq 加一。 + * + * RX 侧不能自己去 CanFd::consumeRxFrame() —— 那个队列已有唯一消费者 + * TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态 + * 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。 + * + * ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)── + * + * 三个动机: + * 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里 + * `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。 + * 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS, + * 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState + * 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口, + * 主站移动 win_base 即可 4 个周期扫完全部 27 槽。 + * 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。 + * + * **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样 + * tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input + * 不改一行也照常工作,扩容只是在后面追加新区段。 + * + * ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)── + * + * 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽 + * (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限, + * 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍, + * 失去"同步下发"的意义。 + * + * 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据: + * 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧 + * 输入 360..639(280B):批量接收,一周期回收多帧而不是只带最近一帧 + * + * 三条兼容性铁律(改动只做追加,不动既有偏移): + * 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32); + * 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧); + * 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。 + * + * 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。 + */ +#ifndef COMM_ETHERCAT_CAN_BRIDGE_H +#define COMM_ETHERCAT_CAN_BRIDGE_H + +#include + +#include "comm/ethercat_pdo_size.h" + +/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */ +enum : uint8_t { + ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */ + ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */ + ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */ + ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */ + ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */ + ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */ + /* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */ + ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */ +}; + +/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */ +enum : uint16_t { + ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */ + ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */ + ECAT_TXB_OFF_RSVD = 50U, /**< 保留 2B,让帧数组从 52 起、每帧 8B 头对齐 */ + ECAT_TXB_OFF_ITEMS = 52U, /**< 帧数组起点,每项 (8B 头 + dlc 数据) 紧密排列 */ +}; + +/** 批量帧项内部布局(头 8 字节,之后紧跟 dlc 字节数据)。收发两向共用。 */ +enum : uint8_t { + ECAT_FRM_OFF_CH = 0U, /**< 通道 0/1/2 */ + ECAT_FRM_OFF_DLC = 1U, /**< 数据长度 0..64(CANFD 全量) */ + ECAT_FRM_OFF_FLAGS = 2U, /**< 发送向:保留; 接收向 bit0=有效 */ + ECAT_FRM_OFF_RES = 3U, /**< 发送向:从站回填该帧发送结果; 接收向:保留 */ + ECAT_FRM_OFF_ID = 4U, /**< CAN ID,小端 uint32 */ + ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */ +}; + +/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */ +enum : uint8_t { + ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */ + ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */ + ECAT_CAN_RX_OFF_DLC = 2U, /**< 该帧数据长度(原始 DLC,未被截断前的值) */ + ECAT_CAN_RX_OFF_FLAGS = 3U, /**< bit0=有过接收;bit1=数据被截断到 8B */ + ECAT_CAN_RX_OFF_ID = 4U, /**< 该帧 CAN ID,小端 uint32 */ + ECAT_CAN_RX_OFF_DATA = 8U, /**< 数据区起点,8 字节(长帧见段 3) */ + ECAT_CAN_RX_OFF_RXCNT = 16U, /**< 累计接收帧数,小端 uint32 */ + ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */ + ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */ + ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */ + ECAT_CAN_RX_OFF_DROP = 24U, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */ +}; + +/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */ +enum : uint16_t { + ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */ + ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */ + ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */ + ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤7) */ + ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */ + ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */ +}; + +/** 电机状态槽内部布局(40 字节)。 */ +enum : uint8_t { + ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */ + ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */ + ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */ + ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 32) */ + ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */ + ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */ +}; + +/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */ +enum : uint16_t { + ECAT_XRX_OFF_CH = 320U, /**< 通道 */ + ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */ + ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */ + ECAT_XRX_OFF_RSVD = 323U, + ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */ + ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */ +}; + +/** 输入 PDO 段 4(360..639):CANFD 批量接收区(1.5.56 新增)。 */ +enum : uint16_t { + ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */ + ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */ + ECAT_RXB_OFF_PENDING = 362U, /**< 环形队列中仍待回收的帧数(主站据此提速) */ + ECAT_RXB_OFF_RSVD = 363U, + ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */ +}; + +/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */ +constexpr uint8_t kEcatCanTxDataMax = 32U; +/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */ +constexpr uint8_t kEcatCanRxDataMax = 8U; +/** 段 3 长快照数据区字节数。 */ +constexpr uint8_t kEcatCanXRxDataMax = 32U; + +/** 单个电机状态槽字节数。 */ +constexpr uint8_t kEcatMotSlotSize = 40U; +/** 一份输入 PDO 携带的电机状态槽数。 */ +constexpr uint8_t kEcatMotSlotsPerPdo = 7U; + +/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */ +constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */ +constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 80×64bit = 640B */ + +/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */ +constexpr uint8_t kEcatCanFdDataMax = 64U; + +/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */ +constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS); +constexpr uint8_t kEcatTxBatchMax = + (uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax)); + +/** 批量接收区可用字节数与最坏情况帧数上限。 */ +constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(kEcatPdoInBytes - ECAT_RXB_OFF_ITEMS); +constexpr uint8_t kEcatRxBatchMax = + (uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax)); + +/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。 + * 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */ +constexpr uint8_t kEcatRxRingSlots = (uint8_t)(kEcatRxBatchMax * 4U); + +/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。 + * 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */ +static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize + <= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠"); +static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_COUNT, + "长帧快照区与批量接收区重叠"); +static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT, + "单帧发送数据区与批量发送区重叠"); +static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界"); +static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界"); +/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */ +static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64"); +static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64"); + +/** + * @brief EtherCAT ↔ CAN 转发桥。 + * @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行, + * onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile + + * 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。 + * 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套), + * 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭—— + * Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。 + */ +class EthercatCanBridge { +public: + /** + * @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。 + * @param[in] out kEcatPdoOutBytes 字节输出过程数据。 + * @note 由 APPL_Application() 每周期调用。 + */ + static void processOutput(const uint8_t *out); + + /** + * @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。 + * @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。 + */ + static void buildInput(uint8_t *in); + + /** + * @brief CAN 收帧投递入口。 + * @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回 + * "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。 + */ + static void onCanFrame(uint8_t channel, uint32_t id, + const uint8_t *data, uint8_t length); + + /** @brief 桥接是否已收到过至少一帧(诊断用)。 */ + static bool hasReceived() { return s_rxValid; } + +private: + /** + * @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。 + * @return 实际受理的帧数。 + * @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count, + * 越界的 count 只会让循环提前结束,不会读到 PDO 之外。 + */ + static uint8_t sendBatch(const uint8_t *out); + + /** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */ + static void fillRxBatch(uint8_t *in); + + static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */ + static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */ + static volatile uint8_t s_lastTxResult; + static volatile uint16_t s_txCount; + static volatile uint16_t s_dropCount; + /** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */ + static volatile uint8_t s_winBase; + + /* 最近一帧接收快照:taskCanRxParse 写、PDO 读。 + * s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。 + * 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据 + * 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */ + static volatile bool s_rxValid; + static volatile uint32_t s_xrxSeq; + static volatile uint8_t s_rxCh; + static volatile uint8_t s_rxDlc; + static volatile bool s_rxTruncated; + static volatile uint32_t s_rxId; + static volatile uint32_t s_rxCount; + static uint8_t s_rxData[kEcatCanXRxDataMax]; + + /* ── 批量接收环形队列(1.5.56)── + * 单生产者(taskCanRxParse 的 onCanFrame) / 单消费者(PDO 的 fillRxBatch)。 + * head/tail 各由一侧独占写入,故无需临界区:生产者只推进 head、 + * 消费者只推进 tail,最坏情况是某周期少读一帧,下周期即取到。 */ + struct RxRingItem { + uint32_t id; + uint8_t ch; + uint8_t dlc; + uint8_t data[kEcatCanFdDataMax]; + }; + static RxRingItem s_rxRing[kEcatRxRingSlots]; + static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */ + static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */ + static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */ +}; + +extern "C" { +/** @brief 供 dc_app.c(C 文件)调用的 shim:处理输出 PDO。 */ +void EcatCan_ProcessOutput(const uint8_t *out); +/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */ +void EcatCan_BuildInput(uint8_t *in); +} + +#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.cpp b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp new file mode 100644 index 0000000..47e53d9 --- /dev/null +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.cpp @@ -0,0 +1,71 @@ +/** + * @file ethercat_pdo.cpp + * @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。 + */ +#include "comm/ethercat_pdo.h" +#include "comm/ethercat_can_bridge.h" + +#include + +uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0}; +uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0}; + +volatile uint32_t EthercatPdo::s_outputCycles = 0U; +volatile uint32_t EthercatPdo::s_inputCycles = 0U; + +void EthercatPdo::readOutputs(const uint8_t *esc) +{ + if (esc == nullptr) { + return; + } + memcpy(s_out, esc, kPdoOutBytes); + ++s_outputCycles; +} + +void EthercatPdo::process() +{ + /* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送, + * 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。 + * + * 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的 + * seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。 + * processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */ + EthercatCanBridge::processOutput(s_out); + EthercatCanBridge::buildInput(s_in); +} + +void EthercatPdo::writeInputs(uint8_t *esc) +{ + if (esc == nullptr) { + return; + } + memcpy(esc, s_in, kPdoInBytes); + ++s_inputCycles; +} + +void EthercatPdo::snapshotOutput(uint8_t *dst) +{ + if (dst == nullptr) { + return; + } + memcpy(dst, s_out, kPdoOutBytes); +} + +extern "C" { + +void EcatPdo_ReadOutputs(const uint8_t *esc) +{ + EthercatPdo::readOutputs(esc); +} + +void EcatPdo_Process(void) +{ + EthercatPdo::process(); +} + +void EcatPdo_WriteInputs(uint8_t *esc) +{ + EthercatPdo::writeInputs(esc); +} + +} /* extern "C" */ diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.h b/GD32E508_DCU_APP/comm/ethercat_pdo.h new file mode 100644 index 0000000..77d8111 --- /dev/null +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.h @@ -0,0 +1,98 @@ +/** + * @file ethercat_pdo.h + * @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。 + * + * 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口: + * 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据 + * 2. APPL_Application() → process() 跑应用逻辑(当前是 PDO↔CAN 转发) + * 3. APPL_InputMapping() → writeInputs() 把输入数据交回 ESC(从站→主站) + * + * 缓冲区归本类所有,dc_app.c 只保留 SSC 对象字典(0x7000/0x6000 的 packed 结构体 + * 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个 + * 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。 + * + * @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出 + * 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写 + * 一个魔数后失配。 + * + * @note 上下文:free-run 时三个回调都在主循环(ECAT_Main / ECAT_Application); + * 启用 DC 后 readOutputs/writeInputs 改由 Sync0_Isr(preempt=0) 调用,而 + * process() 仍在主循环。因此 s_out/s_in 是跨上下文共享的——两者都只在 + * 单一写者下被整块读写,且 CM4 上 memcpy 的撕裂最坏只会让某个周期读到 + * 上/下半帧混合。对 seq 沿触发语义无害(seq 是单字节,要么旧要么新, + * 不会产生第三个值),故不加临界区,避免在 ISR 里关中断拖累抖动。 + */ +#ifndef COMM_ETHERCAT_PDO_H +#define COMM_ETHERCAT_PDO_H + +#include + +#include "comm/ethercat_can_bridge.h" + +/** + * @brief EtherCAT 过程数据收发。 + * @note 全静态类,与 EthercatSlave / EthercatSync 同风格。 + */ +class EthercatPdo { +public: + /** @brief 输出过程数据长度(主站→从站)。 */ + static constexpr uint16_t kPdoOutBytes = kEcatPdoOutBytes; + /** @brief 输入过程数据长度(从站→主站)。 */ + static constexpr uint16_t kPdoInBytes = kEcatPdoInBytes; + + /** + * @brief 取走主站下发的输出过程数据(主站→从站)。 + * @param[in] esc 指向 ESC 输出缓冲的 kPdoOutBytes 字节。 + * @note 由 APPL_OutputMapping() 每周期调用;只做搬运不做业务, + * 业务在 process() 里跑,保证 ISR 上下文(DC 模式)足够短。 + */ + static void readOutputs(const uint8_t *esc); + + /** + * @brief 跑一个应用周期:消费输出、生成输入。 + * @note 由 APPL_Application() 在主循环调用。 + */ + static void process(); + + /** + * @brief 交回输入过程数据(从站→主站)。 + * @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。 + * @note 由 APPL_InputMapping() 每周期调用。 + */ + static void writeInputs(uint8_t *esc); + + /** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */ + static uint32_t outputCycles() { return s_outputCycles; } + + /** @brief 累计交回输入的周期数。 */ + static uint32_t inputCycles() { return s_inputCycles; } + + /** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */ + static bool isExchanging() { return s_outputCycles != 0U; } + + /** + * @brief 拷出最近一份输出过程数据快照(诊断用)。 + * @param[out] dst 至少 kPdoOutBytes 字节。 + */ + static void snapshotOutput(uint8_t *dst); + +private: + /** @brief 最近一份主站下发的输出数据。 */ + static uint8_t s_out[kPdoOutBytes]; + /** @brief 待交回主站的输入数据。 */ + static uint8_t s_in[kPdoInBytes]; + + static volatile uint32_t s_outputCycles; + static volatile uint32_t s_inputCycles; +}; + +extern "C" { +/** @brief 供 dc_app.c(C 文件)调用:取走输出过程数据。 */ +void EcatPdo_ReadOutputs(const uint8_t *esc); +/** @brief 供 dc_app.c 调用:跑一个应用周期。 */ +void EcatPdo_Process(void); +/** @brief 供 dc_app.c 调用:交回输入过程数据。 */ +void EcatPdo_WriteInputs(uint8_t *esc); +} + +#endif /* COMM_ETHERCAT_PDO_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo_size.h b/GD32E508_DCU_APP/comm/ethercat_pdo_size.h new file mode 100644 index 0000000..623233a --- /dev/null +++ b/GD32E508_DCU_APP/comm/ethercat_pdo_size.h @@ -0,0 +1,66 @@ +/** + * @file ethercat_pdo_size.h + * @brief EtherCAT 过程数据长度 —— C / C++ 共用的唯一真源(1.5.55)。 + * + * 过程数据长度必须在**五处**保持一致,任一处失配的表现都是运行期 AL 错误 + * (PREOP→SAFEOP 报 0x001A/0x0011),排查成本远高于编译期报错: + * + * 1. comm/ssc/port/ecat_def.h MAX_PD_OUTPUT_SIZE / MAX_PD_INPUT_SIZE(编译门限) + * 2. comm/ssc/port/dc_app.c 0x7000/0x6000 对象尺寸 + APPL_GenerateMapping + * 3. comm/ssc/port/gen_eeprom.py PD_OUT_SIZE / PD_IN_SIZE(SII,需重刷 EEPROM) + * 4. comm/ssc/port/DCU_ESI.xml SM 长度 + PDO 条目 + * 5. comm/ethercat_can_bridge.h 桥接层字节布局 + * + * 本头只放纯 #define,故 C(dc_app.c)与 C++(ethercat_can_bridge.h)都能包含, + * 双方各自用 _Static_assert / static_assert 绑到这两个宏上。改尺寸时先改这里, + * 编译器会把所有没跟上的地方报出来;EEPROM 侧(3/4)没法编译期检查,改完必须 + * 重新生成 SII 并**断电冷启动**,ESC 只在冷启动时重新加载 EEPROM。 + */ +#ifndef COMM_ETHERCAT_PDO_SIZE_H +#define COMM_ETHERCAT_PDO_SIZE_H + +/** + * ── 1.5.56 扩到 DPRAM 硬上限(64/384 → 384/640)── + * + * 「一次能发多大」的真实约束不是 EtherCAT 协议(单帧可载 ~1486B 数据报), + * 而是 LAN9252 的 4KB DPRAM 加 3-buffer 同步管理器: + * + * 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF; + * 过程数据区只剩 0x1400-0x1FFF = 3072B; + * SM2/SM3 都是 3-buffer 模式(control 0x64/0x20,bit1=0),实占 3×size。 + * ⇒ (out + in) × 3 ≤ 3072 ⇒ **out + in ≤ 1024B 是硬上限**。 + * + * 384 + 640 = 1024,SM2 占 0x1400-0x187F、SM3 占 0x1880-0x1FFF, + * 恰好用满到最后一字节,没有任何浪费也没有越界。 + * + * 其余曾疑似的天花板都已实测排除,都不是瓶颈: + * - SII/EEPROM:640B 输入需 80 个映射条目,SII 共用 1284/4096B(余 2812B); + * nEntry 是单字节字段,80 远低于 255 上限。 + * - EXMC/HBI 搬运:满载 1024B ≈ 218us/周期(每 DWORD 约 0.85us), + * 4ms 周期只占 5.4%,甚至 1ms 周期也留有余量。 + * - MAX_PD_*_SIZE 只影响 ecatappl.c 的 aPdOutputData/aPdInputData 静态数组, + * 共 1024B RAM,在 128KB SRAM 上无压力。 + * + * 若将来还要更大,只有两条路(都有代价,非必要不动): + * a) 邮箱压到 MIN_MBX_SIZE(0x22) 级别 → 上限约 1280B,但会拖慢 SDO/FoE; + * b) 改 1-buffer 模式(control bit1=1)→ 上限 3072B,但失去三缓冲的 + * 读写并发保护,主从同时访问同一缓冲会读到半新半旧的过程数据。 + */ + +/** 输出过程数据字节数(主站→从站,SM2):48×64bit。 */ +#define ECAT_PDO_OUT_BYTES 384 + +/** 输入过程数据字节数(从站→主站,SM3):80×64bit。 */ +#define ECAT_PDO_IN_BYTES 640 + +/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */ +#define ECAT_PD_AREA_START 0x1400 +#define ECAT_PD_AREA_END 0x1FFF +#define ECAT_PD_SM_BUFFERS 3 + +#if (ECAT_PDO_OUT_BYTES + ECAT_PDO_IN_BYTES) * ECAT_PD_SM_BUFFERS > \ + (ECAT_PD_AREA_END - ECAT_PD_AREA_START + 1) +#error "out+in 超出 LAN9252 过程数据区(3-buffer 模式下 (out+in)*3 必须 <= 3072)" +#endif + +#endif /* COMM_ETHERCAT_PDO_SIZE_H */ diff --git a/GD32E508_DCU_APP/comm/ssc/port/DCU_ESI.xml b/GD32E508_DCU_APP/comm/ssc/port/DCU_ESI.xml index 18b0af9..a920756 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/DCU_ESI.xml +++ b/GD32E508_DCU_APP/comm/ssc/port/DCU_ESI.xml @@ -23,11 +23,13 @@ MBoxIn - Outputs + Outputs - Inputs - + Inputs + #x1600 Output @@ -59,8 +61,320 @@ Control Data 4 ULINT + + #x7000 + 5 + 64 + Control Data 5 + ULINT + + + #x7000 + 6 + 64 + Control Data 6 + ULINT + + + #x7000 + 7 + 64 + Control Data 7 + ULINT + + + #x7000 + 8 + 64 + Control Data 8 + ULINT + + + #x7000 + 9 + 64 + Control Data 9 + ULINT + + + #x7000 + 10 + 64 + Control Data 10 + ULINT + + + #x7000 + 11 + 64 + Control Data 11 + ULINT + + + #x7000 + 12 + 64 + Control Data 12 + ULINT + + + #x7000 + 13 + 64 + Control Data 13 + ULINT + + + #x7000 + 14 + 64 + Control Data 14 + ULINT + + + #x7000 + 15 + 64 + Control Data 15 + ULINT + + + #x7000 + 16 + 64 + Control Data 16 + ULINT + + + #x7000 + 17 + 64 + Control Data 17 + ULINT + + + #x7000 + 18 + 64 + Control Data 18 + ULINT + + + #x7000 + 19 + 64 + Control Data 19 + ULINT + + + #x7000 + 20 + 64 + Control Data 20 + ULINT + + + #x7000 + 21 + 64 + Control Data 21 + ULINT + + + #x7000 + 22 + 64 + Control Data 22 + ULINT + + + #x7000 + 23 + 64 + Control Data 23 + ULINT + + + #x7000 + 24 + 64 + Control Data 24 + ULINT + + + #x7000 + 25 + 64 + Control Data 25 + ULINT + + + #x7000 + 26 + 64 + Control Data 26 + ULINT + + + #x7000 + 27 + 64 + Control Data 27 + ULINT + + + #x7000 + 28 + 64 + Control Data 28 + ULINT + + + #x7000 + 29 + 64 + Control Data 29 + ULINT + + + #x7000 + 30 + 64 + Control Data 30 + ULINT + + + #x7000 + 31 + 64 + Control Data 31 + ULINT + + + #x7000 + 32 + 64 + Control Data 32 + ULINT + + + #x7000 + 33 + 64 + Control Data 33 + ULINT + + + #x7000 + 34 + 64 + Control Data 34 + ULINT + + + #x7000 + 35 + 64 + Control Data 35 + ULINT + + + #x7000 + 36 + 64 + Control Data 36 + ULINT + + + #x7000 + 37 + 64 + Control Data 37 + ULINT + + + #x7000 + 38 + 64 + Control Data 38 + ULINT + + + #x7000 + 39 + 64 + Control Data 39 + ULINT + + + #x7000 + 40 + 64 + Control Data 40 + ULINT + + + #x7000 + 41 + 64 + Control Data 41 + ULINT + + + #x7000 + 42 + 64 + Control Data 42 + ULINT + + + #x7000 + 43 + 64 + Control Data 43 + ULINT + + + #x7000 + 44 + 64 + Control Data 44 + ULINT + + + #x7000 + 45 + 64 + Control Data 45 + ULINT + + + #x7000 + 46 + 64 + Control Data 46 + ULINT + + + #x7000 + 47 + 64 + Control Data 47 + ULINT + + + #x7000 + 48 + 64 + Control Data 48 + ULINT + - + #x1A00 Input @@ -92,6 +406,538 @@ Status Data 4 ULINT + + #x6000 + 5 + 64 + Status Data 5 + ULINT + + + #x6000 + 6 + 64 + Status Data 6 + ULINT + + + #x6000 + 7 + 64 + Status Data 7 + ULINT + + + #x6000 + 8 + 64 + Status Data 8 + ULINT + + + #x6000 + 9 + 64 + Status Data 9 + ULINT + + + #x6000 + 10 + 64 + Status Data 10 + ULINT + + + #x6000 + 11 + 64 + Status Data 11 + ULINT + + + #x6000 + 12 + 64 + Status Data 12 + ULINT + + + #x6000 + 13 + 64 + Status Data 13 + ULINT + + + #x6000 + 14 + 64 + Status Data 14 + ULINT + + + #x6000 + 15 + 64 + Status Data 15 + ULINT + + + #x6000 + 16 + 64 + Status Data 16 + ULINT + + + #x6000 + 17 + 64 + Status Data 17 + ULINT + + + #x6000 + 18 + 64 + Status Data 18 + ULINT + + + #x6000 + 19 + 64 + Status Data 19 + ULINT + + + #x6000 + 20 + 64 + Status Data 20 + ULINT + + + #x6000 + 21 + 64 + Status Data 21 + ULINT + + + #x6000 + 22 + 64 + Status Data 22 + ULINT + + + #x6000 + 23 + 64 + Status Data 23 + ULINT + + + #x6000 + 24 + 64 + Status Data 24 + ULINT + + + #x6000 + 25 + 64 + Status Data 25 + ULINT + + + #x6000 + 26 + 64 + Status Data 26 + ULINT + + + #x6000 + 27 + 64 + Status Data 27 + ULINT + + + #x6000 + 28 + 64 + Status Data 28 + ULINT + + + #x6000 + 29 + 64 + Status Data 29 + ULINT + + + #x6000 + 30 + 64 + Status Data 30 + ULINT + + + #x6000 + 31 + 64 + Status Data 31 + ULINT + + + #x6000 + 32 + 64 + Status Data 32 + ULINT + + + #x6000 + 33 + 64 + Status Data 33 + ULINT + + + #x6000 + 34 + 64 + Status Data 34 + ULINT + + + #x6000 + 35 + 64 + Status Data 35 + ULINT + + + #x6000 + 36 + 64 + Status Data 36 + ULINT + + + #x6000 + 37 + 64 + Status Data 37 + ULINT + + + #x6000 + 38 + 64 + Status Data 38 + ULINT + + + #x6000 + 39 + 64 + Status Data 39 + ULINT + + + #x6000 + 40 + 64 + Status Data 40 + ULINT + + + #x6000 + 41 + 64 + Status Data 41 + ULINT + + + #x6000 + 42 + 64 + Status Data 42 + ULINT + + + #x6000 + 43 + 64 + Status Data 43 + ULINT + + + #x6000 + 44 + 64 + Status Data 44 + ULINT + + + #x6000 + 45 + 64 + Status Data 45 + ULINT + + + #x6000 + 46 + 64 + Status Data 46 + ULINT + + + #x6000 + 47 + 64 + Status Data 47 + ULINT + + + #x6000 + 48 + 64 + Status Data 48 + ULINT + + + #x6000 + 49 + 64 + Status Data 49 + ULINT + + + #x6000 + 50 + 64 + Status Data 50 + ULINT + + + #x6000 + 51 + 64 + Status Data 51 + ULINT + + + #x6000 + 52 + 64 + Status Data 52 + ULINT + + + #x6000 + 53 + 64 + Status Data 53 + ULINT + + + #x6000 + 54 + 64 + Status Data 54 + ULINT + + + #x6000 + 55 + 64 + Status Data 55 + ULINT + + + #x6000 + 56 + 64 + Status Data 56 + ULINT + + + #x6000 + 57 + 64 + Status Data 57 + ULINT + + + #x6000 + 58 + 64 + Status Data 58 + ULINT + + + #x6000 + 59 + 64 + Status Data 59 + ULINT + + + #x6000 + 60 + 64 + Status Data 60 + ULINT + + + #x6000 + 61 + 64 + Status Data 61 + ULINT + + + #x6000 + 62 + 64 + Status Data 62 + ULINT + + + #x6000 + 63 + 64 + Status Data 63 + ULINT + + + #x6000 + 64 + 64 + Status Data 64 + ULINT + + + #x6000 + 65 + 64 + Status Data 65 + ULINT + + + #x6000 + 66 + 64 + Status Data 66 + ULINT + + + #x6000 + 67 + 64 + Status Data 67 + ULINT + + + #x6000 + 68 + 64 + Status Data 68 + ULINT + + + #x6000 + 69 + 64 + Status Data 69 + ULINT + + + #x6000 + 70 + 64 + Status Data 70 + ULINT + + + #x6000 + 71 + 64 + Status Data 71 + ULINT + + + #x6000 + 72 + 64 + Status Data 72 + ULINT + + + #x6000 + 73 + 64 + Status Data 73 + ULINT + + + #x6000 + 74 + 64 + Status Data 74 + ULINT + + + #x6000 + 75 + 64 + Status Data 75 + ULINT + + + #x6000 + 76 + 64 + Status Data 76 + ULINT + + + #x6000 + 77 + 64 + Status Data 77 + ULINT + + + #x6000 + 78 + 64 + Status Data 78 + ULINT + + + #x6000 + 79 + 64 + Status Data 79 + ULINT + + + #x6000 + 80 + 64 + Status Data 80 + ULINT + diff --git a/GD32E508_DCU_APP/comm/ssc/port/__pycache__/gen_eeprom.cpython-312.pyc b/GD32E508_DCU_APP/comm/ssc/port/__pycache__/gen_eeprom.cpython-312.pyc index ea8235e..4d0275f 100644 Binary files a/GD32E508_DCU_APP/comm/ssc/port/__pycache__/gen_eeprom.cpython-312.pyc and b/GD32E508_DCU_APP/comm/ssc/port/__pycache__/gen_eeprom.cpython-312.pyc differ diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index b42f3c6..b9ae79e 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -4,12 +4,17 @@ * * 对象字典结构 (匹配 DCU_ESI.xml / gen_eeprom.py): * 0x1C12 RxPDO Assign → 0x1600 - * 0x1600 RxPDO Mapping → 0x7000:01..04 (4×64bit = 32 bytes) - * 0x7000 Output Data (主站→从站, 32 bytes) + * 0x1600 RxPDO Mapping → 0x7000:01..08 (8×64bit = 64 bytes) + * 0x7000 Output Data (主站→从站, 64 bytes) * 0x1C13 TxPDO Assign → 0x1A00 - * 0x1A00 TxPDO Mapping → 0x6000:01..04 (4×64bit = 32 bytes) - * 0x6000 Input Data (从站→主站, 32 bytes) + * 0x1A00 TxPDO Mapping → 0x6000:01..48 (48×64bit = 384 bytes) + * 0x6000 Input Data (从站→主站, 384 bytes) * 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路) + * + * 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。 + * 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见 + * 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个 + * 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。 */ #include "ecat_def.h" @@ -18,9 +23,11 @@ #include "ecatappl.h" #include "objdef.h" #include "dc_app.h" +#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */ #include #include /* offsetof:下方布局守卫断言使用 */ +#include /* printf:0x204 守护 WARN 日志(1.5.48) */ /*--------------------------------------------------------------------------- * el9800appl.h 桩 include (SSC 内部需要) @@ -31,31 +38,49 @@ /*--------------------------------------------------------------------------- * PDO 数据缓冲 + * + * 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。 + * 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定, + * 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。 *---------------------------------------------------------------------------*/ +/** 输出/输入 PDO 的 64bit 条目数。 */ +#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */ +#define PDO_IN_ENTRIES 80u /* 80×8 = 640 B */ + /** 输出数据对象 0x7000 (主站→从站, RxPDO) */ typedef struct OBJ_STRUCT_PACKED_START { UINT16 u16SubIndex0; - UINT64 u64Data[4]; /* 4×8 = 32 bytes */ + UINT64 u64Data[PDO_OUT_ENTRIES]; } OBJ_STRUCT_PACKED_END TOBJ7000; /** 输入数据对象 0x6000 (从站→主站, TxPDO) */ typedef struct OBJ_STRUCT_PACKED_START { UINT16 u16SubIndex0; - UINT64 u64Data[4]; /* 4×8 = 32 bytes */ + UINT64 u64Data[PDO_IN_ENTRIES]; } OBJ_STRUCT_PACKED_END TOBJ6000; -static TOBJ7000 s_outputObj = {4, {0, 0, 0, 0}}; -static TOBJ6000 s_inputObj = {4, {0, 0, 0, 0}}; +static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}}; +static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}}; /* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排" * 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。 * OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */ _Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START"); _Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START"); -_Static_assert(sizeof(TOBJ7000) == 34, "TOBJ7000 尺寸应为 2+32"); +_Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384"); +_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+640"); + +/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败, + * 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */ +_Static_assert(8 * PDO_OUT_ENTRIES == ECAT_PDO_OUT_BYTES, "输出 PDO 长度与桥接层不一致"); +_Static_assert(8 * PDO_IN_ENTRIES == ECAT_PDO_IN_BYTES, "输入 PDO 长度与桥接层不一致"); +/* SSC 编译期门限(ecat_def.h)必须容得下:否则 ecatslv.c 在 SM 校验时拒绝进 SAFEOP。 */ +_Static_assert(8 * PDO_OUT_ENTRIES <= MAX_PD_OUTPUT_SIZE, "MAX_PD_OUTPUT_SIZE 太小"); +_Static_assert(8 * PDO_IN_ENTRIES <= MAX_PD_INPUT_SIZE, "MAX_PD_INPUT_SIZE 太小"); + /*--------------------------------------------------------------------------- * SYNC 诊断对象 0x2000 (只读, 主站 SDO 轮询) @@ -83,6 +108,13 @@ extern UINT32 EcatDbg_GetSync1(void); extern UINT32 EcatDbg_GetEscIrq(void); extern UINT32 EcatDbg_GetUptimeSec(void); +/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。 + * 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发" + * 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */ +extern void EcatPdo_ReadOutputs(const UINT8 *esc); +extern void EcatPdo_Process(void); +extern void EcatPdo_WriteInputs(UINT8 *esc); + /*--------------------------------------------------------------------------- * PDO Assign 对象 (0x1C12 / 0x1C13) *---------------------------------------------------------------------------*/ @@ -98,30 +130,49 @@ static TOBJ_PDO_ASSIGN sTxPDOassign = {1, {0x1A00}}; /*--------------------------------------------------------------------------- * PDO Mapping 对象 (0x1600 / 0x1A00) + * + * 映射条目值 = Index<<16 | SubIndex<<8 | BitLen,这里 BitLen 恒为 0x40(64bit)。 + * 48 个条目手写既冗长又容易错位,改用宏按 8 个一组展开。 *---------------------------------------------------------------------------*/ +#define PDO_MAP_ENTRY(idx, sub) (((UINT32)(idx) << 16) | ((UINT32)(sub) << 8) | 0x40u) +/* 一组 8 个:sub = base+1 .. base+8 */ +#define PDO_MAP_8(idx, base) \ + PDO_MAP_ENTRY(idx, (base) + 1), PDO_MAP_ENTRY(idx, (base) + 2), \ + PDO_MAP_ENTRY(idx, (base) + 3), PDO_MAP_ENTRY(idx, (base) + 4), \ + PDO_MAP_ENTRY(idx, (base) + 5), PDO_MAP_ENTRY(idx, (base) + 6), \ + PDO_MAP_ENTRY(idx, (base) + 7), PDO_MAP_ENTRY(idx, (base) + 8) + typedef struct OBJ_STRUCT_PACKED_START { UINT16 u16SubIndex0; - UINT32 aEntries[4]; + UINT32 aEntries[PDO_OUT_ENTRIES]; } OBJ_STRUCT_PACKED_END -TOBJ_PDO_MAPPING; +TOBJ_RXPDO_MAPPING; -/* 0x1600: 映射 0x7000:01..04, 每个 64 bits */ -static TOBJ_PDO_MAPPING sRxPDOMap = {4, { - 0x70000140, /* Index=0x7000, SubIndex=0x01, BitLen=64 */ - 0x70000240, /* Index=0x7000, SubIndex=0x02, BitLen=64 */ - 0x70000340, /* Index=0x7000, SubIndex=0x03, BitLen=64 */ - 0x70000440 /* Index=0x7000, SubIndex=0x04, BitLen=64 */ +typedef struct OBJ_STRUCT_PACKED_START { + UINT16 u16SubIndex0; + UINT32 aEntries[PDO_IN_ENTRIES]; +} OBJ_STRUCT_PACKED_END +TOBJ_TXPDO_MAPPING; + +/* 0x1600: 映射 0x7000:01..48, 每个 64 bits */ +static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, { + PDO_MAP_8(0x7000, 0), PDO_MAP_8(0x7000, 8), PDO_MAP_8(0x7000, 16), + PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40) }}; -/* 0x1A00: 映射 0x6000:01..04, 每个 64 bits */ -static TOBJ_PDO_MAPPING sTxPDOMap = {4, { - 0x60000140, /* Index=0x6000, SubIndex=0x01, BitLen=64 */ - 0x60000240, /* Index=0x6000, SubIndex=0x02, BitLen=64 */ - 0x60000340, /* Index=0x6000, SubIndex=0x03, BitLen=64 */ - 0x60000440 /* Index=0x6000, SubIndex=0x04, BitLen=64 */ +/* 0x1A00: 映射 0x6000:01..80, 每个 64 bits */ +static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, { + PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16), + PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40), + PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64), + PDO_MAP_8(0x6000, 72) }}; +_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符"); +_Static_assert(sizeof(sTxPDOMap.aEntries) / 4 == PDO_IN_ENTRIES, "0x1A00 条目数不符"); + + /*--------------------------------------------------------------------------- * Entry Descriptions * @@ -131,7 +182,11 @@ static TOBJ_PDO_MAPPING sTxPDOMap = {4, { * 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" / * "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错 * 而 sub1/2 侥幸通过, 正是越界特征)。 - * 另: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING, + * + * 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00 + * 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是 + * ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。 + * 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING, * 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。 *---------------------------------------------------------------------------*/ @@ -142,30 +197,21 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOAssignEntryDesc[] = { {DEFTYPE_UNSIGNED16, 0x10, ACCESS_READ} }; -/* PDO Mapping (0x1600/0x1A00): REC 含 4 个映射条目 -> 必须给 Sub0..Sub4 共 5 项。 */ +/* PDO Mapping (0x1600/0x1A00): ARR of UINT32 映射条目, 2 项即可。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOMappingEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* Sub0: 条目数 */ - {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub1 */ - {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub2 */ - {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub3 */ - {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} /* Sub4 */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} /* Sub1..n: 通用项 */ }; -/* 0x7000 Output Data: REC, Sub0..Sub4 共 5 项; 子条目可被 RxPDO 映射。 */ +/* 0x7000 Output Data: ARR of UINT64; 子条目可被 RxPDO 映射。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asOutputEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING} }; -/* 0x6000 Input Data: REC, Sub0..Sub4 共 5 项; 子条目可被 TxPDO 映射, 输入侧只读。 */ +/* 0x6000 Input Data: ARR of UINT64; 子条目可被 TxPDO 映射, 输入侧只读。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING} }; @@ -199,24 +245,24 @@ TOBJECT OBJMEM ApplicationObjDicList[] = { {NULL, NULL, 0x1C12, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, asPDOAssignEntryDesc, aName0x1C12, &sRxPDOassign, NULL, NULL, 0x0000}, - /* 0x1600: RxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ - {NULL, NULL, 0x1600, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, + /* 0x1600: RxPDO Mapping (ARR of UINT32, PDO_OUT_ENTRIES 个映射条目) */ + {NULL, NULL, 0x1600, {DEFTYPE_PDOMAPPING, PDO_OUT_ENTRIES | (OBJCODE_ARR << 8)}, asPDOMappingEntryDesc, aName0x1600, &sRxPDOMap, NULL, NULL, 0x0000}, - /* 0x7000: Output Data (主站→从站) */ - {NULL, NULL, 0x7000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, + /* 0x7000: Output Data (主站→从站, ARR of UINT64) */ + {NULL, NULL, 0x7000, {DEFTYPE_UNSIGNED64, PDO_OUT_ENTRIES | (OBJCODE_ARR << 8)}, asOutputEntryDesc, aName0x7000, &s_outputObj, NULL, NULL, 0x0000}, /* 0x1C13: TxPDO Assign */ {NULL, NULL, 0x1C13, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, asPDOAssignEntryDesc, aName0x1C13, &sTxPDOassign, NULL, NULL, 0x0000}, - /* 0x1A00: TxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ - {NULL, NULL, 0x1A00, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, + /* 0x1A00: TxPDO Mapping (ARR of UINT32, PDO_IN_ENTRIES 个映射条目) */ + {NULL, NULL, 0x1A00, {DEFTYPE_PDOMAPPING, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)}, asPDOMappingEntryDesc, aName0x1A00, &sTxPDOMap, NULL, NULL, 0x0000}, - /* 0x6000: Input Data (从站→主站) */ - {NULL, NULL, 0x6000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, + /* 0x6000: Input Data (从站→主站, ARR of UINT64) */ + {NULL, NULL, 0x6000, {DEFTYPE_UNSIGNED64, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)}, asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000}, /* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */ @@ -234,8 +280,8 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList; *---------------------------------------------------------------------------*/ UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize) { - *pInputSize = 32; /* 4×UINT64 = 32 bytes */ - *pOutputSize = 32; + *pInputSize = ECAT_PDO_IN_BYTES; /* 80×UINT64 = 640 bytes */ + *pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */ return 0; } @@ -250,7 +296,19 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; } *---------------------------------------------------------------------------*/ UINT16 APPL_StartInputHandler(UINT16 *pIntMask) { - *pIntMask = 0x0800U; /* SM3 AL Event */ + /* 1.5.44:此处**不能**覆写 *pIntMask。 + * + * pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好: + * free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090), + * 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。 + * 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的 + * SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位: + * 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来, + * 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式, + * 主站读到未被从站重写的缓冲不累加 WKC,表现为几乎每周期 wkc < expected_wkc。 + * + * 官方 sampleappl.c / el9800appl.c 同样只 return,不碰 pIntMask。 */ + (void)pIntMask; return 0; } @@ -283,7 +341,11 @@ void APPL_InputMapping(UINT16 *pInputData) s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq(); s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec(); - memcpy(pInputData, &s_inputObj.u64Data[0], 32); + /* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储, + * 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站 + * SDO 读 0x6000 时看到与过程数据一致的内容。 */ + EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]); + memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES); } /*--------------------------------------------------------------------------- @@ -291,7 +353,8 @@ void APPL_InputMapping(UINT16 *pInputData) *---------------------------------------------------------------------------*/ void APPL_OutputMapping(UINT16 *pOutputData) { - memcpy(&s_outputObj.u64Data[0], pOutputData, 32); + memcpy(&s_outputObj.u64Data[0], pOutputData, ECAT_PDO_OUT_BYTES); + EcatPdo_ReadOutputs((const UINT8 *)&s_outputObj.u64Data[0]); } /*--------------------------------------------------------------------------- @@ -299,15 +362,12 @@ void APPL_OutputMapping(UINT16 *pOutputData) *---------------------------------------------------------------------------*/ void APPL_Application(void) { - /* Phase 2: 回环测试 — 将输出数据原样回填到输入数据。 - * TwinCAT 主站写 0x7000 → ESC → APPL_OutputMapping → s_outputObj - * APPL_Application: s_inputObj = s_outputObj (回环) - * APPL_InputMapping → ESC → 主站读 0x6000 - * 主站可验证:写入的值 == 读回的值。 */ - s_inputObj.u64Data[0] = s_outputObj.u64Data[0]; - s_inputObj.u64Data[1] = s_outputObj.u64Data[1]; - s_inputObj.u64Data[2] = s_outputObj.u64Data[2]; - s_inputObj.u64Data[3] = s_outputObj.u64Data[3]; + /* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。 + * + * 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显 + * (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N, + * 同样能证明 输出→应用→输入 整条过程数据链路通。 */ + EcatPdo_Process(); } /*--------------------------------------------------------------------------- @@ -322,3 +382,117 @@ UINT32 EcatDbg_ReadAlDiag(void) alStatus = SWAPWORD(alStatus); return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister()); } + +/*--------------------------------------------------------------------------- + * AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展 + * (C 链接, task_runner 1s 节流调用) + * + * 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出 + * 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直 + * 没被置位。该标志只在 PDI_Isr 收到 SM2 事件(AL Event 0x220 bit10)时置位。 + * 01:03 串口日志钉死现场:evt=0x0400 在 0x220 挂起 10s 无人处理(PDI_Isr 的 + * SM2 分支从未执行),escirq 2.6/s 证明 EXTI 只在 AL Control 事件时跳动—— + * 即 PDI_Isr 在跑但 SM2 分支被某个内部旗标挡住(bEscIntEnabled 门控)。 + * + * 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是 + * "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。 + * + * 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位 + * = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位 + * bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。 + * 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个 + * MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/ + * 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。 + *---------------------------------------------------------------------------*/ +UINT32 EcatDbg_GuardAleventMask(void) +{ + /* SSC 全局(ecatslv.c 定义;除 u16ALEventMask 外均已由 ecatslv.h PROTO 声明) */ + extern UINT16 u16ALEventMask; + extern BOOL bMbxRunning; /* mailbox.c 定义(dc_app.c 未包含 mailbox.h,局部声明) */ + + UINT16 evt = 0U; + UINT16 actual = 0U; + UINT16 wdState = 0U; + UINT16 dummy = 0U; + const UINT8 al = (UINT8)(nAlStatus & 0x0FU); + + if (al < 2U /* STATE_PREOP */) { + return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */ + } + + /* (0) 邮箱 SM 地址/长度失配自愈(2026-09-03 03:00 根治,1.5.52): + * + * 主站侧探针(楔死态)实锤的证据链: + * - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应); + * - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零); + * - 网络侧(FPRD)读 SM0/SM1 寄存器 start 恒正确(0x1000/0x1200); + * => SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值, + * 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError, + * 且预清理重枚举(重跑 StartMailboxHandler)重读仍错 → 死锁,只有断电恢复。 + * 自愈:AL>=PREOP 时用寄存器真值(0x0800/0x0808)校验 SSC 缓存的 start/len, + * 失配即写回 + WARN(旧值/新值一并打印,下次串口复现可确凿定位 CSR 读错还是 + * 变量污染——两种成因此写回均有效,写回后下一个邮箱事务即恢复)。 + * 变量仅在 Stop/StartMailboxHandler(INIT<->PREOP 迁移,主循环上下文)被写, + * 守护同样在主循环——无并发风险。 */ + if (al >= 2U && bMbxRunning) { + UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U; + HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET); + HW_EscReadWord(sm0Len, ESC_SYNCMAN_REG_OFFSET + 2U); + HW_EscReadWord(sm1Start, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER); + HW_EscReadWord(sm1Len, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER + 2U); + sm0Start = SWAPWORD(sm0Start); + sm0Len = SWAPWORD(sm0Len); + sm1Start = SWAPWORD(sm1Start); + sm1Len = SWAPWORD(sm1Len); + if (sm0Len >= 8U && sm1Len >= 8U + && (u16EscAddrReceiveMbx != sm0Start || u16EscAddrSendMbx != sm1Start + || u16ReceiveMbxSize != sm0Len || u16SendMbxSize != sm1Len)) { + printf("[ECAT][MBX] WARN: SM addr/len mismatch, SSC rx=0x%04X/%u tx=0x%04X/%u " + "vs reg SM0=0x%04X/%u SM1=0x%04X/%u -> restored\r\n", + (unsigned)u16EscAddrReceiveMbx, (unsigned)u16ReceiveMbxSize, + (unsigned)u16EscAddrSendMbx, (unsigned)u16SendMbxSize, + (unsigned)sm0Start, (unsigned)sm0Len, + (unsigned)sm1Start, (unsigned)sm1Len); + u16EscAddrReceiveMbx = sm0Start; + u16ReceiveMbxSize = sm0Len; + u16EscAddrSendMbx = sm1Start; + u16SendMbxSize = sm1Len; + } + } + + if (al < 4U /* STATE_SAFEOP */ || nPdOutputSize == 0U) { + return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */ + } + + evt = SWAPWORD(HW_GetALEventRegister()); + HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET); + actual = SWAPWORD(actual); + + /* (1) 0x204 缺位自愈 */ + if ((UINT16)(actual & u16ALEventMask) != u16ALEventMask) { + const UINT16 before = actual; + HW_SetALEventMask(u16ALEventMask); + HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET); + actual = SWAPWORD(actual); + printf("[ECAT][AEM] WARN: AL Event Mask lost bits, actual=0x%04X expect=0x%04X -> restored 0x%04X\r\n", + (unsigned)before, (unsigned)u16ALEventMask, (unsigned)actual); + } + + /* (2) SM2 事件楔死自愈:仅 SAFEOP + 标志未置位 + bit10 挂起时触发 + * (OP 态下 LRW 每周期置位/清除 bit10 属正常,不能误报) */ + if (al == 4U /* SAFEOP */ && (evt & 0x0400U) != 0U && bEcatFirstOutputsReceived == FALSE) { + HW_EscReadWord(wdState, ESC_PD_WD_STATE); + wdState = SWAPWORD(wdState); + bEcatFirstOutputsReceived = TRUE; + HW_EscReadWord(dummy, nEscAddrOutputData); + HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U)); + printf("[ECAT][AEM] WARN: SM2 event stuck evt=0x%04X, PDI_Isr path wedged " + "(bEscInt=%u bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) " + "-> flag set + event acked\r\n", + (unsigned)evt, + (unsigned)(bEscIntEnabled ? 1U : 0U), + (unsigned)(bDcSyncActive ? 1U : 0U), + (unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState); + } + return actual; +} diff --git a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h index 507b8bd..01a13bf 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h +++ b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h @@ -143,9 +143,25 @@ #define DC_SUPPORTED 1 #endif -/** 看门狗由定时器中断检查。 */ +/** 看门狗由定时器中断检查。 + * 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件 + * 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者: + * (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不 + * 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP + * code=0x0000"的卡死就是它); + * (b) DC_CheckWatchdog/PLL 计数器(i16WaitForPllRunningCnt)不前进 → 若 ESC + * 残留 DC 激活位(0x0981,上个 DC 会话遗留、跨重连不清),从站按 DC 同步 + * 模式进 SAFEOP→OP,OP 等待 PLL 序列永远不满足 → SAFEOP+0x0000 永久挂起 + * (00:27 复测日志: 等待期轨迹恒 SAFEOP+0x0000,LRW 每 7ms 也救不回)。 + * 改 0 后 MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的 HW_GetTimerValue/ + * HW_ClearTimerValue 本就是为此写的死代码),每 1ms 调一次 ECAT_CheckTimer。 */ #ifndef ECAT_TIMER_INT -#define ECAT_TIMER_INT 1 +#define ECAT_TIMER_INT 0 +#endif + +/** 1.5.47:DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */ +#ifndef ECAT_TIMER_INC_P_MS +#define ECAT_TIMER_INC_P_MS 180000U #endif #ifndef INTERRUPTS_SUPPORTED @@ -572,18 +588,30 @@ /*----------------------------------------------------------------------------------------- ------ ------- PDO Size (需与 DCU_ESI.xml / gen_eeprom.py 的 PDO 尺寸一致: 4×64bit=32B) +------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致) ------ +------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。 +------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B +------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B +------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B, +------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。 +------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。 +------ +------ 这两个宏是**编译期硬门限**,必须先于其它改动生效: +------ - ecatappl.c:157 用它们定义 PD 暂存区 aPdOutputData/aPdInputData +------ - ecatslv.c:509/574 在 SM 校验时拿 SMLength 与之比较,超限直接返回 +------ ALSTATUSCODE_INVALIDSMINCFG/OUTCFG,表现为 PREOP→SAFEOP 失败 +------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。 -----------------------------------------------------------------------------------------*/ -/** 最大输出 PDO 字节数(RxPDO,主站→从站)。 */ +/** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */ #ifndef MAX_PD_OUTPUT_SIZE -#define MAX_PD_OUTPUT_SIZE 0x20 +#define MAX_PD_OUTPUT_SIZE 0x180 #endif -/** 最大输入 PDO 字节数(TxPDO,从站→主站)。 */ +/** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */ #ifndef MAX_PD_INPUT_SIZE -#define MAX_PD_INPUT_SIZE 0x20 +#define MAX_PD_INPUT_SIZE 0x280 #endif /*----------------------------------------------------------------------------------------- diff --git a/GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py b/GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py index f4e6e7d..03b3cb1 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py +++ b/GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py @@ -54,17 +54,44 @@ PRODUCT_CODE = 0x00009252 REVISION_NO = 0x00010000 SERIAL_NO = 0x00000001 -# Mailbox configuration (ESC process RAM: mailbox 0x1000-0x13FF, PDO 0x1400-0x14BF) +# Mailbox configuration (ESC process RAM: mailbox 0x1000-0x13FF, PDO 0x1400-0x1FFF) MBX_RX_OFFSET = 0x1000 # SM0: Master -> Slave (receive mailbox) MBX_RX_SIZE = 256 MBX_TX_OFFSET = 0x1200 # SM1: Slave -> Master (send mailbox) MBX_TX_SIZE = 256 # Process Data configuration +# +# 1.5.56 扩到 DPRAM 硬上限 64/384 -> 384/640(需要一周期合并多条 CANFD 帧)。 +# 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致 +# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。 +# +# ★ 为何 out+in 最大只能是 1024 ★ +# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。 +# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size: +# SM2 384B -> 0x1400-0x187F (384×3 = 1152B) +# SM3 640B -> 0x1880-0x1FFF (640×3 = 1920B) ← 恰好用满最后一字节 +# (384+640)×3 = 3072 = 过程数据区全长,一字节不剩也一字节不越界。 +# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在 +# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。 +# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见 +# comm/ethercat_pdo_size.h 的完整推导。 PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs) -PD_OUT_SIZE = 32 -PD_IN_OFFSET = 0x1480 # SM3: TxPDO (Slave -> Master inputs) -PD_IN_SIZE = 32 +PD_OUT_SIZE = 384 # 48×64bit +PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs) +PD_IN_SIZE = 640 # 80×64bit + +# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住, +# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。 +_PD_BUFFERS = 3 +assert PD_OUT_OFFSET >= MBX_TX_OFFSET + MBX_TX_SIZE, \ + "过程数据区起点压到了邮箱上(SSC ecatslv.c:708 会报 INVALIDSMINCFG)" +assert PD_OUT_OFFSET + PD_OUT_SIZE * _PD_BUFFERS <= PD_IN_OFFSET, \ + "SM2 的 3 个缓冲会盖住 SM3,请把 PD_IN_OFFSET 往后挪" +assert PD_IN_OFFSET + PD_IN_SIZE * _PD_BUFFERS - 1 <= 0x1FFF, \ + "SM3 超出 LAN9252 4KB DPRAM (0x1FFF)" +assert PD_OUT_SIZE % 8 == 0 and PD_IN_SIZE % 8 == 0, \ + "PDO 尺寸必须是 8 的整数倍(映射条目是 64bit)" # PDI type: LAN9252 HBI 16-bit indexed mode (DS Table 14-2: 0x8C=HBI 8-bit, # 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI. @@ -214,17 +241,24 @@ for start, length, control, enable, sm_type in sm_entries: sm_data.append(sm_type) append_category(41, bytes(sm_data)) -def append_pdo(cat_type, pdo_index, sm_index, obj_index): +def append_pdo(cat_type, pdo_index, sm_index, obj_index, pd_size): """Table 24/25: header [index(2)][nEntry(1)][SyncM(1)][Sync(1)][NameIdx(1)] - [Flags(2)] + entries [idx(2)][sub(1)][name(1)][dtype(1)][bitlen(1)][flags(2)].""" + [Flags(2)] + entries [idx(2)][sub(1)][name(1)][dtype(1)][bitlen(1)][flags(2)]. + + nEntry 由过程数据长度推导(每条目 UNSIGNED64 = 8B),须与 dc_app.c 的 + PDO_OUT_ENTRIES / PDO_IN_ENTRIES 一致;写死 4 会在扩容后让主站按 32B 建映射, + 与从站 APPL_GenerateMapping 报的长度不符,SAFEOP 阶段报 AL 0x001A/0x0011。""" + n_entry = pd_size // 8 + if not 1 <= n_entry <= 255: + raise SystemExit(f"PDO 条目数 {n_entry} 超出 SII 单字节 nEntry 范围") data = bytearray() data.extend(struct.pack(' - omitted on purpose. diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 45b275b..0bfc663 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -120,7 +120,45 @@ 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 = 43U; /* 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_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:根治"反复启停后从站邮箱楔死"(主站侧任何手段都救不回、断电才恢复的硬楔死)——从站固件守护自愈。 + 03:00 主站侧探针钉死完整证据链(楔死态、未断电): + (1) ESC/SSC 邮箱处理全程活着:手动注入合法 CoE 请求(写满 SM0 256B)后 200ms 内 SM1 出响应; + (2) 但响应帧头出现在 SM1 start+8 而非 start(逐会话累积:第一轮 +4、后续 +8),前 8 字节全零; + (3) 网络侧(FPRD)读 SM0/SM1 寄存器恒正确(start=0x1000/0x1200,ctl=0x26/0x22,act=01); + => SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan→CSR 间接读)拿到了错值,响应写到错位地址, + 主站按协议从 0x1200 读永远对不上帧头 → SOEM 报 WkcError/MailboxError;且预清理重枚举 + (重跑 StartMailboxHandler 重读)仍错 → 死锁,断电才清零。错位逐会话 +4(DWORD 粒度)指向 + HBI 访问层状态残留(CSR 间接读或 PRAM 引擎,成因待串口日志确凿——两种成因下述自愈均有效)。 + 修复:EcatDbg_GuardAleventMask 扩展自愈(0):AL>=PREOP 且 bMbxRunning 时,读 SM 寄存器真值 + (0x0800/0x0808 的 start/len)与 SSC 缓存(u16EscAddrReceiveMbx/SendMbx/Receive/SendMbxSize)比对, + 失配即写回+WARN(旧值/寄存器值一并打印——下次串口复现一行即区分"CSR 读错"还是"变量污染")。 + 变量仅在 Stop/StartMailboxHandler(主循环上下文)被写,守护同上下文无并发。 + 守护周期 1s→250ms(task_runner ethercatAleMaskGuard,节流源 g_app_uptime_sec→g_scheduler.tick_1ms): + 自愈必须快过主站 SDO 超时(~700ms),否则永远落在主站放弃之后形同虚设。 + 邮箱自愈+主站侧预清理(ecat_server.py 1.5.51)+自动重试三层叠加:错位发生 → 250ms 内写回 → + 主站重试成功,彻底消除"断电才恢复"。 + 主站工具同步(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 仅为变更记录与编译纪律)。 + 02:05 复现脚本(faulthandler)钉死闪退根因:同一连接内反复 op/start↔op/stop 不重枚举时,从站邮箱逐轮退化 + (第 2 轮起 OP 态 SDO 读 0x2000 全 WkcError——即用户日志"报个读取失败"),第 4 轮 config_map() 内 SOEM CoE + 分段读(ethercatcoe.c ecx_SDOread 的 memcpy 无边界检查,上游缺陷)吃到楔死邮箱的错位帧 → 原生访问违例 + (0xC0000005) → 服务子进程闪退、GUI 连带退出——与用户 01:40 日志逐行吻合(01:40:30"进入 OP"后 + 无任何输出=进程死在 config_map,响应未及回传,"卡住"即 SOEM 分段循环死循环)。 + 修复:_require_op_ready 入口无条件"回 INIT|ACK + config_init 重枚举"预清理(等效实测恒有效的 + "断开重连",单从站成本 ~100ms),api_op_start 自动重试简化为直接重跑;另修 ecat_server.py 第 99 行 + 上一轮编辑粘连("g_op_active = Falseg_httpd = None" 被 Python 当链式赋值,g_httpd 全局缺失、 + g_op_active 初值错成 None,全靠 falsy 侥幸工作);config_map 失败分支新增邮箱 SM 寄存器现场转储 + (0x0810/0x0818 各 8B,start/len/ctl/status/activate/pdi)。 + 02:12 探针进一步定性(楔死态、未断电):AL 状态机活着(PREOP/SAFEOP 都 ack)但 SSC 完全不处理邮箱—— + SM0/SM1 activate/StartAddr 异常(INIT 态读回全零、PREOP 态读回 SII 值 ctl=0x64/0x20 act=0)、 + config_map 跑完会重写全部 SM(SOI 使能)但 SDO 依然全败、close+open 也救不回、 + SOEM 读到的"错误帧" Detail=0x1300/0x2300/0x3300 随会话递增(=邮箱计数器字节模式,读回的是错位帧)。 + 结论:从站侧楔死为固件层问题(疑似 PDI 通路/AL Event Mask 0x204 实测=0x03F3 或 SSC 邮箱处理挂起), + 主站任何手段(含断开重连)都救不回,必须断电重启——从站根因待串口抓取(下次复现时串口 + 看主循环 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 激活后自动停跑 */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index ee6d648..2752046 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -17,6 +17,7 @@ #include "sys/task_runner.h" #include "comm/ethercat_slave.h" #include "comm/ethercat_sync.h" +#include "comm/ethercat_can_bridge.h" #include "comm/esc_exmc_bus.h" extern "C" { @@ -233,6 +234,13 @@ void TaskRunner::taskCanRxParse() s_runtime.last_rx_data, s_runtime.last_rx_len); #endif + /* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧 + * (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响—— + * 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */ + EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel, + s_runtime.last_rx_id, + s_runtime.last_rx_data, + s_runtime.last_rx_len); consumed++; } @@ -273,6 +281,33 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out) /* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */ extern "C" uint32_t EcatDbg_ReadAlDiag(void); +/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */ +extern "C" uint32_t EcatDbg_GuardAleventMask(void); + +/** + * @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱 + * SM 地址/长度失配自愈:250ms 周期。 + * @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 + * bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)—— + * PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位 + * 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标, + * 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。 + * 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧 + * u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时 + * (~700ms),否则自愈永远落在主站放弃之后,形同虚设。 + */ +static void ethercatAleMaskGuard() +{ + static uint32_t s_lastGuardMs = 0U; + if (!EthercatSync::isActive()) { + return; + } + const uint32_t nowMs = g_scheduler.tick_1ms; + if ((uint32_t)(nowMs - s_lastGuardMs) >= 250U) { + s_lastGuardMs = nowMs; + EcatDbg_GuardAleventMask(); + } +} /** * @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。 @@ -547,6 +582,11 @@ void TaskRunner::runPending() * DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */ MainLoop(); + /* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。 + * 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率, + * OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */ + ethercatAleMaskGuard(); + const uint32_t pending = TaskManager::takePending(); if(pending == 0U) return;