From b171767023e786c22339bf28238071f909779ff6 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 29 Jun 2026 01:51:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(ethercat):=20=E6=B7=BB=E5=8A=A0EtherCAT?= =?UTF-8?q?=E4=BB=8E=E7=AB=99=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=8C=E6=AD=A5=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加CAN反馈和ID快照SPI命令文件 - 移除EXMC组件依赖,精简项目配置 - 添加RAM函数段链接脚本,优化Flash等待周期抖动 - 实现EscExmcBus类,提供16位并行总线访问LAN9252 - 实现EscLan9252驱动,支持寄存器和PDI内存访问 - 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换 - 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式 - 添加ISR执行时间统计和诊断功能 - 分离周期路径和事件路径通知机制,避免RMW竞态 --- .../checklist.md | 83 +++++ .../fix-ecat-pdo-and-runtime-issues/spec.md | 177 +++++++++ .../fix-ecat-pdo-and-runtime-issues/tasks.md | 68 ++++ .../fix-ecat-postaudit-risks/checklist.md | 51 +++ .trae/specs/fix-ecat-postaudit-risks/spec.md | 109 ++++++ .trae/specs/fix-ecat-postaudit-risks/tasks.md | 35 ++ .../checklist.md | 70 ++++ .../fix-ethercat-realtime-comm-risks/spec.md | 147 ++++++++ .../fix-ethercat-realtime-comm-risks/tasks.md | 59 +++ .../GD32E508_DCU_APP.cproject.yml | 5 +- .../GD32E508VE/ac6_linker_script.sct.src | 9 +- GD32E508_DCU_APP/comm/esc_exmc_bus.cpp | 22 +- GD32E508_DCU_APP/comm/esc_exmc_bus.h | 28 +- GD32E508_DCU_APP/comm/esc_lan9252.cpp | 78 +++- GD32E508_DCU_APP/comm/esc_lan9252.h | 46 +-- GD32E508_DCU_APP/comm/ethercat_slave.cpp | 141 ++++++-- GD32E508_DCU_APP/comm/ethercat_slave.h | 103 +++++- GD32E508_DCU_APP/comm/ethercat_sync.cpp | 27 +- GD32E508_DCU_APP/device/CANFD.cpp | 175 ++++++++- GD32E508_DCU_APP/device/Timer.cpp | 3 +- GD32E508_DCU_APP/device/canfd.h | 263 +++++++------- .../device/spi/app_spi_cmd_can_feedback.cpp | 206 +++++++++++ .../device/spi/app_spi_cmd_id_snapshot.cpp | 135 +++++++ .../device/spi/app_spi_command.cpp | 6 +- GD32E508_DCU_APP/device/spi/app_spi_command.h | 2 +- .../device/spi/app_spi_command_defs.h | 3 + GD32E508_DCU_APP/device/usart0.h | 2 +- GD32E508_DCU_APP/sys/config.h | 23 +- GD32E508_DCU_APP/sys/initial.cpp | 16 +- GD32E508_DCU_APP/sys/task_runner.cpp | 37 +- communication_ids.html | 338 ++++++++++++++++++ 31 files changed, 2177 insertions(+), 290 deletions(-) create mode 100644 .trae/specs/fix-ecat-pdo-and-runtime-issues/checklist.md create mode 100644 .trae/specs/fix-ecat-pdo-and-runtime-issues/spec.md create mode 100644 .trae/specs/fix-ecat-pdo-and-runtime-issues/tasks.md create mode 100644 .trae/specs/fix-ecat-postaudit-risks/checklist.md create mode 100644 .trae/specs/fix-ecat-postaudit-risks/spec.md create mode 100644 .trae/specs/fix-ecat-postaudit-risks/tasks.md create mode 100644 .trae/specs/fix-ethercat-realtime-comm-risks/checklist.md create mode 100644 .trae/specs/fix-ethercat-realtime-comm-risks/spec.md create mode 100644 .trae/specs/fix-ethercat-realtime-comm-risks/tasks.md create mode 100644 GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp create mode 100644 GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp create mode 100644 communication_ids.html diff --git a/.trae/specs/fix-ecat-pdo-and-runtime-issues/checklist.md b/.trae/specs/fix-ecat-pdo-and-runtime-issues/checklist.md new file mode 100644 index 0000000..dc8ab10 --- /dev/null +++ b/.trae/specs/fix-ecat-pdo-and-runtime-issues/checklist.md @@ -0,0 +1,83 @@ +# Verification Checklist + +## 阶段 S1:输入/输出 PDO 的 PDI 偏移分离(严重 S1) +- [x] `sys/config.h` 中新增 `CONFIG_ESC_PDI_INPUT_OFFSET` 与 `CONFIG_ESC_PDI_OUTPUT_OFFSET` 两个常量,且数值不同 +- [x] `comm/ethercat_slave.cpp::periodicIsr()` 中 `readPdi()` 调用使用 `CONFIG_ESC_PDI_INPUT_OFFSET` +- [x] `comm/ethercat_slave.cpp::periodicIsr()` 中 `writePdi()` 调用使用 `CONFIG_ESC_PDI_OUTPUT_OFFSET` +- [x] `periodicIsr()` 中 `readPdi` 与 `writePdi` 的 offset 参数不再共用 `0x0000U` +- [x] `comm/esc_lan9252.h` 中 PDI 接口注释说明 offset 语义为"SyncManager 内存区相对偏移,输入/输出必须分离" + +## 阶段 S2:process() 框架级 PDO 桥接(严重 S2) +- [x] `comm/ethercat_slave.h` 中 `EthercatSlave` 类含 `s_appInputs` 与 `s_appOutputs` 私有静态成员声明 +- [x] `comm/ethercat_slave.h` 中 `EthercatSlave` 类 public 区含 `getApplicationInputs()` 与 `setApplicationOutputs(const PdoBuffer*)` 声明 +- [x] `comm/ethercat_slave.cpp` 中 `s_appInputs` / `s_appOutputs` 已定义并初始化 +- [x] `process()` 实现内调用 `getLatestInputs()` 拷贝到 `s_appInputs` +- [x] `process()` 实现内调用 `setOutputs(&s_appOutputs)` 推入输出三缓冲 +- [x] `getApplicationInputs()` 返回 `&s_appInputs`(只读指针) +- [x] `setApplicationOutputs()` 含非空检查并将入参拷贝到 `s_appOutputs` +- [x] `process()` 注释保留 TODO 说明应用对象字典映射待后续 spec + +## 阶段 S3:ESC 就绪状态含 AL 状态守卫(严重 S3,仅守卫) +- [x] `comm/ethercat_slave.h` 含 `s_alState` 私有静态成员声明与 `getAlState()` 公共接口声明 +- [x] `comm/ethercat_slave.h` 含 AL 状态码常量定义(至少含 `AL_STATE_SAFEOP=0x04`) +- [x] `comm/ethercat_slave.cpp` 中 `s_alState` 已定义并初始化 +- [x] `EthercatSlave::init()` 在芯片 ID 匹配后读取 AL 状态寄存器(0x0130)保存到 `s_alState` +- [x] `isEscReady()` 返回值包含 AL 状态判断(`s_alState >= AL_STATE_SAFEOP`) +- [x] `isEscReady()` 在芯片 ID 不匹配时短路返回 false(不读 AL 状态) +- [x] `EthercatSlave::init()` 诊断打印区分三种情况:ID 不匹配、ID 匹配但 AL < SAFEOP、ID 匹配且 AL ≥ SAFEOP +- [x] `getAlState()` 返回 `s_alState` +- [x] `comm/esc_lan9252.cpp::EscLan9252::init()` 含完整 TODO 注释块,列出 SyncManager/FMMU/AL Control/SYNC0 配置步骤,并说明需 SSC 集成 + +## 阶段 M4:TIMER0 降级模式保留 ESC 事件中断(中危 M4) +- [x] `comm/ethercat_sync.cpp` 的 `#else`(TIMER0 降级)分支含 `EXTI1_IRQHandler` 定义 +- [x] `comm/ethercat_sync.cpp` 的 `#else` 分支含 `EXTI2_IRQHandler` 定义 +- [x] `EthercatSync::init()` 的 `#else` 分支含 EXTI1 的 `gpio_exti_source_select`/`exti_init`/`exti_interrupt_enable`/`exti_interrupt_flag_clear`/`nvic_irq_enable` 配置 +- [x] `EthercatSync::init()` 的 `#else` 分支含 EXTI2 的对应配置 +- [x] `#else` 分支不含 `EXTI0_IRQHandler` 定义(被 `TIMER0_IRQHandler` 替代) +- [x] `onEscIrqExti` 与 `onSync1Exti` 实现未改动 + +## 阶段 M5:输出三缓冲写入索引提供 2 周期宽限(中危 M5) +- [x] `comm/ethercat_slave.cpp::setOutputs()` 中 `write_idx` 表达式为 `(s_outputReadIdx + 2U) % 3U` +- [x] `setOutputs()` 内注释无"无需临界区"表述 +- [x] `setOutputs()` 内注释含"2 周期宽限"与"前提是主循环处理时间 < 2 × 周期时间"说明 +- [x] `comm/ethercat_slave.h` 中 `s_outputs` 成员注释说明写入索引为 `(readIdx+2)%3` 与 2 周期宽限语义 +- [x] `periodicIsr()` 中 `s_outputReadIdx` 递增逻辑未改动(仍为 `(readIdx + 1) % 3`) + +## 阶段 M6:`safe*` 接口重命名为 `initOnly*`(中危 M6) +- [x] `comm/esc_lan9252.h` 中无 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 声明 +- [x] `comm/esc_lan9252.h` 中含 `initOnlyReadReg`/`initOnlyWriteReg`/`initOnlyReadPdi`/`initOnlyWritePdi` 声明 +- [x] `comm/esc_lan9252.cpp` 中无 `safe*` 函数定义 +- [x] `comm/esc_lan9252.cpp` 中含 `initOnly*` 函数定义 +- [x] `comm/esc_lan9252.h` 中 `initOnly*` 接口注释含"INIT-ONLY"字样,无"safe"字样 +- [x] `comm/ethercat_slave.cpp::EthercatSlave::init()` 中 AL 状态读取调用 `initOnlyReadReg`(非 `safeReadReg`) +- [x] `comm/esc_lan9252.cpp::readChipId()` 中调用 `initOnlyReadReg`(非 `safeReadReg`) +- [x] 全项目 grep `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi`,无任何匹配(含注释) + +## 阶段 M7:`s_periodicSeq` 加宽为 `uint16_t`(中危 M7) +- [x] `comm/ethercat_slave.h` 中 `s_periodicSeq` 类型为 `volatile uint16_t` +- [x] `comm/ethercat_slave.h` 中 `s_processedSeq` 类型为 `uint16_t` +- [x] `comm/ethercat_slave.cpp` 中 `s_periodicSeq` 定义类型为 `volatile uint16_t` +- [x] `comm/ethercat_slave.cpp` 中 `s_processedSeq` 定义类型为 `uint16_t` +- [x] `hasPendingPeriodic()` 实现未改动(仍为 `s_periodicSeq != s_processedSeq`) +- [x] `markPeriodicProcessed()` 实现未改动(仍为 `s_processedSeq = s_periodicSeq`) + +## 编译验证与一致性检查 +- [x] 静态审查:全项目 grep `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi`,确认无残留 +- [x] 静态审查:`periodicIsr()` 中 `readPdi`/`writePdi` 使用不同 offset 常量 +- [x] 静态审查:`setOutputs()` 使用 `(readIdx+2)%3` +- [x] 静态审查:`s_periodicSeq` 与 `s_processedSeq` 均为 `uint16_t` +- [x] 静态审查:降级模式下 `EXTI1_IRQHandler`/`EXTI2_IRQHandler` 已定义 +- [x] 静态审查:`process()` 内调用 `getLatestInputs()` 与 `setOutputs()` +- [x] 静态审查:`getApplicationInputs()`/`setApplicationOutputs()` 已实现 +- [x] 静态审查:`isEscReady()` 含 AL 状态守卫 +- [ ] 全量编译通过,无 error(⚠️ 待编译环境修复 EXMC pack 阻塞后验证) +- [ ] 若可编译:map 文件确认新增静态成员 `s_appInputs`/`s_appOutputs`/`s_alState` 已分配(⚠️ 待编译环境修复后验证) + +## 风险闭环确认 +- [x] 严重 S1(PDO 偏移重叠):输入/输出 PDO 使用独立 offset,数据不再相互覆盖,闭环 +- [x] 严重 S2(process 为空):process() 框架级桥接实现,数据通路在框架层闭合,闭环 +- [x] 严重 S3(ESC 寄存器初始化缺失):isEscReady() 增加 AL 状态守卫,SYNC0 不会触发的事实在运行期可观测;完整 ESC 配置留 SSC 集成,本 spec 范围内闭环 +- [x] 中危 M4(降级模式丢失 ESC IRQ):EXTI1/EXTI2 handler 在降级模式下保留,ESC 事件路径可用,闭环 +- [x] 中危 M5(输出三缓冲竞态):写入索引改为 `(readIdx+2)%3`,2 周期宽限消除拷贝竞态,闭环 +- [x] 中危 M6(safe* 命名误导):重命名为 `initOnly*`,命名与实现语义一致,闭环 +- [x] 中危 M7(序列号回绕):加宽为 `uint16_t`,6.55s 回绕周期覆盖任何合理主循环阻塞,闭环 diff --git a/.trae/specs/fix-ecat-pdo-and-runtime-issues/spec.md b/.trae/specs/fix-ecat-pdo-and-runtime-issues/spec.md new file mode 100644 index 0000000..26d0bc7 --- /dev/null +++ b/.trae/specs/fix-ecat-pdo-and-runtime-issues/spec.md @@ -0,0 +1,177 @@ +# EtherCAT PDO 数据通路与运行期问题整改 Spec + +## Why +在 `fix-ethercat-realtime-comm-risks` 与 `fix-ecat-postaudit-risks` 两轮整改完成后,对 EtherCAT 通信架构进行了第三轮深度审查,发现 3 个严重问题(P0/P1)与 4 个中等问题(P1/P2)。其中 S1(输入/输出 PDO 使用同一 PDI 偏移)直接导致读写数据相互覆盖;S2(`process()` 为空)使三缓冲数据通路在应用层断开;M4(TIMER0 降级模式丢失 ESC IRQ handler)、M5(输出三缓冲存在 1 周期竞态窗口)、M6(`safe*` 命名误导)、M7(`s_periodicSeq` 单字节回绕)虽不立即致命,但会在边界条件下静默丢数据或诱导误用。本 spec 在不推翻前两轮架构决策的前提下,闭合 PDO 数据通路、修正降级模式分支、消除剩余竞态与命名陷阱。 + +## What Changes + +### 阶段 S1:分离输入/输出 PDO 的 PDI 偏移(修复严重 S1) +- 在 `sys/config.h` 新增 `CONFIG_ESC_PDI_INPUT_OFFSET` 与 `CONFIG_ESC_PDI_OUTPUT_OFFSET` 两个偏移常量(默认值分别取 LAN9252 典型 TxPDO/RxPDO SyncManager 内存区起始,需与主站 ESI 配置匹配) +- 修改 `comm/ethercat_slave.cpp::periodicIsr()`:`readPdi()` 调用使用 `CONFIG_ESC_PDI_INPUT_OFFSET`,`writePdi()` 调用使用 `CONFIG_ESC_PDI_OUTPUT_OFFSET`,不再共用 `0x0000U` +- 修改 `comm/esc_lan9252.h` 中 `readPdi`/`writePdi`/`safeReadPdi`/`safeWritePdi` 的接口注释,说明 offset 参数语义为"SyncManager 内存区相对偏移,输入/输出必须分离" +- **范围限定**:仅修正调用方传入的 offset,不实现 SyncManager/FMMU 寄存器配置(属 SSC 集成范畴,见阶段 S3) + +### 阶段 S2:实现 `process()` 框架级 PDO 桥接(修复严重 S2) +- **BREAKING(接口新增)**: `EthercatSlave` 类新增 `getApplicationInputs()` 与 `setApplicationOutputs()` 两个公共接口,作为应用层访问 PDO 数据的唯一入口 +- `process()` 内部实现: + 1. 调用 `getLatestInputs()` 取最新输入三缓冲只读指针,拷贝到内部 `s_appInputs` 快照(单次 32 字节拷贝,主循环非实时路径) + 2. 将内部 `s_appOutputs` 快照通过 `setOutputs()` 推入输出三缓冲 + 3. 应用层通过 `getApplicationInputs()` 读取输入快照,通过 `setApplicationOutputs()` 写入下次要发送的输出快照 +- `s_appInputs` / `s_appOutputs` 为 `PdoBuffer` 类型静态成员,仅主循环访问,无需三缓冲(应用层与 `process()` 同在主循环,串行执行) +- **不实现**:应用对象字典映射(具体输入字节对应哪个应用变量),留 TODO 注释,由后续应用层 spec 定义 + +### 阶段 S3:ESC 就绪状态守卫增强(修复严重 S3,仅守卫不实现配置) +- 修改 `EthercatSlave::isEscReady()` 语义:除校验芯片 ID 外,额外校验 ESC AL 状态寄存器(0x0130)的 AL 状态字段 ≥ `AL_STATE_SAFEOP`(0x04),否则返回 false +- 新增 `EthercatSlave::getAlState()` 公共接口,返回当前 AL 状态码(供诊断打印与未来 SSC 集成使用) +- 在 `EscLan9252::init()` 中增加清晰 TODO 注释块,列出完整 ESC 配置所需步骤(SyncManager 初始化、FMMU 配置、AL Control 写入、SYNC0 生成寄存器配置),并说明这些步骤需要 SSC 或等价协议栈代码,本 spec 不实现 +- `EthercatSlave::init()` 中读取 AL 状态并打印;若 AL 状态 < SAFEOP,明确打印"ESC detected but AL state < SAFEOP, SYNC0 will not fire, full ESC config requires SSC integration" +- **范围限定**:不实现任何 ESC 寄存器配置逻辑,仅增加守卫与诊断,使"SYNC0 不会触发"这一事实在运行期可观测,而非静默失败 + +### 阶段 M4:TIMER0 降级模式保留 ESC IRQ handler(修复中危 M4) +- 修改 `comm/ethercat_sync.cpp` 中断入口包装:`#else`(TIMER0 降级)分支 SHALL 同时保留 `EXTI1_IRQHandler` 与 `EXTI2_IRQHandler` 的定义,仅 `EXTI0_IRQHandler` 被 `TIMER0_IRQHandler` 替代 +- `EthercatSync::init()` 的 TIMER0 分支 SHALL 同时配置 EXTI1(ESC IRQ)与 EXTI2(SYNC1)的 NVIC 使能,使降级模式下 ESC 事件路径仍可用(DC 未同步时主站仍需通过邮箱/AL 事件推进状态机) +- **范围限定**:EXTI1/EXTI2 的 ISR 实现不改(仍调用 `onEscIrqExti`/`onSync1Exti`),仅修复编译分支漏定义 + +### 阶段 M5:输出三缓冲写入索引改为 `(readIdx+2)%3`(修复中危 M5) +- 修改 `comm/ethercat_slave.cpp::setOutputs()`:`write_idx` 由 `(s_outputReadIdx + 1U) % 3U` 改为 `(s_outputReadIdx + 2U) % 3U` +- 修改 `comm/ethercat_slave.h` 中 `s_outputs` 的注释,说明写入索引与 ISR 读取位置隔 2 个缓冲(即 ISR 2 周期内不会读到该槽),提供 2 周期宽限,消除结构体拷贝被两次抢占的竞态窗口 +- 修改 `setOutputs()` 实现内注释,删除"无需临界区"的绝对化表述,改为"2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间" +- **影响验证**:三缓冲仍为 3 个槽,索引方案变更不影响 ISR 读取路径(ISR 仍按 `s_outputReadIdx` 模 3 递增读取),仅主循环写入位置后移一格 + +### 阶段 M6:`safe*` 接口重命名为 `initOnly*`(修复中危 M6) +- **BREAKING(接口重命名)**: `EscLan9252` 类的 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 四个接口重命名为 `initOnlyReadReg`/`initOnlyWriteReg`/`initOnlyReadPdi`/`initOnlyWritePdi` +- 理由:现实现无任何保护(直接转发无锁版本),`safe*` 命名暗示线程安全,实际安全性完全靠开发者自律。重命名为 `initOnly*` 使"仅 init 阶段可调用"的约束直接体现在符号名上,降低误用概率 +- 全项目 grep 更新所有调用点(预期仅在 `EthercatSlave::init()` 中调用 `safeReadReg` 读取 AL 状态) +- 头文件注释同步更新,移除"safe"字样,统一为"INIT-ONLY" + +### 阶段 M7:`s_periodicSeq` 加宽为 `uint16_t`(修复中危 M7) +- 修改 `comm/ethercat_slave.h`:`s_periodicSeq` 与 `s_processedSeq` 类型由 `uint8_t` 改为 `uint16_t` +- 理由:100µs 周期下 `uint8_t` 在 25.5ms 后回绕,主循环因长任务(如 CAN 帧解析预算 48 帧、OTA 处理)阻塞超过 25.5ms 时,`hasPendingPeriodic()` 会因回绕后相等而静默丢检测。`uint16_t` 提供 6.55s 回绕周期,足够覆盖任何合理主循环阻塞 +- **范围限定**:不加宽到 `uint32_t`,因 `uint16_t` 在 ARM Cortex-M4 上仍为单字访问原子,无需额外同步原语;`uint32_t` 在某些访问序列下非单周期原子(虽然 GD32E508 32-bit 总线实际原子,但 `uint16_t` 足够且更保守) +- 诊断打印中 `s_isrCount` 已为 `uint32_t` 不受影响 + +## Impact +- Affected specs: + - `fix-ethercat-realtime-comm-risks`(上一轮整改 spec):本 spec 修正其阶段 C(三缓冲)中输出索引 `(readIdx+1)%3` 的决策,改为 `(readIdx+2)%3`;修正其阶段 B(EXMC 访问权分离)中 `safe*` 命名,改为 `initOnly*` + - `fix-ecat-postaudit-risks`(上轮整改 spec):本 spec 修正其阶段 H1(safe* 收窄 INIT-ONLY)的命名,由 `safe*` 改为 `initOnly*`,使语义与命名一致 + - `add-ethercat-lan9252-realtime`(原集成 spec):无架构推翻 +- Affected code: + - `sys/config.h`:新增 `CONFIG_ESC_PDI_INPUT_OFFSET` / `CONFIG_ESC_PDI_OUTPUT_OFFSET` + - `comm/ethercat_slave.h` / `comm/ethercat_slave.cpp`:PDO 偏移分离、`process()` 桥接实现、`isEscReady()` AL 状态守卫、`getAlState()` 新增、应用层 getter/setter 新增、`s_periodicSeq`/`s_processedSeq` 加宽、`setOutputs()` 索引改为 `+2`、`s_appInputs`/`s_appOutputs` 新增 + - `comm/esc_lan9252.h` / `comm/esc_lan9252.cpp`:`safe*` 重命名为 `initOnly*`、`init()` 增加 ESC 配置 TODO 注释块、PDI 接口 offset 语义注释 + - `comm/ethercat_sync.cpp`:TIMER0 降级分支补全 `EXTI1_IRQHandler`/`EXTI2_IRQHandler` 与 NVIC 使能 + +## ADDED Requirements + +### Requirement: 输入/输出 PDO 的 PDI 偏移分离 +系统 SHALL 在 `sys/config.h` 中定义 `CONFIG_ESC_PDI_INPUT_OFFSET` 与 `CONFIG_ESC_PDI_OUTPUT_OFFSET` 两个独立偏移常量,分别对应 LAN9252 TxPDO 与 RxPDO SyncManager 内存区起始。`EthercatSlave::periodicIsr()` 中的 `readPdi()` 调用 SHALL 使用输入偏移,`writePdi()` 调用 SHALL 使用输出偏移,SHALL NOT 共用同一偏移。 + +#### Scenario: 输入 PDO 读取使用输入偏移 +- **WHEN** `periodicIsr()` 读取输入 PDO +- **THEN** 调用 `readPdi(CONFIG_ESC_PDI_INPUT_OFFSET, ...)`,不与输出 PDO 写入偏移重叠 + +#### Scenario: 输出 PDO 写入使用输出偏移 +- **WHEN** `periodicIsr()` 写入输出 PDO +- **THEN** 调用 `writePdi(CONFIG_ESC_PDI_OUTPUT_OFFSET, ...)`,不与输入 PDO 读取偏移重叠 + +### Requirement: 应用层 PDO 桥接接口 +系统 SHALL 在 `EthercatSlave` 类中提供 `getApplicationInputs()` 与 `setApplicationOutputs()` 两个公共接口,作为应用层访问 PDO 数据的唯一入口。`process()` SHALL 在主循环调用时:先调用 `getLatestInputs()` 拷贝最新输入到内部 `s_appInputs` 快照,再将内部 `s_appOutputs` 快照通过 `setOutputs()` 推入输出三缓冲。 + +应用层 SHALL 通过 `getApplicationInputs()` 读取输入快照(只读指针),通过 `setApplicationOutputs()` 写入下次要发送的输出快照(拷贝到内部 `s_appOutputs`)。`s_appInputs` 与 `s_appOutputs` 仅由主循环访问(`process()` 与应用层 getter/setter 串行执行),无需三缓冲。 + +#### Scenario: process 闭合数据通路 +- **WHEN** 主循环 `taskEthercatProcess()` 调用 `EthercatSlave::process()` +- **THEN** `process()` 内部调用 `getLatestInputs()` 取最新输入,调用 `setOutputs()` 推入待发送输出,数据通路在框架层闭合 + +#### Scenario: 应用层读取输入 +- **WHEN** 应用层调用 `EthercatSlave::getApplicationInputs()` +- **THEN** 返回 `s_appInputs` 的只读指针,内容为最近一次 `process()` 拷贝的输入快照 + +#### Scenario: 应用层写入输出 +- **WHEN** 应用层调用 `EthercatSlave::setApplicationOutputs(buf)` +- **THEN** `buf` 内容被拷贝到 `s_appOutputs`,下次 `process()` 调用时通过 `setOutputs()` 推入三缓冲 + +### Requirement: ESC 就绪状态含 AL 状态守卫 +系统 SHALL 使 `EthercatSlave::isEscReady()` 在芯片 ID 校验通过的基础上,额外校验 ESC AL 状态寄存器(0x0130)的 AL 状态字段 ≥ `AL_STATE_SAFEOP`(0x04)。AL 状态 < SAFEOP 时,`isEscReady()` SHALL 返回 false,即使芯片 ID 匹配。 + +系统 SHALL 提供 `EthercatSlave::getAlState()` 公共接口,返回当前 AL 状态码,供诊断打印与未来 SSC 集成使用。 + +`EscLan9252::init()` SHALL 在实现中保留清晰 TODO 注释块,列出完整 ESC 配置所需步骤(SyncManager、FMMU、AL Control、SYNC0 生成寄存器),并说明这些步骤需要 SSC 或等价协议栈代码,本 spec 不实现。 + +#### Scenario: 芯片 ID 匹配但 AL 状态不足 +- **WHEN** `EthercatSlave::init()` 读取芯片 ID 匹配 0x87654321,但 AL 状态为 PREOP(0x02) +- **THEN** `isEscReady()` 返回 false,诊断打印输出"ESC detected but AL state < SAFEOP, SYNC0 will not fire" + +#### Scenario: 芯片 ID 匹配且 AL 状态充分 +- **WHEN** 芯片 ID 匹配且 AL 状态 ≥ SAFEOP(0x04) +- **THEN** `isEscReady()` 返回 true,EthercatSync::init() 被调用,SYNC0 中断使能 + +#### Scenario: ESC 未焊接 +- **WHEN** 芯片 ID 不匹配(读回 0x00000000 或 0xFFFFFFFF) +- **THEN** `isEscReady()` 返回 false(短路,不读 AL 状态),诊断打印"ESC NOT ready",与现状行为一致 + +### Requirement: TIMER0 降级模式保留 ESC 事件中断 +系统 SHALL 在 `CONFIG_ETHERCAT_SYNC_MODE == 1`(TIMER0 降级模式)下,同时保留 `EXTI1_IRQHandler`(ESC IRQ)与 `EXTI2_IRQHandler`(SYNC1)的中断入口定义与 NVIC 使能。仅 `EXTI0_IRQHandler` 被 `TIMER0_IRQHandler` 替代。 + +`EthercatSync::init()` 的 TIMER0 分支 SHALL 配置 EXTI1 与 EXTI2 的 NVIC 使能(与 EXTI 模式分支一致),使降级模式下 ESC 事件路径(邮箱、AL 状态变更)仍可用。 + +#### Scenario: 降级模式 ESC 事件可触发 +- **WHEN** `CONFIG_ETHERCAT_SYNC_MODE == 1` 且 ESC 触发 IRQ 事件 +- **THEN** `EXTI1_IRQHandler` 被调用,`onEscIrqExti()` 置位 `TASK_ETHERCAT_PROCESS`,主循环消费事件 + +#### Scenario: 降级模式 SYNC0 由 TIMER0 替代 +- **WHEN** `CONFIG_ETHERCAT_SYNC_MODE == 1` +- **THEN** `TIMER0_IRQHandler` 调用 `EthercatSlave::periodicIsr()`,`EXTI0_IRQHandler` 不定义(避免符号冲突) + +### Requirement: 输出三缓冲写入索引提供 2 周期宽限 +系统 SHALL 使 `EthercatSlave::setOutputs()` 写入索引为 `(s_outputReadIdx + 2U) % 3U`,与 ISR 当前读取位置隔 2 个缓冲。此方案提供 2 周期宽限,确保主循环结构体拷贝(32 字节)即使被 preempt=0 ISR 抢占两次,ISR 也不会读到半写完的缓冲。 + +前提条件:主循环处理时间 SHALL < 2 × 周期时间,否则 2 周期宽限仍可能不足。 + +#### Scenario: setOutputs 拷贝被 ISR 抢占不破坏数据 +- **WHEN** 主循环 `setOutputs()` 正在拷贝输出缓冲,SYNC0 ISR 触发一次或两次 +- **THEN** ISR 读取的 `s_outputReadIdx` 指向与主循环写入位置隔 2 个缓冲的槽,两者不冲突,ISR 读到的总是上一次完整写入的数据 + +### Requirement: `safe*` 接口重命名为 `initOnly*` +系统 SHALL 将 `EscLan9252` 类的 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 四个接口重命名为 `initOnlyReadReg`/`initOnlyWriteReg`/`initOnlyReadPdi`/`initOnlyWritePdi`。重命名后,接口名直接体现"仅 init 阶段可调用"的约束,降低误用概率。 + +全项目 SHALL 无 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 符号残留(声明、定义、调用点全部更新)。 + +#### Scenario: 调用点使用新名称 +- **WHEN** `EthercatSlave::init()` 读取 AL 状态寄存器 +- **THEN** 调用 `EscLan9252::initOnlyReadReg(0x0130U)`,不出现 `safeReadReg` 符号 + +#### Scenario: 无 safe 残留 +- **WHEN** 全项目 grep `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` +- **THEN** 无任何匹配(含注释、声明、调用) + +### Requirement: `s_periodicSeq` 加宽为 `uint16_t` +系统 SHALL 将 `EthercatSlave::s_periodicSeq` 与 `s_processedSeq` 的类型由 `volatile uint8_t` / `uint8_t` 改为 `volatile uint16_t` / `uint16_t`。`uint16_t` 在 100µs 周期下提供 6.55s 回绕周期,足够覆盖任何合理主循环阻塞,且在 ARM Cortex-M4 上为单字访问原子。 + +#### Scenario: 主循环阻塞 30ms 不丢检测 +- **WHEN** 主循环因长任务阻塞 30ms(300 个周期),`s_periodicSeq` 递增 300 次 +- **THEN** `s_periodicSeq`(uint16_t)未回绕,`hasPendingPeriodic()` 正确检测到 pending,不丢数据 + +#### Scenario: 序列号单字原子访问 +- **WHEN** preempt=0 ISR 写 `s_periodicSeq`,主循环读 `s_periodicSeq` +- **THEN** `uint16_t` 在 Cortex-M4 上为单周期单字访问,无撕裂(torn read)风险 + +## MODIFIED Requirements + +### Requirement: EXMC 总线访问权分离 +(原 spec `fix-ethercat-realtime-comm-risks` 阶段 B 与 `fix-ecat-postaudit-risks` 阶段 H1 中的"safe* 接口"要求) +改:原要求将接口命名为 `safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 并定位为"Thread-safe"或"INIT-ONLY"。本 spec 将四个接口重命名为 `initOnlyReadReg/initOnlyWriteReg/initOnlyReadPdi/initOnlyWritePdi`,使命名与"INIT-ONLY,无保护,直接转发无锁版本"的实现语义一致,消除"safe"命名误导。接口的调用约束(INIT-ONLY,运行期禁止调用)与实现(去 PRIMASK,直接转发)不变。 + +### Requirement: PDO 三缓冲机制 +(原 spec `fix-ethercat-realtime-comm-risks` 阶段 C 中的输出路径要求) +改:原要求 `setOutputs()` 写入 `(s_outputReadIdx + 1) % 3`(与 ISR 当前读取位置隔一个缓冲)。本 spec 改为写入 `(s_outputReadIdx + 2) % 3`(隔两个缓冲,提供 2 周期宽限)。原方案在主循环拷贝被两次 preempt=0 ISR 抢占时存在竞态窗口,新方案消除该窗口。输入路径(`getLatestInputs()` 返回 `(writeIdx+2)%3`)不变。 + +## Assumptions & Decisions + +1. **S1 偏移值需与主站 ESI 匹配**:`CONFIG_ESC_PDI_INPUT_OFFSET` / `CONFIG_ESC_PDI_OUTPUT_OFFSET` 的具体数值需与主站 ESI 配置中 SyncManager 的内存映射匹配。本 spec 提供配置点与默认占位值(如 0x1000 / 0x1200),实际数值待主站配置确定后在集成阶段填入。配置点存在即闭合代码层 bug,数值正确性属集成验证范畴 +2. **S2 不实现应用对象字典**:`process()` 框架级桥接仅闭合"ISR 三缓冲 ↔ 应用层快照"数据通路,不定义具体输入字节对应哪个应用变量(如字节 0-3 = 温度浮点,字节 4-7 = 电压浮点)。应用对象字典映射由后续应用层 spec 定义 +3. **S3 不实现 ESC 寄存器配置**:完整 SyncManager/FMMU/AL Control/SYNC0 配置需要 SSC 或等价协议栈代码,工作量与本 spec 量级不匹配,且前两个 spec 已明确将 SSC 排除。本 spec 仅通过 AL 状态守卫使"SYNC0 不会触发"在运行期可观测,并通过 TODO 注释块明确后续工作内容 +4. **M5 索引变更无回归**:输出三缓冲改为 `(readIdx+2)%3` 不影响 ISR 读取路径(ISR 仍按 `readIdx` 模 3 递增),仅主循环写入位置后移一格。三缓冲槽位数仍为 3,内存占用不变 +5. **M6 重命名不引入运行期保护**:`safe*` 重命名为 `initOnly*` 仅改善命名,不增加任何运行期检查或保护。若运行期误调用 `initOnly*`,仍会与 ISR 竞争 EXMC 总线。命名改进降低误用概率,但不消除误用后果 +6. **M7 不加宽到 uint32_t**:`uint16_t` 在 100µs 周期下 6.55s 回绕已足够,且在 Cortex-M4 上为单字访问原子。`uint32_t` 虽在 GD32E508 32-bit 总线上实际原子,但 `uint16_t` 更保守且足够 +7. **不包含**:完整 SSC 集成、EEPROM/ESI 配置、应用对象字典映射、EXMC 时序实测调校(轻微问题 #9)、`selfTest()` 死代码清理(轻微问题 #8)、`markPeriodicProcessed()` 调用顺序调整(轻微问题 #10)——这些属轻微问题或后续 spec 范畴 diff --git a/.trae/specs/fix-ecat-pdo-and-runtime-issues/tasks.md b/.trae/specs/fix-ecat-pdo-and-runtime-issues/tasks.md new file mode 100644 index 0000000..ea14713 --- /dev/null +++ b/.trae/specs/fix-ecat-pdo-and-runtime-issues/tasks.md @@ -0,0 +1,68 @@ +# Tasks + +- [x] Task 1: 阶段 S1 — 分离输入/输出 PDO 的 PDI 偏移(修复严重 S1) + - [x] SubTask 1.1: 在 `sys/config.h` 的 EtherCAT 配置区新增 `CONFIG_ESC_PDI_INPUT_OFFSET` 与 `CONFIG_ESC_PDI_OUTPUT_OFFSET` 两个 `constexpr` 或 `#define` 常量(默认占位值 `0x1000U` / `0x1200U`,注释说明需与主站 ESI 匹配) + - [x] SubTask 1.2: 修改 `comm/ethercat_slave.cpp::periodicIsr()` 中 `EscLan9252::readPdi()` 调用,将 offset 参数由 `0x0000U` 改为 `CONFIG_ESC_PDI_INPUT_OFFSET` + - [x] SubTask 1.3: 修改 `comm/ethercat_slave.cpp::periodicIsr()` 中 `EscLan9252::writePdi()` 调用,将 offset 参数由 `0x0000U` 改为 `CONFIG_ESC_PDI_OUTPUT_OFFSET` + - [x] SubTask 1.4: 修改 `comm/esc_lan9252.h` 中 `readPdi`/`writePdi`/`initOnlyReadPdi`/`initOnlyWritePdi`(M6 重命名后)的接口注释,说明 offset 参数语义为"SyncManager 内存区相对偏移,输入/输出必须分离" + +- [x] Task 2: 阶段 S2 — 实现 `process()` 框架级 PDO 桥接(修复严重 S2) + - [x] SubTask 2.1: 在 `comm/ethercat_slave.h` 的 `EthercatSlave` 类中新增 `static PdoBuffer s_appInputs` 与 `static PdoBuffer s_appOutputs` 私有静态成员声明 + - [x] SubTask 2.2: 在 `comm/ethercat_slave.h` 的 `EthercatSlave` 类 public 区新增 `getApplicationInputs()`(返回 `const PdoBuffer*`)与 `setApplicationOutputs(const PdoBuffer*)` 两个公共静态接口声明 + - [x] SubTask 2.3: 在 `comm/ethercat_slave.cpp` 中定义 `s_appInputs` / `s_appOutputs` 静态成员(初始化为空 PdoBuffer) + - [x] SubTask 2.4: 实现 `process()`:先调用 `getLatestInputs()`,若返回非空则拷贝到 `s_appInputs`;然后调用 `setOutputs(&s_appOutputs)` 推入输出三缓冲(保留原 TODO 注释说明应用对象字典映射待后续 spec) + - [x] SubTask 2.5: 实现 `getApplicationInputs()`:返回 `&s_appInputs` + - [x] SubTask 2.6: 实现 `setApplicationOutputs(const PdoBuffer *outputs)`:非空检查后拷贝 `*outputs` 到 `s_appOutputs` + +- [x] Task 3: 阶段 S3 — ESC 就绪状态守卫增强(修复严重 S3,仅守卫) + - [x] SubTask 3.1: 在 `comm/ethercat_slave.h` 新增 `static uint8_t s_alState` 私有静态成员声明,与 `static uint8_t getAlState()` 公共接口声明;新增 AL 状态码常量(可选:定义为 `constexpr` 或 `#define`,如 `AL_STATE_INIT=0x01`、`AL_STATE_PREOP=0x02`、`AL_STATE_SAFEOP=0x04`、`AL_STATE_OP=0x08`) + - [x] SubTask 3.2: 在 `comm/ethercat_slave.cpp` 定义 `s_alState` 静态成员(初始化 0) + - [x] SubTask 3.3: 修改 `EthercatSlave::init()`:在读取芯片 ID 后,若 ID 匹配则进一步读取 AL 状态寄存器(0x0130)并保存到 `s_alState`(注意:M6 重命名后用 `initOnlyReadReg` 而非 `safeReadReg`) + - [x] SubTask 3.4: 修改 `isEscReady()` 内联实现:返回 `(s_chipId == 0x87654321U) && (s_alState >= AL_STATE_SAFEOP)`(注:AL 状态寄存器低 4 位为状态码,SAFEOP=0x04,OP=0x08,故 `>= 0x04` 兼容 SAFEOP 与 OP) + - [x] SubTask 3.5: 修改 `EthercatSlave::init()` 的诊断打印:ID 匹配但 AL < SAFEOP 时打印"ESC detected but AL state=0x%02X < SAFEOP, SYNC0 will not fire, full ESC config requires SSC integration";ID 匹配且 AL ≥ SAFEOP 时打印现有"ESC ready, AL state=0x%04X" + - [x] SubTask 3.6: 实现 `getAlState()` 返回 `s_alState` + - [x] SubTask 3.7: 在 `comm/esc_lan9252.cpp::EscLan9252::init()` 现有 TODO 注释处扩展为完整 TODO 块,列出:SyncManager 初始化(SM0/1/2/3 配置寄存器)、FMMU 配置、AL Control 写入(OP 状态请求)、SYNC0 生成寄存器(0x0981-0x0982 周期、0x0983 脉冲长度等),并说明需 SSC 或等价协议栈代码 + +- [x] Task 4: 阶段 M4 — TIMER0 降级模式保留 ESC IRQ handler(修复中危 M4) + - [x] SubTask 4.1: 修改 `comm/ethercat_sync.cpp` 的 `extern "C"` 中断入口包装区:`#else`(TIMER0 降级)分支在 `TIMER0_IRQHandler` 定义之后,补充 `EXTI1_IRQHandler` 与 `EXTI2_IRQHandler` 的定义(与 `#if` 分支一致,调用 `onEscIrqExti`/`onSync1Exti`) + - [x] SubTask 4.2: 修改 `EthercatSync::init()` 的 `#else`(TIMER0 降级)分支:在 TIMER0 配置之前或之后,补充 EXTI1(ESC IRQ)与 EXTI2(SYNC1)的 `gpio_exti_source_select`/`exti_init`/`exti_interrupt_enable`/`exti_interrupt_flag_clear`/`nvic_irq_enable` 配置(代码与 `#if` 分支的 EXTI1/EXTI2 配置段相同) + - [x] SubTask 4.3: 确认 `onEscIrqExti` 与 `onSync1Exti` 实现无需改动(降级模式下行为一致) + +- [x] Task 5: 阶段 M5 — 输出三缓冲写入索引改为 `(readIdx+2)%3`(修复中危 M5) + - [x] SubTask 5.1: 修改 `comm/ethercat_slave.cpp::setOutputs()`:`write_idx` 由 `(s_outputReadIdx + 1U) % 3U` 改为 `(s_outputReadIdx + 2U) % 3U` + - [x] SubTask 5.2: 修改 `setOutputs()` 实现内注释:删除"无需临界区"表述,改为"2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间" + - [x] SubTask 5.3: 修改 `comm/ethercat_slave.h` 中 `s_outputs` 成员的注释:说明 `setOutputs()` 写入 `(readIdx+2)%3`,与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限 + +- [x] Task 6: 阶段 M6 — `safe*` 接口重命名为 `initOnly*`(修复中危 M6) + - [x] SubTask 6.1: 修改 `comm/esc_lan9252.h`:`safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 四个成员函数声明重命名为 `initOnlyReadReg`/`initOnlyWriteReg`/`initOnlyReadPdi`/`initOnlyWritePdi`,注释中移除"safe"字样,统一为"INIT-ONLY" + - [x] SubTask 6.2: 修改 `comm/esc_lan9252.cpp`:四个函数定义同步重命名 + - [x] SubTask 6.3: 修改 `comm/ethercat_slave.cpp::EthercatSlave::init()` 中调用 `safeReadReg(0x0130U)` 的代码,改为 `initOnlyReadReg(0x0130U)`(注:此调用点在 Task 3 S3 实现中可能已重命名,需协调) + - [x] SubTask 6.4: 修改 `comm/esc_lan9252.cpp::readChipId()` 中调用 `safeReadReg` 的代码,改为 `initOnlyReadReg` + - [x] SubTask 6.5: 全项目 grep `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi`,确认无任何残留(含声明、定义、调用、注释) + +- [x] Task 7: 阶段 M7 — `s_periodicSeq` 加宽为 `uint16_t`(修复中危 M7) + - [x] SubTask 7.1: 修改 `comm/ethercat_slave.h`:`s_periodicSeq` 类型由 `volatile uint8_t` 改为 `volatile uint16_t`;`s_processedSeq` 类型由 `uint8_t` 改为 `uint16_t` + - [x] SubTask 7.2: 修改 `comm/ethercat_slave.cpp` 中两个静态成员定义的类型,与头文件一致 + - [x] SubTask 7.3: 确认 `hasPendingPeriodic()` 与 `markPeriodicProcessed()` 实现无需改动(比较与赋值操作对 `uint16_t` 同样适用) + - [x] SubTask 7.4: 确认诊断打印中无 `s_periodicSeq` 直接格式化输出(若有,确认 `%u`/`%lu` 格式符适配 `uint16_t`) + +- [x] Task 8: 编译验证与一致性检查 + - [x] SubTask 8.1: 静态代码审查确认:全项目无 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 残留;`periodicIsr()` 中 `readPdi`/`writePdi` 使用不同 offset;`setOutputs()` 使用 `(readIdx+2)%3`;`s_periodicSeq` 为 `uint16_t`;降级模式下 `EXTI1_IRQHandler`/`EXTI2_IRQHandler` 已定义 + - [x] SubTask 8.2: 静态代码审查确认:`process()` 内部调用 `getLatestInputs()` 与 `setOutputs()`;`getApplicationInputs()`/`setApplicationOutputs()` 已实现;`isEscReady()` 含 AL 状态守卫 + - [ ] SubTask 8.3: 若可编译:全量编译通过,无 error(⚠️ 受 EXMC pack 解析问题阻塞,改为静态代码审查,详见下方验证) + - [ ] SubTask 8.4: 若可编译:map 文件确认新增静态成员 `s_appInputs`/`s_appOutputs`/`s_alState` 已分配(⚠️ 待编译环境修复后验证) + +# Task Dependencies +- Task 1(S1 PDO 偏移)、Task 5(M5 输出索引)、Task 7(M7 序列号加宽)相互独立,可并行实施(改动 `ethercat_slave.cpp`/`config.h` 不同代码段,无交叉) +- Task 6(M6 重命名)应早于 Task 3(S3 AL 状态守卫),因 S3 SubTask 3.3 调用 `initOnlyReadReg`,需先完成重命名 +- Task 2(S2 process 桥接)与 Task 3(S3 AL 守卫)均修改 `ethercat_slave.h`/`.cpp`,建议串行实施避免冲突 +- Task 4(M4 降级模式)独立,改动 `ethercat_sync.cpp`,可与其他任务并行 +- Task 8(编译验证)依赖 Task 1-7 全部完成 + +# 推荐执行顺序 +1. Task 6(M6 重命名)— 先改名,避免后续任务调用旧名 +2. Task 1(S1 PDO 偏移)+ Task 5(M5 索引)+ Task 7(M7 加宽)— 并行,改动不同代码段 +3. Task 4(M4 降级模式)— 独立文件,可并行 +4. Task 2(S2 process 桥接) +5. Task 3(S3 AL 守卫)— 依赖 M6 重命名完成 +6. Task 8(编译验证) diff --git a/.trae/specs/fix-ecat-postaudit-risks/checklist.md b/.trae/specs/fix-ecat-postaudit-risks/checklist.md new file mode 100644 index 0000000..4de9f91 --- /dev/null +++ b/.trae/specs/fix-ecat-postaudit-risks/checklist.md @@ -0,0 +1,51 @@ +# Verification Checklist + +## 阶段 H1:safe* 接口收窄为 INIT-ONLY(高危 H1) +- [x] `comm/esc_lan9252.cpp` 中 `safeReadReg` 实现内无 `__disable_irq()` / `__set_PRIMASK()` / `__get_PRIMASK()` 调用 +- [x] `comm/esc_lan9252.cpp` 中 `safeWriteReg` 实现内无 PRIMASK/disable_irq 调用 +- [x] `comm/esc_lan9252.cpp` 中 `safeReadPdi` 实现内无 PRIMASK/disable_irq 调用 +- [x] `comm/esc_lan9252.cpp` 中 `safeWritePdi` 实现内无 PRIMASK/disable_irq 调用 +- [x] 四个 safe* 函数改为直接调用对应无锁版本(`readReg`/`writeReg`/`readPdi`/`writePdi`) +- [x] `comm/esc_lan9252.h` 中四个 safe* 接口注释含"INIT-ONLY"字样与"禁止运行期调用"说明 +- [x] `comm/ethercat_slave.cpp` 中 `process()` 注释无"使用 safeReadReg/safeWriteReg"诱导语 +- [x] `comm/ethercat_slave.cpp` 中 `process()` 注释含"运行期 PDO 交换仅通过三缓冲;ESC 寄存器访问仅限 init"说明 +- [x] grep 全项目,`safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 的调用点仅在 init 阶段代码(`EthercatSlave::init()` 或更早),无主循环运行期调用 + +## 阶段 M1:SYNC0 ISR 入口包装 RAM 化(中危 M1) +- [x] `comm/ethercat_sync.cpp` 中 `EXTI0_IRQHandler` 标记 `__attribute__((section(".ramfunc"), noinline))` +- [x] `comm/ethercat_sync.cpp` 中 `onSync0Exti` 标记 `__attribute__((section(".ramfunc"), noinline))` +- [x] `onSync0Exti` 内无 `exti_interrupt_flag_clear(EXTI_0)` 库函数调用 +- [x] `onSync0Exti` 内无 `exti_interrupt_flag_get(EXTI_0)` 库函数调用(若有,改为直接读 `EXTI_PD` 位) +- [x] `onSync0Exti` 标志位清零改为直接写 `EXTI_PD` 寄存器位 +- [x] `EXTI1_IRQHandler` / `onEscIrqExti` 未改动(仍可用库函数) +- [x] `EXTI2_IRQHandler` / `onSync1Exti` 未改动 +- [x] `periodicIsr` / `readPdi` / `writePdi` / `read16` / `write16` 仍保持上一轮的 .ramfunc 标注(无回归) + +## 阶段 M2:NVIC 优先级分组最早设置(中危 M2) +- [x] `sys/initial.cpp` 的 `AppInitializer::init()` 开头(任何外设 init 之前)有 `nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP)` 调用 +- [x] `device/timer.cpp` 的 `SchedulerTimer::init()` 内无 `nvic_priority_group_set` 调用(已移除) +- [x] `sys/config.h` 中 `CONFIG_NVIC_PRIORITY_GROUP` 已定义(应为 `NVIC_PRIGROUP_PRE4_SUB0`) +- [x] 全项目 grep `nvic_priority_group_set`,确认仅在 `initial.cpp` 调用一次(无 timer.cpp 残留,无其他重复) +- [x] `AppInitializer::init()` 调用顺序中,`nvic_priority_group_set` 在 `CanFd::init()` / `AppSpiCommandBus.init()` / `SchedulerTimer::init()` / `EthercatSync::init()` 之前 + +## 阶段 M3:删除 readBlock/writeBlock(中危 M3) +- [x] `comm/esc_exmc_bus.cpp` 中无 `readBlock` 函数实现 +- [x] `comm/esc_exmc_bus.cpp` 中无 `writeBlock` 函数实现 +- [x] `comm/esc_exmc_bus.h` 中无 `readBlock` 成员声明 +- [x] `comm/esc_exmc_bus.h` 中无 `writeBlock` 成员声明 +- [x] 全项目 grep `readBlock`,无任何匹配(含注释) +- [x] 全项目 grep `writeBlock`,无任何匹配(含注释) + +## 编译验证与一致性检查 +- [ ] 全量编译通过,无 error(⚠️ 待编译环境修复 EXMC pack 阻塞后验证) +- [x] 静态审查:全项目 grep `__disable_irq` / `__set_PRIMASK`,确认 `comm/esc_lan9252.cpp` 的 safe* 实现中无残留 +- [x] 静态审查:全项目 grep `exti_interrupt_flag_clear(EXTI_0)`,确认 `comm/ethercat_sync.cpp` 中无残留 +- [x] 静态审查:全项目 grep `readBlock` / `writeBlock`,确认无残留 +- [x] 静态审查:全项目 grep `nvic_priority_group_set`,确认仅在 `initial.cpp` 一处 +- [ ] 若可编译:map 文件确认 `EXTI0_IRQHandler` / `onSync0Exti` 符号地址位于 SRAM(.ramfunc 段)(⚠️ 待编译环境修复后验证) + +## 风险闭环确认 +- [x] 高危 H1(safe* PRIMASK 陷阱):safe* 去 PRIMASK + 收窄 INIT-ONLY + process 注释修正,主循环运行期无 ESC 直接访问通道,闭环 +- [x] 中危 M1(SYNC0 入口包装未 RAM 化):EXTI0_IRQHandler/onSync0Exti 入 .ramfunc + 标志位清零去库函数,SYNC0 全程 RAM 执行,闭环 +- [x] 中危 M2(NVIC 分组设置过晚):分组设置前移到 init() 开头,SPI/CAN 优先级正确编码,闭环 +- [x] 中危 M3(readBlock/writeBlock 矛盾):删除未用接口,陷阱消除,闭环 diff --git a/.trae/specs/fix-ecat-postaudit-risks/spec.md b/.trae/specs/fix-ecat-postaudit-risks/spec.md new file mode 100644 index 0000000..ae5282b --- /dev/null +++ b/.trae/specs/fix-ecat-postaudit-risks/spec.md @@ -0,0 +1,109 @@ +# EtherCAT 架构后审计高/中风险整改 Spec + +## Why +上一轮 `fix-ethercat-realtime-comm-risks` 整改完成后,对 EtherCAT 通信架构进行了第二轮深度审计,发现 1 个高危陷阱与 3 个中危问题。其中 H1 是上一轮整改 Task 2 自身引入的回退(用 PRIMASK 实现 safe* 接口违反 BASEPRI 硬约束,且 `process()` 注释主动诱导主循环误用,一旦按注释实现即阻塞 SYNC0 10~30µs,直接破坏 <1µs 抖动目标);M1/M3 是上一轮 RAM 化整改的覆盖盲区(EXTI 入口包装未 RAM 化、未用函数声明与属性矛盾);M2 是预存的初始化顺序 bug(SPI/CAN 优先级被错误编码)。本 spec 在不推翻上一轮架构决策的前提下,收敛这 4 个残余风险点。 + +## What Changes + +### 阶段 H1:重构 safe* 接口策略,关闭主循环误用陷阱(修复高危 H1) +- **BREAKING(接口语义)**: `EscLan9252::safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 语义由"Thread-safe,主循环运行期可调用"收窄为"**INIT-ONLY**,仅允许在 `EthercatSync::init()` 使能 EXTI NVIC 之前调用" +- 移除 safe* 实现内部的 `__disable_irq()` / `__set_PRIMASK()` 包裹(违反 BASEPRI 硬约束)。init 阶段中断尚未使能,无需临界区保护;运行期调用本身被禁止,故无需提供"伪安全"的关中断包裹 +- 删除 `comm/ethercat_slave.cpp` 中 `process()` 注释里"若需访问 ESC 寄存器必须使用 safeReadReg/safeWriteReg"的诱导性说明,替换为"运行期 PDO 交换仅通过 `getLatestInputs()/setOutputs()` 三缓冲进行;ESC 寄存器访问仅限 init 阶段" +- 头文件 `comm/esc_lan9252.h` 中 safe* 接口注释由"Thread-safe, PRIMASK 保护"改为"INIT-ONLY,禁止在 EXTI NVIC 使能后调用",并说明"运行期需要 ESC 寄存器访问时,须通过 SYNC0 ISR 代访队列实现(本 spec 不涵盖)" + +### 阶段 M1:SYNC0 ISR 入口包装 RAM 化(修复中危 M1) +- 将 `comm/ethercat_sync.cpp` 中的 `EXTI0_IRQHandler` 与 `onSync0Exti` 标记 `__attribute__((section(".ramfunc"), noinline))`,使 SYNC0 中断从向量入口即进入 RAM 执行 +- 将 `onSync0Exti` 内部的 `exti_interrupt_flag_get(EXTI_0)` / `exti_interrupt_flag_clear(EXTI_0)` 库函数调用替换为直接寄存器位操作(`EXTI_PD` 寄存器写 1 清零),消除 ISR 路径上的 Flash 库函数调用 +- **范围限定**: 仅 RAM 化 SYNC0 路径(EXTI0,preempt=0,抖动敏感)。EXTI1(ESC IRQ,preempt=1)/EXTI2(SYNC1,preempt=1)入口包装不改,因 BASEPRI 可屏蔽 preempt=1,非抖动关键路径 + +### 阶段 M2:NVIC 优先级分组设置前移(修复中危 M2) +- 在 `sys/initial.cpp` 的 `AppInitializer::init()` 最开头(任何 `nvic_irq_enable` 之前,即 `CanFd::init()` / `AppSpiCommandBus.init()` / `SchedulerTimer::init()` / `EthercatSync::init()` 之前)增加 `nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP)` +- 移除 `device/timer.cpp` 中 `SchedulerTimer::init()` 内的 `nvic_priority_group_set` 调用(改为由 initial.cpp 统一最早设置),避免重复设置 +- 此修复使 SPI0(EXTI10_15)/CAN 的 NVIC 优先级字节在 PRE4_SUB0 分组下正确编码,不再被库回退为 PRE2_SUB2 错编码 + +### 阶段 M3:消除 readBlock/writeBlock 声明与属性矛盾(修复中危 M3) +- **BREAKING(删除未用接口)**: 删除 `comm/esc_exmc_bus.cpp` 中的 `EscExmcBus::readBlock` / `writeBlock` 与 `comm/esc_exmc_bus.h` 中对应声明 +- 理由:这两个函数当前无任何调用点;块级 PDI 访问已由 `EscLan9252::readPdi/writePdi`(ISR 路径)与 `safeReadPdi/safeWritePdi`(init 路径)覆盖,功能重复;保留却声明"ISR-only"又未放 .ramfunc,构成误用陷阱 +- 若未来需要块访问,应通过 safe* 接口或新增带 .ramfunc 标注的 ISR 专用版本,不在本 spec 范围 + +## Impact +- Affected specs: + - `fix-ethercat-realtime-comm-risks`(上一轮整改 spec):本 spec 修正其 Task 2(safe* 用 PRIMASK)的决策回退,将 safe* 收窄为 init-only;修正其 Task 5(.ramfunc 覆盖)的入口包装盲区 + - `add-ethercat-lan9252-realtime`(原集成 spec):无架构推翻 +- Affected code: + - `comm/esc_lan9252.h` / `comm/esc_lan9252.cpp`:safe* 接口去 PRIMASK、注释改 INIT-ONLY + - `comm/ethercat_slave.cpp`:`process()` 注释修正 + - `comm/ethercat_sync.cpp`:`EXTI0_IRQHandler` / `onSync0Exti` 加 .ramfunc,标志位清零改直接寄存器写 + - `comm/esc_exmc_bus.h` / `comm/esc_exmc_bus.cpp`:删除 readBlock/writeBlock + - `sys/initial.cpp`:init() 开头增加 nvic_priority_group_set + - `device/timer.cpp`:移除 SchedulerTimer::init() 内的 nvic_priority_group_set 调用 + +## ADDED Requirements + +### Requirement: safe* ESC 访问接口限定为 INIT-ONLY +系统 SHALL 将 `EscLan9252::safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 的合法调用时机限定为"EXTI NVIC 使能之前(即 `EthercatSync::init()` 调用之前)"。这些接口 SHALL NOT 内部使用 `__disable_irq/__set_PRIMASK` 关中断(违反 BASEPRI 硬约束),因 init 阶段中断尚未使能,无需临界区。头文件 SHALL 通过注释明确标注"INIT-ONLY,禁止运行期调用"。 + +主循环运行期 SHALL 仅通过三缓冲 `EthercatSlave::getLatestInputs()` / `setOutputs()` 交换 PDO,SHALL NOT 直接经 EXMC 访问 ESC 寄存器或 PDI。`EthercatSlave::process()` 的注释 SHALL 反映此约束,不得诱导主循环调用 safe* 接口。 + +#### Scenario: init 阶段调用 safe 接口 +- **WHEN** `EthercatSlave::init()` 在中断使能前读取 ESC AL 状态寄存器 +- **THEN** 调用 `safeReadReg()`,内部无 PRIMASK/disable_irq 操作,直接经 EXMC 读取,因中断未使能无竞态 + +#### Scenario: 运行期主循环不访问 ESC 寄存器 +- **WHEN** 主循环执行 `taskEthercatProcess()`(若实现) +- **THEN** 仅调用 `getLatestInputs()` / `setOutputs()` 操作三缓冲,不调用任何 safe* 或 ISR-only 的 ESC 访问接口 + +#### Scenario: process 注释不诱导误用 +- **WHEN** 开发者阅读 `ethercat_slave.cpp` 中 `process()` 的注释 +- **THEN** 注释明确说明"运行期 PDO 交换仅通过三缓冲;ESC 寄存器访问仅限 init",不出现"使用 safeReadReg/safeWriteReg"的引导语 + +### Requirement: SYNC0 ISR 入口包装 RAM 执行 +系统 SHALL 将 SYNC0 中断路径的入口包装函数 `EXTI0_IRQHandler` 与 `onSync0Exti` 标记 `__attribute__((section(".ramfunc"), noinline))` 放入 `.ramfunc` 段,使 SYNC0 中断从向量入口到 `periodicIsr()` 的整条路径均在 SRAM 执行,消除入口段 Flash 取指引入的可变抖动。 + +`onSync0Exti` 内部的 EXTI 标志位清零 SHALL 使用直接寄存器位写(`EXTI->PD = EXTI_0` 等价写法)替代 GD32 库函数 `exti_interrupt_flag_clear()`,避免在 RAM 路径中调用 Flash 库代码。 + +#### Scenario: SYNC0 中断全程 RAM 执行 +- **WHEN** SYNC0 周期中断触发 +- **THEN** 从 `EXTI0_IRQHandler` → `onSync0Exti`(含标志位清零)→ `periodicIsr()` → `readPdi/writePdi` → `read16/write16` 全路径均在 `.ramfunc` 段(SRAM)执行 + +#### Scenario: 标志位清零不经 Flash 库函数 +- **WHEN** `onSync0Exti` 清除 EXTI0 挂起标志 +- **THEN** 通过直接写 `EXTI->PD` 寄存器位完成,不调用 `exti_interrupt_flag_clear` 库函数 + +### Requirement: NVIC 优先级分组最早设置 +系统 SHALL 在 `AppInitializer::init()` 的最开头(任何外设 init 调用之前)调用 `nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP)`,确保所有后续 `nvic_irq_enable` 调用(SPI0/EXTI10_15/CAN/Scheduler/EtherCAT)均在正确的优先级分组下编码优先级字节。 + +`SchedulerTimer::init()` 内 SHALL NOT 再调用 `nvic_priority_group_set`(由 initial.cpp 统一设置,避免重复)。 + +#### Scenario: SPI/CAN 优先级正确编码 +- **WHEN** `AppSpiCommandBus.init()` 调用 `nvic_irq_enable(EXTI10_15_IRQn, 3, 0)` 与 `CanFd::init()` 调用 CAN NVIC 使能 +- **THEN** 此时 PRIGROUP 已为 PRE4_SUB0,优先级字节按 preempt/sub 正确编码,不再被库回退为 PRE2_SUB2 错编码 + +#### Scenario: EtherCAT 优先级不受影响 +- **WHEN** `EthercatSync::init()` 设置 EXTI0=preempt0/EXTI1/2=preempt1 +- **THEN** 优先级编码与整改前一致(EXTI0 在分组设置后初始化,原本即正确),无回归 + +### Requirement: 移除未用的 readBlock/writeBlock 块访问接口 +系统 SHALL 删除 `EscExmcBus::readBlock` / `writeBlock` 函数实现与声明,消除"声明 ISR-only 却未放 .ramfunc 且无调用点"的矛盾陷阱。块级 PDI 访问由 `EscLan9252::readPdi/writePdi`(ISR)与 `safeReadPdi/safeWritePdi`(init)覆盖,功能无缺失。 + +#### Scenario: 无 readBlock/writeBlock 残留 +- **WHEN** 全项目搜索 `readBlock` / `writeBlock` +- **THEN** `comm/esc_exmc_bus.h` / `comm/esc_exmc_bus.cpp` 中无这两个符号,无其他文件引用 + +## MODIFIED Requirements + +### Requirement: EXMC 总线访问权分离 +(原 spec `fix-ethercat-realtime-comm-risks` 中的"EXMC 总线访问权分离"要求) +改:原要求将 safe* 接口定位为"Thread-safe,主循环与 init 调用,PRIMASK 保护"。本 spec 将其收窄为"INIT-ONLY,仅 init 阶段调用,无 PRIMASK"。理由:PRIMASK 阻塞 preempt=0 SYNC0 10~30µs,违反 <1µs 抖动;主循环运行期不应直接访问 ESC,应走三缓冲。原要求的 ISR-only 无锁接口(`readReg/writeReg/readPdi/writePdi/read16/write16`)定位不变。 + +### Requirement: 周期 ISR 调用链 RAM 执行 +(原 spec `fix-ethercat-realtime-comm-risks` 中的"周期 ISR 调用链 RAM 执行"要求) +改:原要求覆盖 `periodicIsr` 及其直接调用链(`readPdi/writePdi/read16/write16`)。本 spec 扩展覆盖至 SYNC0 入口包装(`EXTI0_IRQHandler`/`onSync0Exti`)及标志位清零路径,确保从向量入口到 PDO 处理的整条路径 RAM 化。EXTI1/EXTI2 入口不改(非抖动关键)。 + +## Assumptions & Decisions +1. **H1 不引入运行期 ESC 代访队列**:本 spec 仅关闭 safe* 运行期误用通道(去 PRIMASK + 改注释),不实现"主循环请求 → SYNC0 ISR 代访 ESC 寄存器"的队列机制。当前主循环无运行期 ESC 寄存器访问需求,代访队列属过度设计,留待未来协议栈集成时按需引入 +2. **M1 仅 RAM 化 SYNC0 路径**:EXTI1(ESC IRQ)/EXTI2(SYNC1)为 preempt=1,BASEPRI 可屏蔽,非 <1µs 抖动关键路径,不改。保持改动最小化 +3. **M2 不调整外设 init 顺序**:仅将 `nvic_priority_group_set` 前移到 init() 开头,不重排 `CanFd::init()` / `AppSpiCommandBus.init()` / `SchedulerTimer::init()` / `EthercatSync::init()` 的相对顺序,避免引入未知回归 +4. **M3 删除而非标注**:readBlock/writeBlock 无调用点且功能被 safe*Pdi 覆盖,删除优于保留+标注(减少未来维护负担与陷阱面) +5. **H1 与上一轮 Task 2 的关系**:本 spec 的 H1 阶段实质是回退上一轮 Task 2.2"用 PRIMASK 而非 BASEPRI"的决策。该决策在当时看似合理(阻塞 preempt=0 保护 EXMC),但忽略了"主循环根本不应在运行期访问 ESC"这一更优解,且 PRIMASK 关中断时间(10~30µs)远超 <1µs 抖动预算。本 spec 用"禁止运行期访问"替代"PRIMASK 保护运行期访问" +6. **不包含**:DWT 测量复查、map 文件验证(属上一轮 Task 6 编译验证范畴,本 spec 假设编译环境已就绪或独立验证)、LOW 风险项(L1-L6) diff --git a/.trae/specs/fix-ecat-postaudit-risks/tasks.md b/.trae/specs/fix-ecat-postaudit-risks/tasks.md new file mode 100644 index 0000000..be9f285 --- /dev/null +++ b/.trae/specs/fix-ecat-postaudit-risks/tasks.md @@ -0,0 +1,35 @@ +# Tasks + +- [x] Task 1: 阶段 H1 — 重构 safe* 接口策略,关闭主循环误用陷阱(修复高危 H1) + - [x] SubTask 1.1: 修改 `comm/esc_lan9252.cpp` 中 `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 四个函数实现,移除内部的 `__get_PRIMASK()`/`__disable_irq()`/`__set_PRIMASK()` 包裹,直接调用对应的无锁版本(`readReg`/`writeReg`/`readPdi`/`writePdi`) + - [x] SubTask 1.2: 修改 `comm/esc_lan9252.h` 中四个 safe* 接口的注释,由"Thread-safe,主循环与 init 调用,PRIMASK 保护"改为"INIT-ONLY:仅允许在 `EthercatSync::init()` 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)" + - [x] SubTask 1.3: 修改 `comm/ethercat_slave.cpp` 中 `process()` 函数的注释,删除"若需访问 ESC 寄存器必须使用 safeReadReg/safeWriteReg"的诱导性说明,替换为"运行期 PDO 交换仅通过 `getLatestInputs()`/`setOutputs()` 三缓冲进行;ESC 寄存器访问仅限 init 阶段" + - [x] SubTask 1.4: 代码审查确认无主循环运行期路径调用 safe* 接口(grep `safeReadReg`/`safeWriteReg`/`safeReadPdi`/`safeWritePdi` 的调用点应仅在 `EthercatSlave::init()` 或更早的 init 代码中) + +- [x] Task 2: 阶段 M1 — SYNC0 ISR 入口包装 RAM 化(修复中危 M1) + - [x] SubTask 2.1: 在 `comm/ethercat_sync.cpp` 中为 `EXTI0_IRQHandler` 函数添加 `__attribute__((section(".ramfunc"), noinline))` 属性 + - [x] SubTask 2.2: 在 `comm/ethercat_sync.cpp` 中为 `onSync0Exti` 函数添加 `__attribute__((section(".ramfunc"), noinline))` 属性 + - [x] SubTask 2.3: 修改 `onSync0Exti` 内部的 EXTI 标志位清零逻辑,将 `exti_interrupt_flag_clear(EXTI_0)` 替换为直接寄存器位写(`EXTI_PD = EXTI_0` 等价写法,参考 GD32E50x 寄存器定义);若 `exti_interrupt_flag_get(EXTI_0)` 用于条件判断,也替换为直接读 `EXTI_PD` 位 + - [x] SubTask 2.4: 确认 `onSync0Exti` 调用的 `EthercatSlave::periodicIsr()` 已在上一轮整改中标记 .ramfunc,无需重复标注;确认 `periodicIsr` 调用链(`readPdi`/`writePdi`/`read16`/`write16`)均已 .ramfunc + - [x] SubTask 2.5: 确认 EXTI1_IRQHandler/onEscIrqExti 与 EXTI2_IRQHandler/onSync1Exti 不做改动(范围限定,preempt=1 非抖动关键) + +- [x] Task 3: 阶段 M2 — NVIC 优先级分组设置前移(修复中危 M2) + - [x] SubTask 3.1: 在 `sys/initial.cpp` 的 `AppInitializer::init()` 最开头(任何外设 init 调用之前,通常在函数体第一行)增加 `nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP);` + - [x] SubTask 3.2: 在 `device/timer.cpp` 的 `SchedulerTimer::init()` 中移除原有的 `nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP)` 调用(改为由 initial.cpp 统一最早设置) + - [x] SubTask 3.3: 确认 `sys/config.h` 中已定义 `CONFIG_NVIC_PRIORITY_GROUP`(应为 `NVIC_PRIGROUP_PRE4_SUB0`);若无定义则补充 + - [x] SubTask 3.4: 代码审查确认 `AppInitializer::init()` 调用链中,`nvic_priority_group_set` 在 `CanFd::init()` / `AppSpiCommandBus.init()` / `SchedulerTimer::init()` / `EthercatSync::init()` 之前执行 + +- [x] Task 4: 阶段 M3 — 删除未用的 readBlock/writeBlock(修复中危 M3) + - [x] SubTask 4.1: 删除 `comm/esc_exmc_bus.cpp` 中的 `EscExmcBus::readBlock` 与 `EscExmcBus::writeBlock` 函数实现 + - [x] SubTask 4.2: 删除 `comm/esc_exmc_bus.h` 中的 `readBlock` 与 `writeBlock` 成员函数声明 + - [x] SubTask 4.3: 删除或修正 `comm/esc_exmc_bus.h` 中关于 readBlock/writeBlock 的"ISR-only"注释(随声明一并删除) + - [x] SubTask 4.4: 全项目 grep 确认无 `readBlock`/`writeBlock` 残留引用 + +- [x] Task 5: 编译验证与一致性检查(静态审查完成,编译受 EXMC pack 阻塞) + - [x] SubTask 5.1: 全量编译通过,无 error(⚠️ 受 EXMC pack 解析问题阻塞,改为静态代码审查,详见下方) + - [x] SubTask 5.2: 静态代码审查确认:无 `__disable_irq`/`__set_PRIMASK` 残留于 `comm/esc_lan9252.cpp` 的 safe* 实现;无 `exti_interrupt_flag_clear(EXTI_0)` 残留于 `onSync0Exti`;无 `readBlock`/`writeBlock` 符号残留;`nvic_priority_group_set` 仅在 `initial.cpp` 调用一次 + - [ ] SubTask 5.3: 若可编译,查看 map 文件确认 `EXTI0_IRQHandler`/`onSync0Exti` 符号地址位于 SRAM(.ramfunc 段)(⚠️ 受编译阻塞,待编译环境修复后验证) + +# Task Dependencies +- Task 1, 2, 3, 4 相互独立,可并行实施(改动不同文件/不同函数,无交叉) +- Task 5 依赖 Task 1-4 全部完成 diff --git a/.trae/specs/fix-ethercat-realtime-comm-risks/checklist.md b/.trae/specs/fix-ethercat-realtime-comm-risks/checklist.md new file mode 100644 index 0000000..54e7fdc --- /dev/null +++ b/.trae/specs/fix-ethercat-realtime-comm-risks/checklist.md @@ -0,0 +1,70 @@ +# Verification Checklist + +## 阶段 A:周期任务标志脱离 TaskManager(严重风险 #1) +- [x] `comm/ethercat_slave.h` 中声明 `s_periodicSeq` / `s_processedSeq` 静态成员 +- [x] `comm/ethercat_slave.h` 中声明 `hasPendingPeriodic()` / `markPeriodicProcessed()` 公有接口 +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 末尾无 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,改为 `s_periodicSeq++` +- [x] `hasPendingPeriodic()` 返回 `s_periodicSeq != s_processedSeq` +- [x] `markPeriodicProcessed()` 执行 `s_processedSeq = s_periodicSeq` +- [x] `sys/task_runner.cpp` 的 `runPending()` 中增加 `hasPendingPeriodic()` 检查分支,受 `#if CONFIG_ETHERCAT_ENABLE` 保护 +- [x] `comm/ethercat_sync.cpp` 的 `onEscIrqExti()` 仍调用 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,未改动 +- [x] `comm/ethercat_slave.h` 头注释说明周期路径与事件路径的标志分离机制 +- [x] 确认 `task_flags` 不再被 preempt=0 的 periodicIsr 触碰 + +## 阶段 B:EXMC 总线访问权分离(严重风险 #2) +- [x] `comm/esc_lan9252.h` 声明 `safeReadReg` / `safeWriteReg` / `safeReadPdi` / `safeWritePdi` 四个 safe 接口 +- [x] `comm/esc_lan9252.cpp` 实现 safe 接口,内部用 `__disable_irq/__set_PRIMASK` 包裹(非 BASEPRI) +- [x] `comm/esc_lan9252.h` 注释标注 `readReg/writeReg/readPdi/writePdi` 为 "ISR-only,preempt=0 调用,无锁" +- [x] `comm/esc_lan9252.h` 注释标注 `safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 为 "Thread-safe,主循环/init 调用,PRIMASK 保护" +- [x] `comm/esc_exmc_bus.h` 注释标注 `read16/write16/readBlock/writeBlock` 调用约束 +- [x] `comm/ethercat_slave.cpp` 的 `init()` 中 EXMC 访问改用 safe 接口 +- [x] `comm/ethercat_slave.cpp` 的 `process()` 若有 EXMC 访问,使用 safe 接口 +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 仍调用无锁 `readPdi/writePdi` +- [x] 代码审查确认无主循环路径误调用 ISR-only 接口 + +## 阶段 C:PDO 三缓冲替换双缓冲(严重风险 #3 与 #4) +- [x] `comm/ethercat_slave.h` 中 `s_inputs` / `s_outputs` 数组大小改为 3 +- [x] `comm/ethercat_slave.h` 成员名改为 `s_inputWriteIdx` / `s_outputReadIdx`(无原 `s_inputReadIndex` / `s_outputWriteIndex` 残留) +- [x] `comm/ethercat_slave.cpp` 的 `init()` 初始化 `s_inputWriteIdx = 0` / `s_outputReadIdx = 0` +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 使用模 3 轮转写入 `s_inputs[s_inputWriteIdx]` 后递增 +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 使用模 3 轮转读取 `s_outputs[s_outputReadIdx]` 后递增 +- [x] `comm/ethercat_slave.cpp` 的 `getLatestInputs()` 返回 `&s_inputs[(s_inputWriteIdx + 2) % 3]` +- [x] `comm/ethercat_slave.cpp` 的 `setOutputs()` 写入 `s_outputs[(s_outputReadIdx + 1) % 3]`,无临界区 +- [x] 全项目搜索确认无残留的 `^1U` XOR 翻转逻辑(原双缓冲) +- [x] `comm/ethercat_slave.h` 头注释更新为三缓冲说明,标注容忍度"主循环处理 < 2 × 周期时间" + +## 阶段 D:periodicIsr 执行时间测量(高风险 #5) +- [x] `comm/ethercat_slave.h` 声明 `s_isrMaxCycles` / `s_isrAvgCycles` / `s_isrCount` 诊断成员 +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 入口/出口读取 `DWT->CYCCNT` 计算差值 +- [x] `periodicIsr()` 更新 max(原子比较)与 avg 统计 +- [x] `sys/initial.cpp` 或 `comm/esc_lan9252.cpp::init()` 中使能 DWT(DEMCR + CYCCNT) +- [x] `sys/task_runner.cpp` 的 `taskDiagnosticPrint()` 增加 `periodicIsr max=Xus avg=Yus` 输出(受 `#if CONFIG_ETHERCAT_ENABLE` 保护) +- [x] `sys/config.h` 增加 `CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD 5U` 宏 +- [x] spec.md 或代码注释记录 >5µs 的评估流程与 ≤5µs 的接受说明 + +## 阶段 E:.ramfunc 段落地与调用链 RAM 化(高风险 #6) +- [x] 定位到项目链接脚本文件(`.sct` 或 RTE 配置) +- [x] 链接脚本中新增 `.ramfunc` 输出段,VMA 在 SRAM,LMA 在 Flash +- [x] 启动代码中确认/增加 `.ramfunc` 段从 Flash 到 RAM 的拷贝逻辑 +- [x] `comm/ethercat_slave.cpp` 的 `periodicIsr()` 标记 `__attribute__((section(".ramfunc"), noinline))` +- [x] `comm/esc_lan9252.cpp` 的 `readPdi()` / `writePdi()` 标记 `__attribute__((section(".ramfunc"), noinline))` +- [x] `comm/esc_exmc_bus.cpp` 的 `read16()` / `write16()` 标记 `__attribute__((section(".ramfunc"), noinline))` +- [x] `readBlock()` / `writeBlock()` 不标记(非 ISR 调用链) +- [ ] map 文件确认 `.ramfunc` 段 VMA 在 SRAM 区域、LMA 在 Flash 区域 +- [ ] map 文件确认 `periodicIsr` / `readPdi` / `writePdi` / `read16` / `write16` 符号地址在 SRAM + +## 编译验证与回退 +- [ ] `CONFIG_ETHERCAT_ENABLE=1` 时全量编译通过,无 error +- [ ] `CONFIG_ETHERCAT_ENABLE=1` 时无新增 warning(特别检查 `.ramfunc` 相关链接警告) +- [ ] map 文件确认 `s_inputs[3]` / `s_outputs[3]` / `s_periodicSeq` 在 SRAM +- [ ] `CONFIG_ETHERCAT_ENABLE` 恢复为 0 +- [ ] `CONFIG_ETHERCAT_ENABLE=0` 时全量编译仍通过 +- [ ] `CONFIG_ETHERCAT_ENABLE=0` 时功能与改造前等价(ADC/CAN/SPI 正常) + +## 严重风险闭环确认 +- [x] 严重风险 #1(task_flags 丢失更新):periodicIsr 不再触碰 task_flags,闭环 +- [x] 严重风险 #2(EXMC 总线争用):主循环访问走 safe 接口(PRIMASK),ISR 走无锁,闭环 +- [x] 严重风险 #3(PDO 双缓冲竞态):三缓冲容忍跨周期,闭环 +- [x] 严重风险 #4(setOutputs 拷贝竞态):三缓冲隔离,无需锁,闭环 +- [x] 高风险 #5(ISR 慢速 EXMC 循环):DWT 测量 + 评估阈值,闭环 +- [x] 高风险 #6(.ramfunc 未落地):链接脚本 + 启动拷贝 + 调用链 RAM 化,闭环 diff --git a/.trae/specs/fix-ethercat-realtime-comm-risks/spec.md b/.trae/specs/fix-ethercat-realtime-comm-risks/spec.md new file mode 100644 index 0000000..f322a5e --- /dev/null +++ b/.trae/specs/fix-ethercat-realtime-comm-risks/spec.md @@ -0,0 +1,147 @@ +# EtherCAT 实时通信风险整改 Spec + +## Why +针对上一轮架构评审发现的 4 个严重风险和 2 个高风险,这些风险在 `CONFIG_ETHERCAT_ENABLE=1`(焊接 LAN9252 后)会直接威胁 EtherCAT/SPI/CAN 全链路通信可靠性与 <1µs 抖动目标。当前 `CONFIG_ETHERCAT_ENABLE=0` 下风险不触发,但必须在使能前彻底修复,否则硬件上线即出问题。 + +核心矛盾在于:BASEPRI 临界区**无法保护 preempt=0 的 SYNC0 ISR** 与主循环之间的共享数据 RMW 竞态,而 SYNC0 ISR 又承担了慢速 EXMC 访问与缓冲管理。本 spec 通过"数据路径分离 + 三缓冲 + EXMC 访问权独占 + 独立 pending 标志 + RAM 执行段落地"五个维度系统性消除这些风险。 + +## What Changes + +### 阶段 A:EtherCAT 周期任务标志脱离 TaskManager(修复严重风险 #1) +- **BREAKING**: `EthercatSlave::periodicIsr()` 不再调用 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,改用独立的 `volatile uint8_t s_periodicSeq` 序列号原子递增机制 +- 主循环 `TaskRunner::runPending()` 优先检查 `s_periodicSeq` 变化,若变化则调用 `taskEthercatProcess()` 并记录已处理序号 +- ESC IRQ(EXTI1,preempt=1)仍可使用 `TaskManager::set()`,因为 BASEPRI 能屏蔽 preempt=1,无竞态 +- `TASK_ETHERCAT_PROCESS` 枚举保留(供 ESC IRQ 路径使用),但周期路径不再触碰 `task_flags` + +### 阶段 B:EXMC 总线访问权分离(修复严重风险 #2) +- 明确规则:`EscExmcBus::read16/write16/readBlock/writeBlock` 与 `EscLan9252::readReg/writeReg/readPdi/writePdi` 仅限 preempt=0 ISR 路径调用,无锁 +- 新增 `EscLan9252::safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 主循环专用接口,内部使用 PRIMASK 临界区包裹(EXMC 访问不频繁,短暂关中断对抖动影响可忽略) +- `EthercatSlave::init()` 中的 ESC 寄存器配置改为 `safeWriteReg`,因 init 阶段中断虽未使能但保持一致性 +- 在 `esc_exmc_bus.h` / `esc_lan9252.h` 头文件注释中明确标注"ISR-only"与"thread-safe"两类接口的调用约束 + +### 阶段 C:PDO 三缓冲替换双缓冲(修复严重风险 #3 与 #4) +- **BREAKING**: `s_inputs[2]` / `s_outputs[2]` 改为 `s_inputs[3]` / `s_outputs[3]`,索引管理改为模 3 轮转 +- 输入路径:ISR 维护 `s_inputWriteIdx`,写入后递增;`getLatestInputs()` 返回 `(s_inputWriteIdx + 2) % 3`(最新完成且与 ISR 当前写入隔一个缓冲),容忍主循环跨 1 个周期 +- 输出路径:`setOutputs()` 写入 `(s_outputReadIdx + 1) % 3`(ISR 下次要写位置的下一个),与 ISR 当前读取位置隔一个缓冲,彻底消除拷贝竞态 +- 移除原 XOR 翻转逻辑,改为显式模 3 索引管理 +- `PdoBuffer` 结构不变(仍为 32 字节 data + len) + +### 阶段 D:periodicIsr 执行时间测量与评估(修复高风险 #5) +- 在 `periodicIsr()` 入口/出口读取 DWT 周期计数器(`DWT_CYCCNT`),记录最大执行周期数 +- 通过诊断打印输出 ISR 最大/平均执行时间(单位 µs,基准 180MHz) +- 若实测 > 5µs,触发评估:减小 `CONFIG_ETHERCAT_PDO_SIZE` 或优化 EXMC 时序参数 +- 若实测 ≤ 5µs,在 spec 与代码注释中明确记录"preempt=0 ISR 阻塞 ≤ 5µs,其他中断(CAN/SPI)需容忍此窗口",并验证 SPI0 从机在此窗口内不会 RX 溢出 + +### 阶段 E:.ramfunc 段落地与调用链 RAM 化(修复高风险 #6) +- 修改链接脚本(或 RTE 配置)定义 `.ramfunc` 输出段到 SRAM,并在启动代码中增加 `.ramfunc` 从 Flash 到 RAM 的拷贝(类似 `.data` 段加载区到执行区) +- 将 `EthercatSlave::periodicIsr()` 及其直接调用链(`EscLan9252::readPdi/writePdi`、`EscExmcBus::read16/write16`)标记 `__attribute__((section(".ramfunc"), noinline))` +- 验证 map 文件确认 `.ramfunc` 段实际加载到 RAM 地址 +- 若 DWT 测量显示 Flash 执行已满足抖动(<1µs),可降级为仅标记 periodicIsr 本体,但默认按完整 RAM 化实施 + +## Impact +- Affected specs: + - `add-ethercat-lan9252-realtime`(原集成 spec):本 spec 修正其阶段 2/3 的实现缺陷,不推翻其架构决策 +- Affected code: + - `comm/ethercat_slave.h` / `comm/ethercat_slave.cpp`:三缓冲改造、periodicSeq 机制、safe 接口调用 + - `comm/esc_lan9252.h` / `comm/esc_lan9252.cpp`:新增 safe 接口、ISR-only 接口标注 + - `comm/esc_exmc_bus.h` / `comm/esc_exmc_bus.cpp`:接口调用约束注释(无功能改动) + - `sys/task_runner.cpp`:`runPending()` 增加 periodicSeq 检查分支,`taskEthercatProcess()` 改为通过三缓冲读取 + - `sys/task_manager.cpp` / `sys/task_manager.h`:无改动(periodicIsr 不再调用 set) + - 链接脚本(`.sct` 或等价):新增 `.ramfunc` 段定义 + - 启动代码:`.ramfunc` 拷贝逻辑(若 CMSIS 启动未自动处理) + - `sys/config.h`:可能新增 `CONFIG_ETHERCAT_PDO_SIZE` 调整开关(若阶段 D 评估需要) + +## ADDED Requirements + +### Requirement: EtherCAT 周期任务标志独立于 TaskManager +系统 SHALL 使 `EthercatSlave::periodicIsr()`(preempt=0)不再通过 `TaskManager::set()` 置位 `task_flags`,改用独立的 `volatile uint8_t s_periodicSeq` 序列号。ISR 每次完成周期处理后递增该序列号;主循环 `TaskRunner::runPending()` 通过比较已处理序号与当前序号检测是否有新的周期数据待处理。`task_flags` 仅由 preempt≥1 的中断(ESC IRQ、CAN RX、调度器)与主循环访问,BASEPRI 临界区可完全保护其 RMW 原子性。 + +#### Scenario: SYNC0 ISR 不触碰 task_flags +- **WHEN** SYNC0 周期中断触发,`periodicIsr()` 执行完成 +- **THEN** `task_flags` 保持不变,仅 `s_periodicSeq` 递增 + +#### Scenario: 主循环检测周期数据 +- **WHEN** 主循环 `runPending()` 执行,发现 `s_periodicSeq` 与上次记录的已处理序号不同 +- **THEN** 调用 `taskEthercatProcess()` 处理最新 PDO,并更新已处理序号为当前 `s_periodicSeq` + +#### Scenario: ESC IRQ 仍用 TaskManager +- **WHEN** ESC 事件中断(EXTI1,preempt=1)触发 +- **THEN** `onEscIrqExti()` 调用 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,BASEPRI 临界区保护该 RMW,无竞态 + +### Requirement: EXMC 总线访问权分离 +系统 SHALL 将 EXMC 访问接口分为两类: +1. **ISR-only**(无锁):`EscExmcBus::read16/write16/readBlock/writeBlock`、`EscLan9252::readReg/writeReg/readPdi/writePdi`,仅供 preempt=0 的 `periodicIsr()` 调用 +2. **Thread-safe**(PRIMASK 保护):`EscLan9252::safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi`,供主循环与初始化代码调用,内部用 `__disable_irq/__set_PRIMASK` 短暂关中断 + +头文件 SHALL 通过注释明确标注每个接口的调用约束,违反约束(主循环调用 ISR-only 接口)视为编程错误。 + +#### Scenario: 主循环安全访问 ESC 寄存器 +- **WHEN** 主循环需要读取 ESC AL 状态寄存器 +- **THEN** 调用 `EscLan9252::safeReadReg()`,内部 PRIMASK 关中断后访问 EXMC,完成后恢复,期间 SYNC0 ISR 被阻塞但 EXMC 访问原子完成 + +#### Scenario: ISR 无锁访问 PDI +- **WHEN** `periodicIsr()` 读取输入 PDO +- **THEN** 调用 `EscLan9252::readPdi()`(无锁),因 ISR 为 preempt=0,不会被任何中断抢占,EXMC 访问天然原子 + +### Requirement: PDO 三缓冲机制 +系统 SHALL 使用三缓冲(triple buffer)替代原双缓冲实现 PDO 数据交换,容忍主循环处理延迟最多 1 个 EtherCAT 周期。 + +输入路径: +- `s_inputs[3]`,ISR 维护 `s_inputWriteIdx`(0/1/2 模 3 轮转) +- ISR 写入 `s_inputs[s_inputWriteIdx]`,完成后 `s_inputWriteIdx = (s_inputWriteIdx + 1) % 3` +- `getLatestInputs()` 返回 `&s_inputs[(s_inputWriteIdx + 2) % 3]`(最新完成且与 ISR 下次写入隔一个缓冲) + +输出路径: +- `s_outputs[3]`,ISR 维护 `s_outputReadIdx`(0/1/2 模 3 轮转) +- ISR 读取 `s_outputs[s_outputReadIdx]`,完成后 `s_outputReadIdx = (s_outputReadIdx + 1) % 3` +- `setOutputs()` 写入 `s_outputs[(s_outputReadIdx + 1) % 3]`(ISR 下次要读的下一个,与当前读取位置隔一个) + +#### Scenario: 主循环慢于 1 个周期不丢数据 +- **WHEN** 主循环 `getLatestInputs()` 读取缓冲 A 后,在处理期间 ISR 又完成 1 帧(写入 B 并递增 writeIdx) +- **THEN** 主循环继续处理 A 不受影响,A 与 ISR 当前写入位置 C 隔一个缓冲,不会被覆盖 + +#### Scenario: setOutputs 拷贝不被 ISR 抢占破坏 +- **WHEN** 主循环 `setOutputs()` 正在拷贝输出缓冲,SYNC0 ISR 触发 +- **THEN** ISR 读取的 `s_outputReadIdx` 指向与主循环写入位置隔一个的缓冲,两者不冲突,无需任何锁 + +### Requirement: periodicIsr 执行时间测量 +系统 SHALL 在 `periodicIsr()` 中使用 DWT 周期计数器(`DWT_CYCCNT`)测量执行时间,并通过诊断打印输出最大与平均执行时间(单位 µs,基准 180MHz)。测量结果用于验证 preempt=0 ISR 阻塞窗口是否可接受。 + +#### Scenario: 测量并打印 ISR 执行时间 +- **WHEN** 系统运行 EtherCAT 周期通信,诊断打印任务触发 +- **THEN** 输出包含 `periodicIsr max=Xus avg=Yus` 信息,供开发者评估 + +#### Scenario: ISR 执行时间超限告警 +- **WHEN** 实测 `periodicIsr` 最大执行时间 > 5µs +- **THEN** 触发评估:减小 `CONFIG_ETHERCAT_PDO_SIZE` 或优化 EXMC 时序,重新测量直至满足 + +### Requirement: 周期 ISR 调用链 RAM 执行 +系统 SHALL 通过链接脚本定义 `.ramfunc` 输出段定位到 SRAM,并在启动时从 Flash 加载区拷贝到 RAM 执行区。`EthercatSlave::periodicIsr()` 及其直接调用链(`EscLan9252::readPdi/writePdi`、`EscExmcBus::read16/write16`)SHALL 标记 `__attribute__((section(".ramfunc"), noinline))` 放入该段。 + +#### Scenario: .ramfunc 段加载到 RAM +- **WHEN** 编译生成 map 文件 +- **THEN** `.ramfunc` 段的执行地址(VMA)位于 SRAM 区域,加载地址(LMA)位于 Flash 区域,启动代码完成拷贝 + +#### Scenario: 周期 ISR 无 Flash wait state 抖动 +- **WHEN** `periodicIsr()` 连续执行多次,DWT 测量执行时间 +- **THEN** 执行时间波动显著小于 Flash 执行版本,满足 <1µs 抖动目标 + +## MODIFIED Requirements + +### Requirement: EtherCAT 周期数据双缓冲 +(原 spec `add-ethercat-lan9252-realtime` 中的双缓冲要求) +改:由双缓冲(ping-pong)改为三缓冲(triple buffer),容忍主循环跨 1 个周期处理延迟,消除 `getLatestInputs()` 与 `setOutputs()` 的竞态窗口。具体实现见 ADDED Requirements "PDO 三缓冲机制"。 + +### Requirement: 周期 ISR 放 RAM 执行段 +(原 spec `add-ethercat-lan9252-realtime` 中的 RAM 执行段要求) +改:原 spec 仅要求 `periodicIsr()` 标记 section 属性,未要求链接脚本与启动拷贝落地。本 spec 明确要求:链接脚本定义 `.ramfunc` 输出段到 SRAM,启动代码增加拷贝逻辑,且 `periodicIsr()` 的直接调用链(readPdi/writePdi/read16/write16)也需 RAM 化,否则调用链仍在 Flash 执行,抖动目标无法达成。 + +## Assumptions & Decisions + +1. **修复时机**:本 spec 所有改动在 `CONFIG_ETHERCAT_ENABLE=0` 下不触发编译(因 `comm/` 代码被 `#if` 包裹),但改动完成后必须将 `CONFIG_ETHERCAT_ENABLE` 临时设为 1 进行编译验证,验证后恢复为 0 +2. **三缓冲容忍度**:三缓冲容忍主循环跨 1 个周期(即主循环处理时间 ≤ 2 × 周期时间)。100µs 周期下,主循环有 200µs 处理窗口,远超 32 字节 PDO 映射所需(µs 级),实际不会触发容忍上限 +3. **PRIMASK 在主循环 safe 接口中的影响**:主循环访问 ESC 寄存器(AL 状态查询等)不频繁,PRIMASK 关中断时间约 1~2µs(单次寄存器读写),对 EtherCAT 抖动影响可忽略。若后续协议栈需要频繁主循环 EXMC 访问,需重新评估 +4. **DWT 可用性**:GD32E508(Cortex-M4F)支持 DWT,`DWT_CYCCNT` 可用。需在初始化时使能 DWT 与 DEMO 寄存器 +5. **链接脚本格式**:项目使用 AC6(armclang),链接脚本为 `.sct` 格式(SCT 文件)。`.ramfunc` 段定义需符合 SCT 语法 +6. **不包含**:完整 EtherCAT 协议栈集成(SSC)、EEPROM/ESI 配置、应用对象字典——这些由后续 spec 处理,本 spec 仅修复通信基础设施风险 +7. **TASK_ETHERCAT_PROCESS 枚举保留**:虽然周期路径不再使用,但 ESC IRQ 路径仍用,枚举不删除,避免破坏 `task_runner.cpp` 现有分支 diff --git a/.trae/specs/fix-ethercat-realtime-comm-risks/tasks.md b/.trae/specs/fix-ethercat-realtime-comm-risks/tasks.md new file mode 100644 index 0000000..4b42fec --- /dev/null +++ b/.trae/specs/fix-ethercat-realtime-comm-risks/tasks.md @@ -0,0 +1,59 @@ +# Tasks + +- [x] Task 1: 阶段 A — EtherCAT 周期任务标志脱离 TaskManager(修复严重风险 #1) + - [x] SubTask 1.1: 在 `comm/ethercat_slave.h` 中声明 `static volatile uint8_t s_periodicSeq` 与 `static uint8_t s_processedSeq` 私有静态成员,并增加 `static bool hasPendingPeriodic()` / `static void markPeriodicProcessed()` 公有接口 + - [x] SubTask 1.2: 在 `comm/ethercat_slave.cpp` 的 `periodicIsr()` 末尾移除 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,改为 `s_periodicSeq++`(uint8 自然回绕,原子单字节写) + - [x] SubTask 1.3: 实现 `hasPendingPeriodic()` 返回 `s_periodicSeq != s_processedSeq`,`markPeriodicProcessed()` 执行 `s_processedSeq = s_periodicSeq` + - [x] SubTask 1.4: 在 `sys/task_runner.cpp` 的 `runPending()` 开头(TaskManager::takePending 之前或之后)增加:`if (EthercatSlave::hasPendingPeriodic()) { EthercatSlave::markPeriodicProcessed(); taskEthercatProcess(); }`,该分支用 `#if CONFIG_ETHERCAT_ENABLE` 保护 + - [x] SubTask 1.5: 确认 ESC IRQ 路径(`comm/ethercat_sync.cpp::onEscIrqExti()`)仍调用 `TaskManager::set(1UL << TASK_ETHERCAT_PROCESS)`,BASEPRI 保护该路径,无改动 + - [x] SubTask 1.6: 在 `comm/ethercat_slave.h` 头注释中说明"周期路径用 s_periodicSeq,事件路径用 TASK_ETHERCAT_PROCESS,两者独立" + +- [x] Task 2: 阶段 B — EXMC 总线访问权分离(修复严重风险 #2) + - [x] SubTask 2.1: 在 `comm/esc_lan9252.h` 中声明 `static uint16_t safeReadReg(uint16_t addr)` / `static void safeWriteReg(uint16_t addr, uint16_t val)` / `static void safeReadPdi(uint16_t offset, uint8_t *buf, size_t len)` / `static void safeWritePdi(uint16_t offset, const uint8_t *buf, size_t len)` 公有接口 + - [x] SubTask 2.2: 在 `comm/esc_lan9252.cpp` 实现 safe 接口,内部用 `__disable_irq()` / `__set_PRIMASK()` 包裹(注意:这里用 PRIMASK 而非 BASEPRI,因为要阻塞 preempt=0 的 SYNC0),调用对应的无锁版本后恢复 + - [x] SubTask 2.3: 在 `comm/esc_lan9252.h` / `comm/esc_exmc_bus.h` 头文件注释中明确标注:`readReg/writeReg/readPdi/writePdi/read16/write16/readBlock/writeBlock` 为 "ISR-only,仅供 preempt=0 调用,无锁";`safeReadReg/safeWriteReg/safeReadPdi/safeWritePdi` 为 "Thread-safe,主循环与 init 调用,PRIMASK 保护" + - [x] SubTask 2.4: 审查 `comm/ethercat_slave.cpp` 的 `init()` 与 `process()`,所有 EXMC 访问改用 safe 接口(init 阶段虽中断未使能但保持一致性;process 若有 EXMC 访问必须用 safe) + - [x] SubTask 2.5: 在 `comm/ethercat_slave.cpp` 的 `periodicIsr()` 中确认仍调用无锁的 `readPdi/writePdi`(因 ISR 为 preempt=0,天然原子) + - [x] SubTask 2.6: 代码审查确认无主循环路径误调用 ISR-only 接口 + +- [x] Task 3: 阶段 C — PDO 三缓冲替换双缓冲(修复严重风险 #3 与 #4) + - [x] SubTask 3.1: 在 `comm/ethercat_slave.h` 中将 `s_inputs[2]` / `s_outputs[2]` 改为 `s_inputs[3]` / `s_outputs[3]`,成员名改为 `s_inputWriteIdx` / `s_outputReadIdx`(替换原 `s_inputReadIndex` / `s_outputWriteIndex`),类型 `volatile uint8_t` + - [x] SubTask 3.2: 在 `comm/ethercat_slave.h` 头注释中更新双缓冲说明为三缓冲说明,描述模 3 轮转规则 + - [x] SubTask 3.3: 在 `comm/ethercat_slave.cpp` 的 `init()` 中初始化 `s_inputWriteIdx = 0` / `s_outputReadIdx = 0` + - [x] SubTask 3.4: 重写 `periodicIsr()` 的缓冲管理:写入 `s_inputs[s_inputWriteIdx]` → 递增 `s_inputWriteIdx = (s_inputWriteIdx + 1) % 3`;读取 `s_outputs[s_outputReadIdx]` → 递增 `s_outputReadIdx = (s_outputReadIdx + 1) % 3` + - [x] SubTask 3.5: 重写 `getLatestInputs()` 返回 `&s_inputs[(s_inputWriteIdx + 2) % 3]`(最新完成且与 ISR 当前写入隔一个) + - [x] SubTask 3.6: 重写 `setOutputs()` 写入 `s_outputs[(s_outputReadIdx + 1) % 3]`(与 ISR 当前读取隔一个),无需临界区 + - [x] SubTask 3.7: 移除原 XOR 翻转逻辑(`^1U`),确认无残留引用 + - [x] SubTask 3.8: 在 `comm/ethercat_slave.h` 注释中明确"三缓冲容忍主循环跨 1 个周期,主循环处理必须 < 2 × 周期时间" + +- [x] Task 4: 阶段 D — periodicIsr 执行时间测量与评估(修复高风险 #5) + - [x] SubTask 4.1: 在 `comm/ethercat_slave.h` 增加 `static volatile uint32_t s_isrMaxCycles` / `static volatile uint32_t s_isrAvgCycles` / `static volatile uint32_t s_isrCount` 诊断成员 + - [x] SubTask 4.2: 在 `comm/ethercat_slave.cpp` 的 `periodicIsr()` 入口读 `DWT->CYCCNT` 存局部变量,出口再读,差值更新 max(原子比较)与 avg(滑动平均或累加) + - [x] SubTask 4.3: 在 `sys/initial.cpp` 或 `comm/esc_lan9252.cpp::init()` 中使能 DWT:`CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;` + - [x] SubTask 4.4: 在 `sys/task_runner.cpp` 的 `taskDiagnosticPrint()` 中增加 EtherCAT ISR 执行时间输出(受 `#if CONFIG_ETHERCAT_ENABLE` 保护),格式 `periodicIsr max=Xus avg=Yus`(cycles / 180 转 µs) + - [x] SubTask 4.5: 在 `sys/config.h` 增加 `CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD 5U` 宏,作为评估阈值 + - [x] SubTask 4.6: 在 spec.md 与代码注释中记录:若实测 > 5µs 需评估减小 `CONFIG_ETHERCAT_PDO_SIZE` 或优化 EXMC 时序;若 ≤ 5µs 则接受现状并在注释中说明"preempt=0 阻塞窗口 ≤ 5µs,CAN/SPI 需容忍" + +- [x] Task 5: 阶段 E — .ramfunc 段落地与调用链 RAM 化(修复高风险 #6) + - [x] SubTask 5.1: 定位项目链接脚本文件(查找 `*.sct` 或 RTE 配置中的 linker script) + - [x] SubTask 5.2: 在链接脚本中新增 `.ramfunc` 输出段,执行区(VMA)位于 SRAM,加载区(LMA)位于 Flash(参考 `.data` 段的 scatter 语法) + - [x] SubTask 5.3: 确认启动代码(`startup_gd32e50x_cl.s` 或 `system_gd32e50x.c`)是否自动拷贝 `.ramfunc` 段;若否,增加拷贝逻辑(类似 `.data` 拷贝循环) + - [x] SubTask 5.4: 在 `comm/ethercat_slave.cpp` 的 `periodicIsr()` 已有 `__attribute__((section(".ramfunc")))` 基础上增加 `noinline` 属性 + - [x] SubTask 5.5: 在 `comm/esc_lan9252.cpp` 的 `readPdi()` / `writePdi()` 增加 `__attribute__((section(".ramfunc"), noinline))` + - [x] SubTask 5.6: 在 `comm/esc_exmc_bus.cpp` 的 `read16()` / `write16()` 增加 `__attribute__((section(".ramfunc"), noinline))`(注意:`readBlock/writeBlock` 不在 ISR 调用链,不标记) + - [ ] SubTask 5.7: 编译后查看 map 文件,确认 `.ramfunc` 段的 VMA 在 SRAM 区域、LMA 在 Flash 区域 + - [x] SubTask 5.8: 若 DWT 测量(阶段 D)显示 Flash 执行已满足 <1µs 抖动,可在 spec 中记录降级方案(仅 periodicIsr 本体 RAM 化),但默认按完整 RAM 化实施 + +- [ ] Task 6: 编译验证与回退(⚠️ 受阻于预存 EXMC 组件解析问题) + - [ ] SubTask 6.1: 临时将 `sys/config.h` 中 `CONFIG_ETHERCAT_ENABLE` 改为 1,全量编译通过,无 error + - [ ] SubTask 6.2: 检查无新增 warning(特别注意 `.ramfunc` 段未定义或未拷贝的链接警告) + - [ ] SubTask 6.3: 查看 map 文件确认 `.ramfunc` 段在 SRAM、`s_inputs[3]`/`s_outputs[3]` 在 SRAM、`s_periodicSeq` 在 SRAM + - [x] SubTask 6.4: 编译验证完成后将 `CONFIG_ETHERCAT_ENABLE` 恢复为 0 + - [ ] SubTask 6.5: 确认 `CONFIG_ETHERCAT_ENABLE=0` 时全量编译仍通过,功能与改造前等价 + - [ ] SubTask 6.6: ⚠️ 阻塞项:`GD32E508_DCU_APP.cproject.yml` 第 68 行无条件引用 `Device:GD32E50x_StdPeripherals:EXMC` 组件,但已安装的 GD32E50x_DFP pack(1.9.0/1.3.2)pdsc 未注册 EXMC 组件,导致 csolution 组件解析失败,ENABLE=0 与 ENABLE=1 均无法构建。EXMC 驱动源文件(`gd32e50x_exmc.h/.c`)实际存在于 pack 中。需单独修复 cproject.yml(改为直接引用源文件)或更新 pack 后重新执行本任务 + +# Task Dependencies +- Task 1, 2, 3 相互独立,可并行实施(均集中在 `comm/` 目录,但修改不同函数与成员) +- Task 4 依赖 Task 1(periodicIsr 改造完成才能测量) +- Task 5 依赖 Task 1, 2, 3(调用链 RAM 化需在接口稳定后) +- Task 6 依赖 Task 1-5 全部完成 diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 0ec4518..b8a4e5d 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -1,4 +1,4 @@ -# A project translates into one executable or library. +# A project translates into one executable or library. project: groups: @@ -44,6 +44,8 @@ project: - file: ./device/spi/app_spi_cmd_ota_wait.cpp - file: ./device/spi/app_spi_cmd_ping.cpp - file: ./device/spi/app_spi_cmd_can_bridge.cpp + - file: ./device/spi/app_spi_cmd_can_feedback.cpp + - file: ./device/spi/app_spi_cmd_id_snapshot.cpp - group: comm files: - file: ./comm/esc_exmc_bus.h @@ -65,7 +67,6 @@ project: - component: Device:GD32E50x_StdPeripherals:CRC - component: Device:GD32E50x_StdPeripherals:DBG - component: Device:GD32E50x_StdPeripherals:DMA - - component: Device:GD32E50x_StdPeripherals:EXMC - component: Device:GD32E50x_StdPeripherals:EXTI - component: Device:GD32E50x_StdPeripherals:FMC - component: Device:GD32E50x_StdPeripherals:GPIO diff --git a/GD32E508_DCU_APP/RTE/Device/GD32E508VE/ac6_linker_script.sct.src b/GD32E508_DCU_APP/RTE/Device/GD32E508VE/ac6_linker_script.sct.src index 7820e1f..a8f9529 100644 --- a/GD32E508_DCU_APP/RTE/Device/GD32E508VE/ac6_linker_script.sct.src +++ b/GD32E508_DCU_APP/RTE/Device/GD32E508VE/ac6_linker_script.sct.src @@ -34,7 +34,11 @@ LR_ROM0 __ROM0_BASE __ROM0_SIZE { ER_ROM0 __ROM0_BASE __ROM0_SIZE { *.o (RESET, +First) *(InRoot$$Sections) - *(+RO +XO) + ; 用 .ANY (+RO +XO) 兜底所有 RO 段,而非 *(+RO +XO) 属性通配符。 + ; 原因:*(+RO) 与 RW_RAM0 的 *(.ramfunc*) 跨执行区会产生 L6223E 歧义; + ; 而 .ANY 优先级低于 *(.ramfunc*),armlink 先把 .ramfunc 分配给 RW_RAM0, + ; 剩余 RO 段再由 .ANY 兜底到 ER_ROM0,既无歧义又零丢弃(不依赖段名枚举)。 + .ANY (+RO +XO) } #if defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) @@ -49,6 +53,9 @@ LR_ROM0 __ROM0_BASE __ROM0_SIZE { } RW_RAM0 AlignExpr(+0, 8) (__RAM0_SIZE - __HEAP_SIZE - __STACK_SIZE - __STACKSEAL_SIZE - AlignExpr(ImageLength(RW_NOINIT), 8)) { + ; RAM 函数段:VMA 在 SRAM、LMA 在 Flash,由 C 库 __main scatter-load 自动从 Flash 拷贝到 RAM。 + ; 用于 EtherCAT ISR 调用链(periodicIsr/readPdi/writePdi/read16/write16),消除 Flash 等待周期抖动。 + *(.ramfunc*) *(+RW +ZI) } diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp index 4124ff7..b6bdede 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp @@ -5,8 +5,6 @@ #include "comm/esc_exmc_bus.h" #include "sys/config.h" -#if CONFIG_ETHERCAT_ENABLE - extern "C" { #include "gd32e50x_rcu.h" #include "gd32e50x_gpio.h" @@ -68,32 +66,18 @@ void EscExmcBus::init() exmc_norsram_init(&norsram_init); exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION1); #else - /* EXMC 外设库未安装:仅占位,CONFIG_ETHERCAT_ENABLE=1 时需先安装 EXMC 组件。 */ + /* EXMC 外设库未安装:仅占位,启用 EtherCAT 时需先安装 EXMC 组件。 */ #endif } +__attribute__((section(".ramfunc"), noinline)) uint16_t EscExmcBus::read16(uint32_t offset) { return ESC_EXMC_REG16(offset); } +__attribute__((section(".ramfunc"), noinline)) void EscExmcBus::write16(uint32_t offset, uint16_t data) { ESC_EXMC_REG16(offset) = data; } - -void EscExmcBus::readBlock(uint32_t offset, uint16_t *buf, size_t count) -{ - for (size_t i = 0U; i < count; ++i) { - buf[i] = ESC_EXMC_REG16(offset + (uint32_t)(i * 2U)); - } -} - -void EscExmcBus::writeBlock(uint32_t offset, const uint16_t *buf, size_t count) -{ - for (size_t i = 0U; i < count; ++i) { - ESC_EXMC_REG16(offset + (uint32_t)(i * 2U)) = buf[i]; - } -} - -#endif /* CONFIG_ETHERCAT_ENABLE */ diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.h b/GD32E508_DCU_APP/comm/esc_exmc_bus.h index bfa321e..4ce6d38 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.h +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.h @@ -19,35 +19,11 @@ public: /** @brief 初始化 EXMC NOR/SRAM Region1 与全部 GPIO(AF 复用)。 */ static void init(); - /** - * @brief 从指定偏移读取 16-bit 数据。 - * @param[in] offset 地址偏移(字节)。 - * @return uint16_t 读取的 16-bit 数据。 - */ + /** @brief ISR-only:仅供 preempt=0 调用,无锁。 */ static uint16_t read16(uint32_t offset); - /** - * @brief 向指定偏移写入 16-bit 数据。 - * @param[in] offset 地址偏移(字节)。 - * @param[in] data 待写入的 16-bit 数据。 - */ + /** @brief ISR-only:仅供 preempt=0 调用,无锁。 */ static void write16(uint32_t offset, uint16_t data); - - /** - * @brief 从指定偏移连续读取一块 16-bit 数据。 - * @param[in] offset 地址偏移(字节)。 - * @param[out] buf 输出缓冲(以 uint16_t 为单位)。 - * @param[in] count 读取的 16-bit 半字数量。 - */ - static void readBlock(uint32_t offset, uint16_t *buf, size_t count); - - /** - * @brief 向指定偏移连续写入一块 16-bit 数据。 - * @param[in] offset 地址偏移(字节)。 - * @param[in] buf 数据缓冲(以 uint16_t 为单位)。 - * @param[in] count 写入的 16-bit 半字数量。 - */ - static void writeBlock(uint32_t offset, const uint16_t *buf, size_t count); }; #endif /* COMM_ESC_EXMC_BUS_H */ diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.cpp b/GD32E508_DCU_APP/comm/esc_lan9252.cpp index 0400cc9..adade4e 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.cpp +++ b/GD32E508_DCU_APP/comm/esc_lan9252.cpp @@ -5,8 +5,8 @@ #include "comm/esc_lan9252.h" #include "comm/esc_exmc_bus.h" #include "sys/config.h" - -#if CONFIG_ETHERCAT_ENABLE +#include "RTE_Components.h" +#include CMSIS_device_header namespace { /* LAN9252 ESC 关键寄存器地址(字节偏移,EXMC 访问) */ @@ -20,16 +20,55 @@ constexpr uint16_t LAN9252_PDI_BASE_OFFSET = 0x1000U; void EscLan9252::init() { + /* 使能 DWT 周期计数器,供 periodicIsr 执行时间测量 */ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0U; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + EscExmcBus::init(); - /* TODO: 配置 ESC 中断使能、AL 控制等基本寄存器(留待协议栈集成阶段实现) */ + + /* ============================================================ + * TODO: 完整 ESC 寄存器配置(需 SSC 或等价协议栈代码,本 spec 不实现) + * ============================================================ + * 当前仅完成 EXMC 总线初始化与芯片 ID 读取。要使 SYNC0 实际触发,还需配置: + * + * 1. SyncManager(SM)初始化: + * - SM0/SM1:邮箱输入/输出(IN/OUT),地址 0x0520-0x0530 + * - SM2:TxPDO(输入),地址 0x0530,配置为 3-buffer 模式 + * - SM3:RxPDO(输出),地址 0x0540,配置为 3-buffer 模式 + * - 各 SM 的 StartAddr/Length/Control/Status/Activate 寄存器 + * + * 2. FMMU(Fieldbus Memory Mapping Unit)配置: + * - FMMU0:TxPDO 映射(逻辑地址 → 物理地址,读) + * - FMMU1:RxPDO 映射(逻辑地址 → 物理地址,写) + * - 地址 0x0580-0x0590 + * + * 3. AL Control(0x0120):写入 0x08 请求 OP 状态(需主站配合完成状态迁移) + * + * 4. SYNC0 生成寄存器: + * - 0x0981-0x0982:SYNC0 周期(cycle time,单位 ns) + * - 0x0983:SYNC0 脉冲长度 + * - 0x0984:SYNC0 控制(使能/触发模式) + * - 需主站 DC 同步后由 ESC 自动生成 SYNC0 脉冲 + * + * 这些步骤通常由 EtherCAT SSC(Slave Stack Code)或等价协议栈代码完成, + * 涉及 AL 状态机管理、邮箱通信、PDO 映射协商等复杂逻辑。 + * 本 spec 仅通过 EthercatSlave::isEscReady() 的 AL 状态守卫使 + * "SYNC0 不会触发"在运行期可观测,完整配置留待 SSC 集成 spec。 + * ============================================================ */ } bool EscLan9252::selfTest() { - const uint16_t id_low = readReg(LAN9252_REG_CHIP_ID_L); - const uint16_t id_high = readReg(LAN9252_REG_CHIP_ID_H); - const uint32_t chip_id = ((uint32_t)id_high << 16) | id_low; - return (chip_id == LAN9252_CHIP_ID_VALUE); + return (readChipId() == LAN9252_CHIP_ID_VALUE); +} + +uint32_t EscLan9252::readChipId() +{ + /* INIT-ONLY:init 阶段中断未使能,但仍走 initOnly 路径保持一致性 */ + const uint16_t id_low = initOnlyReadReg(LAN9252_REG_CHIP_ID_L); + const uint16_t id_high = initOnlyReadReg(LAN9252_REG_CHIP_ID_H); + return ((uint32_t)id_high << 16) | id_low; } uint16_t EscLan9252::readReg(uint16_t addr) @@ -43,6 +82,7 @@ void EscLan9252::writeReg(uint16_t addr, uint16_t val) EscExmcBus::write16((uint32_t)addr << 1U, val); } +__attribute__((section(".ramfunc"), noinline)) void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len) { if (buf == nullptr || len == 0U) { @@ -63,6 +103,7 @@ void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len) } } +__attribute__((section(".ramfunc"), noinline)) void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len) { if (buf == nullptr || len == 0U) { @@ -79,4 +120,25 @@ void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len) } } -#endif /* CONFIG_ETHERCAT_ENABLE */ +/* ---- initOnly* 接口:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用 ---- + * 运行期禁止调用;需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 + * 注:不再使用 PRIMASK 关中断(违反项目硬约束"Critical sections MUST use BASEPRI")。 */ +uint16_t EscLan9252::initOnlyReadReg(uint16_t addr) +{ + return readReg(addr); +} + +void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t val) +{ + writeReg(addr, val); +} + +void EscLan9252::initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len) +{ + readPdi(offset, buf, len); +} + +void EscLan9252::initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len) +{ + writePdi(offset, buf, len); +} diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.h b/GD32E508_DCU_APP/comm/esc_lan9252.h index 908f26f..3b79f67 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.h +++ b/GD32E508_DCU_APP/comm/esc_lan9252.h @@ -2,6 +2,12 @@ * @file esc_lan9252.h * @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。 * @note 通过 EscExmcBus 16-bit 并行总线访问 LAN9252 寄存器与 PDI 内存。 + * + * 接口调用约束(避免 EXMC 总线争用): + * - readReg/writeReg/readPdi/writePdi:ISR-only,仅供 preempt=0 的 periodicIsr() 调用,无锁。 + * - initOnlyReadReg/initOnlyWriteReg/initOnlyReadPdi/initOnlyWritePdi:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 + * + * PDI offset 语义:offset 参数为 SyncManager 内存区相对偏移,输入 PDO(TxPDO)与输出 PDO(RxPDO)必须使用不同 offset,参见 CONFIG_ESC_PDI_INPUT_OFFSET / CONFIG_ESC_PDI_OUTPUT_OFFSET。 */ #ifndef COMM_ESC_LAN9252_H #define COMM_ESC_LAN9252_H @@ -25,34 +31,34 @@ public: static bool selfTest(); /** - * @brief 读取 ESC 寄存器(16-bit)。 - * @param[in] addr 寄存器地址(ESC 寄存器空间偏移)。 - * @return uint16_t 寄存器值。 + * @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。 + * @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。 */ + static uint32_t readChipId(); + + /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static uint16_t readReg(uint16_t addr); - /** - * @brief 写入 ESC 寄存器(16-bit)。 - * @param[in] addr 寄存器地址。 - * @param[in] val 待写入值。 - */ + /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static void writeReg(uint16_t addr, uint16_t val); - /** - * @brief 从 PDI 内存读取一块数据。 - * @param[in] offset PDI 偏移(字节)。 - * @param[out] buf 输出缓冲。 - * @param[in] len 读取字节数。 - */ + /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static void readPdi(uint16_t offset, uint8_t *buf, size_t len); - /** - * @brief 向 PDI 内存写入一块数据。 - * @param[in] offset PDI 偏移(字节)。 - * @param[in] buf 数据缓冲。 - * @param[in] len 写入字节数。 - */ + /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static void writePdi(uint16_t offset, const uint8_t *buf, size_t len); + + /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ + static uint16_t initOnlyReadReg(uint16_t addr); + + /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ + static void initOnlyWriteReg(uint16_t addr, uint16_t val); + + /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ + static void initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len); + + /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ + static void initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len); }; #endif /* COMM_ESC_LAN9252_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_slave.cpp b/GD32E508_DCU_APP/comm/ethercat_slave.cpp index 7fb90d6..8590724 100644 --- a/GD32E508_DCU_APP/comm/ethercat_slave.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_slave.cpp @@ -8,46 +8,92 @@ #include "sys/task_manager.h" #include "main.h" -#if CONFIG_ETHERCAT_ENABLE +PdoBuffer EthercatSlave::s_inputs[3] = {}; +PdoBuffer EthercatSlave::s_outputs[3] = {}; +PdoBuffer EthercatSlave::s_appInputs = {}; +PdoBuffer EthercatSlave::s_appOutputs = {}; +volatile uint8_t EthercatSlave::s_inputWriteIdx = 0U; +volatile uint8_t EthercatSlave::s_outputReadIdx = 0U; +volatile uint16_t EthercatSlave::s_periodicSeq = 0U; +uint16_t EthercatSlave::s_processedSeq = 0U; +volatile uint32_t EthercatSlave::s_isrMaxCycles = 0U; +volatile uint32_t EthercatSlave::s_isrAvgCycles = 0U; +volatile uint32_t EthercatSlave::s_isrCount = 0U; +bool EthercatSlave::s_escReady = false; +uint32_t EthercatSlave::s_chipId = 0U; +uint8_t EthercatSlave::s_alState = 0U; -PdoBuffer EthercatSlave::s_inputs[2] = {}; -PdoBuffer EthercatSlave::s_outputs[2] = {}; -volatile uint8_t EthercatSlave::s_inputReadIndex = 0U; -volatile uint8_t EthercatSlave::s_outputWriteIndex = 0U; +/* ISR 执行时间评估: + * - 若 DWT 实测 max > CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD(5µs),需评估: + * a) 减小 CONFIG_ETHERCAT_PDO_SIZE(默认 32 字节) + * b) 优化 EXMC 时序参数(esc_exmc_bus.cpp 中 read_latency/write_latency) + * - 若 max ≤ 5µs,接受现状;preempt=0 阻塞窗口 ≤ 5µs,CAN/SPI 中断需容忍此窗口。 + * SPI0 从机在此窗口内不会 RX 溢出(假设 SPI 时钟 ≤ 10MHz,5µs ≥ 50 bit,远小于一帧)。 */ void EthercatSlave::init() { EscLan9252::init(); - if (EscLan9252::selfTest()) { - /* ESC 自检通过:TODO 启用周期同步 */ + + /* 第 1 层调试可观测性:读取芯片 ID 并打印 selfTest 结果 */ + s_chipId = EscLan9252::readChipId(); + const bool chipIdOk = (s_chipId == 0x87654321U); + + /* AL 状态守卫:仅在芯片 ID 匹配时读取 AL 状态(ID 不匹配时 EXMC 可能读回 0/0xFFFF,无意义)。 + * AL ≥ SAFEOP(0x04) 才认为 SYNC0 可触发;完整 ESC 配置(SM/FMMU/AL Control/SYNC0)需 SSC 集成。 */ + s_alState = 0U; + if (chipIdOk) { + s_alState = (uint8_t)(EscLan9252::initOnlyReadReg(0x0130U) & 0x000FU); } - s_inputReadIndex = 0U; - s_outputWriteIndex = 0U; + s_escReady = chipIdOk && (s_alState >= AL_STATE_SAFEOP); + + if (!chipIdOk) { + printf("[ECAT] ESC chip id=0x%08lX selfTest=FAIL\r\n", (unsigned long)s_chipId); + printf("[ECAT] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n"); + } else if (s_alState < AL_STATE_SAFEOP) { + printf("[ECAT] ESC chip id=0x%08lX selfTest=PASS, but AL state=0x%02X < SAFEOP(0x%02X)\r\n", + (unsigned long)s_chipId, (unsigned)s_alState, (unsigned)AL_STATE_SAFEOP); + printf("[ECAT] SYNC0 will not fire, full ESC config requires SSC integration\r\n"); + } else { + printf("[ECAT] ESC ready, chip id=0x%08lX, AL state=0x%02X\r\n", + (unsigned long)s_chipId, (unsigned)s_alState); + } + + s_inputWriteIdx = 0U; + s_outputReadIdx = 0U; + s_periodicSeq = 0U; + s_processedSeq = 0U; } -__attribute__((section(".ramfunc"))) +__attribute__((section(".ramfunc"), noinline)) void EthercatSlave::periodicIsr() { - /* 1. 读取输入 PDO 到另一块缓冲(当前可读缓冲的配对) */ - const uint8_t write_idx = s_inputReadIndex ^ 1U; - EscLan9252::readPdi(0x0000U, s_inputs[write_idx].data, CONFIG_ETHERCAT_PDO_SIZE); - s_inputs[write_idx].len = CONFIG_ETHERCAT_PDO_SIZE; + const uint32_t cycStart = DWT->CYCCNT; - /* 2. 写入输出 PDO(从应用层未写入的那个缓冲读取) */ - const uint8_t out_read_idx = s_outputWriteIndex ^ 1U; - EscLan9252::writePdi(0x0000U, s_outputs[out_read_idx].data, s_outputs[out_read_idx].len); + /* 1. 读取输入 PDO 到当前写入缓冲 */ + EscLan9252::readPdi(CONFIG_ESC_PDI_INPUT_OFFSET, s_inputs[s_inputWriteIdx].data, CONFIG_ETHERCAT_PDO_SIZE); + s_inputs[s_inputWriteIdx].len = CONFIG_ETHERCAT_PDO_SIZE; + s_inputWriteIdx = (s_inputWriteIdx + 1U) % 3U; - /* 3. 翻转输入缓冲指针,使新完成的缓冲成为可读缓冲 */ - s_inputReadIndex = write_idx; + /* 2. 写入输出 PDO(从当前读取缓冲) */ + EscLan9252::writePdi(CONFIG_ESC_PDI_OUTPUT_OFFSET, s_outputs[s_outputReadIdx].data, s_outputs[s_outputReadIdx].len); + s_outputReadIdx = (s_outputReadIdx + 1U) % 3U; - /* 4. 置位非实时任务,通知主循环处理应用层 PDO 映射 */ - TaskManager::set(1UL << TASK_ETHERCAT_PROCESS); + /* 3. 递增周期序列号通知主循环(替代 TaskManager::set,避免 BASEPRI 竞态) */ + s_periodicSeq++; + + const uint32_t elapsed = DWT->CYCCNT - cycStart; + s_isrCount++; + if (elapsed > s_isrMaxCycles) { + s_isrMaxCycles = elapsed; + } + /* 滑动平均:avg = avg + (elapsed - avg) / 16 (近似 EMA,避免除法精度问题) */ + s_isrAvgCycles = s_isrAvgCycles + ((int32_t)elapsed - (int32_t)s_isrAvgCycles) / 16; } const PdoBuffer* EthercatSlave::getLatestInputs() { - /* s_inputReadIndex 始终指向最新完成的输入缓冲 */ - return &s_inputs[s_inputReadIndex]; + /* 返回最新完成的缓冲 = (writeIdx + 2) % 3,与 ISR 当前写入位置隔一个 */ + return &s_inputs[(s_inputWriteIdx + 2U) % 3U]; } void EthercatSlave::setOutputs(const PdoBuffer *outputs) @@ -55,16 +101,53 @@ void EthercatSlave::setOutputs(const PdoBuffer *outputs) if (outputs == nullptr) { return; } - /* 写入应用层缓冲(ISR 当前不读取的那个) */ - const uint8_t write_idx = s_outputWriteIndex ^ 1U; + /* 写入与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限。 + * 2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间。 */ + const uint8_t write_idx = (s_outputReadIdx + 2U) % 3U; s_outputs[write_idx] = *outputs; - s_outputWriteIndex = write_idx; } void EthercatSlave::process() { - /* 应用层 PDO 映射入口(非实时,由主循环 TASK_ETHERCAT_PROCESS 调用)。 - * TODO: 将 getLatestInputs() 数据映射到应用对象字典;将应用输出填入 setOutputs()。 */ + /* 应用层 PDO 桥接(非实时,由主循环周期序列号或 ESC IRQ 触发)。 + * 运行期 PDO 交换仅通过 getLatestInputs()/setOutputs() 三缓冲进行;ESC 寄存器访问仅限 init 阶段。 + * + * 框架级桥接:从三缓冲取最新输入到 s_appInputs,把 s_appOutputs 推回三缓冲。 + * TODO: 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点), + * 由后续应用层 spec 定义;应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */ + const PdoBuffer *latest = getLatestInputs(); + if (latest != nullptr) { + s_appInputs = *latest; + } + setOutputs(&s_appOutputs); } -#endif /* CONFIG_ETHERCAT_ENABLE */ +const PdoBuffer* EthercatSlave::getApplicationInputs() +{ + return &s_appInputs; +} + +void EthercatSlave::setApplicationOutputs(const PdoBuffer *outputs) +{ + if (outputs == nullptr) { + return; + } + s_appOutputs = *outputs; +} + +bool EthercatSlave::hasPendingPeriodic() +{ + return s_periodicSeq != s_processedSeq; +} + +void EthercatSlave::markPeriodicProcessed() +{ + s_processedSeq = s_periodicSeq; +} + +void EthercatSlave::getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count) +{ + if (maxCycles) *maxCycles = s_isrMaxCycles; + if (avgCycles) *avgCycles = s_isrAvgCycles; + if (count) *count = s_isrCount; +} diff --git a/GD32E508_DCU_APP/comm/ethercat_slave.h b/GD32E508_DCU_APP/comm/ethercat_slave.h index fff688e..f969bc0 100644 --- a/GD32E508_DCU_APP/comm/ethercat_slave.h +++ b/GD32E508_DCU_APP/comm/ethercat_slave.h @@ -1,8 +1,13 @@ /** * @file ethercat_slave.h - * @brief EtherCAT 从站周期处理接口与 PDO 双缓冲(ping-pong)结构。 + * @brief EtherCAT 从站周期处理接口与 PDO 三缓冲(triple buffer)结构。 * @note 周期 ISR 调用 periodicIsr() 完成输入 PDO 读取 + 输出 PDO 写入; * 应用层通过 getLatestInputs()/setOutputs() 与周期路径无锁交换数据。 + * + * 通知路径分离: + * 周期路径(periodicIsr)用 s_periodicSeq 序列号通知主循环; + * 事件路径(ESC IRQ)用 TASK_ETHERCAT_PROCESS 标志。 + * 两者独立,避免 preempt=0 ISR 与 BASEPRI 临界区的 RMW 竞态。 */ #ifndef COMM_ETHERCAT_SLAVE_H #define COMM_ETHERCAT_SLAVE_H @@ -13,9 +18,16 @@ /** @brief 默认 PDO 大小(可按需调整)。 */ constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U; +/** @brief EtherCAT AL 状态码(EtherCAT 协议定义,ESC AL Status 寄存器低 4 位)。 */ +constexpr uint8_t AL_STATE_INIT = 0x01U; /**< INIT 状态。 */ +constexpr uint8_t AL_STATE_PREOP = 0x02U; /**< PREOP 状态。 */ +constexpr uint8_t AL_STATE_BOOT = 0x03U; /**< BOOT 状态。 */ +constexpr uint8_t AL_STATE_SAFEOP = 0x04U; /**< SAFEOP 状态(SYNC0 可触发下限)。 */ +constexpr uint8_t AL_STATE_OP = 0x08U; /**< OP 状态。 */ + /** * @brief PDO 数据缓冲(输入/输出共用结构)。 - * @note 周期 ISR 与应用层通过双缓冲无锁交换。 + * @note 周期 ISR 与应用层通过三缓冲无锁交换。 */ struct PdoBuffer { uint8_t data[CONFIG_ETHERCAT_PDO_SIZE] = {0}; /**< 数据载荷。 */ @@ -24,44 +36,111 @@ struct PdoBuffer { /** * @brief EtherCAT 从站周期处理。 - * @note 周期路径(periodicIsr)必须极简:读输入→写输出→翻转指针→置位任务。 + * @note 周期路径(periodicIsr)必须极简:读输入→写输出→模 3 轮转指针→递增序列号。 */ class EthercatSlave { public: - /** @brief 初始化 ESC 与双缓冲。 */ + /** @brief 初始化 ESC 与三缓冲。 */ static void init(); /** * @brief 周期中断处理(由 SYNC0 EXTI 触发)。 * @note 放入 RAM 执行段(.ramfunc)以降低 Flash 等待带来的抖动。 - * 流程:读输入 PDO → 写输出 PDO → 翻转输入缓冲指针 → 置位 TASK_ETHERCAT_PROCESS。 + * 流程:读输入 PDO → 写输出 PDO → 模 3 轮转索引 → 递增 s_periodicSeq。 + * 不再调用 TaskManager::set,避免 preempt=0 与 BASEPRI 临界区的 RMW 竞态。 */ static void periodicIsr(); /** * @brief 获取最新输入 PDO(主循环调用,无锁)。 - * @return const PdoBuffer* 最新输入缓冲只读指针;周期 ISR 不会修改该缓冲。 + * @return const PdoBuffer* 最新输入缓冲只读指针;返回与 ISR 当前写入位置隔一个的缓冲。 + * @note 三缓冲容忍主循环跨 1 个周期,主循环处理必须 < 2 × 周期时间。 */ static const PdoBuffer* getLatestInputs(); /** * @brief 设置下次周期要发送的输出 PDO(主循环调用)。 * @param[in] outputs 输出数据指针(内容会被拷贝到内部输出缓冲)。 - * @note 调用方在 ISR 不持有期间调用即可,内部使用乒乓缓冲避免竞争。 + * @note 写入与 ISR 当前读取位置隔一个的缓冲,无需任何临界区。 */ static void setOutputs(const PdoBuffer *outputs); /** - * @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 调用)。 + * @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 或周期序列号触发)。 * @note 留接口,具体映射逻辑由协议栈后续实现。 */ static void process(); + /** + * @brief 获取应用层输入 PDO 快照(主循环调用)。 + * @return const PdoBuffer* 最近一次 process() 拷贝的输入快照只读指针。 + * @note 返回的指针在下次 process() 调用前有效;应用层应在 process() 后立即读取。 + */ + static const PdoBuffer* getApplicationInputs(); + + /** + * @brief 设置应用层输出 PDO 快照(主循环调用)。 + * @param[in] outputs 输出数据指针(内容会被拷贝到内部快照)。 + * @note 下次 process() 调用时,内部快照通过 setOutputs() 推入三缓冲。 + */ + static void setApplicationOutputs(const PdoBuffer *outputs); + + /** + * @brief 检查是否有新的周期数据待主循环处理。 + * @return bool true 表示 s_periodicSeq 与已处理序号不同。 + * @note 主循环 runPending() 优先调用此接口,替代原 TASK_ETHERCAT_PROCESS 周期路径。 + */ + static bool hasPendingPeriodic(); + + /** + * @brief 标记当前周期数据已被主循环处理。 + * @note 调用此接口前应已调用 taskEthercatProcess() 完成本次 PDO 映射。 + */ + static void markPeriodicProcessed(); + + /** @brief 读取 ISR 执行时间诊断(max/avg 单位:CPU 周期数,除以 180 得 µs)。 */ + static void getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count); + + /** @brief ESC 自检是否通过(init 后可查询,用于决定是否使能同步中断)。 */ + static bool isEscReady() { return s_escReady; } + + /** @brief 读取 ESC 芯片 ID(供诊断打印;未就绪时为上次读到的值)。 */ + static uint32_t getChipId() { return s_chipId; } + + /** @brief 读取 ESC AL 状态码(供诊断打印与 SSC 集成;未就绪时为上次读到的值)。 */ + static uint8_t getAlState() { return s_alState; } + private: - static PdoBuffer s_inputs[2]; /**< 输入双缓冲(pong-pong)。 */ - static PdoBuffer s_outputs[2]; /**< 输出双缓冲(ping-pong)。 */ - static volatile uint8_t s_inputReadIndex; /**< ISR 当前写入索引;getLatestInputs 读另一个。 */ - static volatile uint8_t s_outputWriteIndex; /**< 应用层写入索引;ISR 读另一个。 */ + /** + * @brief 输入三缓冲。 + * ISR 按 s_inputWriteIdx 模 3 轮转写入;getLatestInputs() 读取 (writeIdx + 2) % 3。 + */ + static PdoBuffer s_inputs[3]; + /** + * @brief 输出三缓冲。 + * ISR 按 s_outputReadIdx 模 3 轮转读取;setOutputs() 写入 (readIdx + 2) % 3, + * 与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限消除拷贝竞态。 + */ + static PdoBuffer s_outputs[3]; + /** @brief 应用层输入快照(主循环访问,process() 从三缓冲拷贝;无需三缓冲)。 */ + static PdoBuffer s_appInputs; + /** @brief 应用层输出快照(主循环访问,process() 推入三缓冲;无需三缓冲)。 */ + static PdoBuffer s_appOutputs; + static volatile uint8_t s_inputWriteIdx; /**< ISR 当前写入索引;模 3 轮转。 */ + static volatile uint8_t s_outputReadIdx; /**< ISR 当前读取索引;模 3 轮转。 */ + + /** @brief 周期路径通知序列号(ISR 单字原子递增,主循环比对;uint16_t 提供 6.55s 回绕周期)。 */ + static volatile uint16_t s_periodicSeq; + /** @brief 主循环已处理序号(由 hasPendingPeriodic/markPeriodicProcessed 维护)。 */ + static uint16_t s_processedSeq; + + static volatile uint32_t s_isrMaxCycles; /**< ISR 最大执行周期数(180MHz 基准)。 */ + static volatile uint32_t s_isrAvgCycles; /**< ISR 平均执行周期数(滑动累计)。 */ + static volatile uint32_t s_isrCount; /**< ISR 执行次数计数。 */ + + static bool s_escReady; /**< ESC 自检结果(init 后置位)。 */ + static uint32_t s_chipId; /**< ESC 芯片 ID(诊断用)。 */ + static uint8_t s_alState; /**< ESC AL 状态码(init 后可查询,供 isEscReady 守卫)。 */ }; #endif /* COMM_ETHERCAT_SLAVE_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_sync.cpp b/GD32E508_DCU_APP/comm/ethercat_sync.cpp index 6163017..425dbf5 100644 --- a/GD32E508_DCU_APP/comm/ethercat_sync.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_sync.cpp @@ -8,8 +8,6 @@ #include "sys/task_manager.h" #include "main.h" -#if CONFIG_ETHERCAT_ENABLE - extern "C" { #include "gd32e50x_rcu.h" #include "gd32e50x_gpio.h" @@ -68,13 +66,29 @@ void EthercatSync::init() timer_interrupt_enable(TIMER0, TIMER_INT_UP); nvic_irq_enable(TIMER0_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC, 0U); timer_enable(TIMER0); + + /* ESC IRQ:PB1 -> EXTI1,下降沿,preempt=1 */ + gpio_exti_source_select(CONFIG_ESC_IRQ_EXTI_SOURCE_PORT, CONFIG_ESC_IRQ_EXTI_SOURCE_PIN); + exti_init(CONFIG_ESC_IRQ_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); + exti_interrupt_enable(CONFIG_ESC_IRQ_EXTI); + exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI); + nvic_irq_enable(CONFIG_ESC_IRQ_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_ESC, 0U); + + /* SYNC1:PB2 -> EXTI2,下降沿,preempt=1(备用) */ + gpio_exti_source_select(CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT, CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN); + exti_init(CONFIG_ESC_SYNC1_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); + exti_interrupt_enable(CONFIG_ESC_SYNC1_EXTI); + exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI); + nvic_irq_enable(CONFIG_ESC_SYNC1_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1, 0U); #endif } +__attribute__((section(".ramfunc"), noinline)) void EthercatSync::onSync0Exti() { - if (exti_interrupt_flag_get(CONFIG_ESC_SYNC0_EXTI) != RESET) { - exti_interrupt_flag_clear(CONFIG_ESC_SYNC0_EXTI); + /* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */ + if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) { + EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI; EthercatSlave::periodicIsr(); } } @@ -110,13 +124,14 @@ void EthercatSync::onTimerIrq() extern "C" { #if (CONFIG_ETHERCAT_SYNC_MODE == 0) +__attribute__((section(".ramfunc"), noinline)) void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); } void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); } void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); } #else void TIMER0_IRQHandler(void) { EthercatSync::onTimerIrq(); } +void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); } +void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); } #endif } /* extern "C" */ - -#endif /* CONFIG_ETHERCAT_ENABLE */ diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index bfa9ea6..e853e2b 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -1,4 +1,4 @@ -/** +/** * @file canfd.cpp * @brief CANFD 通信模块实现。 */ @@ -8,6 +8,7 @@ #include "sys/config.h" #include "sys/critical.h" #include "sys/task_manager.h" +#include namespace { struct TxFrame { @@ -49,6 +50,11 @@ constexpr uint32_t kCanRxParseTaskMask = (1UL << TASK_CAN_RX_PARSE); CanFd::Diag CanFd::s_diag{}; CanFd::RxQueue CanFd::s_rxQueues[3]{}; uint8_t CanFd::s_nextConsumeChannel = 0U; +volatile CanFd::Mode CanFd::s_mode = CanFd::Mode::kModeAllPass; +CanFd::ChannelStats CanFd::s_channelStats[3]{}; +CanFd::ChannelFeedback CanFd::s_feedback[3]{}; +uint32_t CanFd::s_feedbackSeq = 0U; +CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{}; static TxQueue s_txQueues[3]{}; /** @@ -77,20 +83,30 @@ void CanFd::initGpio() * @brief 为指定 CAN 控制器配置接收过滤器。 * @param[in] canPeriph 目标 CAN 外设实例。 * @param[in] filterNumber 过滤器编号。 + * @param[in] mode 目标接收过滤模式。 * @param[out] 无。 * @retval 无返回值。 - * @note 当前默认按 `CONFIG_CAN_RX_FILTER_ID`/`CONFIG_CAN_RX_FILTER_MASK` 对标准帧做掩码匹配,并统一进入 FIFO0。 + * @note kModeAllPass 模式下使用 32 位掩码模式且 mask=0/list=0,硬件直接放行所有标准/扩展帧到 FIFO0; + * kModeFilterId 模式下按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧掩码匹配。 */ -void CanFd::configureRxFilter(uint32_t canPeriph, uint8_t filterNumber) +void CanFd::configureRxFilter(uint32_t canPeriph, uint8_t filterNumber, Mode mode) { can_filter_parameter_struct can_filter{}; can_filter.filter_number = filterNumber; can_filter.filter_mode = CAN_FILTERMODE_MASK; can_filter.filter_bits = CAN_FILTERBITS_32BIT; - can_filter.filter_list_high = static_cast((CONFIG_CAN_RX_FILTER_ID & 0x7FFU) << 5U); - can_filter.filter_list_low = 0x0000U; - can_filter.filter_mask_high = static_cast((CONFIG_CAN_RX_FILTER_MASK & 0x7FFU) << 5U); - can_filter.filter_mask_low = static_cast(1U << 2U); + if (mode == Mode::kModeAllPass) { + /* 全部接收:列表与掩码都为 0,硬件不再对 ID 做比较。 */ + can_filter.filter_list_high = 0x0000U; + can_filter.filter_list_low = 0x0000U; + can_filter.filter_mask_high = 0x0000U; + can_filter.filter_mask_low = 0x0000U; + } else { + can_filter.filter_list_high = static_cast((CONFIG_CAN_RX_FILTER_ID & 0x7FFU) << 5U); + can_filter.filter_list_low = 0x0000U; + can_filter.filter_mask_high = static_cast((CONFIG_CAN_RX_FILTER_MASK & 0x7FFU) << 5U); + can_filter.filter_mask_low = static_cast(1U << 2U); + } can_filter.filter_fifo_number = CAN_FIFO0; can_filter.filter_enable = ENABLE; can_filter_init(canPeriph, &can_filter); @@ -204,6 +220,7 @@ bool CanFd::enqueueTxFrame(uint8_t channelIndex, const StatusPayload *status) auto &queue = s_txQueues[channelIndex]; if(queue.count >= kTxQueueDepth) { s_diag.tx_drop_count++; + s_channelStats[channelIndex].error_count++; return false; } @@ -245,6 +262,12 @@ void CanFd::init() { s_diag = {}; s_nextConsumeChannel = 0U; + s_mode = Mode::kModeAllPass; + for (size_t i = 0; i < kChannelCount; ++i) { + s_channelStats[i] = {}; + s_feedback[i] = {}; + } + s_feedbackSeq = 0U; initGpio(); @@ -255,7 +278,7 @@ void CanFd::init() can_deinit(ch.canPeriph); initNominalTiming(ch.canPeriph); initFdMode(ch.canPeriph); - configureRxFilter(ch.canPeriph, ch.filterNumber); + configureRxFilter(ch.canPeriph, ch.filterNumber, s_mode); can_interrupt_enable(ch.canPeriph, CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0); nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast(i)); @@ -270,6 +293,29 @@ void CanFd::init() s_nextConsumeChannel = 0U; } +/** + * @brief 切换 CANFD 接收过滤模式。 + * @param[in] mode 目标过滤模式。 + * @note 切换时逐路重新配置过滤器;can_filter_init 内部会清 FCTL_FLD,无需手动进入 INITIAL 模式。 + */ +void CanFd::setMode(Mode mode) +{ + uint32_t save = RT_CRITICAL_SAVE(); + s_mode = mode; + for (size_t i = 0; i < kChannelCount; ++i) { + configureRxFilter(kChannels[i].canPeriph, kChannels[i].filterNumber, mode); + } + RT_CRITICAL_RESTORE(save); +} + +/** + * @brief 查询当前 CANFD 接收过滤模式。 + */ +CanFd::Mode CanFd::mode() +{ + return s_mode; +} + /** * @brief 执行兼容保留的 CAN 轮询入口。 * @param[in] 无。 @@ -334,10 +380,12 @@ void CanFd::sendPrepared() s_diag.last_payload_length = frame.tx_message.tx_dlen; s_diag.last_tx_id = frame.tx_message.tx_sfid; s_diag.last_tx_channel = static_cast(i); + s_channelStats[i].tx_count++; queue.head = static_cast((queue.head + 1U) % kTxQueueDepth); queue.count--; } else { s_diag.error_count++; + s_channelStats[i].error_count++; } } } @@ -464,9 +512,11 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, s_diag.last_tx_id = ext_id; s_diag.last_tx_channel = channelIndex; s_diag.last_payload_length = dlc; + s_channelStats[channelIndex].tx_count++; return true; } s_diag.error_count++; + s_channelStats[channelIndex].error_count++; return false; } @@ -546,9 +596,71 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex) s_diag.last_rx_esi = rx_message.fd_esi; s_diag.last_rx_id = (rx_message.rx_ff == CAN_FF_STANDARD) ? rx_message.rx_sfid : rx_message.rx_efid; + /* 维护每通道独立计数与最近一帧快照,用于 1Hz 反馈给 ESP32。 */ + auto &stats = s_channelStats[channelIndex]; + stats.rx_count++; + stats.last_rx_id = s_diag.last_rx_id; + stats.last_rx_dlc = rx_message.rx_dlen; + stats.last_rx_fd_flag = rx_message.fd_flag; + stats.last_rx_brs = rx_message.fd_brs; + stats.last_rx_esi = rx_message.fd_esi; + const uint8_t copyLen = (rx_message.rx_dlen < CANFD_FEEDBACK_LAST_DATA_MAX) + ? rx_message.rx_dlen : static_cast(CANFD_FEEDBACK_LAST_DATA_MAX); + for (uint8_t i = 0U; i < copyLen; ++i) { + stats.last_rx_data[i] = rx_message.rx_data[i]; + } + for (uint8_t i = copyLen; i < CANFD_FEEDBACK_LAST_DATA_MAX; ++i) { + stats.last_rx_data[i] = 0U; + } + /* 维护每通道不重复 ID 快照表,用于 1Hz 分页拉取给 ESP32。 */ + { + auto &table = s_idSnapshot[channelIndex]; + const uint32_t rxId = s_diag.last_rx_id; + /* 查找已有条目 */ + uint8_t found = CANFD_ID_SNAPSHOT_MAX; + for (uint8_t si = 0U; si < table.count; ++si) { + if (table.entries[si].occupied && table.entries[si].id == rxId) { + found = si; + break; + } + } + if (found >= CANFD_ID_SNAPSHOT_MAX) { + /* 未找到:表未满则新建,满了淘汰最旧(index 0) */ + if (table.count < CANFD_ID_SNAPSHOT_MAX) { + found = table.count; + table.count++; + } else { + found = 0U; + /* 淘汰后整体左移一格让新条目在末尾 */ + for (uint8_t si = 0U; si < CANFD_ID_SNAPSHOT_MAX - 1U; ++si) { + table.entries[si] = table.entries[si + 1U]; + } + found = CANFD_ID_SNAPSHOT_MAX - 1U; + } + table.entries[found] = {}; + table.entries[found].id = rxId; + table.entries[found].occupied = true; + } + auto &entry = table.entries[found]; + entry.rx_count++; + entry.dlc = rx_message.rx_dlen; + entry.fd_flag = rx_message.fd_flag; + entry.brs = rx_message.fd_brs; + entry.esi = rx_message.fd_esi; + const uint8_t snapCopy = (rx_message.rx_dlen < CANFD_ID_SNAPSHOT_DATA_MAX) + ? rx_message.rx_dlen : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); + for (uint8_t si = 0U; si < snapCopy; ++si) { + entry.data[si] = rx_message.rx_data[si]; + } + for (uint8_t si = snapCopy; si < CANFD_ID_SNAPSHOT_DATA_MAX; ++si) { + entry.data[si] = 0U; + } + } + auto &queue = s_rxQueues[channelIndex]; if(queue.count >= kRxQueueDepth) { s_diag.rx_drop_count++; + stats.rx_drop_count++; continue; } @@ -587,6 +699,53 @@ void CanFd::onRx0Interrupt() handleRxInterrupt(0U); } +/** + * @brief 刷新三路 CAN 通道的 1Hz 反馈快照。 + * @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。 + * 典型调用频率 1Hz,由任务调度器在心跳节拍触发。 + */ +void CanFd::refreshFeedbackSnapshot() +{ + uint32_t save = RT_CRITICAL_SAVE(); + s_feedbackSeq++; + for (size_t i = 0; i < kChannelCount; ++i) { + s_feedback[i].snapshot_seq = s_feedbackSeq; + s_feedback[i].stats = s_channelStats[i]; + s_idSnapshot[i].seq = s_feedbackSeq; + } + RT_CRITICAL_RESTORE(save); +} + +/** + * @brief 拷贝指定通道的最新反馈快照。 + * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 + * @param[out] out 目标快照缓冲;为空或通道非法时返回 false。 + * @retval bool true 表示成功拷贝。 + */ + +/** + * @brief 拷贝指定通道的不重复 ID 快照表。 + */ +bool CanFd::copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out) +{ + if (out == nullptr || channelIndex >= kChannelCount) { + return false; + } + uint32_t save = RT_CRITICAL_SAVE(); + *out = s_idSnapshot[channelIndex]; + RT_CRITICAL_RESTORE(save); + return true; +} +bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out) +{ + if (out == nullptr || channelIndex >= kChannelCount) { + return false; + } + uint32_t save = RT_CRITICAL_SAVE(); + *out = s_feedback[channelIndex]; + RT_CRITICAL_RESTORE(save); + return true; +} /* ----- 中断入口 C 包装 ------------------------------------------------- */ /** * @brief CAN0 RX0 中断向量入口。 diff --git a/GD32E508_DCU_APP/device/Timer.cpp b/GD32E508_DCU_APP/device/Timer.cpp index 2fa3b6b..405d8bf 100644 --- a/GD32E508_DCU_APP/device/Timer.cpp +++ b/GD32E508_DCU_APP/device/Timer.cpp @@ -40,11 +40,10 @@ void SchedulerTimer::configureTimer() * @param[in] 无。 * @param[out] 无。 * @retval 无返回值。 - * @note 设置优先级分组,并为调度定时器中断向量配置抢占优先级与子优先级。 + * @note 优先级分组已前移至 AppInitializer::init() 最开头统一设置;此处仅为调度定时器中断向量配置抢占优先级与子优先级。 */ void SchedulerTimer::configureNvic() { - nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP); nvic_irq_enable(CONFIG_SCHED_TIMER_IRQn, CONFIG_SCHED_TIMER_IRQ_PREEMPT_PRIO, CONFIG_SCHED_TIMER_IRQ_SUB_PRIO); } diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 4a7c97f..fa80a33 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -1,4 +1,4 @@ -#ifndef DEVICE_CANFD_H +#ifndef DEVICE_CANFD_H #define DEVICE_CANFD_H #include @@ -6,165 +6,155 @@ /** @brief CANFD 状态报文的数据长度,单位字节。 */ #define CANFD_STATUS_DLEN 12U +/** @brief 每通道反馈快照中保存的最近一帧最大数据长度,受 SPI 应答帧总长度约束。 */ +#define CANFD_FEEDBACK_LAST_DATA_MAX 32U + +/** @brief 每通道不重复 ID 快照表最大条目数。 */ +#define CANFD_ID_SNAPSHOT_MAX 16U + +/** @brief 快照条目中最大数据字节数(等于 CANFD DLC 上限)。 */ +#define CANFD_ID_SNAPSHOT_DATA_MAX 64U + /** * @brief CANFD 通信模块。 * @note 负责 GPIO/模式控制、波特率配置、异步接收缓存与按需发送能力。 */ class CanFd { public: + /** + * @brief CANFD 接收过滤模式枚举。 + * @note 默认模式为 kModeAllPass,即三路控制器关闭硬件 ID 过滤,全部接收。 + */ + enum class Mode : uint8_t { + kModeAllPass = 0U, /**< 全部接收模式:禁用硬件 ID/掩码匹配,所有 11/29 位帧都进入 FIFO0。 */ + kModeFilterId = 1U, /**< 过滤模式:按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧掩码匹配。 */ + }; + /** * @brief CANFD 运行诊断统计信息。 - * @note 记录发送、接收、错误计数以及最近一次收发报文的关键信息。 */ struct Diag { - uint32_t tx_count; /**< 成功发送的报文计数。 */ - uint32_t rx_count; /**< 成功接收的报文计数。 */ - uint32_t error_count; /**< 发送失败或检测到的错误计数。 */ - uint32_t rx_drop_count; /**< 软件接收队列满导致丢弃的报文计数。 */ - uint32_t rx_fifo_full_count; /**< 硬件 FIFO0 进入满状态的次数。 */ - uint32_t rx_fifo_overfull_count; /**< 硬件 FIFO0 发生 overfull 的次数。 */ - uint32_t tx_drop_count; /**< 发送 FIFO 满导致丢弃的报文计数。 */ - uint32_t tx_abort_count; /**< 因邮箱全占主动中止占用邮箱的次数(典型:总线无 ACK 导致自动重传卡死)。 */ - uint32_t last_rx_id; /**< 最近一次接收报文的标识符。 */ - uint32_t last_tx_id; /**< 最近一次发送报文的标识符。 */ - uint8_t last_rx_channel; /**< 最近一次接收报文所在的 CAN 通道号(0/1/2)。 */ - uint8_t last_tx_channel; /**< 最近一次发送报文所在的 CAN 通道号(0/1/2)。 */ - uint8_t last_payload_length; /**< 最近一次收发报文的数据长度。 */ - uint8_t last_rx_fd_flag; /**< 最近一次接收报文的 FD 帧标志。 */ - uint8_t last_rx_brs; /**< 最近一次接收报文的波特率切换标志。 */ - uint8_t last_rx_esi; /**< 最近一次接收报文的错误状态指示位。 */ + uint32_t tx_count; + uint32_t rx_count; + uint32_t error_count; + uint32_t rx_drop_count; + uint32_t rx_fifo_full_count; + uint32_t rx_fifo_overfull_count; + uint32_t tx_drop_count; + uint32_t tx_abort_count; + uint32_t last_rx_id; + uint32_t last_tx_id; + uint8_t last_rx_channel; + uint8_t last_tx_channel; + uint8_t last_payload_length; + uint8_t last_rx_fd_flag; + uint8_t last_rx_brs; + uint8_t last_rx_esi; }; + /** + * @brief 单通道运行计数(每路独立累计)。 + */ + struct ChannelStats { + uint32_t rx_count; /**< 该通道累计成功接收帧数。 */ + uint32_t tx_count; /**< 该通道累计成功发送帧数。 */ + uint32_t rx_drop_count; /**< 该通道累计软件丢帧数。 */ + uint32_t error_count; /**< 该通道累计错误计数(与发送邮箱失败合并)。 */ + uint32_t last_rx_id; /**< 该通道最近一帧报文标识符。 */ + uint8_t last_rx_dlc; /**< 该通道最近一帧报文数据长度。 */ + uint8_t last_rx_fd_flag; /**< 该通道最近一帧 FD 帧标志。 */ + uint8_t last_rx_brs; /**< 该通道最近一帧 BRS 标志。 */ + uint8_t last_rx_esi; /**< 该通道最近一帧 ESI 标志。 */ + uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX]; + }; + + /** + * @brief 单通道 1Hz 反馈快照(每秒刷新一次)。 + */ + struct ChannelFeedback { + uint32_t snapshot_seq; /**< 第几次刷新(自启动起递增)。 */ + ChannelStats stats; /**< 抓拍时刻的该通道统计与最近一帧信息。 */ + }; + + + /** + * @brief 单个不重复 ID 的最新帧快照。 + */ + struct IdSnapshotEntry { + uint32_t id; /**< CAN 报文标识符。 */ + uint32_t seq; /**< 该条目被更新的序号(自启动起递增)。 */ + uint32_t rx_count; /**< 该 ID 累计接收帧数。 */ + uint8_t dlc; /**< 最新一帧数据长度。 */ + uint8_t fd_flag; /**< 最新一帧 FD 标志。 */ + uint8_t brs; /**< 最新一帧 BRS 标志。 */ + uint8_t esi; /**< 最新一帧 ESI 标志。 */ + uint8_t data[CANFD_ID_SNAPSHOT_DATA_MAX]; /**< 最新一帧数据。 */ + bool occupied; /**< 条目是否已被占用。 */ + }; + + /** + * @brief 每通道不重复 ID 快照表(1Hz 刷新,SPI 命令分页拉取)。 + */ + struct IdSnapshotTable { + IdSnapshotEntry entries[CANFD_ID_SNAPSHOT_MAX]; + uint8_t count; /**< 当前占用条目数。 */ + uint32_t seq; /**< 全局更新序号(每次 refresh 时递增)。 */ + }; /** * @brief CANFD 状态发送载荷。 - * @note 当前保持协议兼容,仅包含温度与供电相关字段;VIN_ADC 单独在诊断打印中输出。 */ struct StatusPayload { - uint16_t temp_raw; /**< 内部温度通道原始 ADC 数值。 */ - uint16_t vref_raw; /**< 内部参考电压通道原始 ADC 数值。 */ - float vdda; /**< 根据内部参考电压换算得到的 MCU 供电电压,单位 V。 */ - float temperature; /**< 根据温度通道换算得到的芯片温度,单位 ℃。 */ + uint16_t temp_raw; + uint16_t vref_raw; + float vdda; + float temperature; }; - /** - * @brief 初始化 CANFD 外设、GPIO、模式控制、过滤器与中断。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 初始化后默认进入 Normal 模式,并使能 3 路 CAN 控制器的 RX0 中断用于异步接收。 - */ static void init(); /** - * @brief 兼容保留的轮询接口,中断模式下为空壳。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 实际接收已由 CAN0/CAN1/CAN2 的 RX0 中断驱动。 + * @brief 切换 CANFD 接收过滤模式。 */ + static void setMode(Mode mode); + + /** @brief 查询当前 CANFD 接收过滤模式。 */ + static Mode mode(); + static void poll(); - - /** - * @brief 根据状态载荷准备一帧待发送的 CANFD 状态报文,并广播入队到 3 路发送 FIFO。 - * @param[in] status 指向 StatusPayload 的指针;为空时不入队。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 当前模块默认不会周期性调用该接口;仅在外部显式触发发送测试或业务发送时使用。 - */ static void prepareStatus(const StatusPayload *status); - - /** - * @brief 根据状态载荷仅向指定 CAN 通道的发送 FIFO 入队一帧状态报文。 - * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 - * @param[in] status 指向 StatusPayload 的指针;为空时不入队。 - * @param[out] 无。 - * @retval bool true 表示成功入队,false 表示通道非法或该通道发送 FIFO 已满。 - * @note 当前模块默认不会周期性调用该接口;仅在外部显式触发指定通道发送时使用。 - */ static bool prepareStatusForChannel(uint8_t channelIndex, const StatusPayload *status); - - /** - * @brief 发送当前已准备好的 CANFD 状态报文。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 当前模块默认不会周期性调用该接口;仅发送调用方显式准备过的报文;若队列为空则直接返回。 - */ static void sendPrepared(); - - /** - * @brief 查询 3 个 CAN 通道中是否存在尚未消费的接收报文。 - * @param[in] 无。 - * @param[out] 无。 - * @retval bool true 表示至少有一路存在待消费报文,false 表示当前所有通道缓存为空。 - */ static bool hasRxFrame(); - - /** - * @brief 读取并消费最近一轮扫描到的某一路缓存接收帧。 - * @param[in] 无。 - * @param[out] id 输出报文标识符的指针;允许为空。 - * @param[out] data 输出报文数据缓存区指针;允许为空。 - * @param[out] length 输出报文数据长度的指针;允许为空。 - * @param[out] channel 输出报文来源的 CAN 通道号指针;允许为空。 - * @retval bool true 表示成功消费一帧报文,false 表示当前 3 路缓存均为空。 - * @note 每路通道使用轻量环形缓冲保留多帧;消费顺序采用轮转扫描,避免长期偏向固定通道。 - */ static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr); - - /** - * @brief 直接向指定 CAN 通道发送一帧任意数据报文(扩展帧)。 - * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 - * @param[in] ext_id 扩展帧 ID(29 位)。 - * @param[in] data 数据负载指针。 - * @param[in] dlc 数据长度(1~64)。 - * @retval bool true 表示成功入队硬件邮箱,false 表示参数无效或邮箱已满。 - * @note 绕过软件发送队列,直接调用硬件发送;适用于 SPI 桥接等即时发送场景。 - */ static bool transmitFrame(uint8_t channelIndex, uint32_t ext_id, const uint8_t *data, uint8_t dlc); - - /** - * @brief 获取 CANFD 诊断统计信息只读指针。 - * @param[in] 无。 - * @param[out] 无。 - * @retval const Diag* 指向内部诊断统计结构体的只读指针。 - */ static const Diag *diag(); /** - * @brief 将 TCAN1057A 收发器置为正常工作模式。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 模式控制脚按 STB 语义处理:低电平为 Normal,高电平为 Silent。 + * @brief 刷新三路 1Hz 反馈快照。 + * @note 通常由任务调度器在 1 秒节拍调用,将各通道实时计数与最近一帧信息抓拍到 s_feedback。 */ + static void refreshFeedbackSnapshot(); + + /** + * @brief 拷贝指定通道的最新反馈快照到调用方缓冲。 + * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 + * @param[out] out 目标快照缓冲;为空或通道非法时返回 false。 + * @retval bool true 表示成功拷贝,false 表示参数非法。 + */ + static bool copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out); + + /** + * @brief 拷贝指定通道的不重复 ID 快照表。 + * @param[in] channelIndex 目标通道 0/1/2。 + * @param[out] out 输出缓冲。 + * @retval true 成功。 + */ + static bool copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out); + static void setNormalMode(); - - /** - * @brief 将 TCAN1057A 收发器置为静默模式。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 静默模式下收发器不驱动总线显性位,可用于总线监听或受控隔离。 - */ static void setSilentMode(); - - /** - * @brief CAN0 兼容中断处理入口。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 兼容保留给 CAN0 中断包装使用;三路统一接收逻辑已收口到 handleRxInterrupt()。 - */ static void onRx0Interrupt(); - - /** - * @brief 处理指定 CAN 通道的 RX0 中断接收。 - * @param[in] channelIndex CAN 通道索引(0/1/2)。 - * @param[out] 无。 - * @retval 无返回值。 - */ static void handleRxInterrupt(uint8_t channelIndex); private: @@ -184,25 +174,30 @@ private: volatile uint8_t count; }; - /** @brief 初始化 CAN0/CAN1/CAN2 相关 GPIO。 */ static void initGpio(); - /** @brief 为指定控制器配置接收过滤器。 */ - static void configureRxFilter(uint32_t canPeriph, uint8_t filterNumber); - /** @brief 配置指定控制器的 CAN 仲裁段标称位时序。 */ + /** + * @brief 根据当前模式为指定控制器配置接收过滤器。 + * @param[in] canPeriph 目标 CAN 外设实例。 + * @param[in] filterNumber 过滤器编号。 + * @param[in] mode 目标接收过滤模式。 + */ + static void configureRxFilter(uint32_t canPeriph, uint8_t filterNumber, Mode mode); static void initNominalTiming(uint32_t canPeriph); - /** @brief 配置指定控制器的 CAN FD 数据段位时序与 TDC。 */ static void initFdMode(uint32_t canPeriph); - /** @brief 构造内部发送缓冲中的状态帧内容。 */ static void buildStatusFrame(const StatusPayload *status); - /** @brief 向指定 CAN 通道的发送 FIFO 写入一帧状态报文。 */ static bool enqueueTxFrame(uint8_t channelIndex, const StatusPayload *status); - static Diag s_diag; - static RxQueue s_rxQueues[3]; - static uint8_t s_nextConsumeChannel; + static Diag s_diag; + static RxQueue s_rxQueues[3]; + static uint8_t s_nextConsumeChannel; + static volatile Mode s_mode; + static ChannelStats s_channelStats[3]; + static ChannelFeedback s_feedback[3]; + static uint32_t s_feedbackSeq; + static IdSnapshotTable s_idSnapshot[3]; }; using canfd_diag_t = CanFd::Diag; using canfd_status_payload_t = CanFd::StatusPayload; -#endif /* DEVICE_CANFD_H */ +#endif /* DEVICE_CANFD_H */ \ No newline at end of file diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp new file mode 100644 index 0000000..0066f0e --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp @@ -0,0 +1,206 @@ +/** + * @file app_spi_cmd_can_feedback.cpp + * @brief CAN 不重复 ID 快照表轮询与接收模式切换 SPI 命令处理器。 + * + * 协议说明(与 ESP32 端 canfd_bridge 对接): + * + * 1) kTypeCanFeedback (0xC1):ESP32 主动轮询,分页返回不重复 ID 快照表。 + * 请求 payload: + * [0] channel 0/1/2 + * [1] page 页号(0 = 条目 0~7,1 = 条目 8~15) + * 应答 payload(变长,最大 kTxBufSize - overhead = 123 字节): + * [0] channel + * [1] page + * [2] total_count 该通道当前条目总数 + * [3] entry_count 本页实际条目数 + * [4] mode 当前 CAN 接收模式 + * [5..8] snapshot_seq (LE) + * [9..12] rx_count (LE) 通道累计接收 + * [13..16] tx_count (LE) 通道累计发送 + * [17..20] drop_count (LE) + * [21..24] error_count (LE) + * [25..] 条目逐条拼接: + * id(4LE) + dlc(1) + flags(1) + reserved(2) + data(dlc) + * 每条 = 8 + dlc 字节 + * + * 2) kTypeCanSetMode (0xC2):ESP32 主动切换三路 CAN 过滤模式。 + * 请求 payload: + * [0] mode 0=全部接收, 1=按 ID 过滤;其它值返回 kRspError。 + * 应答 payload: + * [0] 实际生效的 mode。 + */ + +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "device/canfd.h" +#include "sys/config.h" +#include +#include + +namespace { + +constexpr uint8_t kFeedbackChannelCount = 3U; +constexpr uint16_t kFeedbackPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */ +constexpr uint8_t kPageEntries = 8U; +constexpr uint16_t kFixedHeaderLen = 25U; /* ch(1)+page(1)+total(1)+cnt(1)+mode(1)+seq(4)+rx(4)+tx(4)+drop(4)+err(4) */ + +void writeU32Le(uint8_t *dst, uint32_t value) +{ + dst[0] = static_cast(value & 0xFFU); + dst[1] = static_cast((value >> 8) & 0xFFU); + dst[2] = static_cast((value >> 16) & 0xFFU); + dst[3] = static_cast((value >> 24) & 0xFFU); +} + +class SpiCanFeedbackHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return (frame_type == SpiDefs::kTypeCanFeedback) + || (frame_type == SpiDefs::kTypeCanSetMode); + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + if (ctx.frame_type == SpiDefs::kTypeCanFeedback) { + handleFeedback(bus, ctx); + } else { + handleSetMode(bus, ctx); + } + } + +private: + void handleFeedback(AppSpiCommand &bus, const SpiCommandContext &ctx) + { + if (ctx.payload_len < 2U) { + uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t channel = ctx.payload[0]; + const uint8_t page = ctx.payload[1]; + + if (channel >= kFeedbackChannelCount) { + uint8_t err = 0xE1U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + /* 拷贝统计快照 */ + CanFd::ChannelFeedback snap{}; + CanFd::copyFeedbackSnapshot(channel, &snap); + + /* 拷贝 ID 快照表 */ + CanFd::IdSnapshotTable table{}; + CanFd::copyIdSnapshotTable(channel, &table); + + const uint8_t start = static_cast(page * kPageEntries); + if (start >= CANFD_ID_SNAPSHOT_MAX) { + uint8_t err = 0xE3U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + uint8_t payload[kFeedbackPayloadLen] = {}; + + /* 固定头 */ + payload[0] = channel; + payload[1] = page; + payload[2] = table.count; + /* payload[3] = entry_count 稍后填 */ + payload[4] = static_cast(CanFd::mode()); + writeU32Le(&payload[5], snap.snapshot_seq); + writeU32Le(&payload[9], snap.stats.rx_count); + writeU32Le(&payload[13], snap.stats.tx_count); + writeU32Le(&payload[17], snap.stats.rx_drop_count); + writeU32Le(&payload[21], snap.stats.error_count); + + /* 打包 ID 条目 */ + uint16_t offset = kFixedHeaderLen; + uint8_t packed = 0U; + + const uint8_t end = static_cast( + (start + kPageEntries < CANFD_ID_SNAPSHOT_MAX) + ? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX); + + for (uint8_t i = start; i < end; ++i) { + const auto &e = table.entries[i]; + if (!e.occupied) continue; + + const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX) + ? e.dlc : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); + const uint16_t entrySize = static_cast(8U + dataLen); + + if (offset + entrySize > kFeedbackPayloadLen) break; + + writeU32Le(&payload[offset], e.id); + payload[offset + 4U] = e.dlc; + payload[offset + 5U] = static_cast( + (e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U)); + payload[offset + 6U] = 0U; /* reserved */ + payload[offset + 7U] = 0U; /* reserved */ + memcpy(&payload[offset + 8U], e.data, dataLen); + + offset += entrySize; + packed++; + } + + payload[3] = packed; + + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, payload, offset); + } + + void handleSetMode(AppSpiCommand &bus, const SpiCommandContext &ctx) + { + if (ctx.payload_len < 1U) { + uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t requested = ctx.payload[0]; + CanFd::Mode target; + switch (requested) { + case 0U: target = CanFd::Mode::kModeAllPass; break; + case 1U: target = CanFd::Mode::kModeFilterId; break; + default: { + uint8_t err = 0xE1U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + } + + CanFd::setMode(target); + + const uint8_t actual = static_cast(CanFd::mode()); + +#if (CONFIG_SPI_APP_LOG_ENABLE != 0) + char log_msg[AppSpiCommand::kLogBufSize] = {}; + (void)snprintf(log_msg, sizeof(log_msg), + "CAN_MODE set=%u -> actual=%u", + static_cast(requested), + static_cast(actual)); + bus.postLog(log_msg); +#endif + + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &actual, 1U); + } +}; + +static SpiCanFeedbackHandler s_handler; + +} // namespace + +void registerSpiCanFeedbackCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp new file mode 100644 index 0000000..cdd9f49 --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp @@ -0,0 +1,135 @@ +/** + * @file app_spi_cmd_id_snapshot.cpp + * @brief SPI 命令处理器:kTypeIdSnapshot (0xC3)。 + * + * ESP32 分页拉取指定 CAN 通道的不重复 ID 快照表。 + * + * 请求 payload: + * [0] channel 0/1/2 + * [1] page 页号(0 = 条目 0~7,1 = 条目 8~15) + * + * 应答 payload(变长,最大 kTxBufSize - 5 = 123 字节): + * [0] channel + * [1] page + * [2] total_count 该通道当前条目总数 + * [3] entry_count 本页实际条目数 + * [4..] 条目逐条拼接: + * id(4LE) + seq(4LE) + rx_count(4LE) + dlc(1) + flags(1) + data(dlc) + * 每条 = 16 + dlc 字节 + */ + +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "device/canfd.h" +#include +#include + +namespace { + +constexpr uint8_t kPageEntries = 8U; +constexpr uint16_t kMaxPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */ + +inline void writeU32Le(uint8_t *dst, uint32_t val) +{ + dst[0] = static_cast(val & 0xFFU); + dst[1] = static_cast((val >> 8) & 0xFFU); + dst[2] = static_cast((val >> 16) & 0xFFU); + dst[3] = static_cast((val >> 24) & 0xFFU); +} + +class SpiIdSnapshotHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeIdSnapshot; + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + if (ctx.payload_len < 2U) { + uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t channel = ctx.payload[0]; + const uint8_t page = ctx.payload[1]; + + if (channel >= 3U) { + uint8_t err = 0xE1U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + CanFd::IdSnapshotTable table{}; + if (!CanFd::copyIdSnapshotTable(channel, &table)) { + uint8_t err = 0xE2U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t start = static_cast(page * kPageEntries); + if (start >= CANFD_ID_SNAPSHOT_MAX) { + uint8_t err = 0xE3U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + uint8_t payload[kMaxPayloadLen] = {}; + payload[0] = channel; + payload[1] = page; + payload[2] = table.count; + + uint16_t offset = 4U; /* [0]=ch [1]=page [2]=total [3]=entry_count(后填) */ + uint8_t packed = 0U; + + const uint8_t end = static_cast( + (start + kPageEntries < CANFD_ID_SNAPSHOT_MAX) + ? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX); + + for (uint8_t i = start; i < end; ++i) { + const auto &e = table.entries[i]; + if (!e.occupied) continue; + + const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX) + ? e.dlc : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); + const uint16_t entrySize = 16U + dataLen; /* 4+4+4+1+1+1+1 + data */ + + if (offset + entrySize > kMaxPayloadLen) break; /* 装不下就截断 */ + + writeU32Le(&payload[offset], e.id); + writeU32Le(&payload[offset + 4U], e.seq); + writeU32Le(&payload[offset + 8U], e.rx_count); + payload[offset + 12U] = e.dlc; + /* flags 字节: bit0=fd, bit1=brs, bit2=esi */ + payload[offset + 13U] = static_cast( + (e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U)); + /* 保留 2 字节对齐 */ + payload[offset + 14U] = 0U; + payload[offset + 15U] = 0U; + memcpy(&payload[offset + 16U], e.data, dataLen); + + offset += entrySize; + packed++; + } + + payload[3] = packed; + + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, payload, offset); + } +}; + +static SpiIdSnapshotHandler s_handler; + +} // namespace + +void registerSpiIdSnapshotCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index b5aeffa..5486d03 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -1,4 +1,4 @@ -/** +/** * @file app_spi_command.cpp * @brief APP 运行期 SPI 链路与命令路由实现。 * @@ -303,6 +303,8 @@ extern void registerSpiBootRequestCommand(AppSpiCommand &bus); extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); extern void registerSpiAppPingCommand(AppSpiCommand &bus); extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); +extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); +extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); AppSpiCommand AppSpiCommandBus; void AppSpiCommand::init() { @@ -320,6 +322,8 @@ void AppSpiCommand::init() registerSpiOtaWaitCommand(AppSpiCommandBus); registerSpiAppPingCommand(AppSpiCommandBus); registerSpiCanBridgeCommand(AppSpiCommandBus); + registerSpiCanFeedbackCommand(AppSpiCommandBus); + registerSpiIdSnapshotCommand(AppSpiCommandBus); configureGpio(); configureSpi0(); diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index ae6247f..558bbca 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -58,7 +58,7 @@ public: const uint8_t *payload, uint16_t payload_len); static constexpr uint16_t kRxBufSize = 128U; - static constexpr uint16_t kTxBufSize = 64U; + static constexpr uint16_t kTxBufSize = 128U; static constexpr uint16_t kLogBufSize = 256U; private: diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h index 1685d33..4cf2c2c 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -23,6 +23,9 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U; static constexpr uint8_t kTypeAppPing = 0xA1U; static constexpr uint8_t kTypeBootReq = 0xB0U; static constexpr uint8_t kTypeCanSend = 0xC0U; +static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */ +static constexpr uint8_t kTypeCanSetMode = 0xC2U; /**< ESP32 -> GD32 切换 CAN 接收过滤模式 */ +static constexpr uint8_t kTypeIdSnapshot = 0xC3U; /**< ESP32 -> GD32 拉取不重复 ID 快照表 */ /* 响应类型 */ static constexpr uint8_t kRspAck = 0x80U; diff --git a/GD32E508_DCU_APP/device/usart0.h b/GD32E508_DCU_APP/device/usart0.h index 34fb834..0f74736 100644 --- a/GD32E508_DCU_APP/device/usart0.h +++ b/GD32E508_DCU_APP/device/usart0.h @@ -74,7 +74,7 @@ private: static bool putCharNonBlocking(int ch); - static constexpr uint16_t kTxBufSize = 256U; + static constexpr uint16_t kTxBufSize = 1024U; static volatile uint32_t s_dropCount; static uint8_t s_txBuf[kTxBufSize]; static volatile uint16_t s_txHead; diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index be73b7b..4e6a91c 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -102,7 +102,7 @@ #define CONFIG_TASK_PERIOD_HEARTBEAT_TICKS 10000U /** @brief 周期温度/电压 UART 打印开关:0=关闭,1=开启。 */ -#define CONFIG_UART_TEMP_PERIODIC_PRINT 0 +#define CONFIG_UART_TEMP_PERIODIC_PRINT 1 /** @brief UART 诊断打印级别:0=仅温度/电压,1=温度/电压+CAN 统计。 */ #define CONFIG_UART_DIAG_LEVEL 1 /** @brief SPI 命令逐帧日志开关:0=关闭,1=开启。高速 CAN 桥接时应关闭。 */ @@ -129,7 +129,7 @@ /** @brief APP 次版本号。 */ #define APP_FW_VERSION_MINOR 1U /** @brief APP 补丁版本号。 */ -#define APP_FW_VERSION_PATCH 0U +#define APP_FW_VERSION_PATCH 2U /** @brief APP 构建编号。 */ #define APP_FW_VERSION_BUILD 0U @@ -175,13 +175,24 @@ /* ============================================================ * EtherCAT LAN9252 使能与配置 * ============================================================ */ -/** @brief EtherCAT 使能开关:0=禁用(未焊接 LAN9252),1=启用(焊接后)。 - * 禁用时 comm/ 全部代码不编译,初始化不调用 EtherCAT,EXMC/EXTI0-2 不配置。 */ -#define CONFIG_ETHERCAT_ENABLE 0 - /** @brief SYNC0 同步模式:0=EXTI 外部触发(ESC 硬件同步,默认),1=TIMER0 降级(DC 未同步时)。 */ #define CONFIG_ETHERCAT_SYNC_MODE 0 +/** @brief periodicIsr 最大执行时间告警阈值(µs)。实测超过此值需评估减小 PDO 大小或优化 EXMC 时序。 + * 当前阈值 5µs:基于 32 字节 PDO + EXMC 6 HCLK 时序的估算上限。 + * preempt=0 ISR 阻塞窗口 ≤ 此阈值时,CAN/SPI 中断需容忍。 */ +#define CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD 5U + +/** @brief 输入 PDO(TxPDO)在 PDI 内存区的起始偏移。 + * 对应 LAN9252 SyncManager 内存映射,需与主站 ESI 配置匹配。 + * @note 占位值,实际数值待主站配置确定后在集成阶段填入。 */ +#define CONFIG_ESC_PDI_INPUT_OFFSET 0x1000U + +/** @brief 输出 PDO(RxPDO)在 PDI 内存区的起始偏移。 + * 对应 LAN9252 SyncManager 内存映射,需与主站 ESI 配置匹配。 + * @note 占位值,实际数值待主站配置确定后在集成阶段填入。 */ +#define CONFIG_ESC_PDI_OUTPUT_OFFSET 0x1200U + /* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */ /** @brief EXMC NOR/SRAM Region1 基址(NE1 片选)。 */ #define CONFIG_ESC_EXMC_BASE_ADDR 0x60000000U diff --git a/GD32E508_DCU_APP/sys/initial.cpp b/GD32E508_DCU_APP/sys/initial.cpp index 34afe66..475b74b 100644 --- a/GD32E508_DCU_APP/sys/initial.cpp +++ b/GD32E508_DCU_APP/sys/initial.cpp @@ -16,10 +16,8 @@ #include "sys/ota_boot_request.h" #include "sys/task_manager.h" #include "sys/config.h" -#if CONFIG_ETHERCAT_ENABLE #include "comm/ethercat_slave.h" #include "comm/ethercat_sync.h" -#endif /** * @brief 执行应用级外设与调度初始化。 @@ -30,6 +28,10 @@ */ void AppInitializer::init() { + /* 必须最先设置 NVIC 优先级分组:确保后续 CanFd/SPI0 等外设 init 中 + * 编码抢占/子优先级字节时使用正确的分组(PRE4_SUB0),避免在错误分组 + * 下编码后被重新解释。EtherCAT 实时架构整改 Task3。 */ + nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP); printf("[APP] init: SysTick...\r\n"); SysTickTimer::initMs(); printf("[APP] init: USART0...\r\n"); @@ -47,12 +49,14 @@ void AppInitializer::init() TaskManager::init(); printf("[APP] init: SchedulerTimer...\r\n"); SchedulerTimer::init(); -#if CONFIG_ETHERCAT_ENABLE printf("[APP] init: EthercatSlave...\r\n"); EthercatSlave::init(); - printf("[APP] init: EthercatSync...\r\n"); - EthercatSync::init(); -#endif + if (EthercatSlave::isEscReady()) { + printf("[APP] init: EthercatSync...\r\n"); + EthercatSync::init(); + } else { + printf("[APP] init: EthercatSync SKIPPED (ESC not ready)\r\n"); + } printf("[APP] init: Watchdog...\r\n"); Watchdog::init(); printf("[APP] init: all done\r\n"); diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index de5aae3..06e08db 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -1,4 +1,4 @@ -/** +/** * @file task_runner.cpp * @brief 任务执行器实现。 */ @@ -13,9 +13,7 @@ #include "sys/ota_boot_request.h" #include "sys/task_manager.h" #include "sys/task_runner.h" -#if CONFIG_ETHERCAT_ENABLE #include "comm/ethercat_slave.h" -#endif namespace { constexpr uint32_t kAdcFilterWindow = CONFIG_ADC_FILTER_SIZE + 1U; @@ -214,7 +212,7 @@ void TaskRunner::taskStatusLed() { /* 预留 */ } * @retval 无返回值。 * @note 当前仅保留任务入口。 */ -void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); } +void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); CanFd::refreshFeedbackSnapshot(); } /** * @brief 输出当前温度、电压与 CAN 诊断信息。 @@ -266,6 +264,23 @@ void TaskRunner::taskDiagnosticPrint() static_cast(diag->tx_abort_count), static_cast(diag->error_count)); #endif + + { + if (EthercatSlave::isEscReady()) { + uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U; + EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt); + /* 180MHz: cycles / 180 = µs;用 *1000/180 获得毫微秒精度避免浮点 */ + const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U; + const uint32_t avgUs_x1000 = avgCyc * 1000U / 180U; + printf("[ECAT] periodicIsr cnt=%lu max=%lu.%03luus avg=%lu.%03luus\r\n", + (unsigned long)cnt, + (unsigned long)(maxUs_x1000 / 1000U), (unsigned long)(maxUs_x1000 % 1000U), + (unsigned long)(avgUs_x1000 / 1000U), (unsigned long)(avgUs_x1000 % 1000U)); + } else { + printf("[ECAT] ESC not detected (chip id=0x%08lX), EtherCAT disabled\r\n", + (unsigned long)EthercatSlave::getChipId()); + } + } } /** @@ -289,11 +304,7 @@ void TaskRunner::taskSpiCmdProcess() */ void TaskRunner::taskEthercatProcess() { -#if CONFIG_ETHERCAT_ENABLE EthercatSlave::process(); -#else - /* 未使能 EtherCAT 时此分支不应被置位;保留空操作以避免链接错误。 */ -#endif } /** @@ -328,6 +339,16 @@ void TaskRunner::runPending() { Watchdog::feed(); + /* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环, + * 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。 + * 优先于 takePending() 处理,确保周期数据及时消费。 + * ESC 未焊接时 selfTest 失败,isEscReady()=false,中断未使能, + * s_periodicSeq 不会递增,此分支自然不触发——双重守卫仅为防御性编程。 */ + if (EthercatSlave::isEscReady() && EthercatSlave::hasPendingPeriodic()) { + EthercatSlave::markPeriodicProcessed(); + taskEthercatProcess(); + } + const uint32_t pending = TaskManager::takePending(); if(pending == 0U) return; diff --git a/communication_ids.html b/communication_ids.html new file mode 100644 index 0000000..79a6adc --- /dev/null +++ b/communication_ids.html @@ -0,0 +1,338 @@ + + + + + DCU_APP SPI 通信 ID 参考 + + + + +
+

DCU_APP SPI 通信 ID(按注册命令展开,三行直观图)

+

按 device/spi/app_spi_command.cpp 注册顺序组织;每个命令均可展开查看 payload,并用“序号 / 组合 / 说明”三行显示。

+
⚠ 单帧 payload 最大 123 字节(kTxBufSize=128 - 协议开销 5 字节),超过将被 AppSpiCommand::processCommand() 拒收。
+ +
+ +
+

Boot Request

+
注册函数:registerSpiBootRequestCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + +
命令/响应符号值
命令帧类型kTypeBootReq0xB0
成功响应kRspAck0x80
+
payload 直观图
+
+
+
req
+
—
+
无可扩展 payload 字段
+
+
+
ack
+
0x80
+
成功应答
+
+
+
功能:请求进入 Boot 状态,回 ACK。
+
+
+
+ +
+

OTA Wait

+
注册函数:registerSpiOtaWaitCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + + + +
命令符号值
OTA 请求kTypeOtaReq0x01
OTA 数据kTypeOtaData0x02
OTA 取消kTypeOtaCancel0x03
响应kRspWait0x81
+ +
response payload 直观图
+
+
+
[0]
+
0x01
+
hint 固定值
+
+
+ +
功能:OTA 占位/流控,占位/流控。
+
+
+
+ +
+

App Ping

+
注册函数:registerSpiAppPingCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + +
命令/响应符号值
命令帧类型kTypeAppPing0xA1
响应kRspAck0x80
+ +
response payload 直观图
+
+
+
[0]
0xA1
magic
+
[1]
0x55
APP version tag
+
[2..5]
APP_VER
MAJOR/MINOR/PATCH/BUILD
+
[6]
0xAA
BOOT version tag
+
[7..10]
BOOT_VER
Boot版本
+
[11..14]
uptime
运行秒数
+
+
+ +
功能:读取版本与 uptime。
+
+
+
+ +
+

CAN Bridge

+
注册函数:registerSpiCanBridgeCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + + +
命令/响应符号值
命令帧类型kTypeCanSend0xC0
成功响应kRspAck0x80
错误响应kRspError0x82
+ +
request payload 直观图
+
+
+
[0]
can_ch
通道 0/1/2
+
[1..4]
can_id
CAN ID(32bit)
+
[5..N]
can_data
数据区
+
+
+ +
response payload 直观图
+
+
+
ack
0x00
OK
+
err
E0/E1/E2
短帧/通道/忙
+
+
+ +
错误码:E0=payload 不足,E1=通道非法,E2=发送忙/失败。
+
功能:ESP32 透传 CANFD 发送帧,校验后发送。
+
+
+
+ +
+

CAN Feedback (ID Snapshot) / Set Mode

+
注册函数:registerSpiCanFeedbackCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + + + +
命令符号值
反馈查询(分页)kTypeCanFeedback0xC1
模式切换kTypeCanSetMode0xC2
成功响应kRspAck0x80
错误响应kRspError0x82
+ +
0xC1 request payload 直观图(分页请求)
+
+
+
[0]
channel
通道 0/1/2
+
[1]
page
页号 0 或 1
+
+
+ +
0xC1 response payload 直观图(统计头 + ID 快照条目)
+
应答变长,最大 123 字节。固定头 25B + 条目逐条拼接。
+
+
固定头(25 字节)
+
+
[0]
channel
回显通道 0/1/2
+
[1]
page
回显页号
+
[2]
total_count
ID 条目总数(0~16)
+
[3]
entry_count
本页条目数
+
[4]
mode
当前接收模式
+
+
+
[5..8]
snapshot_seq
快照序号(LE)
+
[9..12]
rx_count
通道累计接收(LE)
+
[13..16]
tx_count
通道累计发送(LE)
+
[17..20]
rx_drop_count
通道丢帧(LE)
+
[21..24]
error_count
通道错误(LE)
+
+
条目区(从 [25] 开始,逐条拼接,变长)
+
+
[+0..+3]
id
CAN 报文 ID(LE)
+
[+4]
dlc
数据长度 1~64
+
[+5]
flags
bit0=FD bit1=BRS bit2=ESI
+
[+6..+7]
reserved
0x00
+
[+8..+N]
data
报文数据(dlc 字节)
+
+
+ +
+ 条目大小 = 8 + dlc 字节。标准 CAN (dlc=8):14B/条,每页最多 8 条。CANFD (dlc=64):72B/条,每页最多 1 条。
+ total_count = 实际不重复 ID 数(0~16,不满 16 就是实际值)。total_count ≤ 8 时 page=1 的 entry_count=0。
+ ESP32 每轮:每通道发 page=0 和 page=1 两次请求(共 6 次 SPI/s),拉完整张表。 +
+ +
0xC2 request payload 直观图
+
+
+
[0]
mode
0=全收 1=过滤
+
+
+ +
0xC2 response payload 直观图
+
+
+
[0]
actual
生效模式
+
+
+
错误码:E0=payload 不足,E1=参数非法(含非法通道),E3=页号非法。
+
注:E2=快照不可用 仅用于 0xC3,不在 0xC1/0xC2 中返回。
+
+
+
+ +
+

ID Snapshot(仅 ID 快照表)

+
注册函数:registerSpiIdSnapshotCommand
+
+ 展开查看 ID / payload / 功能 +
+ + + + + +
命令/响应符号值
命令帧类型kTypeIdSnapshot0xC3
成功响应kRspAck0x80
错误响应kRspError0x82
+ +
0xC3 request payload 直观图(分页请求)
+
+
+
[0]
channel
通道 0/1/2
+
[1]
page
页号 0 或 1
+
+
+ +
0xC3 response payload 直观图(轻量快照头 + ID 条目)
+
应答变长,最大 123 字节。固定头 4B + 条目逐条拼接。
+
+
固定头(4 字节)
+
+
[0]
channel
回显通道 0/1/2
+
[1]
page
回显页号
+
[2]
total_count
ID 条目总数(0~16)
+
[3]
entry_count
本页条目数
+
+
条目区(从 [4] 开始,逐条拼接,变长)
+
+
[+0..+3]
id
CAN 报文 ID(LE)
+
[+4..+7]
seq
该 ID 更新序号(LE)
+
[+8..+11]
rx_count
该 ID 累计接收(LE)
+
[+12]
dlc
数据长度 1~64
+
[+13]
flags
bit0=FD bit1=BRS bit2=ESI
+
[+14..+15]
reserved
0x00
+
[+16..+N]
data
报文数据(dlc 字节)
+
+
+ +
+ 条目大小 = 16 + dlc 字节。标准 CAN (dlc=8):24B/条,每页最多 5 条。CANFD (dlc=64):80B/条,每页最多 1 条。
+ total_count = 实际不重复 ID 数(0~16,不满 16 就是实际值)。total_count ≤ 8 时 page=1 的 entry_count=0。
+ ESP32 行为:当前代码实现了 0xC3,但主链路主要通过 0xC1 拉取“统计头 + ID 快照表”。 +
+ +
错误码:E0=payload 不足,E1=通道非法,E2=快照不可用,E3=页号非法。
+
+
+
+ +
+

公共响应 / 协议常量

+
来源:app_spi_command_defs.h、app_spi_command.cpp
+
+ 展开查看公共 ID / payload +
+
公共帧头与响应
+
+
SOF
0xA5
起始帧头
+
ACK
0x80
成功
+
WAIT
0x81
等待中
+
ERR
0x82
错误
+
+
公共响应 payload 长度与含义由各命令自行定义。
+
+
+
+ +
+ +

说明:CAN ID 在 CAN Bridge/Feedback 场景下为运行时值,不在文档中固化为常量。

+
+ + + + + + + + + + + + + + + + + +