feat(ethercat): 添加EtherCAT从站功能支持并优化同步机制

- 添加CAN反馈和ID快照SPI命令文件
- 移除EXMC组件依赖,精简项目配置
- 添加RAM函数段链接脚本,优化Flash等待周期抖动
- 实现EscExmcBus类,提供16位并行总线访问LAN9252
- 实现EscLan9252驱动,支持寄存器和PDI内存访问
- 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换
- 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式
- 添加ISR执行时间统计和诊断功能
- 分离周期路径和事件路径通知机制,避免RMW竞态
This commit is contained in:
cbb
2026-06-29 01:51:02 +08:00
parent 1612e326ee
commit b171767023
31 changed files with 2177 additions and 290 deletions
@@ -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 回绕周期覆盖任何合理主循环阻塞,闭环
@@ -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 范畴
@@ -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(编译验证)