思维导图
Modbus RTU(先学一点理论)
概念
与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
2. 进入源码目录
cd libmodbus-3.1.7
3.创建文件夹(存放头文件、库文件)
mkdir install
4.执行脚本configure,进行安装配置(指定安装目录)
./configure --prefix=$PWD/install
5. 执行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.基础步骤实现:
#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:蜂鸣器关
#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;
}
五秒打印一次,终端还可以输入命令取改变寄存器和线圈值