为什么要使用驱动库?
上一篇文章,我们介绍了Modbus协议物理层和协议层,我们知道了Modbus是一种总线协议,它可以基于串口或网口,以基于串口的Modbus-RTU为例,我们需要在Windows或Linux下实现一个上位机,上位机的功能是读写Modbus接口传感器设备的数据,或者是和单片机等从设备进行交互。
当需要向某个从机寄存器写入某个值时,如向01地址的设备,0x0105保持寄存器写入1个数据:0x0190为例,那么需要构建这样一个数据帧:
主机发送:01 06 01 05 01 90 99 CB
01表示从机地址,06功能码表示写单个保持寄存器,0105表示寄存器地址,0190表示写入寄存器的数值,99 CB为CRC校验值。
如果从机正确的收到了数据,会回复一个数据帧:
从机回复:01 06 01 05 01 90 99 CB
所以作为主机,写数据的流程是:
- 构建一个Modbus-RTU数据帧
- 等待从机响应的数据
- 如果响应数据正确,说明写入成功,否则写入失败。
读数据也是同样的流程,我们可以基于串口发送、串口接收函数、定时器等,自己写一个Modbus驱动库,来实现对从设备的读写。当然,也可以直接使用别人写好的Modbus驱动库,比如libmodbus,本文将介绍如何使用libmodbus驱动库,Windows/Linux/ARM平台实现Modbus主机和从机。
libmodbus简介
libmodbus,是一个基于C语言实现的Modbus驱动库,作者是Stephane,支持Linux, Mac OS X, FreeBSD, QNX and Win32操作系统,主要应用在PC上,用来开发上位机,也可以对源代码进行交叉编译,以适配更多的平台,比如ARM Linux。源代码开源,遵循 LGPL-2.1 许可。目前最新版本是3.1.6,Github仓库最新提交时间是2021年5月21日。
官方网站:www.libmodbus.org
开源地址:
github.com/stephane/libmodbus
libmodbus支持如下功能:
- 支持Modbus-RTU和Modbus-TCP
- 支持常用功能码,如01/02/03/04/05/06/07/0F/10/11/16/17
- 支持线圈类型读写、寄存器读写、离散量读取等
- 支持广播地址0,从机地址1-247
- 支持浮点数和整形数据转换,大端小端等多种模式
- 参数根据
Modbus_Application_Protocol_V1_1b.pdf
官方标准文档设计,比如最大读写线圈个数,最大读写寄存器个数等。 - 源代码基于C编写,方便在各平台移植,只有11个文件。
libmodbus常用函数
libmodbus库函数非常简洁,读写操作函数对于RTU和TCP完全通用,RTU和TCP切换只需要修改一行代码就可以实现无缝切换。
modbus_t *mb; int ret; //创建一个modbus-rtu对象,指定串口号,波特率,校验位,数据位,停止位 //成功返回指针,否则返回NULL, 会调用malloc申请内存 mb = modbus_new_rtu("/dev/ttySP1", 115200, 'N', 8, 1); //linux mb = modbus_new_rtu("COM1", 115200, 'N', 8, 1); //windows //创建modbus-tcp对象,指定IP地址和端口号 mb = modbus_new_tcp("127.0.0.1", 502); //TCP/IP //设置从机地址,成功返回0, 否则返回-1 ret = modbus_set_slave(mb, slave); //连接Modbus主机,成功返回0, 否则返回-1 ret = modbus_connect(mb); //设置响应超时时间1s,200ms ret = modbus_set_response_timeout(mb, 1, 200000); //读取寄存器数据,起始地址2, 数量5, 保存到table数组中 //成功返回5, 否则返回-1 uint16_t *table; ret = modbus_read_registers(mb, 2, 5, table); //modbus设备关闭和释放内存 modbus_close(mb); modbus_free(mb); //写单个寄存器, 地址2写入56, 成功返回1,否则返回-1 ret = modbus_write_register(mb, 2, 56); //写多个寄存器, 地址12起始,写入5个数据,成功返回5,否则返回-1 uint16_t table[5] = {11, 22, 33, 44, 55}; ret = modbus_write_registers(mb, 12, 5, table); //写单个线圈,线圈地址写入TRUE,成功返回1,否则返回-1 ret = modbus_write_bit(mb, 11, TRUE); //查看错误信息 char *err_str; err_str = modbus_strerror(errno);
Windows平台libmodbus 使用
以Windows下使用libmodbus实现从机和主机为例,Linux下类似。
1.获取源代码
使用Git工具下载GitHub代码仓库源代码到本地,这样可以获取到最新的libmodbus代码,但是也会有一些Bug。
git clone https://github.com/stephane/libmodbus/
如果下载速度缓慢,可以到我的Gitee仓库下载:
git clone https://gitee.com/whik/libmodbus
或者到官方仓库下载最新稳定发布版本v3.1.6:
libmodbus.org/releases/libmodbus-3.1.6.tar.gz
下载完成之后,解压到本地,Linux系统可以使用tar -zxvf libmodbus-3.0.6.tar.gz
命令行解压:
我们重点关注以下3个文件夹:doc,src,tests。
- doc,doc文件夹包含库的使用文档,文件名就是函数名,介绍每个函数的使用方法,参数定制,返回值说明,示例代码等。
- src,src文件夹是libmodbus库源文件和头文件,我们只需要把这些文件添加到工程中,然后包含头文件就可以直接使用了。
- tests,tests文件夹包含libmodbus使用示例,
包括Modbus-RTU/TCP客户端和服务器单元测试,随机测试,效率测试,读写10万个线圈状态,10万个寄存器,记录消耗时间。
//部分代码 nb_points = MODBUS_MAX_READ_BITS; start = gettime_ms(); for (i=0; i<n_loop; i++) { rc = modbus_read_bits(ctx, 0, nb_points, tab_bit); if (rc == -1) { fprintf(stderr, "%s\n", modbus_strerror(errno)); return -1; } } end = gettime_ms(); elapsed = end - start;
官方提供的测试代码太繁琐,后面我们会写两个简单的示例程序,来演示主机和从机的使用。
2.生成config.h配置文件
无论是Windows还是Linux,在使用libmodbus库之前,我们需要先调用configure工具来生成config.h文件和Makefile。configure工具会根据当前系统环境,生成适用于当前平台的config.h文件。
在libmodbus库文件夹下执行./configure
命令。
whik@windows_7 MINGW64 /d/libmodbus-3.1.6 $ ./configure checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for strings.h... yes ....... checking for inttypes.h... yes config.status: creating tests/unit-test.h config.status: executing libtool commands libmodbus 3.1.6 =============== prefix: /usr/local sysconfdir: ${prefix}/etc libdir: ${exec_prefix}/lib includedir: ${prefix}/include compiler: gcc cflags: -g -O2 ldflags: documentation: no tests: yes
整个过程需要1分钟左右的时间,等待运行完成之后,会发现在当前目录下多了一些文件,主要是config.h
和Makefile
如果想使用libmodbus官方提供的测试代码,可以直接在根目录执行make
命令,就可以直接编译tests目录下的测试代码,Linux系统可以使用make install
命令进行和安装。
3.编写测试代码
新建一个文件夹my_test
,把libmodbus/src文件夹中的.c和.h文件,config.h复制到my_test。
学习了libmodbus常用函数之后,我们就可以写一个简单的测试代码了。
Modbus-RTU主机测试:test_rtu_master.c,实现对地址为1的从机设备,读取地址15/16/17的保持寄存器数据,进行+1操作后,再写入。
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "modbus.h" #define PORT_NAME "COM1" int main(int argc, char *argv[]) { int ret; uint16_t table[3]; modbus_t *mb; char port[20]; printf("argc = %d, argv[1] = %s\n", argc, argv[1]); if(argc == 2) strcpy(port, argv[1]); else strcpy(port, PORT_NAME); printf("libmodbus modbu-rtu master demo: %s, 115200, N, 8, 1\n", port); mb = modbus_new_rtu(port, 115200, 'N', 8, 1); if (mb == NULL) { modbus_free(mb); printf("new rtu failed: %s\n", modbus_strerror(errno)); return 0; } modbus_set_slave(mb, 1); ret = modbus_connect(mb); if(ret == -1) { modbus_close(mb); modbus_free(mb); printf("connect failed: %s\n", modbus_strerror(errno)); return 0; } while(1) { ret = modbus_read_registers(mb, 0x0F, 3, table); if(ret == 3) printf("read success : 0x%02x 0x%02x 0x%02x \n", table[0], table[1], table[2]); else { printf("read error: %s\n", modbus_strerror(errno)); break; } for(int i = 0; i < 3; i++) table[i] += 1; ret = modbus_write_registers(mb, 0x0F, 3, table); if(ret == 3) printf("write success: 0x%02x 0x%02x 0x%02x \n", table[0], table[1], table[2]); else { printf("write error: %s\n", modbus_strerror(errno)); break; } Sleep(1000); } modbus_close(mb); modbus_free(mb); system("pause"); return 0; }
Modbus-RTU从机测试:test_rtu_slave.c,创建从机设备,地址为1,初始化了3个保持寄存器,地址分别为15/16/17,数据分别为0x1001/0x1002/0x1003。
#include "stdio.h" #include "stdlib.h" #include "string.h" #include "modbus.h" #define PORT_NAME "COM2" int main(int argc, char *argv[]) { int ret = 0; uint8_t device = 1; uint8_t *query; modbus_t *mb; modbus_mapping_t *mb_mapping; char port[20]; printf("argc = %d, argv[1] = %s\n", argc, argv[1]); if(argc == 2) strcpy(port, argv[1]); else strcpy(port, PORT_NAME); printf("libmodbus modbu-rtu slave demo: %s, 115200, N, 8, 1\n", port); mb = modbus_new_rtu(port, 115200, 'N', 8, 1); if (mb == NULL) { modbus_free(mb); printf("new rtu failed: %s\n", modbus_strerror(errno)); return 0; } //register: 15/16/17 mb_mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 15, 3, 0, 0); if(mb_mapping == NULL) { modbus_free(mb); printf("new mapping 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; modbus_set_slave(mb, device); ret = modbus_connect(mb); if(ret == -1) { modbus_free(mb); printf("connect failed: %s\n", modbus_strerror(errno)); return 0; } printf("create modbus slave success\n"); while(1) { do { ret = modbus_receive(mb, query); //轮询串口数据, } while (ret == 0); if(ret > 0) //接收到的报文长度 { printf("len=%02d: ", ret); for(int idx = 0; idx < ret; idx++) { printf(" %02x", query[idx]); } printf("\n"); modbus_reply(mb, query, ret, mb_mapping); } else { printf("quit the loop: %s", modbus_strerror(errno)); modbus_mapping_free(mb_mapping); break; } } modbus_close(mb); modbus_free(mb); return 0; }
4.编译测试代码
现学了Makefile语法,凑合用。需要注意的是,windows下libmodbus依赖于ws2_32.dll库,需要添加编译参数-lws2_32:
.PHONY: all all: test_rtu_master test_rtu_slave test_rtu_master : test_rtu_master.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o gcc test_rtu_master.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o -o test_rtu_master -lws2_32 test_rtu_slave : test_rtu_slave.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o gcc test_rtu_slave.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o -o test_rtu_slave -lws2_32 test_rtu_slave.o : test_rtu_slave.c gcc test_rtu_slave.c -c -I. test_rtu_master.o : test_rtu_master.c gcc test_rtu_master.c -c -I. modbus.o : modbus.c gcc modbus.c -c -I. modbus-rtu.o : modbus-rtu.c gcc modbus-rtu.c -c -I. modbus-tcp.o : modbus-tcp.c gcc modbus-tcp.c -c -I. modbus-data.o : modbus-data.c gcc modbus-data.c -c -I. clean: rm -rf *.o *.exe
最终的文件目录:
Windows下Make工具我使用的是Qt自带的mingw32-make.exe工具,位于\Qt5.7.0\Tools\mingw530_32\bin
目录下,执行mingw32-make
命令进行,会对两个测试文件进行编译:
whik@Windows_7 MINGW64 /d/my_test $ mingw32-make.exe gcc test_rtu_master.c -c -I. gcc modbus.c -c -I. gcc modbus-tcp.c -c -I. gcc modbus-rtu.c -c -I. gcc modbus-data.c -c -I. In file included from modbus-data.c:24:0: ./config.h:171:0: warning: "WINVER" redefined #define WINVER 0x0501 ^ In file included from D:/Program/Qt5.7.0/Tools/mingw530_32/i686-w64-mingw32/include/windows.h:10:0, from D:/Program/Qt5.7.0/Tools/mingw530_32/i686-w64-mingw32/include/winsock2.h:23, from modbus-data.c:19: D:/Program/Qt5.7.0/Tools/mingw530_32/i686-w64-mingw32/include/sdkddkver.h:162:0: note: this is the location of the previous definition #define WINVER _WIN32_WINNT ^ gcc test_rtu_master.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o -o test_rtu_master -lws2_32 gcc test_rtu_slave.c -c -I. gcc test_rtu_slave.o modbus.o modbus-tcp.o modbus-rtu.o modbus-data.o -o test_rtu_slave -lws2_32
会在当前目录下生成目标文件:test_rtu_master.exe
和test_rtu_slave.exe
。
这里,我的电脑本机虚拟了两个串口COM1和COM2,两个串口直接进行连接。
先启动从机设备,配置为COM1:
$ ./test_rtu_slave.exe "COM1"
再启动主机设备,配置为COM2:
$ ./test_rtu_master.exe "COM2"
可以看到,从机可以正确的对接收的数据帧进行相应,主机可以正确的进行读取和写入。
如果需要测试Modbus-TCP,只需要修改modbus设备创建函数:
//modbus-rtu mb = modbus_new_rtu(port, 115200, 'N', 8, 1); //modbus-tcp mb = modbus_new_tcp("127.0.0.120", 502); //指定IP地址
其他无需任何改动!
Linux平台下libmodbus使用
Ubuntu下使用libmodbus和Windows几乎一样:
//1.解压 tar -zxvf libmodbus-3.0.6.tar.gz //2.配置 ./configure //3.编译 make //4.安装 make install
测试文件和Windows几乎一样,不过不需要ws2_32库的支持了。
(来自:blog.csdn.net/qq_30650153/article/details/83385626)
ARM平台下libmodbus使用
ARM开发板下使用libmodbus,需要使用交叉编译器进行交叉编译,生成so库文件。
1.解压:
tar -zxvf libmodbus-3.0.6.tar.gz
2.创建安装目录:
mkdir install
3.配置编译选项:
./configure --host=arm-fsl-linux-gnueabi --enable-static --prefix=[安装路径]/install/
4.编译:make
5.安装:make install
在install目录会生成3个文件夹:include lib share
进入install/lib目录,执行file libmodbus*,出现如下打印信息,信息中有“ARM”说明libmodbus库移植成功。
libmodbus.a: current ar archive libmodbus.la: libtool library file, libmodbus.so: symbolic link to `libmodbus.so.5.0.5' libmodbus.so.5: symbolic link to `libmodbus.so.5.0.5' libmodbus.so.5.0.5: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked, not stripped
将libmodbus.so、libmodbus.so.5、libmodbus.so.5.0.5
复制到ARM开发板中的/usr/lib
目录下
执行cp libmodbus.so* /usr/lib
如果出现无法创建的问题(cannot create ‘/usr/lib/libmodbus.so*’: Read-only file system)。
可以执行 wr cp libmodbus* /usr/lib
测试与使用,和Windows一样,对测试文件使用ARM交叉编译器进行编译。
(来自:www.cnblogs.com/happybirthdaytoyou/p/11301612.html)
libmodbus 从机地址限制的问题
libmodbus支持1-247从机地址,0为广播地址,但是有些非标准的Modbus传感器,并不是采用0作为广播地址,而是0xfe作为广播地址:
所以使用libmodbus会出现报错终止运行的问题,这是因为libmodbus源代码中限制了从机地址1-247,我们只需要修改源代码即可。
modbus-rtu.c文件95行:
modbus-tcp.c文件80行:
只需要修改这两个数值就可以取消从机地址限制的问题。
详细的从站最大地址限制问题排查记录,可以查看:
blog.csdn.net/qingzhuyuxian/article/details/80391553
其实这个问题,早在2011年,就有人在官方GitHub仓库提Issues了:
github.com/stephane/libmodbus/issues/38
对此问题,作者的答复是,为了遵循Modbus官方标准,所以一直以来都没有进行修改。