- 添加CAN反馈和ID快照SPI命令文件 - 移除EXMC组件依赖,精简项目配置 - 添加RAM函数段链接脚本,优化Flash等待周期抖动 - 实现EscExmcBus类,提供16位并行总线访问LAN9252 - 实现EscLan9252驱动,支持寄存器和PDI内存访问 - 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换 - 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式 - 添加ISR执行时间统计和诊断功能 - 分离周期路径和事件路径通知机制,避免RMW竞态
12 KiB
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 访问接口分为两类:
- ISR-only(无锁):
EscExmcBus::read16/write16/readBlock/writeBlock、EscLan9252::readReg/writeReg/readPdi/writePdi,仅供 preempt=0 的periodicIsr()调用 - 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
- 修复时机:本 spec 所有改动在
CONFIG_ETHERCAT_ENABLE=0下不触发编译(因comm/代码被#if包裹),但改动完成后必须将CONFIG_ETHERCAT_ENABLE临时设为 1 进行编译验证,验证后恢复为 0 - 三缓冲容忍度:三缓冲容忍主循环跨 1 个周期(即主循环处理时间 ≤ 2 × 周期时间)。100µs 周期下,主循环有 200µs 处理窗口,远超 32 字节 PDO 映射所需(µs 级),实际不会触发容忍上限
- PRIMASK 在主循环 safe 接口中的影响:主循环访问 ESC 寄存器(AL 状态查询等)不频繁,PRIMASK 关中断时间约 1~2µs(单次寄存器读写),对 EtherCAT 抖动影响可忽略。若后续协议栈需要频繁主循环 EXMC 访问,需重新评估
- DWT 可用性:GD32E508(Cortex-M4F)支持 DWT,
DWT_CYCCNT可用。需在初始化时使能 DWT 与 DEMO 寄存器 - 链接脚本格式:项目使用 AC6(armclang),链接脚本为
.sct格式(SCT 文件)。.ramfunc段定义需符合 SCT 语法 - 不包含:完整 EtherCAT 协议栈集成(SSC)、EEPROM/ESI 配置、应用对象字典——这些由后续 spec 处理,本 spec 仅修复通信基础设施风险
- TASK_ETHERCAT_PROCESS 枚举保留:虽然周期路径不再使用,但 ESC IRQ 路径仍用,枚举不删除,避免破坏
task_runner.cpp现有分支