系列文章目录
前言
该软件包支持与 CANopen 节点网络交互。
注意 这里的大部分文档都是从 CANopen 维基百科页面上直接盗用的。
本文档正在编写中。欢迎反馈和修改!
CANopen 是用于自动化领域嵌入式系统的通信协议和设备配置文件规范。根据 OSI 模型,CANopen 实现了网络层以上(包括网络层)的各层。CANopen 标准包括一个寻址方案、几个小型通信协议和一个由设备配置文件定义的应用层。通信协议支持网络管理、设备监控和节点之间的通信,包括一个用于报文分段/解分段的简单传输层。
最简单的安装方法是使用 pip:
pip install canopen
- 网络和节点
- 对象字典
- 网络管理 (NMT)
- 服务数据对象 (SDO)
- 进程数据对象 (PDO)
- 同步对象 (SYNC)
- 紧急对象 (EMCY)
- 时间戳对象(TIME)
- 层设置服务 (LSS)
- 与现有代码集成
- 设备配置文件
一、网络和节点
canopen.Network 表示连接到同一条 CAN 总线的节点集合。它处理报文的发送和接收,并将报文发送到它所知道的节点。
每个节点都使用 canopen.RemoteNode 或 canopen.LocalNode 类来表示。它通常与一个对象字典相关联,每个服务都有自己的属性,由该节点所有。
1.1 示例
为每条 CAN 总线创建一个网络:
import canopen
network = canopen.Network()
默认情况下,该库使用 python-can 进行实际通信。有关如何配置特定接口的详细信息,请参阅其文档。
调用 connect() 方法启动通信,可选择提供传递给 can.BusABC 构造函数的参数:
network.connect(channel='can0', bustype='socketcan')
# network.connect(bustype='kvaser', channel=0, bitrate=250000)
# network.connect(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
# network.connect(bustype='ixxat', channel=0, bitrate=250000)
# network.connect(bustype='nican', channel='CAN0', bitrate=250000)
使用 add_node() 方法向网络中添加节点:
node = network.add_node(6, '/path/to/object_dictionary.eds')
local_node = canopen.LocalNode(1, '/path/to/master_dictionary.eds')
network.add_node(local_node)
节点也可以使用作为 Python 字典的网络对象来访问:
for node_id in network:
print(network[node_id])
要自动检测网络上存在哪些节点,可以使用扫描仪属性:
# This will attempt to read an SDO from nodes 1 - 127
network.scanner.search()
# We may need to wait a short while here to allow all nodes to respond
time.sleep(0.05)
for node_id in network.scanner.nodes:
print(f"Found node {node_id}!")
最后,确保完成后断开连接:
network.disconnect()
二、对象字典
CANopen 设备必须有一个对象字典,用于配置和与设备通信。对象字典中的条目定义如下
- 索引,对象在字典中的 16 位地址
- 对象类型,如数组、记录或简单变量
- 名称,描述条目的字符串
- 类型,给出变量的数据类型(或数组中所有变量的数据类型)
- 属性,提供该条目访问权限的信息,可以是读/写(rw)、只读(ro)或只写(wo)。
标准中定义了布尔值、整数和浮点数等对象字典值的基本数据类型,以及字符串、数组和记录等复合数据类型。复合数据类型可使用 8 位索引进行子索引;数组或记录的子索引 0 中的值表示数据结构中的元素个数,其类型为 UNSIGNED8。
2.1 支持的格式
目前支持用于指定节点对象字典的文件格式有
- EDS (类似 INI 文件的标准化格式)
- DCF(与 EDS 格式相同,但指定了比特率和节点 ID)
- EPF(Inmotion Technologies 使用的专有 XML 格式)
2.2 示例
对象字典文件通常在创建节点时提供。下面是一个打印出整个对象字典的示例:
node = network.add_node(6, 'od.eds')
for obj in node.object_dictionary.values():
print(f'0x{obj.index:X}: {obj.name}')
if isinstance(obj, canopen.objectdictionary.ODRecord):
for subobj in obj.values():
print(f' {subobj.subindex}: {subobj.name}')
您可以使用索引/子索引或名称访问对象:
device_name_obj = node.object_dictionary['ManufacturerDeviceName']
vendor_id_obj = node.object_dictionary[0x1018][1]
actual_speed = node.object_dictionary['ApplicationStatus.ActualSpeed']
command_all = node.object_dictionary['ApplicationCommands.CommandAll']
三、网络管理 (NMT)
NMT 协议用于发出状态机更改命令(如启动和停止设备)、检测远程设备启动和错误状况。
模块控制协议用于 NMT 主站改变设备状态。该协议的 CAN 帧 COB-ID 始终为 0,即功能代码为 0,节点 ID 为 0,这意味着网络中的每个节点都将处理该报文。命令的实际节点 ID 在报文的数据部分(第二个字节)给出。节点 ID 也可以是 0,这意味着总线上的所有设备都应进入指定状态。
心跳协议用于监控网络中的节点并验证它们是否存活。心跳生成器(通常是从属设备)会定期发送带有二进制功能代码 1110 及其节点 ID(COB-ID = 0x700 + 节点 ID)的报文。帧的数据部分包含一个表示节点状态的字节。心跳用户会读取这些报文。
CANopen 设备需要在启动过程中自动从 "初始化 "状态过渡到 "预运行 "状态。转换完成后,会向总线发送一条心跳报文。这就是启动协议。
3.1 示例
使用 canopen.Node.nmt 属性访问 NMT 功能。可使用 state 属性改变状态:
node.nmt.state = 'OPERATIONAL'
# Same as sending NMT start
node.nmt.send_command(0x1)
您还可以通过广播信息同时改变所有节点的状态:
network.nmt.state = 'OPERATIONAL'
如果节点发送心跳信息,状态属性就会自动更新为当前状态:
# Send NMT start to all nodes
network.send_message(0x0, [0x1, 0])
node.nmt.wait_for_heartbeat()
assert node.nmt.state == 'OPERATIONAL'
四、服务数据对象 (SDO)
SDO 协议用于设置和读取远程设备对象字典中的值。访问对象字典的设备是 SDO 服务器,访问远程设备的设备是 SDO 客户端。通信始终由 SDO 客户端发起。在 CANopen 术语中,通信是从 SDO 服务器开始的,因此从对象字典中读取数据是 SDO 上传,而写入字典条目则是 SDO 下载。
由于对象字典值可能大于 CAN 帧的 8 字节限制,因此 SDO 协议实现了对较长报文的分割和解分割。实际上,有两个这样的协议: SDO 下载/上传和 SDO 块下载/上传。SDO 数据块传输是新增加的标准,可以传输大量数据,协议开销略低。
客户机到服务器和服务器到客户机的各自 SDO 传输信息的 COB-ID 可在对象字典中设置。最多可在对象字典中设置 128 个 SDO 服务器,地址为 0x1200 - 0x127F。同样,设备的 SDO 客户端连接也可通过 0x1280 - 0x12FF 的变量进行配置。不过,预定义的连接集定义了一个 SDO 通道,即使在启动后(预运行状态)也可使用该通道配置设备。该通道的 COB-ID 为 0x600 + 节点 ID(用于接收)和 0x580 + 节点 ID(用于发送)。
4.1 示例
可以使用 .sdo 成员访问 SDO 对象,其工作方式类似于 Python 字典。索引可以用名称或编号来标识。有两种方法来标识子索引,一种是使用索引和子索引作为单独的参数,另一种是使用点来组合语法。下面的代码只创建对象,还没有发送或接收信息:
# Complex records
command_all = node.sdo['ApplicationCommands']['CommandAll']
command_all = node.sdo['ApplicationCommands.CommandAll']
actual_speed = node.sdo['ApplicationStatus']['ActualSpeed']
control_mode = node.sdo['ApplicationSetupParameters']['RequestedControlMode']
# Simple variables
device_type = node.sdo[0x1000]
# Arrays
error_log = node.sdo[0x1003]
要实际读取或写入变量,请使用 .raw、.phys、.desc 或 .bits 属性:
print(f"The device type is 0x{device_type.raw:X}")
# Using value descriptions instead of integers (if supported by OD)
control_mode.desc = 'Speed Mode'
# Set individual bit
command_all.bits[3] = 1
# Read and write physical values scaled by a factor (if supported by OD)
print(f"The actual speed is {actual_speed.phys} rpm")
# Iterate over arrays or records
for error in error_log.values():
print(f"Error 0x{error.raw:X} was found in the log")
也可以读写不在对象字典中的变量,但只能使用原始字节:
device_type_data = node.sdo.upload(0x1000, 0)
node.sdo.download(0x1017, 0, b'\x00\x00')
变量可以作为可读或可写文件对象打开,这在处理大量数据时非常有用:
# Open the Store EDS variable as a file like object
with node.sdo[0x1021].open('r', encoding='ascii') as infile,
open('out.eds', 'w', encoding='ascii') as outfile:
# Iteratively read lines from node and write to file
outfile.writelines(infile)
大多数接受文件对象的应用程序接口应该也能接受这一点。
如果服务器支持数据块传输,它就能有效地传输大量数据。这可以通过文件对象接口实现:
FIRMWARE_PATH = '/path/to/firmware.bin'
FILESIZE = os.path.getsize(FIRMWARE_PATH)
with open(FIRMWARE_PATH, 'rb') as infile,
node.sdo['Firmware'].open('wb', size=FILESIZE, block_transfer=True) as outfile:
# Iteratively transfer data without having to read all into memory
while True:
data = infile.read(1024)
if not data:
break
outfile.write(data)
警告 区块转移仍处于试验阶段!
五、过程数据对象 (PDO)
过程数据对象协议用于处理各节点之间的实时数据。每个 PDO 最多可从设备或向设备传输 8 字节(64 位)数据。一个 PDO 可包含多个对象字典条目,一个 PDO 中的对象可通过映射和参数对象字典条目进行配置。
PDO 有两种:发送 PDO 和接收 PDO(TPDO 和 RPDO)。前者用于接收来自设备的数据,后者用于接收前往设备的数据;也就是说,使用 RPDO 可以向设备发送数据,使用 TPDO 可以从设备读取数据。在预定义的连接集中,有四(4)个 TPDO 和四(4)个 RPDO 的标识符。可配置 512 个 PDO。
PDO 可以同步或非同步发送。同步 PDO 在 SYNC 信息后发送,而异步信息则在内部或外部触发后发送。例如,您可以通过发送带有 RTR 标志的空 TPDO(如果设备配置为接受 TPDO 请求),请求设备发送包含所需数据的 TPDO。
例如,通过 RPDO,您可以同时启动两个设备。您只需将同一个 RPDO 映射到两个或多个不同的设备中,并确保这些 RPDO 映射的 COB-ID 相同。
5.1 示例
一个 canopen.RemoteNode 具有 canopen.RemoteNode.rpdo 和 canopen.RemoteNode.tpdo 属性,可用于使用 PDO 与节点交互。这些属性可以用子索引来指定使用哪个映射(第一个映射从 1 开始,而不是 0):
# Read current PDO configuration
node.tpdo.read()
node.rpdo.read()
# Do some changes to TPDO4 and RPDO4
node.tpdo[4].clear()
node.tpdo[4].add_variable('Application Status', 'Status All')
node.tpdo[4].add_variable('Application Status', 'Actual Speed')
node.tpdo[4].trans_type = 254
node.tpdo[4].event_timer = 10
node.tpdo[4].enabled = True
node.rpdo[4].clear()
node.rpdo[4].add_variable('Application Commands', 'Command All')
node.rpdo[4].add_variable('Application Commands', 'Command Speed')
node.rpdo[4].enabled = True
# Save new configuration (node must be in pre-operational)
node.nmt.state = 'PRE-OPERATIONAL'
node.tpdo.save()
node.rpdo.save()
# Start RPDO4 with an interval of 100 ms
node.rpdo[4]['Application Commands.Command Speed'].phys = 1000
node.rpdo[4].start(0.1)
node.nmt.state = 'OPERATIONAL'
# Read 50 values of speed and save to a file
with open('output.txt', 'w') as f:
for i in range(50):
node.tpdo[4].wait_for_reception()
speed = node.tpdo['Application Status.Actual Speed'].phys
f.write(f'{speed}\n')
# Using a callback to asynchronously receive values
# Do not do any blocking operations here!
def print_speed(message):
print(f'{message.name} received')
for var in message:
print(f'{var.name} = {var.raw}')
node.tpdo[4].add_callback(print_speed)
time.sleep(5)
# Stop transmission of RxPDO
node.rpdo[4].stop()
六、同步对象 (SYNC)
同步生产者(Sync-Producer)为同步消费者(Sync-Consumer)提供同步信号。同步消费者收到信号后,就开始执行同步任务。
一般来说,同步 PDO 信息传输时间的固定和同步对象传输的周期性保证了传感器设备可以安排对过程变量进行采样,执行器设备可以以协调的方式执行动作。
同步对象的标识符位于索引 1005h。
6.1 示例
使用 canopen.Network.sync 属性启动和停止 SYNC 消息:
# Transmit every 10 ms
network.sync.start(0.01)
network.sync.stop()
七、紧急对象 (EMCY)
紧急信息由设备内部发生致命错误时触发,以高优先级从相关应用设备传输到其他设备。因此,它们适用于中断类型的错误警报。每个 "错误事件 "只能发送一次紧急报文,即紧急报文不得重复发送。只要设备没有发生新的错误,就不能再发送紧急报文。通过 CANopen 通信配置文件定义的紧急错误代码、错误寄存器和设备配置文件中指定的设备特定附加信息。
7.1 示例
要列出某个节点当前激活的紧急事件,可以使用 .active 属性,该属性是 canopen.emcy.EmcyError 对象的列表:
active_codes = [emcy.code for emcy in node.emcy.active]
all_codes = [emcy.code for emcy in node.emcy.log]
canopen.emcy.EmcyError 对象实际上是异常,因此如果您需要,可以很容易地引发异常:
if node.emcy.active:
raise node.emcy.active[-1]
八、时间戳对象(TIME)
通常,时间戳对象表示午夜后以毫秒为单位的绝对时间和自 1984 年 1 月 1 日以来的天数。这是一个长度为 48(6 字节)的位序列。
九、层设置服务 (LSS)
LSS 协议用于更改 CANOpen 目标设备(从站)的节点 ID 和波特率。要更改这些值,主站应首先设置配置状态。然后修改节点 ID 和波特率。从等待状态切换到配置状态有两种选择。一种是一次性切换所有从属设备,另一种是只切换一个从属设备。前者可用于设置所有从属设备的波特率。后者可用于逐个更改节点 ID。
完成设置后,应将数值保存到非易失性存储器中。最后,可以切换到 LSS 等待状态。
注意 某些方法和常量名称有所更改:
send_switch_mode_global() ==> send_switch_state_global()
network.lss.CONFIGURATION_MODE ==> network.lss.CONFIGURATION_STATE
network.lss.NORMAL_MODE ==> network.lss.WAITING_STATE
您仍可使用旧名称,但请使用新名称。
注意 从 v0.8.0 开始支持 Fastscan。未实施 LSS 识别从属服务。
9.1 示例
将所有从属设备切换到 CONFIGURATION(配置)状态。信息无响应。
network.lss.send_switch_state_global(network.lss.CONFIGURATION_STATE)
如果只想切换一个从属设备,也可以使用 4 个 ID 调用此方法:
vendorId = 0x00000022
productCode = 0x12345678
revisionNumber = 0x0000555
serialNumber = 0x00abcdef
ret_bool = network.lss.send_switch_state_selective(vendorId, productCode,
revisionNumber, serialNumber)
或者,可以运行 fastscan 程序
ret_bool, lss_id_list = network.lss.fast_scan()
一旦其中一个传感器进入 "配置 "状态,就可以读取 LSS 从站的当前节点 ID:
node_id = network.lss.inquire_node_id()
更改节点 ID 和波特率:
network.lss.configure_node_id(node_id+1)
network.lss.configure_bit_timing(2)
这是将比特定时的参数索引转换为波特率的表格。
idx | Baud rate |
---|---|
0 | 1 MBit/sec |
1 | 800 kBit/sec |
2 | 500 kBit/sec |
3 | 250 kBit/sec |
4 | 125 kBit/sec |
5 | 100 kBit/sec |
6 | 50 kBit/sec |
7 | 20 kBit/sec |
8 | 10 kBit/sec |
保存配置:
network.lss.store_configuration()
最后,可以将从属设备的状态从 "配置 "状态切换到 "等待 "状态:
network.lss.send_switch_state_global(network.lss.WAITING_STATE)
十、与现有代码集成
有时,您需要将该库与某些现有代码库结合使用,或者您有 Python-can 不支持的 CAN 驱动程序。本章将介绍一些使用案例。
10.1 重新使用总线
如果您需要与本库之外的 CAN 总线进行交互,并希望使用相同的 python-can 总线实例,您需要告诉网络使用哪条总线,并将 canopen.network.MessageListener 添加到现有的 can.Notifier 中。
下面是一个简短的示例:
import canopen
import can
# A Bus instance created outside
bus = can.interface.Bus()
network = canopen.Network()
# Associate the bus with the network
network.bus = bus
# Add your list of can.Listener with the network's
listeners = [can.Printer()] + network.listeners
# Start the notifier
notifier = can.Notifier(bus, listeners, 0.5)
10.2 使用自定义后端
如果 python-can 软件包不支持您的 CAN 接口,您就需要创建 canopen.Network 的子类,并提供自己的消息发送方式。您还需要在后台线程中向 canopen.Network.notify() 发送接收到的消息。
下面是一个示例:
import canopen
class CustomNetwork(canopen.Network):
def connect(self, *args, **kwargs):
# Optionally use this to start communication with CAN
pass
def disconnect(self):
# Optionally use this to stop communincation
pass
def send_message(self, can_id, data, remote=False):
# Send the message with the 11-bit can_id and data which might be
# a bytearray or list of integers.
# if remote is True then it should be sent as an RTR.
pass
network = CustomNetwork()
# Should be done in a thread but here we notify the network for
# demonstration purposes only
network.notify(0x701, bytearray([0x05]), time.time())
十一、设备配置文件
在包括 DS301 应用层在内的标准 CANopen 功能基础上,还可以为某些应用提供专门的附加配置文件。
11.1 用于运动控制器和驱动器的 CiA 402 CANopen 设备配置文件
该设备配置文件具有用于控制驱动器行为的控制状态机。因此,需要使用 BaseNode402 类实例化一个节点
使用 BaseNode402 创建一个节点:
import canopen
from canopen.profiles.p402 import BaseNode402
some_node = BaseNode402(3, 'someprofile.eds')
network = canopen.Network()
network.add_node(some_node)
11.2 电源状态机
PowerStateMachine 类提供了控制该状态机状态的方法。静态方法 on_PDO1_callback() 被添加到 TPDO1 回调中。
通过向寄存器 0x6040 写入特定值(称为 "控制字"),可以控制状态变化。当前状态可通过读取寄存器 0x6041(称为 "Statusword")来读取。只有在 NmtMaster 的 "运行 "状态下才能更改状态。
需要正确设置映射了控制字和状态字的 PDO,这是大多数 DS402 兼容驱动器的默认配置。要使状态机实现可以访问它们,请运行 BaseNode402.setup_402_state_machine() 方法。请注意,该设置例程默认会读取当前的 PDO 配置,从而导致一些 SDO 流量。这仅在 NmtMaster 的 "OPERATIONAL(运行)"或 "PRE-OPERATIONAL(预运行)"状态下有效:
# run the setup routine for TPDO1 and it's callback
some_node.setup_402_state_machine()
写入 Controlword 并读取 Statusword:
# command to go to 'READY TO SWITCH ON' from 'NOT READY TO SWITCH ON' or 'SWITCHED ON'
some_node.sdo[0x6040].raw = 0x06
# Read the state of the Statusword
some_node.sdo[0x6041].raw
在运行过程中,状态可能会变为控制字无法命令的状态,例如 "故障 "状态。因此,BaseNode402 类(与 NmtMaster 类似)会自动监控由 TPDO 发送的状态字的状态变化。然后,TPDO 上的可用回调将提取信息并在 BaseNode402.state 属性中反映状态变化。
与 NmtMaster 类类似,BaseNode402 类状态属性的状态可以通过字符串读取和设置(命令):
# command a state (an SDO message will be called)
some_node.state = 'SWITCHED ON'
# read the current state
some_node.state
可用状态:
- 未准备好开启
- 已禁用
- 准备开启
- 已开启
- 已启用操作
- 故障
- 故障反应激活
- 快速停止激活
可用命令
- 禁用开关
- 禁用电压
- 准备开启
- 已开启
- 启用操作
- 快速停止激活