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

目录

前言

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,如下图下载:

解压后,简单查看源代码根目录的构成:

① doc目录: libmodbus库的各API接口说明文档。

② m4目录: 存放GNU m4文件,在这里对理解代码没有意义,可忽略。

③ src目录: 全部libmodbus源文件。

④ tests目录: 包含自带的测试代码 其他文件对理解源代码关系不大,可以暂时忽略

解压libmodbus源代码:

进一步展开src代码目录

libmodbus源码构成:

各文件作用如下:

① 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的关系:

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


libmodbus源代码解析

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

**1.**核心函数

以Modbus RTU协议为例,主设备、从设备初始化后:

① 主设备就可以启动请求,即"发送消息"给从设备

② 从设备接收到请求后构造数据,启动响应即"发送回复"

③ 主机收到响应后,会"检查响应" 如下图所示:

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


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

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

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

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

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

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

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

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

2.框架分析与数据结构

站在 APP 开发的角度来说,使用上一节里介绍的 libmodbus 函数即可。但是,数据的传输必定涉及到底层数据传输。所以,从数据的收发过程,可以把使用 libmodbus 的源码分为 3 层:

① APP:它知道要做什么,主设备要读写哪些寄存,从设备提供、接收什么数据

② Modbus 核心层:向上提供接口函数,向下调用底层代码构造数据包并发送、接收数据包并解析

③ 后端(数据传输):进行硬件相关的数据封包与发送、接收与解包

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

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

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

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

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


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

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

3.情景分析

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

(1)初始化

主设备:

① modbus_new_rtu

② modbus_set_slave

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

③ modbus_connect

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

(2)主设备发送请求

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

① 调用后端的 build_request_basis 函数

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

③ 发送数据 构造CRC校验码


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

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

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

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

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


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

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

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

(4)从设备回应

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

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

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

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

modbus_reply 解析



至此 分析完毕

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


libmodbus移植与使用

1.移植方法

以串口为例,libmodbus 支持了 windows 系统、Linux 系统。如果要在 Freertos 或者裸机上使用 libmodbus,需要移植 libmodbus 里操作硬件的代码。

要移植 libmodbus 的"后端",就是构造自己的 modbus_backend_t 结构体

本节先写出模板:

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

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

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














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


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

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

流程如下:

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

先来合并代码,实现①:



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

modbus-rtu-private.h

modbus.c

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

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

errno.c:

cpp 复制代码
int errno;

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

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

①modbus_new_st_rtu

②_modbus_rtu_send

cpp 复制代码
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

④_modbus_rtu_flush

cpp 复制代码
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串口为例,要清空数据,就去读队列就好了,把队列全部读空

cpp 复制代码
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

cpp 复制代码
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 函数了

⑥uart_device.h

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


总结

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

相关推荐
kfepiza33 分钟前
CentOS7.9.2009的yum更换vault地窖保险库过期源,epel的archive归档源 笔记241117
linux·笔记·centos
南宫生42 分钟前
力扣-Hot100-链表其一【算法学习day.34】
java·学习·算法·leetcode·链表
h汉堡1 小时前
C语言的内存函数
c语言·开发语言·c++·学习
睿智的菜汪2 小时前
劳动力市场
笔记
Shall#2 小时前
Unix进程
linux·笔记·unix
m0_736180443 小时前
20221403郑骁恒 第十周预习报告
学习
wusong9994 小时前
InfluxDB时序数据库笔记(一)
笔记·时序数据库·influxdb
海盗猫鸥4 小时前
常见排序算法
学习·算法·排序算法
梨子串桃子_4 小时前
物联网通信技术及应用 | 第七章 NB-IoT与LoRa通信技术 | 自用笔记
笔记·物联网·学习
我是汉堡请多指教5 小时前
Unity学习---IL2CPP打包时可能遇到的问题
学习·unity·游戏引擎