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,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 化,闭环
@@ -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` 现有分支
@@ -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 全部完成