单片机点灯小能手 · 2021年08月06日

Modbus驱动库—libmodbus的使用

这篇文章是接上一篇Modbus协议简介,主要介绍Modbus实际项目应用—libmodbus驱动库的使用,断断续续写了近一周时间。

image.png

都有哪些内容?

  • 为什么要使用驱动库
  • libmodbus简介
  • libmodbus常用函数
  • Windows平台使用libmodbus
  • Linux平台使用libmodbus
  • ARM平台使用libmodbus
  • libmodbus从机地址限制的问题
  • 测试代码获取

为什么要使用驱动库?

上一篇文章,我们介绍了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

所以作为主机,写数据的流程是:

  1. 构建一个Modbus-RTU数据帧
  2. 等待从机响应的数据
  3. 如果响应数据正确,说明写入成功,否则写入失败。

读数据也是同样的流程,我们可以基于串口发送、串口接收函数、定时器等,自己写一个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

image.png

GitHub仓库

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命令行解压:

image.png

源代码

我们重点关注以下3个文件夹:doc,src,tests。

  • doc,doc文件夹包含库的使用文档,文件名就是函数名,介绍每个函数的使用方法,参数定制,返回值说明,示例代码等。

image.png

帮助文档

  • src,src文件夹是libmodbus库源文件和头文件,我们只需要把这些文件添加到工程中,然后包含头文件就可以直接使用了。

image.png

源代码

  • tests,tests文件夹包含libmodbus使用示例,

image.png
测试代码

包括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.hMakefile

如果想使用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

最终的文件目录:

image.png

测试文件

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.exetest_rtu_slave.exe

这里,我的电脑本机虚拟了两个串口COM1和COM2,两个串口直接进行连接。

先启动从机设备,配置为COM1:

$ ./test_rtu_slave.exe "COM1"

再启动主机设备,配置为COM2:

$ ./test_rtu_master.exe "COM2"

image.png

测试结果

可以看到,从机可以正确的对接收的数据帧进行相应,主机可以正确的进行读取和写入。

如果需要测试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作为广播地址:
image.png

广播地址0xfe

所以使用libmodbus会出现报错终止运行的问题,这是因为libmodbus源代码中限制了从机地址1-247,我们只需要修改源代码即可。

modbus-rtu.c文件95行:

image.png

modbus-rtu

modbus-tcp.c文件80行:

image.png

modbus-tcp

只需要修改这两个数值就可以取消从机地址限制的问题。

详细的从站最大地址限制问题排查记录,可以查看:

blog.csdn.net/qingzhuyuxian/article/details/80391553

其实这个问题,早在2011年,就有人在官方GitHub仓库提Issues了:

github.com/stephane/libmodbus/issues/38

image.png
issue

对此问题,作者的答复是,为了遵循Modbus官方标准,所以一直以来都没有进行修改。

首发:电子电路开发学习
作者: wcc149

推荐阅读

推荐阅读
关注数
3053
内容数
82
电子电路、单片机、嵌入式、物联网等技术文章分享。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息