libmodbus编程应用(超详细源码讲解+移植到stm32)

发布于:2024-10-16 ⋅ 阅读:(200) ⋅ 点赞:(0)

目录

前言

libmodbus开发库

1.功能概要

2.源码获取

3.libmodbus与应用程序的关系

libmodbus源代码解析

1.核心函数

2.框架分析与数据结构

3.情景分析

(1)初始化

(2)主设备发送请求

(3)主/从设备接收数据

(4)从设备回应

libmodbus移植与使用

1.移植方法

2.使用USB串口和板载串口作为后端

①modbus_new_st_rtu

②_modbus_rtu_send

③_sleep_response_timeout

④_modbus_rtu_flush

⑤_modbus_rtu_recv

⑥uart_device.h

总结


前言

        之前介绍了Modbus协议,见Modbus通讯协议,广泛应用于工业控制领域,协议内部有很多细节;比如报文的预处理、解析等等,所以我们需要移植别人的库,理解核心代码的主要逻辑,修改底层和硬件相关的代码就可以了,这就需要介绍libmodbus开发库。
        最后对底层的移植涉及到我之前博客的代码,建议先去看这两篇:UART开发基础移植USBX实现虚拟串口


libmodbus开发库

1.功能概要

        libmodbus是一个免费的跨平台支持RTU和TCP的Modbus库,遵循LGPL V2.1+协议。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系统。libmodbus可以向符合Modbus协议的设备发送和接收数据,并支持通过串口或者TCP网络进行连接。
        作为一个开源项目,libmodbus库还处于开发测试阶段,代码量还不十分庞大,文档和注释也不够全面,本章通过对libmodbus源代码的阅读过程,一方面可以进一步理解Modbus协议,同时也可以学习一个好的开源项目的代码组织及开发过程。 libmodbus的官方网站为http://libmodbus.org/,可以从http://libmodbus.org/download/下载源代码。作为开源软件,还可以 从GitHub网站获取最新版本的代码GitHub: https://github.com/stephane/libmodbus/tags

2.源码获取

        libmodbus 的源码不断更新,本教程选择版本 v3.1.10 。 打开https://github.com/stephane/libmodbus/tags,如下图下载:

6c231c797b864bf0a213b8c21e82ee9c.png

        解压后,简单查看源代码根目录的构成:
        ① doc目录: libmodbus库的各API接口说明文档。
        ② m4目录: 存放GNU m4文件,在这里对理解代码没有意义,可忽略。
        ③ src目录: 全部libmodbus源文件。
        ④ tests目录: 包含自带的测试代码 其他文件对理解源代码关系不大,可以暂时忽略

        解压libmodbus源代码:

597d4de8e91b4ee595da4dd9e3fe09e9.png

        进一步展开src代码目录
        libmodbus源码构成:

7096e0b2312d4f0a85fdeec785f30b64.png

        各文件作用如下:
        ① win32: 定义在Windows下使用Visual Studio编译时的项目文件和工程文件以及相关配置选项等。其中,modbus-9.sln默认使用Visual Studio 2008。
        ② Makefile.am: Makefile.am是Linux下AutoTool编译时读取相关编译参数的配置文件,用于生成Makefile文件,因为用于Linux下开发,所以在这里暂时忽略
        ③ modbus.c: 核心文件,实现Modbus协议层,定义共通的Modbus消息发送和接收函数各功能码对应的函数。
            modbus.h: libmodbus对外暴露的接口API头文件。

        ④ modbus-data.c: 数据处理的共通函数,包括大小端相关的字节、位交换等函数。
        ⑤ modbus-private.h: libmodbus内部使用的数据结构和函数定义。

        ⑥ modbus-rtu.c: 通信层实现,RTU模式相关的函数定义,主要是串口的设置、连接及消息的发送和接收等。
            modbus-rtu.h: RTU模式对外提供的各API定义。
            modbus-rtu-private.h: RTU模式的私有定义。

        ⑦ modbus-tcp.c: 通信层实现,TCP模式下相关的函数定义,主要包括TCP/IP网络的设置连接、消息的发送和接收等。
            modbus-tcp.h: 定义TCP模式对外提供的各API定义
            modbus-tcp-private.h: TCP模式的私有定义。
        ⑧ modbus-version.h.in: 版本定义文件。

(我们主要分析的就是 modbus.c  modbus-rtu.c 和 modbus-data.c 这三个文件)

3.libmodbus与应用程序的关系

        libmodbus是一个免费的跨平台支持RTU和TCP的Modbus开发库,借助于libmodbus开发库能够非常方便地建立自己的应用程序或者将Modbus通信协议嵌入单体设备libmodbus开发库与应用程序的基本关系如图所示。
        应用程序与libmodbus的关系:

91095a18d142404f97be32d027b49a58.png

        在对libmodbus的接口及代码框架简单了解之后,不妨再深入细节一探究竟,看看libmodbus都实现了哪些基础功能,以及源代码中对Modbus各功能码和消息帧是如何包装的。


libmodbus源代码解析

        libmodbus作为一个优秀且免费开源的跨平台支持RTU和TCP模式的Modbus开发库,非常值得大家借鉴和学习。下面对libmodbus源代码进行阅读和分析。

1.核心函数

        以Modbus RTU协议为例,主设备、从设备初始化后:
        ① 主设备就可以启动请求,即“发送消息”给从设备
        ② 从设备接收到请求后构造数据,启动响应即“发送回复”
        ③ 主机收到响应后,会“检查响应” 如下图所示:

b93e4790d3c84a02a5e9f554c235cc2a.png

        分 析 “ libmodbus-3.1.10\tests\unit-test-client.c ”、“ libmodbus-3.1.10\tests\unit-test-server.c”,可以得到下面核心函数的使用过程:

8f06b2b7e0dc480d90c5c997f70bb900.png


我们看一下官方的测试代码 unit-test-client.c 来验证一下上述流程是否正确

先创建一条Modbus总线,使用 modbus_new_rtu 函数

4bd85e0099124d0a8e3100c8f7d860fb.png

注意:这里是运行在linux系统上的代码,后续我们要改造出运行在单片机上的代码。 

然后设置从机地址和初始化操作

354f998569504b49b59a0c5fece1f537.png

对于从机,即 unit-test-server.c ,上面的初始化操作也是类似的。 

我们看看主机想写一个位寄存器,即函数 modbus_write_bit 它的内部是怎么样的。

ba3f516eff9f47c2883d9a1656770c05.png

再看看从机是怎么等待主机发来的消息的

bdc3dfb1b31a4bf6b50d7920a413224c.png

以上就是主机和从机核心函数的调用过程。

2.框架分析与数据结构

        站在 APP 开发的角度来说,使用上一节里介绍的 libmodbus 函数即可。但是,数据的传输必定涉及到底层数据传输。所以,从数据的收发过程,可以把使用 libmodbus 的源码分为 3 层:
① APP:它知道要做什么,主设备要读写哪些寄存,从设备提供、接收什么数据
② Modbus 核心层:向上提供接口函数,向下调用底层代码构造数据包并发送、接收数据包并解析
③ 后端(数据传输):进行硬件相关的数据封包与发送、接收与解包 

965986e7c2dd49d0a7e8de125ee58194.png

拿主机写一个位寄存器举例:

        modbus_write_bit 函数展开后调用了 write_single 函数,在里面调用了 backend 结构体里的函数。那么 backend 就是底层硬件相关的代码,modbus_write_bit 函数属于 APP 层,write_single 函数在 modbus.c 里定义,属于核心层。

对于核心层、后端,抽象出了如下结构体:

7e32fcd055dc4c109496f2ed78d4cb44.png

        核心层 modbus_t 结构体的成员含义如下:

29ba6537420f426185c5f98cc1569418.png

        后端 modbus_backend_t 结构体的成员含义如下:

5170a2dcea8f41739bd9afc931213bd2.png
8506270ce390428dbe62784f56cc7b96.png

以后我们写底层硬件相关的代码,就需要定义backend结构体,例如:

8c65a714660b4891b178c9c8ba451793.png然后实现里面的函数,就可以实现Modbus协议。 

3.情景分析

        以“modbus_write_bits”函数为例,分析核心函数的执行流程和内部实现。

(1)初始化

主设备:

① modbus_new_rtu 

95f66ea8589a422caef3a61d6d3df5d8.png

② modbus_set_slave 

fbf6ea11e9ff4606b0c3c78fdb563301.png

最终效果就是去设置 modbus_t 结构体的 slave 变量:

f57cf33427624ca39d7467d1a79c5f03.png

③  modbus_connect 

2f0bc6481dbc41fca0a25f71afcb32bd.png

后续我们还要自己改造出基于裸机或者FreeRTOS的版本。 

(2)主设备发送请求

        以函数 modbus_write_bits 写多个位寄存器为例子:

① 调用后端的 build_request_basis 函数

f2ba208949174c6d8dde1cf7da554a93.png

② 继续补充发送请求的数据

 0e399d48be8747e19166eea29b392561.png

③ 发送数据 构造CRC校验码

 fb8844ff22ea4efca17c168e475852ef.png
a4aefcd6043a465e8ba04606e92a0c62.png

以后我们还要自己实现发送函数,可以在裸机或者FreeRTOS上运行。

主设备发送请求的流程就分析到这里,逻辑还是很流畅的,注释也写的很详细了。 

(3)主/从设备接收数据

        打开 unit-test-server.c 文件,来看看从设备接收请求函数的调用流程和内部实现,即函数 modbus_receive

44b7bcfbd5af4f8fa1ba3de1e3de42a6.png

        从main函数的接收函数一路进去,会发现 modbus_receive 的实质就是主设备流程里的 _modbus_receive_msg 函数。下面来分析这个函数的内部实现。 

abf57267ec7249afb029b5102f431f22.png
1aa7c4f151a94fd8925efb42915456e1.png

怎么读取数据?怎么分阶段读?分哪几个阶段? 

        这里的函数调用比较复杂,简单来说就是通过状态机判断 step 变量,当前阶段完成后会改变 step的值。具体分下面这些阶段 

d21566d3c7f944119043e8d58e29d413.png

        我尽量用文字表示了,对源码感兴趣的可以自己去顺着流程走一遍,但这些我们以后都不需要修改,大概了解就行了。 

(4)从设备回应

从设备回应有下面两个函数

1a487b20ef3440f2b0c4e45a841364a2.png

        我们主要分析 reply 函数,他比较简单,至于函数①,他需要我们自己去构造回复的数据,需要我们对各个功能码的报文比较熟悉,感兴趣的可以自行学习,下面就不讲这个函数了。

在之前讲从机的流程时,有一个函数我们没有提到: modbus_mapping_new_start_address 函数, 它就是分配一个结构体,里面保存我们要操作的各个寄存器的参数。

b95a82907251472eb24a31822d6fdfae.png

         modbus协议规定了这些数组,但它是软件层面的,至于如何和硬件相对应,需要我们自己定义,自己使用数组里面的数据来读写硬件传感器,这些都比较好理解。说回正题, modbus_reply 函数能解析主机发来的请求,根据请求里的内容,来读或者写 modbus_mapping_t 这个结构体里的数组,然后发出回应;最后我们就可以将这些数组和硬件对应起来,实现自己的功能。

modbus_reply 解析

53aa5c0c53884f7cac6cb8e2562348cf.png
474d1720d3a34c6e8112e74471eb9b6a.png
7c417068a1d74026b37855f8afa48bad.png

至此 分析完毕

        我的注释都尽量简洁明了,某些地方可能跳转比较快,建议还是自己下载源码,然后跟着我的注释走一遍。接下来就是重头戏,怎么移植libmodbus开发库,在裸机或者FreeRTOS上运行。


libmodbus移植与使用

1.移植方法

        以串口为例,libmodbus 支持了 windows 系统、Linux 系统。如果要在 Freertos 或者裸机上使用 libmodbus,需要移植 libmodbus 里操作硬件的代码。
        要移植 libmodbus 的“后端”,就是构造自己的 modbus_backend_t 结构体

本节先写出模板:

93b9d5678c6d4ff3bf2991815f607a2a.png

原先的backend结构体大多数还是可以使用,我们只需要替换硬件相关的操作,实现自己的代码。

b60ef5c9f5374b54873a1582a87001ff.png

我们将修改 modbus-st-rtu.c 并实现它

从头往下看,看看哪些函数需要保留、删除,或者修改。(跟着我一步一步来,行数和我的不同可能是你没删除,下面不同图片(看图片右下角的水印)的行数都是删除前一个后的行数!!!)

55eb8bc87a9947dabbcb830acdff051e.png0baca536ac444b4e83c4f482d36cc201.png
390c44d622a24ebabc19beb08e0fe191.png
adca62cade2d4275b70d562b85de1b0f.png
26778da788b74e1a8e5abb566722aa12.png
1af8023a881f49b981a4a3ab216b7ff4.png
10bdc6d913f34945bb176891aa609c69.png
45fcc1065e75483497d609fa544359f9.png
e27309619f34426fb54a11c803676363.png
5ca0dea1811f4ad9899923d302fe69da.png
5b8c8ebd64554faa831e4f7b875afa1e.png
ea50ef37e36941af8ccf5db82c33a969.png
5f2e21a2ac164fdfa7d4fb4ffdf7f019.png
42ca8c53a2804cd1921206adf7129783.png
c265824ad8c747909ec5c33782caf9e2.png

至此模板函数就修改完成了。


2.使用USB串口和板载串口作为后端

        使用USB实现虚拟串口看我的这篇博客移植USBX实现虚拟串口,后面调用到里面的串口发送函数我就不再赘述。

流程如下:

a2bd771579e2451693ab37682143c6f5.png

        我自己写的程序里面已经实现了板载串口的数据收发,具体看我这篇博客UART开发基础,里面实现了串口函数的封装,最后usb串口也会定义类似的结构体,封装函数。

先来合并代码,实现①: 

62cbc57eab69475e8b1a18dc8fdce499.png
e479a46be89b452eb94af0ba650ad1e9.png
4e73874fd7ef46fdb7f5af2c1ab44094.png

将报错里找不到的头文件全部删除,在 modbus-private.h 里添加

a5e882f098bf461a8ce23d5ff4e223de.png

在 modbus-rtu-private.h 里

ab6e61814b294653af051bcea875a5e3.png

在 modbus.c 里

b56739789cb049f19dc19ab9e11de27d.png

然后把要修改的底层相关的读写函数通通注释掉,先编译通过再说,凡是找不到的函数和宏都注释掉。

相关宏缺乏定义的要包含 errno.h , errno.h 里包含了 errno-base.h (需要在linux内核里找,需要的可以私信我,这里就不放出来了)

a16bfb2f3e294a28975e1d2663274f1a.png

errno.c:

int errno;

程序编译通过后,来实现②:

341189b1cc5c49fb9c79a94d090dcb8c.png

先将malloc和free函数全部替换成FreeRTOS里的malloc和free函数

①modbus_new_st_rtu

fc10701afd8d43c7a7774b825e1e8263.png

②_modbus_rtu_send

static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
	/*使用usb/UART2/UART4的UART_Device来发送数据*/
	modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;

	/*return 0 表示成功*/
	if(0 == pdev->Send(pdev, (uint8_t *)req, req_length, TIMEOUT_SEND_MSG))
		return req_length;
	else
	{
		errno = EIO;
		return -1;
	} 
}

③_sleep_response_timeout

b3934d2e4bb94a648564d70d6170ef1e.png

④_modbus_rtu_flush

static int _modbus_rtu_flush(modbus_t *ctx)
{
	/*使用usb/UART2/UART4的UART_Device来Flush*/
	modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;
	
	return pdev->Flush(pdev);
}

在usb和板载串口的驱动函数里要实现各自的flush函数

以usb串口为例,要清空数据,就去读队列就好了,把队列全部读空

int ux_device_cdc_acm_flush(void)
{
	int cnt = 0;
	uint8_t data;
	while(1)
	{
		if(pdPASS != xQueueReceive(g_xUSBUART_RX_Queue, &data, 0))
			break;
		cnt++;
	}
	return cnt;
}

⑤_modbus_rtu_recv

static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout)
{
	/*使用usb/UART2/UART4的UART_Device来接收数据*/
	modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;

	/*return 0 表示成功*/
	if(0 == pdev->RecvByte(pdev, rsp, timeout))
		return 1;//表示成功读到一个字节的数据
	else
	{
		errno = EIO;
		return -1;
	} 
}

_modbus_rtu_recv 函数是在 _modbus_rtu_receive 函数里的 _modbus_receive_msg 调用的,新的recv函数加了个超时时间,所以receive函数里就不需要 select 函数了

044262d932de480db6cb67be7ff5bdd6.png

⑥uart_device.h

这个头文件的内容在我之前关于UART编程的博客里有详细的介绍,这里为了适配libmodbus再来修改里面的结构体,添加 flush 函数。

818f660b17c9454cbebb025a867962cb.png

731a4f794b8e483bbce65a2a62533242.pngf5172ae6a6ee402e9315799e319c3a46.png


总结

        由于篇幅过长,还有很多细节不方便展开讲,感兴趣的兄弟可以私信我。至此对于modbus协议和libmodbus库的原理讲解和移植就完成了,可以看到能讲的我就尽量讲了,光是代码注释我就写了很久,还有哪里有疑问的欢迎大家评论留言,我能解决的都尽力给大家解答。希望大家多多点赞支持,后续会更新更多实用的技能。 


网站公告

今日签到

点亮在社区的每一天
去签到