背景:为什么需要 CANopen
CAN 物理层只负责把数据帧传出去------标准帧 11 位 ID 加最多 8 字节。至于帧里每个字节的含义,物理层不提供任何约定。接入第三方驱动器时,看到 ID 0x203 第 2 字节是 0x2A,是电流还是转速、单位是什么,只能翻厂商手册。
CANopen(CiA 301 标准)就是解决这层约定------它是 CAN 总线上用得最广的应用层协议,统一了 ID 含义、参数读写方式、故障上报、在线状态同步。你要做的是在 STM32 上按这套标准实现一个从站,让主站(PLC 或上位机)能读写参数、监控状态。
这套协议栈的实现难点不在通信逻辑,而在对象字典 。本文用 CANopenNode 演示:对象字典、SDO、Heartbeat、EMCY 这些样板代码它全包了,你只需实现 3 个回调。
环境:STM32(HAL 库)+ CAN 外设,CANopenNode 协议栈,任意支持 C99 的编译链。
一、对象字典:CANopen 的数据核心
对象字典(Object Dictionary,OD)同时承担"配置表"和"状态表"两个角色,是一个二维结构:16 位索引 + 8 位子索引,设备的每个参数、状态变量各占一个格子。
索引范围由 CiA 301 规定:
| 索引范围 | 区段 | 典型内容 |
|---|---|---|
| 0x1000-0x1FFF | 通信参数 | 设备类型、心跳周期、节点号 |
| 0x2000-0x5FFF | 厂商自定义 | 传感器读数、IO 配置 |
| 0x6000-0x9FFF | 标准设备参数 | CiA 401 I/O 模块、CiA 402 驱动器 |
| 0xA000-0xFFFF | 保留 | --- |
手写的代价:几十个对象要逐一用 C 结构体加数组定义,还得单独写 SDO 报文的解析和读写逻辑。对象一多就是几百行样板,而且"字典定义"和"实际读写"两处容易脱节。
CANopenNode 的做法是宏声明 + 变量地址绑定,主站的 SDO 读写请求由协议栈自动映射到对应变量。
二、最小可跑例子
c
#include "CO_app.h"
/* 第一步:初始化------传入节点号、波特率、对象字典 */
CO_ReturnError_t err = CO_init(&co, nodeId, baudRate,
&coObjects[0], coObjectsCount);
/* 第二步:主循环里持续驱动协议栈 */
while (1) {
CO_process(&co, ×tamp);
}
两个函数把协议栈转起来,真正的工作量在 coObjects 数组上。
三、宏声明对象
CANopenNode 用 OD_ENTRY 宏声明对象。一个只读温度对象(索引 0x2000)这样写:
c
OD_ENTRY(0x2000, 0x00, "Temperature", /* 索引、子索引、名字 */
ODT_U16, /* 类型:16 位无符号 */
&sensorData.temperature, /* 绑定的变量地址 */
ODA_READ_ONLY, /* 只读 */
NULL); /* 无回调 */
/* 可读写的 IO 输出(索引 0x2001),写操作会触发回调 */
OD_ENTRY(0x2001, 0x00, "GPIO Output",
ODT_U8,
&gpioOutput,
ODA_READ_WRITE,
gpio_write_callback);
末尾两个参数是访问权限和回调。主站写 0x2001 时,协议栈解析报文后调用 gpio_write_callback 并更新 gpioOutput。注册回调后,写入才会通知到业务逻辑。这是"3 个回调"的第一个,另两个是 NMT 状态回调和 CAN 收发回调。
四、三种内置机制
CiA 301 要求从站能自动做三件事,CANopenNode 全部内置。
Heartbeat(心跳):从站周期广播存活信号,主站在周期内没收到即判离线。
c
/* 把心跳周期设为 1000ms */
OD_ENTRY(0x1017, 0x00, "Heartbeat Producer Time",
ODT_U16, &heartbeatTime, ODA_READ_WRITE, NULL);
heartbeatTime = 1000;
/* 协议栈会按这个间隔自动发 ID 0x700+nodeId 的 Heartbeat 帧 */
EMCY(紧急消息):故障时从站主动推送,不等主站轮询。格式统一:2 字节错误码 + 1 字节错误寄存器 + 最多 5 字节厂商自定义。
c
/* 传感器异常------主动上报 EMCY */
CO_errorReport(&co, CO_EM_GENERAL_SOFTWARE_ERROR,
CO_EMC_GENERIC, 0x01);
/* 协议栈自动封装成 EMCY 帧(ID 0x80+nodeId)发出 */
NMT(网络管理):主站用一条 NMT 消息控制从站状态机(运行/停止/复位)。协议栈收到后自动迁移状态,业务在回调里响应:
c
void onNmtEvent(CO_NMT_internalState_t state) {
if (state == CO_NMT_OPERATIONAL) {
startSensorPolling(); /* 进入运行态,开始采样 */
} else if (state == CO_NMT_STOPPED) {
stopSensorPolling(); /* 停止态,关闭外设省电 */
}
}
onNmtEvent 就是第二个回调(NMT 状态回调)。
五、STM32 移植:只写 CAN 收发
CANopenNode 与硬件无关,只要求提供收发两帧的抽象,HAL 下就两段:
c
/* 发帧(HAL 回调) */
void canSend(CO_CANmodule_t *module, uint32_t id,
uint8_t *data, uint8_t len) {
CAN_TxHeaderTypeDef header = {
.StdId = id, .ExtId = 0,
.RTR = CAN_RTR_DATA, .DLC = len
};
HAL_CAN_AddTxMessage(&hcan, &header, data, &txMailbox);
}
/* 收帧(HAL 中断回调) */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef header;
uint8_t data[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data);
CO_CANrxBufferInit(&co, 0, header.StdId, header.DLC, data);
}
收发就是第三个回调的两种形态。SDO 读写、PDO 映射、心跳定时器、NMT 状态机全由 CO_process() 在主循环里处理,移植代码总共十几行。
六、边界与选型
- 主干不支持 CAN-FD :基于经典 CAN 2.0,一帧 8 字节。要 64 字节帧看 CAN-FD 分支 或换商业实现。
- 无官方认证:社区开源实现,未过 CiA 一致性测试。产品要做认证,商用协议栈(Ixxat、emotas 等)绕不过。
- 对象字典维护:大型设备对象多,纯 C 宏维护不如 Vector CANeds 这类图形工具顺手,规模大了要配套工具链。
七、总结
设备要用 CAN 总线对接 PLC 或工业控制系统,CANopenNode 是开源方案里最成熟的选择。它不写业务逻辑,但省掉对象字典管理、SDO 解析、Heartbeat 和 EMCY 那几百行样板代码,代价是理解对象字典结构并实现 3 个回调:对象读写、NMT 状态、CAN 收发。