CANopene Heartbeat运行流程

CANopene Heartbeat运行流程

文章目录

  • [CANopene Heartbeat运行流程](#CANopene Heartbeat运行流程)
    • 先给结论
    • [1. 协议先行:Heartbeat Consumer 解决什么问题](#1. 协议先行:Heartbeat Consumer 解决什么问题)
    • [2. `0x1016` 与 `0x1017`:不要把"我监控别人"和"我发心跳"混在一起](#2. 0x10160x1017:不要把“我监控别人”和“我发心跳”混在一起)
    • [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 time0x1016 的一个子项是 32 位值,高 16 位给出 producer Node-ID,低 16 位给出 expected time,单位为 ms。consumer 必须在该时间窗内收到被监控 producer 的 heartbeat,否则产生 heartbeat event。[2](#2)


2. 0x10160x1017:不要把"我监控别人"和"我发心跳"混在一起

#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 中有两层结构:

  1. CO_HBconsumer_t:整个 Heartbeat Consumer 对象。
  2. 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 == 0consumerTime == 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() 配置一个监控项

该函数做了几个关键判断:

  1. 参数为空或 idx 越界,返回非法参数。
  2. 如果 consumerTime_ms != 0nodeId != 0,检查是否有重复 nodeId。
  3. nodeId != 0time_us != 0
    • COB_ID = 0x700 + nodeId
    • HBstate = CO_HBconsumer_UNKNOWN
  4. 否则:
    • COB_ID = 0
    • time_us = 0
    • HBstate = CO_HBconsumer_UNCONFIGURED
  5. 最后调用 CO_CANrxBufferInit(),把 CAN 接收过滤器和 CO_HBcons_receive() 回调绑定起来。

4.5 动态写 0x1016

如果启用了 CO_CONFIG_FLAG_OD_DYNAMICCO_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 ...
}

这段代码有两个设计点:

  1. 不在 CAN 回调里判断超时、不上报 EMCY、不做复杂状态机。
  2. 只把最新的 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_CHANGECO_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.hCO_HBconsumer.cCO_NMT_Heartbeat.hCO_config.hCiA301 V4.2.0(中文注释版).pdf


  1. CAN in Automation,Network management:https://www.can-cia.org/can-knowledge/network-management ↩︎

  2. Kollmorgen AKD2G CANopen Heartbeat 文档:https://webhelp.kollmorgen.com/akd2g/english/content/AKD2G CANopen/CANopen_06_04_08 Heartbeat.htm ↩︎ ↩︎

  3. CANopenNode 官方 Doxygen,Heartbeat consumer:https://canopennode.github.io/CANopenNode/group__CO__HBconsumer.html ↩︎