CANopene Heartbeat运行流程

文章目录
- [CANopene Heartbeat运行流程](#CANopene Heartbeat运行流程)
-
- 先给结论
- [1. 协议先行:Heartbeat Consumer 解决什么问题](#1. 协议先行:Heartbeat Consumer 解决什么问题)
- [2. `0x1016` 与 `0x1017`:不要把"我监控别人"和"我发心跳"混在一起](#2.
0x1016与0x1017:不要把“我监控别人”和“我发心跳”混在一起) - [3. CANopenNode 里 `CO_HBconsumer_t` 保存了哪些状态](#3. CANopenNode 里
CO_HBconsumer_t保存了哪些状态) -
- [3.1 每个被监控节点的状态](#3.1 每个被监控节点的状态)
- [4. 初始化流程:从 `0x1016` 到 CAN RX buffer](#4. 初始化流程:从
0x1016到 CAN RX buffer) -
- [4.1 绑定外部资源](#4.1 绑定外部资源)
- [4.2 计算实际监控项数量](#4.2 计算实际监控项数量)
- [4.3 逐项读取 `0x1016`](#4.3 逐项读取
0x1016) - [4.4 `CO_HBconsumer_initEntry()` 配置一个监控项](#4.4
CO_HBconsumer_initEntry()配置一个监控项) - [4.5 动态写 `0x1016`](#4.5 动态写
0x1016)
- [5. 接收回调:中断上下文只做轻量预处理](#5. 接收回调:中断上下文只做轻量预处理)
- [6. 主处理流程:`CO_HBconsumer_process()` 真正运行状态机](#6. 主处理流程:
CO_HBconsumer_process()真正运行状态机) -
- [6.1 本节点 NMT 状态门控](#6.1 本节点 NMT 状态门控)
- [6.2 有新帧:先区分 boot-up 和 heartbeat](#6.2 有新帧:先区分 boot-up 和 heartbeat)
- [6.3 无新帧或处理完新帧后:检查超时](#6.3 无新帧或处理完新帧后:检查超时)
- [6.4 聚合状态和 NMT changed callback](#6.4 聚合状态和 NMT changed callback)
- [6.5 所有节点恢复 active 后清错误](#6.5 所有节点恢复 active 后清错误)
- [7. 回调配置:公共回调与每节点回调不能同时启用](#7. 回调配置:公共回调与每节点回调不能同时启用)
- [8. 查询接口:只在启用 `CO_CONFIG_HB_CONS_QUERY_FUNCT` 时存在](#8. 查询接口:只在启用
CO_CONFIG_HB_CONS_QUERY_FUNCT时存在) - [9. STM32/RTOS 工程里怎么接入这段逻辑](#9. STM32/RTOS 工程里怎么接入这段逻辑)
- [10. 调试时重点看这几类现象](#10. 调试时重点看这几类现象)
- [11. 一句话总结](#11. 一句话总结)
- 参考资料
#mermaid-svg-diC35Nu75q2Pzs3T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-diC35Nu75q2Pzs3T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-diC35Nu75q2Pzs3T .error-icon{fill:#552222;}#mermaid-svg-diC35Nu75q2Pzs3T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-diC35Nu75q2Pzs3T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-diC35Nu75q2Pzs3T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-diC35Nu75q2Pzs3T .marker.cross{stroke:#333333;}#mermaid-svg-diC35Nu75q2Pzs3T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-diC35Nu75q2Pzs3T p{margin:0;}#mermaid-svg-diC35Nu75q2Pzs3T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-diC35Nu75q2Pzs3T .cluster-label text{fill:#333;}#mermaid-svg-diC35Nu75q2Pzs3T .cluster-label span{color:#333;}#mermaid-svg-diC35Nu75q2Pzs3T .cluster-label span p{background-color:transparent;}#mermaid-svg-diC35Nu75q2Pzs3T .label text,#mermaid-svg-diC35Nu75q2Pzs3T span{fill:#333;color:#333;}#mermaid-svg-diC35Nu75q2Pzs3T .node rect,#mermaid-svg-diC35Nu75q2Pzs3T .node circle,#mermaid-svg-diC35Nu75q2Pzs3T .node ellipse,#mermaid-svg-diC35Nu75q2Pzs3T .node polygon,#mermaid-svg-diC35Nu75q2Pzs3T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-diC35Nu75q2Pzs3T .rough-node .label text,#mermaid-svg-diC35Nu75q2Pzs3T .node .label text,#mermaid-svg-diC35Nu75q2Pzs3T .image-shape .label,#mermaid-svg-diC35Nu75q2Pzs3T .icon-shape .label{text-anchor:middle;}#mermaid-svg-diC35Nu75q2Pzs3T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-diC35Nu75q2Pzs3T .rough-node .label,#mermaid-svg-diC35Nu75q2Pzs3T .node .label,#mermaid-svg-diC35Nu75q2Pzs3T .image-shape .label,#mermaid-svg-diC35Nu75q2Pzs3T .icon-shape .label{text-align:center;}#mermaid-svg-diC35Nu75q2Pzs3T .node.clickable{cursor:pointer;}#mermaid-svg-diC35Nu75q2Pzs3T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-diC35Nu75q2Pzs3T .arrowheadPath{fill:#333333;}#mermaid-svg-diC35Nu75q2Pzs3T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-diC35Nu75q2Pzs3T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-diC35Nu75q2Pzs3T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-diC35Nu75q2Pzs3T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-diC35Nu75q2Pzs3T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-diC35Nu75q2Pzs3T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-diC35Nu75q2Pzs3T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-diC35Nu75q2Pzs3T .cluster text{fill:#333;}#mermaid-svg-diC35Nu75q2Pzs3T .cluster span{color:#333;}#mermaid-svg-diC35Nu75q2Pzs3T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-diC35Nu75q2Pzs3T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-diC35Nu75q2Pzs3T rect.text{fill:none;stroke-width:0;}#mermaid-svg-diC35Nu75q2Pzs3T .icon-shape,#mermaid-svg-diC35Nu75q2Pzs3T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-diC35Nu75q2Pzs3T .icon-shape p,#mermaid-svg-diC35Nu75q2Pzs3T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-diC35Nu75q2Pzs3T .icon-shape .label rect,#mermaid-svg-diC35Nu75q2Pzs3T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-diC35Nu75q2Pzs3T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-diC35Nu75q2Pzs3T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-diC35Nu75q2Pzs3T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本节点 Heartbeat Consumer
远端 Heartbeat Producer
Node-ID = n
周期广播
CAN 接收过滤
配置监控对象
未按时收到或收到 boot-up
OD 0x1017
Producer heartbeat time
配置发送周期
Heartbeat producer
周期 = producer time
CAN 数据帧
COB-ID = 0x700 + n
DLC = 1
Byte0 = NMT state
Heartbeat consumer
按 expected time 等待下一帧
OD 0x1016
高 16 位: producer Node-ID
低 16 位: expected time(ms)
超时/远端复位事件
上报 EMCY / 更新状态
先给结论
Heartbeat Consumer 的本质是:本节点用对象字典 0x1016 配置要监控的远端节点,然后监听远端节点按 0x700 + Node-ID 发出的 1 字节 Heartbeat;如果已开始监控后在规定时间内没收到下一帧,就把该远端节点判为 heartbeat timeout,并通过 Emergency 模块上报错误。
在 CANopenNode 的 CO_HBconsumer.c/h 中,这条链路可以压缩成:
text
OD 0x1016 sub-index 写入/初始化
-> 解析 producer Node-ID 和 consumer heartbeat time
-> 为每个被监控节点配置一个 CAN RX buffer: 0x700 + Node-ID
-> CAN RX 回调只保存 data[0] 和 CANrxNew 标志
-> CO_HBconsumer_process() 周期处理
-> 区分 boot-up / heartbeat / timeout / NMT state change
-> 更新 HBstate、NMTstate、allMonitoredActive、allMonitoredOperational
-> 必要时调用 CO_errorReport() / CO_errorReset()
这里最重要的分工是:CAN 接收回调只做"收到了什么"的预处理;真正的状态机、超时判断、EMCY 上报都在 CO_HBconsumer_process() 中完成。
1. 协议先行:Heartbeat Consumer 解决什么问题
CANopen 的 NMT 负责节点通信状态管理,Heartbeat 则是错误控制的一部分:远端节点作为 heartbeat producer 周期性广播自己的 NMT 状态;本节点作为 heartbeat consumer 接收这些状态并判断远端节点是否还在线、是否处于期望状态。
CiA 对 NMT 的公开说明中,NMT 状态机包含 Initialization、Pre-operational、Operational、Stopped;设备初始化完成后会进入 Pre-operational,并通过 boot-up message 表示已经准备好工作。NMT 命令本身由 active NMT manager 发送,CAN-ID 为 0x000,数据长度为 2 字节,byte0 是命令,byte1 是目标 Node-ID,Node-ID 为 0 表示所有节点。[1](#1)
Heartbeat 与 NMT 的关系是:Heartbeat 的 1 字节数据就是远端节点当前的 NMT state。 常见状态值如下:
| Byte0 | 含义 | 在源码中的典型名字 |
|---|---|---|
0x00 |
boot-up / Initializing | CO_NMT_INITIALIZING |
0x04 |
Stopped | CO_NMT_STOPPED |
0x05 |
Operational | CO_NMT_OPERATIONAL |
0x7F |
Pre-operational | CO_NMT_PRE_OPERATIONAL |
Heartbeat 的 CAN 帧格式很小:
| 字段 | 值 |
|---|---|
| CAN-ID / COB-ID | 0x700 + producer Node-ID |
| DLC | 1 |
| Byte0 | producer 当前 NMT state |
对于 consumer 来说,核心配置在 0x1016 Consumer heartbeat time;对于 producer 来说,核心配置在 0x1017 Producer heartbeat time。0x1016 的一个子项是 32 位值,高 16 位给出 producer Node-ID,低 16 位给出 expected time,单位为 ms。consumer 必须在该时间窗内收到被监控 producer 的 heartbeat,否则产生 heartbeat event。[2](#2)
2. 0x1016 与 0x1017:不要把"我监控别人"和"我发心跳"混在一起
#mermaid-svg-fv4lUdiVPgc9wRlU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fv4lUdiVPgc9wRlU .error-icon{fill:#552222;}#mermaid-svg-fv4lUdiVPgc9wRlU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fv4lUdiVPgc9wRlU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fv4lUdiVPgc9wRlU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fv4lUdiVPgc9wRlU .marker.cross{stroke:#333333;}#mermaid-svg-fv4lUdiVPgc9wRlU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fv4lUdiVPgc9wRlU p{margin:0;}#mermaid-svg-fv4lUdiVPgc9wRlU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster-label text{fill:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster-label span{color:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster-label span p{background-color:transparent;}#mermaid-svg-fv4lUdiVPgc9wRlU .label text,#mermaid-svg-fv4lUdiVPgc9wRlU span{fill:#333;color:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU .node rect,#mermaid-svg-fv4lUdiVPgc9wRlU .node circle,#mermaid-svg-fv4lUdiVPgc9wRlU .node ellipse,#mermaid-svg-fv4lUdiVPgc9wRlU .node polygon,#mermaid-svg-fv4lUdiVPgc9wRlU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fv4lUdiVPgc9wRlU .rough-node .label text,#mermaid-svg-fv4lUdiVPgc9wRlU .node .label text,#mermaid-svg-fv4lUdiVPgc9wRlU .image-shape .label,#mermaid-svg-fv4lUdiVPgc9wRlU .icon-shape .label{text-anchor:middle;}#mermaid-svg-fv4lUdiVPgc9wRlU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fv4lUdiVPgc9wRlU .rough-node .label,#mermaid-svg-fv4lUdiVPgc9wRlU .node .label,#mermaid-svg-fv4lUdiVPgc9wRlU .image-shape .label,#mermaid-svg-fv4lUdiVPgc9wRlU .icon-shape .label{text-align:center;}#mermaid-svg-fv4lUdiVPgc9wRlU .node.clickable{cursor:pointer;}#mermaid-svg-fv4lUdiVPgc9wRlU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fv4lUdiVPgc9wRlU .arrowheadPath{fill:#333333;}#mermaid-svg-fv4lUdiVPgc9wRlU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fv4lUdiVPgc9wRlU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fv4lUdiVPgc9wRlU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fv4lUdiVPgc9wRlU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fv4lUdiVPgc9wRlU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fv4lUdiVPgc9wRlU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster text{fill:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU .cluster span{color:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fv4lUdiVPgc9wRlU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fv4lUdiVPgc9wRlU rect.text{fill:none;stroke-width:0;}#mermaid-svg-fv4lUdiVPgc9wRlU .icon-shape,#mermaid-svg-fv4lUdiVPgc9wRlU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fv4lUdiVPgc9wRlU .icon-shape p,#mermaid-svg-fv4lUdiVPgc9wRlU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fv4lUdiVPgc9wRlU .icon-shape .label rect,#mermaid-svg-fv4lUdiVPgc9wRlU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fv4lUdiVPgc9wRlU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fv4lUdiVPgc9wRlU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fv4lUdiVPgc9wRlU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 决定发送周期
决定监听 Node-ID
和 expected time
按周期到达
超出 expected time 未收到
远端节点
0x1017 Producer heartbeat time
heartbeat 帧
0x700 + Node-ID
DLC = 1
Byte0 = NMT state
本节点
0x1016 Consumer heartbeat time
Heartbeat consumer 监控窗口
heartbeat event / EMCY
| 对象 | 所在节点 | 方向 | 作用 |
|---|---|---|---|
0x1017 Producer heartbeat time |
被监控节点 | 发出 | 规定该节点多久广播一次自己的 heartbeat |
0x1016 Consumer heartbeat time |
监控者节点 | 接收 | 规定本节点要监听哪个 producer,以及多久收不到算超时 |
一个典型配置是:
text
远端节点 3:0x1017 = 10 ms -> 每 10 ms 发一次 heartbeat
本节点: 0x1016:01 = 0x0003_001E
高 16 位 0x0003 => 监听 Node-ID 3
低 16 位 0x001E => 30 ms 内没收到就超时
通常 consumer 的 expected time 应设置得比 producer 的发送周期稍长,给 CAN 调度、主循环抖动和短时仲裁延迟留余量。公开设备文档也给出类似建议:producer 的 heartbeat 输出周期应略短于 consumer 的 expected time。[2](#2)
3. CANopenNode 里 CO_HBconsumer_t 保存了哪些状态
CO_HBconsumer.h 中有两层结构:
CO_HBconsumer_t:整个 Heartbeat Consumer 对象。CO_HBconsNode_t:数组中的一个被监控远端节点。
渲染错误: Mermaid 渲染失败: Parse error on line 10: ... A -->|收到 boot-up(0)
远端复位| K -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
3.1 每个被监控节点的状态
CO_HBconsumer_state_t 有 4 个状态:
| 状态 | 触发条件 | 含义 |
|---|---|---|
CO_HBconsumer_UNCONFIGURED |
nodeId == 0 或 consumerTime == 0 |
该子项未启用,不监控 |
CO_HBconsumer_UNKNOWN |
已配置但未收到有效 heartbeat,或收到 boot-up 后等待下一帧 | 还不能确认远端在线 |
CO_HBconsumer_ACTIVE |
收到非 boot-up heartbeat,且未超时 | 远端 heartbeat 正常 |
CO_HBconsumer_TIMEOUT |
已 active 后超过 time_us 未收到下一帧 |
远端 heartbeat 超时 |
源码中的关键成员可以按用途分成几组:
| 成员 | 用途 |
|---|---|
nodeId |
被监控 producer 的 Node-ID |
NMTstate |
最近一次 heartbeat 的 Byte0,即远端 NMT state |
HBstate |
本 consumer 对该远端节点的监控状态 |
timeoutTimer |
自上次 heartbeat 后累计的时间 |
time_us |
从 0x1016 低 16 位转换得到的超时时间,单位 us |
CANrxNew |
CAN 接收回调置位,process() 读取后清零 |
| 回调函数指针 | 可选:pre callback、NMT changed、heartbeat started、timeout、remote reset |
CO_HBconsumer_t 还维护两个聚合标志:
| 成员 | 含义 |
|---|---|
allMonitoredActive |
所有已配置节点都处于 ACTIVE,或者没有节点被监控 |
allMonitoredOperational |
所有已配置节点的 NMTstate 都是 CO_NMT_OPERATIONAL,或者没有节点被监控 |
这两个标志适合应用层做"所有关键节点都在线/都进入 Operational 了吗"的快速判断。
4. 初始化流程:从 0x1016 到 CAN RX buffer
#mermaid-svg-W3as42tG0Qr93XtP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-W3as42tG0Qr93XtP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-W3as42tG0Qr93XtP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-W3as42tG0Qr93XtP .error-icon{fill:#552222;}#mermaid-svg-W3as42tG0Qr93XtP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W3as42tG0Qr93XtP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-W3as42tG0Qr93XtP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W3as42tG0Qr93XtP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W3as42tG0Qr93XtP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-W3as42tG0Qr93XtP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W3as42tG0Qr93XtP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W3as42tG0Qr93XtP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W3as42tG0Qr93XtP .marker.cross{stroke:#333333;}#mermaid-svg-W3as42tG0Qr93XtP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W3as42tG0Qr93XtP p{margin:0;}#mermaid-svg-W3as42tG0Qr93XtP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-W3as42tG0Qr93XtP .cluster-label text{fill:#333;}#mermaid-svg-W3as42tG0Qr93XtP .cluster-label span{color:#333;}#mermaid-svg-W3as42tG0Qr93XtP .cluster-label span p{background-color:transparent;}#mermaid-svg-W3as42tG0Qr93XtP .label text,#mermaid-svg-W3as42tG0Qr93XtP span{fill:#333;color:#333;}#mermaid-svg-W3as42tG0Qr93XtP .node rect,#mermaid-svg-W3as42tG0Qr93XtP .node circle,#mermaid-svg-W3as42tG0Qr93XtP .node ellipse,#mermaid-svg-W3as42tG0Qr93XtP .node polygon,#mermaid-svg-W3as42tG0Qr93XtP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-W3as42tG0Qr93XtP .rough-node .label text,#mermaid-svg-W3as42tG0Qr93XtP .node .label text,#mermaid-svg-W3as42tG0Qr93XtP .image-shape .label,#mermaid-svg-W3as42tG0Qr93XtP .icon-shape .label{text-anchor:middle;}#mermaid-svg-W3as42tG0Qr93XtP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-W3as42tG0Qr93XtP .rough-node .label,#mermaid-svg-W3as42tG0Qr93XtP .node .label,#mermaid-svg-W3as42tG0Qr93XtP .image-shape .label,#mermaid-svg-W3as42tG0Qr93XtP .icon-shape .label{text-align:center;}#mermaid-svg-W3as42tG0Qr93XtP .node.clickable{cursor:pointer;}#mermaid-svg-W3as42tG0Qr93XtP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-W3as42tG0Qr93XtP .arrowheadPath{fill:#333333;}#mermaid-svg-W3as42tG0Qr93XtP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-W3as42tG0Qr93XtP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-W3as42tG0Qr93XtP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W3as42tG0Qr93XtP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-W3as42tG0Qr93XtP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W3as42tG0Qr93XtP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-W3as42tG0Qr93XtP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-W3as42tG0Qr93XtP .cluster text{fill:#333;}#mermaid-svg-W3as42tG0Qr93XtP .cluster span{color:#333;}#mermaid-svg-W3as42tG0Qr93XtP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-W3as42tG0Qr93XtP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-W3as42tG0Qr93XtP rect.text{fill:none;stroke-width:0;}#mermaid-svg-W3as42tG0Qr93XtP .icon-shape,#mermaid-svg-W3as42tG0Qr93XtP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W3as42tG0Qr93XtP .icon-shape p,#mermaid-svg-W3as42tG0Qr93XtP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-W3as42tG0Qr93XtP .icon-shape .label rect,#mermaid-svg-W3as42tG0Qr93XtP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W3as42tG0Qr93XtP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-W3as42tG0Qr93XtP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-W3as42tG0Qr93XtP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
CO_HBconsumer_init()
通信复位阶段调用
检查 HBcons / em / OD / CAN 参数
清零 CO_HBconsumer_t
绑定 em、monitoredNodes、CANdevRx、CANdevRxIdxStart
numberOfMonitoredNodes =
min(OD 0x1016 子项数 - 1, 外部数组容量)
遍历 0x1016 子索引
val = OD_get_u32()
解析 val
nodeId = val >> 16
consumer_time = val & 0xFFFF
CO_HBconsumer_initEntry()
校验重复 nodeId
设置 nodeId / time_us / NMTstate / HBstate
CO_CANrxBufferInit()
COB-ID = 0x700 + nodeId
mask = 0x7FF
callback = CO_HBcons_receive
还有 0x1016 子项?
启用 OD_DYNAMIC?
安装 OD extension
OD_write_1016() 写入时重新 initEntry
返回 CO_ERROR_NO
或 OD/参数错误
CO_HBconsumer_init() 必须在 communication reset 阶段调用。它主要做 5 件事。
4.1 绑定外部资源
函数先校验参数,然后清零 CO_HBconsumer_t,绑定:
text
HBcons->em -> Emergency 对象
HBcons->monitoredNodes -> 外部提供的 CO_HBconsNode_t 数组
HBcons->CANdevRx -> CAN RX 模块
HBcons->CANdevRxIdxStart -> HB consumer 使用的 RX buffer 起始下标
注意:monitoredNodes 数组由外部提供,CANopenNode 不在 CO_HBconsumer_init() 里为它动态分配内存。
4.2 计算实际监控项数量
源码取下面两者的较小值:
text
OD_1016_HBcons->subEntriesCount - 1
monitoredNodesCount
这表示:对象字典里配置了多少个 0x1016 子项是一层上限,外部数组容量又是一层上限。
4.3 逐项读取 0x1016
对每个子索引:
c
val = OD_get_u32(OD_1016_HBcons, i + 1U, ...);
nodeId = (val >> 16) & 0xFFU;
consumer_time = val & 0xFFFFU;
CO_HBconsumer_initEntry(HBcons, i, nodeId, consumer_time);
这里 consumer_time 单位是 ms;进入 CO_HBconsumer_initEntry() 后会乘以 1000U 转成 time_us。
4.4 CO_HBconsumer_initEntry() 配置一个监控项
该函数做了几个关键判断:
- 参数为空或
idx越界,返回非法参数。 - 如果
consumerTime_ms != 0且nodeId != 0,检查是否有重复 nodeId。 - 若
nodeId != 0且time_us != 0:COB_ID = 0x700 + nodeIdHBstate = CO_HBconsumer_UNKNOWN
- 否则:
COB_ID = 0time_us = 0HBstate = CO_HBconsumer_UNCONFIGURED
- 最后调用
CO_CANrxBufferInit(),把 CAN 接收过滤器和CO_HBcons_receive()回调绑定起来。
4.5 动态写 0x1016
如果启用了 CO_CONFIG_FLAG_OD_DYNAMIC,CO_HBconsumer_init() 会给 0x1016 安装 OD extension。之后应用或 SDO 写 0x1016 时,OD_write_1016() 会重新解析 32 位值并调用 CO_HBconsumer_initEntry(),从而动态改变被监控节点和接收过滤器。
这也是为什么 CO_CONFIG_HB_CONS 默认包含 CO_CONFIG_GLOBAL_FLAG_OD_DYNAMIC:调试或主站配置阶段可以直接通过对象字典改变监控关系。
5. 接收回调:中断上下文只做轻量预处理
#mermaid-svg-wTrzAWXHuNmXzlaP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wTrzAWXHuNmXzlaP .error-icon{fill:#552222;}#mermaid-svg-wTrzAWXHuNmXzlaP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wTrzAWXHuNmXzlaP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wTrzAWXHuNmXzlaP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wTrzAWXHuNmXzlaP .marker.cross{stroke:#333333;}#mermaid-svg-wTrzAWXHuNmXzlaP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wTrzAWXHuNmXzlaP p{margin:0;}#mermaid-svg-wTrzAWXHuNmXzlaP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster-label text{fill:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster-label span{color:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster-label span p{background-color:transparent;}#mermaid-svg-wTrzAWXHuNmXzlaP .label text,#mermaid-svg-wTrzAWXHuNmXzlaP span{fill:#333;color:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP .node rect,#mermaid-svg-wTrzAWXHuNmXzlaP .node circle,#mermaid-svg-wTrzAWXHuNmXzlaP .node ellipse,#mermaid-svg-wTrzAWXHuNmXzlaP .node polygon,#mermaid-svg-wTrzAWXHuNmXzlaP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wTrzAWXHuNmXzlaP .rough-node .label text,#mermaid-svg-wTrzAWXHuNmXzlaP .node .label text,#mermaid-svg-wTrzAWXHuNmXzlaP .image-shape .label,#mermaid-svg-wTrzAWXHuNmXzlaP .icon-shape .label{text-anchor:middle;}#mermaid-svg-wTrzAWXHuNmXzlaP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wTrzAWXHuNmXzlaP .rough-node .label,#mermaid-svg-wTrzAWXHuNmXzlaP .node .label,#mermaid-svg-wTrzAWXHuNmXzlaP .image-shape .label,#mermaid-svg-wTrzAWXHuNmXzlaP .icon-shape .label{text-align:center;}#mermaid-svg-wTrzAWXHuNmXzlaP .node.clickable{cursor:pointer;}#mermaid-svg-wTrzAWXHuNmXzlaP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wTrzAWXHuNmXzlaP .arrowheadPath{fill:#333333;}#mermaid-svg-wTrzAWXHuNmXzlaP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wTrzAWXHuNmXzlaP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wTrzAWXHuNmXzlaP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTrzAWXHuNmXzlaP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wTrzAWXHuNmXzlaP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTrzAWXHuNmXzlaP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster text{fill:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP .cluster span{color:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wTrzAWXHuNmXzlaP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wTrzAWXHuNmXzlaP rect.text{fill:none;stroke-width:0;}#mermaid-svg-wTrzAWXHuNmXzlaP .icon-shape,#mermaid-svg-wTrzAWXHuNmXzlaP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTrzAWXHuNmXzlaP .icon-shape p,#mermaid-svg-wTrzAWXHuNmXzlaP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wTrzAWXHuNmXzlaP .icon-shape .label rect,#mermaid-svg-wTrzAWXHuNmXzlaP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTrzAWXHuNmXzlaP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wTrzAWXHuNmXzlaP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wTrzAWXHuNmXzlaP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主循环 / RTOS 任务上下文
CAN RX 中断/驱动回调上下文
共享标志
CAN RX 匹配
COB-ID = 0x700 + nodeId
DLC = 1
CO_HBcons_receive()
只做预处理
NMTstate = data0
CANrxNew = true
可选 pre callback 唤醒任务
CO_HBconsumer_process()
区分 boot-up / heartbeat
更新 HBstate
检查超时
触发回调 / EMCY
CO_HBcons_receive() 是 CAN RX 回调。它只接受 DLC 为 1 的帧:
c
if (DLC == 1U) {
HBconsNode->NMTstate = (CO_NMT_internalState_t)data[0];
CO_FLAG_SET(HBconsNode->CANrxNew);
... optional pre callback ...
}
这段代码有两个设计点:
- 不在 CAN 回调里判断超时、不上报 EMCY、不做复杂状态机。
- 只把最新的 NMT state 和
CANrxNew标志交给周期处理函数。
如果启用了 CO_CONFIG_FLAG_CALLBACK_PRE,收到 heartbeat 后可以调用 pre callback。这个回调常用于 RTOS 场景下唤醒处理 CO_HBconsumer_process() 的任务。
6. 主处理流程:CO_HBconsumer_process() 真正运行状态机
#mermaid-svg-rcvCc9R5gJoBV1UR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rcvCc9R5gJoBV1UR .error-icon{fill:#552222;}#mermaid-svg-rcvCc9R5gJoBV1UR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rcvCc9R5gJoBV1UR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rcvCc9R5gJoBV1UR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rcvCc9R5gJoBV1UR .marker.cross{stroke:#333333;}#mermaid-svg-rcvCc9R5gJoBV1UR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rcvCc9R5gJoBV1UR p{margin:0;}#mermaid-svg-rcvCc9R5gJoBV1UR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster-label text{fill:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster-label span{color:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster-label span p{background-color:transparent;}#mermaid-svg-rcvCc9R5gJoBV1UR .label text,#mermaid-svg-rcvCc9R5gJoBV1UR span{fill:#333;color:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR .node rect,#mermaid-svg-rcvCc9R5gJoBV1UR .node circle,#mermaid-svg-rcvCc9R5gJoBV1UR .node ellipse,#mermaid-svg-rcvCc9R5gJoBV1UR .node polygon,#mermaid-svg-rcvCc9R5gJoBV1UR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rcvCc9R5gJoBV1UR .rough-node .label text,#mermaid-svg-rcvCc9R5gJoBV1UR .node .label text,#mermaid-svg-rcvCc9R5gJoBV1UR .image-shape .label,#mermaid-svg-rcvCc9R5gJoBV1UR .icon-shape .label{text-anchor:middle;}#mermaid-svg-rcvCc9R5gJoBV1UR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rcvCc9R5gJoBV1UR .rough-node .label,#mermaid-svg-rcvCc9R5gJoBV1UR .node .label,#mermaid-svg-rcvCc9R5gJoBV1UR .image-shape .label,#mermaid-svg-rcvCc9R5gJoBV1UR .icon-shape .label{text-align:center;}#mermaid-svg-rcvCc9R5gJoBV1UR .node.clickable{cursor:pointer;}#mermaid-svg-rcvCc9R5gJoBV1UR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rcvCc9R5gJoBV1UR .arrowheadPath{fill:#333333;}#mermaid-svg-rcvCc9R5gJoBV1UR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rcvCc9R5gJoBV1UR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rcvCc9R5gJoBV1UR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rcvCc9R5gJoBV1UR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rcvCc9R5gJoBV1UR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rcvCc9R5gJoBV1UR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster text{fill:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR .cluster span{color:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rcvCc9R5gJoBV1UR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rcvCc9R5gJoBV1UR rect.text{fill:none;stroke-width:0;}#mermaid-svg-rcvCc9R5gJoBV1UR .icon-shape,#mermaid-svg-rcvCc9R5gJoBV1UR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rcvCc9R5gJoBV1UR .icon-shape p,#mermaid-svg-rcvCc9R5gJoBV1UR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rcvCc9R5gJoBV1UR .icon-shape .label rect,#mermaid-svg-rcvCc9R5gJoBV1UR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rcvCc9R5gJoBV1UR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rcvCc9R5gJoBV1UR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rcvCc9R5gJoBV1UR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否,但边界变化
是
是
否
有新帧
是
否
无新帧
否
是
是
否
是
否
是
否
CO_HBconsumer_process(HBcons,
NMTisPreOrOperational, timeDifference_us, timerNext_us)
本节点本次和上次都处于
Pre-operational 或 Operational?
状态边界变化
清 CANrxNew
NMTstate = UNKNOWN
HBstate 回到 UNKNOWN
保存 allMonitored*
保存 NMTisPreOrOperationalPrev
遍历 monitoredNodes\[\]
HBstate == UNCONFIGURED?
下一个监控项
CANrxNew?
NMTstate == INITIALIZING(0)?
识别为 boot-up
可选 remote reset callback
若此前 ACTIVE: 报告远端复位
HBstate = UNKNOWN
识别为 heartbeat
可选 hb started callback
HBstate = ACTIVE
timeoutTimer = 0
HBstate == ACTIVE?
更新 allMonitoredActive / Operational
必要时触发 NMT changed callback
timeoutTimer += delta
timeoutTimer >= time_us?
可选 timeout callback
CO_errorReport(HB consumer)
NMTstate = UNKNOWN
HBstate = TIMEOUT
未超时则更新 timerNext_us
还有监控项?
所有监控项从非全 active
变为全 active?
CO_errorReset 两类 HB 错误
CO_HBconsumer_process() 周期调用,入参有 3 个关键量:
| 参数 | 作用 |
|---|---|
NMTisPreOrOperational |
本节点当前是否处于 Pre-operational 或 Operational |
timeDifference_us |
距离上次调用过去了多少 us |
timerNext_us |
可选:告诉上层下一次最晚多久后需要再次调用 |
6.1 本节点 NMT 状态门控
源码只有在下面条件为真时,才执行正常监控逻辑:
c
if (NMTisPreOrOperational && HBcons->NMTisPreOrOperationalPrev) {
... normal processing ...
}
也就是本节点需要连续处于 Pre-operational 或 Operational。若刚进入或刚离开这些状态,源码会清理每个监控项:
text
NMTstate = CO_NMT_UNKNOWN
NMTstatePrev = CO_NMT_UNKNOWN
CANrxNew = false
非 UNCONFIGURED 项的 HBstate = UNKNOWN
这能避免本节点 NMT 状态切换边界上的旧 heartbeat 状态被误用。
6.2 有新帧:先区分 boot-up 和 heartbeat
对每个已配置节点,process() 先看 CANrxNew。
如果 NMTstate == CO_NMT_INITIALIZING,这帧被视为 boot-up message:
text
可选调用 remote reset callback
若之前 HBstate == ACTIVE:报告 CO_EM_HB_CONSUMER_REMOTE_RESET
HBstate = UNKNOWN
这里有一个关键点:boot-up 不会把节点变成 ACTIVE。 它只说明远端刚启动或通信刚复位,consumer 会回到 UNKNOWN,等待后续第一帧真正的 heartbeat。
如果 NMTstate != CO_NMT_INITIALIZING,这帧才被视为正常 heartbeat:
text
如果此前不是 ACTIVE:可选调用 heartbeat started callback
HBstate = ACTIVE
timeoutTimer = 0
timeDifference_us_copy = 0
timeDifference_us_copy = 0 的目的,是避免"刚收到新 heartbeat 的同一轮 process 又把本轮 delta 加到 timeoutTimer 上"。
6.3 无新帧或处理完新帧后:检查超时
只有 HBstate == ACTIVE 时,源码才累加 timeoutTimer:
text
timeoutTimer += timeDifference_us_copy
如果:
text
timeoutTimer >= time_us
就执行 timeout 处理:
text
可选调用 timeout callback
CO_errorReport(HBcons->em, CO_EM_HEARTBEAT_CONSUMER, CO_EMC_HEARTBEAT, i)
NMTstate = CO_NMT_UNKNOWN
HBstate = CO_HBconsumer_TIMEOUT
这说明:未收到第一帧 heartbeat 前不会超时;只有进入 ACTIVE 后,consumer 才开始计时。 CANopenNode 官方文档也明确写到,monitoring starts after the reception of the first HeartBeat, not bootup。[3](#3)
#mermaid-svg-EftIsXAn3Y2AZDwi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EftIsXAn3Y2AZDwi .error-icon{fill:#552222;}#mermaid-svg-EftIsXAn3Y2AZDwi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EftIsXAn3Y2AZDwi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EftIsXAn3Y2AZDwi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EftIsXAn3Y2AZDwi .marker.cross{stroke:#333333;}#mermaid-svg-EftIsXAn3Y2AZDwi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EftIsXAn3Y2AZDwi p{margin:0;}#mermaid-svg-EftIsXAn3Y2AZDwi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster-label text{fill:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster-label span{color:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster-label span p{background-color:transparent;}#mermaid-svg-EftIsXAn3Y2AZDwi .label text,#mermaid-svg-EftIsXAn3Y2AZDwi span{fill:#333;color:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi .node rect,#mermaid-svg-EftIsXAn3Y2AZDwi .node circle,#mermaid-svg-EftIsXAn3Y2AZDwi .node ellipse,#mermaid-svg-EftIsXAn3Y2AZDwi .node polygon,#mermaid-svg-EftIsXAn3Y2AZDwi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EftIsXAn3Y2AZDwi .rough-node .label text,#mermaid-svg-EftIsXAn3Y2AZDwi .node .label text,#mermaid-svg-EftIsXAn3Y2AZDwi .image-shape .label,#mermaid-svg-EftIsXAn3Y2AZDwi .icon-shape .label{text-anchor:middle;}#mermaid-svg-EftIsXAn3Y2AZDwi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EftIsXAn3Y2AZDwi .rough-node .label,#mermaid-svg-EftIsXAn3Y2AZDwi .node .label,#mermaid-svg-EftIsXAn3Y2AZDwi .image-shape .label,#mermaid-svg-EftIsXAn3Y2AZDwi .icon-shape .label{text-align:center;}#mermaid-svg-EftIsXAn3Y2AZDwi .node.clickable{cursor:pointer;}#mermaid-svg-EftIsXAn3Y2AZDwi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EftIsXAn3Y2AZDwi .arrowheadPath{fill:#333333;}#mermaid-svg-EftIsXAn3Y2AZDwi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EftIsXAn3Y2AZDwi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EftIsXAn3Y2AZDwi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EftIsXAn3Y2AZDwi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EftIsXAn3Y2AZDwi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EftIsXAn3Y2AZDwi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster text{fill:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi .cluster span{color:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EftIsXAn3Y2AZDwi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EftIsXAn3Y2AZDwi rect.text{fill:none;stroke-width:0;}#mermaid-svg-EftIsXAn3Y2AZDwi .icon-shape,#mermaid-svg-EftIsXAn3Y2AZDwi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EftIsXAn3Y2AZDwi .icon-shape p,#mermaid-svg-EftIsXAn3Y2AZDwi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EftIsXAn3Y2AZDwi .icon-shape .label rect,#mermaid-svg-EftIsXAn3Y2AZDwi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EftIsXAn3Y2AZDwi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EftIsXAn3Y2AZDwi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EftIsXAn3Y2AZDwi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} t0
收到 heartbeat
HBstate = ACTIVE
timeoutTimer = 0
t1
process 周期调用
timeoutTimer += dt
t2
仍未收到下一帧
无 CANrxNew
timeoutTimer 继续累加
t3 >= time_us
超过 0x1016 时间窗
判定 TIMEOUT
CO_errorReport
之后重新收到 heartbeat
远端恢复发送
HBstate 回 ACTIVE
timeoutTimer = 0
6.4 聚合状态和 NMT changed callback
每轮遍历时,源码会同步计算:
text
allMonitoredActiveCurrent
allMonitoredOperationalCurrent
判断规则很直接:
text
只要有一个已配置节点不是 ACTIVE,allMonitoredActiveCurrent = false
只要有一个已配置节点 NMTstate 不是 OPERATIONAL,allMonitoredOperationalCurrent = false
如果启用了 CO_CONFIG_HB_CONS_CALLBACK_CHANGE 或 CO_CONFIG_HB_CONS_CALLBACK_MULTI,源码还会比较:
text
monitoredNode->NMTstate != monitoredNode->NMTstatePrev
变化时触发 NMT changed callback,然后更新 NMTstatePrev。
6.5 所有节点恢复 active 后清错误
源码最后有一段容易忽略:
c
if (!HBcons->allMonitoredActive && allMonitoredActiveCurrent) {
CO_errorReset(HBcons->em, CO_EM_HEARTBEAT_CONSUMER, 0);
CO_errorReset(HBcons->em, CO_EM_HB_CONSUMER_REMOTE_RESET, 0);
}
含义是:当系统从"不是所有被监控节点都 active"变为"所有被监控节点都 active"时,清除 heartbeat consumer timeout 和 remote reset 相关错误。
源码注释也提醒:Heartbeat consumer 对所有被监控节点共用一个 emergency index,所以 reset 时用 0 清该类错误;而 report 时用循环下标 i 作为 info code,便于定位是哪一个 0x1016 子项对应的节点。
7. 回调配置:公共回调与每节点回调不能同时启用
CO_config.h 对 Heartbeat Consumer 的配置宏做了分层:
| 宏/flag | 作用 |
|---|---|
CO_CONFIG_HB_CONS_ENABLE |
编译并启用 Heartbeat Consumer |
CO_CONFIG_HB_CONS_CALLBACK_CHANGE |
启用"公共 NMT state changed 回调" |
CO_CONFIG_HB_CONS_CALLBACK_MULTI |
启用每个被监控节点独立的多类回调 |
CO_CONFIG_HB_CONS_QUERY_FUNCT |
启用查询函数:按 Node-ID 或 idx 查状态 |
CO_CONFIG_FLAG_CALLBACK_PRE |
收到 heartbeat 后先调用轻量 pre callback |
CO_CONFIG_FLAG_TIMERNEXT |
在 process() 中计算 timerNext_us |
CO_CONFIG_FLAG_OD_DYNAMIC |
写 0x1016 时动态重配置监控项 |
CO_HBconsumer.c 明确禁止同时启用:
c
CO_CONFIG_HB_CONS_CALLBACK_CHANGE
CO_CONFIG_HB_CONS_CALLBACK_MULTI
原因是这两种回调模型一个是公共回调,一个是每节点回调,语义上互斥。
CALLBACK_MULTI 打开后可以分别注册:
| 回调 | 触发时机 |
|---|---|
CO_HBconsumer_initCallbackNmtChanged() |
远端 NMT state 变化 |
CO_HBconsumer_initCallbackHeartbeatStarted() |
consumer 从非 ACTIVE 收到有效 heartbeat,转入 ACTIVE |
CO_HBconsumer_initCallbackTimeout() |
ACTIVE 后超时 |
CO_HBconsumer_initCallbackRemoteReset() |
收到 boot-up,识别到远端复位/通信复位 |
8. 查询接口:只在启用 CO_CONFIG_HB_CONS_QUERY_FUNCT 时存在
如果打开 CO_CONFIG_HB_CONS_QUERY_FUNCT,源码会编译 3 个查询函数:
| 函数 | 用途 |
|---|---|
CO_HBconsumer_getIdxByNodeId() |
用 producer Node-ID 查 monitoredNodes[] 下标 |
CO_HBconsumer_getState() |
用 idx 查询当前 CO_HBconsumer_state_t |
CO_HBconsumer_getNmtState() |
用 idx 查询远端 NMT state |
其中 CO_HBconsumer_getNmtState() 有一个约束:只有该节点 HBstate == CO_HBconsumer_ACTIVE 时,返回的 NMT state 才有效;否则返回 -1。
这与协议语义一致:如果没收到有效 heartbeat,或者已经 timeout,远端 NMT 状态就是未知的。
9. STM32/RTOS 工程里怎么接入这段逻辑
在 CANopenNode 工程里,Heartbeat Consumer 通常不是普通从机最小配置必须项。普通 STM32 从机通常先需要:
text
NMT slave + Heartbeat producer + SDO server + EMCY producer + PDO + OD
只有当本节点还需要监控主站或其他从站时,才需要打开 Heartbeat Consumer。
最小接入路径如下:
text
1. CO_config.h 中打开 CO_CONFIG_HB_CONS_ENABLE
2. OD 中存在 0x1016 Consumer heartbeat time
3. 为 monitoredNodes[] 提供足够容量
4. CO_new()/初始化阶段分配 HBcons 与 monitoredNodes
5. communication reset 阶段调用 CO_HBconsumer_init()
6. 主循环或 RTOS 任务周期调用 CO_HBconsumer_process()
7. 需要事件通知时注册 callback
8. 应用层读取 allMonitoredActive / allMonitoredOperational 或查询接口
在 RTOS 中,如果启用了 CO_CONFIG_FLAG_CALLBACK_PRE,可以让 CO_HBcons_receive() 在收到 CAN 帧后唤醒任务;但仍建议把真正处理放在任务上下文,不要在 CAN 接收中断中直接做复杂业务。
10. 调试时重点看这几类现象
| 现象 | 优先检查 |
|---|---|
一直是 UNCONFIGURED |
0x1016 子项是否为 0;Node-ID 或 time 是否为 0 |
一直是 UNKNOWN |
是否只收到 boot-up;producer 的 0x1017 是否启用;CAN-ID 是否为 0x700 + Node-ID |
一进入 ACTIVE 很快 timeout |
0x1016 expected time 是否太短;主循环周期是否太长;producer 周期是否比 consumer expected time 短 |
| 收到帧但没有处理 | CO_HBconsumer_process() 是否周期调用;本节点是否连续处于 Pre-operational/Operational |
写 0x1016 后没生效 |
是否启用 CO_CONFIG_FLAG_OD_DYNAMIC;写入值是否触发 OD_write_1016() |
allMonitoredOperational 一直 false |
被监控节点可能只是 Pre-operational;它必须上报 CO_NMT_OPERATIONAL 才会为 true |
| 远端复位后没有 timeout | 收到 boot-up 后源码把状态置为 UNKNOWN,监控等待下一帧 heartbeat;这不是普通 timeout 路径 |
11. 一句话总结
CO_HBconsumer.c/h 的主线不是"收到 heartbeat 就保存一下"这么简单,而是:用 0x1016 建立本节点对远端 producer 的监控表,用 CAN RX 回调捕获 0x700 + Node-ID 的 1 字节 NMT state,再由 CO_HBconsumer_process() 周期性完成 boot-up 识别、ACTIVE/TIMEOUT 状态转换、NMT 状态变化回调、聚合在线状态,以及 heartbeat 相关 Emergency 上报与恢复清除。
参考资料
本地材料:CO_HBconsumer.h、CO_HBconsumer.c、CO_NMT_Heartbeat.h、CO_config.h、CiA301 V4.2.0(中文注释版).pdf。
-
CAN in Automation,Network management:https://www.can-cia.org/can-knowledge/network-management ↩︎
-
Kollmorgen AKD2G CANopen Heartbeat 文档:https://webhelp.kollmorgen.com/akd2g/english/content/AKD2G CANopen/CANopen_06_04_08 Heartbeat.htm ↩︎ ↩︎
-
CANopenNode 官方 Doxygen,Heartbeat consumer:https://canopennode.github.io/CANopenNode/group__CO__HBconsumer.html ↩︎