CANopen是基于CAN 总线的一种高层通信协议,主要用于工业自动化、嵌入式设备、机器人、汽车电子等领域,用来让不同厂家的设备(如电机、传感器、控制器)之间实现标准化、可靠的通信。
下面给你一个嵌入式 / 工控视角的清晰解释,尽量直白、不绕弯。
一、CANopen 是什么?
- 底层:基于CAN 2.0A/B硬件总线(差分信号、抗干扰、多主 / 多从)。
- 上层:定义了数据怎么组织、怎么交互、设备怎么配置的一套标准。
- 本质:给 CAN 总线加了一套 “通信语法 + 设备模型 + 状态机”。
一句话:CANopen = CAN 硬件 + 标准化的应用层协议。
二、CANopen 的核心概念(必须懂)
1. 对象字典(Object Dictionary, OD)
- 每个 CANopen 设备都有一个 “对象字典”,本质是一张地址表。
- 用16 位索引 + 8 位子索引定位一个参数 / 数据。
- 例如:
- 0x1000:设备类型
- 0x1001:错误寄存器
- 0x1800+:TPDO 通信参数
- 0x2000+:厂家自定义参数
你可以把它理解为设备的 “寄存器映射表”,所有读写都通过它。
2. 通信对象(COB)
CANopen 用固定格式的 CAN 报文来完成通信,常见几种:
SDO(Service Data Object)
- 作用:读写对象字典(配置、参数读写)。
- 特点:点对点、确认机制、可传长数据(分段传输)。
- 典型用途:设置电机转速、读取传感器校准值、配置节点 ID。
PDO(Process Data Object)
- 作用:实时过程数据传输(控制、反馈)。
- 特点:无确认、快、数据短(最多 8 字节)、可周期性 / 事件触发。
- 典型用途:电机电流 / 速度反馈、控制字、急停信号。
NMT(Network Management)
- 作用:管理整个网络的节点状态。
- 命令:启动、停止、重启、进入预操作等。
- 节点状态:初始化 → 预操作 → 操作 → 停止。
EMCY(Emergency Message)
- 作用:设备故障时主动上报紧急事件。
- 包含错误码、错误寄存器等,用于快速诊断。
SYNC / TIME
- SYNC:同步信号,让多节点同时采样 / 执行。
- TIME:网络时间同步。
3. 节点 ID(Node-ID)
- 每个设备一个唯一 ID:1–127。
- 决定报文 ID 的一部分,用于区分不同设备。
- 例如:节点 1 的 NMT 报文 ID 是固定的,PDO/SDO 也按节点 ID 计算。
4. 设备行规(Device Profile)
- CANopen 不只定义通信,还定义设备行为标准。
- 例如:
- CiA 402:运动控制 / 电机驱动(你之前看的 FOC、BLDC 常用)
- CiA 401:I/O 模块
- CiA 406:编码器
- 好处:不同厂家的伺服驱动器,用同一套命令就能控制。
三、CANopen 典型工作流程(简化)
- 上电 → 节点进入初始化 → 自动进入预操作状态。
- 主机通过SDO配置节点参数(波特率、PDO 映射、周期等)。
- 主机发NMT 启动命令→ 节点进入操作状态。
- 进入实时控制:
- 主机发 PDO 控制字 → 电机 / 设备动作。
- 设备发 PDO 反馈状态 / 数据。
- 故障时:设备发 EMCY,主机处理或发 NMT 停止。
四、和你之前关注的内容有什么关系?
你之前问过:
- 电机驱动、FOC、BLDC、PMSM
- CV18xx、K230、AM32、XTC100 等芯片 / 开发板
这些场景里CANopen 非常常见:
- 机器人关节、AGV、伺服驱动器、工业控制器 → 基本都用 CANopen(尤其是 CiA 402)。
- 很多电机驱动芯片 / 开发板自带 CANopen 协议栈或示例。
五、如果你要上手 CANopen(嵌入式开发路线)
- 先搞懂CAN 控制器收发、滤波、中断(基础)。
- 理解 CANopen 的对象字典、SDO/PDO/NMT机制。
- 用现成协议栈(如 CANopenNode、FreeCANopen、厂家 SDK)。
- 按设备行规(如 CiA 402)实现控制逻辑。
- 用 CANoe / CANalyzer / 串口转 CAN 工具抓包调试。
逐行解释 CANopen 代码,我先给你一段嵌入式场景下最典型、最核心的 CANopen 基础代码(基于通用协议栈框架,适配 STM32/MCU 开发),然后逐行拆解,保证新手能看懂每一行的作用和背后的逻辑。
这段代码聚焦CANopen 节点初始化 + SDO 读写 + PDO 实时传输这三个核心场景,是实际项目中最常用的核心逻辑。
完整 CANopen 核心代码示例
#include "can.h" // CAN硬件驱动头文件 #include "canopen.h" // CANopen协议栈头文件(如CANopenNode) #include "od.h" // 对象字典头文件 // 定义CANopen节点参数 #define CO_NODE_ID 0x01 // 节点ID:1(范围1-127) #define CO_BAUDRATE 500000 // CAN波特率:500kbps #define CO_TXPDO1_ID 0x181 // 发送PDO1的CAN ID(节点1的TPDO1:0x180+1) #define CO_RXPDO1_ID 0x201 // 接收PDO1的CAN ID(节点1的RPDO1:0x200+1) // 全局CANopen节点句柄(协议栈核心结构体) CO_t *canopenNode; // 【函数1】CANopen节点初始化 void CANopen_Init(void) { // 1. 初始化CAN硬件(底层驱动) CAN_InitTypeDef canConfig = {0}; canConfig.Mode = CAN_MODE_NORMAL; // 正常模式(非回环) canConfig.BaudRate = CO_BAUDRATE; // 波特率500k canConfig.AutoBusOff = ENABLE; // 总线关闭自动恢复 CAN_HW_Init(&canConfig); // 调用硬件层初始化函数 // 2. 初始化CANopen协议栈核心 // 参数:节点ID、波特率、对象字典指针、硬件回调函数 canopenNode = CO_new(CO_NODE_ID, CO_BAUDRATE, &OD, CAN_HW_Send); if(canopenNode == NULL) { Error_Handler(); // 初始化失败,进入错误处理 return; } // 3. 配置PDO1(实时过程数据传输) // 3.1 配置发送PDO1(TPDO1):映射"电机实际转速"到对象字典0x2000:01 CO_TPDO_config(canopenNode, 1, CO_TXPDO1_ID, 0x2000, 0x01, CO_PERIOD_10MS); // 3.2 配置接收PDO1(RPDO1):映射"电机目标转速"到对象字典0x2001:01 CO_RPDO_config(canopenNode, 1, CO_RXPDO1_ID, 0x2001, 0x01, CO_TRIGGER_SYNC); // 4. 启动CANopen节点(从预操作状态进入操作状态) CO_NMT_setState(canopenNode, CO_OPERATIONAL); // 5. 初始化SDO服务器(支持上位机读写对象字典) CO_SDO_init(canopenNode); } // 【函数2】CANopen主循环(需放在while(1)中,1ms调用一次) void CANopen_MainLoop(void) { // 1. 处理CAN接收中断的报文(解析PDO/SDO/NMT/EMCY) CO_processRxMsg(canopenNode); // 2. 周期性更新TPDO1数据(读取电机转速,写入对象字典) static uint32_t tick = 0; if(++tick >= 10) // 10ms更新一次(和TPDO1周期匹配) { tick = 0; // 读取硬件层的电机实际转速(单位:rpm) uint16_t motorSpeed = Motor_GetSpeed(); // 将转速写入对象字典0x2000:01(TPDO1会自动发送这个值) CO_OD_write(canopenNode, 0x2000, 0x01, &motorSpeed, sizeof(motorSpeed)); } // 3. 检查RPDO1数据(上位机下发的目标转速) uint16_t targetSpeed = 0; if(CO_OD_read(canopenNode, 0x2001, 0x01, &targetSpeed, sizeof(targetSpeed))) { // 读取到新的目标转速,下发给电机驱动 Motor_SetTargetSpeed(targetSpeed); } // 4. 处理SDO请求(上位机读写参数) CO_SDO_process(canopenNode); // 5. 状态机和错误处理(如总线故障、EMCY上报) CO_errorHandler(canopenNode); } // 【函数3】SDO读写示例(手动读写对象字典,调试/初始化用) void CANopen_SDO_Demo(void) { uint16_t deviceType = 0; uint32_t baudRate = 0; // 1. 读取对象字典:0x1000:00(设备类型,CANopen标准对象) CO_SDO_read(canopenNode, 0x1000, 0x00, &deviceType, sizeof(deviceType)); printf("设备类型:0x%04X\r\n", deviceType); // 2. 写入对象字典:0x2002:00(厂家自定义参数:过流保护阈值) uint16_t overCurrentLimit = 20; // 20A CO_SDO_write(canopenNode, 0x2002, 0x00, &overCurrentLimit, sizeof(overCurrentLimit)); printf("已设置过流保护阈值:%d A\r\n", overCurrentLimit); }逐行详细解释(核心部分)
一、头文件与宏定义
#include "can.h" // CAN硬件驱动头文件 #include "canopen.h" // CANopen协议栈头文件(如CANopenNode) #include "od.h" // 对象字典头文件can.h:底层 CAN 硬件驱动(初始化 CAN 控制器、发送 / 接收报文),由芯片厂家提供(如 STM32 的 HAL 库)。canopen.h:CANopen 协议栈核心(封装 SDO/PDO/NMT 逻辑),常用开源栈如 CANopenNode、FreeCANopen。od.h:对象字典定义(设备所有可读写参数的地址表)。
#define CO_NODE_ID 0x01 // 节点ID:1(范围1-127) #define CO_BAUDRATE 500000 // CAN波特率:500kbps #define CO_TXPDO1_ID 0x181 // 发送PDO1的CAN ID(节点1的TPDO1:0x180+1) #define CO_RXPDO1_ID 0x201 // 接收PDO1的CAN ID(节点1的RPDO1:0x200+1)CO_NODE_ID:每个 CANopen 节点唯一 ID,决定报文 ID 的最后几位(比如 TPDO1 的基础 ID 是 0x180,加节点 ID 就是 0x181)。CO_BAUDRATE:CAN 总线波特率,常见 500k/1M,所有节点必须一致。CO_TXPDO1_ID/CO_RXPDO1_ID:PDO 的 CAN ID 是 CANopen 标准规定的(TPDO1:0x180 + 节点 ID,RPDO1:0x200 + 节点 ID)。
二、CANopen_Init () 初始化函数
1. 初始化 CAN 硬件
CAN_InitTypeDef canConfig = {0}; canConfig.Mode = CAN_MODE_NORMAL; // 正常模式(非回环) canConfig.BaudRate = CO_BAUDRATE; // 波特率500k canConfig.AutoBusOff = ENABLE; // 总线关闭自动恢复 CAN_HW_Init(&canConfig); // 调用硬件层初始化函数- 先配置 CAN 硬件的基础参数(模式、波特率),这是 CANopen 的底层基础,必须先初始化硬件才能跑协议栈。
AutoBusOff:CAN 总线出错导致 BusOff 时,自动尝试恢复,提高鲁棒性。
2. 初始化 CANopen 协议栈核心
canopenNode = CO_new(CO_NODE_ID, CO_BAUDRATE, &OD, CAN_HW_Send); if(canopenNode == NULL) { Error_Handler(); return; }CO_new():协议栈的核心创建函数,返回一个节点句柄(类似文件句柄,后续所有操作都基于这个句柄)。- 参数说明:
CO_NODE_ID/CO_BAUDRATE:节点 ID 和波特率;&OD:指向对象字典的指针(所有参数的地址表);CAN_HW_Send:硬件层发送函数的指针(协议栈组装好报文后,调用这个函数发出去)。
- 判空:如果创建失败(比如内存不足、参数错误),进入错误处理。
3. 配置 PDO(实时过程数据)
// 配置发送PDO1(TPDO1):映射"电机实际转速"到对象字典0x2000:01 CO_TPDO_config(canopenNode, 1, CO_TXPDO1_ID, 0x2000, 0x01, CO_PERIOD_10MS); // 配置接收PDO1(RPDO1):映射"电机目标转速"到对象字典0x2001:01 CO_RPDO_config(canopenNode, 1, CO_RXPDO1_ID, 0x2001, 0x01, CO_TRIGGER_SYNC);CO_TPDO_config():配置发送 PDO(设备主动上报数据):- 参数 1:节点句柄;参数 2:PDO 编号(1);参数 3:PDO 的 CAN ID;
- 参数 4-5:对象字典的索引 + 子索引(0x2000:01 对应 “电机实际转速”);
- 参数 6:发送周期(10ms 一次,实时性要求高的场景可设 1ms)。
CO_RPDO_config():配置接收 PDO(接收上位机下发的控制指令):- 参数 6:触发方式(
CO_TRIGGER_SYNC表示同步触发,也可设周期触发)。
- 参数 6:触发方式(
4. 启动 CANopen 节点
CO_NMT_setState(canopenNode, CO_OPERATIONAL);- NMT 是 CANopen 的网络管理模块,节点上电后默认是 “预操作状态”(只能 SDO 配置,不能传 PDO);
CO_OPERATIONAL:操作状态,节点进入正常工作模式,PDO 可以正常收发。
5. 初始化 SDO 服务器
CO_SDO_init(canopenNode);- SDO 是 “服务数据对象”,用于点对点读写对象字典(比如上位机配置电机参数);
- 初始化后,节点就能响应上位机的 SDO 读写请求。
三、CANopen_MainLoop () 主循环
CO_processRxMsg(canopenNode);- 处理 CAN 硬件接收到的所有报文,协议栈会自动解析:是 PDO/SDO/NMT/EMCY?然后调用对应处理逻辑。
if(++tick >= 10) // 10ms更新一次(和TPDO1周期匹配) { tick = 0; uint16_t motorSpeed = Motor_GetSpeed(); // 读电机实际转速 CO_OD_write(canopenNode, 0x2000, 0x01, &motorSpeed, sizeof(motorSpeed)); }- 周期性读取硬件层的电机转速,写入对象字典 0x2000:01;
- TPDO1 会按 10ms 周期自动读取这个地址的值,然后发送给上位机(无需手动调用发送函数)。
uint16_t targetSpeed = 0; if(CO_OD_read(canopenNode, 0x2001, 0x01, &targetSpeed, sizeof(targetSpeed))) { Motor_SetTargetSpeed(targetSpeed); // 下发目标转速给电机 }- 读取对象字典 0x2001:01(上位机通过 RPDO1 写入的目标转速);
- 如果读取成功,就把目标转速下发给电机驱动模块。
CO_SDO_process(canopenNode); // 处理SDO请求 CO_errorHandler(canopenNode); // 错误处理(如总线故障、EMCY上报)CO_SDO_process():响应上位机的 SDO 读写请求(比如上位机改参数);CO_errorHandler():监控 CAN 总线状态,上报紧急错误(EMCY),比如电机过流、总线断开。
四、CANopen_SDO_Demo () SDO 读写示例
CO_SDO_read(canopenNode, 0x1000, 0x00, &deviceType, sizeof(deviceType)); CO_SDO_write(canopenNode, 0x2002, 0x00, &overCurrentLimit, sizeof(overCurrentLimit));CO_SDO_read():读取标准对象字典 0x1000:00(设备类型,CANopen 标准定义);CO_SDO_write():写入厂家自定义对象 0x2002:00(过流保护阈值);- SDO 是 “确认式” 通信,读写都会有应答,适合配置参数(非实时)。
总结
- 核心逻辑:CANopen = 底层 CAN 硬件 + 上层协议栈,所有数据交互都基于对象字典(索引 + 子索引)。
- PDO vs SDO:PDO 是 “无确认、高速、短数据”(实时控制),SDO 是 “有确认、低速、长数据”(参数配置)。
- 关键步骤:初始化 CAN 硬件 → 初始化协议栈 → 配置 PDO/SDO → 启动节点 → 主循环处理收发和数据更新。