Modbus TCP和Modbus RTU以及Modbus库的介绍

1. Modbus

1.1 起源

  • Modbus由Modicon公司于1979年开发,是全球第一个真正用于工业现场的总线协议
  • 在中国,Modbus 已经成为国家标准,并有专业的规范文档,感兴趣的可以去查阅相关的文件,详情如下:标准编号为:GB/T19582-2008文件名称:《基于 Modbus 协议的工业自动化网络规范》
  • Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种,其中Modbus TCP是在施耐德收购Modicon后1997年发布的。

1.2 分类

1)Modbus RTU

运行在串口上的协议,采用二进制表现形式以及紧凑的数据结构,通信效率较高,应用比较广泛

2)Modbus ASCII

运行在串口上的协议,采用ASCII码进行传输,并且每个字节的开始和结束都有特殊字符作为标志,传输效率远远低于Modbus RTU,一般只有通讯量比较少时才会考虑它。

3)Modbus TCP

是一种基于以太网的协议,使用 TCP/IP 协议栈进行通信。它使用以太网帧作为数据传输的封装,通过 IP 地址和端口号来标识设备

1.3 优势

简单、免费、容易使用

1.4 应用场景

Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备

2. Modbus TCP协议

2.1 特点

1. 遵循主从问答协议 (主机 从机 主从问答:采集信息 控制)

(主机问---从机答247 1-247:从机 0:广播 248-255:保留 )

2. Modbus TCP协议是应用层协议,基于传输层TCP通信

3. Modbus TCP的默认端口号是502

2.2 组成

Modbus TCP协议包含三部分:报文头、功能码、数据

Modbus TCP和Modbus RTU以及Modbus库的介绍_第1张图片

报文头有7个字节,功能码有1个字节,Modbus TCP协议最大数据帧长度为260个字节,数据最多为252个字节。

2.3 报文头:7个字节

Modbus TCP和Modbus RTU以及Modbus库的介绍_第2张图片

事务处理标识符:包的标识,没有什么限制,一般主机发什么从机回什么

协议标识符:0x0000 两个字节

长度:长度后面的字节个数,必须占2个字节

单元标识符:从机地址,从机ID

2.4 寄存器(存储数据)

Modbus TCP通过寄存器的方式存储数据。

一共有四种类型的寄存器,分别是:离散量输入、线圈、输入寄存器、保持寄存器。

1) 离散量和线圈其实就是位寄存器(每个寄存器数据占1字节),工业上主要用于控制IO设备。

线圈寄存器,类比为开关量,每一个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。

对应的功能码也就是:0x01 0x05 0x0f

离散输入寄存器,离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。

所以功能码也简单就一个读的 0x02

2) 输入和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值。

保持寄存器,这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。比如我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写

所以功能码有对应的三个:0x03 0x06 0x10

输入寄存器,这个和保持寄存器类似,但是也是只支持读而不能写。一个寄存器也是占据两个byte的空间。类比我通过读取输入寄存器获取现在的AD采集值

对应的功能码也就一个 0x04

2.5 功能码

Modbus TCP和Modbus RTU以及Modbus库的介绍_第3张图片

寄存器PLC地址和寄存器的对应关系:

线圈: 00001-09999

离散量输入:10001-19999

输入寄存器:30001-39999

保持寄存器:40001-49999

开灯:05

读温度传感器数据:03 04

2.6 总结

读数据:

主机--》从机:

报文头(7)+功能码(1)+起始地址(2)+数量(2)

从机--》主机:

报文头(7)+功能码(1)+字节计数(1)+数据(?)

写数据:

写单个:

主机--》从机:

报文头(7)+功能码(1)+地址(2)+断通标志/数据(2)

(线圈:断通标志 0x0000:复位 0xff00:置位 保持寄存器:数据)

从机--》主机:

原文返回

对于读数据和写单个主机--》从机来说,报文头中字节长度固定为0x0006,(1个字节的单元标识符+1个字节的功能码+4个字节的数据)

写多个:

主机--》从机:

报文头(7)+功能码(1)+起始地址(2)+数量(2)+字节计数(1)+数据(?)

从机--》主机:

报文头(7)+功能码(1)+起始地址(2)+数量(2)(原文返回)

3. Modbus RTU

3.1 与Modbus TCP的区别

Modbus TCP和Modbus RTU以及Modbus库的介绍_第4张图片

在一般工业场景使用modbus RTU的场景还是更多一些,modbus RTU基于串行协议进行收发数据,包括RS232/485等工业总线协议。

与modbus TCP不同的是RTU没有报文头MBAP字段,但是在尾部增加了两个CRC检验字节(CRC16),因为网络协议中自带校验,所以在TCP协议中不需要使用CRC校验码。

RTU和TCP的总体使用方法基本一致,只是在创建modbus对象时有所不同,TCP需要传入网络socket信息;而RTU需要传入串口相关信息。

3.2 特点

1. 遵循主从问答的通信方式

2. 采用串口的方式进行通信

设置串口参数时要求:
波特率为9600(波特率是指每秒钟传输的比特数)
8位数据位 (数据位是指每个字符中包含的比特数)
1位停止位 (停止位是指在每个字符传输结束后添加的比特数)
无流控 (流控是指在数据传输过程中控制数据流量的一种机制,无流控表示在该设置下没有额外的控制机制来控制数据流量)

3.3 modbus rtu协议格式

地址码 功能码 数据 校验码

地址码(1字节):从机ID

功能码(1字节):同modbus tcp(01 02 03 04 05 06 0f 10H)

数据:起始地址、地址、数量、数据、字节计数

校验码(2字节):对地址码、功能码、数据进行校验,由函数生成

例子:

主机--》从机:

01 03 00 00 00 01 84 0a

01 :从机id

03 :功能码

00 00 :起始地址

00 01 :数量

84 0a:校验码

从机--》主机:

01 03 02 0014 b8 44

01 :从机id

03 :功能码

02 :字节计数

00 14 :数据

b8 44 :校验码

4. Modbus 库

官方文档:libmodbus

4.1 库的安装

库的安装配置(共四步)

1. 在linux中解压压缩包

tar -xvf libmodbus-3.1.7.tar.gz

2. 进入源码目录,创建文件夹(存放头文件、库文件)

cd libmodbus-3.1.7

mkdir install

3. 执行脚本configure,进行安装配置(指定安装目录)

./configure --prefix=$PWD/install

4. 执行make和make install

make//编译

make install//安装

执行完成后会在install文件夹下生产对应的头文件、库文件件夹install,用于存放产生的头文件、库文件等

4.2 库的使用

要想编译方便,可以将头文件和库文件放到系统路径下

sudo cp include/modbus/*.h /usr/include

sudo cp lib/* -r /lib -d

后期编译时,可以直接 gcc xx.c -lmodbus

头文件默认搜索路径:/usr/include 、/usr/local/include

库文件默认搜索路径:/lib、/usr/lib

4.3 函数接口

modbus_t*   modbus_new_tcp(const char *ip, int port)
功能:以TCP方式创建Modbus实例,并初始化
参数:
    ip   :ip地址
    port:端口号
返回值:成功:Modbus实例
      失败:NULL
int modbus_set_slave(modbus_t *ctx, int slave)
功能:设置从机ID
参数:
    ctx   :Modbus实例
    slave:从机ID
返回值:成功:0
       失败:-1
int   modbus_connect(modbus_t *ctx)
功能:和从机(slave)建立连接
参数:
    ctx:Modbus实例
返回值:成功:0
       失败:-1
void   modbus_free(modbus_t *ctx)
功能:释放Modbus实例
参数:ctx:Modbus实例
void   modbus_close(modbus_t *ctx)
功能:关闭套接字
参数:ctx:Modbus实例
int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取线圈状态,可读取多个连续线圈的状态(对应功能码为0x01
参数:
    ctx   :Modbus实例
    addr :寄存器起始地址
    nb    :寄存器个数
    dest :得到的状态值
返回值:成功:读到的数量
      失败:-1
int  modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取输入状态,可读取多个连续输入的状态(对应功能码为0x02
参数:
    ctx   :Modbus实例
    addr :寄存器起始地址
    nb   :寄存器个数
    dest :得到的状态值
返回值:成功:返回nb的值
      失败:-1
int  modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读取保持寄存器的值,可读取多个连续保持寄存器的值(对应功能码为0x03
参数:
    ctx   :Modbus实例
    addr :寄存器起始地址
    nb    :寄存器个数
    dest :得到的寄存器的值
返回值:成功:读到寄存器的个数
       失败:-1
int   modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读输入寄存器的值,可读取多个连续输入寄存器的值(对应功能码为0x04
参数:
    ctx   :Modbus实例
    addr :寄存器起始地址
    nb    :寄存器个数
    dest :得到的寄存器的值
返回值:成功:读到寄存器的个数
       失败:-1
int  modbus_write_bit(modbus_t *ctx, int addr, int status);
功能:写入单个线圈的状态(对应功能码为0x05
参数:
    ctx     :Modbus实例
    addr  :线圈地址
    status:线圈状态
返回值:成功:1
      失败:-1
int  modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
功能:写入多个连续线圈的状态(对应功能码为15
参数:
    ctx     :Modbus实例
    addr  :线圈地址
    nb     :线圈个数
    src    :多个线圈状态
返回值:成功:写入的数量
      失败:-1
int  modbus_write_register(modbus_t *ctx, int addr, int value);
功能:  写入单个寄存器(对应功能码为0x06
参数: 
    ctx    :Modbus实例
    addr  :寄存器地址
    value :寄存器的值 
返回值:成功:1
       失败:-1
int  modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);
功能:写入多个连续寄存器(对应功能码为16
参数:
    ctx    :Modbus实例
    addr  :寄存器地址
    nb     :寄存器的个数
    src    :多个寄存器的值 
返回值:成功:写入的数量
      失败:-1
float modbus_get_float_dcba(const uint16_t *src)
功能:读取浮点类型的数据
参数:src:读到数据的存放数组
返回值:转换后的浮点类型

4.4 编程步骤

1. 创建实例

2. 设置从机ID

3. 建立连接

4. 寄存器操作(按需选择)

5. 关闭套接字

6. 释放实例

4.5 练习:通过库函数实现03功能码

#include 
#include 

int main(int argc, char const *argv[])
{
    char ip[128] = {"192.168.51.232"};
    int port = 502;
    int slave = 1;
    int addr = 0x0000;
    int nb = 0x0002;
    uint16_t dest[12] = {0};
    // 创建实例
    // IP与端口号可作为命令行传参
    modbus_t *modbus = modbus_new_tcp(ip, port);
    if (modbus == NULL)
    {
        perror("create err");
        return -1;
    }
    modbus_set_slave(modbus, slave); // 设置从机ID
    int c = modbus_connect(modbus);
    if (c == -1)
    {
        perror("connect err");
        return -1;
    }
    int r = modbus_read_registers(modbus, addr, nb, dest);
    if (r == -1)
    {
        perror("read err");
        return -1;
    }
    for (int i = 0; i < r; i++)
    {
        printf("%d ", dest[i]);
    }
    putchar(10);
    modbus_close(modbus);
    modbus_free(modbus);
    return 0;
}


如有问题请私信联系,谢谢

你可能感兴趣的:(tcp/ip,网络,网络协议,服务器,c语言)