Files
DCU_APP/.trae/specs/fix-ecat-pdo-and-runtime-issues/spec.md
T
cbb b171767023 feat(ethercat): 添加EtherCAT从站功能支持并优化同步机制
- 添加CAN反馈和ID快照SPI命令文件
- 移除EXMC组件依赖,精简项目配置
- 添加RAM函数段链接脚本,优化Flash等待周期抖动
- 实现EscExmcBus类,提供16位并行总线访问LAN9252
- 实现EscLan9252驱动,支持寄存器和PDI内存访问
- 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换
- 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式
- 添加ISR执行时间统计和诊断功能
- 分离周期路径和事件路径通知机制,避免RMW竞态
2026-06-29 01:51:02 +08:00

17 KiB

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 范畴