目录
前言
之前介绍了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库的原理讲解和移植就完成了,可以看到能讲的我就尽量讲了,光是代码注释我就写了很久,还有哪里有疑问的欢迎大家评论留言,我能解决的都尽力给大家解答。希望大家多多点赞支持,后续会更新更多实用的技能。