Lely CANopen SYNC、TIME 与 EMCY学习

在这里插入图片描述

@[toc]

SYNC、TIME 和 EMCY 都属于 CiA 301 通信对象,但在 Lely CANopen 中,它们并不是三个“收到 CAN 帧后直接调用用户回调”的简单模块。三者都被实现成依附于 can_net_tco_dev_t 的被动服务对象:对象字典决定角色与 CAN-ID,can_recv_t 接收匹配报文,can_timer_t 驱动周期或抑制时间,最终通过回调或 can_net_send() 把事件交给上层。

三者的触发模型并不相同:

  • co_sync_t 的 producer 周期直接由对象 0x1006 驱动,修改 0x1005/0x1006/0x1019 会立即重配 receiver、timer 和 counter;
  • co_time_t0x1012 只决定 producer/consumer 角色和 COB-ID,实际发送起点与周期仍要由 co_time_start_prod() 显式提交;
  • co_emcy_t 不做周期发送,它把错误变化压入活动错误栈和 CAN 帧缓冲,再按 0x1015 inhibit time 串行发送;
  • sync.hpptime.hppemcy.hpp 没有重新实现协议,只使用 incomplete_c_type 管理 C 对象并适配 C++ callable。

这篇文章以用户提供的本地 lely-core 源码包为主要证据,结合 CiA 301 V4.2.0 和 Lely 官方文档校对协议边界。源码包未携带 .git 元数据,因此本文不把它静默等同于某个 Git commit;文中涉及行号时,均指本次本地归档文件。


1. 九个文件在整条链路中的位置

本文重点分析以下文件:

文件 主要内容 是否保存运行状态
include/lely/co/sync.h SYNC C API、COB-ID 位定义、indication/error callback
include/lely/co/sync.hpp COSync C++ 不完整类型包装与 callable 适配
src/co/sync.c SYNC receiver、periodic timer、计数器、OD 动态重配置
include/lely/co/time.h TIME C API、TIME_OF_DAY 转换、producer 调度接口
include/lely/co/time.hpp COTime C++ 包装
src/co/time.c TIME receiver、timer、时间编码与 0x1013 更新
include/lely/co/emcy.h EMCY producer/consumer API、错误栈接口和 indication
include/lely/co/emcy.hpp COEmcy C++ 包装
src/co/emcy.c 活动错误栈、0x1003、发送缓冲、inhibit timer、远端 EMCY consumer

Lely 官方概览把 CANopen 核心描述为完全被动的实现,并在 standards support 页面列出 CiA 301 的 SYNC、TIME、EMCY 及相关 OD 对象支持范围:[^lely-standards]

协议库不创建线程,不自行执行 CAN I/O,也不直接读取系统时钟;应用需要把 CAN 帧和当前时间送入 can_net_t。SYNC、TIME 和 EMCY 都是这种架构中的 service object。[^lely-overview]

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
flowchart TB
App["应用或 co_nmt_t"] --> Dev["co_dev_t<br/>对象字典与当前参数"]
App --> Net["can_net_t<br/>CAN 接收分发与逻辑定时"]

Dev --> Sync["co_sync_t"]
Dev --> TimeSvc["co_time_t"]
Dev --> Emcy["co_emcy_t"]

Net --> Sync
Net --> TimeSvc
Net --> Emcy

Sync --> SyncRx["can_recv_t<br/>接收 SYNC"]
Sync --> SyncTimer["can_timer_t<br/>周期发送 SYNC"]

TimeSvc --> TimeRx["can_recv_t<br/>接收 TIME"]
TimeSvc --> TimeTimer["can_timer_t<br/>按 API 调度 TIME"]

Emcy --> EmcyRx["最多 127 个 can_recv_t<br/>远端 EMCY consumer"]
Emcy --> EmcyBuf["活动错误栈 + CAN 帧缓冲"]
Emcy --> EmcyTimer["can_timer_t<br/>inhibit deadline"]

SyncRx --> Bus["CAN 帧入口/出口"]
SyncTimer --> Bus
TimeRx --> Bus
TimeTimer --> Bus
EmcyRx --> Bus
EmcyTimer --> Bus

这里要特别区分两层“时间”:

1
2
3
4
5
can_timer_t
= 协议核心内部的逻辑定时器

Linux timerfd / io_timer_t / io_tqueue
= io2 层把逻辑截止时间映射到操作系统的机制

sync.ctime.cemcy.c 只操作 can_timer_t。它们不知道 Linux timerfd、SocketCAN、epoll 或线程,实际系统 I/O 仍由 io_can_net 等桥接层完成。


2. 三个服务共享的生命周期模型

三组源码都采用相同的对象生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__co_xxx_alloc()
-> __co_xxx_init(net, dev)
-> 创建 receiver/timer/buffer
-> 读取必要对象是否存在
-> co_xxx_start()

co_xxx_stop()
-> 停止 receiver/timer
-> 移除对象字典 download indication

__co_xxx_fini()
-> co_xxx_stop()
-> 销毁内部对象

__co_xxx_free()

2.1 创建时已经启动

co_sync_create()co_time_create()co_emcy_create() 都通过内部 __co_xxx_init() 完成初始化,并在成功路径中启动服务。对应头文件也明确说明“service is started as if by co_xxx_start()”。因此:

  • create() 不是单纯分配内存;
  • 创建后对象字典回调和 CAN receiver/timer 可能已经生效;
  • stop() 必须解除这些外部关联,否则仅释放外层指针会留下悬空回调。

2.2 对象字典写入直接驱动运行期重配置

三个服务启动时都会为关键 OD 对象安装 download indication:

服务 被拦截对象 重配置效果
SYNC 0x10050x10060x1019 重注册 receiver、重设周期 timer、复位 counter
TIME 0x1012 重注册 consumer receiver;必要时停止 producer timer
EMCY 0x10030x10140x1028 清错误历史、改变 producer COB-ID、重配指定远端 consumer

这类写入不是“先修改 OD,等下次复位再生效”。实际链路是:

1
2
3
4
5
SDO download / 本地 SDO 写入
-> co_obj_set_dn_ind() 安装的 handler
-> 校验活动状态、CAN-ID 和取值范围
-> co_sub_dn() 保存新值
-> 立即更新 receiver/timer/service state

因此运行期写通信参数时,访问规则本身也是状态机的一部分。例如 SYNC producer 活动时不允许直接把旧 CAN-ID 改成另一个仍然有效的 producer CAN-ID;正确过程通常是先禁用,再修改标识符,最后重新启用。

2.3 条件编译直接改变接口和 ABI

源码分别受以下宏控制:

1
2
3
#if !LELY_NO_CO_SYNC
#if !LELY_NO_CO_TIME
#if !LELY_NO_CO_EMCY

Autotools 对应选项为:

1
2
3
--disable-sync  -> LELY_NO_CO_SYNC
--disable-time -> LELY_NO_CO_TIME
--disable-emcy -> LELY_NO_CO_EMCY

Lely 官方构建文档列出了这些裁剪选项,并说明部分结构体相关特性宏必须在库与应用侧保持一致。对于嵌入式静态库,不能只在应用代码中包含头文件后假定服务一定存在;库、头文件和应用的条件编译配置必须匹配。[^lely-config]


3. SYNC:对象字典直接驱动 producer 周期

3.1 struct __co_sync 保存了什么

src/co/sync.c:40-67 的对象非常紧凑:

1
2
3
4
5
6
7
8
9
10
11
co_sync_t
├─ net / dev
├─ stopped
├─ cobid <- 0x1005
├─ us <- 0x1006,单位 µs
├─ max_cnt <- 0x1019
├─ recv
├─ timer
├─ cnt
├─ indication callback
└─ error callback

它没有独立线程,没有报文队列,也不直接维护 PDO。SYNC 服务只负责:

  1. 接收或产生 SYNC 帧;
  2. 维护可选计数器;
  3. 把同步事件交给上层;
  4. 在帧长度不符合配置时报告通信错误。

3.2 三个对象决定 SYNC 本体,0x1007 不在这里处理

OD 作用 sync.c 中的行为
0x1005 SYNC COB-ID、producer 位、标准/扩展帧 决定 receiver 或 producer 角色及 CAN-ID
0x1006 communication cycle period,单位 µs producer 周期;为 0 时不启动周期发送
0x1019 synchronous counter overflow 0 表示无 payload;2..240 表示一字节计数器上限
0x1007 synchronous window length 不由 sync.c 处理,由 RPDO/TPDO 服务用于同步窗口判定

这一区分很重要。co_sync_t 只产生“同步边沿”;同步窗口内是否允许发送 TPDO、是否接收并执行 RPDO,是 PDO 服务的职责。把 0x1007 写入 sync.c 的心智模型,会导致调用关系完全错位。

3.3 co_sync_update() 的角色矩阵

src/co/sync.c:380-420 是配置落地的中心:

producer 位 0x1006 receiver timer
0 任意 0x1005 注册,作为 consumer 停止
1 0 停止,避免接收自己的 SYNC 停止
1 非 0 停止 周期启动

producer 周期不是从“当前时刻 + 周期”任意起算。源码先取得 can_net_t 当前时间,再对周期取模,把首次发送时间对齐到下一个周期整数倍:

1
2
3
4
5
now
-> now % period
-> 向下对齐到最近的 period 边界
-> 加一个 period
-> 作为首次 deadline

这使多个使用同一时间基准、同一周期的节点更容易落在一致的时间栅格上。

3.4 consumer 接收流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sequenceDiagram
participant CanBus as CAN 总线
participant Net as can_net_t
participant SyncSvc as co_sync_t
participant Nmt as co_nmt_t
participant Tpdo as TPDO services
participant Rpdo as RPDO services
participant UserCb as 用户回调

CanBus->>Net: SYNC frame
Net->>SyncSvc: co_sync_recv(msg)
SyncSvc->>SyncSvc: 期望长度 = max_cnt ? 1 : 0
alt DLC 不匹配
SyncSvc->>Nmt: error callback(0x8240, 0x10)
end
SyncSvc->>Nmt: indication(cnt 或 0)
Nmt->>Tpdo: co_tpdo_sync(cnt)
Nmt->>Rpdo: co_rpdo_sync(cnt)
Nmt->>UserCb: sync indication

co_sync_recv() 的细节有两点:

  1. 0x1019 == 0,期望 DLC 为 0;否则期望 DLC 为 1;
  2. 即使 DLC 错误,源码仍会调用 indication,只是传入 cnt = 0,同时通过 error callback 报告 0x8240 / 0x10

在 NMT 集成路径中,SYNC indication 会进入 co_nmt_on_sync()。源码明确先处理 TPDO,再处理 RPDO,以确保同一对象同时映射到同步 TPDO 和 RPDO 时,发送的是“上一个同步窗口”的值,然后才用本次收到的 RPDO 更新对象字典(src/co/nmt.c:1621-1643)。这不是普通的调用顺序偏好,而是同步数据一致性的语义要求。

3.5 producer 发送流程

周期到期后 co_sync_timer()

1
2
3
4
5
6
7
can_timer_t 到期
-> 根据 0x1005 生成标准或扩展 CAN-ID
-> max_cnt == 0:DLC = 0
-> max_cnt != 0:DLC = 1,发送 cnt
-> cnt 在 1..max_cnt 之间循环
-> can_net_send()
-> 调用同一个 SYNC indication

因此本节点作为 producer 时,也会通过 indication 驱动本地同步 PDO,不必等待 CAN 控制器把自己发送的帧再回环接收。co_sync_update() 同时会停掉 receiver,正是为了避免“本地 timer indication + 自收回环”造成重复同步事件。

3.6 0x1019 的运行期限制

CiA 301 规定 0x1019:[^cia301]

  • 0:SYNC 无数据;
  • 1:保留;
  • 2..240:一字节 counter;
  • 241..255:保留。

源码进一步禁止在 0x1006 != 0 时修改 0x1019,返回 CO_SDO_AC_DATA_DEV。也就是说,要改变 counter 形式,应先把通信周期清零,再修改 overflow,最后恢复周期。


4. TIME:角色由 0x1012 决定,调度由 API 决定

4.1 TIME 与 SYNC 的根本差异

SYNC 解决“同步动作发生在什么时候”,TIME 解决“网络当前时间是什么”。两者虽然都使用 producer/consumer 模型,但 Lely 的调度方式不同:

1
2
3
4
5
6
SYNC producer
0x1006 非零后自动周期运行

TIME producer
0x1012 只允许 producer
仍需 co_time_start_prod() 提交起点与周期

这一点由 co_time_update() 直接体现:

  • consumer 位有效时注册 receiver;
  • consumer 位无效时停止 receiver;
  • producer 位无效时停止 timer;
  • producer 位有效时,函数本身并不启动 timer。

4.2 相关对象

OD 作用 本源码中的使用方式
0x1012 TIME COB-ID、consumer 位、producer 位、帧类型 决定角色和 CAN-ID
0x1013 High resolution time stamp,单位 µs producer 每次发送前写入“服务创建后经过的微秒数”

0x1013 不是 TIME 帧的第七或第八个字节。它是本地对象字典中的高分辨率时间戳;标准 TIME 帧仍固定为 6 字节。

4.3 TIME_OF_DAY 编码与 epoch 转换

TIME payload:

1
2
Byte 0..3:当天经过的毫秒数,低 28 bit 有效
Byte 4..5:自 1984-01-01 起的天数

Lely 对外使用 struct timespec。转换函数完成 Unix epoch 与 CANopen epoch 的偏移:

1
2
1970-01-01 -> 1984-01-01
= 14 年 + 3 个闰日

co_time_of_day_get() 把 CANopen TIME_OF_DAY 转为 Unix timespecco_time_of_day_set() 执行反向转换。Lely v2.3.4 的发布说明曾专门修复 co_time_of_day_set() “只计算但没有真正写回结果”的问题,说明这部分转换应纳入版本核对范围。[^lely-v234]

4.4 consumer 接收流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
participant CanBus as CAN 总线
participant Net as can_net_t
participant TimeSvc as co_time_t
participant Converter as TIME_OF_DAY 转换
participant UserCb as indication callback

CanBus->>Net: TIME frame
Net->>TimeSvc: co_time_recv(msg)
alt RTR 或 CAN FD 帧
TimeSvc-->>Net: return 0
else DLC 小于 6
TimeSvc-->>Net: return 0
else 至少 6 字节
TimeSvc->>TimeSvc: 提取 28-bit ms 与 16-bit days
TimeSvc->>Converter: 转换为 struct timespec
Converter-->>TimeSvc: Unix epoch time
TimeSvc->>UserCb: ind(time, &timespec)
TimeSvc-->>Net: return 1
end

源码只要求 msg->len >= 6,不会因经典 CAN 帧带有额外字节而拒绝;实际只解析前 6 字节。它会忽略 RTR 和 CAN FD 格式帧。

4.5 producer 调度与发送

co_time_start_prod(time, start, interval) 直接把参数交给 can_timer_start()

start interval 语义
非空 非空 从绝对时间 start 开始周期发送
非空 在绝对时间发送一次
非空 相对当前时间等待一个 interval 后周期发送
停止 producer timer;实际工程直接调用 co_time_stop_prod() 表意更清楚

发送回调 co_time_timer(tp) 使用的 tp 是本次 timer deadline:

1
2
3
4
更新 0x1013 = tp - service creation time(µs,截断为 u32)
-> 把 tp 转为 CANopen TIME_OF_DAY
-> 构造 6 字节 TIME 帧
-> can_net_send()

这里存在两个值得注意的时基边界:

  1. TIME payload 表达的是绝对日期时间,通常需要与真实 UTC/日历时钟一致;
  2. can_net_t 的超时调度在工程上往往接入单调时钟。若直接用单调时钟值编码 TIME payload,得到的不会是有意义的日历时间。

因此 TIME producer 的 can_net_t 时间源、应用提供的 start 和系统实时时钟之间必须有明确的映射策略。源码不会替应用自动完成时钟同步、时区处理或 UTC 校准。


5. EMCY:活动错误栈、历史对象和发送抑制是三套不同状态

5.1 co_emcy_t 内部不是一个错误码变量

src/co/emcy.c:85-123 的核心状态包括:

1
2
3
4
5
6
7
8
co_emcy_t
├─ 0x1001 指针
├─ 可选 0x1003 对象
├─ 活动错误栈 msgs[]
├─ CAN 帧缓冲 can_buf
├─ inhibit timer 与下一允许发送时间
├─ 127 个远端节点 consumer 槽位
└─ received EMCY indication

必须区分:

  • 活动错误栈:当前仍未解除的错误条件,用于计算组合 error register;
  • 0x1003:向对象字典暴露的预定义错误域;
  • CAN 帧缓冲:已经形成、但受 inhibit time 限制尚未发送的 EMCY 帧;
  • consumer receiver:接收别的节点的 EMCY,与本节点 producer 错误栈无直接所有权关系。

5.2 相关对象

OD 作用 Lely 实现
0x1001 Error register 每次 co_emcy_send() 前更新为当前组合值
0x1003 Pre-defined error field 把活动错误栈中的 EEC 写入子项,最新错误位于最前
0x1014 EMCY producer COB-ID 决定 producer 是否有效及 CAN-ID
0x1015 EMCY inhibit time,单位 100 µs 决定相邻发送帧最小间隔
0x1028 Emergency consumer object 每个子项把远端 Node-ID 关联到一个 EMCY COB-ID

CiA 301 对 0x1003 的每个错误项定义为 32 位:[^cia301]

低 16 位是 error code,高 16 位是制造商附加信息。本源码 co_emcy_set_1003() 只写 emcy->msgs[i].eec,因此高 16 位保持 0。这是合法的“未提供附加信息”实现,但要注意:co_emcy_push() 传入的 5 字节 MSEF 不会进入 0x1003

5.3 push() 如何建立新的错误状态

co_emcy_push() 的执行顺序:

1
2
3
4
5
6
7
检查 eec != 0
-> 强制置 Error Register bit0(generic error)
-> 为活动错误栈扩容或检查静态容量
-> 旧错误向后移动,新错误放到 index 0
-> 更新 0x1003
-> 对全部活动错误的 er 做按位 OR
-> 形成 EMCY 帧并进入发送缓冲

组合 error register 不是只取最新错误:

1
2
for (size_t i = 0; i < emcy->nmsg; i++)
er |= emcy->msgs[i].er;

因此多个错误属于不同类别时,0x1001 可以同时反映多个类别。Lely v2.3.4 发布说明明确提到“error register 改为按需计算”,以支持按任意顺序解除错误。[^lely-v234]

5.4 remove()pop()clear()

API 行为
co_emcy_pop() 读取并移除 index 0,即最近错误
co_emcy_remove(n) 移除任意栈位置,用于错误恢复顺序与发生顺序不同的情况
co_emcy_find(eec) 从新到旧查找指定 EEC
co_emcy_clear() 清空所有活动错误并发送 no-error EMCY

移除一个错误后,源码发送 EEC 0x0000 的 reset 报文:

  • 若仍有活动错误,error register 是剩余错误的 OR;MSEF 前两字节放当前最新错误 EEC;
  • 若已无活动错误,发送 EEC 0x0000、ER 0x00

这解释了为什么“解除其中一个错误”不等于 0x1001 必然清零。

5.5 inhibit time 与发送缓冲

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
ErrorEvent["push/remove/clear 产生 EMCY 状态变化"] --> Build["构造 8 字节 EMCY 帧"]
Build --> Queue["写入 can_buf"]
Queue --> Check{"当前时间 >= inhibit deadline?"}
Check -- "否" --> Arm["can_timer_start(deadline, one-shot)"]
Arm --> TimerCb["co_emcy_timer()"]
TimerCb --> Check
Check -- "是" --> Update["deadline = now + 0x1015 * 100 us"]
Update --> Send["can_net_send() 发送一帧"]
Send --> More{"缓冲区还有帧?"}
More -- "是" --> Check
More -- "否" --> Done["结束"]

0x1015 == 0 时,deadline 每次仍等于 now,循环可以在一次 co_emcy_flush() 中把全部缓存帧提交给 can_net_send()。当 inhibit 非零时,函数每次最多发送当前允许的一帧,然后为下一帧建立 one-shot deadline。

注意 can_buf 只是 Lely CAN 协议层的帧缓冲。can_net_send() 之后,io2 CAN channel 仍可能有自己的异步写队列;两者解决的是不同层的背压问题。

5.6 远端 EMCY consumer

0x1028 的每个有效子项对应一个远端节点。初始化阶段,源码为 1..127 的节点槽位准备 receiver;启动或写入 0x1028 时,再按配置的 COB-ID 注册指定 receiver。

收到帧后:

1
2
3
4
5
6
忽略 RTR
-> 忽略 CAN FD 格式
-> len >= 2 时读取 EEC
-> len >= 3 时读取 ER
-> len >= 4 时读取 MSEF
-> indication(emcy, node_id, eec, er, msef)

本地归档的 MSEF 复制代码为:

1
2
memcpy(msef, msg->data + 3,
MAX((uint_least8_t)(msg->len - 3), 5));

从协议语义看,这里更合理的长度应是 MIN(msg->len - 3, 5)。当前 MAX 会在 DLC 为 4..7 时复制 5 字节,把有效 payload 之外的数据数组内容也交给用户回调。对经典 CAN 的固定 data[8] 布局而言,这通常不会越过结构体数组边界,但会把未由该帧声明的尾部字节当成 MSEF,属于已确认的有效长度处理异常。同样,本文未声称已完成运行时复现或确认上游当前分支状态。

5.7 无动态内存时的资源边界

定义 LELY_NO_MALLOC 后,EMCY 改用静态存储:

默认值 影响
CO_EMCY_MAX_NMSG 8 同时保留的活动错误数量上限
CO_EMCY_CAN_BUF_SIZE 16 帧 等待 inhibit time 发送的帧缓冲容量

co_emcy_push() 在活动错误达到上限后返回 ERRNUM_NOMEM;CAN 帧缓冲不能扩容时,co_emcy_send() 也会失败。嵌入式目标应根据最坏情况下的并发错误数和错误突发速率配置这两个宏,而不是只看单帧 EMCY 的 8 字节 payload。


6. SYNC 错误如何进入 EMCY

SYNC 与 EMCY 并非完全独立。Lely 的 NMT service manager 在创建 SYNC 时安装两个回调:

1
2
SYNC indication -> co_nmt_on_sync()
SYNC error -> co_nmt_on_err()

当 SYNC DLC 与 0x1019 配置不匹配时:

1
2
3
4
5
6
7
co_sync_recv()
-> sync error callback(0x8240, 0x10)
-> co_nmt_srv_sync_err()
-> co_nmt_on_err()
-> co_emcy_push()
-> 更新 0x1001/0x1003
-> 受 0x1015 约束发送 EMCY

如果 EEC 属于 0x81xx communication error,co_nmt_on_err() 还会调用由 0x1029:01 指定的 communication error behavior。0x8240 不属于 0x81xx,因此它进入 EMCY,但不会走这条 communication error behavior 分支。

TIME 在当前 nmt_srv.c 中只被创建和销毁,没有像 SYNC 那样自动安装 indication/error bridge。应用若需要在收到 TIME 后校准本地时钟,必须自行取得 TIME service 并设置 indication,或在更高层完成对应接入。


7. 三个模块的运行模型对照

维度 SYNC TIME EMCY
通信模型 producer/consumer producer/consumer producer/consumer
典型默认 CAN-ID 0x080 0x100 0x080 + Node-ID
payload 0 或 1 字节 固定 6 字节 固定 8 字节
producer 触发 0x1006 周期 co_time_start_prod() 错误状态变化
consumer 注册 非 producer 时自动注册 0x1012 consumer 位 0x1028 每个远端节点
使用 timer 周期 timer 应用提交的 timer inhibit deadline timer
内部队列 活动错误栈 + CAN 帧缓冲
与 NMT 集成 自动驱动 TPDO/RPDO,错误可进入 EMCY 当前只创建/销毁 作为 NMT 错误出口
动态配置对象 1005/1006/1019 1012 1003/1014/1028,发送时读取 1015

三者共同的设计思想可以概括为:

1
2
3
4
对象字典定义通信参数
+ can_net_t 提供 CAN 和时间输入
+ service object 保存最小协议状态
+ callback 把协议事件交给 NMT 或应用

8. C++ 头文件:所有权和回调适配,不是第二套实现

三个 .hpp 的结构基本一致:

1
2
3
4
5
6
c_type_traits<__co_xxx>
alloc/free/init/fini

incomplete_c_type<__co_xxx>

COSync / COTime / COEmcy

例如:

1
2
3
4
5
6
7
class COSync : public incomplete_c_type<__co_sync> {
public:
COSync(CANNet* net, CODev* dev) : c_base(net, dev) {}
int start() noexcept { return co_sync_start(this); }
void stop() noexcept { co_sync_stop(this); }
// setInd()/setErr() 仅把 callable 适配为 C callback。
};

关键点:

  1. alloc/init/fini/free 最终仍调用 C 层 __co_xxx_*()
  2. start()stop()push()startProd() 等均是薄转发;
  3. 模板版 setInd()/setErr() 使用 c_obj_callc_mem_call 把函数对象、成员函数适配为 C callback;
  4. 三个类的析构函数都是 protected,说明它们不是按普通值类型随意栈上创建和销毁的接口;
  5. 协议行为、条件编译和 ABI 最终仍由 C 实现决定。

Lely 官方 Doxygen 当前标识为 2.4.0,并将这些头文件描述为对应 C service object 的 C++ interface。[^lely-doxygen] 在新应用中,若已经使用 liblely-coapp,通常应优先通过 Node/BasicMaster/BasicSlave 等上层对象取得服务能力,而不是绕过 NMT service manager 再创建一套重复的 SYNC/TIME/EMCY 对象。


参考资料

[^lely-overview]: Lely CANopen - Library overview,说明协议栈的被动、异步架构以及 can_net_t、SYNC、TIME、EMCY service object。
[^lely-config]: Lely CANopen - Build configuration,说明 --disable-sync--disable-time--disable-emcyLELY_NO_MALLOC 以及静态 EMCY 容量宏。
[^lely-v234]: Lely CANopen - Release v2.3.4,记录 co_time_of_day_set()、EMCY 任意错误移除、组合 error register 和 SYNC stop 行为修复。
[^lely-doxygen]: Lely core libraries 2.4.0 Doxygen,用于核对公开 API、C/C++ 包装和版本标识。
[^lely-standards]: Lely CANopen - Standards support,说明 CiA 301 中 SYNC、TIME、EMCY 及相关对象的支持范围。
[^cia301]: 《CiA 301 CANopen 应用层和通信协议 V4.2.0(中文注释版)》,重点参考 7.2.5 SYNC、7.2.6 TIME、7.2.7 EMCY,以及 7.5.2 中 0x1003/0x1005/0x1006/0x1007/0x1012/0x1013/0x1014/0x1015/0x1019/0x1028 对象定义。