CANopen TIME/SYNC 运行流程

在这里插入图片描述

@[toc]
在这里插入图片描述


1. 先把 TIME 和 SYNC 分开

TIME 和 SYNC 都属于 CANopen 的特殊功能通信对象,但它们解决的问题不同:

对象 核心问题 默认 CAN-ID 数据长度 典型用途
SYNC “什么时候进入一个同步周期?” 0x080 01 同步 TPDO/RPDO、周期采样、周期控制
TIME “网络当前时间是多少?” 0x100 6 网络时间校准、时间戳、日志时间基准

一句话区分:

1
SYNC 是节拍;TIME 是时钟。

SYNC 不携带实际过程数据,它只广播一个同步事件。同步 PDO 在这个事件前后按配置动作。TIME 也不携带过程数据,它只携带“当天毫秒数 + 自 1984-01-01 起的天数”。


2. 协议侧:SYNC 规定了什么

2.1 SYNC 的角色

SYNC 使用 producer/consumer 模型:

1
2
3
4
5
6
7
flowchart LR
P["SYNC producer<br/>周期发送同步对象"] -->|SYNC frame| C1["SYNC consumer A"]
P -->|SYNC frame| C2["SYNC consumer B"]
P -->|SYNC frame| C3["SYNC consumer C"]
C1 --> A1["同步 TPDO/RPDO 动作"]
C2 --> A2["同步采样或控制"]
C3 --> A3["同步窗口判断"]

网络中通常只有一个 SYNC producer,其他节点作为 consumer。producer 的任务是周期性发送 SYNC 帧;consumer 收到后把它当成同步边界。

2.2 SYNC 帧格式

在这里插入图片描述

SYNC 默认是一个空数据帧:

1
2
CAN-ID = 0x080
DLC = 0

如果启用同步计数器,则变成:

1
2
3
CAN-ID = 0x080
DLC = 1
Byte0 = counter

计数器从 1 开始递增,到 0x1019 Synchronous counter overflow value 指定的最大值后回到 1。如果本地配置要求 DLC=0,却收到 DLC=1;或配置要求 DLC=1,却收到其他长度,consumer 应视为同步数据长度错误。

2.3 与 SYNC 直接相关的对象字典

OD 名称 对源码的影响
0x1005 COB-ID SYNC message 决定 SYNC CAN-ID;bit30 决定本节点是否 producer
0x1006 Communication cycle period producer 的发送周期;consumer 的超时监控基准
0x1007 Synchronous window length 同步窗口;窗口外同步 PDO 应被限制
0x1019 Synchronous counter overflow value 0 时 SYNC 无数据;大于 1 时 SYNC 携带 1 字节 counter

0x1006 = 0 等价于不启用周期 SYNC;0x1007 = 0 等价于关闭同步窗口约束;0x1019 = 0 等价于不使用 SYNC counter。

2.4 同步窗口的含义

1
2
3
4
flowchart LR
S["收到/发送 SYNC<br/>timer = 0"] --> W["0 .. 0x1007<br/>同步窗口内"]
W --> O["timer > 0x1007<br/>同步窗口外"]
O --> N["下一次 SYNC<br/>重新进入窗口"]

同步窗口是从 SYNC 边沿开始的一段时间。窗口内,同步 PDO 可以按配置处理;窗口外,源码用 syncIsOutsideWindow 记录状态,并通过 CO_SYNC_PASSED_WINDOW 通知上层“刚刚越过窗口”。


3. 协议侧:TIME 规定了什么

3.1 TIME 的角色

TIME 也是 producer/consumer 模型:

1
2
3
4
5
6
7
flowchart LR
T["TIME producer<br/>网络时间源"] -->|6-byte TIME stamp| A["TIME consumer A"]
T -->|6-byte TIME stamp| B["TIME consumer B"]
T -->|6-byte TIME stamp| C["TIME consumer C"]
A --> A1["校准本地 ms/days"]
B --> B1["日志/事件时间戳"]
C --> C1["时间相关应用"]

TIME 解决的是“网络时间一致性”,不是“周期动作边界”。周期动作边界仍应看 SYNC。

3.2 TIME 帧格式

在这里插入图片描述

TIME 的数据长度固定为 6 字节:

1
2
3
4
CAN-ID = 0x100 默认值
DLC = 6
Byte0..Byte3 = milliseconds after midnight,低 28 bit 有效
Byte4..Byte5 = days since 1984-01-01

源码中的结构对应为:

1
2
uint32_t ms;   /* milliseconds after midnight */
uint16_t days; /* days since January 1, 1984 */

3.3 与 TIME 直接相关的对象字典

OD 名称 对源码的影响
0x1012 COB-ID time stamp object bit31 使能 consumer;bit30 使能 producer;bits0..10 是 CAN-ID
0x1013 High resolution time stamp 高分辨率时间戳对象;当前上传的 CO_TIME.* 源码没有处理它

在 CANopenNode 的 CO_TIME.h 中,TIME 的 producer/consumer 角色完全由 0x1012 解析得到:

1
2
3
bit31 = 1 -> isConsumer = true
bit30 = 1 -> isProducer = true
bits0..10 -> COB-ID,默认 0x100

4. 源码阅读地图

在这里插入图片描述

4.1 四个文件各看什么

文件 先看位置 作用
CO_SYNC.h CO_SYNC_tCO_SYNC_status_tCO_SYNCsend()CO_SYNC_process() 声明 理解 SYNC 对象状态、返回值和发送行为
CO_SYNC.c CO_SYNC_receive()OD_write_1005()OD_write_1019()CO_SYNC_init()CO_SYNC_process() 理解接收中断、OD 动态写、初始化、周期处理
CO_TIME.h CO_TIME_tCO_TIME_set()CO_TIME_process() 声明 理解 TIME 对象字段、设置当前时间和 producer 周期
CO_TIME.c CO_TIME_receive()OD_write_1012()CO_TIME_init()CO_TIME_process() 理解接收、OD 动态写、初始化、时间推进和发送

4.2 两个模块的共同模式

1
2
3
4
5
6
7
8
flowchart TB
A["CO_xxx_init()"] --> B["读取对象字典"]
B --> C["配置 CAN RX buffer"]
B --> D["配置 CAN TX buffer<br/>如果启用 producer"]
C --> E["CAN receive callback<br/>只做短路径处理"]
E --> F["设置 CANrxNew flag"]
F --> G["CO_xxx_process()<br/>主循环/任务中处理"]
D --> G

这也是 CANopenNode 很常见的设计方式:中断回调只做轻量接收和置标志,实际协议状态推进放到周期性 process() 里。


5. SYNC 源码运行流程

5.1 初始化:CO_SYNC_init()

CO_SYNC_init() 做的事情可以按对象字典展开:

1
2
3
4
5
6
7
8
9
flowchart TB
A["CO_SYNC_init()"] --> B["读取 0x1005<br/>COB-ID SYNC"]
A --> C["读取 0x1006<br/>communication cycle period"]
A --> D["读取 0x1007<br/>sync window,可选"]
A --> E["读取 0x1019<br/>counter overflow,可选"]
B --> F["解析 CAN-ID / isProducer"]
E --> G["决定 TX/RX DLC<br/>0 或 1"]
F --> H["CO_CANrxBufferInit()<br/>注册 CO_SYNC_receive"]
G --> I["CO_CANtxBufferInit()<br/>配置发送 buffer"]

关键点:

  1. 0x1005 决定 SYNC CAN-ID,并在启用 producer 时决定 isProducer
  2. 0x1006 对 producer 是必要参数;对纯 consumer,可用于超时监控。
  3. 0x1007 是可选参数;为空或为 0 时不启用同步窗口。
  4. 0x1019 是可选参数;源码会把非法值做约束:1 修正为 2,大于 240 修正为 240
  5. RX buffer 注册的回调是 CO_SYNC_receive()

5.2 接收中断:CO_SYNC_receive()

接收回调只做三类事情:

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TB
A["收到 CAN frame"] --> B{"counterOverflowValue == 0 ?"}
B -->|是| C{"DLC == 0 ?"}
B -->|否| D{"DLC == 1 ?"}
C -->|是| E["syncReceived = true"]
C -->|否| F["receiveError = DLC | 0x40"]
D -->|是| G["counter = data[0]<br/>syncReceived = true"]
D -->|否| H["receiveError = DLC | 0x80"]
E --> I["CANrxToggle 翻转"]
G --> I
I --> J["CO_FLAG_SET(CANrxNew)"]
J --> K["可选 callback 唤醒任务"]

这里最重要的变量是:

变量 含义
CANrxNew 通知 CO_SYNC_process() 有新 SYNC 到达
CANrxToggle 给同步 RPDO 双缓冲使用;每个 SYNC 边沿翻转
receiveError 延迟到主循环里上报 EMCY,避免中断里做重处理
counter 启用 1 字节 counter 时保存收到的 counter

CANrxToggle 容易被忽略。源码注释说明,同步 RPDO 需要双接收缓冲;SYNC 边沿到来时切换 buffer,使“正在接收”和“正在处理”的 RPDO buffer 分离。

5.3 发送:CO_SYNCsend()

CO_SYNCsend()CO_SYNC.h 中的 static inline 函数。它的行为很短:

1
2
3
4
5
6
7
8
9
10
flowchart TB
A["CO_SYNCsend()"] --> B["counter++"]
B --> C{"counter > counterOverflowValue ?"}
C -->|是| D["counter = 1"]
C -->|否| E["保持当前 counter"]
D --> F["timer = 0"]
E --> F
F --> G["CANrxToggle 翻转"]
G --> H["data[0] = counter"]
H --> I["CO_CANsend()"]

注意:即使 counterOverflowValue == 0,TX buffer 的 DLC 已在初始化时配置为 0,因此 data[0] 写入不会让帧变成 1 字节帧。帧长度由 CO_CANtxBufferInit() 的 DLC 参数决定。

5.4 周期处理:CO_SYNC_process()

在这里插入图片描述

CO_SYNC_process() 是 SYNC 的主状态推进函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
flowchart TB
A["CO_SYNC_process()"] --> B{"NMT pre-operational<br/>or operational ?"}
B -->|否| Z["清 CANrxNew / receiveError / counter / timer<br/>返回 NONE"]
B -->|是| C["timer += timeDifference_us"]
C --> D{"CANrxNew ?"}
D -->|是| E["timer = 0<br/>status = RX_TX<br/>clear CANrxNew"]
D -->|否| F["继续"]
E --> G["读取 0x1006 period"]
F --> G
G --> H{"period > 0 ?"}
H -->|否| M["只做窗口/错误检查"]
H -->|是| I{"isProducer ?"}
I -->|是| J{"timer >= period ?"}
J -->|是| K["CO_SYNCsend()<br/>status = RX_TX"]
J -->|否| L["更新 timerNext"]
I -->|否| N["consumer 超时监控<br/>timer > 1.5 × period 则 EMCY"]
K --> M
L --> M
N --> M
M --> O{"timer > 0x1007 ?"}
O -->|首次越过| P["status = PASSED_WINDOW"]
O -->|否则| Q["更新 syncIsOutsideWindow"]
P --> R["处理 receiveError<br/>返回 status"]
Q --> R

返回值只有三种:

返回值 含义 上层怎么用
CO_SYNC_NONE 本轮没有 SYNC 事件 常规空转
CO_SYNC_RX_TX 本轮收到或发送了 SYNC 驱动同步 PDO / 应用同步动作
CO_SYNC_PASSED_WINDOW 本轮刚越过同步窗口 可用于禁止窗口外同步 PDO 或触发检查

5.5 consumer 超时监控

0x1006 非零、当前节点不是 producer,并且已经收到过至少一次 SYNC 后,源码开始超时监控:

1
2
3
periodTimeout = 1.5 × OD_1006_period
if timer > periodTimeout:
CO_errorReport(CO_EM_SYNC_TIME_OUT, ...)

收到新的 SYNC 后,如果之前处于超时错误状态,则调用 CO_errorReset() 清除错误。

5.6 0x1005 / 0x1019 动态写入

如果启用了 CO_CONFIG_FLAG_OD_DYNAMIC,写对象字典时会进入扩展写函数:

1
2
3
4
5
6
7
8
9
flowchart TB
A["SDO/本地写 OD"] --> B{"写 0x1005 ?"}
A --> C{"写 0x1019 ?"}
B -->|是| D["OD_write_1005()<br/>校验 COB-ID 和 producer bit"]
D --> E["必要时重新初始化 RX/TX buffer"]
C -->|是| F["OD_write_1019()<br/>校验 0 或 2..240"]
F --> G{"0x1006 period == 0 ?"}
G -->|是| H["重新配置 TX DLC 0/1"]
G -->|否| I["拒绝:ODR_DATA_DEV_STATE"]

这里的限制很实际:0x1019 会改变 SYNC 帧 DLC,源码要求在通信周期未启用时修改,避免运行中改变帧格式造成网络不一致。


6. TIME 源码运行流程

6.1 初始化:CO_TIME_init()

CO_TIME_init() 的主线比 SYNC 更短:

1
2
3
4
5
6
7
8
9
flowchart TB
A["CO_TIME_init()"] --> B["读取 0x1012<br/>COB-ID time stamp"]
B --> C["解析 CAN-ID"]
B --> D["bit31 -> isConsumer"]
B --> E["bit30 -> isProducer"]
D --> F{"isConsumer ?"}
F -->|是| G["CO_CANrxBufferInit()<br/>注册 CO_TIME_receive"]
F -->|否| H["不注册接收"]
E --> I["CO_CANtxBufferInit()<br/>DLC = 6"]

关键点:

  1. TIME 的启停方向由 0x1012 控制,不是由 Node-ID 控制。
  2. consumer 使能后才注册接收 buffer。
  3. producer 相关编译能力还受 CO_CONFIG_TIME_PRODUCER 控制;编译时没打开 producer,运行时 bit30 也不能让它真正发送。
  4. TX buffer 的 DLC 固定是 CO_TIME_MSG_LENGTH = 6

6.2 接收中断:CO_TIME_receive()

TIME 的接收回调只接受 6 字节帧:

1
2
3
4
5
6
flowchart TB
A["收到 TIME CAN frame"] --> B{"DLC == CO_TIME_MSG_LENGTH(6) ?"}
B -->|否| C["忽略"]
B -->|是| D["memcpy(data -> TIME->timeStamp)"]
D --> E["CO_FLAG_SET(CANrxNew)"]
E --> F["可选 callback 唤醒任务"]

TIME 源码没有像 SYNC 那样记录接收长度错误。长度不等于 6 时直接忽略。

6.3 周期处理:CO_TIME_process()

在这里插入图片描述

CO_TIME_process() 有三段逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
flowchart TB
A["CO_TIME_process()"] --> B{"NMT pre-operational<br/>or operational<br/>并且 isConsumer ?"}
B -->|是| C{"CANrxNew ?"}
C -->|是| D["解析 timeStamp[0..5]"]
D --> E["ms = swapped32 & 0x0FFFFFFF<br/>days = swapped16<br/>residual_us = 0"]
E --> F["timestampReceived = true<br/>clear CANrxNew"]
C -->|否| G["没有新 TIME"]
B -->|否| H["clear CANrxNew"]
F --> I{"本轮是否未收到 TIME<br/>且 timeDifference_us > 0 ?"}
G --> I
H --> I
I -->|是| J["用 timeDifference_us 本地累计 ms/days"]
I -->|否| K["跳过本地累计"]
J --> L{"isProducer 且 producerInterval_ms > 0 ?"}
K --> L
L -->|是| M{"producerTimer_ms 到期 ?"}
M -->|是| N["打包 ms/days 到 CANtxBuff<br/>CO_CANsend()"]
M -->|否| O["producerTimer_ms += 本轮新增 ms"]
L -->|否| P["producerTimer_ms = producerInterval_ms"]
N --> Q["返回 timestampReceived"]
O --> Q
P --> Q

6.4 接收到 TIME 后如何解析

源码解析逻辑:

1
2
3
4
5
6
uint32_t ms_swapped = CO_getUint32(&TIME->timeStamp[0]);
uint16_t days_swapped = CO_getUint16(&TIME->timeStamp[4]);

TIME->ms = CO_SWAP_32(ms_swapped) & 0x0FFFFFFFU;
TIME->days = CO_SWAP_16(days_swapped);
TIME->residual_us = 0;

要点:

  1. ms 只取低 28 bit。
  2. days 是 16 bit。
  3. 收到 TIME 后清零 residual_us,避免前一次本地微秒余数污染对时结果。
  4. 返回值 true 只表示本轮刚收到并处理了 TIME stamp,不表示 producer 已发送 TIME。

6.5 没有收到 TIME 时如何本地走时

如果本轮没有新 TIME,源码用 timeDifference_us 累加本地时间:

1
2
3
4
uint32_t us = timeDifference_us + TIME->residual_us;
ms = us / 1000U;
TIME->residual_us = us % 1000U;
TIME->ms += ms;

ms 超过一天时:

1
2
3
4
if (TIME->ms >= 24h_ms) {
TIME->ms -= 24h_ms;
TIME->days += 1;
}

这说明 TIME consumer 并不是“只靠网络 TIME 才能走时”。收到 TIME 是校准点;两次校准之间,它仍靠本地周期时间差继续推进。

6.6 TIME producer 如何启动

TIME producer 需要两件事同时成立:

1
2
编译期:CO_CONFIG_TIME_PRODUCER 打开
运行期:0x1012 bit30 = 1,并且 CO_TIME_set(..., producerInterval_ms > 0)

CO_TIME_set() 做了三件事:

1
2
3
4
TIME->residual_us = 0;
TIME->ms = ms;
TIME->days = days;
TIME->producerTimer_ms = TIME->producerInterval_ms = producerInterval_ms;

因此,producer 至少要先调用一次 CO_TIME_set() 设置当前时间和发送周期。只配置 0x1012 bit30 不足以产生周期 TIME 帧。


7. TIME 与 SYNC 在主循环中的关系

在 CANopenNode 的常规处理模型中,SYNC 通常先被处理,再用它的结果驱动同步 PDO:

1
2
3
4
5
6
7
8
9
flowchart TB
A["周期 tick / RTOS task"] --> B["CO_process_SYNC() 或 CO_SYNC_process()"]
B --> C{"syncWas ?"}
C -->|是| D["处理同步 RPDO/TPDO"]
C -->|否| E["处理非同步对象"]
A --> F["CO_TIME_process()"]
F --> G{"timestampReceived ?"}
G -->|是| H["应用获得新的网络时间"]
G -->|否| I["本地时间继续递增"]

SYNC 和 TIME 没有直接依赖关系:

问题 看哪个对象
周期 PDO 为什么没发? 先看 SYNC、PDO 传输类型、NMT 状态
网络时间为什么没更新? 先看 TIME、0x1012、DLC 是否为 6
同步窗口外 PDO 为什么被丢? 0x1007syncIsOutsideWindow、PDO 处理顺序
节点是否收到过 SYNC? CO_SYNC_process() 返回值和 timeoutError
TIME producer 为什么没发? CO_TIME_set() 是否设置了非零 producerInterval_ms

8. 最小心智模型

1
2
3
4
5
6
7
8
flowchart TB
A["对象字典决定角色和参数"] --> B["init() 读取 OD 并配置 CAN buffer"]
B --> C["CAN receive callback 只搬运数据和置 flag"]
C --> D["process() 根据 NMT 状态、timer 和 flag 推进状态"]
D --> E["SYNC 输出同步事件/窗口状态"]
D --> F["TIME 输出 timestampReceived 并维护 ms/days"]
E --> G["PDO/应用同步逻辑"]
F --> H["日志/时间戳/应用时间基准"]

记住这三条就不容易读偏:

  1. SYNC0x1005/0x1006/0x1007/0x1019,核心是“周期边界 + 同步窗口 + counter”。
  2. TIME0x1012,核心是“6 字节时间戳 + 本地走时 + 可选周期发送”。
  3. CANopenNode 的 receive() 不是完整协议处理,它只是中断短路径;真正的状态变化主要在 process()

参考资料