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;
}

实现效果如下:

相关推荐
小蜗牛慢慢爬行2 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
MARIN_shen8 分钟前
Marin说PCB之POC电路layout设计仿真案例---06
网络·单片机·嵌入式硬件·硬件工程·pcb工艺
荒古前9 分钟前
龟兔赛跑 PTA
c语言·算法
岑梓铭12 分钟前
(CentOs系统虚拟机)Standalone模式下安装部署“基于Python编写”的Spark框架
linux·python·spark·centos
努力学习的小廉13 分钟前
深入了解Linux —— make和makefile自动化构建工具
linux·服务器·自动化
MZWeiei16 分钟前
Zookeeper基本命令解析
大数据·linux·运维·服务器·zookeeper
7yewh32 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
小张认为的测试36 分钟前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
嵌入式科普44 分钟前
十三、从0开始卷出一个新项目之瑞萨RZN2L串口DMA接收不定长
c语言·stm32·瑞萨·e2studio·rzn2l
打鱼又晒网44 分钟前
linux网络套接字 | 深度解析守护进程 | 实现tcp服务守护进程化
linux·网络协议·计算机网络·tcp