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