Lely CANopen 启动与从机联机后的运行机制:Executor、Loop、Timer、CAN 与 Future/Promise
摘要:梳理 Lely CANopen 主站启动、从机 boot 和稳定运行阶段实际存在的任务、事件循环、逻辑定时器、CAN 报文及 Future/Promise 完成链路。
@[toc]
结论
以 Linux、SocketCAN、liblely-io2、liblely-ev、liblely-coapp 和 canopen::AsyncMaster 的典型主站为基线,可以先得到六个结论:
- 通常只有一个主
ev::Loop,不是每个从机一个 Loop。 多个从机的 NMT boot、SDO、Heartbeat 和 PDO 状态机可由同一个事件循环驱动。 - 通常只有一个 Loop executor。
AsyncMaster将用户侧OnBoot()、OnConfig()、OnState()、OnHeartbeat()、OnEmcy()、OnRpdoWrite()等回调包装为ev_task,再投递到相应 Driver 的 executor。 - 不会为每个从机创建 boot 线程。 每个正在 boot 的从机有自己的 NMT boot 状态上下文、CAN receiver、逻辑 timer 和 SDO 状态,但由 CAN 接收事件、定时器到期事件和 SDO confirmation 推进。
- 一个 CANopen Node 通常只占用一个底层系统 Timer。 NMT、Heartbeat、SDO、PDO、SYNC、TIME 等大量
can_timer_t通过can_net和io_tqueue的两级最小堆,最终复用同一个 Linuxtimerfd。 - CANopen 没有 TCP 式“连接建立”。 工程上通常把“从机 boot 成功、配置完成、错误控制已建立,并观测到从机进入 Operational”视为联机完成。
- Future/Promise 是一次异步请求的结果通道,不是连接对象。 每次
AsyncRead()、AsyncWrite()、异步等待或异步反配置通常产生一个共享完成状态;CAN 响应、SDO abort、超时或取消最终完成 Promise,使 Future ready,并将后续 task 投递到 executor。
分析边界
本文按下列运行模型分析:
1 | io::Context ctx; |
具体对象数量会受以下因素影响:
- 使用
BasicMaster还是AsyncMaster; - Driver 使用
BasicDriver、FiberDriver、LoopDriver还是自定义 executor; - 主站 DCF 中
0x1F80、0x1F81、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 | flowchart TD |
需要避免三个概念混淆:
| 概念 | 实际含义 | 不代表什么 |
|---|---|---|
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 | 队列中有真实 task |
因此,如果没有 CAN read、timer wait、signal wait 或 Future waiter 等 outstanding work,event_loop.run() 会立即返回。
4. io::Timer
构造 io::Timer 只会创建并注册底层计时资源。Linux 路径通常包括:
1 | timerfd_create() |
创建 Timer 不等于已经开始任何 CANopen 周期定时。
5. io::CanChannel
打开 CAN channel 后,Linux 后端接入 SocketCAN。Node 构造过程中,io::CanNet::start() 开始处理 CAN 帧,通常形成一个持续重提交的异步读取链:
1 | 提交一次 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 | sequenceDiagram |
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 类型:
- CAN read completion task:读取一帧或一批 CAN 帧,交给
can_net分发,再重提下一次 read。 - CAN write completion task:处理发送成功、错误或确认超时,并继续发送队列中的下一帧。
- Timer 内部 wait task:
timerfd到期后读取到期计数,将到期的io_tqueue_wait投递给 executor。 io_can_net.wait_nexttask:把当前单调时间回灌到can_net_set_time(),触发所有到期的can_timer_t。- Driver callback task:
AsyncMaster将 CANopen 事件包装后投递到 Driver executor。 - Future continuation task:某个 Promise 完成后,Future 中登记的 task 被投递到指定 executor。
- Signal wait task:SIGINT/SIGTERM 到来时执行清理逻辑。
四、主站开始 boot 一个从机时产生什么
“建立链接”在 Lely 中的准确含义
CANopen 是广播总线协议,没有 TCP connect,也没有一个长期保持的“连接句柄”。Lely 的 NMT master 通过 boot slave 流程建立的是以下逻辑事实:
1 | 目标 Node-ID 已被发现 |
工程上还应额外确认从机 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 序列
具体报文由 0x1F80、0x1F81、设备期望对象和配置文件决定。常见序列为:
1 | 主站 -> 0x000 : Reset Communication,目标为单节点或全部节点 |
按你当前默认从机测试计划中的 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 | SocketCAN 收到从机帧 |
主要用户侧 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 | NMT boot 将默认 Client-SDO 临时交给 Driver |
遗漏 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 | NMT / SDO / PDO / SYNC / TIME 的 can_timer_t |
io_can_net 至少直接维护两个 io_tqueue_wait:
| 等待对象 | 作用 |
|---|---|
wait_next |
等待 can_net 中最早的协议 logical timer 到期 |
wait_confirm |
CAN write confirmation 超时 |
Node 上提交的用户等待也使用同一 timer queue。底层 Timer 到期后,调用链大致为:
1 | timerfd 到期 |
所以“一个主站有多少定时器”应分两层回答:
- 系统 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 | sequenceDiagram |
每个 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 | 主 ev::Loop:1 |
典型 AsyncMaster + FiberDriver
1 | 主 ev::Loop:1 |
AsyncMaster + LoopDriver
1 | 主 ev::Loop:1 |
多 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 | 0x701#00 从机 boot-up |
其中:
- 只看到
OnBoot(es == 0):表示 Lely boot/config/error-control 流程成功; - 再看到
0x701#05:表示主站已从总线观测到从机 Operational; - 再看到预期 PDO:才说明 PDO 通道和应用映射已进入有效运行状态。
建议业务层把“设备可用”定义为:
1 | boot_success |
而不是只依据 OnBoot()。
十二、最终心智模型
Lely CANopen 主站不是“主线程创建多个从机任务并等待连接”,而是一个分层异步系统:
1 | 一个或少量 event loop 线程 |
对典型单主站、多从机应用,最准确的资源描述是:
1 | 1 个主 ev::Loop |
参考资料
- Lely CANopen Library overview
- Lely CANopen C++ tutorial
- Lely core
NodeAPI - Lely core
BasicMasterAPI - Lely core polling event loop API
- Lely core CAN timer API
- Lely issue #76: default Client-SDO ownership during boot
- Lely issue #92: OnBoot and earliest reliable PDO usage
- 用户提供的
canopen_mcu_slave_test_plan(2).md - 用户提供的 Lely CANopen 系列源码学习笔记压缩包
canopen(1).zip









