基础知识

硬件层协议:解决0和1的可靠传输,常有RS232、RS485、CAN、IIC、SPI …
软件层协议:解决传输目的,常有Modbus、TCP/IP、CANopen …

协议优点:

  • Modbus协议标准开放、公开发表且无版权要求
  • Modbus协议支持多种电气接口,包括RS232、RS485、TCP/IP等,还可以在各种介质上传输,如双绞线、光纤、红外、无线等
  • Modbus协议消息帧格式简单、紧凑、通俗易懂。用户理解和使用简单,厂商容易开发和集成,方便形成工业控制网络

Modbus是一主多从的通信协议

Modbus通信中只有一个设备可以发送请求。其他从设备接收主机发送的数据来进行响应,从机是任何外围设备,如I/O传感器,阀门,网络驱动器,或其他测量类型的设备,从站处理信息和使用Modbus将其数据发送给主站。不能Modbus同步进行通信,主机在同一时间内只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。 从机不会自己发送消息给主站,只能回复从主机发送的消息请求。

RTU协议

Modbus报文是指主机发送给从机的一帧数据,其中包含着从机的地址,主机想执行的操作,校验码等内容
Modbus协议在串行链路上的报文格式如下所示:

  • 从机地址: 每个从机都有唯一地址,占用一个字节,范围0-255,其中有效范围是1-247,其中255是广播地址(广播就是对所有从机发送应答)
  • 功能码: 占用一个字节,功能码的意义就是,知道这个指令是干啥的,比如你可以查询从机的数据,也可以修改从机的数据,所以不同功能码对应不同功能.
  • 数据: 根据功能码不同,有不同功能,比方说功能码是查询从机的数据,这里就是查询数据的地址和查询字节数等。
  • 校验: 在数据传输过程中可能数据会发生错误,CRC检验检测接收的数据是否正确

Modbus功能码

Modbus协议同时规定了二十几种功能码,但是常用的只有8种,用于对存储区的读写,如下表所示:

功能码 功能说明
01H 读取输出线圈
02H 读取输入线圈
03H 读取保持寄存器
04H 读取输入寄存器
05H 写入单线圈
06H 写入单寄存器
0FH 写入多线圈
10H 写入多寄存器

当然我们用的最多的就是03和06 一个是读取数据,一个是修改数据。

1、主机对从机读数据操作
主机发送报文格式如下:

从站地址 功能码 起始(高) 起始(低) 数量(高) 数量(低) 校验
0x01 0x03 0x00 0x01 0x00 0x01 0xD5 0xCA

含义:

0x01:从机的地址
0x03:查询功能,读取从机寄存器的数据
0x00 0x01: 代表读取的起始寄存器地址.说明从0x0001开始读取.
0x00 0x01: 查询的寄存器数量为0x0001个 Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据; 寄存器地址对应着从机实际的存储地址
0xD5 0xCA: 循环冗余校验 CRC

从机回复报文格式如下:

从站地址 功能码 字节计数 字节1 字节2 校验
0x01 0x03 0x02 0x00 0x00 0xB8 0x44

含义:

0x01:从机的地址
0x03:查询功能,读取从机寄存器的数据
0x02: 返回字节数为2个 一个寄存器2个字节
0x00 0x00:寄存器的值是0000
0xB8 0x44: 循环冗余校验 CRC

2、主机对从机写数据操作

主机发送报文格式如下:

从站地址 功能码 数据地址(高) 数据地址(低) 数据(高) 数据(低) 校验
0x01 0x06 0x00 0x01 0x00 0x17 0x98 0x04

含义:

0x01:从机的地址
0x06:修改功能,修改从机寄存器的数据
0x00 0x01: 代表修改的起始寄存器地址.说明修改0x0001-0x0003的存储内容
0x00 0x17: 要修改的数据值为0017
0x98 0x04: 循环冗余校验 CRC

从机回复报文格式如下:

从站地址 功能码 数据地址(高) 数据地址(低) 数据(高) 数据(低) 校验
0x01 0x06 0x00 0x01 0x00 0x17 0x98 0x04

含义:

0x01:从机的地址
0x06:修改功能,修改从机寄存器的数据
0x00 0x01: 代表修改的起始寄存器地址.说明是0x0000
0x00 0x17:修改的值为0017
0x98 0x04: 循环冗余校验 CRC

从机的回复和主机的发送是一样的,如果不一样说明出现了错误

python库 modbus_tk

import serial
import modbus_tk.defines as cst
from modbus_tk import modbus_rtu


class Modbus:
    def __init__(self, port: str, baud: int):
        """
        初始化modbus
        默认的slave_id为1
        :param port: 串口号
        :param baud: 波特率
        """
        self.port = port
        self.baud = int(baud)
        self.master = None

    def open(self):
        try:
            self.master = modbus_rtu.RtuMaster(
                serial.Serial(port=self.port, baudrate=self.baud, bytesize=8, parity='N', stopbits=1))
            self.master.set_timeout(5.0)
        except Exception as err:
            print("---异常---", err)

    def master_write_single_coil(self, slave_id: int, register_address: int, output_value: int) -> tuple:
        """
        05指令
        向slave从机中写寄存器
        :param slave_id: 从机id
        :param register_address: 从机寄存器的地址 十进制
        :param output_value: 要写入的数值 十进制
        :return: (0, 32762) 类似元组(写入的从机寄存器地址,写入的值)
        """
        result = self.master.execute(slave_id, cst.WRITE_SINGLE_COIL, register_address, output_value=output_value)
        return result

    def master_write_single_register(self, slave_id: int, register_address: int, output_value: int) -> tuple:
        """
        06指令
        向slave从机中写寄存器
        :param slave_id: 从机id
        :param register_address: 从机寄存器的地址 十进制
        :param output_value: 要写入的数值 十
        :return: (0, 32762) 类似元组(写入的从机寄存器地址,写入的值)
        """
        # 写入操作当对动作指令寄存器写入对应动作参数,就会立刻进行执行动作
        # 站点号;cst.WRITE_SINGLE_REGISTER写入单个寄存器;寄存器写入地址;output_value写入的值
        result = self.master.execute(slave_id, cst.WRITE_SINGLE_REGISTER, register_address, output_value=output_value)
        return result