Lely CANopen 启动与从机联机后的运行机制:Executor、Loop、Timer、CAN 与 Future/Promise

摘要:梳理 Lely CANopen 主站启动、从机 boot 和稳定运行阶段实际存在的任务、事件循环、逻辑定时器、CAN 报文及 Future/Promise 完成链路。
在这里插入图片描述

@[toc]

结论

以 Linux、SocketCAN、liblely-io2liblely-evliblely-coappcanopen::AsyncMaster 的典型主站为基线,可以先得到六个结论:

  1. 通常只有一个主 ev::Loop,不是每个从机一个 Loop。 多个从机的 NMT boot、SDO、Heartbeat 和 PDO 状态机可由同一个事件循环驱动。
  2. 通常只有一个 Loop executor。 AsyncMaster 将用户侧 OnBoot()OnConfig()OnState()OnHeartbeat()OnEmcy()OnRpdoWrite() 等回调包装为 ev_task,再投递到相应 Driver 的 executor。
  3. 不会为每个从机创建 boot 线程。 每个正在 boot 的从机有自己的 NMT boot 状态上下文、CAN receiver、逻辑 timer 和 SDO 状态,但由 CAN 接收事件、定时器到期事件和 SDO confirmation 推进。
  4. 一个 CANopen Node 通常只占用一个底层系统 Timer。 NMT、Heartbeat、SDO、PDO、SYNC、TIME 等大量 can_timer_t 通过 can_netio_tqueue 的两级最小堆,最终复用同一个 Linux timerfd
  5. CANopen 没有 TCP 式“连接建立”。 工程上通常把“从机 boot 成功、配置完成、错误控制已建立,并观测到从机进入 Operational”视为联机完成。
  6. Future/Promise 是一次异步请求的结果通道,不是连接对象。 每次 AsyncRead()AsyncWrite()、异步等待或异步反配置通常产生一个共享完成状态;CAN 响应、SDO abort、超时或取消最终完成 Promise,使 Future ready,并将后续 task 投递到 executor。

分析边界

本文按下列运行模型分析:

1
2
3
4
5
6
7
8
9
10
11
12
io::Context ctx;
io::Poll poll(ctx);
ev::Loop event_loop(poll.get_poll());
auto exec = event_loop.get_executor();
io::Timer timer(poll, exec, CLOCK_MONOTONIC);
io::CanController controller("can1");
io::CanChannel channel(poll, exec);
channel.open(controller);
canopen::AsyncMaster master(timer, channel, "master.dcf", "master.bin", master_id);
Driver driver(exec, master, slave_id);
master.Reset();
event_loop.run();

具体对象数量会受以下因素影响:

  • 使用 BasicMaster 还是 AsyncMaster
  • Driver 使用 BasicDriverFiberDriverLoopDriver 还是自定义 executor;
  • 主站 DCF 中 0x1F800x1F81、Heartbeat、SYNC、PDO、配置文件等参数;
  • 编译时是否关闭 NMT boot、node guarding、block SDO、LSS 等功能;
  • 同一进程中创建了多少个 CANopen Node
  • Linux SocketCAN、Windows IXXAT 或用户自定义 I/O 后端。

官方当前开发文档显示版本为 2.4.0;最新稳定发布页列出的稳定版本为 v2.3.5。两者的总体调度架构一致,但具体字段、条件编译和实现细节应以你实际编译版本为准。

一、运行时分层:哪些对象属于哪一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
flowchart TD
App["应用层 main 和 Driver"] --> Master["coapp::AsyncMaster"]
App --> DriverExec["Driver executor"]
Master --> Node["coapp::Node"]
Node --> CanNet["io::CanNet / io_can_net"]
Node --> Nmt["co_nmt_t 和 CANopen 服务"]

EvLoop["ev::Loop"] --> LoopExec["ev_loop executor"]
LoopExec --> TaskQ["ev_task 全局队列"]
Poll["io::Poll / epoll"] --> EvLoop

CanChan["io::CanChannel / SocketCAN"] --> Poll
SysTimer["io::Timer / timerfd"] --> Poll
LoopExec --> CanNet
CanNet --> CanChan
CanNet --> SysTimer

Nmt --> Boot["每从机 co_nmt_boot_t"]
Nmt --> Sdo["CSDO / SSDO"]
Nmt --> Pdo["RPDO / TPDO"]
Nmt --> Hb["Heartbeat / guarding"]
Nmt --> SyncTime["SYNC / TIME"]

Sdo --> LogicTimer["can_timer_t"]
Pdo --> LogicTimer
Hb --> LogicTimer
SyncTime --> LogicTimer
Boot --> LogicTimer
LogicTimer --> CanNet

Master --> DriverExec
DriverExec --> TaskQ

需要避免三个概念混淆:

概念 实际含义 不代表什么
ev_task 一个待执行函数及其目标 executor 的侵入式任务对象 不等于线程,不一定动态分配
executor 接受、排队、运行或取消 task 的抽象接口 不一定拥有独立线程
ev::Loop 保存任务队列、统计 outstanding work、在无任务时调用 Poll 的运行器 不理解 NMT、SDO、PDO 协议语义

CANopen 协议状态机通常在某个 CAN 接收任务或定时器任务的调用栈中同步推进若干状态;只有遇到“等待下一帧、等待 SDO confirmation、等待超时或等待应用配置完成”时才退出当前调用栈。不能把每个协议状态都理解成一个独立 ev_task

二、程序创建后、调用 Reset() 前存在什么

1. io::Context

io::Context 用于统一管理 I/O 服务生命周期和 shutdown。它不是 event loop,也不执行 CANopen 状态机。

2. io::Poll

Linux 下通常对应 epoll 后端,用于监听:

  • SocketCAN channel 的可读、可写或错误事件;
  • timerfd 到期;
  • SignalSet 对应的信号事件;
  • 其他已注册 I/O 服务。

3. ev::Loop 和主 executor

ev::Loop 内部拥有任务队列和一个标准 executor。循环的核心行为是:

1
2
3
4
5
6
7
8
9
10
11
12
队列中有真实 task
-> 取出一个 task
-> ev_exec_run(task->exec, task)
-> task 返回后继续

队列为空但还有 outstanding work
-> 调用 Poll 阻塞等待 I/O
-> I/O 完成处理把 task post 到 executor
-> Loop 被唤醒并执行 task

队列为空且没有 outstanding work
-> Loop 停止并返回

因此,如果没有 CAN read、timer wait、signal wait 或 Future waiter 等 outstanding work,event_loop.run() 会立即返回。

4. io::Timer

构造 io::Timer 只会创建并注册底层计时资源。Linux 路径通常包括:

1
2
3
timerfd_create()
-> 注册到 io::Poll
-> 等待未来 settime 和 submit_wait

创建 Timer 不等于已经开始任何 CANopen 周期定时。

5. io::CanChannel

打开 CAN channel 后,Linux 后端接入 SocketCAN。Node 构造过程中,io::CanNet::start() 开始处理 CAN 帧,通常形成一个持续重提交的异步读取链:

1
2
3
4
5
6
提交一次 CAN read
-> Poll 等待 SocketCAN 可读
-> 完成 read task
-> 将 CAN 帧交给 can_net
-> 匹配所有相关 can_recv_t
-> 重新提交下一次 CAN read

它不是不断占用 CPU 的轮询线程;没有帧时,执行线程阻塞在 Poll。

6. AsyncMaster 和 Driver

AsyncMaster 构造阶段完成对象装配、DCF/对象字典加载、NMT 回调注册和 Driver 注册表准备。官方接口明确说明:构造完成后仍处于 NMT Initialisation 状态,尚未启动完整 NMT boot-up 流程;需要调用 Reset()

注册一个 Driver 本身通常不会创建线程:

  • BasicDriver:不自动创建线程;
  • FiberDriver:创建或使用 fiber executor,fiber 仍由底层 event loop 线程运行;
  • LoopDriver:是例外,会为 Driver 建立专用 event loop,并使用独立线程运行;
  • 自定义 Driver executor:线程和并行语义由应用决定。

三、调用 master.Reset() 后产生什么

Reset() 的语义相当于本节点收到 NMT Reset Node。其主要作用不是“启动线程”,而是让本地主站 NMT 状态机进入 Reset Application、Reset Communication,再创建或启动当前对象字典允许的 CANopen 服务。

典型时序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sequenceDiagram
participant App as "应用 main"
participant Master as "AsyncMaster"
participant Nmt as "co_nmt_t"
participant CanNet as "io_can_net"
participant Chan as "SocketCAN channel"
participant Ev as "ev::Loop"

App->>Master: master.Reset()
Master->>Nmt: 注入 RESET_NODE
Nmt->>Nmt: Reset Application
Nmt->>Nmt: Reset Communication
Nmt->>CanNet: 启动接收器和逻辑定时器
Nmt->>CanNet: 发送本地主站 boot-up
CanNet->>Chan: 提交异步 CAN write
Master-->>App: Reset() 返回
App->>Ev: event_loop.run()
Ev->>Ev: 执行已投递 task 或阻塞在 Poll

Reset 后可能激活的协议对象

协议对象 何时存在或激活 主要事件来源
co_nmt_t Node 创建后存在,Reset() 后进入运行状态 NMT command、boot-up、Heartbeat、timer
NMT command receiver Reset Communication 后 CAN-ID 0x000
EMCY service 对象字典和编译配置允许时 错误上报、远端 EMCY
Server-SDO 本地 OD 中启用时 SDO request CAN 帧、transfer timeout
Client-SDO 主站为远端节点创建或请求时 SDO response、abort、timeout
RPDO/TPDO 对象字典中存在并有效时 CAN 帧、SYNC、event timer
SYNC service 0x1005/0x1006/0x1007 配置有效时 SYNC CAN、周期 timer
TIME service 0x1012 配置及 StartTime 调用允许时 TIME CAN、producer timer
Heartbeat producer/consumer 0x1017/0x1016 非零时 周期 timer、Heartbeat CAN
NMT boot slave 0x1F81 指定节点需要 boot 时 boot-up、SDO confirmation、timer

Reset 后常见的基础 task

以下不是固定线程,而是 event loop 中可能出现的 task 类型:

  1. CAN read completion task:读取一帧或一批 CAN 帧,交给 can_net 分发,再重提下一次 read。
  2. CAN write completion task:处理发送成功、错误或确认超时,并继续发送队列中的下一帧。
  3. Timer 内部 wait tasktimerfd 到期后读取到期计数,将到期的 io_tqueue_wait 投递给 executor。
  4. io_can_net.wait_next task:把当前单调时间回灌到 can_net_set_time(),触发所有到期的 can_timer_t
  5. Driver callback taskAsyncMaster 将 CANopen 事件包装后投递到 Driver executor。
  6. Future continuation task:某个 Promise 完成后,Future 中登记的 task 被投递到指定 executor。
  7. Signal wait task:SIGINT/SIGTERM 到来时执行清理逻辑。

四、主站开始 boot 一个从机时产生什么

“建立链接”在 Lely 中的准确含义

CANopen 是广播总线协议,没有 TCP connect,也没有一个长期保持的“连接句柄”。Lely 的 NMT master 通过 boot slave 流程建立的是以下逻辑事实:

1
2
3
4
5
目标 Node-ID 已被发现
+ 设备类型和身份符合主站 DCF 期望
+ 必要配置已下载
+ 错误控制已建立
+ 主站已执行启动策略

工程上还应额外确认从机 NMT state 已被观测为 Operational。因为 NMT Start Remote Node 是无确认命令,OnBoot() 成功不必然意味着从机已经被后续 Heartbeat/PDO 观测为 Operational。

每个 booting 从机的状态对象

Boot(id) 或自动 startup 策略启动某个从机时,Lely 不创建线程,而是使用该节点对应的状态上下文:

对象 作用
co_nmt_boot_t 保存当前 boot state、Node-ID、错误状态、超时和回调上下文
boot CAN receiver 接收目标节点 boot-up、Heartbeat 或 guarding response
boot logical timer Reset 等待、重试、状态检查和阶段超时
默认 Client-SDO 读取 0x1000、0x1018,下载配置或程序
configuration request context 执行 concise DCF 或进入用户 OnConfig() 扩展阶段
Master ready/configuring 标志 提供 IsReady()IsConfig() 等观察状态

boot 状态机可能在一次事件中连续穿过多个同步状态,但遇到下列事件边界时返回 event loop:

  • 等待从机 boot-up 或 Heartbeat;
  • 等待某个 boot timer 到期;
  • 等待 Client-SDO upload/download confirmation;
  • 等待配置子流程完成;
  • 等待用户调用 OnConfig() 提供的完成函数。

典型 boot CAN 序列

具体报文由 0x1F800x1F81、设备期望对象和配置文件决定。常见序列为:

1
2
3
4
5
6
7
8
9
主站 -> 0x000 : Reset Communication,目标为单节点或全部节点
从机 -> 0x700 + Node-ID : 00 boot-up
主站 -> 0x600 + Node-ID : SDO upload 0x1000 Device Type
从机 -> 0x580 + Node-ID : SDO response
主站 -> 0x600 + Node-ID : SDO upload 0x1018 Identity
从机 -> 0x580 + Node-ID : SDO response
主站 <-> 从机 : 可选配置 SDO、程序下载、PDO 参数下载
主站 -> 0x000 : Start Remote Node
从机 -> 0x700 + Node-ID : 后续 Heartbeat,状态通常为 0x05

按你当前默认从机测试计划中的 Node-ID 1,常用 CAN-ID 为:

功能 CAN-ID
NMT command 0x000
EMCY 0x081
SYNC 0x080
TIME 0x100
TPDO1 0x181
RPDO1 0x201
SDO request 0x601
SDO response 0x581
boot-up / Heartbeat 0x701

五、boot 期间会产生哪些 executor task

boot state machine 本身主要由 C 层 callback 推进,并不保证“每个状态一个 task”。典型执行链为:

1
2
3
4
5
6
7
8
9
10
SocketCAN 收到从机帧
-> Poll 返回
-> CAN read completion task 进入 loop
-> can_net_recv()
-> 匹配 boot receiver 或 CSDO receiver
-> boot/CSDO state handler 同步推进
-> 若产生 Master/Driver 事件
AsyncMaster 将 OnState/OnConfig/OnBoot 包装为 task
post 到 Driver executor
-> 当前 read task 返回

主要用户侧 task 包括:

task 触发条件 执行位置
OnState(BOOTUP/PREOP/OPERATIONAL/STOPPED) 收到远端状态变化 Driver executor
OnConfig(done) boot 进入用户扩展配置窗口 Driver executor
OnBoot(st, es, what) boot 流程成功或失败结束 Driver executor
OnHeartbeat(...) 收到 Heartbeat Driver executor
OnEmcy(...) 收到远端 EMCY Driver executor
OnRpdoWrite(...) 远端 TPDO 被主站 RPDO 接收并写入本地映射对象 Driver executor

BasicMaster,部分回调可能在协议事件调用栈中直接执行;对 AsyncMaster,Driver 回调被投递为 task,使协议栈回调与用户业务代码解耦。

OnConfig(done) 不是普通通知

OnConfig() 接收的 done/res 是 boot 状态机继续运行所必需的一次性完成回调:

1
2
3
4
5
6
NMT boot 将默认 Client-SDO 临时交给 Driver
-> AsyncMaster post OnConfig task
-> Driver 发起一个或多个异步 SDO
-> 所有配置完成后调用 done(error_code)
-> Master 将结果交还 NMT boot
-> boot 状态机继续

遗漏 done() 会使节点长期停留在 configuring 状态。boot 期间默认 Client-SDO 通常由 NMT master 独占;应用通常只能在 OnConfig() 窗口内,或 OnBoot() 后稳定使用它。

六、与从机联机后还剩下哪些长期任务

boot 完成后,不会留下一个“连接线程”。稳定运行阶段通常保持以下异步工作:

1. 永久 CAN read 链

只要 Node/CanNet 仍在运行,CAN channel 会持续保持下一次异步 read。它是 event loop 不退出的重要 outstanding work 来源之一。

2. CAN write 队列

NMT、SDO、PDO、SYNC、TIME、EMCY 和 Heartbeat 的发送最终汇入 CAN network/channel 的异步写路径。发送通常串行排队,写完成或失败后继续队列。

3. 周期或超时逻辑定时器

只有配置有效或请求正在进行时,对应 logical timer 才处于 active 状态。

类别 典型逻辑 timer 是否长期 active
主站 Heartbeat producer NMT error-control timer 0x1017 != 0 时周期 active
从机 Heartbeat consumer 每个监控节点一个 timeout timer 0x1016 配置有效时 active,并在每帧 Heartbeat 后重装
node guarding 每从机 guarding/lifetime timer 仅启用 legacy guarding 时 active
SYNC producer SYNC cycle timer 0x1006 != 0 且本节点为 producer 时周期 active
TIME producer TIME interval timer 调用 StartTime 且 interval 非零时 active
TPDO event timer、同步窗口 timer 对应 PDO 参数有效时 active
RPDO deadline/event monitoring、同步窗口 timer 对应参数有效时 active
CSDO transfer timeout timer 仅 SDO 请求 pending 时 active
SSDO segmented/block transfer timeout 仅一次服务端传输进行中时 active
NMT boot reset/check/retry timer 仅节点 booting 时 active,结束后停止
LSS response/inhibit/scan timer 仅 LSS 操作进行时 active
用户等待 SubmitWait()/AsyncWait() deadline 仅等待 pending 时 active

4. Driver 业务 task

稳定阶段每次收到 Heartbeat、EMCY、TPDO、状态变化或用户 Future 完成,仍会产生短生命周期 task。它们执行完就从队列消失,不是长期驻留线程。

七、为什么大量协议定时器最终只有一个 timerfd

Lely 的定时链分两级复用:

1
2
3
4
5
6
NMT / SDO / PDO / SYNC / TIME 的 can_timer_t
-> can_net.timer_heap,选出最早协议截止时间
-> io_can_net.wait_next
-> io_tqueue,和发送确认、用户等待一起再次选最早截止时间
-> 一个 io::Timer
-> 一个 Linux timerfd

io_can_net 至少直接维护两个 io_tqueue_wait

等待对象 作用
wait_next 等待 can_net 中最早的协议 logical timer 到期
wait_confirm CAN write confirmation 超时

Node 上提交的用户等待也使用同一 timer queue。底层 Timer 到期后,调用链大致为:

1
2
3
4
5
6
7
8
9
timerfd 到期
-> epoll 返回
-> Timer 内部 wait_task 被 post
-> 读取 timerfd 到期计数
-> io_tqueue 弹出所有已到期 wait
-> io_can_net.wait_next task 执行
-> can_net_set_time(now)
-> 弹出所有已到期 can_timer_t
-> 调用具体 NMT/SDO/PDO/SYNC/TIME timer callback

所以“一个主站有多少定时器”应分两层回答:

  • 系统 Timer 数:典型单 Node 主站为 1 个 io::Timer,Linux 下对应 1 个 timerfd
  • 协议 logical timer 数:可能有很多,数量与远端节点数、启用服务和当前 pending 请求相关。

八、Future/Promise 承诺机制如何参与 SDO 和配置

1. Promise 和 Future 的职责

对象 职责
Promise 异步操作生产者持有,发布成功值或错误,且只能完成一次
Future 调用方持有,观察 ready 状态、读取结果、登记 continuation task
task Future ready 后需要执行的动作,明确绑定 executor
executor 真正安排 task 的执行时机和执行线程

Promise 和 Future 共享同一控制块、同一结果区和引用计数,不是两个独立通信对象。

2. AsyncWrite() 的典型链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sequenceDiagram
participant Driver as "Driver task"
participant Req as "SDO request object"
participant Fut as "Future shared state"
participant CSDO as "co_csdo_t"
participant Bus as "CAN bus"
participant Exec as "executor"

Driver->>Req: AsyncWrite(index, subindex, value)
Req->>Fut: 创建 Promise/Future
Req->>CSDO: 提交 SDO download
CSDO->>Bus: 发送 SDO request
Req-->>Driver: 返回 Future

alt 收到成功响应
Bus-->>CSDO: SDO response
CSDO->>Fut: Promise set success
else 收到 SDO abort
Bus-->>CSDO: abort response
CSDO->>Fut: Promise set SdoError
else transfer timeout
CSDO->>Fut: Promise set timeout error
end

Fut->>Exec: post 已登记的 continuation task
Exec->>Driver: 恢复 Wait 或执行 submit callback

每个 pending AsyncRead()AsyncWrite() 通常至少对应:

  • 一个 SDO 请求状态对象;
  • 一个 Promise/Future 共享状态;
  • 一个 CSDO timeout logical timer;
  • 零个或多个在 Future 上登记的 completion task。

3. Future 本身不驱动 event loop

Future::get() 只允许读取已经 ready 的结果,不会负责处理 SocketCAN 或定时器。必须有线程持续运行 event_loop.run()wait() 或相应 executor,Future 才能因为 CAN response 或 timeout 而完成。

4. Wait() 为什么看起来像同步

  • FiberDriver::Wait(future):暂停当前 fiber,底层 event loop 继续处理其他 CAN 和 timer task;Future ready 后恢复该 fiber。
  • LoopDriver::Wait(future):运行 Driver 自己的专用或嵌套 event loop 等待 Future,容易引入重入、优先级反转或死锁风险。
  • 普通 BasicDriver:不应阻塞 event loop,通常使用 Future continuation 或 callback。

5. Future task 与 outstanding work

Future 尚未 ready 时,等待 task 还没有进入 event loop 的真实执行队列。Lely 会通过 on_task_init() 增加 outstanding work,防止 loop 因暂时没有可执行 task 而退出。Promise 完成并把 task post 到 executor 后,再通过 on_task_fini() 释放这份保活计数。

九、Loop 和线程数量的准确计算

典型 AsyncMaster + BasicDriver

1
2
3
4
5
主 ev::Loop:1
主 Loop executor:1
额外 Driver Loop:0
额外 Driver 线程:0
每从机 boot 线程:0

典型 AsyncMaster + FiberDriver

1
2
3
4
5
主 ev::Loop:1
主 Loop executor:1
Fiber executor:每 Driver 可有包装/上下文,但底层仍由主 Loop 驱动
额外 OS 线程:通常 0
每个并发 Driver 回调:作为 fiber task 运行

AsyncMaster + LoopDriver

1
2
3
主 ev::Loop:1
每个 LoopDriver:通常增加 1 个专用 Loop 和 1 个线程
N 个 LoopDriver:可能为 1 + N 个 Loop、N 个额外线程

event_loop.run() worker

同一个 ev::Loop 可以由多个线程调用 run()。这会提高并行性,但也意味着不同 task 可能并发执行。是否单线程、任务顺序是否串行、不同 Driver 是否互斥,取决于 executor、strand/fiber/loop 选择和应用锁设计,不能只凭 AsyncMaster 名称判断。

十、从启动到稳定运行的完整清单

阶段 新增或激活的对象/工作 主要停止条件
创建 Poll/Loop Poll backend、主 Loop、主 executor 对象析构
创建 Timer 一个底层 timerfd 和内部 wait task Timer shutdown/析构
打开 CanChannel SocketCAN channel、异步读写基础设施 channel shutdown/析构
创建 AsyncMaster CanNet、对象字典、NMT 对象、回调注册 Master 析构
注册 Driver node-ID 到 Driver/executor 的映射 Driver 解除注册/析构
Reset() NMT 服务、CAN receivers、有效 PDO/SDO/SYNC/TIME/EMCY 服务 Reset、stop 或析构
自动 boot 从机 每节点 boot state、receiver、timer、默认 CSDO 所有权 OnBoot() 成功/失败
OnConfig() Driver callback task、配置 SDO Future、完成回调 done(ec)
boot 完成 OnBoot() task、ready 状态 新 boot-up、reset、重新 boot
稳定运行 CAN read、周期 Heartbeat/SYNC/PDO timer、短期业务 task shutdown/reset/参数关闭
每次 Async SDO Promise/Future、SDO timer、completion task 响应、abort、timeout、cancel
shutdown cancel read/write/timer wait,执行剩余 completion task outstanding work 清零,Loop 返回

十一、针对你当前 Node-ID 1 从机的运行判断

按已提供的默认从机测试计划,联机后观察到以下组合时,才能较可靠地判断主站已进入稳定运行:

1
2
3
4
5
6
0x701#00                         从机 boot-up
0x601 / 0x581 boot 身份检查或配置 SDO
0x000#0101 主站发 Start Remote Node
0x701#05 从机 Heartbeat 已观测为 Operational
0x181 周期或事件发送 TPDO1 路径工作
0x201 写入后 OD 变化 RPDO1 路径工作

其中:

  • 只看到 OnBoot(es == 0):表示 Lely boot/config/error-control 流程成功;
  • 再看到 0x701#05:表示主站已从总线观测到从机 Operational;
  • 再看到预期 PDO:才说明 PDO 通道和应用映射已进入有效运行状态。

建议业务层把“设备可用”定义为:

1
2
3
boot_success
&& nmt_state == Operational
&& 必要的应用状态/PDO 首帧已满足

而不是只依据 OnBoot()

十二、最终心智模型

Lely CANopen 主站不是“主线程创建多个从机任务并等待连接”,而是一个分层异步系统:

1
2
3
4
5
6
7
一个或少量 event loop 线程
-> executor 运行短生命周期 ev_task
-> Poll 等待 SocketCAN、timerfd 和信号
-> CanNet 把 CAN 帧和时间交给被动协议栈
-> NMT/SDO/PDO/Heartbeat 等状态机由事件推进
-> AsyncMaster 把用户事件投递到 Driver executor
-> Future/Promise连接异步请求与后续 task

对典型单主站、多从机应用,最准确的资源描述是:

1
2
3
4
5
6
7
8
1 个主 ev::Loop
1 个主 executor
1 个 CANopen Node 对应 1 个专用 CanChannel
1 个 CANopen Node 对应 1 个专用 io::Timer/timerfd
N 个从机对应 N 份远端状态和按需激活的 boot/heartbeat/SDO 上下文
0 个固定的“每从机连接线程”
大量短生命周期 task、receiver 和 logical timer
每个异步请求对应一份一次性 Future/Promise 完成状态

参考资料

  1. Lely CANopen Library overview
  2. Lely CANopen C++ tutorial
  3. Lely core Node API
  4. Lely core BasicMaster API
  5. Lely core polling event loop API
  6. Lely core CAN timer API
  7. Lely issue #76: default Client-SDO ownership during boot
  8. Lely issue #92: OnBoot and earliest reliable PDO usage
  9. 用户提供的 canopen_mcu_slave_test_plan(2).md
  10. 用户提供的 Lely CANopen 系列源码学习笔记压缩包 canopen(1).zip