系列文章目录
Modbus通讯开发随记1——LibModbus库的学习;
Modbus通讯开发随记3——基于LibModbus库的简易Modbus主机类封装(C++版);
1. LibModbus安装和使用
关于LibModbus驱动库的安装推荐参考:
[LibModbus驱动库的使用]([https://blog.csdn.net/whik1194/article/details/119010616]
- 下载
源代码建议下载官方仓库稳定版本:https://libmodbus.org/releases/libmodbus-3.1.6.tar.gz - 解压
解压后将生成.\libmodbus-3.1.6文件夹,其中.\libmodbus-3.1.6\src中为LibModbus库源文件和头文件;.\libmodbus-3.1.6\doc中为各个库函数的介绍文档;.\libmodbus-3.1.6\tests中为用例 - 库安装
Libmodbus库在Windows下的安装和使用 - 将Config.h与./src目录下的*.c与*.h文件复制到项目目录(文件夹)下。
2 保存寄存器测试demo
2.1 从机demo
代码如下(示例):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "modbus.h"
int main(int argc, char *argv[]){
int socket=-1;
uint8_t *query;
modbus_t *mb;
int ret;
_modbus_mapping_t *mb_mapping;
mb=modbus_new_tcp("127.0.0.1", 1502);
query = (uint8_t *)malloc(MODBUS_TCP_MAX_ADU_LENGTH);
if((mb_mapping=modbus_mapping_new_start_address(0,0,0,0,15,3,0,0))==NULL){
modbus_free(mb);
printf("new map failed: %s\n", modbus_strerror(errno));
return 0;
}
mb_mapping->tab_registers[0]=0x1001;
mb_mapping->tab_registers[1]=0x1002;
mb_mapping->tab_registers[2]=0x1003;
socket = modbus_tcp_listen(mb, 1);
modbus_tcp_accept(mb, &socket);
printf("create modbus slave success\n");
while(1){
do{ /*轮询串口数据*/
ret=modbus_receive(mb,query);
}while(ret==0);
if(ret>0){
printf("len=%02d\t",ret); /*%02d格式限定符,其中0表示变量宽度不足时以0作为填充,2表示显示宽度至少为2,d表示十进制整数*/
for(int i=0; i<ret; ++i)
printf("%02x", query[i]);
printf("\n");
modbus_reply(mb,query,ret,mb_mapping);
}
else{
printf("quit the loop: %s\n", modbus_strerror(errno));
break;
}
}
modbus_mapping_free(mb_mapping);
modbus_close(mb);
free(query);
modbus_free(mb);
return 0;
}
2.2 主机demo
代码如下(示例):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "modbus.h"
int main(int argc, char *argv[]){
uint16_t table[3];
modbus_t *mt;
mt = modbus_new_tcp("127.0.0.1", 1502);
if(modbus_connect(mt)==-1){
modbus_free(mt);
printf("connect failed: %s\n", modbus_strerror(errno));
return 0;
}
while(1){
if(modbus_read_registers(mt, 0X0F, 3, table)==3)
printf("read success: 0x%04x 0x%04x 0x%04x \n",table[0],table[1],table[2]);/*%04x格式限定符
,其中0表示变量宽度不足时以0作为填充,4表示显示宽度至少为4,x表示十六进制整数*/
else{
printf("read error: %s\n", modbus_strerror(errno));
break;
}
for(int i=0; i<3; ++i)
table[i]++;
if(modbus_write_registers(mt, 0X0F, 3, table)==3)
printf("write success: 0x%04x 0x%04x 0x%04x \n",table[0],table[1],table[2]);
else{
printf("write error: %s\n", modbus_strerror(errno));
break;
}
Sleep(1000);
}
modbus_close(mt);
modbus_free(mt);
system("pause");
return 0;
}
2.3 运行
先运行从机使之进入监听状态,再运行主机两者即进入通讯状态。
./master_demo.exe
read success: 0x1001 0x1002 0x1003 #主机读取从机对应寄存器的值
write success: 0x1002 0x1003 0x1004 #主机将对应寄存器的值处理后再写入从机的寄存器
read success: 0x1002 0x1003 0x1004
write success: 0x1003 0x1004 0x1005
read success: 0x1003 0x1004 0x1005
write success: 0x1004 0x1005 0x1006
read success: 0x1004 0x1005 0x1006
write success: 0x1005 0x1006 0x1007
#保存寄存器既可以读也可以写
./slave_demo.exe
create modbus slave success
len=12 000100000006ff03000f0003
len=19 00020000000dff10000f000306100210031004
len=12 000300000006ff03000f0003
len=19 00040000000dff10000f000306100310041005
len=12 000500000006ff03000f0003
len=19 00060000000dff10000f000306100410051006
len=12 000700000006ff03000f0003
len=19 00080000000dff10000f000306100510061007
quit the loop: No error
Demo中的读保持寄存器的报文格式(以从站第1条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 |
---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
0001 | 0000 | 0006 | ff | 03 | 00 | 0f | 00 | 03 |
释义:读取服务器(标识符0xff)保持寄存器(功能码0x03),从0x000f(起始寄存器高位+起始寄存器低位)开始的0x0003(寄存器数量高位+寄存器数量低位)个寄存器的值。
Demo中的写保持寄存器的报文格式(以从站第2条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 | 字节计数 | 字节的值 |
---|---|---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 字节计数 |
0002 | 0000 | 000d | ff | 10 | 00 | 0f | 00 | 03 | 06 | 100210031004 |
释义:写入服务器(标识符0xff)保持寄存器(功能码0x10),从0x000f(起始寄存器高位+起始寄存器低位)开始的0x0003(寄存器数量高位+寄存器数量低位)个寄存器,写入数值为0x1002、0x1003、0x1004(每个保持寄存器占2字节)。
3 输入寄存器测试
3.1 从机demo
在2.1示例中作如下修改:
//增加输入寄存器的映射
if((mb_mapping=modbus_mapping_new_start_address(0,0,0,0,0x0f,0x03,0x20,0x03))==NULL){
modbus_free(mb);
printf("new map failed: %s\n", modbus_strerror(errno));
return 0;
}
mb_mapping->tab_registers[0]=0x1001;
mb_mapping->tab_registers[1]=0x1002;
mb_mapping->tab_registers[2]=0x1003;
#增加输入寄存器赋值
mb_mapping->tab_input_registers[0]=0x1001;
mb_mapping->tab_input_registers[1]=0x1002;
mb_mapping->tab_input_registers[2]=0x1003;
3.2 主机demo
在2.2示例中作如下修改:
//将while(1)循环体内部代码段作如下修改
if(modbus_read_input_registers(mt, 0X20, 3, table)==3) #读取寄存器的函数需要改为modbus_read_input_registers(),若不修改将显示“Illegal data address”(非法数据地址),在LibModbus中每种寄存器的读写都有对应的方法,使用时它会检查寄存器地址是否合法
printf("read success: 0x%04x 0x%04x 0x%04x \n",table[0],table[1],table[2]);/*%04x格式限定符
,其中0表示变量宽度不足时以0作为填充,4表示显示宽度至少为4,x表示十六进制整数*/
else{
printf("read error: %s\n", modbus_strerror(errno));
break;
}
Sleep(1000);
3.3 运行
先运行从机使之进入监听状态,再运行主机两者即进入通讯状态。
./master_demo.exe
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
read success: 0x1001 0x1002 0x1003
#输入寄存器只可读不可写
./slave_demo.exe
create modbus slave success
len=12 000100000006ff0400200003
len=12 000200000006ff0400200003
len=12 000300000006ff0400200003
len=12 000400000006ff0400200003
len=12 000500000006ff0400200003
len=12 000600000006ff0400200003
len=12 000700000006ff0400200003
len=12 000800000006ff0400200003
Demo中的读保持寄存器的报文格式(以从站第1条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 |
---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
0001 | 0000 | 0006 | ff | 04 | 00 | 20 | 00 | 03 |
释义:读取服务器(标识符0xff)输入寄存器(功能码0x04)从0x0020(起始寄存器高位+起始寄存器低位)开始的0x0003(寄存器数量高位+寄存器数量低位)个寄存器的值。
4 线圈寄存器测试
4.1 从机demo
在3.1示例中作如下修改:
//增加线圈状态寄存器的映射
if((mb_mapping=modbus_mapping_new_start_address(0x30,0x03,0,0,0x0f,0x03,0x20,0x03))==NULL){
modbus_free(mb);
printf("new map failed: %s\n", modbus_strerror(errno));
return 0;
}
mb_mapping->tab_bits[0]=true;
mb_mapping->tab_bits[1]=false;
mb_mapping->tab_bits[2]=false;
4.2 主机demo
在3.2示例中作如下修改:
uint8_t table[3] //更改table数组的声明,libModbus中每个线圈寄存器占1字节
//将while(1)循环体内部代码段作如下修改
if(modbus_read_bits(mt, 0x30, 3, table)==3){ //读线圈寄存器状态
printf("read success:\n");
}
else{
printf("read error: %s\n", modbus_strerror(errno));
break;
}
for(int i=0; i<3; ++i)
printf("Line circuit %d: %s\n", i+1, table[i]?"ON":"OFF");
//将线圈寄存器值置反
table[0]=table[0]?false:true;
table[1]=table[1]?false:true;
table[2]=table[2]?false:true;
if(modbus_write_bits(mt, 0X30, 3, table)==3) //写线圈寄存器状态
printf("write success:\n");
else{
printf("write error: %s\n", modbus_strerror(errno));
break;
}
printf("----------------------------------\n");
Sleep(1000);
4.3 运行
先运行从机使之进入监听状态,再运行主机两者即进入通讯状态。
./master_demo.exe
read success:
Line circuit 1: ON
Line circuit 2: OFF
Line circuit 3: OFF
write success:
----------------------------------
read success:
Line circuit 1: OFF
Line circuit 2: ON
Line circuit 3: ON
write success:
----------------------------------
read success:
Line circuit 1: ON
Line circuit 2: OFF
Line circuit 3: OFF
write success:
#线圈寄存器支持读和写
----------------------------------
./slave_demo.exe
create modbus slave success
len=12 000100000006ff0100300003
len=14 000200000008ff0f003000030106
len=12 000300000006ff0100300003
len=14 000400000008ff0f003000030101
len=12 000500000006ff0100300003
len=14 000600000008ff0f003000030106
Demo中的读线圈寄存器的报文格式(以从站第1条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 |
---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
0001 | 0000 | 0006 | ff | 01 | 00 | 30 | 00 | 03 |
释义:读取服务器(标识符0xff)线圈寄存器(功能码0x01)从0x0030(起始寄存器高位+起始寄存器低位)开始的0x0003(寄存器数量高位+寄存器数量低位)个线圈寄存器的状态值(开true或关false)。
Demo中的写线圈寄存器的报文格式(以从站第2条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 | 字节计数 | 字节的值 |
---|---|---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 字节计数 |
0002 | 0000 | 0008 | ff | 0f | 00 | 30 | 00 | 03 | 01 | 06 |
释义:写入服务器(标识符0xff)保持寄存器(功能码0x10),从0x000f(起始寄存器高位+起始寄存器低位)开始的0x0003(寄存器数量高位+寄存器数量低位)个寄存器,写入寄存器状态0x06(00000110,由低位开始算第一个线圈0,第二个线圈1,第3个线圈1)。
注意!Modbus报文中线圈是以bit位为存储单元的,而在libmodbus从机镜像中线圈是以byte字节为存储单元的。
5 离散输入状态寄存器测试
5.1 从机demo
在4.1示例中作如下修改:
//增加离散输入线圈状态寄存器的映射
if((mb_mapping=modbus_mapping_new_start_address(0x30,0x08,0x00,0x08,0x0f,0x03,0x20,0x03))==NULL){
modbus_free(mb);
printf("new map failed: %s\n", modbus_strerror(errno));
return 0;
}
mb_mapping->tab_input_bits[0]=true;
mb_mapping->tab_input_bits[1]=false;
mb_mapping->tab_input_bits[2]=false;
mb_mapping->tab_input_bits[3]=true;
mb_mapping->tab_input_bits[4]=false;
mb_mapping->tab_input_bits[5]=false;
mb_mapping->tab_input_bits[6]=false;
mb_mapping->tab_input_bits[7]=false;
5.2 主机demo
在4.2示例中作如下修改:
//离散输入线圈的读取使用modbus_read_input_bits()函数
if(modbus_read_input_bits(mt, 0x00, 8, table)==8){
printf("read success:\n");
}
5.3 运行
先运行从机使之进入监听状态,再运行主机两者即进入通讯状态。
./master_demo.exe
read success:
Line circuit 1: ON
Line circuit 2: OFF
Line circuit 3: OFF
Line circuit 4: ON
Line circuit 5: OFF
Line circuit 6: OFF
Line circuit 7: OFF
Line circuit 8: OFF
./slave_demo.exe
create modbus slave success
len=12 000100000006ff0200000008
len=12 000200000006ff0200000008
len=12 000300000006ff0200000008
len=12 000400000006ff0200000008
Demo中的读离散输入线圈寄存器的报文格式(以从站第1条报文为例):
事务处理标识符 | 协议标识符 | 长度(字节) | 单元标识符 | 功能码 | 起始寄存器高位 | 起始寄存器低位 | 寄存器数量高位 | 寄存器数量低位 |
---|---|---|---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
0001 | 0000 | 0006 | ff | 02 | 00 | 00 | 00 | 08 |
释义:读取服务器(标识符0xff)的离散输入线圈寄存器(功能码0x02)从0x0000(起始寄存器高位+起始寄存器低位)开始的0x0008(寄存器数量高位+寄存器数量低位)个线圈寄存器的状态值(开true或关false)。
总结
主从机的定义、通讯建立、从机寄存器镜像的存储结构、线圈寄存器读取和写入、离散输入寄存器的读取、保持寄存器的读取和写入、输入寄存器的读取是Modbus通讯开发的基础,本文以ModbusTCP示例演示了libmodbus库相关接口的使用,希望对看到这篇博文的大家学习Modbus有所帮助。在这篇博文编辑的过程中发现LibModbus库中RTU从机无法进入监听,运行至connect()函数直接跳出,并无errno,希望路过的大神有了解的指正一下。