libmodbus编程应用(超详细源码讲解)

news2024/12/29 15:54:28

目录

前言

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:

int errno;

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

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

①modbus_new_st_rtu

②_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

④_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 函数了

⑥uart_device.h

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


总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2209897.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Adobe PR】Adobe Premiere Pro 快捷键介绍

Adobe Premiere Pro (简称PR) 是一款广泛使用的视频编辑软件,熟练掌握其快捷键可以显著提高编辑效率。下面是整理的一些常用的Adobe Premiere Pro快捷键,适用于Windows和Mac操作系统。对于Mac用户,除非特别指出,通常可以将Ctrl键替…

视频画面变糊是平台在做手脚吗?

人气视频博主“影视飓风”一条新发布的视频被全网下架,这两日更是引起轩然大波,不仅频上热门,甚至还有不少数码与视频同行为其抱不平。那么影视飓风到底发布了一条什么视频?又为何会惨被全网下架呢? 10月8日&#xff0…

Android Studio Ladybug使用经典主题UI

背景 Android Studio Ladybug | 2024.2.1 默认使用new ui,但是已经找不到之前经典主题了。 通过插件方式设置经典主题 打开设置。Plugins。 在Marketplace中搜索:Classic UI 下面的截图是我已经安装完成的。 插件安装完成了,重启as即可。…

闭着眼学机器学习——K近邻分类

引言: 在正文开始之前,首先给大家介绍一个不错的人工智能学习教程:https://www.captainbed.cn/bbs。其中包含了机器学习、深度学习、强化学习等系列教程,感兴趣的读者可以自行查阅。 1. 算法介绍 K近邻(K-Nearest Neighbors, KNN…

ScriptableObject基本使用

使用方法 自定义类继承ScriptableObject 可以在类内部增加数据或者数据类,一般用于配置 注意事项 给继承ScriptableObject的类增加CreateAssetMenu特性。 CreateAssetMenu一般默认三个参数 第一个参数是父目录 第二个参数是父目录的子选项 第三个参数是可以…

虚幻闪烁灯光材质

创建一个材质 材质域改成光照函数 , Time让材质动起来 参数B用来控制速度 , Sine 让灯光闪烁 , Frac 增加了闪烁细节 把材质放到灯光材质上 效果还是挺不错的! 可以用于一些恐怖游戏~

FileZilla的简单使用

FileZilla的下载与安装以及简单使用(有图解超简单)-CSDN博客 参考这篇文章。 我在Window下安装了客户端,在虚拟机下启动Ubuntu,启用了Ubuntu的FTP服务。 按照该文章的流程进行下去,成功的把客户端上的文件上传到了F…

STM32学习--6-1 定时器定时中断

定时器电路图 第一步:RCC开启时钟,基本上每个代码都是第一步 第二步:选择时基单元的时钟源(内部时钟) 第三步:配置时基单元,包括预分频器、自动重装器、计数模式等,通过结构体配置…

Dockerfile(Jenkins)

1.创建⼀个jenkins的Dockerfile mkdir tomcat cd tomcat 2、上传需要的安装包 apache-tomcat-8.5.47.tar.gz jdk-8u211-linux-x64.tar.gz jenkins.war 3、编写Dockerfile vim Dockerfile # This my first jenkins Dockerfile # Version 1.0 FROM centos:7 MAINTAINER l…

Echarts+vue3+高德渲染地图

Echartsvue3高德渲染地图 一&#xff1a;安装 npm install echarts二&#xff1a;渲染地图 1. html <template><div class"content"><div ref"myChartsRef" id"map" style"width: 100%;height: 560px;" ></d…

JAVA自动化测试TestNG框架

1.TestNG简介 JAVA自动化测试最重要的基石。官网&#xff1a;https://testng.org 使用注解来管理我们的测试用例。 发现测试用例 执行测试用例 判断测试用例 生成测试报告 2.创建Maven工程 2.1创建一个maven工程 2.2设置maven信息 2.3设置JDK信息 2.4引入testng依赖 <dep…

Linux下Docker方式Jenkins安装和配置

一、下载&安装 Jenkins官方Docker仓库地址&#xff1a;https://hub.docker.com/r/jenkins/jenkins 从官网上可以看到&#xff0c;当前最新的稳定版本是 jenkins/jenkins:lts-jdk17。建议下在新的&#xff0c;后面依赖下不来 所以&#xff0c;我们这里&#xff0c;执行doc…

达梦数据库性能优化

1、SQL执行计划 拿到一条SQL的时候&#xff0c;首先要下达梦手册中提出的有效SQL规范&#xff0c;及是否命中了特殊OR子句的不规范&#xff0c;是否用了复杂的正则表达式&#xff0c;避免重复很高的索引&#xff0c;UINON ALL 是否可以替换UNION操作等,某些场景INSTR函数导致的…

FunASR离线文件转写服务开发指南-debian-10.13

FunASR离线文件转写服务开发指南-debian-10.13 服务器环境 debian10.13 64位 第一步 配置静态网卡 auto eth0 iface eth0 inet static address 192.168.1.100 netmask 255.255.255.0 gateway 192.168.1.1 dns-nameservers 8.8.8.8 8.8.4.4/etc/init.d/networking restart第…

C++面试速通宝典——25

473. HTTP如何减少重定向请求 重定向请求&#xff1a; ‌‌‌‌  服务器上的一个资源可能由于迁移、维护等原因从url1移至url2后&#xff0c;而客户端不知情&#xff0c;他还是继续请求url1&#xff0c;这时服务器不能粗暴地返回错误&#xff0c;而是通过302响应码和Locati…

甲方安全和乙方安全的区别

信息安全工作&#xff0c;总会被人分成甲方和乙方&#xff0c;甲乙方原本只是商务层面需方和供方的代称&#xff0c;在安全领域&#xff0c;成了做公司内部安全和为客户提供安全的区别。 通常意义上&#xff0c;什么是甲方安全人员呢&#xff1f;就是在非安全业务的公司从事信…

ROS2 通信三大件之动作 -- Action

通信最后一个&#xff0c;也是不太容易理解的方式action&#xff0c;复杂且重要 1、创建action数据结构 创建工作空间和模块就不多说了 在模块 src/action_moudle/action/Counter.action 下创建文件 Counter.action int32 target # Goal: 目标 --- int32 current_value…

[Python学习日记-45] Python 中模块的介绍与导入

[Python学习日记-45] Python 中模块的介绍与导入 简介 模块的概念与好处 模块的分类 模块导入和调用 自定义模块 模块的查找路径 简介 在前面的学习当中偶尔我们会看到 import ... 一个什么东西的&#xff0c;或者 from ... import ...&#xff0c;那时候并没有进行介绍&…

react+ts+vite 别名一直爆红问题

已经配置如下代码安装了types/node import path from "path"; // https://vitejs.dev/config/ export default defineConfig({plugins: [react()],server: {proxy: {"/api": {target: "http://localhost:3000",changeOrigin: true,rewrite: (pa…

如何选择安全的谷歌浏览器插件

在数字时代&#xff0c;浏览器插件为我们提供了极大的便利&#xff0c;增强了我们的浏览体验。然而&#xff0c;随着便利性的增加&#xff0c;安全性问题也日益凸显。选择安全的谷歌浏览器插件是保障个人信息安全的重要步骤。以下是详细的教程&#xff0c;帮助你选择和使用安全…