CANOpen总线之IO模块读写(DS401协议)
瑞清派采用Rockchip RK3506作为主控芯片,底层搭载RT-Thread操作系统。它基于专为工业场景打造的瑞庆工业平台而开发。该平台是全栈自主可控一体化软硬件解决方案,集成了数据采集、通信、控制、工业协议、AI、显示六大核心功能,精准满足工业应用需求。
官方仅提供了基于CANOpen协议(即DS402设备规范)操作伺服电机的示例代码。目前还没有IO模块相关的操作参考文档和实际案例。经过几天的深入研究和反复调试,终于成功实现了乐赛EM32DX-C4模块的IO信号采集和输出控制功能。
下面将简单分享这段时间积累的CANOpen相关技术点,以及代码编写和调试的具体实践过程。
一、CANOpen背景知识介绍CAN总线于1986年2月正式发布,CANOpen协议于1994年11月推出。CANOpen作为基于CAN总线的工业级通信协议,遵循EN 50325-4标准,是工业自动化领域主流的现场总线解决方案之一。
其核心优势体现在标准化设计上。 ——通过统一的对象字典保证设备之间的互操作性,支持PDO(过程数据对象)、SDO(服务数据对象)等灵活的通信机制,并兼顾实时性能和数据完整性。该协议内置了DS401(IO模块)、DS402(运动控制)等特殊配置文件,可适配伺服电机、IO模块、传感器等各种工业设备。
我们自产的网关产品很早就集成了CAN总线功能,但仅用于与我们自己的IO模块进行实时通信,很少与第三方模块进行接口。目前国内主流的智能模块仍然基于RS485通信。虽然了解CANOpen协议很久了,但是实际应用的机会并不多。随着瑞思德德瑞清工业平台计划开发新一代网关产品,各种现场工业总线都需要深入研究。
CANOpen协议相对复杂。核心原因是它要求设备遵循严格的状态机模型,主要包括四个核心状态:
(1) 初始化状态(Initialization)设备刚刚上电,正在进行硬件自检和协议栈初始化。
不参与网络通信,不接收任何命令(基本复位除外)
完成后自动进入预操作状态,并发送启动心跳信号。
(2) 预操作状态(Pre-operationa)设备已初始化,正在等待配置。
可以接收SDO(服务数据对象)进行参数配置和诊断
PDO(过程数据对象)通信被禁用,无法进行正常数据交换
这是唯一可以修改对象字典的状态,适合设备参数配置。
(3) 操作状态(Operational)设备处于正常工作状态,所有功能均已激活。
PDO和SDO通信均启用以发送和接收过程数据
设备自主执行任务并响应网络请求
条目由NMT 主机发送“启动节点”命令触发
(4) 停止状态(Stopped)设备的安全状态、功能受到限制。
PDO通信完全禁用,仅允许NMT命令和心跳
设备保持配置状态,但不执行控制任务
由NMT主机发送“停止节点”命令触发,常用于安全暂停
注:对于伺服运动设备(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-IDCOB-ID实际上是一个11位的CAN ID。它由两部分组成。高4位为功能码,低7位为从机地址码,因此最多支持127个从机。
功能码与具体的通讯服务相关(如下图):
(3) 网络管理(NMT)NMT服务用于通过NMT命令(如启动、停止、复位)控制CANopen设备的状态(如预运行、运行、停止)。
要更改状态,NMT 主站会发送带有CAN ID(即功能代码和节点ID)的2 字节消息。所有从节点都会处理该消息。节点ID 0代表广播命令。
功能代码如下:
(4) 服务数据对象(SDO)SDO(服务器数据对象)的核心功能是访问或修改CANopen设备对象字典中的参数值。例如,当应用主站需要调整CANopen设备的具体配置参数时,可以使用SDO服务完成参数的读写操作,实现设备配置的灵活改变。
(5) 过程数据对象(PDO)PDO(过程数据对象)是为设备之间实时、高速数据传输而设计的核心通信服务。是工业场景中过程数据交互的关键通道。
触发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:00起的天数(范围: 0-65535天,大约179.4年)。
二、CANOpen DS401协议实现官方示例(06_bus_canopen_master_motor)是基于免费开源的CanFestival(LGPLv2许可证)实现的。该开源代码实现了CANOpen协议的以下功能:
(1)NMT(网络管理):节点状态控制(初始化、预运行、运行、停止)和心跳监控
(2)PDO(Process Data Object):高速实时数据传输,支持循环和事件触发模式,优化工控场景的响应速度
(3)SDO(Service Data Object):对象字典参数访问,支持快速下载和分段下载,用于设备配置和参数调整
(4)SYNC(同步对象):网络时钟同步和周期性数据传输协调
(5)EMCY(Emergency Object):错误报告和故障通知机制
我在官方例子06_bus_canopen_master_motor的基础上做了重大修改。除了canopen_callback.*相关内容没有太大变化外,其他文件变化较大。
在解释代码之前,我们先简单说一下硬件接线。查看EM32DX-C4手册,CANOpen接口采用以太网接口,引脚定义如下:
根据这个定义,我做了一根CAN网络连接线,主要用了1和2两根线,对应的网线是1-白-橙和2-橙。白色和橙色是连接瑞清牌CAN_H接口的CAN_P,橙色是连接瑞清牌CAN_L接口的。
由于我对CANOpen协议的理解还不够深入,而且是第一次接触Serret EM32DX-C4硬件模块,所以在初期的调试工作中遇到了很多障碍。幸运的是,我手头正好有一个PCAN-USB模块。将其连接到CAN总线后,我通过PCAN-View工具实时监控CAN帧数据。这个操作直接显着提高了开发调试的效率(如下图)。
master402_od.c改名为master401_od.c主要是定义DS301和DS401对象字典的地方。原有的数据字典已经大幅缩减。
原有的对象字典定义:const 索引表master402_objdict[]={ { (子索引*)master402_Index1000,sizeof(master402_Index1000)/sizeof(master402_Index1000[0]),0x1000}, { (子索引*)master402_Index1001,sizeof(master402_Index1001)/sizeof(master402_Index1001[0]),0x1001}, { (子索引*)master402_Index1005,sizeof(master402_Index1005)/sizeof(master402_Index1005[0]),0x1005}, { (子索引*)master402_Index1006,sizeof(master402_Index1006)/sizeof(master402_Index1006[0]),0x1006}, { (子索引*)master402_Index1014,sizeof(master402_Index1014)/sizeof(master402_Index1014[0]),0x1014}, { (子索引*)master402_Index1016,sizeof(master402_Index1016)/sizeof(master402_Index1016[0]),0x1016}, { (子索引*)master402_Index1017,sizeof(master402_Index1017)/sizeof(master402_Index1017[0]),0x1017}, { (子索引*)master402_Index1018,sizeof(master402_Index1018)/sizeof(master402_Index1018[0]),0x1018}, { (子索引*)master402_Index1200,sizeof(master402_Index1200)/sizeof(master402_Index1200[0]),0x1200}, { (子索引*)master402_Index1280,sizeof(master402_Index1280)/sizeof(master402_Index1280[0]),0x1280}, { (子索引*)master402_Index1281,sizeof(master402_Index1281)/sizeof(master402_Index1281[0]),0x1281}, { (子索引*)master402_Index1400,sizeof(master402_Index1400)/sizeof(master402_Index1400[0]),0x1400}, { (子索引*)master402_Index1401,sizeof(master402_Index1401)/sizeof(master402_Index1401[0]),0x1401}, { (子索引*)master402_Index1402,sizeof(master402_Index1402)/sizeof(master402_Index1402[0]),0x1402}, { (子索引*)master402_Index1403,sizeof(master402_Index1403)/sizeof(master402_Index1403[0]),0x1403}, { (子索引*)master402_Index1600,sizeof(master402_Index1600)/sizeof(master402_Index1600[0]),0x1600}, { (子索引*)master402_Index1601,sizeof(master402_Index1601)/sizeof(master402_Index1601[0]),0x1601}, { (子索引*)master402_Index1602,sizeof(master402_Index1602)/sizeof(master402_Index1602[0]),0x1602}, { (子索引*)master402_Index1603,sizeof(master402_Index1603)/sizeof(master402_Index1603[0]),0x1603}, { (子索引*)master402_Index1800,sizeof(master402_Index1800)/sizeof(master402_Index1800[0]),0x1800}, { (子索引*)master402_Index1801,sizeof(master402_Index1801)/sizeof(master402_Index1801[0]),0x1801}, { (子索引*)master402_Index1802,sizeof(master402_Index1802)/sizeof(master402_Index1802[0]),0x1802}, { (子索引*)master402_Index1803,sizeof(master402_Index1803)/sizeof(master402_Index1803[0]),0x1803}, { (子索引*)master402_Index1A00,sizeof(master402_Index1A00)/sizeof(master402_Index1A00[0]),0x1A00}, { (子索引*)master402_Index1A01,sizeof(master402_Index1A01)/sizeof(master402_Index1A01[0]),0x1A01}, { (子索引*)master402_Index1A02,sizeof(master402_Index1A02)/sizeof(master402_Index1A02[0]),0x1A02}, { (子索引*)master402_Index1A03,sizeof(master402_Index1A03)/sizeof(master402_Index1A03[0]),0x1A03}, { (子索引*)master402_Index2001,sizeof(master402_Index2001)/sizeof(master402_Index2001[0]),0x2001}, { (子索引*)master402_Index2002,sizeof(master402_Index2002)/sizeof(master402_Index2002[0]),0x2002}, { (子索引*)master402_Index2003,sizeof(master402_Index2003)/sizeof(master402_Index2003[0]),0x2003}, { (子索引*)master402_Index2004,sizeof(master402_Index2004)/sizeof(master402_Index2004[0]),0x2004}, { (子索引*)master402_Index2005,sizeof(桅杆
er402_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_tmaster401_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_tmaster401_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输入)*/staticUNS8IO_DIS_SLAVE_TPDO1(uint8_tnodeId){ rt_kprintf("config...0!\n"); UNS32 TPDO_COBId =PDO_DISANBLE(0x00000180, nodeId); // COB-ID: 0x182(IO_NODEID=2) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,1,4, uint32, &TPDO_COBId, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Type(uint8_tnodeId){ rt_kprintf("config...1!\n"); UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步传输 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,2,1, uint8, &trans_type, config_node_param_cb,0);}staticUNS8IO_Clear_SLAVE_TPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...2!\n"); UNS8 pdo_map_cnt =0; // 清除原有映射 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Map(uint8_tnodeId){ rt_kprintf("config...3!\n"); // TPDO1映射:DI0-DI15(模块DI对应索引0x6100,子索引0x01,2字节) UNS32 pdo_map_val =0x61000110; // 索引0x6100 + 子索引0x01 + 16位长度(0x10) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,1,4, uint32, &pdo_map_val, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_TPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...4!\n"); UNS8 pdo_map_cnt =1; // 1个映射项(2字节) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1A00,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_EN_SLAVE_TPDO1(uint8_tnodeId){ rt_kprintf("config...5!\n"); UNS32 TPDO_COBId =PDO_ENANBLE(0x00000180, nodeId); returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1800,1,4, uint32, &TPDO_COBId, config_node_param_cb,0);}//-----------------------------------------------------------///* RPDO1配置(主站→从站:DO输出)*/staticUNS8IO_DIS_SLAVE_RPDO1(uint8_tnodeId){ rt_kprintf("config...6!\n"); UNS32 RPDO_COBId =PDO_DISANBLE(0x00000200, nodeId); // COB-ID: 0x202(IO_NODEID=2) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,1,4, uint32, &RPDO_COBId, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Type(uint8_tnodeId){ rt_kprintf("config...7!\n"); UNS8 trans_type = PDO_TRANSMISSION_TYPE; // 同步传输 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,2,1, uint8, &trans_type, config_node_param_cb,0);}staticUNS8IO_Clear_SLAVE_RPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...8!\n"); UNS8 pdo_map_cnt =0; // 清除原有映射 returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Map(uint8_tnodeId){ rt_kprintf("config...9!\n"); // RPDO1映射:DO0-DO15(模块DO对应索引0x6300,子索引0x01,2字节) UNS32 pdo_map_val =0x63000110; // 索引0x6300 + 子索引0x01 + 16位长度(0x10) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,1,4, uint32, &pdo_map_val, config_node_param_cb,0);}staticUNS8IO_Write_SLAVE_RPDO1_Cnt(uint8_tnodeId){ rt_kprintf("config...10!\n"); UNS8 pdo_map_cnt =1; // 1个映射项(2字节) returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1600,0,1, uint8, &pdo_map_cnt, config_node_param_cb,0);}staticUNS8IO_EN_SLAVE_RPDO1(uint8_tnodeId){ rt_kprintf("config...11!\n"); UNS32 RPDO_COBId =PDO_ENANBLE(0x00000200, nodeId); returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1400,1,4, uint32, &RPDO_COBId, config_node_param_cb,0);}//-----------------------------------------------------------///* 心跳配置(IO模块生产者心跳)*/staticUNS8IO_Write_SLAVE_Heartbeat(uint8_tnodeId){ rt_kprintf("config...12!\n"); UNS16 producer_heartbeat_time = PRODUCER_HEARTBEAT_TIME; returnwriteNetworkDictCallBack(OD_Data, nodeId,0x1017,0,2, uint16, &producer_heartbeat_time, config_node_param_cb,0);}/* 配置完成回调 */staticUNS8IO_Config_Done(uint8_tnodeId){ rt_kprintf("config...13!\n"); node_config_state *conf = &slave_conf; rt_sem_release(&(conf->finish_sem)); return0;}// IO模块配置函数指针数组(按顺序执行)staticUNS8(*IOCFG_Operation[])(uint8_tnodeId)= { // 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输出*@paramdo_val: 16位DO值(bit0=DO0, bit15=DO15,1=导通,0=断开)*@retvalRT_EOK: 成功,-RT_ERROR: 失败*/rt_err_tem32dx_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值 UNS32size=2; UNS32errorCode=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; } returnRT_EOK;}/***@brief读取EM32DX-C4的DI输入*@paramdi_val: 输出参数,存储16位DI值(bit0=DI0, bit15=DI15,1=导通,0=断开)*@retvalRT_EOK: 成功,-RT_ERROR: 失败*/rt_err_tem32dx_get_di(){ if(*can_node[1].nmt_state != Operational) { rt_kprintf("EM32DX-C4 not ready!\n"); return-RT_ERROR; } // 从本地字典读取TPDO1接收的DI值 uint16_tdi_val=0; UNS32size=2; UNS8 data_type; UNS32errorCode=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; returnRT_EOK;}MSH_CMD_EXPORT(em32dx_get_di, Get EM32DX-C4 DI input);/***@brief单独控制某一路DO*@paramchannel: DO通道(0-15)*@paramstate: 0=断开,1=导通*@retvalRT_EOK: 成功,-RT_ERROR: 失败*/rt_err_tem32dx_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等全套协议下载:
https://link.gitcode.com/i/614ed2a5064e1990bff8ffcde2328ada?uuid_tt_dd=10_19283516180-1733805088376-790686&isLogin=1&from_id=142936482
(2)DS301协议中文版
https://files.cnblogs.com/files/winshton/301_v04020005_cn_v02_ro.pdf
https://winshton.gitbooks.io/canopen-ds301-cn/content/
————————————————
版权声明:本文为RT-Thread论坛用户「yefanqiu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://club.rt-thread.org/ask/article/bb8a52de0882d43b.html