睿擎派(Ruijing Pi)是一款基于瑞芯微 RK3506 主控芯片的嵌入式开发平台,其底层运行着 RT-Thread 操作系统,并构建于专为工业场景设计的睿擎工业平台之上。该平台整合了数据采集、通信、控制、工业协议等多重功能。
在开发基于该平台的工业网关时,我们发现官方提供的示例代码主要围绕 CANOpen 协议下的 DS402(伺服电机控制)设备规范,而缺少针对 DS401(IO模块)协议的操作文档与案例。经过一段时间的深入研究与实践,我们成功实现了对雷赛 EM32DX-C4 IO 模块的信号采集与输出控制。本文将分享相关的 CANOpen 核心知识、调试方法及具体的代码实现。
一、CANOpen 协议核心概念解析
CANOpen 是一种基于 CAN 总线的、遵循 EN 50325-4 标准的工业通信协议。其核心优势在于通过标准化的对象字典(Object Dictionary, OD) 确保设备互操作性,并提供了 PDO(过程数据对象)、SDO(服务数据对象)等多种灵活的通信机制。
1. 设备状态机
CANOpen 设备必须遵循一个严格的状态机,主要包括以下四个状态:
- 初始化状态 (Initialization):设备上电自检及协议栈初始化,不参与网络通信。
- 预操作状态 (Pre-operational):设备就绪,允许通过 SDO 进行参数配置,但 PDO 通信被禁止。
- 操作状态 (Operational):设备正常工作的状态,PDO 和 SDO 通信均被启用,可进行实时数据交换。
- 停止状态 (Stopped):设备功能受限的安全状态,仅允许 NMT 命令和心跳通信,PDO 被禁用。

2. 核心通信模型与概念
- 对象字典 (OD):设备所有参数和行为定义的集合,采用“16位索引 + 8位子索引”进行寻址。DS301协议定义了所有设备必须支持的通用对象字典,而 DS401 等子协议则定义了特定设备(如IO模块)的专有字典条目。

(DS301通用对象字典示例)

(EM32DX-C4设备参数对象字典示例)
- COB-ID:即11位 CAN 帧ID,由高4位功能码和低7位节点地址组成。
- 网络管理 (NMT):用于控制节点状态(启动、停止、复位)的服务。
- 服务数据对象 (SDO):用于读写对象字典中的参数,适合非实时、低频次的配置操作。
- 过程数据对象 (PDO):用于设备间高速、实时的数据传输,是工业控制中数据交互的关键通道。
- 心跳 (Heartbeat):从节点定期发送的报文,用于向主站宣告自身状态和在线情况。
- 同步 (SYNC):由主站发起的同步报文,用于协调多个从节点的数据采集与输出动作。
二、CANOpen DS401 IO模块控制实战
睿擎派官方示例基于开源的 CanFestival 协议栈实现。我们在其 06_bus_canopen_master_motor(DS402 主站示例)的基础上,进行了大幅修改以适应 DS401 IO 模块的控制。
1. 硬件连接
EM32DX-C4 模块的 CANOpen 接口采用 RJ45 以太网口定义。接线时,仅需将模块的 CAN_H(白橙线)和 CAN_L(橙线)分别连接至睿擎派的 CAN_H 和 CAN_L 接口即可。

调试利器:在开发初期,使用 PCAN-USB 模块配合 PCAN-View 软件监听总线数据,能极大提升问题排查效率。

2. 关键代码适配与实现
a. 主站对象字典简化 (master401_od.c)
由于主站(睿擎派)的角色是控制IO模块,因此需要定义自身的对象字典,用于映射和缓存IO数据。我们大幅删减了原DS402示例中关于伺服电机的复杂字典条目,并新增了用于缓存DO输出和DI输入数据的对象。
// 主站对象字典索引表(简化后)
const indextable master401_objdict[] = {
{ (subindex*)master401_Index1000, sizeof(master401_Index1000)/sizeof(master401_Index1000[0]), 0x1000},
{ (subindex*)master401_Index1001, sizeof(master401_Index1001)/sizeof(master401_Index1001[0]), 0x1001},
// ... 其他DS301必要索引 ...
{ (subindex*)master401_Index2000, sizeof(master401_Index2000)/sizeof(master401_Index2000[0]), 0x2000}, // 新增:DO缓存
{ (subindex*)master401_Index2001, sizeof(master401_Index2001)/sizeof(master401_Index2001[0]), 0x2001}, // 新增:DI缓存
};
其中,0x2000 和 0x2001 索引的定义如下,分别对应16位DO输出值和DI输入值:
/* 0x2000: 本地DO输出缓存 */
uint16_t master401_obj2000_do_val = 0x0000; // 关联全局DO变量
subindex master401_Index2000[] = {
{ RO, uint8, sizeof (UNS8), (void*)&master401_highestSubIndex_obj2000, NULL },
{ RW, uint16, sizeof (uint16_t), (void*)&master401_obj2000_do_val, NULL } // 可读可写
};
/* 0x2001: 本地DI输入缓存 */
uint16_t master401_obj2001_di_val = 0x0000; // 关联全局DI变量
subindex master401_Index2001[] = {
{ RO, uint8, sizeof (UNS8), (void*)&master401_highestSubIndex_obj2001, NULL },
{ RO, uint16, sizeof (uint16_t), (void*)&master401_obj2001_di_val, NULL } // 只读
};
b. 从站PDO映射配置 (master401_canopen.c)
这是实现IO通信的核心。我们需要通过SDO配置从站(EM32DX-C4)的PDO,将它的DI输入映射到它的TPDO1发送给主站,并配置它的RPDO1来接收主站发送的DO输出命令。
配置过程遵循标准流程:禁用PDO -> 设置传输类型 -> 清除旧映射 -> 写入新映射 -> 设置映射条目数 -> 启用PDO。
关键配置代码示例如下(以TPDO1,即从站发送DI为例):
static UNS8 IO_Write_SLAVE_TPDO1_Map(uint8_t nodeId) {
// TPDO1映射:将从站索引0x6100子索引0x01(16位DI值)映射到TPDO1
UNS32 pdo_map_val = 0x61000110; // 索引0x6100 + 子索引0x01 + 16位长度(0x10)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1A00, 1, 4, uint32, &pdo_map_val, config_node_param_cb, 0);
}
同样地,需要配置RPDO1映射到从站的0x6300子索引0x01(16位DO值)。所有这些配置函数被组织成一个数组顺序执行。
c. 上层应用API
为了方便测试和控制,我们封装了简单的IO操作函数,并注册为RT-Thread的MSH命令。
/* 设置所有DO输出 */
rt_err_t em32dx_set_do(uint16_t do_val) {
if (*can_node[1].nmt_state != Operational) {
return -RT_ERROR;
}
g_em32dx_do = do_val;
// 通过写本地对象字典0x2000,触发PDO发送
UNS32 size = 2;
UNS32 errorCode = writeLocalDict(OD_Data, 0x2000, 1, &do_val, &size, 0);
return (errorCode == OD_SUCCESSFUL) ? RT_EOK : -RT_ERROR;
}
/* 读取所有DI输入 */
rt_err_t em32dx_get_di() {
uint16_t di_val = 0;
UNS32 size = 2;
UNS8 data_type;
// 从本地对象字典0x2001读取(由从站TPDO1自动更新)
UNS32 errorCode = readLocalDict(OD_Data, 0x2001, 1, &di_val, &size, &data_type, 0);
if(errorCode == OD_SUCCESSFUL){
g_em32dx_di = di_val;
rt_kprintf("Read DI: 0x%04X\n", di_val);
}
return (errorCode == OD_SUCCESSFUL) ? RT_EOK : -RT_ERROR;
}
MSH_CMD_EXPORT(em32dx_get_di, Get EM32DX-C4 DI input);
/* 控制单路DO通道 */
rt_err_t em32dx_set_do_channel(uint8_t argc, char **argv) {
uint8_t channel = atoi(argv[1]);
uint8_t state = atoi(argv[2]);
if (channel >= 16) return -RT_ERROR;
// 更新全局DO变量对应位
if (state) g_em32dx_do |= (1 << channel);
else g_em32dx_do &= ~(1 << channel);
// 调用设置函数
return em32dx_set_do(g_em32dx_do);
}
MSH_CMD_EXPORT(em32dx_set_do_channel, Set single DO channel (channel 0-15, state 0/1));
三、部署与测试
- 启动服务:在睿擎派控制台执行
canopen_start,初始化CANOpen主站并尝试与从站建立连接。
- 读取输入:执行
em32dx_get_di 命令,可以读取EM32DX-C4模块上16路数字量输入(DI)的当前状态。
- 控制输出:执行
em32dx_set_do_channel 1 1 命令。第一个参数为通道号(0-15),第二个参数为状态(0/1)。此命令将控制模块上对应的数字量输出(DO)通道导通,观察模块上的指示灯会发生相应变化。

通过上述步骤,我们成功在睿擎派平台上实现了基于CANOpen DS401协议对第三方工业IO模块的读写控制,为构建支持多协议、可编程逻辑控制器(PLC)功能的工业网关奠定了基础。
源码与资料
- 本文涉及的完整项目源码可通过文末提供的链接下载。
- CANOpen DS301、DS401、DS402等协议官方文档可在CAN in Automation (CiA)官网获取。