CanFestival 协议栈解析笔记(一)
一、协议栈分层架构
┌──────────────────────────────────────────────┐
│ CiA 402 驱动层 (cia402_fsm / modes / servo) │ ← 应用层状态机
├──────────────────────────────────────────────┤
│ CANopen 协议核心 │
│ ┌────────┬───────┬──────┬───────┬─────────┐ │
│ │ NMT │ SDO │ PDO │ EMCY │ SYNC │ │ ← CiA 301 协议栈
│ │ co_nmt │co_sdo │co_pdo│co_emcy│co_sync │ │
│ └────────┴───────┴──────┴───────┴─────────┘ │
├──────────────────────────────────────────────┤
│ 对象字典 OD (co_od) — 二分查找 + 回调机制 │ ← 数据总线层
├──────────────────────────────────────────────┤
│ 硬件抽象层 (can_send 函数指针) │ ← 驱动接口
├──────────────────────────────────────────────┤
│ CAN 控制器中断 / 定时器中断 │ ← 中断层
└──────────────────────────────────────────────┘
关键设计特点:
| 模块 | 核心函数 | 被调用上下文 |
|---|---|---|
| NMT | co_nmt_process_command() | CAN RX 中断 |
| NMT | co_nmt_tick() | 定时器中断 (心跳) |
| SDO | co_sdo_server_process() | CAN RX 中断 |
| SDO | co_sdo_server_tick() | 定时器中断 (超时) |
| PDO | co_pdo_on_rpdo() | CAN RX 中断 |
| PDO | co_pdo_on_sync() | CAN RX 中断 (SYNC帧) |
| PDO | co_pdo_mark_changed() | 应用层/主循环 |
| PDO | co_pdo_tick()/co_pdo_tick_ms() | 定时器中断 |
| EMCY | co_emcy_trigger() | 任意上下文 |
| EMCY | co_emcy_tick() | 定时器中断 (重发) |
二、canDispatch与TimeDispatch的中断关系
在经典 CanFestival 中,两者都存在,且都需要在中断上下文中被调用:
canDispatch() ← 在 CAN RX 中断 ISR 中调用 TimeDispatch() ← 在硬件定时器 ISR(如 SysTick)中调用场景分析(以 ARM Cortex-M 为例)
NVIC 优先级配置 (数字越小优先级越高): 场景 A — 同优先级(推荐): CAN_RX_IRQ 优先级 = 4 TIMER_IRQ 优先级 = 4 → 不会互相抢占 (Cortex-M 对同优先级用尾链 Tail-Chaining) → canDispatch 和 TimeDispatch 是原子的,不会嵌套 场景 B — CAN 优先级高于定时器: CAN_RX_IRQ 优先级 = 2 TIMER_IRQ 优先级 = 4 → canDispatch 可以抢占 TimeDispatch ⚠️ 危险 → TimeDispatch 修改 OD 数据时,canDispatch 也可能读写同一 OD 条目 场景 C — 定时器优先级高于 CAN: → TimeDispatch 可以抢占 canDispatch ⚠️ 同样危险同优先级是安全
看关键代码路径。以RPDO 写入 OD为例,在co_pdo_on_rpdo()中:
// src/canopen/co_pdo.c — co_pdo_on_rpdo() od_write(pdo->od, me->index, me->subindex, buf, byte_len); 而 od_write() 内部是纯 memcpy(src/canopen/co_od.c:73): memcpy(entry->pdata, buf, size);没有任何锁保护。如果TimeDispatch(定时器中断)正在通过co_pdo_tick_ms()遍历 TPDO 并调用od_read(),而canDispatch(CAN 中断)同时通过co_pdo_on_rpdo()写入同一个 OD 条目——这就是典型的数据竞争。
但若两者同优先级,Cortex-M 内核保证:
- 当 CPU 正在处理 TIMER_IRQ 时,同优先级的 CAN_RX_IRQ 不会被响应,而是等到 TIMER_IRQ 返回后才进入
- 反之亦然
结论:canDispatch和TimeDispatch在设计良好的系统中不会互相打断,前提是 CAN RX 中断和定时器中断配置为同一优先级。
三、高优先级 ADC 中断能否抢占它们?
能,而且这是一个需要特别注意的问题。
NVIC 优先级: ADC_IRQ 优先级 = 1 (最高) CAN_RX_IRQ 优先级 = 4 TIMER_IRQ 优先级 = 4 当 canDispatch 或 TimeDispatch 正在执行时,优先级更高的 ADC 电流采样中断会直接抢占: 时间轴: ──────────────────────────────────────────────────── TIMER_IRQ 开始 → TimeDispatch() 执行到一半 ↓ ADC 触发 ADC_IRQ 抢占 ───── ADC_ISR 执行 ───── 返回 ↓ TimeDispatch() 继续执行 ────────────────────────────────────────────────────这是否危险?取决于 ADC ISR 做了什么。
危险场景 — ADC ISR 访问了 CANopen 数据:
如果 ADC ISR 中通过od_read()读取电流反馈值(如0x6078实际电流),而canDispatch正在通过od_write()修改同一 OD 条目:
// ADC ISR 中: od_read(od, 0x6078, 0, &buf, &size); // 读 current_actual_value // 同时 canDispatch 在 SDO 处理中: od_write(sdo->od, index, sub, &write_data, data_size);由于od_read/od_write内部是裸memcpy,会导致读到撕裂值 (torn read)——数据部分来自写入前、部分来自写入后。
安全场景 — ADC ISR 只写自己的 buffer:
如果 ADC ISR 使用 DMA 双缓冲,将采集数据写入独立的adc_buffer[],在主循环中再同步到 OD,则抢占是安全的。
仓库的实际情况
查看 OD 回调机制(src/canopen/co_od.c:74-80):
if (od->post_write_cb) { uint32_t r = od->post_write_cb(index, subindex, entry->pdata, size, true); if (r != 0) return r; }如果 ADC ISR 的高优先级抢占导致 post_write_cb 中的回调函数 (可能触发电机控制参数更新)执行到一半被更高优先级中断嵌套, 会产生不可预期的行为,因为OD 回调函数本身没有可重入保护。四、总结与建议
| 问题 | 结论 |
|---|---|
| canDispatch 与 TimeDispatch 能否互抢? | 不应该。应配置 CAN RX 和 Timer 中断为同优先级,利用 Cortex-M 尾链机制保证原子性 |
| 高优先级 ADC ISR 能否抢占? | 能。ARM NVIC 允许高优先级中断抢占低优先级 |
| 抢占是否有风险? | 有条件:若 ADC ISR 只写独立 buffer 则安全;若读写 OD 结构体则存在数据竞争 |
推荐的加固措施:
- 配置 CAN RX 和 Timer 中断为同一优先级(你已经提到)
- 保护 OD 关键区——在
od_read/od_write周围使用__disable_irq()/__enable_irq()或 BASEPRI 屏蔽(参考经典 CanFestival 的EnterMutex/LeaveMutex宏) - ADC ISR 使用独立的数据通道——通过 DMA 双缓冲写入,不直接访问
od_entry_t.pdata - 使用 BASEPRI 精确屏蔽——只屏蔽 CANopen 相关优先级,不阻断 ADC:
// 进入 CANopen 关键区时 UINT32 oldbasepri = __get_BASEPRI(); __set_BASEPRI(3 << (8 - __NVIC_PRIO_BITS)); // 屏蔽优先级 4-15 od_write(...); // 安全操作 __set_BASEPRI(oldbasepri); // 恢复, ADC (优先级1) 仍可抢占这样 ADC 仍能实时采样,而 CANopen 协议栈内部的数据一致性得到保障。
以上就是完整的分析。
核心要点:canDispatch 和 TimeDispatch 若配置为同优先级则不会互相打断(利用 Cortex-M 尾链机制),但高优先级 ADC 中断确能抢占它们。是否安全取决于 ADC ISR 是否访问共享数据。如需加固,BASEPRI 是兼顾实时性和数据一致性的最优方案。