一文讲清libmodbus在STM32上基于FreeRTOS的移植

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 RTU协议为例,主设备、从设备初始化后便可开始进行通信:

image-20250701111633187

软件架构层次
  • 从数据的收发过程, 可以把使用 libmodbus 的源码分为 3 层:

    • ① Modbus APP 应用层:它需要知道要做什么,即主设备要读/写哪个设备的哪些寄存,从设备需要提供/接收什么样的数据
    • ② Modbus 核心层: 向APP层提供接口函数, 向下调用底层代码“构造、发送、接收、解析” 数据包
    • ③ Modbus 底层 : 针对不同硬件(串口、网络等)提供具体的数据封包、收发和解包服务
  • APP应用层

    • libmodbus-3.1.10 中数据收发核心接口函数及其应用:

image-20250701145310284

  • 核心层

    • modbus.c文件:实现了应用层使用的各类Modbus函数

    • modbus-private.h:抽象出了的主要数据结构,如struct _modbusstruct _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:回应广播包
  • 底层

    • 根据具体硬件,实例化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种错误恢复模式可选(单选或复选)
    1. MODBUS_ERROR_RECOVERY_NONE :应用程序自身处理错误 (默认选项)
    2. MODBUS_ERROR_RECOVERY_LINK :经过一段延时,libmodbus 内部自动尝试进行断开/连接
    3. 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各参数进行了设置和连接。
modbus_close()
void modbus_close (modbus_t * ctx);
  • 函数功能:在应用程序结束之前,一定记得调用此函数关闭Modbus 连接。
    • 在 RTU 模式下,实质是调用函数 _modbus_rtu_close(modbus_t * ctx) 关闭串口句柄;
    • 在 TCP 模式下 , 实质是调用函数_modbus_tcp_close(modbust * ctx) 关闭Socket 句柄 。
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,成功则返回读取的寄存器个数。

  • 函数内部调用关系如下图所示

    image-20250701181049634

  • 示例

    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() 。

  • 此函数的调用依赖关系如下图

    image-20250701181517113

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等形式的字符串来表示
      • baud: 表示串口波特率的设置值,例如:9600、 19200、 57600、 115200等
      • parity :表示奇偶校验位,取值有:‘N’:无奇偶校验; ‘E’:偶校验; ‘O’:奇校验。
      • data_bit :表示数据位的长度,取值范围为 5、 6、 7和8
      • stop_bit :表示停止位长度,取值范围为1或2
  • 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 表示远端设备的端口号。
  • modbus_tcp_liste()

    int modbus_tcp_listen (modbus_t * ctx, int nb_connection);
    
    • 此函数创建并监听一个 TCP/IPv4 上的套接字。参数 nb_connection 代表最大的监听数量,在调用此函数之前,必须首先调用modbus_new_tcp()创建modbus_t结构体。
  • 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
    image-20250701215905926

1. 新建后端文件

  • 我们是基于STM32开发板进行开发,且通信方式采用板载USB转串口进行通信,故以modbus-rtu.c为模板,创建modbus-st-rtu.c文件

  • 首先,复制modbus-rtu.cmodbus-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.htask.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);
}

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

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

相关文章

go语言安装达梦数据完整教程

一、安装 go-dm 驱动 1. 使用 go get 命令安装 # 打开PowerShell或命令提示符 go get github.com/dmdbms/go-dm# 若网络问题&#xff0c;配置代理 go env -w GOPROXYhttps://goproxy.cn,direct2. 验证驱动安装 go list -m github.com/dmdbms/go-dm# 预期输出类似 github.com…

华为云Flexus+DeepSeek征文|基于Dify构建音视频内容转录工作流

华为云FlexusDeepSeek征文&#xff5c;基于Dify构建音视频内容转录工作流 一、构建音视频内容转录工作流前言二、构建音视频内容转录工作流环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建音视频内容转录工作流实战3.1 配置Dify环境3.2 配置Dify工具…

Pandas6(数据清洗2)——置换和随机采样、get_dummies、扩展数据类型、字符串处理函数

数据清洗2 一、置换和随机采样&#xff08;permutation,sample&#xff09; 随机置换&#xff08;打乱排序&#xff08;洗牌&#xff09;&#xff09;函数&#xff1a;numpy.random.permutation &#xff0c;可以对一个序列进行随机排序&#xff0c;常用于数据集随机划分等场景…

按月设置索引名的完整指南:Elasticsearch日期索引实践

按月设置索引名的完整指南:Elasticsearch日期索引实践 在时序数据场景中,按月设置索引名(如logs-2024-01)是优化查询效率、降低管理成本的关键策略。以下是三种实现方案及详细步骤: 方案一:索引模板 + 日期数学表达式(推荐) 原理:利用ES内置的日期数学表达式动态生成…

西南交通大学【机器学习实验7】

实验目的 理解和掌握朴素贝叶斯基本原理和方法&#xff0c;理解极大似然估计方法&#xff0c;理解先验概率分布和后验概率分布等概念&#xff0c;掌握朴素贝叶斯分类器训练方法。 实验要求 给定数据集&#xff0c;编程实现朴素贝叶斯分类算法&#xff0c;计算相应先验概率&a…

java生成pdf文件

1.依赖 <dependency><groupId>com.itextpdf</groupId><artifactId>itext-core</artifactId><version>8.0.4</version><type>pom</type></dependency> 2.代码 package org.example;import com.itextpdf.io.image…

macOS挂载iOS应用沙盒文件夹

背景 工具 libimobiledevice: linux&#xff0c;macOS等与ios设备通信是的工具 macFUSE 是 macOS 文件系统扩展的“引擎”&#xff0c;支持开发者创建各类虚拟文件系统。 iFUSE 是专为 iOS 设备设计的“连接器”&#xff0c;需依赖 macFUSE 实现功能。 若需访问 iPhone/iP…

嵌入式软件面经(四)Q:请说明在 ILP32、LP64 与 LLP64 三种数据模型下,常见基本类型及指针的 sizeof 值差异,并简要解释其原因

从事嵌入式开发深入理解 ILP32、LP64、LLP64 三种主流数据模型及其在平台上的实际表现&#xff0c;可以帮助我们避免诸如类型越界、结构错位、指针截断等致命错误。 一、何为数据模型&#xff1f;为何重要&#xff1f; 数据模型&#xff08;Data Model&#xff09;是指在某一编…

计算机组成原理与体系结构-实验二 ALU(Proteus 8.15)

目录 一、实验目的 二、实验内容 三、实验器件 四、实验原理 五、实验步骤 六、思考题 一、实验目的 1、了解算术逻辑运算器&#xff08;74LS181&#xff09;的组成和功能。 2、掌握基本算术和逻辑运算的实现方法。 二、实验内容 设计算数逻辑运算器系统的通路&#x…

ubuntu下免sudo执行docker

前言 在ubuntu中&#xff0c;默认是无法使用root账号的&#xff0c;安装完docker后&#xff0c;不可避免的要使用sudo来执行docker命令&#xff0c;这就让运维变得很麻烦 避免sudo # 添加当前用户到 docker 组 sudo usermod -aG docker $USER# 刷新组权限 newgrp docker# 验…

微处理原理与应用篇---STM32寄存器控制GPIO

在 ARM 架构下使用 C 语言控制 32 位寄存器实现 GPIO 操作&#xff0c;需结合芯片手册进行寄存器映射和位操作。以下以 STM32F103&#xff08;Cortex-M3 内核&#xff09;为例&#xff0c;详细介绍实现方法&#xff1a; 一、STM32F103 GPIO 控制&#xff08;标准外设库&#x…

基于OPUS-MT模型的中译英程序实现

这是我的kaggle账号名“fuliuqin” 代码参考如下&#xff1a; nlp.paperflq | KaggleExplore and run machine learning code with Kaggle Notebooks | Using data from [Private Datasource]https://www.kaggle.com/code/fuliuqin/nlp-paperflq 目录 绪论 研究背景与意义 研究…

炸鸡派-定时器基础例程

定时器简介 基本定时器&#xff0c;计数中断、产生DMA请求。 通用定时器&#xff0c;PWM输出、输入捕获、脉冲计数。 高级定时器&#xff0c;输出比较、互补输出带死区控制、PWM输入。 中心对齐的计数模式可以生成对称的PWM波形信号。计数可以先增后减。 这种模式下&#xff…

利用不坑盒子的Copilot,快速排值班表

马上放暑假了&#xff0c;有多少人拼命排值班表的&#xff1f; 今天用我亲身制作值班表的一些Excel操作&#xff0c;给大家分享一些在Excel中的小技巧&#xff0c;需要的及时收藏&#xff0c;有一天用得上~ 值班表全貌 先给大家看看我制作的值班表的样子&#xff0c;应该大家…

Linux 面试知识(附常见命令)

目录结构与重要文件 Linux 中一切皆文件&#xff0c;掌握目录结构有助于理解系统管理与配置。 目录说明/根目录&#xff0c;所有文件起点/bin基本命令的可执行文件&#xff0c;如 ls, cp/sbin系统管理员用的命令&#xff0c;如 shutdown/etc配置文件目录&#xff0c;如 /etc/…

Lua 安装使用教程

一、Lua 简介 Lua 是一门轻量级、高性能的脚本语言&#xff0c;具有简洁语法、嵌入性强、可扩展性高等特点。广泛应用于游戏开发&#xff08;如 Roblox、World of Warcraft&#xff09;、嵌入式开发、配置脚本、Nginx 扩展&#xff08;OpenResty&#xff09;等领域。 二、Lua …

SPAD像素概念理解

SPAD(Single Photon Avalanche Diode,单光子雪崩二极管)像素是一种能够检测单个光子的超灵敏光电探测器,其核心原理是通过雪崩倍增效应将单个光子产生的微弱电流信号放大到可观测水平。 一、工作原理 雪崩倍增效应 当SPAD反向偏压超过其击穿电压时,进入盖革模式(Geiger M…

SSSSS

#include <iostream> void LineOf(bool** n1, bool** n2, int column, int raw, int* result) { for (int i 0; i < column; i) { int d -1, n -1; // 反向遍历&#xff0c;找最后一个 true for (int j raw - 1; j > 0; j--) { …

【AI智能体】社交娱乐-智能助教

智能助教是扣子官方提供的教育类智能体模板。助教模板分为学习陪伴和作业批改两种场景&#xff0c;分别适用于学生角色和教师角色&#xff0c;你可以根据需求选择对应的模板&#xff0c;并将其改造为其他学科或其他教育阶段的智能助教。 模板介绍 在智能学伴/助教的落地过程中…

自动化保护 AWS ECS Fargate 服务:使用 Prisma Cloud 实现容器安全

引言 在云原生时代,容器化技术已成为现代应用部署的标准方式。AWS ECS Fargate 作为一种无服务器容器服务,让开发者能够轻松运行容器化应用而无需管理底层基础设施。然而,随着容器技术的普及,安全问题也日益突出。本文将介绍如何通过 Python 脚本自动化地为 ECS Fargate 服…