前言

SDO是Service Data Object的缩写,中文叫服务数据对象,关键是这个服务二字,指的是读写服务,Client可以通过SDO读写Server里的对象字典(Object Dictionary,简称OD)

OD存在于Server中,用户在和Server通信之前,也会有一份相同的OD表,不然没法继续操作。如果用户想读写OD里的值,那么就可以使用SDO。

PS:要记住:用户是Client,CANOpen设备是Server;如果2个CANOpen设备间互相通信,那么发起通信的那个就是Client,这里统一使用Client指代


一 工作原理

SDO的收发有点像TCP,读写请求发送出去后必须要有一个来自Server的应答,如下图
在这里插入图片描述
如果是读,那么应答里会包含读取的数据,如果是写,那么应答里会包含写成功的标志。如果读写发生错误,Server这边会返回一个紧急报文,里面包含错误码。

Client一般都会设置个SDO超时时间,如果在规定时间内没有收到应答,Client这边就会报错。

PS:CANOpen文档里读叫upload,写叫download,感觉是从Server角度来看的


二 使用范围

只有当Server处于Pre-Operational和Operational状态下,Client才可以使用SDO去和Server通信,如下图,
在这里插入图片描述
一般来说CANOen设备启动后会自动进入Pre-Operational状态,所以可以直接使用SDO,如果设备特殊,那么就要看下该设备的详细资料。

关于状态切换,请查看讲NMT的那篇文章。


三 例子

下面讲例子,实战可以让理解更加深入,首先使用pythonCANOpen来创建2个文件:server.py和client.py,如下,

'''
server.py
'''
import signal
import canopen
import time


running = True

def sigint_handler(signum, frame):
    global running
    print('')
    running = False
    exit(0)

# 处理按键发送的信号,优雅的关闭程序
signal.signal(signal.SIGINT,  sigint_handler)
signal.signal(signal.SIGHUP,  sigint_handler)
signal.signal(signal.SIGTERM, sigint_handler)

# 创建一个网络用来表示CAN总线
network = canopen.Network()

# 连接到CAN总线
network.connect(bustype='socketcan', channel='vcan0')

# 创建slave节点,其id是6,对象字典为CANopenSocket.eds
node = network.create_node(6, 'CANopenSocket.eds')

# node向CAN总线上发送启动消息
node.nmt.send_command(0)

# node进入PRE-OPERATIONAL状态
node.nmt.state = 'PRE-OPERATIONAL'

# node发送心跳报文,每隔1s发送一次
node.nmt.start_heartbeat(1000) # 1000ms


# 循环, 持续睡眠
while running:
    time.sleep(0.6)

client.py如下,

'''
client.py
'''
import time
import canopen

# 创建一个网络用来表示CAN总线
network = canopen.Network()

# 添加slave节点,其id是6,对象字典为CANopenSocket.eds
node = canopen.RemoteNode(6, 'CANopenSocket.eds')
network.add_node(node)


# 连接到CAN总线
network.connect(bustype='socketcan', channel='vcan0')

# 读取Index为0x2341,Subindex是2的OD项
data = node.sdo[0x2341][2].raw
print('0x2341_02: 0x{:X}'.format(data))

time.sleep(2)

# 修改Index为0x2341,Subindex是2的OD项
node.sdo[0x2341][2].raw = 0x123

time.sleep(2)

# 再读一次验证一下
data = node.sdo[0x2341][2].raw
print('0x2341_02: 0x{:X}'.format(data))

PS:eds文件来自这个

然后创建vcan0,

$ sudo modprobe vcan
$ sudo ip link add dev vcan0 type vcan
$ sudo ip link set up vcan0

创建好之后开一个终端,然后使用candump进行观察,

candump vcan0

最后是运行,先运行server.py,

python3 server.py

接着运行client.py,

python3 client.py

client运行结束后,观察得到的CAN报文如下,有三组,注意数字都是16进制,
在这里插入图片描述

1. COB-ID分析

先看606和586,这2个是SDO报文的COB-ID,

  • 对于发送的SDO报文来说,其COB-ID是0x600+设备id,例子中Server的id是6,所以是0x606,由Client发出,表示发给id是6的设备
  • 对于返回的应答报文来说,其COB-ID是0x580+设备id,例子中Server的id是6,所以是0x586,由Server发出,表示该报文是由id为6的设备返回的

PS:0x600号0x580是专属于SDO的,且是固定的

2. 报文内容分析 — 读操作

在这里插入图片描述

报文内容的基本结构如下,发送和接收的报文结构一样,
在这里插入图片描述
总共8个字节,

  • 字节0是命令说明符,用来描述报文意义,简写为CS,占1个字节
  • 字节1-2是Server对象字典项的Index,占2个字节
  • 字节3是Server对象字典项的Subindex,占1个字节
  • 字节4-7是报文内容,占4个字节
命令提示符

例子中第一个SDO报文是读取字典项0x2341_02,读操作对应的CS字节如下,注意这是一个字节,
在这里插入图片描述
Client发送报文的CS值是0x40,只要是读,其CS都是0x40

Server返回报文的CS值则需要考虑被读字典项的数据类型,n, e, s的含义如下,
在这里插入图片描述
PS:n是number的首字母,e是expedited的首字母,s是size的首字母

例子中返回的CS是0x4B,转为二进制后得出:n值为10b,十进制数字为2,e=1b,s=1b,根据上述含义可以得出:

  • 字节8-n到字节7,即字节6-7不包含数据,那么有效数据在字节4-5,总共2个字节
  • e为1b表示这是expedited传输,意思是数据可以一次传输完成,可以想象只要数据字节数不大于4,那么都可以一次传输完成
  • s为1b,但是在expedited传输里s是没有意义的,只有在e=0时s才有意义

然后打开eds文件,找到0x2341_02对应的描述,如下,
在这里插入图片描述
其DataType是0x0006,根据官方文档,其对应的数据长度就是2个字节,和前面分析相符,
在这里插入图片描述

索引和子索引

字节1的值为0x41,字节2的值是0x23,从右往左读就是0x2341,即字典项的索引

字节3的值是0x02,也就是字典项的子索引

发送报文和返回报文在字节1~3写入的都是相同的索引和子索引

报文中的数据值

发送报文的最后四个字节都是0,因为是读,这4个字节都是无意义的,虽然无意义,但是还是要传输,所以就写0

返回报文里字节5的值为0x7F,字节4的值为0xFF,合起来就是0x7FFF,与EDS文件里看到的这个字典项对应的默认值相同

3. 报文内容分析 — 写操作

在这里插入图片描述

写操作的内容结构和读一样,
在这里插入图片描述

命令提示符

写操作的CS值如下,
在这里插入图片描述
可以看到发送报文的CS值和字典项的数据类型有关,n,e,s的含义和读操作相同,所以这里是2B

返回报文的CS值则是固定的0x60,表示写入OK

索引和子索引

与读操作相同,就是把0x2341_02写入到字节1~3里

报文中的数据值

由于是写,所以在发送报文的字节4~5里填入期望的目标值,这里是0x123,字节4是0x23,字节5是0x01,字节6和7则为0

在返回报文里字节4~7都是无意义的,虽然无意义,但是还是要传输,所以都写0

3. Segment传输

前面的读写操作都是expedited传输,即一个来回就能传输完成,要求数据长度<=4字节,但有的字典项长度是大于4字节的,这样一次就无法传输完成,此时就需要Segment传输,也叫段传输

在eds文件里,字典项0x1009的默认值超过了4,其数据类型是0x0009,即Visible String,
在这里插入图片描述
这里改下,把其DefaultValue改长一点,如下,总共32个字节,不包含尾巴的’\0’,
在这里插入图片描述
然后重启server.py,client.py内容改为如下,

'''
client.py
'''
import time
import canopen

# 创建一个网络用来表示CAN总线
network = canopen.Network()

# 添加slave节点,其id是6,对象字典为CANopenSocket.eds
node = canopen.RemoteNode(6, 'CANopenSocket.eds')
network.add_node(node)


# 连接到CAN总线
network.connect(bustype='socketcan', channel='vcan0')

# 读取Index为0x1009的OD项
data = node.sdo[0x1009].raw
print('0x1009: {}'.format(data))

运行client.py后观察到的报文如下,2道橘黄色的横线之间是一次收发,
在这里插入图片描述
可以看到总共出现了6次收发,需要逐个分析

读的发起

第一次收发是读的发起,其CS含义如下,
在这里插入图片描述
和之前分析expedited读是一样,只是返回报文里的n,e,s的值变了:n为00b,e为0b,s为1b,根据前面n,e,s的释义,可以得出以下结论:

  • n的值在此次报文里无效,因为e不为1
  • e为0表示这不是expedited传输,需要多次传输才能把数据传完
  • s为1表示返回报文里字节4~7的值是字典项的size,这里是0x20,即32,和前面修改后的长度一致;也可以理解为需要传输的字节数
读的数据传输

剩余的5次收发就是读的数据传输,其CS值定义如下,
在这里插入图片描述
t,n,c含义如下,
在这里插入图片描述
Client发送报文的CS值比较简单,只有个toggle bit在变化,所以其CS值在0x60和0x70间互相变化,第一次是0x60

Server返回的报文里,除了t,还有n和c,字面意义也比较好理解,要注意这里的n是3个bit,expedited传输里n是2个bit。

只要不是最后一次收发,那么n和c都为0,CS值在0x00和0x10间互相变化,第一次是0x00;每次能传输7个字节,4次收发传了28个字节;

第6次收发是最后一次,此时还剩32-28=4个字节,那么n就为3,即011b,c为1,即1b,组合在一起是0111b,另外此次的t是0b,那么最后一次返回的报文的CS就是0x07

写的发起和数据传输

写也是类似,分为写的发起和写的数据传输,分别对应下面2张图
在这里插入图片描述
在这里插入图片描述
由于这个eds文件里超过4字节的字典项都是const的,不可以修改,那么就需要改个其它项来做测试,这里修改0x2FF4_4,其原始内容如下,
在这里插入图片描述
改之后如下,只是改了DataType,
在这里插入图片描述
然后重启server.py,client.py内容改成如下,

'''
client.py
'''
import time
import canopen

# 创建一个网络用来表示CAN总线
network = canopen.Network()

# 添加slave节点,其id是6,对象字典为CANopenSocket.eds
node = canopen.RemoteNode(6, 'CANopenSocket.eds')
network.add_node(node)


# 连接到CAN总线
network.connect(bustype='socketcan', channel='vcan0')

# 修改0x2FF4_04的OD项
node.sdo[0x2FF4][4].raw = "AABBCCDDEEFFGGHH"


运行client.py后观察得到的can报文如下,
在这里插入图片描述
总共发生了4次收发,第一次是写的发起,通过CS值来告诉Server总共有16个字节要写;后面三次是数据传输,内容分析和读类似,只要注意CS的高3位不一样就行了。

4. Block传输

请查看这篇文章

5. Abort

Server和Client都可以发送abort报文来中断报文传输,其CS值定义如下,
在这里插入图片描述
即0x80


四 总结

本文讲述了SDO报文的含义和实践,通过例子+理论,可以更加深刻的理解SDO及其用途。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐