【睿擎派】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" 命令触发,常用于安全暂停

 001

注:对伺服运动设备(DS402),还额外定义了电源相关状态,比如电源禁用区,电源启用区,故障区等相关定义。

 

CANOpen 协议的四大核心状态中,嵌套了三种核心通信模型,具体如下:

(1)       主 / 从站模型:与 Modbus 协议逻辑类似,核心差异是支持多主机架构,最多可接入 127 个从站,主要用于网络诊断和设备状态管理。

(2)       客户端 / 服务器模型:借鉴 TCP/IP 协议的交互模式,专门适配对象字典(OD)的读写操作。从设备作为服务器提供参数访问服务,主设备作为客户端发起读写请求。

(3)       生产者 / 消费者模型:与 MQTT 协议的通信逻辑一致,从设备担任数据生产者,主设备担任数据消费者。生产者可按主设备的明确请求(拉模型),或主动无请求推送(推模型),传输预设的目标数据。

 

了解了以上知识后,还需要了解CANOpen协议的如下相关概念

(1)      对象字典

    对象字典的功能类似 Modbus 协议中的寄存器,是定义 CANOpen 节点所有行为、参数及通信规则的核心。与 Modbus 寄存器不同,它通过 “16 位索引 + 8 位子索引” 的双标识方式,唯一确定每个条目。

对象字典分为公共对象字典和私有对象字典。其中 DS301 协议作为 CANOpen 的基础通用协议,明确了所有 CANOpen 设备必须遵循的公共对象字典规范。

 002

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

 003

   设备参数如下:

004

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

005

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

006

(2)      COB-ID

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

 007

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

 008

(3)      网络管理(NMT

NMT服务用于通过NMT命令(如:启动、停止、复位)来控制CANopen设备的状态(如:预运行、运行、停止)。

为了改变状态,NMT主机发送一个带有 CAN ID 的2字节消息(即功能代码和节点ID )。所有从站节点都处理这个报文。节点ID 0表示广播命令。

 009

功能代码如下:

010

(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主站)在设定时限内未收到该消息,将触发预设的离线响应机制。

 011

(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(紧急对象):错误报告和故障通知机制

 012

我是在官方示例06_bus_canopen_master_motor的基础上进行大幅度修改而完成。除了canopen_callback.*相关内容没有多少变化外,其他文件改动比较大。

讲解代码之前,先简单说一下硬件接线。查EM32DX-C4手册,CANOpen接口采用了以太网的接口,管脚定义如下:

 013

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

014

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

 015

   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 = 打开)。

 016

017

 

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

018

源代码下载链接:

链接: 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/

 

内容概要:本文介绍了基于贝叶斯优化的CNN-LSTM混合神经网络在时间序列预测中的应用,并提供了完整的Matlab代码实现。该模型结合了卷积神经网络(CNN)在特征提取方面的优势与长短期记忆网络(LSTM)在处理时序依赖问题上的强大能力,形成一种高效的混合预测架构。通过贝叶斯优化算法自动调参,提升了模型的预测精度与泛化能力,适用于风电、光伏、负荷、交通流等多种复杂非线性系统的预测任务。文中还展示了模型训练流程、参数优化机制及实际预测效果分析,突出其在科研与工程应用中的实用性。; 适合人群:具备一定机器学习基基于贝叶斯优化CNN-LSTM混合神经网络预测(Matlab代码实现)础和Matlab编程经验的高校研究生、科研人员及从事预测建模的工程技术人员,尤其适合关注深度学习与智能优化算法结合应用的研究者。; 使用场景及目标:①解决各类时间序列预测问题,如能源出力预测、电力负荷预测、环境数据预测等;②学习如何将CNN-LSTM模型与贝叶斯优化相结合,提升模型性能;③掌握Matlab环境下深度学习模型搭建与超参数自动优化的技术路线。; 阅读建议:建议读者结合提供的Matlab代码进行实践操作,重点关注贝叶斯优化模块与混合神经网络结构的设计逻辑,通过调整数据集和参数加深对模型工作机制的理解,同时可将其框架迁移至其他预测场景中验证效果。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值