一文講清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
英文地址,請注明出處:http://en.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 服…