CANopen Emergency: 搞懂 EM 运行流程
文章目录
- CANopen Emergency: 搞懂 EM 运行流程
- 先给结论
- 1. 协议层:EMCY 解决什么问题
- 2. 帧格式和相关 OD 对象
- 2.1 `0x1001 Error register`
- 2.2 `0x1003 Pre-defined error field`
- 2.3 `0x1014 COB-ID EMCY`
- 2.4 `0x1015 Inhibit time EMCY`
- 3. 源码文件分工
- 4. `CO_EM_init()`:初始化时把 OD、FIFO 和 CAN buffer 接起来
- 4.1 为什么 `em_fifo` 是 `ARR_1003 + 1`
- 5. `CO_error()`:错误变化如何进入内部状态和 FIFO
- 5.1 `errorBit` 转成数组下标和位掩码
- 5.2 重复调用不会重复入队
- 5.3 `CO_error()` 先留空 error register
- 6. `CO_EM_process()`:计算 `0x1001` 并发送 EMCY
- 6.1 CAN driver 错误也会变成 EM 输入
- 6.2 `0x1001` 是由条件宏汇总的
- 6.3 发送前补齐 byte2
- 7. FIFO 与 `0x1003`:事件队列和历史读取共用一套存储
- 8. `CO_CONFIG_EM`:配置宏影响哪些源码路径
- 9. `CO_CONFIG_EM_STATUS_BITS`:补充说明
- 9.1 初始化时需要 `OD_statusBits`
- 9.2 读路径:读取内部错误位图
- 9.3 写路径:可直接改内部错误位图
- 9.4 它和 `0x1001`、`0x1003` 的区别
- 9.5 什么时候需要打开
- 10. 从机工程中的最小使用路径
- 11. 参考资料
先给结论
Emergency(下文简称EMCY或EM)不是普通日志,也不是 PDO。它是 CANopen 用于故障事件上报的标准机制。CAN in Automation 对 EMCY 的公开说明包括:由设备内部错误触发、映射到单个 CAN Classic 帧、内容包含0x1001error register、16 位 emergency error code 和最多 5 字节制造商信息,默认 CAN-ID 为0x80 + node-ID,且同一 error event 只发送一次。1
在 CANopenNode 的301/CO_Emergency.c/h中,运行链路可以压缩成:
应用/协议栈检测到错误变化 -> 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 本身只定义错误信息的上报通道;收到后要停机、报警、降级还是忽略,是设备或系统策略决定的。1
这解释了 CANopenNode 源码里的几个设计:
| 协议要求 | CANopenNode 源码对应 |
|---|---|
| 错误事件触发,不周期重复发送 | CO_error()检查状态位是否变化;重复 report/reset 直接返回 |
| EMCY 是 8 字节 CAN 帧 | producer 初始化 TX buffer 时使用 DLC8U |
默认 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 消息内容为:bytes0..1是 error code,byte2是 error register,byte3是 error condition index,bytes4..7是CO_errorReport()的附加信息。2
| 字节 | 内容 | 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.10x1001 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.20x1003 Pre-defined error field
如果启用CO_CONFIG_EM_HISTORY,最新错误可以从对象字典0x1003读取;CANopenNode 官方说明指出0x1003内容对应 EMCY message 的 bytes0..3。2
这意味着:
0x1003 保存:errorCode + errorRegister + errorBit 0x1003 不保存:infoCode,也就是 EMCY bytes 4..7源码上,0x1003与 producer 发送共用CO_EM_fifo_t。fifo.msg保存 bytes0..3,fifo.info保存 producer 发帧用的 bytes4..7。
2.30x1014 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.40x1015 Inhibit time EMCY
0x1015用于限制两帧 EMCY 的最小间隔。源码中OD_write_1015()把 OD 中的UNSIGNED16数值按100 us单位换算为微秒:
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:
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 | 指向 OD0x1001,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 运行时:
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))!=0co->em_fifo,(CO_GET_CNT(ARR_1003)+1U),#endif#if((CO_CONFIG_EM)&CO_CONFIG_EM_PRODUCER)!=0OD_GET(H1014,OD_H1014_COBID_EMERGENCY),CO_GET_CO(TX_IDX_EM_PROD),#if((CO_CONFIG_EM)&CO_CONFIG_EM_PROD_INHIBIT)!=0OD_GET(H1015,OD_H1015_INHIBIT_TIME_EMCY),#endif#endif#if((CO_CONFIG_EM)&CO_CONFIG_EM_HISTORY)!=0OD_GET(H1003,OD_H1003_PREDEF_ERR_FIELD),#endif#if((CO_CONFIG_EM)&CO_CONFIG_EM_STATUS_BITS)!=0OD_statusBits,#endif#if((CO_CONFIG_EM)&CO_CONFIG_EM_CONSUMER)!=0co->CANmodule,CO_GET_CO(RX_IDX_EM_CONS),#endifnodeId,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_fifo是ARR_1003 + 1
环形 FIFO 需要保留一个空槽来区分“空”和“满”:
fifoWrPtr == fifoPpPtr -> 空 fifoWrPtrNext == fifoPpPtr -> 满所以:
fifoSize = 0x1003 历史容量 + 1 实际容量 = fifoSize - 1CO_Emergency.c文件顶部也用fifoSize = 7、实际容量6的图示说明了这个关系。
5.CO_error():错误变化如何进入内部状态和 FIFO
CO_errorReport()和CO_errorReset()都是宏,最终进入CO_error():
#defineCO_errorReport(em,errorBit,errorCode,infoCode)CO_error(em,true,errorBit,errorCode,infoCode)#defineCO_errorReset(em,errorBit,infoCode)CO_error(em,false,errorBit,CO_EMC_NO_ERROR,infoCode)5.1errorBit转成数组下标和位掩码
uint8_tindex=errorBit>>3;uint8_tbitmask=1U<<(errorBit&0x7U);例如errorBit = 0x12:
index = 0x12 >> 3 = 2 bitmask = 1 << 2 = 0x04 目标位 = errorStatusBits[2] 的 bit25.2 重复调用不会重复入队
源码先判断状态是否变化:
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.3CO_error()先留空 error register
CO_error()准备的errMsg是:
uint32_terrMsg=((uint32_t)errorBit<<24)|CO_SWAP_16(errorCode);此时只放入:
byte0..1 = errorCode byte2 = 暂空 byte3 = errorBitbyte2 不在这里填,是因为 error register 需要综合全部errorStatusBits[]和CO_CONFIG_ERR_CONDITION_xxx宏计算。这个工作放在周期性的CO_EM_process()更合适。
6.CO_EM_process():计算0x1001并发送 EMCY
CANopenNode 官方 Doxygen 说明CO_EM_process()必须周期调用,它会检查部分通信错误、计算 OD0x1001,并在必要时发送 EMCY。2
源码顺序是:
- 检查 CAN driver 的错误状态变化。
- 根据
errorStatusBits[]计算errorRegister,写入*em->errorRegister。 - 若当前上下文不允许发送,则返回。
- 若 producer、FIFO、TX buffer、inhibit time 条件满足,则发送一帧 EMCY。
6.1 CAN driver 错误也会变成 EM 输入
CO_EM_process()读取:
uint16_tCANerrSt=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.20x1001是由条件宏汇总的
默认配置中,CANopenNode 预定义了三类条件:
#defineCO_CONFIG_ERR_CONDITION_GENERIC(em->errorStatusBits[5]!=0U)#defineCO_CONFIG_ERR_CONDITION_COMMUNICATION((em->errorStatusBits[2]!=0U)||(em->errorStatusBits[3]!=0U))#defineCO_CONFIG_ERR_CONDITION_MANUFACTURER((em->errorStatusBits[8]!=0U)||(em->errorStatusBits[9]!=0U))这就是errorStatusBits[]与0x1001的关系:前者是细粒度内部状态,后者是 CANopen 标准对象中对错误类别的汇总。
6.3 发送前补齐 byte2
producer 分支中,满足条件后源码补齐 byte2:
em->fifo[fifoPpPtr].msg|=(uint32_t)errorRegister<<16;然后复制 8 字节并发送:
(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 字节复制会把msg和info一起送入 CAN TX buffer。
7. FIFO 与0x1003:事件队列和历史读取共用一套存储
errorStatusBits[]保存当前状态;FIFO 保存状态变化事件。例如:
过压出现 -> report -> FIFO 加一条错误事件 过压仍然存在 -> report -> 状态未变,不加 过压恢复 -> reset -> FIFO 加一条恢复事件 过压已经恢复 -> reset -> 状态未变,不加OD_read_1003()中,subindex0返回fifoCount,subindex1返回最新错误。它从fifoWrPtr往回数,因此0x1003:01是最新一条,0x1003:02是次新一条。
写0x1003:00 = 0会清空错误历史计数:
if(CO_getUint8(buf)!=0U){returnODR_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。3
| 配置 | 作用 | 主要源码路径 |
|---|---|---|
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。3
它的作用可以概括为:把CO_EM_t.errorStatusBits[]暴露给一个自定义 OD 项,供 SDO/调试工具/厂商扩展读取或写入内部错误位图。
9.1 初始化时需要OD_statusBits
头文件的CO_EM_init()参数说明写明:OD_statusBits是用于访问CO_EM_t中errorStatusBits的自定义 OD entry;这个 entry 需要在 subindex0上提供(CO_CONFIG_EM_ERR_STATUS_BITS_COUNT / 8)字节变量,并要求 IO extension。源码在CO_CONFIG_EM_STATUS_BITS打开时才会展开这个参数。
对应初始化分支:
#if((CO_CONFIG_EM)&CO_CONFIG_EM_STATUS_BITS)!=0em->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);#endif9.2 读路径:读取内部错误位图
OD_read_statusBits()的核心行为是把内部状态位复制出去:
OD_size_t countReadLocal=CO_CONFIG_EM_ERR_STATUS_BITS_COUNT/8U;...(void)memcpy((void*)(buf),(constvoid*)(&em->errorStatusBits[0]),countReadLocal);因此,如果你通过 SDO 读取这个自定义 OD 项,读到的是当前errorStatusBits[]的原始位图。它比0x1001更细,但也更偏 CANopenNode 内部实现。
9.3 写路径:可直接改内部错误位图
OD_write_statusBits()的核心行为是反向复制:
OD_size_t countWrite=CO_CONFIG_EM_ERR_STATUS_BITS_COUNT/8U;...(void)memcpy((void*)(&em->errorStatusBits[0]),(constvoid*)(buf),countWrite);这说明它不是只读诊断入口,而是可写入口。写入后,下一次CO_EM_process()会基于新的errorStatusBits[]重新计算0x1001。但要注意:直接写errorStatusBits[]不会像CO_errorReport()那样自动生成 FIFO 事件,也不会自动形成一帧 EMCY。需要事件上报时,应用仍应调用CO_errorReport()/CO_errorReset()。
9.4 它和0x1001、0x1003的区别
| 对象/入口 | 内容 | 是否标准固定对象 | 是否触发 EMCY |
|---|---|---|---|
0x1001 | 错误类别汇总 | 是 | 否,只是当前状态输出 |
0x1003 | 最近错误历史 bytes0..3 | 是 | 否,只是历史读取 |
OD_statusBits | 内部errorStatusBits[]原始位图 | 否,由工程自定义 | 否,直接读写不产生事件 |
CO_errorReport() | 错误发生事件输入 | API | 是,入 FIFO 后由CO_EM_process()发送 |
9.5 什么时候需要打开
建议按用途决定:
| 场景 | 建议 |
|---|---|
| 只学习 EMCY producer/history 主流程 | 可不打开,避免混淆 |
需要通过 SDO 观察内部每个CO_EM_xxxbit | 打开,并在 OD 中放一个自定义 byte array |
需要调试CO_CONFIG_ERR_CONDITION_xxx为什么使0x1001置位 | 打开有帮助 |
| 希望主站通过 OD 强行清/置内部错误位 | 可以打开,但要明确这不会自动生成 EMCY 事件 |
工程上更推荐:应用错误的正常生命周期仍用CO_errorReport()/CO_errorReset();OD_statusBits主要作为调试和诊断入口。
10. 从机工程中的最小使用路径
应用层上报自定义错误时,优先使用 manufacturer 范围的errorBit:
#defineAPP_EM_MOTOR_OVERLOAD(CO_EM_MANUFACTURER_START+0U)voidapp_report_motor_overload(CO_t*co,uint32_tdetail){CO_errorReport(co->em,APP_EM_MOTOR_OVERLOAD,CO_EMC_DEVICE_SPECIFIC,detail);}voidapp_reset_motor_overload(CO_t*co,uint32_tdetail){CO_errorReset(co->em,APP_EM_MOTOR_OVERLOAD,detail);}调试时按这个顺序检查:
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. 参考资料
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 官方 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 官方 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 ↩︎ ↩︎