- 添加CAN反馈和ID快照SPI命令文件 - 移除EXMC组件依赖,精简项目配置 - 添加RAM函数段链接脚本,优化Flash等待周期抖动 - 实现EscExmcBus类,提供16位并行总线访问LAN9252 - 实现EscLan9252驱动,支持寄存器和PDI内存访问 - 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换 - 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式 - 添加ISR执行时间统计和诊断功能 - 分离周期路径和事件路径通知机制,避免RMW竞态
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-ONLYcomm/ethercat_slave.cpp:process()注释修正comm/ethercat_sync.cpp:EXTI0_IRQHandler/onSync0Exti加 .ramfunc,标志位清零改直接寄存器写comm/esc_exmc_bus.h/comm/esc_exmc_bus.cpp:删除 readBlock/writeBlocksys/initial.cpp:init() 开头增加 nvic_priority_group_setdevice/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
- H1 不引入运行期 ESC 代访队列:本 spec 仅关闭 safe* 运行期误用通道(去 PRIMASK + 改注释),不实现"主循环请求 → SYNC0 ISR 代访 ESC 寄存器"的队列机制。当前主循环无运行期 ESC 寄存器访问需求,代访队列属过度设计,留待未来协议栈集成时按需引入
- M1 仅 RAM 化 SYNC0 路径:EXTI1(ESC IRQ)/EXTI2(SYNC1)为 preempt=1,BASEPRI 可屏蔽,非 <1µs 抖动关键路径,不改。保持改动最小化
- M2 不调整外设 init 顺序:仅将
nvic_priority_group_set前移到 init() 开头,不重排CanFd::init()/AppSpiCommandBus.init()/SchedulerTimer::init()/EthercatSync::init()的相对顺序,避免引入未知回归 - M3 删除而非标注:readBlock/writeBlock 无调用点且功能被 safe*Pdi 覆盖,删除优于保留+标注(减少未来维护负担与陷阱面)
- H1 与上一轮 Task 2 的关系:本 spec 的 H1 阶段实质是回退上一轮 Task 2.2"用 PRIMASK 而非 BASEPRI"的决策。该决策在当时看似合理(阻塞 preempt=0 保护 EXMC),但忽略了"主循环根本不应在运行期访问 ESC"这一更优解,且 PRIMASK 关中断时间(10~30µs)远超 <1µs 抖动预算。本 spec 用"禁止运行期访问"替代"PRIMASK 保护运行期访问"
- 不包含:DWT 测量复查、map 文件验证(属上一轮 Task 6 编译验证范畴,本 spec 假设编译环境已就绪或独立验证)、LOW 风险项(L1-L6)