1. CANOpen——在ISO层级中位置和诞生
CAN(Controller Area Network)现场总线仅仅定义了第1层(物理层,见ISO11898-2标准)、第2层(数据链路层,见ISO11898-1标准),而在实际设计中,这两层完全由硬件实现,设计人员无需再为此开发相关软件(Software)或固件(Firmware),只要了解如何调用相关的接口和寄存器,即可完成对CAN的控制。
但CAN没有规定应用层。也就是没有规定与实际应用相关的逻辑,比如开关量输入输出,模拟量输入输出。所以本身对于应用来说,是不完整的。这就像铁矿石(物理层)冶炼成铁锭(数据链路层),然后针对具体应用,再加工做成汽车、轮船、钢筋、坦克、钢结构建筑等等。CANOPEN协议是基于CAN总线协议建立的应用层协议。
CANopen协议是在20世纪90年代末,由总部位于德国纽伦堡的CiA组织——CAN-in-Automation,(http://www.can-cia.org )在CAL(CAN Application Layer)的基础上发展而来。
由于CANopen协议的创始人团队也是CAN-bus的创始人团队,此协议充分发挥了CAN-bus所具备的所有优势,特别是CiA组织的主席蔡豪格(Holger Zeltwanger)先生对于CANopen协议坚持开放、免费、非盈利的原则。一经推出便在欧洲得到了广泛的认可与应用。时至今日已经成为全世界最为流行的CAN应用层协议。让我们记住这位可爱的德国老人。
2、CANOpen——协议帧
CANOPEN协议的基本通信单元叫做“通信对象”。常用的通信对象有NMT、SYNC、EMERGENCY、TIME STAMP、SDO、PDO这几个,他们结构相同,包括funciton Code、Node-ID、DLC(数据长度)、DATA(数据)四部分构成,本质上都是通过封装CAN总线协议的数据帧实现的。他们的不同体现在DATA这个部分,有的对象DATA部分可以完全用来传输数据,有的对象针对DATA部分进一步做了划分和要求。报文传输采用CAN标准帧格式。即11bit的ID域,以尽量减小传输时间;
3、CANOpen——对象
CANopen总线协议也分为两种基本的数据传输机制:通过进程数据对象(PDO)对小型的数据进行高速数据交换以及通过服务数据对象(SDO)对对象字典进行访问。后者主要用于在设备配置过程中传输参数以及传输大数据块。
4、CANOpen——网络管理NMT(Network management)
一个CANopen网络中为了保证可靠、可控,必须要NMT网络管理,就像一个军队一样,要令行禁止,才能达到稳定、高效的目标。如图 6.1所示。指挥员(NMT主机,CANOPEN主站)通过发号施令,士兵(NMT从机)进行自由俯卧训练,这样整个训练都是有序的。
所以每个CANopen从节点的CANopen协议栈中,必须具备NMT管理的相应代码,这是节点具备CANopen协议的最基本的要素。
5、CANOpen——NMT节点状态
6、CANOpen——NMT节点上线报文
任何一个CANopen从站上线后,为了提示主站它已经加入网络(便于热插拔),或者避免与其他从站Node-ID冲突。这个从站必须发出节点上线报文(boot-up),如图所示,节点上线报文的ID为700h+Node-ID,数据为1个字节0。生产者为CANopen从站。
7、CANOpen——NMT节点状态与心跳报文
为了监控CANopen节点是否在线与目前的节点状态。CANopen应用中通常都要求在线上电的从站定时发送状态报文(心跳报文),以便于主站确认从站是否故障、是否脱离网络。
如图所示,为心跳报文发送的格式,CANID与节点上线报文相同为700h+Node-ID,数据为1个字节,代表节点目前的状态,04h为停止状态,05h为操作状态,7Fh为预操作状态。
CANopen从站按其对象字典中1017h中填写的心跳生产时间(ms)进行心跳报文的发送,而CANopen主站(NMT主站)则会按其1016h中填写的心跳消费时间进行检查,假设超过诺干次心跳消费时间没有收到从站的心跳报文,则认为从站已经离线或者损坏。
8、CANOpen——NMT节点状态切换命令
NMT网络管理中,最核心的就是NMT节点状态切换命令,这是NMT主站所进行网络管理的“命令”报文。使用者必须牢记这些命令。
CANID均为000h,具备最高的CAN优先级。数据为2个字节:
第1个字节代表命令类型:
01h为启动命令(让节点进入操作状态);
02h为停止命令(让节点进入停止状态);
80h为进入预操作状态(让节点进入预操作状态);
81h为复位节点应用层(让节点的应用恢复初始状态,比如列车门都恢复打开状态);
82h为复位节点通讯(让节点的CAN和CANopen通讯重新初始化,一般用于总线收到干扰,导致节点总线错误被动,或者总线关闭时)。
第二个字节代表被控制的节点Node-ID,
如果要对整个网络所有节点同时进行控制,则这个数值为0即可。
9、CANOpen——PDO对象(Process data object)
PDO对象称为“过程数据对象”,用于无连接的数据传输,即A站发送数据给B站后,不需要等待B站给出确认收到的应答。当然B站也可以应答一些信息给A站,这个有点像网络通信中的UDP协议,即应答不是强制要求的,B站可以回答,也可以不回答。
PDO对象的DATA部分可以完全用来传输数据,没有进步做要求。一个PDO最大可传输8字节数据。
PDO分为TPDO(发送PDO)和(接收RPDO),发送和接收是以CANopen节点自身为参考(如果CAN主站或者其他从站就相反)。TPDO和RPDO分别有4个数据对象,每种数据对象就是1条CAN报文封装。
10、CANOpen——SDO对象(Service data object)
SDO称“服务数据对象”,主要用于CANopen主站对从节点的参数配置,是一个有应答的通信对象,即主站发送RSDO给从站后,从站如果接收到,必须发送TSDO给主站进行应答。
SDO的通讯原则非常单一,发送方(客户端)发送CAN-ID为600h+Node-ID的报文,其中Node-ID为接收方(服务器)的节点地址,数据长度均为8字节; 接收方(服务器)成功接收后,回应CAN-ID为580h+Node-ID的报文。这里的Node-ID依然是接收方(服务器)的节点地址,数据长度均为8字节。
11、CANOpen——快速SDO协议(Expedited SDO protocol)
最常用最常见的SDO协议是快速SDO,所谓快速,就是1次来回就搞定。前提是读取和写入的值不能大于32位。如图所示,为快速SDO协议的示意图。命令中直接包含了要读写的索引、子索引、数据。可谓直接命中。快速SDO的难点在于CS命令符的记忆。
通过快速SDO,可以直接对CANopen节点的对象字典中的值进行读取和修改,所以在做参数配置之外,也经常作为关键性数据传输之用。比如CANopen控制机器人的电机转动角度时,就使用SDO来传输,保证可靠到达。
12、CANOpen——特殊协议(Special protocols)
一.同步协议(Sync protocol)
同步(SYNC),该报文对象主要实现整个网络的同步传输。
在同步协议中,有2个约束条件:
同步窗口时间:索引1007h约束了同步帧发送后,从节点发送PDO的时效,即在这个时间内发送的PDO才有效,超过时间的PDO将被丢弃;
通讯循环周期:索引1006h规定了同步帧的循环周期。
二.时间戳协议(Time-stamp protocol)
时间标记对象(Time Stamp),NMT主机发送自身的时钟,为网络各个节点提供公共的时间参考,即网络对时。
三.紧急报文协议(Emergency protocol)
紧急事件对象(Emergency),是当设备内部发生错误,触发该对象,发送设备内部错误代码,提示NMT主站。紧急报文属于诊断性报文,一般不会影响CANopen通讯.
13、CANOpen——对象字典OD(Object dictionary)
CANopen对象字典(OD: Object Dictionary)是CANopen协议最为核心的概念。所谓的对象字典就是一个有序的对象组,描述了对应CANopen节点的所有参数,包括通讯数据的存放位置也列入其索引,这个表变成可以传递形式就叫做EDS文件(电子数据文档Electronic Data Sheet)
每个对象采用一个16位的索引值来寻址,这个索引值通常被称为索引,其范围在0x0000到0xFFFF之间。为了避免数据大量时无索引可分配,所以在某些索引下也定义了一个8 位的索引值,这个索引值通常被称为子索引,其范围是0x00到0xFF之间。 每个索引内具体的参数,最大用32位的变量来表示,即Unsigned32,四个字节。
14、CANOpen——通讯对象子协议区(Communication profile area)
通讯对象子协议区(Communication profile area)定义了所有和通信有关的对象参数,如表 5.2所示,标绿色底纹的索引范围1000h to 1029h为通用通讯对象,所有CANopen节点都必须具备这些索引,否则将无法加入CANopen网络。其他索引根据实际情况进行分配与定义。
15、CANOpen——通用通讯对象(General communication objects)
由于通用通讯对象十分重要,NMT主站(CANopen主站)在启动时,通常都全部或者部分读取所有从站中通用通讯对象中的索引,所以所有的通用通讯对象都必须在CANopen从站中实现,使用者也必须熟知这些索引地址与其含义。
16、CANOpen——子协议
一、制造商特定子协议(Manufacturer-specific Profile)
对象字典索引2000h to 5FFFh为制造商特定子协议,通常是存放所应用子协议的应用数据。而上文所描述的通讯对象子协议区(Communication profile area)是存放这些应用数据的通信参数。比如广州致远电子的XGate-COP10从站模块规定了:
RPDO的通讯参数存放在1400h to 15FFh ,映射参数存放在1600h to 17FFh ,数据存放为2000h 之后厂商自定义区;
TPDO的通讯参数存放在1800h to 19FFh ,映射参数存放在1A00h to 1BFFh ,数据存放为2000h 之后厂商自定义区。
对于在设备子协议中未定义的特殊功能,制造商也可以在此区域根据需求定义对象字典对象。因此这个区域对于不同的厂商来说,相同的对象字典项其定义不一定相同。
二. 标准化设备子协议(Standardized profile area)
标准化设备子协议,为各种行业不同类型的标准设备定义对象字典中的对象。目前已有十几种为不同类型的设备定义的子协议,例如 DS401、DS402、DS406等,其索引值范围为0x6000~0x9FFF。同样,这个区域对于不同的标准化设备子协议来说,相同的对象字典项其定义不一定相同。
17、一些为python调试代码
from ctypes import *
if 1:
dll = windll.LoadLibrary('./ControlCAN.dll')
nDeviceType = 21 #* USBCAN-2E-U *
nDeviceInd = 0#* 索引号0 *
nReserved = 0
nCANInd = 1 #can通道号
class _VCI_INIT_CONFIG(Structure):
_fields_ = [("AccCode", c_ulong), ("AccMask", c_ulong), ("Reserved", c_ulong), ("Filter", c_ubyte),
("Timing0", c_ubyte), ("Timing1", c_ubyte), ("Mode", c_ubyte)]
class _VCI_CAN_OBJ(Structure):
_fields_ = [("ID", c_uint), ("TimeStamp", c_uint), ("TimeFlag", c_ubyte), ("SendType", c_ubyte),
("RemoteFlag", c_ubyte), ("ExternFlag", c_ubyte), ("DataLen", c_ubyte), ("Data", c_ubyte*8),
("Reserved", c_ubyte*3)]
vic = _VCI_INIT_CONFIG()
vic.AccCode = 0x00000000
vic.AccMask = 0xffffffff
vic.Filter = 0
vic.Timing0 = 0x00
vic.Timing1 = 0x1c
vic.Mode = 0
vco = _VCI_CAN_OBJ()
vco.ID = 0x00000001
vco.SendType = 0
vco.RemoteFlag = 0
vco.ExternFlag = 0
vco.DataLen = 8
vco.Data = (0, 0, 0, 0, 0, 0, 0, 0)
vco2 = _VCI_CAN_OBJ()
#ubyte_array8 = c_ubyte*8
#ubyte_array3 = c_ubyte*3
vco2.ID = 0x00000001
vco2.SendType = 0
vco2.RemoteFlag = 0
vco2.ExternFlag = 0
vco2.DataLen = 8
vco2.Data = (0, 0, 0, 0, 0, 0, 0, 0)
ret = dll.VCI_OpenDevice(nDeviceType, nDeviceInd, nReserved)
print("opendevice:",ret)
ret = dll.VCI_SetReference(nDeviceType, nDeviceInd, nCANInd, 0, pointer(c_int(0x060007)))
print("setrefernce1:",ret)
ret = dll.VCI_SetReference(nDeviceType, nDeviceInd, 0, 0, pointer(c_int(0x060007)))
print("setrefernce0:",ret) #注意,SetRefernce必须在InitCan之前
ret = dll.VCI_InitCAN(nDeviceType, nDeviceInd, nCANInd, pointer(vic))
print("initcan1:",ret)
ret = dll.VCI_InitCAN(nDeviceType, nDeviceInd, 0, pointer(vic))
print("initcan0:",ret)
ret = dll.VCI_StartCAN(nDeviceType, nDeviceInd, nCANInd)
print("startcan1:",ret)
ret = dll.VCI_StartCAN(nDeviceType, nDeviceInd, 0)
print("startcan0:",ret)
rx_n = 0
while 1:
ret = dll.VCI_Receive(nDeviceType, nDeviceInd, 0, pointer(vco2), 1, -1)# 发送1帧
if ret > 0:
rx_n +=1
show = 'RXD n: {}\t ID: {}\t len: {}\t data:{}'.format(rx_n,hex(vco2.ID), vco2.DataLen, [hex(i) for i in list(vco2.Data[0:vco2.DataLen])])
print(show)
# ret = dll.VCI_Transmit(nDeviceType, nDeviceInd, 0, pointer(vco2), 1) # 发送两帧
# print("transmit:", ret)
# ret = 0
if vco2.ID&0xf80 == 0x600 and vco2.DataLen==8 and (vco2.Data[0]==0x23 or vco2.Data[0] == 0x2f or vco2.Data[0] == 0x2b or vco2.Data[0] == 0x27):
vco.ID = 0x580+vco2.ID&0x07f
Data = [0,0,0,0,0,0,0,0]
Data[0] = 0x60
Data[1] = vco2.Data[1]
Data[2] = vco2.Data[2]
Data[3] = vco2.Data[3]
vco.Data = tuple(Data)
show = 'TXD n: {}\t ID: {}\t len: {}\t data:{}'.format(rx_n, hex(vco.ID), vco.DataLen,[hex(i) for i in list(vco.Data[0:vco.DataLen])])
print(show)
ret = dll.VCI_Transmit(nDeviceType, nDeviceInd, 0, pointer(vco), 1) # 发送两帧
print("transmit:", ret)
from ctypes import *
dll = windll.LoadLibrary('./ControlCAN.dll')
nDeviceType = 21 #* USBCAN-2E-U *
nDeviceInd = 0#* 索引号0 *
nReserved = 0
nCANInd = 1 #can通道号
class _VCI_INIT_CONFIG(Structure):
_fields_ = [("AccCode", c_ulong), ("AccMask", c_ulong), ("Reserved", c_ulong), ("Filter", c_ubyte),
("Timing0", c_ubyte), ("Timing1", c_ubyte), ("Mode", c_ubyte)]
class _VCI_CAN_OBJ(Structure):
_fields_ = [("ID", c_uint), ("TimeStamp", c_uint), ("TimeFlag", c_ubyte), ("SendType", c_ubyte),
("RemoteFlag", c_ubyte), ("ExternFlag", c_ubyte), ("DataLen", c_ubyte), ("Data", c_ubyte*8),
("Reserved", c_ubyte*3)]
#ubyte_array8 = c_ubyte*8
#ubyte_array3 = c_ubyte*3
vic = _VCI_INIT_CONFIG()
vic.AccCode = 0x00000000
vic.AccMask = 0xffffffff
vic.Filter = 0
vic.Timing0 = 0x00
vic.Timing1 = 0x1c
vic.Mode = 0
vco = _VCI_CAN_OBJ()
vco.ID = 0x00000001
vco.SendType = 0
vco.RemoteFlag = 0
vco.ExternFlag = 0
vco.DataLen = 8
vco.Data = (1, 2, 3, 4, 5, 6, 7, 8)
ret = dll.VCI_OpenDevice(nDeviceType, nDeviceInd, nReserved)
print("opendevice:",ret)
ret = dll.VCI_SetReference(nDeviceType, nDeviceInd, nCANInd, 0, pointer(c_int(0x060007)))
print("setrefernce0:",ret) #注意,SetRefernce必须在InitCan之前
ret = dll.VCI_InitCAN(nDeviceType, nDeviceInd, nCANInd, pointer(vic))
print("initcan0:",ret)
ret = dll.VCI_StartCAN(nDeviceType, nDeviceInd, nCANInd)
print("startcan0:",ret)
while 1:
# print("transmit:",ret)
ret = dll.VCI_Receive(nDeviceType, nDeviceInd, nCANInd, pointer(vco), 1, -1)#
if ret > 0:
show = 'ID:{},len:{},data:{}'.format(hex(vco.ID),vco.DataLen,vco.Data)
print(show)
ret = dll.VCI_Transmit(nDeviceType, nDeviceInd, nCANInd, pointer(vco), 1) #