libmodbus 开发库概述
libmodbus是一个免费的跨平台支持RTU和TCP的Modbus库,遵循LGPL V2.1+协议。libmodbus支持Linux、 Mac Os X、 FreeBSD、 QNX和Windows等操作系统。 libmodbus可以向符合Modbus协议的设备发送和接收数据,并支持通过串口或者TCP网络进行连接。
可以从libmodbus的官方网站下载源代码,也可以从Git仓库下载,本文以版本v3.1.10 为例进行讲解。
代码结构分析:
- 解压后, 源代码根目录下有4个文件夹:
- ① doc目录: libmodbus库的各API接口说明文档。
- ② m4目录: 存放GNU m4文件,在这里对理解代码没有意义,可忽略。
- ③ src目录: 全部libmodbus源文件。
- ④ tests目录: 包含自带的测试代码 其他文件对理解源代码关系不大,可以暂时忽略
- 展开src代码目录,除了有modbus的核心文件外,还有不少编译环境的配置文件
- modbus核心文件
modbus.c/h
: 核心文件,实现Modbus协议层,定义共通的Modbus消息发送和接收函数各功能码对应的函数。modbus-private.h
: libmodbus内部使用的数据结构和函数定义。modbus-data.c
: 数据处理的共通函数,包括大小端相关的字节、位交换等函数modbus-rtu.c/h
: 通信层实现, RTU模式相关的函数定义,主要是串口的设置、连接及消息的发送和接收等modbus-rtu-private.h
: RTU模式的私有定义modbus-tcp.c/h
: 通信层实现, TCP模式下相关的函数定义,主要包括TCP/IP网络的设置连接、消息的发送和接收等modbus-tcp-private.h
: TCP模式的私有定义。
- IDE的配置文件
- win32文件夹: 定义在Windows下使用Visual Studio编译时的项目文件和工程文件以及相关配置选项等。
Makefile.am
: Linux下AutoTool编译时读取相关编译参数的配置文件,用于生成Makefile文件。
- 其它文件
- modbus-version.h.in: 版本定义文件
- modbus核心文件
源代码解析
核心函数解析
以Modbus RTU协议为例,主设备、从设备初始化后便可开始进行通信:
软件架构层次
-
从数据的收发过程, 可以把使用 libmodbus 的源码分为 3 层:
- ① Modbus APP 应用层:它需要知道要做什么,即主设备要读/写哪个设备的哪些寄存,从设备需要提供/接收什么样的数据
- ② Modbus 核心层: 向APP层提供接口函数, 向下调用底层代码“构造、发送、接收、解析” 数据包
- ③ Modbus 底层 : 针对不同硬件(串口、网络等)提供具体的数据封包、收发和解包服务
-
APP应用层
- libmodbus-3.1.10 中数据收发核心接口函数及其应用:
-
核心层
-
modbus.c
文件:实现了应用层使用的各类Modbus函数 -
modbus-private.h
:抽象出了的主要数据结构,如struct _modbus
和struct _modbus_backend等。
//结构体定义位于modbus-private.hstruct _modbus {/* Slave address*/int slave; //从站设备地址/* Socket or file descriptor */int s; //RTU 下是串口句柄, TCP 下是 Socketint debug; //是否启动 Debug 模式(打印调试信息)int error_recovery; //错误恢复模式:具体见下文注解int quicks; //见下文注解struct timeval response_timeout; //等待回应的超时时间,默认是 0.5Sstruct timeval byte_timeout; //接收一个字节的超时时间,默认是 0.5Sstruct timeval indication_timeout; //等待请求的超时时间const modbus_backend_t *backend; //硬件传输层的结构体void *backend_data; //硬件传输层的私有数据 }; typedef struct _modbus modbus_t;
- error_recovery可能的取值
- MODBUS_ERROR_RECOVERY_NONE:由 APP 处理 错误
- MODBUS_ERROR_RECOVERY_LINK:如果有连接 错误,则重连
- MODBUS_ERROR_RECOVERY_PROTOCOL:如果数 据不符合协议要求,则清空所有数据
- quirks可能的取值
- MODBUS_QUIRK_MAX_SLAVE:从站地址最大值设为255,默认是247
- MODBUS_QUIRK_REPLY_TO_BROADCAST:回应广播包
- error_recovery可能的取值
-
-
底层
-
根据具体硬件,实例化
struct _modbus_backend
结构体//结构体定义位于modbus-private.htypedef struct _modbus_backend {unsigned int backend_type; //后端类型(RTU 还是 TCP)unsigned int header_length; //头部长度(比如 RTU 数据包前面1字节长的设备地址)unsigned int checksum_length; //校验码长度, RTU 的校验码是 2 字节unsigned int max_adu_length; //ADU(数据包) 最大长度int (*set_slave)(modbus_t *ctx, int slave); //设置从站地址int (*build_request_basis)(modbus_t *ctx, //设置 RTU 请求包的基本数据int function, //功能码int addr, //寄存器地址int nb, //寄存器数量uint8_t *req);int (*build_response_basis)(sft_t *sft, //设置RTU回应包基本数据(从设备地址、功能码)uint8_t *rsp);int (*prepare_response_tid)(const uint8_t *req, //生产传输标识TID,在TCP中使用int *req_length);int (*send_msg_pre)(uint8_t *req, //发送消息前的准备,如填充CRC(RTU)或填充头部长度(TCP) int req_length);ssize_t (*send)(modbus_t *ctx, const uint8_t *req, int req_length); //发送数据包int (*receive)(modbus_t *ctx, uint8_t *req); //接收数据包ssize_t (*recv)(modbus_t *ctx, uint8_t *rsp, int rsp_length); //接收原始数据包int (*check_integrity)(modbus_t *ctx, uint8_t *msg, const int msg_length);//检查数据包完整性int (*pre_check_confirmation)(modbus_t *ctx, //检查响应数据包是否有效前的工作const uint8_t *req,const uint8_t *rsp,int rsp_length);int (*connect)(modbus_t *ctx); //硬件相关的连接,对于RTU就是打开串口、设置波特率等;对于TCP 则是连接对端unsigned int (*is_connected)(modbus_t *ctx); //判断是否已经连接void (*close)(modbus_t *ctx); //关闭连接int (*flush)(modbus_t *ctx); //清空接收到的、未处理的数据int (*select)(modbus_t *ctx, //阻塞一段时间以等待数据fd_set *rset, struct timeval *tv, int msg_length); void (*free)(modbus_t *ctx); //释放分配的 modbus_t 等结构体 } modbus_backend_t;
-
modbus-rtu.c
:实现了基于串口传输的各类底层收发函数 -
modbus-tcp.c
:实现了基于TCP/IP网络传输的各类底层收发函数
-
APP应用层接口函数介绍
应用层接口函数主要位于modbus.c 文件中,大致可分为3类
辅助接口函数
modbus_set_slave()
int modbus_set_slave(modbus_t *ctx, int slave);
- 函数功能:设置从站地址,但是由于传输方式不同而意义稍有不同。 RTU模式下: 若为主站设备,则相当于定义远端设备ID,若为从站设备端 ,则相当于定义自身设备 ID。TCP 模式下:此函数一般不需要,但在串行 Modbus设备转换为 TCP模式传输的情况下,此函数才被使用。
modbus_set_error_recovery()
int modbus_set_error_recovery(modbus_t *ctx, modbus_error_recovery_mode error_recovery);
- 函数功能:在连接失败或者传输异常的情况下,设置错误恢复模式。有 3种错误恢复模式可选(单选或复选)
- MODBUS_ERROR_RECOVERY_NONE :应用程序自身处理错误 (默认选项)
- MODBUS_ERROR_RECOVERY_LINK :经过一段延时,libmodbus 内部自动尝试进行断开/连接
- MODBUS_ERROR_RECOVERY_PROTOCOL :在 CRC 错误或功能码错误的情况下,传输会进入延时状态,同时数据直接被清除,一般不推荐。
modbus_set_socket()
int modbus_set_socket(modbus t * ctx, int s);
-
函数功能:在多客户端连接到单一服务器的场合下,设置当前的 SOCKET 或串口句柄
-
用法示例
#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_get_response_timeout() / modbus_get_response_timeout()
int modbus_get_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
int modbus_set_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
- 函数功能:用于获取或设置响应超时时间。注意时间单位分别是秒和微秒。
modbus_get_byte_timeout() / modbus_get_byte_timeout()
int modbus_get_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
int modbus_set_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
- 函数功能:用于获取或设置连续字节之间的超时时间。注意时间单位分别是秒和微秒。
modbus_get_header_length()
int modbus_get_header_length (modbus_t *ctx);
- 函数功能:获取报文头长度
modbus_connect()
int modbus_connect (modbus_t *ctx);
- 函数功能:用于主站设备与从站设备建立连接。
- 在 RTU 模式下,它实质调用了文件 modbus_rtu.c 中的
_modbus_rtu_connect()
函数,进行了串口波特率校验位、数据位、停止位等的设置。 - 在 TCP 模式下,它实质调用了文件 modbus_rtu.c 中的
_modbus_tcp_connect()
函数,对TCP/IP各参数进行了设置和连接。
- 在 RTU 模式下,它实质调用了文件 modbus_rtu.c 中的
modbus_close()
void modbus_close (modbus_t * ctx);
- 函数功能:在应用程序结束之前,一定记得调用此函数关闭Modbus 连接。
- 在 RTU 模式下,实质是调用函数
_modbus_rtu_close(modbus_t * ctx)
关闭串口句柄; - 在 TCP 模式下 , 实质是调用函数
_modbus_tcp_close(modbust * ctx)
关闭Socket 句柄 。
- 在 RTU 模式下,实质是调用函数
modbus_free()
void modbus_free (modbus_t * ctx);
- 函数功能:在应用程序结束之前,一定记得调用此函数释放结构体 modbus_t 占用的内存。
modbusmodbus_set_debug()
int modbus_set_debug (modbust * ctx, int flag);
- 函数功能:用于是否设置为DEBUG模式。参数 flag 设置为TRUE,则进入 DEBUG模式。若设置为FALSE,则切换为非 DEBUG模式。在 DEBUG模式下所有通信数据将按十六进制方式显示在屏幕上,以方便调试。
modbus_strerror()
const char * modbus_strerror (int errnum);
- 函数功能:用于根据返回的错误号,获取错误字符串。
功能接口函数
modbus_read_bits()
int modbus_read_bits (modbus t * ctx, int addr, int nb, uint8_t * dest);
-
函数功能:此函数对应于功能码 01(0x01) 读取线圈/离散量输出状态DOs。所读取的值存放于参数 dest 指向的数组空间。注意数组空间至少为 nb 个字节。
-
示例:
#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 + 1; tab_rp_bits = (uint8_t *)malloc(nb * sizeof(uint8_t)); memset(tab_rp_bits, 0, nb * sizeof(uint8_t));//读取一个线圈1 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_read_input_bits()
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_read_registers()
int modbus_read_registers (modbus_t * ctx, int addr, int nb, uint16_t * dest);
-
函数功能:此函数对应于功能码 03(0x03) 读取保持寄存器 ,所读取的值存放于参数 uint16_t * dest 指向的数组空间(大小至少为 nb * sizeof(uint16_t) 个字节)
-
返回值:若读取失败,则返回-1,成功则返回读取的寄存器个数。
-
函数内部调用关系如下图所示
-
示例
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_read_input_registers()
int modbus_read_input_registers (modbus_t * ctx, int addr, int nb, uint16_t* dest );
-
函数功能:此函数对应于功能码 04(0x04) 读取输人寄存器(Read Iput Register),各参数的意义与用法,类似于函数 modbus_read_registers() 。
-
此函数的调用依赖关系如下图
modbus_write_bit()
int modbus_write_bit (modbus_t * ctx, int coil_addr, int status);
- 函数功能:该函数对应于功能码 05(0x05) 写单个线圈或单个离散输出(Force Single Coil)。其中参数 coil_addr 代表线圈地址;参数 status 代表写值取值只能是TRUE(1)或 FALSE(0) 。
modbus_write_register()
int modbus_write_register (modbus_t * ctx, int reg_addr, int value);
- 函数功能:该函数对应于功能码 06(0x06) 写单个保持寄存器(Preset Single Register)。
modbus_write_bits()
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_write_registers()
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。
modbus_mask_write _registers()
int modbus_mask_write_registers (modbus_t * ctx, int addr, uint16_t and_mask, uint16_t or_mask );
- 函数功能:该功能使用 Modbus 功能代码 0x16(掩码单个寄存器),即修改远程设备地址“addr”处保持寄存器的值。其采用如下算法:寄存器新值 = (寄存器原值 AND ‘and_mask’) OR (‘or_mask’ AND (NOT ‘and_mask’))
modbus_write_and_read_registere()
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);
- 函数功能:该功能使用功能代码 0x17(写/读寄存器),将 write_nb 保持寄存器的内容从数组 “src” 写入远程设备的地址 write_addr ,然后将 read_nb 保持寄存器的内容读取到远程设备的地址read_addr 。 读取结果作为字值(16 位)存储在 dest 数组(大小至少 nb * sizeof(uint16_t)中。
modbus_report_slave_id()
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"); }
数据处理函数
多字节数处理宏
在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 类型的读写函数。
RTU/TCP 关联接口函数
在文件 modbus.h 的最后位置,有如下语句:
#include "modbus-tcp.h"
#include "modbus-rtu.h"
可以发现,除了 modbus.h 包含的接口函数之外, modbus-rtu.h 和 modbus-tcp.h 也包含了一些必要的接口函数。
RTU 模式关联函数
-
modbus_new_rtu()
modbus_t *modbus_new_rtu (const char * device, int baud, char parity, int data_bit, int top_bit);
- 函数功能:此函数的功能是创建一个 RTU 类型的 modbus_t 结构体。
- 参数:
- device:代表串口字符串
- 在 Windows 操作系统下形态如 “COMx” ,x 取值1 - 9,10以上应该用形如
\\\\.\\COM10
表示 - 在Linux操作系统下可以使用
/dev/ttyS0
”或/dev/ttyUSB0
等形式的字符串来表示
- 在 Windows 操作系统下形态如 “COMx” ,x 取值1 - 9,10以上应该用形如
- baud: 表示串口波特率的设置值,例如:9600、 19200、 57600、 115200等
- parity :表示奇偶校验位,取值有:‘N’:无奇偶校验; ‘E’:偶校验; ‘O’:奇校验。
- data_bit :表示数据位的长度,取值范围为 5、 6、 7和8
- stop_bit :表示停止位长度,取值范围为1或2
- device:代表串口字符串
-
modbus_rtu_set_serial_mode()
int modbus_rtu_set_serial_mode (modbus_t * ctx, int mode);
- 该函数用于设置串口为 MODBUS RTU RS232或MODBUSRTU_RS485模式,此函数只适用于 Linux 操作系统下。
-
modbus_rtu_set_rt()
int modbus_rtu_set_rts (modbus_t * ctx, int mode); int modbus_rtu_set_custom_rts (modbus_t *ctx, void ( *set_rts)(modbus_t *ctx, int on)); int modbus_rtu_set_rts_delay (modbus_t * ctx, int us)
- 以上函数只适用于 Linux 操作系统下。RTS 即Request To Send 的缩写,一般情况下,此类函数可忽略。
TCP 模式关联函数
-
modbus_new_tcp()
modbus_t * modbus_new_tcp (const char *ip_address, int port);
- 此函数的功能是创建一个TCP/IPv4 类型的modbus_t 结构体。参数
const char *ip_address
为IP地址,port 表示远端设备的端口号。
- 此函数的功能是创建一个TCP/IPv4 类型的modbus_t 结构体。参数
-
modbus_tcp_liste()
int modbus_tcp_listen (modbus_t * ctx, int nb_connection);
- 此函数创建并监听一个 TCP/IPv4 上的套接字。参数 nb_connection 代表最大的监听数量,在调用此函数之前,必须首先调用
modbus_new_tcp()
创建modbus_t结构体。
- 此函数创建并监听一个 TCP/IPv4 上的套接字。参数 nb_connection 代表最大的监听数量,在调用此函数之前,必须首先调用
-
modbus_tcp_accep()
int modbus_tcp_accept (modbus_t * ctx, int * s);
- 此函数接收一个 TCP/IPv4 类型的连接请求,如果成功将进入数据接收状态。
libmodbus 移植与使用
- 思路:libmodbus 支持了 windows 系统、 Linux 系统。如果要在 Freertos 或者裸机上使用 libmodbus,需要移植 libmodbus 里操作硬件的代码。根据前文说讲的libmodbus 的三级层次,就是要移植 libmodbus 的“底层后端” ,即构造自己的
modbus_backend_t
。
1. 新建后端文件
-
我们是基于STM32开发板进行开发,且通信方式采用板载USB转串口进行通信,故以
modbus-rtu.c
为模板,创建modbus-st-rtu.c
文件 -
首先,复制
modbus-rtu.c
为modbus-st-rtu.c
文件,删除其中所有#if defined(_WIN32)
,#if HAVE_DECL_TIOCSRS485
,#if HAVE_DECL_TIOCM_RTS
等不相关的代码段 -
删除,Linux操作系统下使用的rts相关函数:
modbus_rtu_set_custom_rts()
,modbus_rtu_set_rts()
,modbus_rtu_get_rts()
,modbus_rtu_set_rts_delay()
,modbus_rtu_get_rts_delay
。 -
删除,与我们使用的USB串口不相关的操作函数:
modbus_rtu_get_serial_mode()
,modbus_rtu_set_serial_mode
-
重写以下函数:
_modbus_rtu_connect()
,_modbus_rtu_close()
,_modbus_rtu_is_connected()
。/* POSIX */ static int _modbus_rtu_connect(modbus_t *ctx) {modbus_rtu_t *ctx_rtu = (modbus_rtu_t *) ctx->backend_data;ctx->s = open(ctx_rtu->device, flags);return 0; }static unsigned int _modbus_rtu_is_connected(modbus_t *ctx) {return 1; }static void _modbus_rtu_close(modbus_t *ctx) { }static int _modbus_rtu_flush(modbus_t *ctx) {return tcflush(ctx->s, TCIOFLUSH); }
-
最后,将
modbus_new_st_rtu()
函数重命名为modbus_new_st_rtu()
函数,将不相关的宏定义的代码删除。modbus_t *modbus_new_st_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit) {modbus_t *ctx;modbus_rtu_t *ctx_rtu;/* Check device argument */if (device == NULL || *device == 0) {fprintf(stderr, "The device string is empty\n");errno = EINVAL;return NULL;}ctx = (modbus_t *) malloc(sizeof(modbus_t));if (ctx == NULL) {return NULL;}_modbus_init_common(ctx);ctx->backend = &_modbus_rtu_backend; //此处要根据实际,改成你自己的modbus_backend_t结构体!!ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));if (ctx->backend_data == NULL) {modbus_free(ctx);errno = ENOMEM;return NULL;}ctx_rtu = (modbus_rtu_t *) ctx->backend_data;/* Device name and \0 */ctx_rtu->device = (char *) malloc((strlen(device) + 1) * sizeof(char));if (ctx_rtu->device == NULL) {modbus_free(ctx);errno = ENOMEM;return NULL;}strcpy(ctx_rtu->device, device);ctx_rtu->baud = baud;if (parity == 'N' || parity == 'E' || parity == 'O') {ctx_rtu->parity = parity;} else {modbus_free(ctx);errno = EINVAL;return NULL;}ctx_rtu->data_bit = data_bit;ctx_rtu->stop_bit = stop_bit;ctx_rtu->confirmation_to_ignore = FALSE;return ctx; }
2. 复制核心文件到STM32工程目录
-
将解压后的 libmodbus/src 文件夹复制到STM32工程目录下的 /Middlewares/Third_Party/libmodbus 下。
-
编译工程,根据提示暂先“做空”部分未实现的函数,确保工程先编译通过。
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length) {return write(ctx->s, req, req_length); } 改为 static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length) {//return write(ctx->s, req, req_length);return 0; }static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length) {return read(ctx->s, rsp, rsp_length); } 改为 static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length) {return 0; // read(ctx->s, rsp, rsp_length); }static int _modbus_rtu_connect(modbus_t *ctx) {ctx->s = open(ctx_rtu->device, flags);return 0; } 改为 static int _modbus_rtu_connect(modbus_t *ctx) {ctx->s = 1; //open(ctx_rtu->device, flags);return 0; }static int _modbus_rtu_flush(modbus_t *ctx) {return tcflush(ctx->s, TCIOFLUSH); } 改为 static int _modbus_rtu_flush(modbus_t *ctx) {return 0; //tcflush(ctx->s, TCIOFLUSH); }static int _modbus_rtu_select(modbus_t *ctx, fd_set *rset, struct timeval *tv, int length_to_read) {int s_rc;while ((s_rc = select(ctx->s + 1, rset, NULL, NULL, tv)) == -1) {if (errno == EINTR) {if (ctx->debug) {fprintf(stderr, "A non blocked signal was caught\n");}/* Necessary after an error */FD_ZERO(rset);FD_SET(ctx->s, rset);} else {return -1;}}if (s_rc == 0) {/* Timeout */errno = ETIMEDOUT;return -1;}return s_rc; } 改为 static int _modbus_rtu_select(modbus_t *ctx, fd_set *rset, struct timeval *tv, int length_to_read) { // int s_rc; // while ((s_rc = select(ctx->s + 1, rset, NULL, NULL, tv)) == -1) { // if (errno == EINTR) { // if (ctx->debug) { // fprintf(stderr, "A non blocked signal was caught\n"); // } // /* Necessary after an error */ // FD_ZERO(rset); // FD_SET(ctx->s, rset); // } else { // return -1; // } // }// if (s_rc == 0) { // /* Timeout */ // errno = ETIMEDOUT; // return -1; // }return 0; }
3. 添加自己的底层收发函数
-
本文以基于FreeRTOS操作系统的,以USB串口作为Modbus协议进行通信的STM32工程为例,假设工程已经实现了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);
-
修改
modbus_st_rtu.c
文件-
添加RTOS相关头文件:
FreeRTOS.h
,task.h
,定义数据发送超时宏定义:#define TIMEROUT_SEND_MSG 1000
-
添加USB发送函数,代替_modbus_rtu_send()函数
static ssize_t _modbus_rtu_send_usbserial(modbus_t *ctx, const uint8_t *req, int req_length) {/* 发送数据 */int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);if (0 == ux_device_cdc_acm_send((uint8_t *)req, req_length, TIMEROUT_SEND_MSG)) {return req_length; // write(ctx->s, req, req_length);}else{errno = EIO;return -1;} }
-
添加USB接收函数,代替_modbus_rtu_recv()函数
static ssize_t _modbus_rtu_recv_usbserial(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout) {/* 接收数据 */int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout);if (ux_device_cdc_acm_getchar(rsp, timeout) == 0)return 1; // read(ctx->s, rsp, rsp_length);elsereturn -1; }//其中因为添加了timeout参数,需要在modbus-private.h中添加时间相关变量定义 #define ssize_t unsigned int #define fd_set unsigned intstruct timeval {unsigned int tv_sec;unsigned int tv_usec; };struct timespec {unsigned int tv_sec;unsigned int tv_nsec; };
-
添加链路刷新函数,代替
_modbus_rtu_flush()
函数static int _modbus_rtu_flush_usbserial(modbus_t *ctx) {/* 清空usb串口的队列 */int ux_device_cdc_acm_flush(void);return ux_device_cdc_acm_flush(); }
-
根据实际情况,定义自己的modbus_backend_t 结构体
const modbus_backend_t _modbus_rtu_backend_usbserial = { _MODBUS_BACKEND_TYPE_RTU, _MODBUS_RTU_HEADER_LENGTH, _MODBUS_RTU_CHECKSUM_LENGTH, MODBUS_RTU_MAX_ADU_LENGTH, _modbus_set_slave, _modbus_rtu_build_request_basis, _modbus_rtu_build_response_basis, _modbus_rtu_prepare_response_tid, _modbus_rtu_send_msg_pre, _modbus_rtu_send_usbserial, /* 根据实际自定义实现 */ _modbus_rtu_receive, _modbus_rtu_recv_usbserial, /* 根据实际自定义实现 */ _modbus_rtu_check_integrity, _modbus_rtu_pre_check_confirmation, _modbus_rtu_connect, _modbus_rtu_is_connected, _modbus_rtu_close, _modbus_rtu_flush_usbserial, /* 根据实际自定义实现 */ _modbus_rtu_select, _modbus_rtu_free /* 根据实际自定义实现 */ };
-
用FreeRTOS的内存分配(pvPortMalloc())和释放函数(vPortFree())替换掉所有 malloc、 free函数。
-
用空的宏函数
debug_fprint()
替换掉所有 fprintf、 fprintf、 vfprintf 等打印函数。//在modbus.h文件中定义如下宏函数 #define debug_printf(...) #define debug_fprintf(...)
-
在
modbus_rtu.h
等头文件中声明相关接口函数,如modbus_new_st_rtu()
函数。
-
-
修改
modbus.c
文件-
用空的宏函数
debug_fprint()
替换掉所有 fprintf、 fprintf、 vfprintf 等打印函数 -
用FreeRTOS的内存分配(pvPortMalloc())和释放函数(vPortFree())替换掉所有 malloc、 free函数。
-
修改
_sleep_response_timeout()
回应超时函数static void _sleep_response_timeout(modbus_t *ctx) {vTaskDelay(ctx->response_timeout.tv_sec / 1000 + ctx->response_timeout.tv_usec * 1000); }
-
修改
_modbus_receive_msg()
函数
/* 1.注释掉文件描述符部分 */ //FD_ZERO(&rset); //FD_SET(ctx->s, &rset);/* 2.默认超时时间设为0 */ 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 */tv.tv_sec = 0;tv.tv_usec = 0;p_tv = &tv;} /* 3.为接收函数添加超时时间 */while (length_to_read != 0) {//rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);//if (rc == -1) {// _error_print(ctx, "select");//if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {//#ifdef _WIN32// wsa_err = WSAGetLastError();// no equivalent to ETIMEDOUT when select fails on Windows//if (wsa_err == WSAENETDOWN || wsa_err == WSAENOTSOCK) {// modbus_close(ctx);// modbus_connect(ctx);// }// #else// int saved_errno = errno;// if (errno == ETIMEDOUT) {// _sleep_response_timeout(ctx);// modbus_flush(ctx);// } else if (errno == EBADF) {// modbus_close(ctx);// modbus_connect(ctx);// }// errno = saved_errno;// #endif// }// return -1;// }// rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read, p_tv->tv_sec*1000 + p_tv->tv_usec/1000);if (rc == 0) {errno = ECONNRESET;rc = -1;}
-
libmodbus的使用
libmodbus既可以安装在从机(服务器)上,也可以安装在主机(客户端)上,下面分别以这两种情况进行讲解
libmodbus在从机(服务器)上的应用编程
- 以采用USB转串口方式进行通信的RTU模式为例进行讲解,并假设该从机具有离散输入量、线圈数、保持寄存器和输入寄存器各10个
static void LibmodbusServerTask( void *pvParameters )
{uint8_t *query; //ADU请求包指针modbus_t *ctx;int rc;modbus_mapping_t *mb_mapping; //设备寄存器单元的映射// 1. 创建rtu操作句柄ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);// 2. 设置从机地址为 1modbus_set_slave(ctx, 1);// 3. 动态分配数据包存储空间query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);// 4. 分配4个数组分别用于 线圈、离散输入、保持寄存器和输入寄存器,// 注意,每一个线圈/离散输入分配1个字节,每一个保持/输入寄存器分配2字节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); //对保持寄存器数组0x55初始化//5. 连接(本例已实现硬件上的连接,故在连接函数中仅是简单将ctx->s赋为1)rc = modbus_connect(ctx);if (rc == -1) {//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));modbus_free(ctx);vTaskDelete(NULL);;}//6. 循环等待/处理客户端的数据请求for (;;) {do {rc = modbus_receive(ctx, query); //6.1 循环等待数据请求} while (rc == 0);/* 6.2 当发生错误时,返回错误响应包(含错误代码) */if (rc == -1 && errno != EMBBADCRC) {/* Quit */continue; //对错误做出处理后退出,为方便讲解此处简单忽略}/* 6.3 正常返回响应包(含请求数据) */rc = modbus_reply(ctx, query, rc, mb_mapping);if (rc == -1) {//对错误做出处理后退出break; }/* 6.4 对接收到的线圈数据进行硬件响应 */if (mb_mapping->tab_bits[0])HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);elseHAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);}//7. 释放动态分配的映射内存modbus_mapping_free(mb_mapping);//8. 释放动态申请的数据请求包存储单元vPortFree(query);//9.关闭RTU连接modbus_close(ctx);//10.释放动态分配的rtu操作句柄modbus_free(ctx);//11.任务结束,删除自身vTaskDelete(NULL);
}
-
modbus映射单元结构体定义如下:
//位于modbus.h文件中 typedef struct _modbus_mapping_t {int nb_bits;int start_bits;int nb_input_bits;int start_input_bits;int nb_input_registers;int start_input_registers;int nb_registers;int start_registers;uint8_t *tab_bits; //线圈 数组首地址uint8_t *tab_input_bits; //离散输入 数组首地址uint16_t *tab_input_registers; //输入寄存器 数组首地址uint16_t *tab_registers; //保持寄存器 数组首地址 } modbus_mapping_t;
libmodbus在主机(客户端)上的应用编程
- 以采用USB转串口方式进行通信的RTU模式为例进行讲解,并假设读取的从机具有至少2个保持寄存器,现在编程实现读从机的保持寄存器1,将其值加1后写到保持寄存器2中。
static void LibmodbusClientTask( void *pvParameters )
{modbus_t *ctx;int rc;uint16_t val;int nb = 1;ctx = modbus_new_st_rtu("usb", 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 (;;) {/* 读保持寄存器1 */rc = modbus_read_registers(ctx, 1, nb, &val);if (rc != nb)continue;/* display on lcd */Draw_Number(0, 0, val, 0xff0000);/* val ++ */val++;/* 写保持寄存器2 */rc = modbus_write_registers(ctx, 2, nb, &val);}/* For RTU */modbus_close(ctx);modbus_free(ctx);vTaskDelete(NULL);
}