学习CANopen --- [7] 使用块(Block)下载

本文详细介绍了CANopen协议中Block传输机制的实现原理及其在Python canopen库中的应用。通过对比Segment传输,Block传输能够显著减少下载时间。文章提供了具体的Python代码示例,并通过实例分析了Block传输过程中的关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于一次传输超过4字节的情形,SDO可以使用Segment传输或者Block传输,Segment传输在第6篇文章中已经介绍,本文讲解Block传输中的下载情况。


一 与Segment传输的比较

相比于Segment传输,Block传输可以大幅缩短下载时间,因为其规定传输多个Segment之后才需要一次应答,而Segment传输需要对每个Segment都进行应答。

一个Block由一次传输的多个Segment组成,Block大小就是指一个Block里包含的Segment数量,最大值是127,最小值是1。注意:Block大小不是传输的字节数量,但是可以换算成字节数,即Block大小乘以7。


二 使canopen库支持Block下载

python的这个canopen库在SDO Server这边暂时不支持Block下载,在SDO Client端支持,本人仔细阅读源码后,发现可以通过对SDO Server的SDO回调函数进行修改,这样就能支持Block下载了。


源码

SDO Client源码如下,使用Block下载对0x100A进行修改,

import time, os
import canopen


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

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

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

FileSize = os.path.getsize('random.bin')
infile = open('random.bin', 'rb')

outfile = node.sdo.open(0x100A, mode='wb', block_transfer=True, size=FileSize)

data = infile.read()
outfile.write(data)

time.sleep(1)

infile.close()
outfile.close()

time.sleep(1)


network.disconnect()

代码里的random.bin是通过dd命令生成的,大小是128字节,生成命令如下,

time dd if=/dev/urandom of=./random.bin bs=1 count=128

重点是SDO Server源码,如下,这里把Block大小设置为4,即一次传输4*7=28个字节,

import signal
import canopen
import struct
from canopen.sdo.constants import *

running = True

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


class SDOBlockDownloadDealer(object):
    def __init__(self, network, tx_cobid, blockSize=64):
        self.network = network
        self.tx_cobid = tx_cobid

        self._index = None
        self._subindex = None

        self.blk_dnld_state = False
        self._blk_size = blockSize # default 64 seg per one block
        self._blk_dnld_seg_num = 0
        self._blk_dnld_received_seg_num = 0


    def send_response(self, response):
        self.network.send_message(self.tx_cobid, response)


    def abort(self, abort_code=0x08000000):
        """Abort current transfer."""
        data = struct.pack("<BHBL", RESPONSE_ABORTED,
                           self._index, self._subindex, abort_code)
        self.send_response(data)


    def block_download(self, data):
        if self.blk_dnld_state == False: # init block download
            cmd, index, subindex = SDO_STRUCT.unpack_from(data)
            if cmd & (REQUEST_BLOCK_DOWNLOAD | INITIATE_BLOCK_TRANSFER) > 0:
                self._index = index
                self._subindex = subindex
                
                self.blk_dnld_state  = True
                _, totalSize = struct.unpack_from("<LL", data)
                
                self._blk_dnld_seg_num = totalSize // 7
                if totalSize % 7:
                    self._blk_dnld_seg_num += 1
                # print("total seg num: {}".format(self._blk_dnld_seg_num))

                res_command = 0xA0
                response = bytearray(8)
                SDO_STRUCT.pack_into(response, 0, res_command, index, subindex)
                response[4] = self._blk_size
                self.send_response(response)

        else:
            command, = struct.unpack_from("B", data, 0)

            # download done
            if self._blk_dnld_received_seg_num == self._blk_dnld_seg_num:
                
                self.blk_dnld_state  = False # reset
                self._blk_dnld_received_seg_num = 0 # reset

                if command & (REQUEST_BLOCK_DOWNLOAD | END_BLOCK_TRANSFER):
                    response = bytearray(8)
                    response[0] = RESPONSE_BLOCK_DOWNLOAD | END_BLOCK_TRANSFER
                    self.send_response(response)

                else:
                    self.abort(0x05040001)

                return
            
            # dealing download segments
            moreSeg = command & NO_MORE_BLOCKS
            segNum = command & 0x7F

            if moreSeg:

                self._blk_dnld_received_seg_num += segNum

                response = bytearray(8)
                response[0] = RESPONSE_BLOCK_DOWNLOAD | BLOCK_TRANSFER_RESPONSE
                response[1] = segNum
                response[2] = self._blk_size
                self.send_response(response)

            elif segNum == self._blk_size:

                self._blk_dnld_received_seg_num += segNum

                response = bytearray(8)
                response[0] = RESPONSE_BLOCK_DOWNLOAD | BLOCK_TRANSFER_RESPONSE
                response[1] = self._blk_size
                response[2] = self._blk_size
                self.send_response(response)


def on_request_supportBlockDownload(can_id, data, timestamp):
    global node
    global sdoBlockDldDealer

    # 如果正在进行传输,又来数据,那么就是当前传输中新的一帧
    if sdoBlockDldDealer.blk_dnld_state:
        sdoBlockDldDealer.block_download(data)
        return

    # 解析client command specifier
    command, = struct.unpack_from('B', data, 0)
    ccs = command & 0xE0

    if ccs == REQUEST_BLOCK_DOWNLOAD: # 使用自定义的函数处理块下载的ccs
        sdoBlockDldDealer.block_download(data)
    else: # 使用canopen库自带的函数处理其它ccs
        node.sdo.on_request(can_id, data, timestamp)


if __name__ == "__main__":

    # 处理按键发送的信号,优雅的关闭程序
    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, 'test.eds')



    rx_cobid = node.sdo.rx_cobid
    network.unsubscribe(rx_cobid, node.sdo.on_request) # 取消默认回调
    network.subscribe(rx_cobid, on_request_supportBlockDownload) # 使用自定义的回调

    sdoBlockDldDealer = SDOBlockDownloadDealer(network, node.sdo.tx_cobid)


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

    # 把状态从INITIALISING设置为PRE-OPERATIONAL状态
    node.nmt.state = 'PRE-OPERATIONAL'

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


    # 循环
    while running:
        pass

代码是在运行时修改SDO的回调函数,不用修改库的源码。


三 测试及分析

先运行SDO Server,然后运行SDO Client,最后CAN报文如下,
在这里插入图片描述
这里先简要总结下传输情况:需要传输128个字节,SDO Server支持的Block大小是6,即6*7=42个字节

下面进行具体分析

1. 发起Block下载

绿框中的CAN报文是发起Block下载,首先分析其command specifier,即CS值,其定义如下图,

在这里插入图片描述
SDO Client这边发送的是0xC6,即cc和s都为1,cc用于表示Client这边是否支持传输数据的crc检查,为1表示支持,为0表示不支持;s表示是否指示要传输的总的字节数,为1就会指示,然后在这条报文中的最后4个字节里放入总的字节数,本例子要传输128个字节,就是0x80,与报文一致

SDO Server回应的是A0,即sc为0,sc用于表示Slave是否支持传输数据的crc检查,为1表示支持,为0表示不支持,这里不支持。

2. 下载Block数据

橙色框里正式下载数据,即Download Block Segment,其CS定义如下,每个橙色框是一次Block传输,
在这里插入图片描述

Block中每一次Segment的第一个字节都是CS,bit0~6组成的值表示该Segment的Sequence number,bit7表示本次Segment传输后是否还有更多Segment需要传输,0表示还有,1表示没有了。

CS之后的7个字节就是实际传输的数据

一次Block传输结束后会有一次Server的的应答,其值为0xA2,其后面跟着7个字节,其中第一个字节表示:该次Block传输最后一次Segment传输的Sequence number,如果为0则表示没有收到sequence 1;第二个字节表示下一次Block传输使用的Block size,即6

由于一个Block传输6*7=42字节,对于128个字节就要传输4个Block,前3个是完整的,最后一个Block只传输2个字节,其报文如下
在这里插入图片描述
Client的CS是0x81,即c为1,表示后续没有segment需要传输了,其sequence number是1
Server回应的字节里,CS依然为A2,和之前分析一致,后面数据的第一个字节是0x01,即该次Block传输最后一次的Segment的sequence number是1

3. 结束Block下载

最后的蓝色框是结束块下载,即End Block Download,其CS定义如下,
在这里插入图片描述
n表示上次Block中最后一次Segment中不包含数据的字节数,由于最后一次Segment只传输了2个字节,那么7个字节中有5个字节不包含数据,组合后就是1101 0101b,即0xD5,和实际报文一致

Server回应的值就是0xA1

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值