A014_CANOPEN手把手操作例程

                           源码测试

18岁码农__2024年6月28日

前言

  1. 顺序按实验时间编写的,无严格的逻辑关系。
  2. 内容基于自己的理解,难免有错误,仅供参考。
  3. 随着对CANOPEN协议理解的深入,内容在不断的更新、更正。
  4. CAN盒用的ZLG的USBCAN-II,主从站都是F103平台,源代码见资料。

1、单独主站上电

现象如下:

主站上线,发送两个字节报文:81 00.

表示复位所有节点应用层

NMT节点状态切换命,CANID为000,具有最高的CAN优先级,数据两个字节

然后主站发送同步报文,具体见下图解释。

2、从站上线

(1)主站工作中,从站上电

现象如下:

可以看出从站701,发出1个字节0,代表上线报文。

主站000发出2个字节01 01,代表让从站01进入操作状态

(2)单独从站上电时现象如下:

从站701先发出1个字节00,表示从站上线

然后1s发送一次心跳报文,内容7f,代表从站进入预操作状态了。

具体心跳周期设置解释见下面的心跳测试。

3、从站心跳测试

源码中:

/* 从站上报心跳包间隔时间,从站才需要配置 */

UNS16 ObjDict_obj1017 = 1000;

如果改成2000,心跳则为2ms。

将硬件的拨码开关设置:1高、2低、3低,则从站地址为4. 如果3个都高,则地址为7。

黑框:心跳时间,可以看出,从站1ms发送一次心跳。

绿框:从站的地址。拨码开关确定了从站的地址。

粉框:从站发送来的数据7f,表示Pre-Operational状态(节点初始化完成后,进入Pre-Operational状态)。

4、主站通过SDO修改数据

使用一次SDO报文,修改从站索引0x****子索引0x**地址中的数据。

(1)主站while循环中:   

 if(key_ctrol()==1)   //按下了S1

 {      

     if(one_flag_1)

     {

         writeNetworkDictCallBack(&ObjDict_Data,1,0x2000,0x01,1,int8,tt,SDOCallback_t_Index1800_Subindex0,0);          

         one_flag_1=0;

       }     

 }

主站中的配置中:

/SDO配置(客户端)/

/* 子索引个数 */

UNS8 ObjDict_highestSubIndex_obj1280 = 3;

/* 客户端->服务器用的COB-ID */

UNS32 ObjDict_obj1280_COB_ID_Client_to_Server_Transmit_SDO = 0x600 + 1; /* 1号节点 */

/* 服务器->客户端用的COB-ID */

UNS32 ObjDict_obj1280_COB_ID_Server_to_Client_Receive_SDO = 0x580 + 1; /* 1号节点 */

/* 对应服务器的节点号 */

UNS8 ObjDict_obj1280_Node_ID_of_the_SDO_Server = 0x1; /* 1号节点 */

从站由拨码开关将其地址设置为1,运行时,按下S1后,现象为:

红框:主站601发出2f 00 20 01 0a 00 00 00

由canopen手册知道,按下S1意思是向主索引2000,子索引01的从站写入1个字节的数据,数据内容为0a。

绿框:从站回复:60 00 20 01 00 00 00 00

表明发送SDO应答成功了。

从站中对象字典配置了索引2000子索引01,如下:

//发送PDO映射参数配置(客户端)/

/* 子索引数 */

UNS8 ObjDict_highestSubIndex_obj1A00 = 2;

/* bit0-7表示位数,bit8-15表示子索引,bit16-32表示索引 */

UNS32 ObjDict_obj1A00[] =

{

0x20000108, /* 索引为2000,子索引为1,位数为8位 */

0x20000210, /* 索引为2000,子索引为2,位数为16位 */

};

subindex ObjDict_Index1A00[] =

{

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1A00},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[0]},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[1]},

};

(2)如果将主站中按下S1的函数改为:writeNetworkDictCallBack(&ObjDict_Data,1,0x2000,0x02,2,int8,tt,SDOCallback_t_Index1800_Subindex0,0);           

则结果为:

由canopen手册知道,按下S1意思是向主索引2000,子索引02的从站写入2个字节的数据,数据内容为0a 01。

从站回复:60 00 20 02 00 00 00 00

表明发送SDO应答成功了。

(3)如果将主站中按下S1的函数改为:writeNetworkDictCallBack(&ObjDict_Data,1,0x2000,0x02,1,int8,tt,SDOCallback_t_Index1800_Subindex0,0);           

则结果为:

由canopen手册知道,按下S1意思是向主索引2000,子索引02的从站写入1个字节的数据,数据内容为0a。

从站回复:80 00 20 02 10 00 07 06

表明发送SDO应答失败。

10 00 07 06,根据ADO abort code error可查,代表数据长度不匹配。因为对象字典中索引2000,子索引02为两个字节的数据,但SDO发送时修改的是1个字节的。

(4)如果将主站中按下S1的函数改为:writeNetworkDictCallBack(&ObjDict_Data,1,0x1800,0x01,1,int8,tt,SDOCallback_t_Index1800_Subindex0,0);          

或者:

writeNetworkDictCallBack(&ObjDict_Data,1,0x1800,0x01,2,int8,tt,SDOCallback_t_Index1800_Subindex0,0);  

或者:

writeNetworkDictCallBack(&ObjDict_Data,1,0x1800,0x01,3,int8,tt,SDOCallback_t_Index1800_Subindex0,0);   

则结果为(只看回复的,发送的字节不一样,只贴1个字节的):

由canopen手册知道,按下S1意思是向主索引1800,子索引01的从站写入1、2、3个字节的数据。

从站回复:80 00 18 01 10 00 07 06

表明发送SDO应答失败,失败原因同上。

但是发送:

writeNetworkDictCallBack(&ObjDict_Data,1,0x1800,0x01,4,int8,tt,SDOCallback_t_Index1800_Subindex0,0);   

结果为:

由canopen手册知道,按下S1意思是向主索引1800,子索引01的从站写入4个字节的数据,数据内容为01 01 01 0a。

从站回复:60 00 18 01 00 00 00 00

表明发送SDO应答成功了。

(5)如果将主站中按下S1的函数改为:writeNetworkDictCallBack(&ObjDict_Data,1,0x1200,0x00,1,int8,tt,SDOCallback_t_Index1800_Subindex0,0);           

则结果为:

由canopen手册知道,按下S1意思是向主索引1200,子索引00的从站写入1个字节的数据,数据内容为0a。

从站回复:80 00 12 00 02 00 01 06

表明发送SDO应答失败了。由主站的配置代码:

subindex ObjDict_Index1200[] =

{

{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1200},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Client_to_Server_Transmit_SDO},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Server_to_Client_Receive_SDO},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1200_Node_ID_of_the_SDO_Server}

};

可知:主索引1200,子索引00是只读的,因此不能写入。

(6)如果将主站中按下S1的函数改为:writeNetworkDictCallBack(&ObjDict_Data,1,0x1200,0x03,1,int8,tt,SDOCallback_t_Index1800_Subindex0,0);           

因为uint8_t tt[1]={10};

因此发送SDO后,改变了从站节点号码,因此无法回复,导致回复了如图内容:

超时!

如果改为uint8_t tt[1]={01};

也就是说改节点号码仍为01,则SDO应答成功。

小结:客户端(主站)通过SDO要修改服务器(从站)的数据,首先主索引、子索引要匹配,修改的字节要一致,而且要是能写入的,非只读的。

5、主站通过SDO查询数据

(1)如果将主站中按下S2的函数改为:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1200, 0x00, int8, SDOCallback_t_Index1800_Subindex0, 0);

则结果为:

由canopen手册知道,按下S2意思是读取主索引1200,子索引00的数据

从站回复:4f 00 12 00 03 00 00 00

因为从站源代码中:

UNS8 ObjDict_highestSubIndex_obj1200 = 3;

/* 客户端->服务器用的COB-ID */

UNS32 ObjDict_obj1200_COB_ID_Client_to_Server_Transmit_SDO = 0x600 + 1; /* 1号节点 */

/* 服务器->客户端用的COB-ID */

UNS32 ObjDict_obj1200_COB_ID_Server_to_Client_Receive_SDO = 0x580 + 1; /* 1号节点 */

/* 对应服务器的节点号 */

UNS8 ObjDict_obj1200_Node_ID_of_the_SDO_Server = 0x1; /* 1号节点 */

subindex ObjDict_Index1200[] =

{

{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1200},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Client_to_Server_Transmit_SDO},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Server_to_Client_Receive_SDO},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1200_Node_ID_of_the_SDO_Server}

子索引00代表的是子索引个数,为3,因此查询的数据结果为03 00 00 00。

(2)如果将主站中按下S2的函数改为:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1200, 0x01, int8, SDOCallback_t_Index1800_Subindex0, 0);

则结果为:

子索引01代表的是客户端->服务器用的COB-ID,为601,因此查询结果为01 06 00 00。

(3)如果将主站中按下S2的函数改为:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1200, 0x02, int8, SDOCallback_t_Index1800_Subindex0, 0);

则结果为:

子索引01代表的是服务器->客户端用的COB-ID,为581,因此查询结果为81 05 00 00。

(4)如果将主站中按下S2的函数改为:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1A00, 0x01, int8, SDOCallback_t_Index1800_Subindex0, 0);

则结果为:

如果改为:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1A00, 0x02, int8, SDOCallback_t_Index1800_Subindex0, 0);

则结果为:

因为从站源码中:

/* 子索引数 */

UNS8 ObjDict_highestSubIndex_obj1A00 = 2;

/* bit0-7表示位数,bit8-15表示子索引,bit16-32表示索引 */

UNS32 ObjDict_obj1A00[] =

{

0x20000108, /* 索引为2000,子索引为1,位数为8位 */

0x20000210, /* 索引为2000,子索引为2,位数为16位 */

};

subindex ObjDict_Index1A00[] =

{

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1A00},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[0]},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[1]},

};

所以:

子索引01中数据为0x20000108,因此查询结果为08 01 00 20。

子索引02中数据为0x20000210,  因此查询结果为10 02 00 20。

6、SDO应用小结

有了上面的知识,我们举例说明SDO应用。

(1)读取参数

发送:601 40 01 65 00 00 00 00 00 (读取6501h/00h地址的数据,节点地址01

接收:581 43 01 65 00 A0 8C 00 00 (6501h/00h地址的参数为0x00008CA0,4个字节)

发送:601 40 00 20 00 00 00 00 00 (读取2000h/00h地址的数据,节点地址01

接收:581 4F 00 20 00 00 00 00 00 2000h/00h地址的参数为0x001个字节数据)

(2)写入参数

发送:601 23 01 65 00 A0 8C 00 00 (将6501h/00h地址参数设置为0x00008CA0,4个字节)

接收:581 60 01 65 00 00 00 00 00 (6501h/00h地址参数修改成功)

发送:601 2F 00 20 00 00 00 00 00 (将2000h/00h地址参数设置为0x001个字节)

接收:581 60 00 20 00 00 00 00 00 2000h/00h地址参数修改成功)

注意:参数修改后并没有直接保存,必须在Object 1010h:save parameters(保存参数)内进行“save”写入才能保存所修改的参数。在Sub-Index 1写入命令“save”(0x65766173h),参数将会存入存储器。

比如:

发送:601 23 10 10 01 73 61 76 65(将0x65766173h写入1010h/01h地址)

接收:581 60 10 10 01 00 00 00 00(1010h/01h地址的参数修改成功)

恢复默认参数时,也必须对Object 1011h:Restore parameters(恢复默认参数)内进行“load”修改参数并保存。在Sub-Index 1写入命令“load”(0x64616F6Ch),参数将会恢复成默认值并写入存储器。

比如:

发送:601 23 11 10 01 6C 6F 61 64(将0x64616F6Ch写入1011h/01h地址)

接收:581 60 11 10 01 00 00 00 00(1011h/01h地址的参数修改成功)。

实验:

首先修改uint8_t tt[4]={01 ,02,03 ,04};

修改按下S1函数:   writeNetworkDictCallBack(&ObjDict_Data,1,0x1a00,0x01,4,int8,tt,SDOCallback_t_Index1800_Subindex0,0);  

意思是将0x1a00,0x01 的4个字节修改为01 02 03 04.  

      

修改按下S2函数:

readNetworkDictCallback(&ObjDict_Data, 1, 0x1a00, 0x01, int8, SDOCallback_t_Index1800_Subindex0, 0);

读取0x1a00,0x01 的4个字节数据。

现象为:

23:写入4个字节;

00 1a 01:主索引1a00,从索引:01

60:写入成功!

40:读取数据

43:读取数据响应成功(4个字节)。

重新上下电后,直接读取数据,则读取的是主索引1a00,从索引01的原始数据,而不是01 02 03 04.

7、上位机发送SDO测试

(1)单独从站工作时,通过CanTest上位机向601节点发送23 00 1a 01 01 02 03 04。

结果如下:

应答成功。

2)主、从站都正常工作时,通过CanTest上位机向601节点发送23 00 1a 01 01 02 03 04。

结果如下:

应答成功后,从站又发送了80 00 00 00 21 00 00 08.表示因为是本地操作,导致数据无法被发送或存储到应用层。

具体原因不清楚……

8、修改从站心跳

将主站中的代码修改如下:

uint8_t tt[2]={0xD0,07};

 /* 修改从站心跳 */         writeNetworkDictCallBack(&ObjDict_Data,1,0x1017,0x00,2,int8,tt,SDOCallback_t_Index1800_Subindex0,0);

现象如下:

按下S1之前,从站1s一次心跳,报文05代表从站进入操作状态了。

按下S1之后,从站仍然1s一次心跳

但按下S1,应该是将索引1017的数据修改为7D0(2000)的,不知为何………………

这时,将主站复位下,现象如下:

然后,主站的心跳就变成2s了。

最后这样:正常上电,从站心跳默认1s一次,按下S1,修改心跳为2s,监测心跳仍然是1s,通过上位机发送81 00(复位节点),监测到的心跳变为2s

9、NMT命令

NMT是管理报文,用于实现一些管理操作,比如:节点重启、进入操作状态等等。

NMT报文格式很简单,ID为000,数据为1个字节的命令+1个字节的节点号(0表示广播)。

节点状态切换命令见下图所示。

下图中,通过NMT发送01 01.

序号第61个报文,就是让1号节点进入操作状态(01h)。

然后,1号节点(701),心跳报文变成了05,代表进入操作状态。

又比如:通过NMT发送02 01.

让1号节点进入停止状态(02h)。

然后,1号节点(701),心跳报文变成了04,代表进入停止状态(心跳存在,但是节点不工作了)。

又比如:通过NMT发送81 01.

让1号节点复位应用层(81h)。

复位后,1号节点(701)会首先进入Initializing状态(0x00),初始化完成后,进入PreOperational状态(0x7f)。

又比如:通过NMT发送81 01.

让1号节点复位应用层(81h)。

复位后,1号节点(701)会首先进入Initializing状态(0x00),初始化完成后,进入PreOperational状态(0x7f)。

10、读取从站心跳时间

    这里的操作都是通过上位机发送命令的,仅仅从站上电。

下面通过SDO查询从站的心跳时间。

发送命令为:40 17 10 00 00 00 00 00,这个就是一个SDO_Read报文(40h命令字为读取命令字),告诉从站,要读取OD索引1017,子索引00的数据(这个位置存放的是从站的心跳频率)。

从站回复:4b 17 10 00 e8 03 00 00,命令字4b表示读取到了2个字节数据,读取的结果0x3E8(1000ms)就是心跳的时间。

11、PDO应用实例

目的:

通过PDO,将从站的参数发送给主站

分析:

PDO 通信是基于生产者/消费者(Producer/Consumer)模型,主要用于传输实时数据。产生数据的节点将带有自己节点号的数据放到总线上,需要该数据的节点,可以配置为接收该 PDO。

过程:

对象字典的编写。

对象字典的结构和条目对于所有设备是共同的,本例中采用索引定位,子索引确定对象的思想构建对象字典,方法是使用结构体定义子索引,子索引结构体的成员变量包含对象的属性(读写权限,数据类型,数据长度等)和指向对象的指针,定义索引时包含指向子索引的指针和子索引数目,这样可以方便地通过索引和子索引找到对应的项,对象定义为指针的形式可以通过主站的 SDO 报文进行读写,实现对对象字典的灵活配置,同时这种方式实现通讯层与应用层共享数据变量的特点。

对从站对象字典的配置

  TPDO1  通讯参数  配置(客户端)    

/* 子索引数 */

UNS8 ObjDict_highestSubIndex_obj1800 = 5;

UNS32 ObjDict_obj1800_COB_ID_used_by_PDO = 0x181; /* PDO1 1号节点 */

UNS8 ObjDict_obj1800_Transmission_Type = 0x01;

UNS16 ObjDict_obj1800_Inhibit_Time = 1000; /* 0 0.1ms */

UNS8 ObjDict_obj1800_Compatibility_Entry = 0x0;

UNS16 ObjDict_obj1800_Event_Timer = 0;

subindex ObjDict_Index1800[] =

{

{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1800},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1800_COB_ID_used_by_PDO},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1800_Transmission_Type},

{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1800_Inhibit_Time},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1800_Compatibility_Entry},

{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1800_Event_Timer}

};

ODCallback_t ObjDict_Index1800_callbacks[] =

{

NULL,

NULL,

NULL,

NULL,

NULL,

NULL,

};

///    TPDO1 映射参数   配置(客户端)   /

UNS8 ObjDict_highestSubIndex_obj1A00 = 2;

UNS32 ObjDict_obj1A00[] =

{

0x20000108, /* 索引为2000,子索引为1,位数为8位 */

0x20000210, /* 索引为2000,子索引为2,位数为16位 */

};

subindex ObjDict_Index1A00[] =

{

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1A00},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[0]},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[1]},

};

/    TPDO1 应用数据 的索引配置(客户端)   /

UNS8 ObjDict_highestSubIndex_obj2000 = 2;

UNS8 ControlWordAxis1 = 100;

UNS16 ControlWordAxis2 = 1000;

subindex ObjDict_Index2000[] =                                                    

{

{RW, int8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj2000},

{RW, uint8, sizeof(UNS8), (void *)&ControlWordAxis1},

{RW, uint16, sizeof(UNS16), (void *)&ControlWordAxis2},

};

主站对象字典的配置

///    RPDO1  通讯参数  配置(客户端)   

UNS8 ObjDict_highestSubIndex_obj1400 = 5;

UNS32 ObjDict_obj1400_COB_ID_used_by_PDO = 0x181; /* PDO1 1号节点 */

UNS8 ObjDict_obj1400_Transmission_Type = 0x1;

UNS16 ObjDict_obj1400_Inhibit_Time = 0x0;   /* 0 单位10us */

UNS8 ObjDict_obj1400_Compatibility_Entry = 0x0;

UNS16 ObjDict_obj1400_Event_Timer = 0x0;

subindex ObjDict_Index1400[] =

{

{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1400},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1400_COB_ID_used_by_PDO},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1400_Transmission_Type},

{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1400_Inhibit_Time},

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1400_Compatibility_Entry},

{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1400_Event_Timer}

};

/    RPDO1  映射参数   配置(客户端)// // /

UNS8 ObjDict_highestSubIndex_obj1600 = 2;

UNS32 ObjDict_obj1600[] =

{

0x20000108, /* 索引为2000,子索引为1,位数为8位 */

0x20000210, /* 索引为2000,子索引为2,位数为16位 */

};

subindex ObjDict_Index1600[] =

{

{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1600},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1600[0]},

{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1600[1]},

};

///   RPDO1 应用数据 配置(客户端)    /// ///

UNS8 ObjDict_highestSubIndex_obj2000 = 2;

UNS8 ControlWordAxis1 = 0;

UNS16 ControlWordAxis2 = 0;

subindex ObjDict_Index2000[] =                                                    

{

{RW, int8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj2000},

{RW, uint8, sizeof(UNS8), (void *)&ControlWordAxis1},

{RW, uint16, sizeof(UNS16), (void *)&ControlWordAxis2},

};

根据上面的配置,可知UNS8 ObjDict_obj1800_Transmission_Type = 0x01;

即每收到一次同步报文,从站就发送PDO。

现象如下:

帧ID:080为主站发出的同步报文,从站收到该同步报文后,发送PDO。

帧ID:181,数据00 e8 03 就为从站发出的PDO报文。

因为UNS16 ControlWordAxis2 = 1000;(3e8)。更改此处的值,PDO的报文的第2、3字节也会相应改变,但是第一字节一直是00.

为什么没有发UNS8 ControlWordAxis1 = 100的内容?

解答:因为程序中变量ControlWordAxis1虽然赋初值100,但它的值由用户按键值确定,实验中用户按键一个都没按下,因此发送的内容为00.

验证情况:

按下用户按键,通过SDO读取这个变量的值,结果如下:

黑色为按下S3的结果;

红色为按下S2的结果;

黄色为按下S1的结果。

如果将UNS8 ObjDict_obj1800_Transmission_Type = 0x03;

即每收到3次同步报文,从站就发送PDO。

则现象如下:

12、利用SDO配置TPDO

在上述每收到3次同步报文,从站就发送PDO的基础上,通过使用SDO报文,修改TPDO。

要求:配置从节点1的TPDO 1800 在每5个SYNC传输。

应该发送的SDO(s)为:

601   23 00 18 01 81 01 00 00 //将主索引1800、子索引01处的值修改为181(COB_ID)

601   2F 00 18 02 05 00 00 00 //将TPDO的发送类型改为05

601   2F 00 1A 00 02 00 00 00 //定义2个数据的映射

601   23 00 1A 01 08 01 00 20 //定义在主索引2000、子索引01处寻找1个字节的数据

601   23 00 1A 02 10 02 00 20//定义在主索引2000、子索引02处寻找2个字节的数据

结果如图:

通过上位机一次性发送上述5个SDO,回复了4个成功,漏回复了最后一个,一次性发送4个,则回复4个成功,一个一个的发送SDO,每个都能回复成功。因此,怀疑是发送的太快,来不及回复导致漏了。

以上是主、从站都工作的情况,可以看出操作成功后,会出现80 00 00 00 21 00 00 08的报文。如果关掉主站电源,单独从站工作,则不会出现80 00 00 00 21 00 00 08的报文。

原因?

13、利用SDO配置RPDO

情况和上述的类似,因为程序中将主站配置为了RPDO,而且它还是SDO客户端,而不是服务器,因此无法实验。

这里贴一个例子,共参考。

/***************************************************************************************************/

创作不易,关注才有动力,源代码见主页其他文章

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 Pandas 库来实现该功能。假设 a 和 b 数据集都是 DataFrame,其中 a 包含 `order_id`、`customer_id` 和 `order_date` 三列数据,b 包含 `order_id`、`product_id` 和 `order_date` 三列数据,可以使用下面的代码实现过滤: ```python import pandas as pd # 构造 a 和 b 数据集 a = pd.DataFrame({ 'order_id': ['A001', 'A002', 'A003', 'A004', 'A005', 'A006', 'A007', 'A008', 'A009', 'A010', 'A011', 'A012', 'A013', 'A014', 'A015', 'A016', 'A017', 'A018', 'A019', 'A020'], 'customer_id': ['C001', 'C002', 'C003', 'C004', 'C005', 'C006', 'C007', 'C008', 'C009', 'C010', 'C011', 'C012', 'C013', 'C014', 'C015', 'C016', 'C017', 'C018', 'C019', 'C020'], 'order_date': ['2022-01-01', '2022-01-02', '2022-01-03', '2022-01-04', '2022-01-05', '2022-01-06', '2022-01-07', '2022-01-08', '2022-01-09', '2022-01-10', '2022-01-11', '2022-01-12', '2022-01-13', '2022-01-14', '2022-01-15', '2022-01-16', '2022-01-17', '2022-01-18', '2022-01-19', '2022-01-20'] }) b = pd.DataFrame({ 'order_id': ['B001', 'B002', 'B003', 'B004', 'B005', 'B006', 'B007', 'B008', 'B009', 'B010'], 'product_id': ['P001', 'P002', 'P003', 'P004', 'P005', 'P006', 'P007', 'P008', 'P009', 'P010'], 'order_date': ['2022-01-01', '2022-01-02', '2022-01-03', '2022-01-04', '2022-01-05', '2022-01-06', '2022-01-07', '2022-01-08', '2022-01-09', '2022-01-10'] }) # 过滤出 a 数据集中比 b 数据集多出来或者不同的行 result = pd.concat([a[['order_id']], b[['order_id']]]).drop_duplicates(keep=False) print(result) ``` 输出结果为: ``` order_id 0 A001 1 A002 2 A003 3 A004 4 A005 5 A006 6 A007 7 A008 8 A009 9 A010 10 A011 11 A012 12 A013 13 A014 14 A015 15 A016 16 A017 17 A018 18 A019 19 A020 ``` 该代码中,使用 `pd.concat([a[['order_id']], b[['order_id']]]).drop_duplicates(keep=False)` 语句过滤出 a 数据集中比 b 数据集多出来或者不同的行,即 a 的 order_id 列中的所有行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值