网络编程(Modbus进阶)

思维导图

Modbus RTU(先学一点理论)

概念

Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议 ,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现 的特点成为工业控制系统的通信标准。 包括RS232/485等工业总线协议。

与Modbus TCP区别

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

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

特点

1.遵循主从问答 的通信方式
2.采用串口 的方式进行通信

设置串口参数时要求:(了解,后续还会用)

波特率为9600(波特率是指每秒钟传输的比特数)

8位数据位 (数据位是指每个字符中包含的比特数)

1位停止位 (停止位是指在每个字符传输结束后添加的比特数)

无流控 (流控是指在数据传输过程中控制数据流量的一种机制,无流控表示在该设置下没有额外的控制机制来控制数据流量)

modbus rtu协议格式

地址码 功能码 数据 校验码
**地址码(1字节):**从机ID

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

**数据:**起始地址、地址、数量、数据、字节计数;和modbus tcp一样。

**校验码(2字节):**对地址码、功能码、数据进行校验,由函数生成,循环冗余校验 (低字节在前)

其实modbus rtu协议的格式和modbus tcp是很像,就是把tcp的MBAP报文头去掉,只保留了一个字节的主机ID,最后结尾加上了两个字节的校验码。(校验码没有实际意义,是函数生成的不用管

以01发送的数据格式为例,可以看到数据位是一样的,上图就是tcp和rtu协议格式的区别。数据接收也是和tcp一样的,所以就不再讲了。

modbus 库

官方文档:libmodbus

1库的安装

**第一步和第二步都要运行,第一步是为了安装配置,第二步是为了让你使用这个库更方便,把它放在你的C语言库里。**​​​​​

1.1库的安装配置(共四步)

通过网盘分享的文件:sqlite-autoconf-3460000.tar.gz

链接: https://pan.baidu.com/s/1ro8-xbsFitDSEEK6mSYzwQ?pwd=3521 提取码: 3521

直接复制命令,别手打,按顺序

  1. (先下载压缩包,CtrlC+V复制到虚拟机任意路径下)在linux中解压压缩包

tar -xvf libmodbus-3.1.7.tar.gz

  1. 进入源码目录

cd libmodbus-3.1.7

3.创建文件夹(存放头文件、库文件)

mkdir install

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

./configure --prefix=$PWD/install

  1. 执行make

make //编译

6.执行make install

make install //安装

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

1.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

2.函数接口

在上面的官方文档里包含所有的函数接口,以下是常用的modbus tcp函数接口,上个文章尝试自己写函数,这里就是使用这些别人写好的库函数(更方便)。

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:读到数据的存放数组

**返回值:**转换后的浮点类型

编程

从上往下写就可以连接到Modbus Slave,ip要写主机的IP地址,不要写成虚拟机的IP地址。,你要是在虚拟机运行的Modbus Slave,那就可以写虚拟机地址。

编程步骤

1.创建实例

2.设置从机ID

3.建立连接

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

5.关闭套接字

6.释放实例

编程实现

1.基础步骤实现:
cs 复制代码
#include <modbus.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char ip[128] = {"192.168.50.224"};         //IP
    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("err\n");
        return -1;
    }
    //设置从机ID
    modbus_set_slave(modbus, slave);   
    //建立连接
    int con = modbus_connect(modbus);
    if (con == -1)
    {
        perror("con:\n");
        return -1;
    }
    //寄存器操作
    int read = modbus_read_registers(modbus, addr, nb, dest);
    if (read == -1)
    {
        perror("read:\n");
        return -1;
    }
    for (int i = 0; i < read; i++)
    {
        printf("%d ", dest[i]);
    }
    putchar(10);
    //关闭套接字
    modbus_close(modbus);
    //释放实例
    modbus_free(modbus);
    return 0;
}
2.数据采集小项目:

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

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

**硬件设备:**2个,led灯、蜂鸣器

要求:

1.多任务编程:建议多线程

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

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

0 1:led灯打开

0 0:led灯关闭

1 1:蜂鸣器开

1 0:蜂鸣器关

cs 复制代码
#include <stdio.h>
#include <modbus.h>
#include <unistd.h>
#include <pthread.h>

void *handler1(void *arg){            //内不含阻塞,相当于后台运行
	uint8_t dest1[32] = {0};
	uint16_t dest2[32] = {0};
	modbus_t *modbusid = (modbus_t *)arg;
	int size = 0;
	while(1){								
		//读取线圈状态
		size = modbus_read_bits(modbusid,0,2,dest1);
		if(size == -1){					//容错判断
			perror("modbus_read_registers err");
			break;
		}
		printf("LED:%02x 蜂鸣器:%02x\n",dest1[0],dest1[1]);
		//查询寄存器数值
		size = modbus_read_registers(modbusid,0,2,dest2);
		if(size == -1){					//容错判断
			perror("modbus_read_registers err");
			break;
		}
		printf("温度传感器:%02x 加速度传感器:%02x\n",dest2[0],dest2[1]);
		sleep(5);                        //5秒打印一次
	}
}

void *handler2(void *arg){                    //用于执行写操作,需要输入指令
	modbus_t *modbusid = (modbus_t *)arg;
	int addr,nb,status;
	int a = 0;				        //标志操作05,06
	while(1){
		scanf("%d",&a);				//选择操作
		if(a == 5){					//操作单个线圈
			scanf("%d %d",&addr,&status);
			modbus_write_bit(modbusid,addr,status);
		}
		else if(a == 6){			//操作单个寄存器
			scanf("%d %d",&addr,&nb);
			modbus_write_register(modbusid,addr,nb);
		}
		if(a == -1)
			break;
	}
}

int main(int argc, const char *argv[])
{
	//创建实例
	modbus_t *modbusid = modbus_new_tcp("192.168.43.148",502);
	if(modbusid == NULL){
		perror("modbus_new_tcp err");
		return -1;
	}
	//设置从机ID
	int slave = 1;
	if(modbus_set_slave(modbusid,slave) == -1){
		perror("modbus_set_slave err");
		return -1;
	}
	//建立连接
	if(modbus_connect(modbusid) == -1){
		perror("modbus_connect err");
		return -1;
	}
	//寄存器操作
	pthread_t ptid1;
	pthread_create(&ptid1,NULL,handler1,modbusid);        //创建第一个线程
	pthread_detach(ptid1);
	pthread_t ptid2;
	pthread_create(&ptid2,NULL,handler2,modbusid);        //创建第二个线程
	pthread_join(ptid2,NULL);

	//关闭套接字
	modbus_close(modbusid);
	//释放实例
	modbus_free(modbusid);
	return 0;
}

五秒打印一次,终端还可以输入命令取改变寄存器和线圈值

相关推荐
江池俊6 分钟前
通过Docker和内网穿透技术在Linux上搭建远程Logseq笔记系统
linux·笔记·docker
孙克旭_30 分钟前
day032-网站集群架构与环境准备
linux·运维·架构·自动化
whoarethenext31 分钟前
使用 C/C++ 和 OpenCV 提取图像的感兴趣区域 (ROI)
c语言·c++·opencv
WarPigs34 分钟前
Unity网络通信笔记
网络·unity
凌肖战1 小时前
力扣上C语言编程题:最大子数组和(涉及数组)
c语言·算法·leetcode
緈福的街口1 小时前
【leetcode】125.验证回文串
linux·算法·leetcode
Invinciblenuonuo1 小时前
Linux【5】-----编译和烧写Linux系统镜像(RK3568)
linux·操作系统·rk3568·系统编译
T0uken1 小时前
【Linux】HAProxy:结合 WG 实现内网 TCP 反代
linux·运维·tcp/ip
vortex52 小时前
Linux Shell 中的 dash 符号 “-”
linux·运维·服务器
月堂2 小时前
Linux操作系统-性能优化
linux·运维·服务器