CANopen Emergency: 搞懂 EM 运行流程

在这里插入图片描述

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

先给结论

Emergency(下文简称 EMCYEM)不是普通日志,也不是 PDO。它是 CANopen 用于故障事件上报的标准机制。CAN in Automation 对 EMCY 的公开说明包括:由设备内部错误触发、映射到单个 CAN Classic 帧、内容包含 0x1001 error register、16 位 emergency error code 和最多 5 字节制造商信息,默认 CAN-ID 为 0x80 + node-ID,且同一 error event 只发送一次。[^cia-emcy]

在 CANopenNode 的 301/CO_Emergency.c/h 中,运行链路可以压缩成:

1
2
3
4
5
6
7
应用/协议栈检测到错误变化
-> CO_errorReport() / CO_errorReset()
-> CO_error() 修改 errorStatusBits[] 并写入 FIFO
-> CO_EM_process() 周期运行
-> 计算 OD 0x1001 Error register
-> 补齐 EMCY byte2 并按 NMT/TX/inhibit 条件发送 CAN 帧
-> OD 0x1003 可读取最近错误历史

关键点:CO_errorReport() 不直接发 CAN 帧。 它只把错误变化写入内部状态和 FIFO;真正发帧发生在 CO_EM_process()


1. 协议层:EMCY 解决什么问题

CANopen 中,PDO 负责过程数据,SDO 负责对象字典访问,NMT 负责节点状态控制,Heartbeat 负责在线监控。EMCY 的职责不同:它把设备内部错误、通信错误或应用错误,以事件方式通知网络中的其他节点。

CiA 公开说明中还强调:EMCY producer 发送后,零个或多个 EMCY consumer 可以接收这些消息,并执行应用相关的反应。也就是说,EMCY 本身只定义错误信息的上报通道;收到后要停机、报警、降级还是忽略,是设备或系统策略决定的。[^cia-emcy]

这解释了 CANopenNode 源码里的几个设计:

协议要求 CANopenNode 源码对应
错误事件触发,不周期重复发送 CO_error() 检查状态位是否变化;重复 report/reset 直接返回
EMCY 是 8 字节 CAN 帧 producer 初始化 TX buffer 时使用 DLC 8U
默认 CAN-ID 为 0x80 + nodeId CO_CAN_ID_EMERGENCY + nodeId,由 0x1014 参与配置
帧中包含错误寄存器 CO_EM_process() 计算 errorRegister 后写入 byte2
可保存错误历史 CO_CONFIG_EM_HISTORY 使能 0x1003 读写扩展
可限制过密发送 CO_CONFIG_EM_PROD_INHIBIT 使能 0x1015 抑制时间

2. 帧格式和相关 OD 对象

在这里插入图片描述

CANopenNode 官方 Doxygen 给出的 Emergency producer 消息内容为:bytes 0..1 是 error code,byte 2 是 error register,byte 3 是 error condition index,bytes 4..7CO_errorReport() 的附加信息。[^canopennode-em]

字节 内容 CANopenNode 来源
0..1 Emergency error code CO_errorReport()errorCode;reset 时变成 CO_EMC_NO_ERROR
2 Error register CO_EM_process() 根据 errorStatusBits[] 计算
3 Error condition index CO_EM_errorStatusBits_t 中的 errorBit
4..7 Additional information CO_errorReport()infoCode

2.1 0x1001 Error register

0x1001 是 CANopen 的错误寄存器。CANopenNode 头文件把它描述为必需对象,CO_EM_init() 会通过 OD_getPtr() 取得 0x1001,00 的真实存储地址;之后 CO_EM_process() 直接写这个地址。

它不是每个错误的明细列表,而是错误类别汇总:generic、current、voltage、temperature、communication、device profile、manufacturer 等。CANopenNode 先把具体错误记录在 errorStatusBits[],再通过 CO_CONFIG_ERR_CONDITION_xxx 宏汇总到 0x1001

2.2 0x1003 Pre-defined error field

如果启用 CO_CONFIG_EM_HISTORY,最新错误可以从对象字典 0x1003 读取;CANopenNode 官方说明指出 0x1003 内容对应 EMCY message 的 bytes 0..3。[^canopennode-em]

这意味着:

1
2
0x1003 保存:errorCode + errorRegister + errorBit
0x1003 不保存:infoCode,也就是 EMCY bytes 4..7

源码上,0x1003 与 producer 发送共用 CO_EM_fifo_tfifo.msg 保存 bytes 0..3fifo.info 保存 producer 发帧用的 bytes 4..7

2.3 0x1014 COB-ID EMCY

0x1014 决定 EMCY producer 使用的 COB-ID。若 CO_CONFIG_EM_PROD_CONFIGURABLE 未启用,CANopenNode 使用默认 CO_CAN_ID_EMERGENCY + nodeId;若启用,则 OD_write_1014() 会校验写入值,尤其避免 producer 已启用时随意切换正在使用的 CAN-ID。

2.4 0x1015 Inhibit time EMCY

0x1015 用于限制两帧 EMCY 的最小间隔。源码中 OD_write_1015() 把 OD 中的 UNSIGNED16 数值按 100 us 单位换算为微秒:

1
2
em->inhibitEmTime_us = (uint32_t)CO_getUint16(buf) * 100U;
em->inhibitEmTimer = 0;

因此,0x1015 = 10 表示 1000 us,即 1 ms


3. 源码文件分工

建议先读 CO_Emergency.h,再读 CO_Emergency.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CO_Emergency.h
-> 默认配置宏
-> CO_errorRegister_t
-> CO_EM_errorCode_t
-> CO_EM_errorStatusBits_t
-> CO_EM_fifo_t
-> CO_EM_t
-> CO_EM_init / CO_error / CO_EM_process 声明

CO_Emergency.c
-> OD 读写扩展函数
-> CO_EM_init()
-> CO_EM_receive() / callback
-> CO_EM_process()
-> CO_error()

CO_EM_t 是运行态中心结构体,可以按功能拆成:

字段 作用
errorStatusBits[] 当前内部错误位图,一个错误条件对应一个 bit
errorRegister 指向 OD 0x1001,00 的指针
CANerrorStatusOld 保存上次 CAN driver 错误状态,用于检测变化
fifo/fifoWrPtr/fifoPpPtr/fifoCount/fifoOverflow 环形 FIFO,用于 producer 发送和 0x1003 历史
producerEnabled/nodeId/CANtxBuff/inhibitEmTimer producer 发送状态
OD_1014_extension/OD_1015_extension/OD_1003_extension/OD_statusBits_extension OD 动态访问扩展
pFunctSignalRx/pFunctSignalPre consumer 回调和 pre callback

4. CO_EM_init():初始化时把 OD、FIFO 和 CAN buffer 接起来

在这里插入图片描述

你贴的初始化调用本质上是在把 CiA 301 EMCY 需要的对象连接到 CANopenNode 运行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
err = CO_EM_init(co->em, co->CANmodule, OD_GET(H1001, OD_H1001_ERR_REG),
#if ((CO_CONFIG_EM) & (CO_CONFIG_EM_PRODUCER | CO_CONFIG_EM_HISTORY)) != 0
co->em_fifo, (CO_GET_CNT(ARR_1003) + 1U),
#endif
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_PRODUCER) != 0
OD_GET(H1014, OD_H1014_COBID_EMERGENCY), CO_GET_CO(TX_IDX_EM_PROD),
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_PROD_INHIBIT) != 0
OD_GET(H1015, OD_H1015_INHIBIT_TIME_EMCY),
#endif
#endif
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_HISTORY) != 0
OD_GET(H1003, OD_H1003_PREDEF_ERR_FIELD),
#endif
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_STATUS_BITS) != 0
OD_statusBits,
#endif
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_CONSUMER) != 0
co->CANmodule, CO_GET_CO(RX_IDX_EM_CONS),
#endif
nodeId, errInfo);

逐项看:

初始化参数 含义 影响
co->em Emergency 对象实例 保存运行态:状态位、FIFO、OD 扩展、TX/RX 回调
co->CANmodule CAN 模块 用于 TX,也用于读取 CANerrorStatus
OD_GET(H1001, ...) 0x1001 Error register CO_EM_process() 计算后写入
co->em_fifo EM FIFO producer 发送队列和 0x1003 历史共用
CO_GET_CNT(ARR_1003) + 1U FIFO 数组大小 实际可保存历史条数是 ARR_1003
OD_GET(H1014, ...) 0x1014 COB-ID EMCY 决定 producer CAN-ID 和 enabled 状态
CO_GET_CO(TX_IDX_EM_PROD) TX buffer index EMCY producer 使用的 CAN TX buffer
OD_GET(H1015, ...) 0x1015 Inhibit time 控制 EMCY 最小发送间隔
OD_GET(H1003, ...) 0x1003 Pre-defined error field 挂接错误历史读写扩展
OD_statusBits 自定义内部状态位 OD 入口 仅在 CO_CONFIG_EM_STATUS_BITS 打开时出现
CO_GET_CO(RX_IDX_EM_CONS) consumer RX buffer index 接收其他节点默认范围 EMCY
nodeId 当前节点号 默认 EMCY CAN-ID = 0x80 + nodeId

4.1 为什么 em_fifoARR_1003 + 1

环形 FIFO 需要保留一个空槽来区分“空”和“满”:

1
2
fifoWrPtr == fifoPpPtr       -> 空
fifoWrPtrNext == fifoPpPtr -> 满

所以:

1
2
fifoSize = 0x1003 历史容量 + 1
实际容量 = fifoSize - 1

CO_Emergency.c 文件顶部也用 fifoSize = 7、实际容量 6 的图示说明了这个关系。


5. CO_error():错误变化如何进入内部状态和 FIFO

在这里插入图片描述

CO_errorReport()CO_errorReset() 都是宏,最终进入 CO_error()

1
2
#define CO_errorReport(em, errorBit, errorCode, infoCode) CO_error(em, true, errorBit, errorCode, infoCode)
#define CO_errorReset(em, errorBit, infoCode) CO_error(em, false, errorBit, CO_EMC_NO_ERROR, infoCode)

5.1 errorBit 转成数组下标和位掩码

1
2
uint8_t index = errorBit >> 3;
uint8_t bitmask = 1U << (errorBit & 0x7U);

例如 errorBit = 0x12

1
2
3
index   = 0x12 >> 3 = 2
bitmask = 1 << 2 = 0x04
目标位 = errorStatusBits[2] 的 bit2

5.2 重复调用不会重复入队

源码先判断状态是否变化:

1
2
3
4
5
6
7
8
9
10
if (setError) {
if (errorStatusBitMasked != 0U) {
return;
}
} else {
if (errorStatusBitMasked == 0U) {
return;
}
errorCode = CO_EMC_NO_ERROR;
}

效果如下:

调用 当前 bit 结果
report 1 直接返回
report 0 置位并写 FIFO
reset 0 直接返回
reset 1 清位并写 FIFO,error code 改成 0x0000

这与 EMCY “同一 error event 只发送一次”的协议原则一致。

5.3 CO_error() 先留空 error register

CO_error() 准备的 errMsg 是:

1
uint32_t errMsg = ((uint32_t)errorBit << 24) | CO_SWAP_16(errorCode);

此时只放入:

1
2
3
byte0..1 = errorCode
byte2 = 暂空
byte3 = errorBit

byte2 不在这里填,是因为 error register 需要综合全部 errorStatusBits[]CO_CONFIG_ERR_CONDITION_xxx 宏计算。这个工作放在周期性的 CO_EM_process() 更合适。


6. CO_EM_process():计算 0x1001 并发送 EMCY

在这里插入图片描述

CANopenNode 官方 Doxygen 说明 CO_EM_process() 必须周期调用,它会检查部分通信错误、计算 OD 0x1001,并在必要时发送 EMCY。[^canopennode-em]

源码顺序是:

  1. 检查 CAN driver 的错误状态变化。
  2. 根据 errorStatusBits[] 计算 errorRegister,写入 *em->errorRegister
  3. 若当前上下文不允许发送,则返回。
  4. 若 producer、FIFO、TX buffer、inhibit time 条件满足,则发送一帧 EMCY。

6.1 CAN driver 错误也会变成 EM 输入

CO_EM_process() 读取:

1
uint16_t CANerrSt = em->CANdevTx->CANerrorStatus;

如果与 CANerrorStatusOld 不同,就把变化转换成 CO_error() 调用。例如:

CAN driver 状态 CANopenNode errorBit errorCode
TX/RX warning CO_EM_CAN_BUS_WARNING CO_EMC_NO_ERROR
TX passive CO_EM_CAN_TX_BUS_PASSIVE CO_EMC_CAN_PASSIVE
TX bus off CO_EM_CAN_TX_BUS_OFF CO_EMC_BUS_OFF_RECOVERED
TX overflow CO_EM_CAN_TX_OVERFLOW CO_EMC_CAN_OVERRUN
RX overflow CO_EM_CAN_RXB_OVERFLOW CO_EMC_CAN_OVERRUN

6.2 0x1001 是由条件宏汇总的

默认配置中,CANopenNode 预定义了三类条件:

1
2
3
#define CO_CONFIG_ERR_CONDITION_GENERIC       (em->errorStatusBits[5] != 0U)
#define CO_CONFIG_ERR_CONDITION_COMMUNICATION ((em->errorStatusBits[2] != 0U) || (em->errorStatusBits[3] != 0U))
#define CO_CONFIG_ERR_CONDITION_MANUFACTURER ((em->errorStatusBits[8] != 0U) || (em->errorStatusBits[9] != 0U))

这就是 errorStatusBits[]0x1001 的关系:前者是细粒度内部状态,后者是 CANopen 标准对象中对错误类别的汇总。

6.3 发送前补齐 byte2

producer 分支中,满足条件后源码补齐 byte2:

1
em->fifo[fifoPpPtr].msg |= (uint32_t)errorRegister << 16;

然后复制 8 字节并发送:

1
2
(void)memcpy((void*)em->CANtxBuff->data, (void*)&em->fifo[fifoPpPtr].msg, sizeof(em->CANtxBuff->data));
(void)CO_CANsend(em->CANdevTx, em->CANtxBuff);

CO_EM_fifo_t 的字段顺序是 msg 后接 info,所以这次 8 字节复制会把 msginfo 一起送入 CAN TX buffer。


7. FIFO 与 0x1003:事件队列和历史读取共用一套存储

在这里插入图片描述

errorStatusBits[] 保存当前状态;FIFO 保存状态变化事件。例如:

1
2
3
4
过压出现       -> report -> FIFO 加一条错误事件
过压仍然存在 -> report -> 状态未变,不加
过压恢复 -> reset -> FIFO 加一条恢复事件
过压已经恢复 -> reset -> 状态未变,不加

OD_read_1003() 中,subindex 0 返回 fifoCount,subindex 1 返回最新错误。它从 fifoWrPtr 往回数,因此 0x1003:01 是最新一条,0x1003:02 是次新一条。

0x1003:00 = 0 会清空错误历史计数:

1
2
3
4
if (CO_getUint8(buf) != 0U) {
return ODR_INVALID_VALUE;
}
em->fifoCount = 0;

它清的是历史读取计数,不是直接清 errorStatusBits[] 当前状态。


8. CO_CONFIG_EM:配置宏影响哪些源码路径

在这里插入图片描述

CANopenNode 官方配置文档列出 CO_CONFIG_EM 的可选 flag:producer、producer COB-ID configurable、producer inhibit、history、consumer、status bits、callback pre、timer next。[^canopennode-config]

配置 作用 主要源码路径
CO_CONFIG_EM_PRODUCER 启用本节点 EMCY 发送 CO_EM_init() 初始化 0x1014 和 TX buffer;CO_EM_process() 发送
CO_CONFIG_EM_PROD_CONFIGURABLE 允许运行期配置 producer COB-ID OD_read_1014() / OD_write_1014()
CO_CONFIG_EM_PROD_INHIBIT 启用发送抑制时间 OD_write_1015()inhibitEmTimer
CO_CONFIG_EM_HISTORY 启用错误历史 OD_read_1003() / OD_write_1003()
CO_CONFIG_EM_CONSUMER 接收其他节点 EMCY CO_EM_receive()CO_EM_initCallbackRx()
CO_CONFIG_EM_STATUS_BITS 通过 OD 访问内部 errorStatusBits[] OD_read_statusBits() / OD_write_statusBits()
CO_CONFIG_FLAG_CALLBACK_PRE 错误变化后触发回调 CO_EM_initCallbackPre()CO_error() 末尾回调
CO_CONFIG_FLAG_TIMERNEXT 计算下次处理时间 inhibit 分支更新 timerNext_us

9. CO_CONFIG_EM_STATUS_BITS:补充说明

在这里插入图片描述

CO_CONFIG_EM_STATUS_BITS 容易被忽略,因为它不影响 EMCY 帧格式,也不是 CiA 301 标准的固定对象号。CANopenNode 官方配置文档对它的定义是:从 OD 访问 Error status bits。[^canopennode-config]

它的作用可以概括为:CO_EM_t.errorStatusBits[] 暴露给一个自定义 OD 项,供 SDO/调试工具/厂商扩展读取或写入内部错误位图。

9.1 初始化时需要 OD_statusBits

头文件的 CO_EM_init() 参数说明写明:OD_statusBits 是用于访问 CO_EM_terrorStatusBits 的自定义 OD entry;这个 entry 需要在 subindex 0 上提供 (CO_CONFIG_EM_ERR_STATUS_BITS_COUNT / 8) 字节变量,并要求 IO extension。源码在 CO_CONFIG_EM_STATUS_BITS 打开时才会展开这个参数。

对应初始化分支:

1
2
3
4
5
6
#if ((CO_CONFIG_EM)&CO_CONFIG_EM_STATUS_BITS) != 0
em->OD_statusBits_extension.object = em;
em->OD_statusBits_extension.read = OD_read_statusBits;
em->OD_statusBits_extension.write = OD_write_statusBits;
(void)OD_extension_init(OD_statusBits, &em->OD_statusBits_extension);
#endif

9.2 读路径:读取内部错误位图

OD_read_statusBits() 的核心行为是把内部状态位复制出去:

1
2
3
OD_size_t countReadLocal = CO_CONFIG_EM_ERR_STATUS_BITS_COUNT / 8U;
...
(void)memcpy((void*)(buf), (const void*)(&em->errorStatusBits[0]), countReadLocal);

因此,如果你通过 SDO 读取这个自定义 OD 项,读到的是当前 errorStatusBits[] 的原始位图。它比 0x1001 更细,但也更偏 CANopenNode 内部实现。

9.3 写路径:可直接改内部错误位图

OD_write_statusBits() 的核心行为是反向复制:

1
2
3
OD_size_t countWrite = CO_CONFIG_EM_ERR_STATUS_BITS_COUNT / 8U;
...
(void)memcpy((void*)(&em->errorStatusBits[0]), (const void*)(buf), countWrite);

这说明它不是只读诊断入口,而是可写入口。写入后,下一次 CO_EM_process() 会基于新的 errorStatusBits[] 重新计算 0x1001。但要注意:直接写 errorStatusBits[] 不会像 CO_errorReport() 那样自动生成 FIFO 事件,也不会自动形成一帧 EMCY。需要事件上报时,应用仍应调用 CO_errorReport() / CO_errorReset()

9.4 它和 0x10010x1003 的区别

对象/入口 内容 是否标准固定对象 是否触发 EMCY
0x1001 错误类别汇总 否,只是当前状态输出
0x1003 最近错误历史 bytes 0..3 否,只是历史读取
OD_statusBits 内部 errorStatusBits[] 原始位图 否,由工程自定义 否,直接读写不产生事件
CO_errorReport() 错误发生事件输入 API 是,入 FIFO 后由 CO_EM_process() 发送

9.5 什么时候需要打开

建议按用途决定:

场景 建议
只学习 EMCY producer/history 主流程 可不打开,避免混淆
需要通过 SDO 观察内部每个 CO_EM_xxx bit 打开,并在 OD 中放一个自定义 byte array
需要调试 CO_CONFIG_ERR_CONDITION_xxx 为什么使 0x1001 置位 打开有帮助
希望主站通过 OD 强行清/置内部错误位 可以打开,但要明确这不会自动生成 EMCY 事件

工程上更推荐:应用错误的正常生命周期仍用 CO_errorReport() / CO_errorReset()OD_statusBits 主要作为调试和诊断入口。


10. 从机工程中的最小使用路径

应用层上报自定义错误时,优先使用 manufacturer 范围的 errorBit

1
2
3
4
5
6
7
8
9
10
11
#define APP_EM_MOTOR_OVERLOAD  (CO_EM_MANUFACTURER_START + 0U)

void app_report_motor_overload(CO_t *co, uint32_t detail)
{
CO_errorReport(co->em, APP_EM_MOTOR_OVERLOAD, CO_EMC_DEVICE_SPECIFIC, detail);
}

void app_reset_motor_overload(CO_t *co, uint32_t detail)
{
CO_errorReset(co->em, APP_EM_MOTOR_OVERLOAD, detail);
}

调试时按这个顺序检查:

1
2
3
4
5
6
7
8
9
1. CAN 总线上是否出现 0x80 + Node-ID 的 8 字节帧
2. SDO 读 0x1001,看错误类别位是否变化
3. SDO 读 0x1003:00,看历史条数
4. SDO 读 0x1003:01,看最新错误 bytes 0..3
5. 确认 CO_EM_process() 被周期调用
6. 确认当前上下文允许发送 EMCY
7. 检查 0x1014 bit31 是否禁用了 producer
8. 检查 0x1015 是否导致发送被抑制
9. 若打开 STATUS_BITS,读自定义 OD_statusBits 看内部 bit 是否符合预期

最小移植检查表:

检查项 预期
CO_GET_CNT(EM) == 1 工程里确实有 Emergency 对象
0x1001 存在,UNSIGNED8,可被 EM 模块绑定
0x1003 启用 history 时存在,容量和 ARR_1003 一致
0x1014 启用 producer 时存在,默认值能得到 0x80 + nodeId
0x1015 启用 inhibit 时存在,单位是 100 us
OD_statusBits 仅启用 CO_CONFIG_EM_STATUS_BITS 时需要自定义 OD entry
CO_EM_process() 在 CANopen 主循环中周期调用
TX buffer index TX_IDX_EM_PROD 不与其他对象冲突

11. 参考资料

[^cia-emcy]: CAN in Automation (CiA), “Special function protocols”,Emergency protocol 说明 EMCY 用于通知设备内部错误、映射到单个 CAN CC 帧、默认 CAN-ID 为 80h + node-ID,且每个 error event 只发送一次。https://www.can-cia.org/can-knowledge/special-function-protocols

[^canopennode-em]: CANopenNode 官方 Doxygen,“Emergency”,说明 CO_EM_init()CO_EM_process()CO_error() 以及 Emergency producer message bytes 布局和 0x1003 历史。https://canopennode.github.io/CANopenNode/group__CO__Emergency.html

[^canopennode-config]: CANopenNode 官方 Doxygen,“Emergency producer/consumer”,说明 CO_CONFIG_EM 的 producer、configurable、inhibit、history、consumer、status bits、callback pre、timer next 等配置,以及 CO_CONFIG_EM_ERR_STATUS_BITS_COUNT 默认值和范围。https://canopennode.github.io/CANopenNode/group__CO__STACK__CONFIG__EMERGENCY.html

[^canopennode-repo]: CANopenNode GitHub README 说明 CANopenNode 可以运行在不同设备或微控制器上,具体设备接口通常在独立工程中维护。https://github.com/CANopenNode/CANopenNode