【睿擎派】CANOpen总线之IO模块读写(DS401协议)
睿擎派以瑞芯微 RK3506 为主控芯片,底层搭载 RT-Thread 操作系统,基于专为工业场景打造的睿擎工业平台进行开发。该平台是全栈自主可控的软硬件一体化解决方案,整合了数据采集、通信、控制、工业协议、AI、显示六大核心功能,精准适配工业应用需求。
官方仅提供了基于 CANOpen 协议(即 DS402 设备规范)操作伺服电机的示例代码,暂无 IO 模块相关的操作参考文档与实践案例。经过数日的深入钻研与反复调试,最终成功实现雷赛 EM32DX-C4 模块的 IO 信号采集与输出控制功能。
下面将简要分享这段时间积累的 CANOpen 相关技术要点,以及代码编写与调试的具体实践过程。
一、CANOpen背景知识介绍
CAN 总线于 1986 年 2 月正式发布,CANOpen 协议则在 1994 年 11 月推出。作为基于 CAN 总线的工业级通信协议,CANOpen 遵循 EN 50325-4 标准,是工业自动化领域主流的现场总线解决方案之一。
其核心优势体现在标准化设计上 —— 通过统一的对象字典保障设备间互操作性,支持 PDO(过程数据对象)、SDO(服务数据对象)等灵活通信机制,兼顾实时性与数据完整性。协议内置 DS401(IO 模块)、DS402(运动控制)等专用行规,可适配伺服电机、IO 模块、传感器等各类工业设备。
我们自行生产的网关产品很早就集成了 CAN 总线功能,但仅用于与自有 IO 模块的实时通信,较少对接第三方模块。目前国内主流智能模块仍以 RS485 通信为主,我虽早已知晓 CANOpen 协议,但实际应用机会不多。随着计划基于睿赛德睿擎工业平台开发新一代网关产品,各类现场工业总线均需深入研究。
CANOpen 协议相对复杂,核心原因是它要求设备遵循严格的状态机模式,主要包含四大核心状态:
(1) 初始化状态(Initialization)
设备刚上电,正在进行硬件自检和协议栈初始化
不参与网络通信,不接收任何命令 (除基本复位)
完成后自动进入 Pre-operational 状态,并发送一个启动心跳信号
(2) 预操作状态(Pre-operationa)
设备已初始化完成,等待配置
可接收 SDO (服务数据对象),进行参数配置和诊断
PDO (过程数据对象) 通信被禁用,无法进行正常数据交换
是唯一可修改对象字典的状态,适合设备参数配置
(3) 操作状态(Operational)
设备正常工作状态,所有功能激活
PDO 和 SDO 通信全部启用,可收发过程数据
设备自主执行任务,响应网络请求
由 NMT 主机发送 "Start Node" 命令触发进入
(4) 停止状态(Stopped)
设备的安全状态,功能受到限制。
`PDO 通信被完全禁用,仅允许 NMT 命令和心跳
设备保持配置,但不执行控制任务
由 NMT 主机发送 "Stop Node" 命令触发,常用于安全暂停

注:对伺服运动设备(DS402),还额外定义了电源相关状态,比如电源禁用区,电源启用区,故障区等相关定义。
CANOpen 协议的四大核心状态中,嵌套了三种核心通信模型,具体如下:
(1) 主 / 从站模型:与 Modbus 协议逻辑类似,核心差异是支持多主机架构,最多可接入 127 个从站,主要用于网络诊断和设备状态管理。
(2) 客户端 / 服务器模型:借鉴 TCP/IP 协议的交互模式,专门适配对象字典(OD)的读写操作。从设备作为服务器提供参数访问服务,主设备作为客户端发起读写请求。
(3) 生产者 / 消费者模型:与 MQTT 协议的通信逻辑一致,从设备担任数据生产者,主设备担任数据消费者。生产者可按主设备的明确请求(拉模型),或主动无请求推送(推模型),传输预设的目标数据。
了解了以上知识后,还需要了解CANOpen协议的如下相关概念
(1) 对象字典
对象字典的功能类似 Modbus 协议中的寄存器,是定义 CANOpen 节点所有行为、参数及通信规则的核心。与 Modbus 寄存器不同,它通过 “16 位索引 + 8 位子索引” 的双标识方式,唯一确定每个条目。
对象字典分为公共对象字典和私有对象字典。其中 DS301 协议作为 CANOpen 的基础通用协议,明确了所有 CANOpen 设备必须遵循的公共对象字典规范。

针对我们对接的EM32DX-C4查看设备手册,DS301对应的数据字典的通用参数如下:

设备参数如下:

DS401属于子协议,专门定义数字量 / 模拟量 IO 的采集、控制、诊断等特有功能,索引范围集中在 0x6000-0x77FF,核心条目按 “数字量 IO”“模拟量 IO”“诊断” 分类

查EM32DX-C4设备手册 DS401协议对应的对象字典参数如下:

(2) COB-ID
COB-ID其实就是11位CAN ID,它分两部分组成,高4位为功能码,低7位为从设备地址码,所以最多支持127个从设备。

功能码和具体的通信服务相关(如下图所示):

(3) 网络管理(NMT)
NMT服务用于通过NMT命令(如:启动、停止、复位)来控制CANopen设备的状态(如:预运行、运行、停止)。
为了改变状态,NMT主机发送一个带有 CAN ID 的2字节消息(即功能代码和节点ID )。所有从站节点都处理这个报文。节点ID 0表示广播命令。

功能代码如下:

(4) 服务数据对象(SDO)
SDO(Server Data Object,服务数据对象)的核心功能是访问或修改 CANopen 设备对象字典内的参数值。例如,当应用主站需调整 CANopen 设备的特定配置参数时,可借助 SDO 服务完成参数的读写操作,实现设备配置的灵活变更。
(5) 过程数据对象(PDO)
PDO(Process Data Object,过程数据对象) 是专为设备间实时、高速数据传输设计的核心通信服务,是工业场景中过程数据交互的关键通道。
PDO数据上传有四种方式触发:
定时发送,同步传输(同步信号触发),远程请求和事件触发。
(6) 心跳信号(Heartbeat)
CANopen 的心跳服务具有双重核心目的:一是向网络发送 “设备在线” 的活动消息,二是确认 NMT 命令的执行状态。NMT 从设备会按预设周期(例如 200 毫秒)发送心跳消息,消息 CAN ID 遵循固定规则(如节点 2 的 CAN ID 为 0x702),其第一个数据字节携带节点当前的 NMT 状态码(如下图)。若心跳消息的接收方(如NMT主站)在设定时限内未收到该消息,将触发预设的离线响应机制。

(7) 同步(SYNC)
CANopen 的 SYNC 报文核心作用是同步多个从设备的输入采集与输出响应,通常由应用主站发起。主站向 CANopen 网络发送 SYNC 消息(COB-ID 为 0x080),支持带或不带 SYNC 计数器两种传输形式。多个从节点可预先配置为响应 SYNC 信号,要么同步捕获输入数据并传输,要么与其他参与同步的节点协同设置输出,确保动作一致性。
SYNC 计数器的存在可灵活划分同步组,实现多组设备的独立同步操作,适配不同场景下的协同需求。
(8) 紧急情况(EMCY)
CANopen 的紧急服务(EMCY)专为设备发生致命错误(如传感器故障)设计,用于向网络其他节点及时上报故障状态。受影响的节点会以高优先级、单次触发式向网络发送 EMCY 消息(例如节点 2 的消息 COB-ID 为 0x082)。消息的数据字节携带具体错误码及相关辅助信息,通过查询设备手册或协议规范,可获取对应的故障详情。
(9) 时间戳(Timestamp)
该消息由主站发起,对应的 CAN ID 为 0x100。使用 6 字节 (48 位)表示。前 4 个字节 (32 位): 表示从午夜开始的毫秒数(范围: 0-4294967295 毫秒,约 1193 小时)。后 2 个字节 (16 位): 表示自 1984 年 1 月 1 日 0 时起的天数(范围: 0-65535 天,约 179.4 年) 。
二、CANOpen DS401协议实现
官方示例(06_bus_canopen_master_motor)在免费开源的CanFestival(LGPLv2 许可证)的基础上实现的。该开源代码实现了CANOpen协议如下功能:
(1)NMT(网络管理):节点状态控制(初始化、预操作、操作、停止)和心跳监测
(2)PDO(过程数据对象):高速实时数据传输,支持循环和事件触发模式,优化工业控制场景响应速度
(3)SDO(服务数据对象):对象字典参数访问,支持快速下载和分段下载,用于设备配置和参数调整
(4)SYNC(同步对象):网络时钟同步和周期性数据传输协调
(5)EMCY(紧急对象):错误报告和故障通知机制

我是在官方示例06_bus_canopen_master_motor的基础上进行大幅度修改而完成。除了canopen_callback.*相关内容没有多少变化外,其他文件改动比较大。
讲解代码之前,先简单说一下硬件接线。查EM32DX-C4手册,CANOpen接口采用了以太网的接口,管脚定义如下:

根据这个定义,自己做了一个CAN网络连接线,主要用到1,2两根线,对应的网线是1-白橙和2-橙色。白橙也就是CAN_P接入睿擎派的CAN_H接口,橙色接入睿擎派的CAN_L接口。

由于我对 CANOpen 协议的了解不够深入,且是初次接触塞雷 EM32DX-C4 硬件模块,初期的调试工作遇到了不少阻碍。好在手头恰好有 PCAN-USB 模块,将其接入 CAN 总线后,我通过 PCAN-View 工具实时监听 CAN 帧数据,这一操作直接显著提升了开发与调试的效率(如下图)。

master402_od.c改名为master401_od.c
主要是DS301和DS401对象字典定义的地方。对原有的数据字典进行了大幅度的删减。
原有的对象字典定义:
const indextable master402_objdict[] =
{
{ (subindex*)master402_Index1000,sizeof(master402_Index1000)/sizeof(master402_Index1000[0]), 0x1000},
{ (subindex*)master402_Index1001,sizeof(master402_Index1001)/sizeof(master402_Index1001[0]), 0x1001},
{ (subindex*)master402_Index1005,sizeof(master402_Index1005)/sizeof(master402_Index1005[0]), 0x1005},
{ (subindex*)master402_Index1006,sizeof(master402_Index1006)/sizeof(master402_Index1006[0]), 0x1006},
{ (subindex*)master402_Index1014,sizeof(master402_Index1014)/sizeof(master402_Index1014[0]), 0x1014},
{ (subindex*)master402_Index1016,sizeof(master402_Index1016)/sizeof(master402_Index1016[0]), 0x1016},
{ (subindex*)master402_Index1017,sizeof(master402_Index1017)/sizeof(master402_Index1017[0]), 0x1017},
{ (subindex*)master402_Index1018,sizeof(master402_Index1018)/sizeof(master402_Index1018[0]), 0x1018},
{ (subindex*)master402_Index1200,sizeof(master402_Index1200)/sizeof(master402_Index1200[0]), 0x1200},
{ (subindex*)master402_Index1280,sizeof(master402_Index1280)/sizeof(master402_Index1280[0]), 0x1280},
{ (subindex*)master402_Index1281,sizeof(master402_Index1281)/sizeof(master402_Index1281[0]), 0x1281},
{ (subindex*)master402_Index1400,sizeof(master402_Index1400)/sizeof(master402_Index1400[0]), 0x1400},
{ (subindex*)master402_Index1401,sizeof(master402_Index1401)/sizeof(master402_Index1401[0]), 0x1401},
{ (subindex*)master402_Index1402,sizeof(master402_Index1402)/sizeof(master402_Index1402[0]), 0x1402},
{ (subindex*)master402_Index1403,sizeof(master402_Index1403)/sizeof(master402_Index1403[0]), 0x1403},
{ (subindex*)master402_Index1600,sizeof(master402_Index1600)/sizeof(master402_Index1600[0]), 0x1600},
{ (subindex*)master402_Index1601,sizeof(master402_Index1601)/sizeof(master402_Index1601[0]), 0x1601},
{ (subindex*)master402_Index1602,sizeof(master402_Index1602)/sizeof(master402_Index1602[0]), 0x1602},
{ (subindex*)master402_Index1603,sizeof(master402_Index1603)/sizeof(master402_Index1603[0]), 0x1603},
{ (subindex*)master402_Index1800,sizeof(master402_Index1800)/sizeof(master402_Index1800[0]), 0x1800},
{ (subindex*)master402_Index1801,sizeof(master402_Index1801)/sizeof(master402_Index1801[0]), 0x1801},
{ (subindex*)master402_Index1802,sizeof(master402_Index1802)/sizeof(master402_Index1802[0]), 0x1802},
{ (subindex*)master402_Index1803,sizeof(master402_Index1803)/sizeof(master402_Index1803[0]), 0x1803},
{ (subindex*)master402_Index1A00,sizeof(master402_Index1A00)/sizeof(master402_Index1A00[0]), 0x1A00},
{ (subindex*)master402_Index1A01,sizeof(master402_Index1A01)/sizeof(master402_Index1A01[0]), 0x1A01},
{ (subindex*)master402_Index1A02,sizeof(master402_Index1A02)/sizeof(master402_Index1A02[0]), 0x1A02},
{ (subindex*)master402_Index1A03,sizeof(master402_Index1A03)/sizeof(master402_Index1A03[0]), 0x1A03},
{ (subindex*)master402_Index2001,sizeof(master402_Index2001)/sizeof(master402_Index2001[0]), 0x2001},
{ (subindex*)master402_Index2002,sizeof(master402_Index2002)/sizeof(master402_Index2002[0]), 0x2002},
{ (subindex*)master402_Index2003,sizeof(master402_Index2003)/sizeof(master402_Index2003[0]), 0x2003},
{ (subindex*)master402_Index2004,sizeof(master402_Index2004)/sizeof(master402_Index2004[0]), 0x2004},
{ (subindex*)master402_Index2005,sizeof(master402_Index2005)/sizeof(master402_Index2005[0]), 0x2005},
{ (subindex*)master402_Index2006,sizeof(master402_Index2006)/sizeof(master402_Index2006[0]), 0x2006},
{ (subindex*)master402_Index2007,sizeof(master402_Index2007)/sizeof(master402_Index2007[0]), 0x2007},
{ (subindex*)master402_Index2124,sizeof(master402_Index2124)/sizeof(master402_Index2124[0]), 0x2124},
{ (subindex*)master402_Index2F00,sizeof(master402_Index2F00)/sizeof(master402_Index2F00[0]), 0x2F00},
{ (subindex*)master402_Index2F01,sizeof(master402_Index2F01)/sizeof(master402_Index2F01[0]), 0x2F01},
{ (subindex*)master402_Index6040,sizeof(master402_Index6040)/sizeof(master402_Index6040[0]), 0x6040},
{ (subindex*)master402_Index6041,sizeof(master402_Index6041)/sizeof(master402_Index6041[0]), 0x6041},
{ (subindex*)master402_Index6060,sizeof(master402_Index6060)/sizeof(master402_Index6060[0]), 0x6060},
{ (subindex*)master402_Index6064,sizeof(master402_Index6064)/sizeof(master402_Index6064[0]), 0x6064},
{ (subindex*)master402_Index606C,sizeof(master402_Index606C)/sizeof(master402_Index606C[0]), 0x606C},
{ (subindex*)master402_Index607A,sizeof(master402_Index607A)/sizeof(master402_Index607A[0]), 0x607A},
{ (subindex*)master402_Index607C,sizeof(master402_Index607C)/sizeof(master402_Index607C[0]), 0x607C},
{ (subindex*)master402_Index6081,sizeof(master402_Index6081)/sizeof(master402_Index6081[0]), 0x6081},
{ (subindex*)master402_Index6098,sizeof(master402_Index6098)/sizeof(master402_Index6098[0]), 0x6098},
{ (subindex*)master402_Index6099,sizeof(master402_Index6099)/sizeof(master402_Index6099[0]), 0x6099},
{ (subindex*)master402_Index60C1,sizeof(master402_Index60C1)/sizeof(master402_Index60C1[0]), 0x60C1},
{ (subindex*)master402_Index60C2,sizeof(master402_Index60C2)/sizeof(master402_Index60C2[0]), 0x60C2},
{ (subindex*)master402_Index60FF,sizeof(master402_Index60FF)/sizeof(master402_Index60FF[0]), 0x60FF},
};
删减后的对象字典定义:
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},
{ (subindex*)master401_Index1005,sizeof(master401_Index1005)/sizeof(master401_Index1005[0]), 0x1005},
{ (subindex*)master401_Index1006,sizeof(master401_Index1006)/sizeof(master401_Index1006[0]), 0x1006},
{ (subindex*)master401_Index1014,sizeof(master401_Index1014)/sizeof(master401_Index1014[0]), 0x1014},
{ (subindex*)master401_Index1016,sizeof(master401_Index1016)/sizeof(master401_Index1016[0]), 0x1016},
{ (subindex*)master401_Index1017,sizeof(master401_Index1017)/sizeof(master401_Index1017[0]), 0x1017},
{ (subindex*)master401_Index1018,sizeof(master401_Index1018)/sizeof(master401_Index1018[0]), 0x1018},
{ (subindex*)master401_Index1200,sizeof(master401_Index1200)/sizeof(master401_Index1200[0]), 0x1200},
{ (subindex*)master401_Index1280,sizeof(master401_Index1280)/sizeof(master401_Index1280[0]), 0x1280},
{ (subindex*)master401_Index1400,sizeof(master401_Index1400)/sizeof(master401_Index1400[0]), 0x1400},
{ (subindex*)master401_Index1600,sizeof(master401_Index1600)/sizeof(master401_Index1600[0]), 0x1600},
{ (subindex*)master401_Index1800,sizeof(master401_Index1800)/sizeof(master401_Index1800[0]), 0x1800},
{ (subindex*)master401_Index1A00,sizeof(master401_Index1A00)/sizeof(master401_Index1A00[0]), 0x1A00},
{ (subindex*)master401_Index2000,sizeof(master401_Index2000)/sizeof(master401_Index2000[0]), 0x2000},
{ (subindex*)master401_Index2001,sizeof(master401_Index2001)/sizeof(master401_Index2001[0]), 0x2001},
};
相比原有代码,增加了DO和DI相关的对象字典的定义
/* -------------------------- 0x2000 本地DO输出缓存 -------------------------- */
// 子索引0:最高子索引编号(=1,因为有2个子索引:0和1)
// 子索引1:实际DO数据存储(uint16,RW)
UNS8 master401_highestSubIndex_obj2000 = 1; /* 最高子索引编号 = 子索引数量-1 */
uint16_t master401_obj2000_do_val = 0x0000; /* DO数据存储变量(关联g_em32dx_do)*/
subindex master401_Index2000[] =
{
// 子索引0:声明最高子索引编号(RO,不可写)
{ RO, uint8, sizeof (UNS8), (void*)&master401_highestSubIndex_obj2000, NULL },
// 子索引1:实际DO数据(RW,uint16)
{ RW, uint16, sizeof (uint16_t), (void*)&master401_obj2000_do_val, NULL }
};
/* -------------------------- 0x2001 本地DI输入缓存 -------------------------- */
// 子索引0:最高子索引编号(=1)
// 子索引1:实际DI数据存储(uint16,RO)
UNS8 master401_highestSubIndex_obj2001 = 1; /* 最高子索引编号 = 子索引数量-1 */
uint16_t master401_obj2001_di_val = 0x0000; /* DI数据存储变量(关联g_em32dx_di)*/
subindex master401_Index2001[] =
{
// 子索引0:声明最高子索引编号(RO,不可写)
{ RO, uint8, sizeof (UNS8), (void*)&master401_highestSubIndex_obj2001, NULL },
// 子索引1:实际DI数据(RO,uint16,协议栈自动更新)
{ RO, uint16, sizeof (uint16_t), (void*)&master401_obj2001_di_val, NULL }
};
需要特别注意的是,master401_od.c中定义的对象字典仅适用于主设备 —— 这是我初期的核心困惑点,曾误以为主设备无需额外定义对象字典。且主设备对象字典中0x1400、0x1800 索引的含义,与从设备对应索引的描述恰好相反:具体来说,主设备的 TPDO1(发送过程数据对象 1)对应从设备的 RPDO1(接收过程数据对象 1),而主设备的 RPDO1 则对应从设备的 TPDO1。
文件调整方面:已移除motor_control.c与motor_control.h文件,并将原文件中的相关 IO 操作整合至master401_canopen.c中;同时将原master402_canopen.c文件重命名为master401_canopen.c,且对文件内大部分核心代码进行了适配性修改。
从设备 IO 模块的对象字典配置,均在该文件中完成实现,具体代码如下:
/************************** 核心修改:IO模块PDO映射配置 **************************/
// 说明:
// - 从站(EM32DX-C4)接收DO输出:RPDO1(0x1400)映射DO0-DO15(2字节)
// - 从站(EM32DX-C4)发送DI输入:TPDO1(0x1800)映射DI0-DI15(2字节)
// - 复用原PDO通道,删除伺服相关映射
/* TPDO1配置(从站→主站:DI输入)*/
static UNS8 IO_DIS_SLAVE_TPDO1(uint8_t nodeId) {
rt_kprintf("config...0!\n");
UNS32 TPDO_COBId = PDO_DISANBLE(0x00000180, nodeId); // COB-ID: 0x182(IO_NODEID=2)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1800, 1, 4, uint32, &TPDO_COBId, config_node_param_cb, 0);
}
static UNS8 IO_Write_SLAVE_TPDO1_Type(uint8_t nodeId) {
rt_kprintf("config...1!\n");
UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步传输
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1800, 2, 1, uint8, &trans_type, config_node_param_cb, 0);
}
static UNS8 IO_Clear_SLAVE_TPDO1_Cnt(uint8_t nodeId) {
rt_kprintf("config...2!\n");
UNS8 pdo_map_cnt = 0; // 清除原有映射
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1A00, 0, 1, uint8, &pdo_map_cnt, config_node_param_cb, 0);
}
static UNS8 IO_Write_SLAVE_TPDO1_Map(uint8_t nodeId) {
rt_kprintf("config...3!\n");
// TPDO1映射:DI0-DI15(模块DI对应索引0x6100,子索引0x01,2字节)
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);
}
static UNS8 IO_Write_SLAVE_TPDO1_Cnt(uint8_t nodeId) {
rt_kprintf("config...4!\n");
UNS8 pdo_map_cnt = 1; // 1个映射项(2字节)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1A00, 0,1, uint8, &pdo_map_cnt, config_node_param_cb, 0);
}
static UNS8 IO_EN_SLAVE_TPDO1(uint8_t nodeId) {
rt_kprintf("config...5!\n");
UNS32 TPDO_COBId = PDO_ENANBLE(0x00000180, nodeId);
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1800, 1,4, uint32, &TPDO_COBId, config_node_param_cb, 0);
}
//-----------------------------------------------------------//
/* RPDO1配置(主站→从站:DO输出)*/
static UNS8 IO_DIS_SLAVE_RPDO1(uint8_t nodeId) {
rt_kprintf("config...6!\n");
UNS32 RPDO_COBId = PDO_DISANBLE(0x00000200, nodeId); // COB-ID: 0x202(IO_NODEID=2)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1400, 1,4, uint32, &RPDO_COBId, config_node_param_cb, 0);
}
static UNS8 IO_Write_SLAVE_RPDO1_Type(uint8_t nodeId) {
rt_kprintf("config...7!\n");
UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步传输
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1400, 2,1, uint8, &trans_type, config_node_param_cb, 0);
}
static UNS8 IO_Clear_SLAVE_RPDO1_Cnt(uint8_t nodeId) {
rt_kprintf("config...8!\n");
UNS8 pdo_map_cnt = 0; // 清除原有映射
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1600, 0,1, uint8, &pdo_map_cnt, config_node_param_cb, 0);
}
static UNS8 IO_Write_SLAVE_RPDO1_Map(uint8_t nodeId) {
rt_kprintf("config...9!\n");
// RPDO1映射:DO0-DO15(模块DO对应索引0x6300,子索引0x01,2字节)
UNS32 pdo_map_val = 0x63000110; // 索引0x6300 + 子索引0x01 + 16位长度(0x10)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1600, 1,4, uint32, &pdo_map_val, config_node_param_cb, 0);
}
static UNS8 IO_Write_SLAVE_RPDO1_Cnt(uint8_t nodeId) {
rt_kprintf("config...10!\n");
UNS8 pdo_map_cnt = 1; // 1个映射项(2字节)
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1600, 0,1, uint8, &pdo_map_cnt, config_node_param_cb, 0);
}
static UNS8 IO_EN_SLAVE_RPDO1(uint8_t nodeId) {
rt_kprintf("config...11!\n");
UNS32 RPDO_COBId = PDO_ENANBLE(0x00000200, nodeId);
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1400, 1,4, uint32, &RPDO_COBId, config_node_param_cb, 0);
}
//-----------------------------------------------------------//
/* 心跳配置(IO模块生产者心跳)*/
static UNS8 IO_Write_SLAVE_Heartbeat(uint8_t nodeId) {
rt_kprintf("config...12!\n");
UNS16 producer_heartbeat_time = PRODUCER_HEARTBEAT_TIME;
return writeNetworkDictCallBack(OD_Data, nodeId, 0x1017, 0,2, uint16, &producer_heartbeat_time, config_node_param_cb, 0);
}
/* 配置完成回调 */
static UNS8 IO_Config_Done(uint8_t nodeId) {
rt_kprintf("config...13!\n");
node_config_state *conf = &slave_conf;
rt_sem_release(&(conf->finish_sem));
return 0;
}
// IO模块配置函数指针数组(按顺序执行)
static UNS8 (*IOCFG_Operation[])(uint8_t nodeId) = {
// TPDO1(DI输入)配置(6步)
IO_DIS_SLAVE_TPDO1, // 步骤0:禁用TPDO1
IO_Write_SLAVE_TPDO1_Type, // 步骤1:写TPDO1传输类型
IO_Clear_SLAVE_TPDO1_Cnt, // 步骤2:清除TPDO1映射数
IO_Write_SLAVE_TPDO1_Map, // 步骤3:写TPDO1映射
IO_Write_SLAVE_TPDO1_Cnt, // 步骤4:设置TPDO1映射数
IO_EN_SLAVE_TPDO1, // 步骤5:启用TPDO1
// RPDO1(DO输出)配置(6步)
IO_DIS_SLAVE_RPDO1, // 步骤6:禁用RPDO1
IO_Write_SLAVE_RPDO1_Type, // 步骤7:写RPDO1传输类型
IO_Clear_SLAVE_RPDO1_Cnt, // 步骤8:清除RPDO1映射数
IO_Write_SLAVE_RPDO1_Map, // 步骤9:写RPDO1映射
IO_Write_SLAVE_RPDO1_Cnt, // 步骤10:设置RPDO1映射数
IO_EN_SLAVE_RPDO1, // 步骤11:启用RPDO1
// 心跳配置(1步)
IO_Write_SLAVE_Heartbeat, // 步骤12:写从站心跳
// 配置完成(1步)
IO_Config_Done, // 步骤13:释放信号量
};
原先DS301一些逻辑我们进行了保留。
并且新增了一些 IO操作接口函数,代码如下:
/************************** 新增IO操作API(上层调用) **************************/
/**
* @brief 设置EM32DX-C4的DO输出
* @param do_val: 16位DO值(bit0=DO0, bit15=DO15,1=导通,0=断开)
* @retval RT_EOK: 成功,-RT_ERROR: 失败
*/
rt_err_t em32dx_set_do(uint16_t do_val) {
if (*can_node[1].nmt_state != Operational) {
rt_kprintf("EM32DX-C4 not in Operational state!\n");
return -RT_ERROR;
}
// 更新全局缓存
g_em32dx_do = do_val;
// 通过RPDO1发送DO值
UNS32 size = 2;
UNS32 errorCode = writeLocalDict(OD_Data, 0x2000, 1, &do_val, &size, 0);
if (errorCode != OD_SUCCESSFUL) {
rt_kprintf("Write DO failed! Error code: 0x%08X\n", errorCode);
return -RT_ERROR;
}
return RT_EOK;
}
/**
* @brief 读取EM32DX-C4的DI输入
* @param di_val: 输出参数,存储16位DI值(bit0=DI0, bit15=DI15,1=导通,0=断开)
* @retval RT_EOK: 成功,-RT_ERROR: 失败
*/
rt_err_t em32dx_get_di() {
if (*can_node[1].nmt_state != Operational) {
rt_kprintf("EM32DX-C4 not ready!\n");
return -RT_ERROR;
}
// 从本地字典读取TPDO1接收的DI值
uint16_t di_val = 0;
UNS32 size = 2;
UNS8 data_type;
UNS32 errorCode = readLocalDict(OD_Data, 0x2001, 1, &di_val, &size, &data_type, 0);
if (errorCode != OD_SUCCESSFUL) {
rt_kprintf("Read DI failed! Error code: 0x%08X\n", errorCode);
return -RT_ERROR;
}
rt_kprintf("Read DI: 0x%04X\n", di_val);
// 更新全局缓存
g_em32dx_di = di_val;
return RT_EOK;
}
MSH_CMD_EXPORT(em32dx_get_di, Get EM32DX-C4 DI input);
/**
* @brief 单独控制某一路DO
* @param channel: DO通道(0-15)
* @param state: 0=断开,1=导通
* @retval RT_EOK: 成功,-RT_ERROR: 失败
*/
rt_err_t em32dx_set_do_channel(uint8_t argc, char **argv) {
if (argc < 2) {
rt_kprintf("em32dx_set_do_channel 1 1\n");
return -RT_ERROR;
}
uint8_t channel = atoi(argv[1]);
uint8_t state = atoi(argv[2]);
rt_kprintf("channel=%d state=%d\n",channel,state);
if (channel >= 16) {
rt_kprintf("DO channel out of range (0-15)!\n");
return -RT_ERROR;
}
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));
代码编译完成后,我们将其部署至睿擎派,具体操作步骤如下:
(1)执行 canopen_start 指令,完成 CANOpen 服务的初始化与启动;
(2)执行 em32dx_get_di 指令,获取 16 路开关量的当前状态;
(3)执行 em32dx_set_do_channel 1 1 指令,配置 16 路 DO 通道的输出状态。
其中第一个参数为通道索引(取值范围:0–15),第二个参数为输出状态(0 = 关闭,1 = 打开)。


上述指令执行完成后,我们可以观察到对应的 DO 通道状态指示灯,会同步呈现出预期的状态变化(与指令配置的输出状态一致)。

源代码下载链接:
链接: https://pan.baidu.com/s/1aZDxzb3NNhn3WRBA4OeN4w?pwd=w8au
提取码: w8au
附录:
(1)CANOpen DS301、DS302、DS401、DS402等全套协议下载:
(2)DS301协议中文版
https://files.cnblogs.com/files/winshton/301_v04020005_cn_v02_ro.pdf
https://winshton.gitbooks.io/canopen-ds301-cn/content/
CANOpen 与 EtherCAT 协议不仅是工业自动化领域的主流通信方案,在当下热门的具身智能领域也有着广泛应用。近期我基于睿擎派平台,对这两种协议展开了深入研究,本文将重点分享这段时间积累的 CANOpen 相关技术要点,以及代码编写与调试的具体实践过程。CANOpen 协议相对复杂,与我们常用的其他协议相比,其核心特征在于要求设备严格遵循特定状态机模式,且嵌套了三种核心通信模型 :主 / 从站模型、客户端 / 服务器模型、生产者 / 消费者模型,这也构成了其灵活适配多场景通信需求的基础。

浙公网安备 33010602011771号