按 LED 源码学习 CiA 303-3:CANopenNode 如何计算 RUN / ERROR 指示灯

@[toc]

结论

303/CO_LEDs.c/h 的核心不是“驱动 LED”,而是实现 CiA 303-3 CANopen indicators:把 CANopen 设备的通信相关状态,转换成标准的 绿色 RUN LED红色 ERROR LED 指示模式。

本模块的输入来自 CANopen 状态与错误上下文,例如 NMT 状态、LSS 配置状态、CAN bus-off、CAN warning、Heartbeat consumer 错误、SYNC 超时、RPDO event timer 超时、其他错误、固件下载状态等;输出是 LEDred / LEDgreen bitfield 中的 CO_LED_CANopen 位。

本文只讨论 CANopenNode 经典 CANopen 的:

  • 303/CO_LEDs.h
  • 303/CO_LEDs.c

NMT、LSS、Heartbeat consumer、SYNC、PDO、CAN bus-off 只作为 LED 输入上下文出现,不展开这些协议模块本身。

在这里插入图片描述

1. 先理解 CiA 303-3:它规定的是“通信状态怎么被看见”

CiA 303 是 CANopen 相关设备与网络设计建议的一组文档。CiA 官方资料说明,CiA 303-3 是 indicator specification,用于描述 CANopen 设备上的通信相关指示器;额外的应用相关指示器应由设备 profile 或制造商自行定义。官方说明还指出,303-3 推荐用 LED 闪烁模式指示设备状态,帮助服务人员在没有完整诊断工具时识别通信问题。[^cia303]

所以,CiA 303-3 的重点不是:

1
2
3
4
怎么配置 NMT?
怎么收 Heartbeat?
怎么发 SYNC?
怎么映射 PDO?

而是:

1
当这些 CANopen 通信状态已经产生之后,设备应该用什么 LED 模式表达出来?

CANopenNode 的 Doxygen 明确把 CO_LEDs.h 标为 CANopen Indicator specification (CiA 303-3 v1.4.0),并说明 CiA 303-3 使用绿色 RUN LED、红色 ERROR LED 或红绿双色 LED 来反映 CANopen 设备状态。[^coleds]

2. 协议层面的两类指示灯

2.1 RUN 绿灯:回答“设备运行到哪一步了”

CANopenNode 对 RUN LED 的语义列在 CO_LEDs.h 注释和 Doxygen 中:[^coleds]

RUN LED 模式 表示的 CANopen 状态
flickering LSS configuration state is active
blinking device is in NMT pre-operational state
single flash device is in NMT stopped state
triple flash software download is running in the device
on device is in NMT operational state

这里的 RUN LED 不是“程序有没有运行”的通用心跳灯,而是 CANopen 通信状态指示灯。它重点表达设备处于配置、预操作、停止、运行或固件下载等状态。

2.2 ERROR 红灯:回答“通信/配置是否异常”

ERROR LED 的语义同样来自 CO_LEDs.h 注释和 Doxygen:[^coleds]

ERROR LED 模式 表示的 CANopen 错误状态
off no error
flickering LSS node ID is not configured, CANopen is not initialized
blinking invalid configuration, general error
single flash CAN warning limit reached
double flash heartbeat consumer error in remote monitored node
triple flash sync message reception timeout
quadruple flash PDO has not been received before the event timer elapsed
on CAN bus off

ERROR LED 不是错误日志,也不是错误队列。它在某一时刻只能显示一个最终模式。多个错误同时存在时,CANopenNode 在 CO_LEDs.c 里用固定优先级选择最终显示内容。

在这里插入图片描述

3. CANopenNode 为什么把它放在 303/ 目录

CANopenNode 仓库结构中,301/ 是 CANopen application layer and communication profile,里面有 NMT、Heartbeat、Emergency、PDO、SYNC 等模块;303/ 被标为 CANopen Recommendation,并包含 CO_LEDs.h/.c - CANopen LED Indicators。[^repo]

这说明 CO_LEDs 的定位不是 CAN 收发驱动,也不是 NMT 状态机本体,而是 CANopen recommendation 层的显示规则实现:

1
2
3
4
5
301/ 模块产生状态或错误上下文

303/CO_LEDs.c 按 CiA 303-3 语义选择 LED 模式

LEDred / LEDgreen 输出最终 bitfield

4. 从 CO_LEDs.h 开始读:头文件先给出协议语义

CO_LEDs.h 里最重要的内容有三类。

4.1 模式 bitmask

1
2
3
4
5
6
7
#define CO_LED_flicker  0x01U  /**< LED flickering 10Hz */
#define CO_LED_blink 0x02U /**< LED blinking 2,5Hz */
#define CO_LED_flash_1 0x04U /**< LED single flash */
#define CO_LED_flash_2 0x08U /**< LED double flash */
#define CO_LED_flash_3 0x10U /**< LED triple flash */
#define CO_LED_flash_4 0x20U /**< LED quadruple flash */
#define CO_LED_CANopen 0x80U /**< LED CANopen according to CiA 303-3 */

这些宏分成两层:

作用
CO_LED_flicker / CO_LED_blink / CO_LED_flash_1..4 表示某种闪烁模式当前相位是否为 ON
CO_LED_CANopen 表示 CANopen 指示灯最终输出是否为 ON

也就是说,CO_LED_flash_3 不是“错误类型”,而是“三闪模式在当前时间片是否点亮”的节拍位;CO_LED_CANopen 才是应用层最终要读取的 CANopen LED 开关结果。

4.2 输出读取宏

1
2
#define CO_LED_RED(LEDs, BITMASK)   ((((LEDs)->LEDred & BITMASK) != 0U) ? 1U : 0U)
#define CO_LED_GREEN(LEDs, BITMASK) ((((LEDs)->LEDgreen & BITMASK) != 0U) ? 1U : 0U)

对 CANopen 指示灯而言,读取方式是:

1
2
CO_LED_RED(LEDs, CO_LED_CANopen)
CO_LED_GREEN(LEDs, CO_LED_CANopen)

这里仍然只是在读 CO_LEDs_t 内部计算结果,不涉及具体板级输出。

4.3 CO_LEDs_t 保存时基、模式相位和输出结果

1
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t LEDtmr50ms;
uint8_t LEDtmr200ms;
uint8_t LEDtmrflash_1;
uint8_t LEDtmrflash_2;
uint8_t LEDtmrflash_3;
uint8_t LEDtmrflash_4;
uint8_t LEDred;
uint8_t LEDgreen;
} CO_LEDs_t;

这个结构体不是只保存两个 LED 的开关状态,而是同时保存:

  1. LEDtmr50ms:50 ms 基准时钟累计值;
  2. LEDtmr200ms:200 ms 模式时间槽计数;
  3. LEDtmrflash_1..4:单闪、双闪、三闪、四闪的状态机计数;
  4. LEDred / LEDgreen:红绿两路 bitfield,里面既有模式相位位,也有最终 CO_LED_CANopen 输出位。
    在这里插入图片描述

5. 再读 CO_LEDs.c:源码把协议模式拆成“时基 + 优先级 + 输出位”

CO_LEDs.c 只有两个公开函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CO_ReturnError_t CO_LEDs_init(CO_LEDs_t* LEDs);

void CO_LEDs_process(CO_LEDs_t* LEDs,
uint32_t timeDifference_us,
CO_NMT_internalState_t NMTstate,
bool_t LSSconfig,
bool_t ErrCANbusOff,
bool_t ErrCANbusWarn,
bool_t ErrRpdo,
bool_t ErrSync,
bool_t ErrHbCons,
bool_t ErrOther,
bool_t firmwareDownload,
uint32_t* timerNext_us);

Doxygen 说明 CO_LEDs_process() 必须周期调用,并且参数中包含 NMT operating state、LSS configuration indication、CAN bus-off、CAN warning、RPDO timeout、SYNC timeout、Heartbeat consumer error、other error、firmware download 等输入。[^coleds]

因此,它的运行逻辑可以分成三层。

在这里插入图片描述

5.1 第一层:累计时间,只在 50 ms tick 上更新

源码先做:

1
2
3
4
5
6
LEDs->LEDtmr50ms += timeDifference_us;
while (LEDs->LEDtmr50ms >= 50000U) {
tick = true;
LEDs->LEDtmr50ms -= 50000U;
...
}

含义是:CO_LEDs_process() 可以被更高频调用,但 LED 模式本身以 50 ms 为基本离散时间片更新。

每个 50 ms tick 都会更新 flicker 相位;每累计 4 个 50 ms tick,即 200 ms,再更新 blinkflash_1..4

CO_LEDs.h 给出的模式位含义是:

模式位 频率/形态
CO_LED_flicker 10 Hz flickering
CO_LED_blink 2.5 Hz blinking
CO_LED_flash_1 single flash
CO_LED_flash_2 double flash
CO_LED_flash_3 triple flash
CO_LED_flash_4 quadruple flash

源码内部用 rdgr 两个临时 bitfield 生成红、绿两套相位。后面 ERROR LED 只取 rd 中对应模式位,RUN LED 只取 gr 中对应模式位。

这一步还没有决定“当前是什么 CANopen 状态”,只是准备好各种标准模式在当前时刻的 ON/OFF 相位。

5.3 第三层:用输入状态选择最终 CANopen 输出位

tick == true 时,源码开始计算:

1
uint8_t rd_co, gr_co;

其中:

  • rd_co:红色 ERROR LED 在当前时刻是否点亮;
  • gr_co:绿色 RUN LED 在当前时刻是否点亮。

最后再写回:

1
2
3
4
5
6
7
8
if (rd_co != 0U) {
rd |= CO_LED_CANopen;
}
if (gr_co != 0U) {
gr |= CO_LED_CANopen;
}
LEDs->LEDred = rd;
LEDs->LEDgreen = gr;

所以最终输出的关键不是 rd_co / gr_co 变量本身,而是 LEDred / LEDgreen 里的 CO_LED_CANopen bit。

6. ERROR 红灯:协议错误语义在源码中的优先级

CO_LEDs.c 对 ERROR LED 的判断顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (ErrCANbusOff) {
rd_co = 1;
} else if (NMTstate == CO_NMT_INITIALIZING) {
rd_co = rd & CO_LED_flicker;
} else if (ErrRpdo) {
rd_co = rd & CO_LED_flash_4;
} else if (ErrSync) {
rd_co = rd & CO_LED_flash_3;
} else if (ErrHbCons) {
rd_co = rd & CO_LED_flash_2;
} else if (ErrCANbusWarn) {
rd_co = rd & CO_LED_flash_1;
} else if (ErrOther) {
rd_co = rd & CO_LED_blink;
} else {
rd_co = 0;
}

对应关系如下:

优先级 输入条件 ERROR LED 模式 语义
1 ErrCANbusOff on CAN bus off
2 NMTstate == CO_NMT_INITIALIZING flickering CANopen 未初始化 / Node-ID 未配置语义
3 ErrRpdo quadruple flash RPDO event timer timeout
4 ErrSync triple flash SYNC receive timeout
5 ErrHbCons double flash Heartbeat consumer error
6 ErrCANbusWarn single flash CAN warning limit reached
7 ErrOther blinking invalid configuration / general error
8 无以上条件 off no error

在这里插入图片描述

这里要注意两点:

  1. 表格中的协议语义不是多个 LED 同时叠加显示。 由于源码使用 if / else-if,同一时刻只选择一个 ERROR 模式。
  2. ErrCANbusOff 是最高优先级。 一旦 bus-off 为真,红灯直接常亮,不再显示其他错误模式。

7. RUN 绿灯:运行状态在源码中的优先级

RUN LED 的源码判断顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (LSSconfig) {
gr_co = gr & CO_LED_flicker;
} else if (firmwareDownload) {
gr_co = gr & CO_LED_flash_3;
} else if (NMTstate == CO_NMT_STOPPED) {
gr_co = gr & CO_LED_flash_1;
} else if (NMTstate == CO_NMT_PRE_OPERATIONAL) {
gr_co = gr & CO_LED_blink;
} else if (NMTstate == CO_NMT_OPERATIONAL) {
gr_co = 1;
} else {
gr_co = 0;
}

对应关系如下:

优先级 输入条件 RUN LED 模式 语义
1 LSSconfig flickering LSS configuration state is active
2 firmwareDownload triple flash software download is running
3 NMTstate == CO_NMT_STOPPED single flash NMT stopped
4 NMTstate == CO_NMT_PRE_OPERATIONAL blinking NMT pre-operational
5 NMTstate == CO_NMT_OPERATIONAL on NMT operational
6 其他状态 off 不显示 RUN 状态

在这里插入图片描述

从这个顺序可以看出,RUN LED 不是单纯显示 NMT 状态。LSSconfigfirmwareDownload 会优先覆盖 NMT 状态显示。

8. 把完整流程串起来

CO_LEDs_process() 每次被调用时,逻辑可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输入:
timeDifference_us
NMTstate
LSSconfig
ErrCANbusOff / ErrCANbusWarn / ErrRpdo / ErrSync / ErrHbCons / ErrOther
firmwareDownload

处理:
1. 累计到 50 ms tick
2. 生成 flicker / blink / flash_1..4 当前相位
3. 按 ERROR 优先级得到 rd_co
4. 按 RUN 优先级得到 gr_co
5. rd_co / gr_co 非 0 时,置位 CO_LED_CANopen

输出:
LEDs->LEDred bitfield
LEDs->LEDgreen bitfield

最终,CANopen 指示灯的开关状态由以下两个表达式读取:

1
2
CO_LED_RED(LEDs, CO_LED_CANopen)
CO_LED_GREEN(LEDs, CO_LED_CANopen)

这就是 303/CO_LEDs.c/h 的核心:

先按 CiA 303-3 的模式定义生成标准闪烁相位,再按 CANopen 状态与错误输入选择当前应显示的 RUN / ERROR 语义,最后写成红绿 LED 的 CO_LED_CANopen 输出位。

9. 学习这份源码时的主线

建议按下面顺序理解,而不是一开始就陷入定时器细节:

  1. 先看 CiA 303-3 要表达什么。 绿色 RUN 表示运行/配置状态,红色 ERROR 表示错误/异常状态。
  2. 再看 CO_LEDs.h 它把协议里的 flicker、blink、single flash、double flash、triple flash、quadruple flash、CANopen 输出位定义成 bitmask。
  3. 再看 CO_LEDs_t 它不是两个 bool,而是保存 50 ms/200 ms 计时器、flash 状态机和红绿 bitfield。
  4. 最后看 CO_LEDs_process() 它先生成模式相位,再按 ERROR 与 RUN 两条优先级链选择最终输出。

只要抓住这条主线,CO_LEDs.c 就不再是零散的闪灯代码,而是 CiA 303-3 指示语义在 CANopenNode 里的一个小型状态显示器。

参考资料

[^cia303]: CAN in Automation, “CiA® 303 series: CANopen-related documents”。该页面说明 CiA 303-3 是 CANopen indicators,并描述其用于通信相关指示器和 LED 闪烁模式。https://www.can-cia.org/can-knowledge/cia-303-series-canopen-related-documents

[^coleds]: CANopenNode Doxygen, “LED indicators” 与 “303/CO_LEDs.h File Reference”。该文档说明 CO_LEDs.hCANopen Indicator specification (CiA 303-3 v1.4.0),列出 RUN/ERROR LED 模式,并给出 CO_LEDs_process() 输入参数。https://canopennode.github.io/CANopenNode/group__CO__LEDs.html

[^repo]: CANopenNode GitHub repository README, “File structure”。该仓库结构说明 303/ - CANopen Recommendation 下包含 CO_LEDs.h/.c - CANopen LED Indicatorshttps://github.com/CANopenNode/CANopenNode