Visual Studio 上基于libmodbus库的modbus RTU模式的通讯模拟
本文主要为《Modbus 软件开发实战指南》的代码。看这本书的缘由是,我在做libmodbus+qt上位机,参考了网上的一些资料,照着来了一遍,总是编译不过,就在博客中搜索,意外中看到了这本书,下载pdf居然也要充钱收费,于是借了书,针对其中有用的部分,码字分享之,希望对各位有用。
libmodbus库的编译
libmodbus的库在VS平台上的编译,与在Msys上的编译略有不同。编译的步骤如下:
- 在github上下载libmodbus源代码,下载网址:https://github.com/stephane/libmodbus.
- 解压完成后,在src/win32文件目录下,双击configure.js,生成了config.h文件和modbus-version.h文件。
- 打开项目文件modbus-9.sln,在弹出的文件升级对话框中,点击确定。
- 将源文件夹下的modbus-version.h替换成新生成的modbus-version.h。
- 右键点击项目,选择属性,打开属性对话框,选择该VS版本下的SDK,并去除掉Version选项的定义。
- 编译通过。生成modbus.dll和modbus.lib文件。
RTU Master端的编写
新建项目工程,将之前生成的dll文件、lib文件以及libmodbus库里的头文件,复制到项目文件所在的目录下,并通过添加现有文件到项目中,至此libmodbus中的各种接口函数可以应用了。
1.TestRtuMaster.cpp
#include <stdio.h>
#ifdef _MSC_VER
#ifndef _UNISTD_H /*unistd.h头文件的替代*/
#define _UNISTD_H
#include <io.h>
#include <process.h>
#endif
#endif
#include <string.h>
#include<stdlib.h>
#include <errno.h> //查看错误代码的头文件
#include "modbus.h" //引用libmodbus数据库
#define LOOP 1 //宏定义循环次数
#define SERVER_ID 17 //从机端设备地址
#define ADDRESS_START 0 //测试寄存器的起始地址
#define ADDRESS_END 99 //测试寄存器的结束地址
int main(void)
{
modbus_t *ctx; //modbus_t结构体指针变量
int rc;
int nb_fail; //表示什么
int nb_loop;
int addr;
int nb; //寄存器的个数
uint8_t *tab_rq_bits;
uint8_t *tab_rp_bits;
uint16_t *tab_rq_registers;
uint16_t *tab_rw_rq_registers;
uint16_t *tab_rp_registers;
/*RTU*/
ctx = modbus_new_rtu("COM3", 19200, 'N', 8, 1); //生成并初始化一个modbus结构体在串行线路中使用RTU模式通讯,N表示无奇偶位校验
modbus_set_slave(ctx, SERVER_ID); //设置从机端地址
modbus_set_debug(ctx, TRUE); //启动调试模式,标志位设置为TRUE,表示会在stdout和stderr(标准输出设备)上显示很多冗长的信息,用于显示modbus消息的字节
if (modbus_connect(ctx) == -1) //建立modbus连接,若连接成功,该函数返回0,否则,返回-1,并将errno设置为底层系统调用的定义值
{
fprintf(stderr, "Connection failed :%s\n", modbus_strerror(errno)); //通过调用errno,捕获程序失败的原因
modbus_free(ctx); //释放已分配的modbus结构体
return -1;
}
nb = ADDRESS_END - ADDRESS_START; //计算需测试的寄存器个数
//申请内存,用于保存发送以及接收的数据
tab_rq_bits = (uint8_t *)malloc(nb * sizeof(uint8_t));
memset(tab_rq_bits, 0, nb * sizeof(uint8_t));
tab_rp_bits = (uint8_t *)malloc(nb * sizeof(uint8_t));
memset(tab_rp_bits, 0, nb * sizeof(uint8_t));
tab_rq_registers = (uint16_t *)malloc(nb * sizeof(uint16_t));
memset(tab_rq_registers, 0, nb * sizeof(uint16_t));
tab_rp_registers = (uint16_t *)malloc(nb * sizeof(uint16_t));
memset(tab_rp_registers, 0, nb * sizeof(uint16_t));
tab_rw_rq_registers = (uint16_t *)malloc(nb * sizeof(uint16_t));
memset(tab_rw_rq_registers, 0, nb * sizeof(uint16_t));
nb_loop = nb_fail = 0;
while (nb_loop++ < LOOP)
{
//从起始地址开始顺序测试
for (addr = ADDRESS_START; addr < ADDRESS_END; addr++)
{
int i;
//生成随机数
for (i = 0; i < nb; i++)
{
tab_rq_registers[i] = (uint16_t)(65535.0 * rand() / (RAND_MAX + 1.0));
tab_rw_rq_registers[i] = ~tab_rq_registers[i];
tab_rq_bits[i] = tab_rq_registers[i] % 2;
}
nb = ADDRESS_END - addr;
rc = modbus_write_bit(ctx, addr, tab_rq_bits[0]); //这个函数的第三个参数是status值,不对应,我有点不太理解
if (rc != 1)
{
printf("ERROR modbus _write_bit(%d)\n", rc);
printf("Address = %d, value = %d\n", addr, tab_rq_bits[0]);
nb_fail++; //nb_fail==nb_loop
}
else
{
//写入之后,读取并进行比较
rc = modbus_read_bits(ctx, addr, 1, tab_rp_bits);
if (rc != 1 || tab_rq_bits[0] != tab_rp_bits[0])
{
printf("ERROR modbus_read_bits single(%d)\n", rc);
printf("address =%d\n", addr);
nb_fail++;
}
}
/*测试线圈寄存器的批量读写*/
rc = modbus_write_bits(ctx, addr, nb, tab_rq_bits); //对应0x0F功能
if (rc != nb)
{
printf("ERROR modbus_write_bits(%d)\n", rc);
printf("Address=%d,nb=%d\n", addr, nb);
nb_fail++;
}
else
{
//写入之后,再读取并比较
rc = modbus_read_bits(ctx, addr, nb, tab_rp_bits); //对应0x01功能
if (rc != nb)
{
printf("ERROR modbus_read_bits\n");
printf("Address=%d,nb=%d\n", addr, nb);
nb_fail++;
}
else
{
//进行比较
for (i = 0; i < nb; i++)
{
if (tab_rp_bits[i] != tab_rq_bits[i])
{
printf("ERROR modbus_read_bits\n");
printf("Addr=%d,Val=%d(0x%X)!=%d(0x%X)\n", addr, tab_rq_bits[i], tab_rq_bits[i], tab_rp_bits[i], tab_rp_bits[i]);
nb_fail++;
}
}
}
}
/*测试保持寄存器的单个读写*/
rc = modbus_write_register(ctx, addr, tab_rq_registers[0]); //写单个寄存器,对应功能0x06
if (rc != 1)
{
printf("ERROR modbus_write_register(%d)\n", rc);
printf("Address=%d,Val=%d(0x%X)\n", addr, tab_rq_registers[0], tab_rq_registers[0]);
nb_fail++;
}
else
{
//写入后再进行读取
rc = modbus_read_registers(ctx, addr, 1, tab_rp_registers); //读取保持寄存器,对应功能0x03
if (rc != 1)
{
printf("ERROR modbus_read_registers(%d)\n", rc);
printf("Address = %d\n", addr);
nb_fail++;
}
else
{
//读取后进行比较
if (tab_rq_registers[0] != tab_rp_registers[0])
{
printf("ERROR modbus_read_registers\n");
printf("Address=%d,Val=%d(0x%X)!=%d(0x%X)\n", addr, tab_rq_registers[0], tab_rq_registers[0], tab_rp_registers[0], tab_rp_registers[0]);
nb_fail++;
}
}
}
/*测试多个寄存器的批量读写*/
rc = modbus_write_registers(ctx, addr, nb, tab_rq_registers); //写多个寄存器,对应功能0x16
if (rc != nb)
{
printf("ERROR modbus_write_registers(%d)\n", rc);
printf("Address=%d,nb=%d\n", addr, nb);
nb_fail++;
}
else
{
rc = modbus_read_registers(ctx, addr, nb, tab_rp_registers); //读取保持寄存器,对应功能0x03
if (rc != nb)
{
printf("ERROR modbus_read_registers(%d)\n", rc);
printf("Address=%d\n", addr, nb);
nb_fail++;
}
else
{
for (i = 0; i < nb; i++)
{
//进行比较
if (tab_rq_registers[i] != tab_rp_registers[i])
{
printf("ERROR modbus_read_registers\n");
printf("Address=%d,value%d(0x%X)!=%d(0x%X)\n,addr,tab_rq_registers[i],tab_rq_registers[i],tab_rp_registers[i],tab_rp_registers[i]");
nb_fail++;
}
}
}
/*读写多个寄存器的测试*/
rc = modbus_write_and_read_registers(ctx, addr, nb, tab_rw_rq_registers, addr, nb, tab_rp_registers);//读写多个寄存器,对应功能ox17
if (rc != nb)
{
printf("ERROR modbus_read_and_write_registers(%d)\n", rc);
printf("Address=%d,nb=%d\n", addr, nb);
nb_fail++;
}
else
{
//读取并比较
for (i = 0; i < nb; i++)
{
if (tab_rp_registers[i] != tab_rw_rq_registers[i])
{
printf("ERROR modbus_read_and_write_registers READ\n");
printf("Address=%d,value%d(0x%X)!=%d(0x%X)\n", addr, tab_rp_registers[i], tab_rq_registers[i], tab_rp_registers[i], tab_rw_rq_registers[i]);
nb_fail++;
}
}
rc = modbus_read_registers(ctx, addr, nb, tab_rp_registers);
if (rc != nb)
{
printf("ERROR modbus_read_registers(%d)\n", rc);
printf("Address=%d,nb=%d\n", addr, nb);
nb_fail++;
}
else
{
for (i = 0; i < nb; i++)
{
if (tab_rw_rq_registers[i] != tab_rp_registers[i])
{
printf("ERROR modbus_read_and_write_registers\n");
printf("Address=%d,Val %d(0x%X)!=%d(0x%X)\n", addr, tab_rw_rq_registers[i], tab_rw_rq_registers[i], tab_rp_registers[i], tab_rp_registers[i]);
nb_fail++;
}
}
}
}
}
printf("Test:");
if (nb_fail)
printf("%d FAIL\n", nb_fail);
else
printf("SUCCESS\n");
}
/*释放内存空间*/
free(tab_rq_bits);
free(tab_rp_bits);
free(tab_rq_registers);
free(tab_rp_registers);
free(tab_rw_rq_registers);
/*关闭连接*/
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
}
其中有几个需要补充的说明和几个函数用法介绍如下:
- unistd.h头文件无法使用,采用了它的替换方式;
- include <errno.h>:是查看错误代码的头文件;
- modbus_new_rtu():用来生成并初始化一个modbus_t结构体,在串行线路中使用RTU模式通讯,N表示无奇偶位校验;
- modbus_set_slave():设置从机端的ID;
- modbus_set_debug():启动调试模式,标志位设置为TRUE,表示会在stdout和stderr(标准输出设备)上显示很多冗长的信息,用于显示modbus消息的字节;
- modbus_connect():建立modbus连接,若连接成功,该函数返回0,否则,返回-1,并将errno设置为底层系统调用的定义值;
- modbus_strerror():通过调用errno,可捕获程序失败的原因;
- modbus_free():释放已分配的modbus_t结构体;
- modbus_write_bit():线圈的单个写操作
int modbus_write_bit(modbus_t *ctx,int addr,int status),其中status为True或false,对应Modbus功能码0x05; - modbus_read_bits():线圈的读操作
int modbus_read_bits(modbus_t *ctx,int addr,int nb,uint8_t *dest),表示将nb位(线圈)的状态读取到地址为addr的设备上,并存储到dest数组中去,设置为TRUE或FALSE的无符号字节。对应Modbus功能码0x01; - modbus_write_bits(): 多个线圈的批量写操作
int modbus_write_bits(modbus_t *ctx,int addr,int nb,const uint8_t *src),表示从设备地址addr处的src数组写入nb位(线圈)的状态,其中src的数组要包含设置有TRUE或FALSE的字节,对应Modbus功能码0x0F; - modbus_write_register(): 单个保持寄存器的写操作
int modbus_write_register(modbus_t *ctx,int addr,int value),用于将保持寄存器的值写在地址为addr的设备上,对应Modbus功能码为0x06; - modbus_write_registers():多个保持寄存器的写操作
int modbus_write_registers(modbus_t *ctx,int addr,int nb,const uint16_t *src);表示将src数组中的内容写到地址为addr的nb个寄存器中,对应Modbus功能码为0x16; - modbus_read_registers():读取保持寄存器
int modbus_read_registers(modbus_t *ctx,int addr, int nb, uint16_t *dest),表示读取地址设备在addr处开始的nb位(保持寄存器)的状态,读取结果以uint16位的形式存储在dest数组中,对应Modbus功能码0x03; - modbus_write_and_read_registers:多个寄存器的读和写
int modbus_write_and_read_registers(modbus_t *ctx, int write_addr, int write_nb, const uint16_t *src, int read_addr, int read_nb, const uint16_t *dest),表示将src数组中的内容写到远程设备write_addr地址处的一组write_nb个寄存器,然后读取read_addr处的一组read_nb个寄存器内容并保存到dest数组,对应Modbus功能码0x17。
RTU-Master的代码流程小结
1.初始化并生成modbus_t 结构体;
2.设置从机端的ID;
3.启动调试模式;
4.建立modbus连接;
5.申请内存;
6.寄存器/线圈的赋值;
7.线圈寄存器的单个读写/批量读写/保持寄存器的单个读写/批量读写/读取多个寄存器;
8.释放内存;
9.关闭modbus连接;
10.释放modbus结构体
RTU-Slave端的编写
同上
1.TestRtuSlave.cpp:
#include <stdio.h>
#ifndef _MSC_VER
#endif
#include <stdlib.h>
#include <errno.h>
#include "modbus.h"
#define SERVER_ID 17
int main(void)
{
modbus_t *ctx;
modbus_mapping_t *mb_mapping;
/*RTU*/
ctx = modbus_new_rtu("COM2", 19200, 'N', 8, 1); //创建一个RTU的容器
modbus_set_slave(ctx, SERVER_ID);
modbus_set_debug(ctx, TRUE);
if (modbus_connect(ctx) == -1)
{
fprintf(stderr, "Connection failed:%s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
mb_mapping = modbus_mapping_new(500, 500, 500, 500);
if (mb_mapping == NULL)
{
fprintf(stderr, "Error mapping: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
for (;;)
{
uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
int rc;
rc = modbus_receive(ctx, query);
if (rc > 0)
{
modbus_reply(ctx, query, rc, mb_mapping);
}
else
{
printf("Connection Closed\n");
}
}
printf("Quit the loop :%s\n", modbus_strerror(errno));
modbus_mapping_free(mb_mapping);
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
补充函数说明
- modbus_mapping_new():
modbus_mapping_t modbus_mapping_new(int nb_bits,int nb_input_bits,int nb_registers,int nb_registers);
modbus_mapping_new()函数需要分配4个数组来存储位、输入位、寄存器和输入寄存器。若不需要为特定类型的数据分配数组,可在参数中传递0值,关联指针则为NULL。
该函数返回一个modbus_mapping_t指针。 - modbus_receive():
int modbus_receive(modbus_t *ctx,uint8_t *req);
modbus_receive()函数是Modbus从机端/服务器使用此功能接收和分析主机端/客户机发送的指示请求。 - modbus_reply():
*int modbus_reply(modbus_t *ctx,const uint8_t *req,int req_length,modbus_mapping_t *mb_mapping);
modbus_reply()函数是对接收到的请求发送响应,先是分析参数中请求req,然后利用ctx来构建和发送响应。此功能是为Modbus 服务器设计的。 - modbus_mapping_free():
void modbus_mapping_free(modbus_mapping_t *mb_mapping);
modbus_mapping_free()函数将释放mb_mapping_t结构中的四个数组,并最终释放mb_mapping所引用的mb_mapping_t。
RTU-Slave的代码流程小结:
1.初始化并生成modbus_t 结构体;
2.设置从机端的ID;
3.起点调试模式;
4.建立modbus连接;
5.modbus_mapping_new()初始化寄存器,返回一个modbus_mapping_t 指针;
6.调用modbus_receive()函数判断串口的接收数据,负责接收和分析;
7.调用modbus_reply()函数,对接收到的请求指示发送响应(回复);
8.释放modbus_mapping_t 结构体;
9.关闭modbus连接;
10.释放modbus_t 结构体。
创建虚拟串口
下载Configure Virtual Serial Port Driver,新建虚拟端口COM2和COM3,可在设备管理器中查询是否建立。
分别启动两个项目,则可完成modbus RTU模式的通讯。
至此,Modbus RTU模式的通讯测试完成,希望对你有用,谢谢~
参考1:libmodbus手册翻译
参考2:《Modbus 软件开发实战指南》
参考3:modbus之libmodbus