Lely CANopen coapp::Master:NMT 总控、SDO 仲裁与 Driver 事件分发机制

摘要:从 master.hpp 与 master.cpp 追踪 BasicMaster/AsyncMaster 如何组织 NMT 启动、SDO 所有权、配置握手及 Driver 事件分发。

在这里插入图片描述

@[toc]
lely::canopen::BasicMasterlely::canopen::AsyncMaster 是 Lely CANopen C++ 应用层中负责主站编排的核心对象。它们并不是简单地把 NMT、SDO、PDO API 包装成 C++ 成员函数,而是在 Node 的本地 CANopen 节点能力之上,进一步维护远端节点 Driver、节点启动状态、配置阶段状态以及 Client-SDO 使用权。

理解这两个类的关键,不是逐个记住 SubmitRead()Command()OnBoot() 等接口,而是建立下面这条主线:

1
2
3
4
5
本地主站 Node 启动
-> NMT master 发起或接管远端节点 boot slave 流程
-> boot 过程中独占或临时移交 Client-SDO
-> Driver 完成设备相关配置
-> Master 汇总状态并分发 NMT/PDO/EMCY/SYNC/TIME 事件

1. 先建立整体心智模型

BasicMaster 同时承担四类职责:

  1. 本地 CANopen 节点:继承 Node,拥有本地对象字典、NMT、PDO、SYNC、TIME、EMCY、CAN channel、timer 和 executor 等能力。
  2. 远端节点编排器:通过 NMT master 的 boot slave 流程识别、检查、配置并启动从站。
  3. Driver 注册表:按 node-ID 保存一个 DriverBase*,把属于某个远端节点的事件交给对应 Driver。
  4. Client-SDO 仲裁器:决定应用层 SDO 当前是否可用,以及由普通请求、NMT boot 还是配置阶段占用。

AsyncMaster 没有重新实现一套协议状态机。它继承 BasicMaster,主要改变“事件如何进入用户 Driver”:

  • BasicMaster 在协议事件处理路径中直接调用 Driver 回调,但调用前暂时释放 Master 锁;
  • AsyncMaster 把回调封装为 task,投递到该 Driver 的 executor,当前协议回调随后返回。

官方教程也用这个差异解释 AsyncMaster:用户定义的 CANopen 事件回调会作为事件循环任务执行,而不是直接嵌在协议栈事件处理调用栈中。[S3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flowchart LR
App["应用程序"] --> Master["BasicMaster / AsyncMaster"]

Master --> Node["Node:本地主站节点"]
Node --> Dev["Device:本地对象字典"]
Node --> Nmt["NMT / PDO / EMCY / SYNC / TIME"]
Node --> Io["CAN channel / timer / executor"]

Master --> Registry["Driver 注册表 node-ID -> DriverBase*"]
Master --> State["ready[] / config[]"]
Master --> Sdos["sdos:按节点维护 Client-SDO"]

Nmt --> Master
Master --> DriverA["Driver:node 2"]
Master --> DriverB["Driver:node 3"]

这张图中最重要的关系是:Master 本身仍然是一个 CANopen Node;Driver 才是远端设备的应用层接口。


2. master.hpp 定义了什么抽象

2.1 继承关系不是装饰,而是职责组合

master.hpp 中的核心声明可以概括为:

1
2
3
4
class BasicMaster : public Node,
protected std::map<uint8_t, DriverBase*>;

class AsyncMaster : public BasicMaster;

这意味着:

  • Node 继承本地主站所需的协议服务和对象字典访问能力;
  • 从受保护的 std::map 继承 Driver 容器行为;
  • 派生类可以按关联容器方式遍历 Driver,但外部应用不能随意破坏注册表;
  • AsyncMaster 只需覆盖事件入口,即可改变整个 Driver 回调的执行上下文。[S1][S4]

2.2 BasicMaster::Impl_ 保存真正的主站协调状态

master.cpp:45-68 中的私有实现包含以下数据:

成员 作用
self 回指所属 BasicMaster
on_node_guarding 可注册的 node guarding 通知函数
on_boot 可注册的 boot 完成通知函数
ready[CO_NUM_NODES] 每个 node-ID 的 boot-ready 标志
config[CO_NUM_NODES] 每个 node-ID 是否处于 update configuration 阶段
sdos node-ID -> Sdo,保存默认或配置阶段 Client-SDO 队列

这里没有保存一份“远端完整对象字典”。远端设备类型、业务行为和回调由 Driver 表达;Master 只保存完成编排所需的最小状态。

2.3 三组访问接口对应三种不同的数据路径

BasicMaster 对外暴露的对象访问接口容易混淆,实际上分别对应:

接口形态 操作对象 是否产生远端 SDO 报文
master[idx][subidx] 主站本地对象字典
master.RpdoMapped(id)[idx][subidx] 主站 RPDO 代理中缓存的远端数据
master.TpdoMapped(id)[idx][subidx] 主站 TPDO 代理中准备发送给远端的数据
SubmitRead/AsyncRead 远端节点对象字典
SubmitWrite/AsyncWrite 远端节点对象字典

从总线方向理解更直观:

  • RpdoMapped(id) 是只读入口,读取主站已经通过 RPDO 接收的数据;这些数据来源于远端节点的 TPDO。
  • TpdoMapped(id) 是读写入口,修改主站本地 TPDO 代理;随后数据可通过主站 TPDO 发往远端节点的 RPDO。
  • 只有 SDO API 才会按 index/sub-index 对远端对象字典发起请求。

因此,PDO 映射访问是“本地代理对象访问”,SDO 访问才是“远端对象字典事务”。[S1][S3]


3. 从构造到 Reset():Master 何时真正开始工作

3.1 构造函数只完成装配

BasicMaster 支持三种设备描述来源:

  1. 已创建的 co_dev_t*
  2. 文本 EDS/DCF 与可选 concise DCF;
  3. 静态设备描述 co_sdev*

三个构造函数都遵循同一结构:

1
2
3
4
5
构造 Node
-> 构造 BasicMaster::TpdoEventMutex
-> 创建 Impl_
-> 将 Node 内部的 co_nmt_t* 交给 Impl_
-> 注册 NMT boot/config/node-guarding indication

官方 API 明确说明:构造完成后 Master 仍处于 NMT Initialisation 状态,不会立即创建全部服务或进行通信;调用 Reset() 后才启动本地 NMT boot-up。[S2][S4]

3.2 dcf_txtdcf_bin 的角色

构造参数中的:

  • dcf_txt 描述主站本地对象字典和 NMT 配置;
  • dcf_bin 是加载到主站本地对象字典的 concise DCF;
  • 对远端从站的自动配置通常由主站 DCF 中的网络管理对象引用相应配置内容,再由 NMT boot slave 流程执行。

官方 C++ 教程使用 dcfgen 生成 master.dcf,并说明某些配置还会生成 master.bin;Master 构造后调用 Reset(),由本地 NMT 服务开始运行。[S3]

下面是用于说明装配关系的最小化代码,省略 I/O 初始化细节:

1
2
3
4
5
6
7
lely::canopen::AsyncMaster master(
executor, timer, channel, "master.dcf", "master.bin", 1);

MyDriver node2_driver(driver_executor, master, 2);
MyDriver node3_driver(driver_executor, master, 3);

master.Reset();

这里的顺序有实际含义:

  • Master 必须先存在,Driver 才能注册到它;
  • Driver 应在远端节点事件到来前完成注册;
  • Reset() 只是启动本地主站 NMT,具体是否自动 boot 某个从站还受主站 DCF 中 CiA 302 网络管理配置影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant App as "Application"
participant M as "AsyncMaster"
participant N as "Node / co_nmt_t"
participant D as "Driver"
participant E as "event loop"

App->>M: "construct(timer, channel, DCF, node-ID)"
M->>N: "construct local CANopen node"
M->>N: "register boot/config indications"
App->>D: "construct and register node-ID"
App->>M: "Reset()"
M->>N: "enter reset application / communication"
N->>E: "schedule CANopen and I/O work"

4. Driver 注册表如何把多从站拆成独立接口

4.1 一个 node-ID 只能注册一个 Driver

BasicMaster::Insert() 在加锁后检查:

  • node-ID 必须位于 1…127;
  • 不能等于 Master 自己的 node-ID;
  • 同一 node-ID 不能重复注册。

通过检查后,注册表保存:

1
node-ID -> DriverBase*

Erase() 只删除与当前指针完全匹配的 Driver,并先取消该节点的 SDO 队列。这使该节点的排队或进行中 SDO 与 Driver 注册生命周期一并结束。[S2]

4.2 事件分为“广播型”和“单节点型”

事件 分发范围 典型 Driver 回调
CAN state 变化 所有 Driver OnCanState()
CAN error 所有 Driver OnCanError()
Master 本地 NMT command 所有 Driver OnCommand()
SYNC 所有 Driver OnSync()
SYNC 长度错误 所有 Driver OnSyncError()
TIME 所有 Driver OnTime()
某节点 PDO 数据写入 指定 node-ID OnRpdoWrite()
heartbeat 发生/恢复 指定 node-ID OnHeartbeat()
远端 NMT state 变化 指定 node-ID OnState()
EMCY 指定 node-ID OnEmcy()
node guarding 超时/恢复 指定 node-ID OnNodeGuarding()
boot slave 完成 指定 node-ID OnBoot()
update configuration 指定 node-ID OnConfig()

这种划分使主站可以同时管理不同类型的从站:通用总线事件广播给所有 Driver,设备相关事件只进入对应 node-ID 的 Driver。


5. Boot(id):单节点启动流程的编排入口

5.1 Boot() 不是发送一帧 NMT 命令

BasicMaster::Boot(id) 请求的是完整 NMT boot slave 过程,而不是简单调用一次 Command(START, id)。其等价逻辑如下:

1
2
3
4
5
6
7
校验 node-ID
-> 获取 Master 锁
-> 若该节点已经 booting,返回 false
-> 取消该节点正在执行或排队的应用 SDO
-> 暂时清除 ready 标志
-> co_nmt_boot_req(...)
-> 请求提交失败时恢复原 ready 状态并报错

为什么先取消 SDO?源码注释直接给出原因:NMT master 在 boot slave 过程中可能需要使用同一个默认 Client-SDO 服务,例如读取设备类型、身份对象或下发配置。[S2]

5.2 boot slave 的核心阶段

master.cpp 并未重新实现 CiA 302 boot 状态机;它调用 C 层 NMT 服务,并通过 indication 接收关键节点:

  • co_nmt_set_boot_ind():boot slave 完成;
  • co_nmt_set_cfg_ind():进入 update configuration 阶段;
  • co_nmt_set_ng_ind():node guarding 事件。

因此 BasicMaster 的作用更接近“C 层 NMT 状态机的 C++ 编排和应用桥接层”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
participant App as "Application"
participant M as "BasicMaster"
participant N as "C NMT master"
participant S as "Remote slave"
participant D as "Driver"

App->>M: "Boot(id)"
M->>M: "CancelSdo(id), ready=false"
M->>N: "co_nmt_boot_req(id, timeout)"
N->>S: "NMT / SDO checks"

opt "需要应用层更新配置"
N->>M: "configuration indication with co_csdo_t*"
M->>D: "OnConfig(done)"
D-->>M: "done(error_code)"
M->>N: "co_nmt_cfg_res(id, abort-code)"
end

N->>M: "boot indication(id, state, error-status)"
M->>M: "update ready flag"
M->>D: "OnBoot(state, status, description)"

5.3 ready 的准确语义

官方 API 对 IsReady(id) 的定义是:

  • 该节点的 boot slave 流程已经成功完成;
  • 此后没有再次收到该节点的 boot-up 事件;
  • 调用 AsyncDeconfig() 也会将其标记为 not ready。[S4]

Impl_::OnBootInd() 在无错误,或错误状态字符为 L 时,把 ready[id - 1] 置为 trueL 表示从站最初已处于 Operational,NMT master 可以继续处理其他节点。[S2][S4]

需要特别区分:

1
IsReady(id) == true

不等价于:

1
远端设备此刻已经完成应用内部初始化,并稳定处于 Operational

维护者在 issue #92 中解释过:NMT start 命令没有确认机制,OnBoot() 可能在从站真正进入 Operational 前先被调用;判断 PDO 是否已经可靠可用,应同时观察 Master 和从站的 NMT state。[S6]


6. Client-SDO 为什么必须由 Master 仲裁

这是 master.cpp 中最关键、也最容易在实际项目中触发异常的设计。

6.1 默认 SDO 不是永久可用资源

GetSdo(id) 按以下顺序决定是否返回 Client-SDO:

条件 结果 原因
node-ID 非法 抛出范围异常 请求本身无效
Master 不在 Pre-operational 或 Operational 返回空 CSDO 服务在当前 NMT 状态不可用
sdos[id] 已存在 返回现有队列 可能是普通默认 SDO,也可能是配置阶段临时包装
该节点正在 boot slave 返回空 NMT master 正在占用默认 Client-SDO
其余情况 延迟创建并返回默认 Sdo 应用可提交普通远端 SDO 请求

这里的判断顺序很重要:配置阶段的 sdos[id] 检查位于 booting 检查之前。 这使得节点虽然仍处于 boot slave 流程,Driver 在 OnConfig() 内却可以合法使用 NMT 服务临时移交的 Client-SDO。

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
Start["GetSdo(id)"] --> IdOk{"node-ID 有效?"}
IdOk -- "否" --> Throw["抛出 out_of_range"]
IdOk -- "是" --> StateOk{"Master 为 PREOP 或 START?"}
StateOk -- "否" --> NoneA["返回 nullptr / NO_SDO"]
StateOk -- "是" --> Existing{"sdos[id] 已存在?"}
Existing -- "是" --> ReturnExisting["返回现有 Sdo"]
Existing -- "否" --> Booting{"节点正在 booting?"}
Booting -- "是" --> NoneB["返回 nullptr / NO_SDO"]
Booting -- "否" --> Create["创建默认 Client-SDO 队列"]
Create --> ReturnNew["返回新 Sdo"]

6.2 所有 SDO API 最终都经过同一闸门

SubmitRead()SubmitWrite()、block 传输、future 版本以及 DCF 下载接口,虽然模板重载很多,主干基本一致:

1
2
3
4
5
获取 Master 锁
-> GetSdo(id)
-> 更新 CAN network time
-> 向 Sdo 队列提交请求
-> 不可用时返回或抛出 SdoErrc::NO_SDO

因此,看到 060A0023 Resource not available: SDO connection 时,不能直接判断为从站 SDO server 故障。它可能只是:

  • Master 本地 NMT 状态不允许创建 Client-SDO;
  • NMT boot slave 正在使用默认 Client-SDO;
  • boot-up 事件触发后旧队列已被主动取消;
  • NMT command 使 Master 离开 Pre-operational/Operational,全部应用 SDO 被清理。

Lely 维护者在 issue #76 中明确说明:默认 SDO client 只有在 NMT master 不使用它时才可用;通常应用应在 OnBoot() 后使用,或在 OnConfig() 的配置窗口内使用。[S5]

6.3 CancelSdo() 是生命周期切换的一部分

源码在以下位置主动清理 SDO:

  • Boot(id) 开始前;
  • Driver 从注册表移除时;
  • Master 收到使本地状态离开 Pre-operational/Operational 的 NMT command 时;
  • 远端节点出现新的 boot-up,且当前不在配置阶段时;
  • 配置阶段结束并把 Client-SDO 重新交还 NMT boot 流程时。

这说明 Sdo 队列的生命周期服从 NMT 生命周期,而不是应用对象的生命周期。应用不能保存一个 Sdo*,然后假设它在 reset、boot-up 或重新配置后仍然有效。


7. OnConfig():NMT 与 Driver 之间的配置握手

7.1 C 层把正在使用的 Client-SDO 临时交给 C++ 层

Impl_ 构造时通过 co_nmt_set_cfg_ind() 注册配置 indication。当 NMT boot slave 进入 update configuration 阶段时,C 层回调提供一个 co_csdo_t*

Impl_::OnCfgInd() 执行三件事:

  1. 用该 co_csdo_t* 构造一个 C++ Sdo 包装对象并放入 sdos[id]
  2. config[id - 1] 设为 true
  3. 调用 self->OnConfig(id)

此时 GetSdo(id) 会优先返回这个已存在的队列,所以 Driver 可以在 boot slave 尚未结束时提交配置 SDO。

7.2 无 Driver 与有 Driver 的处理不同

BasicMaster::OnConfig(id)

  • 没有为该 node-ID 注册 Driver:直接向 NMT 报告配置成功;
  • 有 Driver:释放 Master 锁,调用 Driver 的 OnConfig(done)
  • Driver 完成后调用 done(ec)
  • 完成函数重新获取 Master 锁并进入 ConfigResult()

AsyncMaster::OnConfig(id) 的业务逻辑相同,但它先把 Driver 配置任务投递到 driver->GetExecutor(),避免在 NMT indication 调用栈内执行用户代码。[S2]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sequenceDiagram
participant N as "C NMT master"
participant I as "BasicMaster::Impl_"
participant M as "BasicMaster / AsyncMaster"
participant X as "Driver executor"
participant D as "Driver"

N->>I: "OnCfgInd(id, co_csdo_t*)"
I->>I: "sdos[id] = Sdo(co_csdo_t*)"
I->>I: "config[id] = true"
I->>M: "OnConfig(id)"

alt "BasicMaster"
M->>D: "OnConfig(done), Master 锁已释放"
else "AsyncMaster"
M->>X: "post configuration task"
X->>D: "OnConfig(done)"
end

D-->>M: "done(error_code)"
M->>M: "lock and ConfigResult(id, ec)"
M->>M: "config[id] = false"
M->>M: "erase temporary Sdo when booting"
M->>N: "co_nmt_cfg_res(id, SDO abort code)"

7.3 done 是继续 NMT 状态机的必要条件

官方 API 说明,NMT boot slave 会停在 update configuration 阶段,直到应用通过 ConfigResult() 报告结果;Driver 层对应的就是 OnConfig(done) 中的完成函数。[S4]

因此 OnConfig() 的正确语义是:

1
2
3
4
执行设备相关配置
-> 将所有异步结果归并为一个 error_code
-> 调用 done(error_code)
-> 允许 NMT boot slave 继续

它不是普通通知回调。若遗漏完成函数,boot slave 会保持在配置阶段,IsConfig(id) 持续为真,最终也不会进入正常的 boot 完成路径。

7.4 ConfigResult() 完成所有权回收

ConfigResult()

  • 清除 config 标志;
  • 若节点仍处于 booting,删除 sdos[id] 包装对象,因为 CSDO 将重新由 NMT master 接管;
  • std::error_code 转换为 SDO abort code;
  • 调用 co_nmt_cfg_res() 让 C 层 NMT 状态机继续运行。

所以这不是“配置线程结束”这么简单,而是一次明确的资源交还:

1
2
3
4
NMT master 持有 CSDO
-> OnCfgInd 临时交给 Driver
-> Driver 调用 done
-> ConfigResult 归还 NMT master

8. BasicMasterAsyncMaster 的执行时序差异

8.1 BasicMaster:同步调用,但先释放锁

OnCanState() 为例,BasicMaster 遍历 Driver,并用 UnlockGuard 暂时释放 Master 锁后调用 Driver::OnCanState()。单节点事件则先查注册表,再以相同方式调用对应 Driver。

这解决两个问题:

  • Driver 回调可以再次调用需要 Master 锁的 API,不会直接自锁;
  • 用户代码的执行时间不会把 Master 锁长期占住。

但回调仍发生在当前协议事件处理调用链中。若 Driver 执行阻塞操作,当前事件循环仍可能被拖延。

8.2 AsyncMaster:只负责投递,Driver 稍后执行

AsyncMaster 的覆盖函数通常执行:

1
2
3
4
找到 Driver
-> 复制回调参数
-> driver->GetExecutor().post(task)
-> 当前 Master 事件入口返回

例如:

  • CAN state/error、NMT command、SYNC/TIME 会给每个 Driver 投递 task;
  • heartbeat、state、EMCY、boot 等只投递给对应 node-ID;
  • OnConfig() 也在 Driver executor 上运行。

OnEmcy() 还有一个值得注意的细节:源码先把 5 字节 manufacturer-specific error field 复制到 std::array,再捕获进异步 task。原因是原始 uint8_t msef[5] 指针的生命周期只覆盖当前回调,不能直接跨越异步投递边界。[S2]

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant Stack as "CANopen stack"
participant M as "AsyncMaster::OnEmcy"
participant E as "Driver executor"
participant D as "Driver::OnEmcy"

Stack->>M: "OnEmcy(id, eec, er, msef*)"
M->>M: "copy msef into owned array"
M->>E: "post(lambda with copied values)"
M-->>Stack: "return"
E->>D: "invoke OnEmcy later"

8.3 异步不等于并行,也不自动保证全局顺序

从这两个文件能确认的是“任务被投递到各 Driver 的 executor”。以下行为取决于应用选择的 executor、strand、fiber 或独立线程模型:

  • 同一 Driver 内多个任务是否串行;
  • 不同 Driver 回调是否并行;
  • 回调相对其他应用 task 的排队顺序;
  • 长耗时回调是否阻塞共享 event loop。

因此,AsyncMaster 保证的是调用栈解耦,不应被直接解释为“每个节点都有独立线程”。官方教程进一步提供 FiberDriverLoopDriver 等不同执行模型;它们的阻塞语义和死锁风险并不相同。[S3]


9. NMT command、远端状态与 SDO 清理如何联动

9.1 Command() 只是发起 NMT command

BasicMaster::Command(cs, id) 获取锁后调用 C 层 co_nmt_cs_req()。node-ID 为 0 时可表示广播,具体帧和状态行为由 NMT 服务执行。[S2]

9.2 OnCommand() 处理的是 Master 自身状态迁移

当 Master 本地 NMT 收到 command 时,OnCommand(cs) 会检查即将进入的状态:

  • 若不是 Pre-operational 或 Operational,取消全部应用 SDO;
  • 再将 command 事件通知各 Driver。

这与 GetSdo() 的可用条件一致:离开可提供 Client-SDO 的 NMT 状态后,旧请求队列必须失效。

9.3 新 boot-up 事件会使旧 ready 与 SDO 状态失效

远端节点发送 boot-up 后,OnState(id, BOOTUP) 在非配置阶段执行:

1
2
ready[id] = false
CancelSdo(id)

因为新的 boot-up 表示远端通信状态已重置,之前的“已完成 boot”结论以及应用 SDO 会话都不能继续沿用;NMT master 也可能马上重新进入 boot slave 流程并接管 Client-SDO。[S2]

1
2
3
4
5
6
7
8
9
stateDiagram-v2
[*] --> NotReady
NotReady --> Booting: "Boot(id) 或检测到 boot-up"
Booting --> Configuring: "update configuration indication"
Configuring --> Booting: "done(ec) / ConfigResult"
Booting --> Ready: "boot indication success 或 L"
Booting --> NotReady: "boot failure"
Ready --> NotReady: "新的 boot-up"
Ready --> NotReady: "AsyncDeconfig"

这张图描述的是 BasicMaster 暴露的应用状态,不是 CiA 302 内部 boot state machine 的完整替代。


10. AsyncDeconfig():与 boot 相反的 Driver 生命周期入口

AsyncDeconfig(id) 查找对应 Driver,随后由 Impl_::AsyncDeconfig()

  1. 先把该节点标记为 not ready;
  2. 创建 promise/future;
  3. 向 Driver executor 投递 OnDeconfig(done)
  4. Driver 完成后设置 promise;
  5. 调用者通过 future 获得完成或错误结果。

无参数版本会为全部 Driver 创建 future,再通过 when_all 聚合。[S2]

这一接口主要表达应用层设备资源的解除过程。它不会自动等同于 NMT stop、reset communication 或物理掉线;具体设备资源、业务状态和 I/O 停止动作由 Driver 的 OnDeconfig() 实现。


11. tpdo_event_mutex 为什么在 Master 中重新包装

Node::TpdoEventMutex 用于延迟事件驱动或异步 TPDO 的发送,使应用可以批量修改多个映射值后再统一触发。

BasicMaster::TpdoEventMutex::lock()unlock() 在调用基类实现前,先获取 Master 锁。这样能维持两层同步关系:

1
2
3
Master 状态与对象访问同步
+
Node 的 TPDO event 延迟机制

典型意图是把多个 TpdoMapped() 写入放入同一个临界区,避免更新到一半时触发 TPDO:

1
2
3
4
5
6
7
{
std::lock_guard<lely::canopen::BasicMaster::TpdoEventMutex> guard(
master.tpdo_event_mutex);

master.TpdoMapped(2)[0x6040][0] = uint16_t{0x0006};
master.TpdoMapped(2)[0x607A][0] = int32_t{100000};
}

是否立即产生总线帧仍取决于 TPDO transmission type、event timer、SYNC 和 mapping 配置;mutex 只负责推迟事件触发,不改变 PDO 通信参数。[S1][S2]


12. 把完整流程串起来

下面以一个 AsyncMaster + Driver 管理 node 2 的典型启动过程为例:

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
33
34
35
36
37
38
39
40
41
sequenceDiagram
participant App as "Application"
participant Loop as "event loop"
participant M as "AsyncMaster"
participant N as "C NMT master"
participant S as "Slave node 2"
participant E as "Driver executor"
participant D as "Driver node 2"

App->>M: "construct with master DCF"
App->>D: "register Driver for node 2"
App->>M: "Reset()"
M->>N: "start local NMT service"
N->>S: "reset communication / boot checks"
S-->>N: "boot-up and NMT responses"

N->>M: "OnState(node 2, BOOTUP)"
M->>M: "ready=false, CancelSdo(2)"
M->>E: "post Driver::OnState"

N->>M: "OnCfgInd(node 2, CSDO)"
M->>M: "wrap CSDO, config=true"
M->>E: "post Driver::OnConfig"
E->>D: "OnConfig(done)"
D->>M: "AsyncWrite / AsyncRead"
M->>M: "GetSdo returns config CSDO"
M->>N: "submit SDO through handed-off queue"
N->>S: "SDO requests"
S-->>N: "SDO responses"
D-->>M: "done(0)"
M->>N: "co_nmt_cfg_res(success)"

N->>S: "remaining boot steps / NMT start"
N->>M: "OnBoot(node 2, state, status)"
M->>M: "ready=true on accepted result"
M->>E: "post Driver::OnBoot"
E->>D: "OnBoot"

S-->>M: "TPDO / heartbeat / EMCY"
M->>E: "post per-node event"
E->>D: "OnRpdoWrite / OnHeartbeat / OnEmcy"

从这个流程可以得到三个工程结论:

  1. OnState(BOOTUP) 不是执行普通应用 SDO 的稳定窗口,因为 NMT boot 可能已经准备接管默认 Client-SDO。
  2. OnConfig() 是 boot 期间由 NMT 明确授权给 Driver 的 SDO 窗口,必须以完成回调结束。
  3. OnBoot() 表示 boot slave 流程完成,但对依赖 PDO 的业务逻辑,仍应结合远端 NMT state 判断设备是否真正进入可运行阶段。

13. 条件编译决定哪些路径真实存在

master.hpp/master.cpp 中的重要能力受编译配置控制:

条件 影响
LELY_NO_COAPP_MASTER 整个 C++ Master 实现不编译
LELY_NO_CO_DCF 文本 DCF 构造路径不可用
LELY_NO_CO_SDEV 静态设备描述构造路径不可用
LELY_NO_CO_NMT_BOOT Boot() 与 boot indication 相关路径不可用
LELY_NO_CO_NMT_CFG update configuration、config[] 和配置 CSDO 交接不可用
LELY_NO_CO_NG node guarding indication 不注册

因此,对具体构建产物分析时,不能只看头文件里是否存在某个 API,还要确认目标库的 feature macros。本文描述的是这些功能启用时的主流程。[S1][S2]


14. 如何理解 BasicMasterAsyncMaster 与 Driver 的边界

可以把三者压缩成下面的职责模型:

对象 负责什么 不负责什么
Node 本地 CANopen 节点、对象字典与协议服务 不表达多个远端设备的业务差异
BasicMaster NMT 主站编排、Driver 注册、ready/config、SDO 仲裁、同步事件分发 不为每种从站实现设备业务逻辑
AsyncMaster BasicMaster 基础上把 Driver 事件投递到 executor 不自动创建线程,也不改变 CANopen 协议语义
DriverBase/派生 Driver 单个远端节点的配置、状态处理和业务行为 不管理全网 NMT 主状态机
Sdo 一条 Client-SDO 队列及传输请求 不决定自己何时可与 NMT boot 并发使用

最终可以用一句话概括 coapp::Master

它是本地 CANopen Node 与远端设备 Driver 之间的协调层,以 NMT 生命周期为主轴,对 Client-SDO 所有权、节点 ready/config 状态和协议事件执行上下文进行统一管理。


参考资料