全场景——(七)libmodbus 使用

news2024/9/20 16:39:45

文章目录

  • 一、libmodbus开发库
    • 1.1 功能概要
    • 1.2 源码获取
    • 1.3 libmodbus与应用程序的关系
  • 二、libmodbus源代码解析
    • 2.1 核心函数
    • 2.2 框架分析与数据结构
    • 2.3 情景分析
      • 2.3.1 初始化
      • 2.3.2 主设备发送请求
      • 2.3.3 从设备接收请求
      • 2.3.4 从设备回应
    • 2.4 常用接口函数
      • 2.4.1 各类辅助接口函数
      • 2.4.2 各类Modbus功能接口函数
      • 2.4.3 数据处理的相关函数或宏定义
    • 2.5 RTU/TCP关联接口函数
      • 2.5.1 RTU模式关联函数
      • 2.5.2 TCP模式关联函数
  • 三、libmodbus移植与使用
    • 3.1 移植方法
    • 3.2 使用USB串口作为后端
    • 3.3 libmodbus从机实验(USB串口)
    • 3.4 libmodbus主机实验(USB串口)
    • 3.5 使用板载串口作为后端
      • 3.5.1 使用UART_Device
      • 3.5.2 用作后端
    • 3.6 libmodbus实验(板载串口)

一、libmodbus开发库

1.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.git

1.2 源码获取

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

在这里插入图片描述

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

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

图6-2解压libmodbus源代码:

在这里插入图片描述

进一步展开src代码目录,如图6-3所示:

图6-3libmodbus源码构成:

在这里插入图片描述

各文件作用如下:

  • 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: 版本定义文件。

1.3 libmodbus与应用程序的关系

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

图6-4应用程序与libmodbus的关系:

在这里插入图片描述

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

二、libmodbus源代码解析

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

2.1 核心函数

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

  • 主设备就可以启动请求,即“发送消息”给从设备
  • 从设备接收到请求后构造数据,启动响应即“发送回复”
  • 主机收到响应后,会“检查响应”

如下图所示:

在这里插入图片描述

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

在这里插入图片描述
在主设备(Client)中流程如下:
在这里插入图片描述
下面有写数据函数,一路进去解析如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在从设备(Server)中流程如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 框架分析与数据结构

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

  • APP:它知道要做什么,主设备要读写哪些寄存,从设备提供、接收什么数据
  • Modbus核心层:向上提供接口函数,向下调用底层代码构造数据包并发送、接收数据包并解析
  • 后端(数据传输):进行硬件相关的数据封包与发送、接收与解包

在这里插入图片描述

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

在这里插入图片描述

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

成员含义
int slave;从站设备地址
int s;RTU下是串口句柄,TCP下是Socket
int debug;是否启动Debug模式(打印调试信息)
int error_recovery;错误恢复模式:MODBUS_ERROR_RECOVERY_NONE:由APP处理错误MODBUS_ERROR_RECOVERY_LINK:如果有连接错误,则重连MODBUS_ERROR_RECOVERY_PROTOCOL:如果数据不符合协议要求,则清空所有数据
int quirks;一些奇怪的功能,比如:MODBUS_QUIRK_MAX_SLAVE:从站地址最大值可以到达255MODBUS_QUIRK_REPLY_TO_BROADCAST:回应广播包
struct timeval response_timeout;等待回应的超时时间,默认是0.5S
struct timeval byte_timeout;接收一个字节的超时时间,默认是0.5S
struct timeval indication_timeout;等待请求的超时时间
const modbus_backend_t *backend;硬件传输层的结构体
void *backend_data;硬件传输层的私有数据

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

成员含义
unsigned int backend_type;后端类型,是RTU还是TCP
unsigned int header_length;头部长度,比如RTU数据包前面需要有1字节的设备地址,头部长度就是1
unsigned int checksum_length;校验码长度,RTU的校验码是2字节
unsigned int max_adu_length;ADU(数据包)最大长度
set_slave设置从站地址
build_request_basis设置RTU请求包的基本数据,这些数据的格式是一样的,比如req[0]是从设备地址,req[1]是功能码,req[2]和req[3]是寄存器地址,req[4]和req[5]是寄存器数量
build_response_basis设置RTU回应包的基本数据,这些数据的格式是一样的,比如req[0]是从设备地址,req[1]是功能码
prepare_response_tid生产传输标识TID,在TCP中使用
send_msg_pre发送消息前的准备工作,对于RTU是填充CRC检验码,对于TCP是填充头部的Length
send发送数据包
receive接收数据包
recv接收原始数据,receive会调用recv得到原始数据然后解析出数据包
check_integrity检查数据包的完整性
pre_check_confirmation检查响应数据包是否有效时,先执行pre_check_confirmation做一些简单的检查
connect硬件相关的连接,对于RTU就是打开串口、设置串口波特率等;对于TCP则是连接对端
is_connected判断是否已经连接
close关闭连接
flush清空接收到的、未处理的数据
select阻塞一段时间以等待数据
free释放分配的modbus_t等结构体

2.3 情景分析

2.3.1 初始化

在 client 的 main.c 主函数中可以看到 modbus_new_rtu 函数,详细如下:
在这里插入图片描述
进入到 modbus_new_rtu 函数内部,分析如下:
在这里插入图片描述
在这里插入图片描述
对于设置后端详细数据如下:
在这里插入图片描述
在 client.c 中设置需要访问的从设备地址,如下:
在这里插入图片描述
进入到 modbus_set_slave 函数中,可以看到调用到前面我们强调的 backend 中的一些参数:
在这里插入图片描述
进入到 backend 结构体中可以找到该参数:
在这里插入图片描述
进入到该参数函数内部,详细如下:
在这里插入图片描述
modbus 结构体参数详细如下:
在这里插入图片描述
主设备要想去访问从设备,通过串口进行收发数据,在主设备中为串口创建了一个 modbus_t 结构体,并且我们设置了其中的 slave 参数,设置完之后一开始输入的宏 SERVER_ID 就等于 salve 参数,即为主设备要去访问的从设备地址。
主设备知道了从设备的地址,接下来的操作就是连接从设备,如下:
在这里插入图片描述
进入到 modbus_connect 函数内部,详细如下:
在这里插入图片描述
可见还是调用到后端中的 connect 函数,详细如下:
在这里插入图片描述
进入到 conne 函数内部,打开了串口,并且设置参数,如下(这里截图了主要部分):
在这里插入图片描述

以“modbus_write_bits”函数(写多个寄存器)为例,分析下图的执行流程:

在这里插入图片描述

2.3.2 主设备发送请求

在这里插入图片描述
在上面的 modbus_write_bits 函数中,构造基础请求包:
在这里插入图片描述
这里的 byte_count 计数含义如下:这里意思是要写入的数据数量(字节),比如我们想写入9个位寄存器,有公式可得 9/8 = 1、(9%8 = 1) ? 1:0 = 1,两者相加则等于 2 ,所以这里需要发送两个字节,因为一个字节表示八位数据,这里要写入9个位寄存器就只能发送两个字节。
构造基础请求包调用的是后端 backend 结构体中的函数,如下:
在这里插入图片描述
函数内部详细如上,这里我们可以观察前面的写多个线圈的函数内部进行比较,如下:

在这里插入图片描述
在这里插入图片描述
可见数据能够进行一一对应。这里的构造基础信息就是帮助主设备构造好从设备地址、功能码、起始地址以及寄存器数。(这里有个小细节:最前面是构造请求包,但是它下面有条计算字节数的公式,通过计算我们得知 byte_count 为2,成功对应上写多个线圈的字节数,同时字节数对应 req[6],所以这里 req[6] = 2)

在这里插入图片描述
这里需要注意:modbus_write_bits 函数的src 传入的是一个字节表示一个位,用一个位来表示寄存器的数值,构造该请求包时需要把一个字节转换成一个位。
在这里插入图片描述

2.3.3 从设备接收请求

进入到 server.c 中,找到 main.c,对其中接收数据函数进行分析,其初始化与主机发送请求类似:
在这里插入图片描述
进入 modbus_receive 函数内部,可以看到其调用的是 backend 结构体中的 receive 函数:
在这里插入图片描述
找到 backend 结构体,并进入 _modbus_rtu_receive 函数内部:
在这里插入图片描述
不难发现,函数内部调用的是 backend 结构体中的 _modbus_receive_msg 函数,同时此函数对应了主设备中的 _modbus_receive_msg 函数,但是两者是有区别的,两者的区别在于第三个参数,如下:
在这里插入图片描述
对于主机调用 _modbus_receive_msg 函数是接收从机答复,对于从机调用 _modbus_receive_msg 函数是接收主机发送的请求。

一、等待数据:调用 select 函数判断是否有数据,如果返回值不等于-1则说明等待到了数据但是还没有去读数据,如果返回值等于1则说明没有等待到数据,同时第三个参数是时间参数,可以设置一个超时时间。
在这里插入图片描述

二、读取原始数据:如果返回值不等于-1说明等待到了数据,那么就调用 backend 结构体中的 recv 函数来读取原始数据。
在这里插入图片描述

三、分阶段读取数据:接收到的字符数量在 rc 中记录下来,将上一次已经读取的字符减掉接收到的字符数量赋给 length_to_read,如果 length_to_read = 0,则说明读取完毕。即为上一阶段的数据是否已经接收完毕。如果已经接收完毕,下面的函数会继续计算下一个阶段要读取的数据长度。先计算下一阶段的数据长度(length_to_read),再循环读。

在这里插入图片描述
一、二、三为一个完整的循环,会读取到以一个完整的数据包。
四、检查数据包的完整性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

想去读一个数据包,一开始要先知道读取的字节长度,第一次要读取的数据为头部长度+1,第一个状态要去读取 _STEP_FUNCTION 这个功能(即功能码),所以在这里是要读取到功能码,功能码在第二个字节处,所以 length_to_read = 2,则 header_length = 1。
在这里插入图片描述
与(一)一样,在下面的 while 循环中,会去读取两个字节,读到两个字节之后(从设备地址和功能码)进行判断后面需要读取多少个字节,进入到compute_meta_length_after_function函数内部,发现如果是写入多个线圈则需要读取5个字节,刚好对应上功能码后面的起始地址高位到字节数。
在这里插入图片描述
在这里插入图片描述
确定了后面要读取5个字节(原始数据,对应 _STEP_META),就会再次进行循环,等待5个数据,当读取到五个数据之后会进入 compute_data_length_after_meta 函数,由前面可知 header_length = 1,所以这里的 length = msg[6],刚好是前面的从设备地址到字节数总共6个字节。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后一步是校验码,对应 _STEP_DATA,前面总共读取了7个字节,对应到msg[6],所以这里需要再读取msg[6]+2个字节,刚好对应上校验位(包括前面的字节数)。所以在这里确定了读数据的字节数,会再次进行循环,等待数据全部读取完毕,数据读取完毕则会检查数据完整性。
在这里插入图片描述
这里调用的是 backend 结构体中的函数,进入函数内部可以知道:从设备进行判断,如果是发给从设备的,会进行判断,如果不是发给从设备,则返回0。同时会去检查校验码,会使用收到i的数据(从设备地址到变更数据低位)算出一个校验码,与接收到的校验码进行对比,如果两者相同则表示接收到正确的数据。
在这里插入图片描述

补充:对于前面的 select 函数会判断是否有数据,同时会设置一个超时时间,对于这个超时时间我们可以进行了解:
在这里插入图片描述
从机调用 modbus_receive 函数 是不知道主机什么时候将数据发送给从机,那么在该函数中到底愿意等待多久呢,这个时间是如何确定的,从机等待主机发送请求,在 _modbus_receive_msg 函数的第三个参数 msg_typeMSG_INDICATION表示等待的数据包是主机发送的,如果想等待主机发送过来的请求,这个超时时间就是这条总线中的 indication_timeout(从机等待主机请求的时间),并且这个时间是可以去设置的,如果将该超时时间设置的非常大,那么在接下来的循环中等待第一个数据时会等待很久,类似于 freertos 中的阻塞,不会浪费 CPU 资源,调用的是查询方式。这里如果是主教调用该函数,则这个超时时间就是主机愿意等待从机回应的超时时间。
在这里插入图片描述
在这里插入图片描述
这里主要是接收主机发送请求(从设备地址和功能码)可以将超时时间设置很大,并且会在一个循环中,会一直等待请求,如果是执行接收后续数据(起始地址、寄存器数和数据),可以将超时时间设置为只愿意等待一个字节的时间。
在这里插入图片描述

2.3.4 从设备回应

在这里插入图片描述

创建一个 modbus 设备时,可以去分配下面几个寄存器的内存。

在这里插入图片描述

在 server.c 中的 modbus_mapping_new_start_address 函数会创建一个 mapping 结构体,这个结构体就是用来描述一个从机的寄存器。

在这里插入图片描述

在这里插入图片描述

想使用上面的数组,只需要去调用 modbus_reply 函数,当从机接收到主机发送的请求,调用该函数会根据请求来写或读寄存器。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

当接收到主设备发送的消息之后,从设备调用 modbus_reply 函数会先去解析数据包(即主机的请求),接下来就是读或写 mapping 结构体中的数组,最后就是发出回应。比如现在主机要读取单个线圈,从机接收到请求之后调用 modbus_reply 函数就会去读 mapping 结构体中的读单个线圈数组,将其中每一位的数值找到,将数值构造成一个回应的数据包并发送给主机;若主机要写单个线圈,从机接收到请求之后调用 modbus_reply 函数把请求数据包中的数据挑出来,写入到 mapping 结构体中的某个读单个线圈数组,写完之后需要发送一个回应给主机表示已经写入完毕。

在这里插入图片描述

在读取多个线圈中,由一开始可知 offest = 1,所以第一行代码 offest + 3 对应的位置就是寄存器数的高位,offest + 4 对应的是寄存器数的低位;第二行代码以此类推,得到字节数;第三行中的 address - mb_mapping->start_bits(起始位)主要是做了下标的转换,例如在构造 mapping 结构体时,buf 的第0项对应了 start_bits 的第100个寄存器,如果此时主机想去访问第101个寄存器,则需要进行如下操作:101 - 100 = 1,此时的 1 就是 buf[1],对应了第101个寄存器。

在这里插入图片描述

进行某些异常判断:若要写入的寄存器数小于1则为异常;写入的寄存器数超过写入的最大值则为异常;写入位寄存器后后续提供的数据字节数不足则为异常。

在这里插入图片描述

这个函数主要是将主机发送来的请求中含有的数据挑出来,写到 mb_mapping->tab_bits 数组中,完成了数据的更新。

在这里插入图片描述

在这里插入图片描述

该函数主要是构建基础回应包,进入函数内部可以看到,主要是设置了从机的设备地址和功能码,接着就将后面的四位数据复制下来,也就是起始地址高低位、寄存器数高低。

在这里插入图片描述

在这里插入图片描述

最后就是发送数据给主机,主要就是构造校验码(CRC),接着就是写串口,将数据写入串口发送给主机,完成回应。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在上面我们用的都是 modbus_reply 函数,我们也可以通过自己构造数据包,调用 modbus_send_raw_request 函数进行回应。

在这里插入图片描述

进入到函数内部,他会构造一个基础回应包,该回应包会含有从机地址以及功能码,接着拷贝后面的原始数据(数据域字节数、数据高低位),拷贝进前面构造的req[MAX_MESSAGE_LENGTH] 数组,最后发送消息,在发送消息函数中会去构造两个字节的校验码,接着写串口,将数据发送给主机。

2.4 常用接口函数

下面分析 libmodbus开发库提供的所有接口API函数。其主要对象文括 modbus.h 和 modbus.c ,接口函数大致可分为3类,以下分别进行介绍。

2.4.1 各类辅助接口函数

MODBUS_API int modbus_set_slave(modbus t * ctx,int slave)

此函数的功能是设置从站地址,但是由于传输方式不同而意义稍有不同。

  • RTU模式 :

如果 libmodbus应用于 主站设备端,则相当于定义 远端设备ID ;如果libmodbus应用于从站设备端 ,则相当于定义 自身设备 ID ;在 RTU 模式下参数 slave 取值范围为 0~247 ,其中 0(MODBUS_BROADCAST_ADDRESS) 为广播地址。

  • TCP模式:

通常,TCP 模式下此函数不需要使用。在某些特殊场合,例如串行 Modbus设备转换为 TCP模式传输的情况下,此函数才被使用。此种情况下,参数 slave取值范围为 0~247 ,0 为广播地址;如果不进行设置,则 TCP 模式下采用默认值 MODBUS TCP SLAVE(OXFF) 。

下面的代码以 RTU模式、主设备(MASTER)端为例:

modbus_t * ctx;

ctx=modbus_new_rtu("COM4"115200'N'81);

if (ctx ==NULL)

{

  fprintf(stderr"Unable to create the libmodbus context\n");

  return -1;

}

rc =modbus_set_slave(ctx,YOUR DEVICE ID);

if (rc==-1)

{

  fprintf(stderr"Invalid slave ID\n");

  modbus free(ctx);

  return -1;

}

if (modbus connect(ctx)==-1)

{

  fprintf(stderr,"Connection failed:sn",modbus strerror(errno));

  modbus free(ctx);

  return -1;

} 

MODBUS_APIintmodbus_set_error_recovery(modbus_t*ctx,modbus_error_recovery_mode error_recovery):

此函数用于在连接失败或者传输异常的情况下,设置错误恢复模式。有 3种错误恢复模式可选。

typedef enum

{

  MODBUS_ERROR_RECOVERY_NONE        =0,             //不恢复

  MODBUS_ERROR_RECOVERY_LINK        =(1<<1),           //链路层恢复

  MODBUS_ERROR_RECOVERY_PROTOCOL      =(1<<2)           //协议层恢复

}modbus error recovery mode;

默认情况下,设置为 MODBUS_ERROR_RECOVERY_NONE ,由应用程序自身处理错误;若设置为 MODBUS_ERROR_RECOVERY_LINK ,则经过一段延时 libmodbus 内部自动尝试进行断开/连接;若设置为 MODBUS_ERROR_RECOVERY_PROTOCOL ,则在传输数据 CRC 错误或功能码错误的情况下,传输会进入延时状态,同时数据直接被清除。在 SLAVE/SERVER 端,不推荐使用此函数。

基本用法举例:

modbus_set_error_recovery(ctx,MODBUS_ERROR_RECOVERY_LINK|MODBUS_ERROR_RECOVERY_PROTOCOL);

MODBUS_API int modbus_set_socket(modbus t * ctx,int s)

此函数设置当前 SOCKET 或串口句柄要用于多客户端连接到单一服务器的场合。简单用法举例如下,后续介绍函数 modbus_tcp_listen() 时将会进一步介绍相关用法。

#define NB_CONNECTION 5

modbus_t * ctx;

ctx=modbus_new_tcp("127.0.0.1", 1502)

server_socket = modbus_tcp_listen(ctx,NB_CONNECTION);

FD_ZERO(&rdset);

FD_SET(server_socket,&rdset);

/* ... */

if (FD_ISSET(master_socket,&rdset))

{

  modbus_set_socket(ctx,master_socket);

  rc =modbus_receive(ctx,query);

  if(rc!=-1)

  {

  modbus_reply(ctx,query, rc,mb_mapping);

  }

}

MODBUS_API int modbus_get_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);

MODBUS_API int modbus_set_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);

用于获取或设置响应超时,注意时间单位分别是秒和微秒。

MODBUS_API int modbus_get_byte_timeout (modbus_t * ctx, uint32_t * to_sec,uint32_t * to_usec);

MODBUS_API int modbus_set_byte_timeout (modbus_t * ctx, uint32_t * to_sec,uint32_t * to_usec);

用于获取或设置连续字节之间的超时时间,注意时间单位分别是秒和微秒。

MODBUS_API intmodbus_get_header_length (modbus_t * ctx);

获取报文头长度。

MODBUS_API int modbus_connect (modbus_t * ctx);

此函数用于主站设备与从站设备建立连接。

在 RTU 模式下,它实质调用了文件 modbus_rtu.c 中的函数 static int modbus_rtu_connect (modbus_t * ctx) ;在此函数中进行了串口波特率校验位、数据位、停止位等的设置。

在 TCP 模式下,modbus_connect() 调用了文件 modbus_tcp.c 中的函数 static int_modbus_tcp_connect (modbus_t * ctx ) ;在函数 _modbus_tcp_connect() 中,对 TCP/IP 各参数进行了设置和连接。

MODBUS_API void modbus_close (modbus_t * ctx);

关闭 Modbus 连接。在应用程序结束之前,一定记得调用此函数关闭连接在 RTU 模式下,实质是调用函数 _modbus_rtu_close(modbus_t * ctx) 关闭串口句柄;在 TCP 模式下,实质是调用函数 _modbus_tcp_close(modbust * ctx) 关闭 Socket 句柄。

MODBUS_API void modbus_free (modbus_t * ctx);

释放结构体 modbus_t 占用的内存。在应用程序结束之前,一定记得调用此函数

MODBUS_API int modbus_set_debug (modbust * ctx, int flag);

此函数用于是否设置为DEBUG模式。

若参数 flag 设置为TRUE,则进入 DEBUG模式。若设置为FALSE,则切换为非 DEBUG模式。在 DEBUG模式下所有通信数据将按十六进制方式显示在屏幕上,以方便调试。

MODBUS_API const char * modbus_strerror (int errnum);

此函数用于获取当前错误字符串。

2.4.2 各类Modbus功能接口函数

MODBUS_API int modbus_read_bits (modbus t * ctx, int addr, int nb, uint8_t * dest);

此函数对应于功能码 01(0x01) 读取线圈/离散量输出状态(Read Coil Status/DOs),其中,所读取的值存放于参数 uint8_t * dest 指向的数组空间因此 dest 指向的空间必须足够大,其大小至少为 nb * sizeof(uint8_t) 个字节。

用法举例:

#define SERVER ID        1
#define ADDRESS START      0
#define ADDRESS END       99

modbus_t * ctx;

uint8_t * tab_rp_bits;
int rc;
int nb;

ctx=modbus_new_tcp("127.0.0.1",502);
modbus_set_debug(ctx,TRUE);
if (modbus_connect(ctx)==-1)

{
  fprintf(stderr,"Connection failed:%s\n", modbus_strerror(errno));
  modbus free(ctx);
  return -1;
}

//申请存储空间并初始化
int nb = ADDRESS_END - ADDRESS_START;
tab_rp_bits = (uint8_t * ) malloc (nb * sizeof(uint8_t));
memset(tab_rp_bits, 0, nb * sizeof(uint8_t));

//读取一个线圈
int addr =1;
rc =modbus_read_bits(ctx,addr,1,tab_rp_bits);
if (rc !=1)
{
  printf("ERROR modbus_read_bits_single (%d)\n", rc);
  printf("address =%d\n", addr);
}

//读取多个线圈

rc =modbus_read_bits(ctx,addr,nb,tab_rp_bits);
if (rc !=nb)
{
  printf("ERROR modbus_read_bits\n");
  printf("Address =%d,nb =%d\n", addr, nb);
}

//释放空间关闭连接

free(tab_rp_bits);
modbus_close(ctx);
modbus_free(ctx);

MODBUS_API int modbus_read_input_bits (modbus_t * ctx, int addr, int nb,uint8_t * dest);

此函数对应于功能码 02(0x02) 读取离散量输入值(Read Input Status/DIs),各参数的意义与用法,类似于函数 modbus_read_bits() 。

MODBUS_API int modbus_read_registers (modbus_t * ctx, int addr, int nb,uint16_t * dest);

此函数对应于功能码 03(0x03) 读取保持寄存器(Read Holding Register),其中,所读取的值存放于参数 uint16_t * dest 指向的数组空间因此 dest 指向的空间必须足够大,其大小至少为 nb * sizeof(uint16_t) 个字节。

当读取成功后,返回值为读取的寄存器个数;若读取失败,则返回-1。此函数调用依赖关系如下图6-5所示。

用法举例:

img

modbust * ctx;
uint16_t tab_reg[64];
int rc;
int i;

ctx=modbus_new_tcp("127.0.0.1",502);
if (modbusconnect(ctx)==-1)
{
  fprintf(stderr,"Connection failed:%s\n", modbus_strerror(errno));
  modbus_free(ctx);
  return -1;
}

//从地址0开始连续读取10个

rc =modbus_read_registers(ctx,0,10,tab_reg);
if (rc ==-1)
{
  fprintf(stderr,"%s\n",modbus_strerror(errno));
  return -1;
}

for (i=0;i<rc;i++)
{
  printf("reg[%d]=%d(0x%X)\n",i,tab_reg[i],tab_reg[i]);
}

modbus_close(ctx);
modbus_free(ctx);

MODBUS_API int modbus_read_input_registers (modbus_t * ctx,int addr, int nb, uint16_t * dest );

此函数对应于功能码 04(0x04) 读取输人寄存器(Read Iput Register),各参数的意义与用法,类似于函数 modbus_read_registers() 。

此函数的调用依赖关系如下图 6-6 所示。

图6-6函数 modbus_read input_registers()的调用依赖关系

img

MODBUS_API int modbus_write_bit (modbus_t * ctx, int coil_addr, int status):

该函数对应于功能码 05(0x05) 写单个线圈或单个离散输出(Force SingleCoil)。其中参数 coil_addr 代表线圈地址;参数 status 代表写值取值只能是TRUE(1)或 FALSE(0) 。

MODBUS_API int modbus_write_register (modbus_t * ctx,int reg_addr, int value):

该函数对应于功能码 06(0x06) 写单个保持寄存器(Preset Single Register)。

MODBUS_API int modbus_write_bits (modbus_t * ctx, int addr, int nb, const uint8_t * data):

该函数对应于功能码 15(0x0F) 写多个线圈(Force Multiple Coils)

参数 addr 代表寄存器起始地址,参数 nb 表示线圈个数,而参数 const uint8_t * data 表示待写入的数据块。一般情况下,可以使用数组存储写入数据,数组的各元素取值范围只能是 TRUE(1)或 FALSE(0) 。

MODBUS_API int modbus_write_registers (modbus_t * ctx, int addr, int nb, const uint16_t * data):

该函数对应于功能码 16(0x10) 写多个保持存器(Preset MultipleRegisters)

参数 addr 代表寄存器起始地址,参数 nb 表示存器的个数而参数 const uint16_t * data 表示待写人的数据块。一般情况下,可以使用数组存储写入数据数组的各元素取值范围是 0~0xFFFF 即数据类型 uint16_t 的取值范围。

MODBUS_API int modbus_mask_registers (modbus_t * ctx, int addr, uint16_t and_mask, uint16_t or_mask ):

modbus_mask_write_register() 函数应使用以下算法修改远程设备地址“addr”处的保持寄存器的值:

新值 = (current value AND ‘and’) OR (‘or’ AND (NOT ‘and’)) 。

该功能使用 Modbus 功能代码 0x16(掩码单个寄存器)。

MODBUS_API int modbus_write_and_read_registers (mobus_t * ctx ,

int writer_addr,

int writer_nb,

const uint16_t * src,

int read_addr,

int read_nb,

uint16_t * dest);

modbus_write_and_read_registers() 函数应将 write_nb 保持寄存器的内容从数组 “src” 写入远程设备的地址 write_addr ,然后将 read_nb 保持寄存器的内容读取到远程设备的地址 read_addr 。读取结果作为字值(16 位)存储在 dest 数组中。

必须注意分配足够的内存来存储结果 dest (至少 nb * sizeof(uint16_t))。该功能使用 Modbus 功能代码 0x17(写/读寄存器)。

MODBUS_API int modbus_report_slave_id (modbus_t * ctx, int max_dest, uint8_t * dest):

该函数对应于功能码 17(0x11) 报告从站ID。参数 max_dest 代表最大的存储空间,参数 dest 用于存储返回数据。返回数据可以包括如下内容:从站 ID状态值(0x00= OFF状态, 0xFF=ON状态) 以及其他附加信息,具体的各参数意义由开发者指定。

用法举例:

uint8_t tab_bytes[MODBUS_MAX_PDU_LENGTH];

...

rc =modbus_report_slave_id(ctx, MODBUS_MAX_PDU_LENGTH, tab_bytes);

if (rc>1)
{
  printf("Run Status Indicator: %s\n",tab_bytes[1] ?"ON":"OFF");
}

2.4.3 数据处理的相关函数或宏定义

在libmodbus开发库中,为了方便数据处理在 modbus.h 文件中定义了一系列数据处理宏。

例如获取数据的高低字节序宏定义:

#define MODBUS_GET_HIGH_BYTE (data) (((data) >>8) & 0xFF)
#define MODBUS_GET_LOW_BYTE (data) ((data) & 0xFF)

对于浮点数等多字节数据而言,由于存在字节序与大小端处理等的问题,所以辅助定义了一些特殊函数:

MODBUS_API float modbus_get_float (const uint16_t * src);

MODBUS_API float modbus_get_float_abcd (const uint16_t * src);

MODBUS_API float modbus_get_float_dcba (const uint16_t * src);

MODBUS_API float modbus_get_float_badc (const uint16_t * src);

MODBUS_API float modbus_get_float_cdab (const uint16_t * src);

MODBUS_API void modbus_set_float (float f,uint16_t * dest);

MODBUS_API void modbus_set_float_abcd (float f,uint16_t * dest);

MODBUS_API void modbus_set_float_dcba (float f,uint16_t * dest);

MODBUS_API void modbus_set_float_badc (float f,uint16_t * dest);

MODBUS_API void modbus_set_float_cdab (float f,uint16_t * dest);

当然,可以参照 float 类型的处理方法,继续定义其他多字节类型的数据例如int32_t、uint32_t、 int64_t、uint64_t 以及 double 类型的读写函数。

2.5 RTU/TCP关联接口函数

在文件 modbus.h 的最后位置,有如下语句

#include “modbus-tcp.h”

#include “modbus-rtu.h”

可以发现,除了 modbus.h 包含的接口函数之外,modbus-rtu.h 和 modbus-tcp.h 也包含了必要的接口函数。

2.5.1 RTU模式关联函数

MODBUS_API modbus_t * modbus_new_rtu (const char * device, int baud, char parity, int data_bit, int stop_bit):

此函数的功能是创建一个 RTU 类型的 modbus_t 结构体。参数 const char * device 代表串口字符串,在 Windows 操作系统下形态如 “COMx” ,有一点需要注意的是,对于串口1串口9来说,,传递 “COM1”“COM9” 可以 成功 ,但是如果操作对象为 COM10及以上端口 ,则会出现 错误。

产生这种奇怪现象的原因是:微软预定义的标准设备中含有 “COM1”~“COM9” 。所以,“COM1”~“COM9” 作为文件名传递给函数时操作系统会自动地将之解析为相应的设备。但对于 COM10 及以上的串口,“COM10” 之类的文件名系统只视之为 一般意义上的文件,而非串行设备。为了增加对 COM10 及以上串行端口的支持,微软规定,如果要访问这样的设备,应使用这样的文件名(以COM10 为例):\\.\ COM10。

所以,使用时在代码中可以如此定义:.

const char * device = “\\.\COM10”;

在Linux操作系统下可以使用”/dev/ttySo”或”/dev/ttyUSB0”等形式的字符串来表示。而参数 int baud 表示串口波特率的设置值,例如:9600、19200、57600、115200等。

参数char parity 表示奇偶校验位,取值范围:

  • ‘N’:无奇偶校验;
  • ‘E’:偶校验;
  • ‘O’:奇校验。

参数 int data_bit 表示数据位的长度,取值范围为 5、6、7和8。

参数int stop_bit 表示停止位长度,取值范围为1或2。

用法举例:

modbus t *ctx;

ctx=modbus_new_rtu("\\\\.\\COM10",115200,'N',8,1);

if (ctx ==NULL)
{
  fprintf(stderr,"Unable to create the libmodbus context\n");
  return -1;
}

modbus_set_slave(ctx,SLAVE_DEVICE_ID);

if (modbus connect(ctx)==-1)
{
  fprintf(stderr,"Connection failed:%s\n",modbus_strerror(errno));
  modbus_free(ctx);
  return -1;
}

MODBUS_API int modbus_rtu_set_serial_mode (modbus_t * ctx, int mode):

该函数用于设置串口为 MODBUS RTU RS232或MODBUSRTU_RS485模式,此函数只适用于 Linux 操作系统下。

MODBUS_API int modbus_rtu_set_rts (modbus_t * ctx, int mode)。

MODBUS_API int modbus_rtu_set_custom_rts (modbus_t * ctx, void ( * set_rts) (modbus_t * ctx, int on))。

MODBUS_API int modbus_rtu_set_rts_delay (modbus_t * ctx, int us)。

以上函数只适用于 Linux 操作系统下,RTS 即Request ToSend 的缩写,具体的意义可通过网络搜索,一般情况下,此类函数可忽略。

2.5.2 TCP模式关联函数

MODBUS_API modbus_t * modbus_new_tcp (const char *ip_address, int port)

此函数的功能是创建一个TCP/IPv4 类型的modbus_t 结构体。

参数 const char * ip_address 为IP地址,port 表示远端设备的端口号。

MODBUS_API int modbus_tcp_listen (modbus_t * ctx, int nb_connection)。

此函数创建并监听一个 TCP/IPv4 上的套接字。

参数int nb_connection 代表最大的监听数量,在调用此函数之前,必须首先调用modbus_new_tcp() 创建modbus_t结构体。

MODBUS_API int modbus_tcp_accept (modbus_t * ctx,int * s)。

此函数接收一个 TCP/IPv4 类型的连接请求,如果成功将进入数据接收状态。

三、libmodbus移植与使用

3.1 移植方法

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

根据下图的层次,要移植libmodbus的“后端”,就是构造自己的modbus_backend_t结构体:

在这里插入图片描述

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

成员含义
unsigned int backend_type;后端类型,是RTU还是TCP
unsigned int header_length;头部长度,比如RTU数据包前面需要有1字节的设备地址,头部长度就是1
unsigned int checksum_length;校验码长度,RTU的校验码是2字节
unsigned int max_adu_length;ADU(数据包)最大长度
set_slave设置从站地址
build_request_basis设置RTU请求包的基本数据,这些数据的格式是一样的,比如req[0]是从设备地址,req[1]是功能码,req[2]和req[3]是寄存器地址,req[4]和req[5]是寄存器数量
build_response_basis设置RTU回应包的基本数据,这些数据的格式是一样的,比如req[0]是从设备地址,req[1]是功能码
prepare_response_tid生产传输标识TID,在TCP中使用
send_msg_pre发送消息前的准备工作,对于RTU是填充CRC检验码,对于TCP是填充头部的Length
send发送数据包
receive接收数据包
recv接收原始数据,receive会调用recv得到原始数据然后解析出数据包
check_integrity检查数据包的完整性
pre_check_confirmation检查响应数据包是否有效时,先执行pre_check_confirmation做一些简单的检查
connect硬件相关的连接,对于RTU就是打开串口、设置串口波特率等;对于TCP则是连接对端
is_connected判断是否已经连接
close关闭连接
flush清空接收到的、未处理的数据
select阻塞一段时间以等待数据
free释放分配的modbus_t等结构体

打开 backend 结构体所在 .c 文件的目录位置,复制该文件并修改名字为 modbus-st-rtu.c:
在这里插入图片描述
将其添加到 source insight 工程中,修改其中代码。
我们是要在 Freertos 或者裸机上使用 libmodbus ,所以不会用到 linux 相关代码,将其删除:
在这里插入图片描述
将与 win32 相关代码删除:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 使用USB串口作为后端

基于这2个程序:

在这里插入图片描述

在这里插入图片描述

第1步:合并上述2个源码,并修改到能编译成功(但是libmodbus里对USB串口的操作),结果放在如下目录:

在这里插入图片描述

第2步,继续修改上图的代码,实现USB串口作为后端,得到以下代码:

在这里插入图片描述

USB串口的操作函数:

/* 发送数据 */

int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);

/* 接收数据 */

int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout);

3.3 libmodbus从机实验(USB串口)

本节源码为:
在这里插入图片描述

参考“libmodbus-3.1.10\tests\unit-test-server.c”,把开发板当做从机,使用PC上Modbus Poll软件读写开发板:控制LED。

要点:

① printf、fprintf、vfprintf都不能使用,改成空的宏

② p_tv 不能成为空指针

仿照下图流程编写代码:

在这里插入图片描述

app_freertos.c

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os2.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "draw.h"
#include "stdio.h"
#include "draw.h"
#include "ux_api.h"
#include "modbus.h"
#include "errno.h"

static void LibmodbusServerTask( void *pvParameters )	
{
	uint8_t *query;
	modbus_t *ctx;											//创建一个modbus结构体
	int rc;
	modbus_mapping_t *mb_mapping;

	/* modbus_new_st_rtu 函数需要在 modbus.h 中进行声明 */
	ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);		//设置串口相关参数
	modbus_set_slave(ctx, 1);								//设置地址为1
	query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);		//分配空间,后续会调用modbus_receive来获得主机发过来请求,query相当于是一个查询报文


	/*
    mb_mapping = modbus_mapping_new_start_address(UT_BITS_ADDRESS,
                                                  UT_BITS_NB,
                                                  UT_INPUT_BITS_ADDRESS,
                                                  UT_INPUT_BITS_NB,
                                                  UT_REGISTERS_ADDRESS,
                                                  UT_REGISTERS_NB_MAX,
                                                  UT_INPUT_REGISTERS_ADDRESS,
                                                  UT_INPUT_REGISTERS_NB);
                                                  */	
	mb_mapping = modbus_mapping_new_start_address(0,		//可读可写bit起始地址				
												  10,		//个数
												  0,		//只读bit起始地址
												  10,		//个数
												  0,		//可读可写寄存器起始地址
												  10,		//个数
												  0,		//只读寄存器起始地址
												  10);		//个数

	memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);					//内存块初始化函数(可读可写字节) 将tab_bits中的nb_bits个字节设置为0
	memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers * 2);	//内存块初始化函数(可读可写寄存器,注意每个寄存器对应两个字节,所以是0x5555,需要*2,对应十进制是21845) 将tab_registers中的nb_registers * 2个字节设置为0x55

	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);
	}

	for (;;) {
		do {
			rc = modbus_receive(ctx, query);	//调试时发现没现象 跳过去此函数的编写 找到modbus.c的387行
			/* Filtered queries return 0 */
		} while (rc == 0);

		/* The connection is not closed on errors which require on reply such as
		   bad CRC in RTU. */
		if (rc == -1 && errno != EMBBADCRC) {
			/* Quit */
			continue;
		}

		rc = modbus_reply(ctx, query, rc, mb_mapping);
		if (rc == -1) {
			//break;
		}
		if (mb_mapping->tab_bits[0])//tab_bits的第1位如果非0就点灯
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
		else
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);
	}
	
	modbus_mapping_free(mb_mapping);
	vPortFree(query);
	/* For RTU */
	modbus_close(ctx);
	modbus_free(ctx);

	vTaskDelete(NULL);	//这里原本是return 0;但作为任务函数不能直接返回,结束后需要自杀
}

xTaskCreate(
	LibmodbusServerTask, 
	"LibmodbusServerTask", 
	200, 
	NULL, 
	osPriorityNormal, 
	NULL); 

modbus.c

if (msg_type == MSG_INDICATION) {
    /* Wait for a message, we don't know when the message will be
     * received */
    if (ctx->indication_timeout.tv_sec == 0 && ctx->indication_timeout.tv_usec == 0) {
        /* By default, the indication timeout isn't set */
        //p_tv = NULL;
        tv.tv_sec = 0;	//主要修改位置在此,尽管sec或usec为0,但不能让p_tv成为空指针
        tv.tv_usec = 0;
        p_tv = &tv;
    } else {
        /* Wait for an indication (name of a received request by a server, see schema)
         */
        tv.tv_sec = ctx->indication_timeout.tv_sec;
        tv.tv_usec = ctx->indication_timeout.tv_usec;
        p_tv = &tv;
    }
} else {
    tv.tv_sec = ctx->response_timeout.tv_sec;
    tv.tv_usec = ctx->response_timeout.tv_usec;
    p_tv = &tv;
}

modbus.h

#define debug_printf(...)
#define debug_fprintf(...)

打开 Modbus Poll 软件,进行相关配置:
在这里插入图片描述
成功读取数据:
在这里插入图片描述
再开启另一个窗口,进行如下配置:
在这里插入图片描述
在这里插入图片描述
配置成功后如下,点击第一个数据进行改写,可以看到板子上的led出现现象:
在这里插入图片描述

3.4 libmodbus主机实验(USB串口)

本节源码为:

在这里插入图片描述

参考“libmodbus-3.1.10\tests\unit-test-client.c”,把开发板当做主机,去读写PC上Modbus Slave软件模拟的从机。

仿照下图流程编写代码

在这里插入图片描述
主机先读取寄存器1中的数据,读取完毕后将数据+1并写入到寄存器2,同时将新数据显示在lcd上。

app_freertos.c

static void LibmodbusClientTask( void *pvParameters )	
{
	modbus_t *ctx;											//创建一个modbus结构体
	int rc;
	uint16_t val;
	int nb = 1;

	ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);		//设置串口相关参数
	modbus_set_slave(ctx, 1);								//设置地址为1

	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);
	}

	for (;;) {
		/* read holding register 1 */
		rc = modbus_read_registers(ctx, 1, 1, &val);
		if(rc != nb)
			continue;
		
		/* display on led */
		Draw_Number(0, 0, val, 0xffff00);

		/* val++ */
		val++;

		/* write val to  holding register 2 */
		rc = modbus_write_registers(ctx, 2, 1, &val);
	}

	/* For RTU */
	modbus_close(ctx);
	modbus_free(ctx);

	vTaskDelete(NULL);//这里原本是return 0;但作为任务函数不能直接返回,结束后需要自杀
}

#if 0
	xTaskCreate(
		LibmodbusServerTask, 
		"LibmodbusServerTask", 
		200, 
		NULL, 
		osPriorityNormal, 
		NULL); 
#else
	xTaskCreate(
		LibmodbusClientTask, 
		"LibmodbusClientTask", 
		200, 
		NULL, 
		osPriorityNormal, 
		NULL);
#endif

打开 Modbus Slave 软件,进行相关配置:
在这里插入图片描述
在这里插入图片描述

3.5 使用板载串口作为后端

本节代码如下:

在这里插入图片描述

按照下图连线:调试、供电、两个485互连,使用CH1(左边的RS485接口)作为主设备,访问CH2(右边的RS485接口):

在这里插入图片描述

3.5.1 使用UART_Device

把UART2、UART4的发送、接收功能都补全了,并且构造了对应的UART_Device结构体,里面实现了初始化、发送、接收一个自己的的函数,如下:

把UART2、UART4封装为UART_Device的代码为:UART\uart_rtos_all_ok.7z。需要把它的代码移植到本节的工程里:

  • 使用STM32CubeMX配置UART2、UART4:发送、接收都使用DMA
  • 复制代码:Core\Src\usart.c、Drivers\Module_driver\uart_device.c/h

使用STM32CubeMX配置的过程如下:

  • 使能DMA通道:

在这里插入图片描述

  • 各个DMA通道的配置如下:

在这里插入图片描述

3.5.2 用作后端

把UART2、UART4用作libmodbus后端时,只需要修改这几个函数即可:

在这里插入图片描述

有两个UART_Device,调用哪个UART_Device?在使用“modbus_new_st_rtu”创建modbus_t时,根据传入的设备名在modbus_t结构体里记录对应的UART_Device。_modbus_rtu_connect、_modbus_rtu_send、_modbus_rtu_recv这三个函数,就可以直接调用modbus_t结构体里的UART_Device函数了。
移植代码时,以下部分代码需要注意,解释如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
modbus_rtu_send 函数代码如下:

static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
	/* 使用usb/UART2/UART4的UART_Device来发送数据 */
	/* 取出记录在ctx_rtu结构体中的dev来发送数据 */
	modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;
	if (0 == pdev->Send(pdev, (uint8_t *)req, req_length, TIMEROUT_SEND_MSG))
		return req_length;
	else
	{
		errno = EIO;
		return -1;	
	}
	return 0;
}

_modbus_rtu_recv 函数代码如下:

	/* 使用usb/UART2/UART4的UART_Device来接收数据 */
	modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;

	if (0 == pdev->RecvByte(pdev, rsp, timeout))
		return 1;
	else
	{
		errno = EIO;
		return -1;
	}

接下来仿照原来UART2、UART4的多个函数结构体写一个USB结构体:
UART2、UART4结构体及相关函数如下(usart.c):

int UART2_GetData(struct UART_Device *pdev, uint8_t *pData, int timeout)
{
	if (pdPASS == xQueueReceive(g_xUART2_RX_Queue, pData, timeout))
		return 0;
	else
		return -1;
}

int UART2_Rx_Start(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
	if (!g_xUART2_RX_Queue)
	{
		g_xUART2_RX_Queue = xQueueCreate(200, 1);
		g_UART2_TX_Semaphore = xSemaphoreCreateBinary( );
		
		HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);
	}
	return 0;
}

int UART2_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
	HAL_UART_Transmit_DMA(&huart2, datas, len);
	
	if (pdTRUE == xSemaphoreTake(g_UART2_TX_Semaphore, timeout))
		return 0;
	else
		return -1;
}

int UART4_GetData(struct UART_Device *pDev, uint8_t *pData, int timeout)
{
	if (pdPASS == xQueueReceive(g_xUART4_RX_Queue, pData, timeout))
		return 0;
	else
		return -1;
}

int UART4_Rx_Start(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
	if (!g_xUART4_RX_Queue)
	{
		g_xUART4_RX_Queue = xQueueCreate(200, 1);
		g_UART4_TX_Semaphore = xSemaphoreCreateBinary( );
		
		HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100);
	}
	return 0;
}

int UART4_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
	HAL_UART_Transmit_DMA(&huart4, datas, len);
	
	if (pdTRUE == xSemaphoreTake(g_UART4_TX_Semaphore, timeout))
		return 0;
	else
		return -1;
}

int UART2_Flush(struct UART_Device *pDev)
{
	int cnt = 0;
	uint8_t data;
	while (1)
	{
		if (pdPASS != xQueueReceive(g_xUART2_RX_Queue, &data, 0))
			break;
		cnt++;
	}
	return cnt;
}

int UART4_Flush(struct UART_Device *pDev)
{
	int cnt = 0;
	uint8_t data;
	while (1)
	{
		if (pdPASS != xQueueReceive(g_xUART4_RX_Queue, &data, 0))
			break;
		cnt++;
	}
	return cnt;
}

struct UART_Device g_uart2_dev = {"uart2", UART2_Rx_Start, UART2_Send, UART2_GetData, UART2_Flush};
struct UART_Device g_uart4_dev = {"uart4", UART4_Rx_Start, UART4_Send, UART4_GetData, UART4_Flush};

USB结构体及相关函数如下(ux_device_cdc_acm.c):

static int USBSerial_Init(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
	return 0;
}

static int USBSerial_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
	return ux_device_cdc_acm_send(datas, len, timeout);
}

static int USBSerial_GetData(struct UART_Device *pdev, uint8_t *pData, int timeout)
{
	return ux_device_cdc_acm_getchar(pData, timeout);
}

static int USBSerial_Flush(struct UART_Device *pdev)
{
	return ux_device_cdc_acm_flush();
}

struct UART_Device g_usbserial_dev = {"usb", USBSerial_Init, USBSerial_Send, USBSerial_GetData, USBSerial_Flush};

在ux_device.c中进行声明:

extern struct UART_Device g_uart2_dev;
extern struct UART_Device g_uart4_dev;
extern struct UART_Device g_usbserial_dev;

static struct UART_Device *g_uart_devices[] = {&g_uart2_dev, &g_uart4_dev, &g_usbserial_dev};

_modbus_rtu_connect函数如下:

static int _modbus_rtu_connect(modbus_t *ctx)
{
	/* 使用usb/UART2/UART4的UART_Device来初始化设备 */
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
	struct UART_Device *pdev = ctx_rtu->dev;

	pdev->Init(pdev, ctx_rtu->baud, ctx_rtu->parity, ctx_rtu->data_bit, ctx_rtu->stop_bit);
	
    ctx->s = 1; //open(ctx_rtu->device, flags);
    return 0;
}

_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);
}

3.6 libmodbus实验(板载串口)

本节源码为:

在这里插入图片描述

按照下图连线:调试、供电、两个485互连:

在这里插入图片描述

创建一个ServerTask,使用CH2(右边的RS485接口,对应UART4)对外通信。

创建一个ClientTask,使用CH1(左边的RS485接口,对应UART2)读写从设备数据。

app_freertos.c

static void CH2_UART4_ServerTask( void *pvParameters )	
{
	uint8_t *query;
	modbus_t *ctx;
	int rc;
	modbus_mapping_t *mb_mapping;
	
	ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);
	modbus_set_slave(ctx, 1);
	query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);

	mb_mapping = modbus_mapping_new_start_address(0,
												  10,
												  0,
												  10,
												  0,
												  10,
												  0,
												  10);
	
	memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
	memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers*2);

	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);;
	}

	for (;;) {
		do {
			rc = modbus_receive(ctx, query);
			/* Filtered queries return 0 */
		} while (rc == 0);
 
		/* The connection is not closed on errors which require on reply such as
		   bad CRC in RTU. */
		if (rc == -1 && errno != EMBBADCRC) {
			/* Quit */
			continue;
		}

		rc = modbus_reply(ctx, query, rc, mb_mapping);
		if (rc == -1) {
			//break;
		}

		if (mb_mapping->tab_bits[0])
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
		else
			HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);

		vTaskDelay(1000);
		mb_mapping->tab_registers[1]++;		//将第一个寄存器的数值从0x55开始自增
	}

	modbus_mapping_free(mb_mapping);
	vPortFree(query);
	/* For RTU */
	modbus_close(ctx);
	modbus_free(ctx);

	vTaskDelete(NULL);
}

static void CH1_UART2_ClientTask( void *pvParameters )	
{
	modbus_t *ctx;
	int rc;
	uint16_t val;
	int nb = 1;
	int level = 1;
	
	ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);
	modbus_set_slave(ctx, 1);
	
	rc = modbus_connect(ctx);
	if (rc == -1) {
		//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
		modbus_free(ctx);
		vTaskDelete(NULL);;
	}

	for (;;) {
		/* read hoding register 1 */
		rc = modbus_read_registers(ctx, 1, nb, &val);
		if (rc != nb)
			continue;

		/* display on lcd */
		Draw_Number(0, 0, val, 0xff0000);

		/* delay 2s */
		vTaskDelay(2000);	
		modbus_write_bit(ctx, 0, level);	//对应上面的server任务,在上面任务中设置了第0位对led灯进行操作,所以在这里读取到数据后去设置第0位的数值
		level =! level;						//lever取反,即灯会闪烁
	}

	/* For RTU */
	modbus_close(ctx);
	modbus_free(ctx);	

	vTaskDelete(NULL);
}

xTaskCreate(
	CH1_UART2_ClientTask, 
	"CH1_UART2_ClientTask", 
	200, 
	NULL, 
	osPriorityNormal, 
	NULL); 
	
xTaskCreate(
	CH2_UART4_ServerTask,
	"CH2_UART4_ServerTask", 
	200, 
	NULL, 
	osPriorityNormal, 
	NULL); 		

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

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

相关文章

2024版Assimp配置教程

最近想看看图形学&#xff0c;选择速通LearnOpenGL&#xff0c;不出意外最耗时间的依然是配置环境。按照教程上的把GLFW等等配置的没有问题&#xff0c;但是在Assimp这里卡住了。原因是教程上说的不详细&#xff0c;而网上查的又和现在的版本相去甚远&#xff0c;导致捣鼓了好一…

Linux基础1-基本指令6(grep,zip,tar,查看系统等命令)

一.本章重点 1.grep命令用于过滤文本信息,sort,uniq 2.zip/uzip命令用于压缩&#xff0c;解压文件 3.tar命令用于压缩&#xff0c;解压文件 二.grep grep命令 gerp(文件内容的行过滤工具)&#xff0c;默认会&#xff0c;会匹配文本中的关键字&#xff0c;匹配上的进行行显示 …

全民k歌怎么去水印保存?盘点分享3个无水印保存方法

在全民K歌的世界里&#xff0c;我们尽情展现音乐才华&#xff0c;但有时却会遇到一个棘手的问题&#xff1a;如何将歌曲视频无水印保存&#xff0c;以便自由分享到其他社交平台&#xff1f;为了解决这一难题&#xff0c;本文将为你盘点三种简单有效的无水印保存方法&#xff0c…

Python课堂笔记

1.大小写 大写&#xff1a;True、 None、 False 注意&#xff1a;大小写含义不相同 2.一行多个短句 短句&#xff1a;“ &#xff1b;” 长句&#xff1a;“ \” 3.变量 (1) int A[100] 整型 char B[100] 字符型 &#xff08;2&#xff09;type: 查看变量类型 补充&…

Language Models are Unsupervised Multitask Learners

摘要 自然语言处理任务&#xff0c;如问答、机器翻译、阅读理解和摘要&#xff0c;通常在任务特定的数据集上使用监督学习来处理。当在一个名为WebText的数百万网页的新数据集上训练时,我们证明了语言模型在没有任何明确监督的情况下开始学习这些任务。在不使用127,000多个训练…

【BPF之旅】认识eBPF

文章目录 一、eBPF基础认识1.1 eBPF历史演进1.2 eBPF特点和使用场景eBPF的特点&#xff08;优势&#xff09;eBPF的限制&#xff08;安全性的体现&#xff09;eBPF vs 内核模块应用场景 1.3 eBPF工作原理eBPF程序执行过程eBPF的开销 二、eBPF简单实践&#xff08;Hello World&a…

大数据技术

4v特点 volume&#xff08;体量大&#xff09; velocity&#xff08;处理速度快&#xff09; variety&#xff08;数据类型多&#xff09; value&#xff08;价值密度低&#xff09; 核心设计理念 并行化 规模经济 虚拟化 分布式系统满足需求 系统架构 大数据处理流程 结构化…

如何在QT6上配置文心一言的接口,从而生成一个自己的对话框

这里写自定义目录标题 前言&#xff1a;效果展示&#xff1a;环境配置&#xff1a;计划完善&#xff1a;核心代码&#xff1a; 前言&#xff1a; 网上有很多在前端调用文心一言接口的&#xff0c;想在QT上配置文心一言的接口&#xff0c;从而生成一个自己的对话框。 效果展示…

Sentinel-1 Level 1数据处理的详细算法定义(九)

《Sentinel-1 Level 1数据处理的详细算法定义》文档定义和描述了Sentinel-1实现的Level 1处理算法和方程&#xff0c;以便生成Level 1产品。这些算法适用于Sentinel-1的Stripmap、Interferometric Wide-swath (IW)、Extra-wide-swath (EW)和Wave模式。 今天介绍的内容如下&…

JavaScript学习文档(10):日期对象、节点操作、 M端事件、JS插件、学生信息表案例

目录 一、日期对象 1、实例化 2、时间对象方法 3、时间戳 &#xff08;1&#xff09;时间戳 &#xff08;2&#xff09;获取时间戳的三种方式&#xff1a; &#xff08;3&#xff09;倒计时效果 二、节点操作 1、DOM 节点 2、 查找节点 3、增加节点 &#xff08;1&…

windows11 上安装了python的wxpython模块,vscode运行时还是报错的解决方法

遇到问题&#xff1a;windows11 上明明安装了python的wxpython模块&#xff0c;vscode运行时还是报错“Traceback (most recent call last): File “c:\pythoncode\new\tonguedetect.py”, line 1, in import wx ModuleNotFoundError: No module named ‘wx’” 如何解决&…

uniapp uni-popup底部弹框留白 底部颜色修改 滚动穿刺

做底部弹框的时候&#xff0c;可能出现以下场景需要处理。 一、出现底部留白不是白色&#xff0c;需要修改颜色的时候&#xff1a; 1、如果弹框不需要圆角效果&#xff0c;则在uni-popup加上背景色就行&#xff0c;弹框是个直角样式&#xff1a; 2、如果需要圆角效果&#xff0…

芒格-“用幸存者心态去对待问题,永远不要有受害者心态”

我不会因为人性而感到意外&#xff0c;也不会花太多时间感受背叛&#xff0c; 我总是低下头去调整自己&#xff0c;去适应这一类事情&#xff0c; 所以我不允许自己花太多时间&#xff0c;去感受背叛&#xff0c; 但凡有一丁点这种想法&#xff0c;从我脑海闪过&#xff0c;我就…

Ubuntu 18.04升级gclibc为2.28版本

一、查看系统支持的 GLIBC 版本号 ​strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC_出现以下&#xff0c;说明到2.27版本&#xff0c;没有2.28版本&#xff0c;所以我们需要手动安装 GLIBC_2.2.5 GLIBC_2.2.6 GLIBC_2.3 GLIBC_2.3.2 GLIBC_2.3.3 GLIBC_2.3.4 GLIBC_…

Linux环境下的MySQL的卸载、安装与使用[以CentOS7为例说明]

Linux环境下的MySQL的卸载、安装与使用[以CentOS7为例说明] 1、下载MySQL安装包2、卸载MySQL&#xff08;1&#xff09;检查是否安装过MySQL和mariadb&#xff08;2&#xff09;卸载MySQL和mariadb&#xff08;3&#xff09;问题记录&#xff08;了解&#xff09; 3、安装MySQL…

MacOS通过Docker部署MySQL数据库,以及Docker Desktop进行管理

目录 一.不需要持久化存储 1.启动容器 2.查看容器和镜像 3.容器管理 二.持久化存储启动mysql容器 1.创建docker卷 2.运行容器,指定卷 3.在mysql里面随便建个库,建张表,弄点数据 4.停止并删除MySQL容器 5.重新运行容器,并且挂载相同的卷,也就是上面第二步的命令 6.连…

【412】【统计近似相等数对 II】

差130个样例&#xff0c;等佬解 class Solution:def ifqual(self,str1,str2):return int(str1)int(str2)def change(self,str1,str2):str1 list(str1)nlen(str1)t0for i in range(n):for j in range(i1,n):str1[i],str1[j]str1[j],str1[i]t1if self.ifqual("".join…

k8s-使用Network Policies实现网络隔离

一、需求 Kubernetes 的命名空间主要用于组织和隔离资源&#xff0c;但默认情况下&#xff0c;不同命名空间中的 Pod 之间是可以相互通信的。为了实现更严格的网络隔离&#xff0c;同一套k8s需要根据不同的命名空间进行网络环境隔离&#xff0c;例如开发&#xff08;dev01&…

Plik文件上传系统本地docker部署与远程访问传输文件详细操作流程

文章目录 前言1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 前言 本文介绍如何使用Linux docker方式快速安装Plik并且结合Cpolar内网穿透工具实现远程访问&#xff0c;实现随时随地在任意设…

blender4.2中安装插件的方式

1&#xff0c;安装好blender之后&#xff0c;打开软件&#xff0c;找到 编辑&#xff0c;---> 偏好设置&#xff0c; 插件---> 从磁盘安装...., 找到插件.zip格式的文件&#xff0c; 选择 .zip格式的文件后&#xff0c;点击“从磁盘安装”按钮&#xff0c;即可