Modbus RTU、Modbus 库函数

Modbus RTU

与 Modbus TCP 的区别

一般在工业场景中,使用 Modbus RTU 的场景更多一些,Modbus RTU 基于串行协议进行收发数据,包括 RS232/485 等工业总线协议。采用主从问答式(master / slave)通信。

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

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

特点

通信

采用主从问答式(master / slave)通信,由主机发起,一问一答。

设置串口参数

波特率:9600
数据位:8
停止位:1
无流控

协议格式(地址码 + 功能码 + 数据 + 校验码)

Modbus RTU 数据帧包含:地址码、功能码、数据、校验码。
地址码 : 从机 ID
功能码 : 同 Modbus TCP
数据 : 起始地址、数量、数据
CRC 校验码: 两个字节,对 地址码、功能码、数据 进行校验,可以通过函数自动生成

报文详解

(👆 链接至另一博主,放心跳转)

以 03 功能码为例:

主机 ------> 从机:
从机 ------> 主机:

模拟器的安装、配置、使用

实际硬件产品成本较高,可以使用一系列 Modbus 软件模拟器,进行数据模拟,从而分析 Modbus RTU 协议。

所用工具

Modbus Slave、vspd 虚拟串口、UartAssist 串口调试工具、虚拟机

安装与配置

一)vspd 虚拟串口的安装

1)将压缩包解压后,双击 vspd.exe 文件进行安装;

2)打开软件,添加 COM1 和 COM2 端口(用完之后记得删除端口);

3)打开设备管理器,出现如下图所示即可;

4)可以汉化,将 Cracked 下的文件复制到软件安装目录即可。

二)虚拟机绑定端口

1)VMware 虚拟机(注意不是 ubuntu)在系统关机(必须是关机状态,挂起不行)状态下,

点击:虚拟机 ------> 设置 ------> 硬件 ------> 添加串行端口,添加 COM1;

2)添加完成后,第一次使用需要将电脑重启;

3)重启之后,打开虚拟机,点击虚拟机 ------> 可移动设备 ------> 串行端口 ------> 连接;

4)在终端输入dmesg|grep tty ,查看对应的设备文件,其中默认的会有 ttyS0 文件,

其余一个(ttyS1 或 ttyS2)就是虚拟串口对应的设备文件。

三)测试通信

1)Windows 下打开串口调试工具,选择好串口 COM2 ------> COM1,设置对应的波特率;

2)以下步骤在虚拟机下完成,在虚拟机安装 minicom 软件;sudo apt-get install minicom

3)在终端执行 sudo minicom -s ,选择 Serial port setup;

4)设置设备文件,波特率,关闭流控;(按 Ctrl + 相应字母)

5)回车,保存修改,选择 Save setup as dfl;

6)可以在以下界面输入字符,查看串口助手的显示情况;

7)测试通信(终端输入不可见);

8)退出:Ctrl + A,然后按 Z,在弹出的界面里输入X,即可退出。

四)将 Modbus Slave 模拟器作为 RTU 设备的从机

虚拟机绑定 COM1 端口,Modbus Slave 连接 COM2 端口,虚拟机通过编程测试串口通信;

五)可能遇到的问题

虚拟串口完成主机与 vmware 下虚拟机进行串口通信
VSPD 虚拟串口工具 ------ 从此告别硬件串口调试
vmware 虚拟机检测不到 vspd 虚拟串口问题

(👆 链接至其他博主,放心跳转)

Modbus 库

库的安装

安装与配置

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 			   // 安装


5)执行完成后会在 install 文件夹下产生对应的头文件、库文件。

使用

1、一般操作:

复制代码
gcc xxx.c -I ./install/include/modbus -L ./install/lib -lmodbus
./a.out

-I : 后需要指定出头文件的路径(大写的i)

-L : 后需要指定库的路径

-l : 后需要指定库名(小写的L)

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

复制代码
sudo  cp install/include/modbus/*.h  /usr/include 
sudo  cp install/lib/*  -r /lib -d 

后期编译时,就可以直接 gcc xxx.c -lmodbus,
头文件默认搜索路径:/usr/include、/usr/local/include
库文件默认搜索路径:/lib、/usr/lib

函数接口

0x01(modbus_read_bits)

复制代码
int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 

功能:读取线圈状态,可读取多个连续线圈的状态(对应功能码为0x01)
参数:
    ctx   :	Modbus实例
    addr  :	寄存器起始地址
    nb    :	寄存器个数
    dest  :	得到的状态值

0x02(modbus_read_input_bits)

复制代码
int  modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 

功能:读取输入状态,可读取多个连续输入的状态(对应功能码为0x02)
参数:
    ctx  :	Modbus 实例
    addr :	寄存器起始地址
    nb   :	寄存器个数
    dest :	得到的状态值
返回值:成功:返回nb的值

0x03(modbus_read_registers)

复制代码
int  modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); 

功能:读取保持寄存器的值,可读取多个连续保持寄存器的值(对应功能码为0x03)
参数:
    ctx  :	Modbus 实例
    addr :	寄存器起始地址
    nb   :	寄存器个数
    dest :	得到的寄存器的值
返回值:成功:读到寄存器的个数
       失败:-1

0x04(modbus_read_input_registers)

复制代码
int  modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);

功能:读输入寄存器的值,可读取多个连续输入寄存器的值(对应功能码为0x04)
参数:
    ctx   :	Modbus 实例
    addr  :	寄存器起始地址
    nb    :	寄存器个数
    dest  :	得到的寄存器的值
返回值:成功:读到寄存器的个数
       失败:-1

0x05(modbus_write_bit)

复制代码
int  modbus_write_bit(modbus_t *ctx, int addr, int status);

功能:写入单个线圈的状态(对应功能码为0x05)
参数:
    ctx   :	Modbus 实例
    addr  :	线圈地址
    status:	线圈状态
返回值:成功:0
       失败:-1

0x06(modbus_write_register)

复制代码
int  modbus_write_register(modbus_t *ctx, int addr, int value);

功能:写入单个寄存器(对应功能码为0x06)
参数: 
    ctx   :	Modbus 实例
    addr  :	寄存器地址
    value :	寄存器的值 
返回值:成功:0
       失败:-1

0x0F(modbus_write_bits)

复制代码
int  modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);

功能:写入多个连续线圈的状态(对应功能码为15)
参数:
    ctx   :	Modbus 实例
    addr  :	线圈地址
    nb    :	线圈个数
    src   :	多个线圈状态
返回值:成功:0
       失败:-1

0x10(modbus_write_registers)

复制代码
int  modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);

功能:写入多个连续寄存器(对应功能码为16)
参数:
    ctx   :	Modbus 实例
    addr  :	寄存器地址
    nb    :	寄存器的个数
    src   :	多个寄存器的值 
返回值:成功:0
       失败:-1

编程流程

1)创建实例(modbus_new_tcp / modbus_new_rtu)

复制代码
modbus_t *modbus_new_tcp(const char *ip, int port); 

功能:以 TCP 方式创建 Modbus 实例,并初始化
参数:
    ip  :	ip 地址
    port:	端口号
返回值:成功:Modbus 实例
       失败:NULL

modbus_t *modbus_new_rtu(const char *device, int baud, 
                         		char parity, int data_bit, int stop_bit);

功能:用于创建一个用于 Modbus RTU 通信的 modbus_t 结构体实例
参数:
	device:	要打开的串口设备的路径(例如:"/dev/ttyUSB0")
	baud:		波特率(如 9600、19200 等)
	parity:	校验位(可选值:'N' - 无校验、'E' - 偶校验、'O' - 奇校验)
	data_bit:	数据位(常用值为 8)
	stop_bit:	停止位(常用值为 1)
 返回值:成功:Modbus 实例
        失败:NULL

2)设置从机地址(modbus_set_slave)

复制代码
int  modbus_set_slave(modbus_t *ctx, int slave); 
功能:设置从机ID
参数:
    ctx  :		Modbus 实例
    slave:		从机 ID
返回值:成功:0
       失败:-1

3)建立连接(modbus_connect)

复制代码
int  modbus_connect(modbus_t *ctx); 
功能:和从机(slave)建立连接
参数:
    ctx:		Modbus 实例
返回值:成功:0
       失败:-1

4)各种操作(见函数接口)

5)关闭套接字(modbus_close)

复制代码
void  modbus_close(modbus_t *ctx); 
功能:关闭套接字
参数:ctx:Modbus 实例

6)释放实例(modbus_free)

复制代码
void   modbus_free(modbus_t *ctx); 
功能:释放 Modbus 实例
参数:ctx:Modbus 实例

练习:

c 复制代码
// 和 Slave 通信,读保持寄存器的三个值

#include <stdio.h>
#include <modbus.h>
#include <stdlib.h>
#include <string.h>
#include <modbus-rtu.h>

int main(int argc, char const *argv[])
{   
    if (argc != 3){
        printf("Please input %s <ip> <port>. \n", argv[0]);
        return -1;
    }

    modbus_t *ctx;
    ctx = modbus_new_tcp(argv[1], atoi(argv[2]));
    // ctx = modbus_new_rtu("/dev/ttyS1", 9600, N, 8, 1);
    
    if (ctx == NULL){
        perror("Failed to modbus_new_tcp");			// "Failed to modbus_new_rtu"
        return -1;
    }

    if (modbus_set_slave(ctx, 1) < 0){
        perror("Failed to modbus_set_slave");
        return -1;
    }

    if (modbus_connect(ctx) < 0){
        perror("Failed to modbus_connect");
        return -1;
    }

    uint16_t dest[32] = {};

    if (modbus_read_registers(ctx, 0, 3, dest) < 0){
        perror("Failed to modbus_read_registers");
        return -1;
    }

    for (int i = 0; i < 3; i++)
        printf("%#x ", dest[i]);
    putchar(10);
    for (int i = 0; i < 3; i++)
        printf("%d ", dest[i]);
    putchar(10);

    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

运行结果如下:

注意:

1、使用 Modbus TCP 协议时,将 slave 的 connect 设置为"Modbus TCP/IP"。

2、使用 Modbus RTU 协议时,将 slave 的 connect 设置为"Serial Port"。

小目标:

编程实现采集传感器数据和控制硬件设备(传感器和硬件通过 slave 模拟)。

传感器:2个,光线传感器、加速度传感器(x \ y \ z);

硬件设备:2个,LED灯、蜂鸣器。

要求:

1、多任务编程:多线程、多进程

2、循环 1s 采集一次数据,并将数据打印至终端

3、同时从终端输入指令控制硬件设备

0 1:LED 灯开

0 0:LED 灯关

1 1:蜂鸣器开

1 0:蜂鸣器关

c 复制代码
// 同步实现

#include <stdio.h>
#include <modbus.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

modbus_t *ctx;
sem_t sem1, sem2;

void *collector(void *arg){

    uint16_t *dest = (uint16_t *)arg;
    while (1){
        sleep(5);
        sem_wait(&sem1);

        if (modbus_read_registers(ctx, 0, 4, dest) < 0){
            perror("Failed to modbus_read_registers");
            return NULL;
        }
        for (int i = 0; i < 4; i++)
            printf("%d ", dest[i]);
        putchar(10);

        sem_post(&sem2);
    }
    pthread_exit(0);
}

void *control(void *arg){
    
    uint8_t writer[2];
    while (1){
        sem_wait(&sem2);

        printf("Please set status of LED or BUZZER: ");
        for (int i = 0; i < 2; i++)
            scanf("%hhu", &writer[i]);
        
        modbus_write_bit(ctx, writer[0], writer[1]);

        sem_post(&sem1);
    }
    pthread_exit(0);
}

int main(int argc, char const *argv[])
{   
    if (argc != 3){
        printf("Please input %s <ip> <port>. \n", argv[0]);
        return -1;
    }

    ctx = modbus_new_tcp(argv[1], atoi(argv[2]));
    if (ctx == NULL){
        perror("Failed to modbus_new_tcp");
        return -1;
    }

    if (modbus_set_slave(ctx, 1) < 0){
        perror("Failed to modbus_set_slave");
        return -1;
    }

    if (modbus_connect(ctx) < 0){
        perror("Failed to modbus_connect");
        return -1;
    }

    uint16_t dest[32] = {};

    pthread_t tid1, tid2;
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);

    if (pthread_create(&tid1, NULL, collector, dest)){
        perror("Failed to create a thread named collector");
        return -1;
    }
    pthread_detach(tid1);

    if (pthread_create(&tid2, NULL, control, NULL)){
        perror("Failed to create a thread named input");
        return -1;
    }
    pthread_detach(tid2);

    while (1);
    

    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

实现效果如下:

相关推荐
virelin_Y.lin22 分钟前
系统与网络安全------弹性交换网络(2)
网络·安全·web安全·链路聚合·lacp·eth-trunk
傻啦嘿哟1 小时前
Python正则表达式:用“模式密码“解锁复杂字符串
linux·数据库·mysql
浪裡遊3 小时前
Linux常用指令
linux·运维·服务器·chrome·功能测试
段ヤシ.4 小时前
银河麒麟(内核CentOS8)安装rbenv、ruby2.6.5和rails5.2.6
linux·centos·银河麒麟·rbenv·ruby2.6.5·rails 5.2.6
深夜情感老师6 小时前
centos离线安装ssh
linux·centos·ssh
EasyDSS6 小时前
视频监控从安装到优化的技术指南,视频汇聚系统EasyCVR智能安防系统构建之道
大数据·网络·网络协议·音视频
rufeike6 小时前
UDP协议理解
网络·网络协议·udp
江理不变情7 小时前
海思ISP调试记录
网络·接口隔离原则
YuforiaCode7 小时前
第十三届蓝桥杯 2022 C/C++组 修剪灌木
c语言·c++·蓝桥杯
世界尽头与你7 小时前
【安全扫描器原理】网络扫描算法
网络·安全