Files
DCU_APP/.trae/specs/fix-ethercat-realtime-comm-risks/spec.md
T

148 lines
12 KiB
Markdown
Raw Normal View History

# 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` 现有分支