Files
DCU_APP/.trae/specs/fix-ecat-postaudit-risks/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

11 KiB

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)