第6章 libmodbus使用
6.1 libmodbus開發庫
6.1.1 功能概要
libmodbus是一個免費的跨平臺支持RTU和TCP的Modbus庫,遵循LGPL V2.1+協議。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系統。libmodbus可以向符合Modbus協議的設備發送和接收數據,并支持通過串口或者TCP網絡進行連接。
作為一個開源項目,libmodbus庫還處于開發測試階段,代碼量還不十分龐大,文檔和注釋也不夠全面,本章通過對libmodbus源代碼的閱讀過程,一方面可以進一步理解Modbus協議,同時也可以學習一個好的開源項目的代碼組織及開發過程。 libmodbus的官方網站為 http://libmodbus.org/, 可以從 http://libmodbus.org/download/ 下載源代碼。作為開源軟件,還可以從GitHub網站獲取最新版本的代碼GitHub: https://github.com/stephane/libmodbus.git
6.1.2 源碼獲取
libmodbus的源碼不斷更新,本教程選擇版本v3.1.10。打開https://github.com/stephane/libmodbus/tags ,如下圖下載:
本源碼也放在網盤中如下目錄里:
解壓后,簡單查看源代碼根目錄的構成:
- doc目錄: libmodbus庫的各API接口說明文檔。
- m4目錄: 存放GNU m4文件,在這里對理解代碼沒有意義,可忽略。
- src目錄: 全部libmodbus源文件。
- tests目錄: 包含自帶的測試代碼 其他文件對理解源代碼關系不大,可以暫時忽略
圖6-2解壓libmodbus源代碼:
進一步展開src代碼目錄,如圖6-3所示:
圖6-3libmodbus源碼構成:
各文件作用如下:
- win32: 定義在Windows下使用Visual Studio編譯時的項目文件和工程文件以及相關配置選項等。其中,modbus-9.sln默認使用Visual Studio 2008。
- Makefile.am: Makefile.am是Linux下AutoTool編譯時讀取相關編譯參數的配置文件,用于生成Makefile文件,因為用于Linux下開發,所以在這里暫時忽略
- modbus.c: 核心文件,實現Modbus協議層,定義共通的Modbus消息發送和接收函數各功能碼對應的函數。
- modbus.h: libmodbus對外暴露的接口API頭文件。
- modbus-data.c: 數據處理的共通函數,包括大小端相關的字節、位交換等函數
- modbus-private.h: libmodbus內部使用的數據結構和函數定義。
- modbus-rtu.c: 通信層實現,RTU模式相關的函數定義,主要是串口的設置、連接及消息的發送和接收等。
- modbus-rtu.h: RTU模式對外提供的各API定義
- modbus-rtu-private.h: RTU模式的私有定義。
- modbus-tcp.c: 通信層實現,TCP模式下相關的函數定義,主要包括TCP/IP網絡的設置連接、消息的發送和接收等。
- modbus-tcp.h: 定義TCP模式對外提供的各API定義
- modbus-tcp-private.h: TCP模式的私有定義。
- modbus-version.h.in: 版本定義文件。
6.1.3 源碼閱讀
對比比較復雜的源碼,使用sourceinsight可以很方便地閱讀、分析、編輯源碼。
1. 新建工程
運行source Insight,點擊菜單“Project->New Project”,如下圖所示:
設置工程名及工程數據目錄:在彈出的New Project對話框中設置“New project name”(項目的名稱),然后設置Where do you want to store the project data file? (項目文件保存位置),點擊Browse按鈕選擇源碼的目錄即可,如下圖:
指定源碼目錄:在上圖界面中點擊OK后,彈出如下圖所示窗口,填入源碼路徑:
添加源碼:在新彈出的對話框中,點擊“Add”或“Add All”。“Add”是手動選擇需要添加的文件,而“Add All”是添加所有文件。我們使用“Add All”,在彈出的提示框中選中“Recursively add lower sub-directories”(遞歸添加下級的子目錄)并點擊OK。同樣的Remove File,Remove All是移除單個文件或者移除所有文件,如下圖所示:
添加文件完成后會彈出下面窗口,點擊“確定”即可:
此時界面會返回到主界面,如下圖所示,點擊“Close”:
2. 同步文件
同步文件的意思是讓Source Insight去解析源碼,生成數據庫,這樣有助于以后閱讀源碼。比如點擊某個函數時就可以飛快地跳到它定義的地方。
先點擊菜單“Project->Synchronize Files”,如圖 2.23所示:
在彈出的對話框中 選中“Force all files to be re-parsed”(強制解析所有文件),并點擊“Start”按鈕開始同步,如下圖所示:
3.打開工程
前面建議工程后,就會自動打開了工程。如果下次你想打開工程,啟動Souce Insight后,點擊菜單“Project -> Open Porject”就可以在一個列表中選擇以前建立的工程,如下圖所示:
4. 操作示例
在工程中打開文件:點擊"P"圖標打開文件列表,雙擊文件打開文件,也可以輸入文件名查找文件,如下圖所示:
在文件中查看函數或變量的定義:打開文件后,按住ctrl鍵的同時,用鼠標點擊函數、變量,就會跳到定義它的位置,如下圖所示:
查找函數或變量的引用:右鍵點擊函數或變量,彈出對話框選擇“Lookup Reference”;或者雙擊函數后,使用快捷鍵"ctrl+/"來查找引用,如下圖:
5. 快捷鍵
快捷鍵 | 說明 |
---|---|
Alt + , | 后退 |
Alt + . | 前進 |
F8 | 高亮選中的字符 |
Ctrl+F | 查找 |
F3或Shift+F3 | 往前查找 |
F4或Shift+F4 | 往后查找 |
6.1.4 libmodbus與應用程序的關系
libmodbus是一個免費的跨平臺支持RTU和TCP的Modbus開發庫,借助于libmodbus發庫能夠非常方便地建立自己的應用程序或者將Modbus通信協議嵌入單體設備libmodbus開發庫與應用程序的基本關系如圖6-4所示。
圖6-4應用程序與libmodbus的關系:
在對libmodbus的接口及代碼框架簡單了解之后,不妨再深入細節一探究竟,看看libmodbus都實現了哪些基礎功能,以及源代碼中對Modbus各功能碼和消息頓是如何包裝的。具體內容請參看下一章。
6.2 libmodbus源代碼解析
libmodbus作為一個優秀且免費開源的跨平臺支持RTU和TCP模式的Modbus開發庫,非常值得大家借鑒和學習。本章對libmodbus源代碼進行閱讀和分析。
6.2.1 核心函數
以Modbus RTU協議為例,主設備、從設備初始化后:
- 主設備就可以啟動請求,即“發送消息”給從設備
- 從設備接收到請求后構造數據,啟動響應即“發送回復”
- 主機收到響應后,會“檢查響應”
如下圖所示:
分析“libmodbus-3.1.10testsunit-test-client.c”、“libmodbus-3.1.10testsunit-test-server.c”,可以得到下面核心函數的使用過程:
6.2.2 框架分析與數據結構
站在APP開發的角度來說,使用上一節里介紹的libmodbus函數即可。但是,數據的傳輸必定涉及到底層數據傳輸。所以,從數據的收發過程,可以把使用libmodbus的源碼分為3層:
- APP:它知道要做什么,主設備要讀寫哪些寄存,從設備提供、接收什么數據
- Modbus核心層:向上提供接口函數,向下調用底層代碼構造數據包并發送、接收數據包并解析
- 后端(數據傳輸):進行硬件相關的數據封包與發送、接收與解包
對于核心層、后端,抽象出了如下結構體:
核心層modbus_t結構體的成員含義如下:
成員 | 含義 |
---|---|
int slave; | 從站設備地址 |
int s; | RTU下是串口句柄,TCP下是Socket |
int debug; | 是否啟動Debug模式(打印調試信息) |
int error_recovery; | 錯誤恢復模式:MODBUS_ERROR_RECOVERY_NONE:由APP處理錯誤MODBUS_ERROR_RECOVERY_LINK:如果有連接錯誤,則重連MODBUS_ERROR_RECOVERY_PROTOCOL:如果數據不符合協議要求,則清空所有數據 |
int quirks; | 一些奇怪的功能,比如:MODBUS_QUIRK_MAX_SLAVE:從站地址最大值可以到達255MODBUS_QUIRK_REPLY_TO_BROADCAST:回應廣播包 |
struct timeval response_timeout; | 等待回應的超時時間,默認是0.5S |
struct timeval byte_timeout; | 接收一個字節的超時時間,默認是0.5S |
struct timeval indication_timeout; | 等待請求的超時時間 |
const modbus_backend_t *backend; | 硬件傳輸層的結構體 |
void *backend_data; | 硬件傳輸層的私有數據 |
后端modbus_backend_t結構體的成員含義如下:
成員 | 含義 |
---|---|
unsigned int backend_type; | 后端類型,是RTU還是TCP |
unsigned int header_length; | 頭部長度,比如RTU數據包前面需要有1字節的設備地址,頭部長度就是1 |
unsigned int checksum_length; | 校驗碼長度,RTU的校驗碼是2字節 |
unsigned int max_adu_length; | ADU(數據包)最大長度 |
set_slave | 設置從站地址 |
build_request_basis | 設置RTU請求包的基本數據,這些數據的格式是一樣的,比如req[0]是從設備地址,req[1]是功能碼,req[2]和req[3]是寄存器地址,req[4]和req[5]是寄存器數量 |
build_response_basis | 設置RTU回應包的基本數據,這些數據的格式是一樣的,比如req[0]是從設備地址,req[1]是功能碼 |
prepare_response_tid | 生產傳輸標識TID,在TCP中使用 |
send_msg_pre | 發送消息前的準備工作,對于RTU是填充CRC檢驗碼,對于TCP是填充頭部的Length |
send | 發送數據包 |
receive | 接收數據包 |
recv | 接收原始數據,receive會調用recv得到原始數據然后解析出數據包 |
check_integrity | 檢查數據包的完整性 |
pre_check_confirmation | 檢查響應數據包是否有效時,先執行pre_check_confirmation做一些簡單的檢查 |
connect | 硬件相關的連接,對于RTU就是打開串口、設置串口波特率等;對于TCP則是連接對端 |
is_connected | 判斷是否已經連接 |
close | 關閉連接 |
flush | 清空接收到的、未處理的數據 |
select | 阻塞一段時間以等待數據 |
free | 釋放分配的modbus_t等結構體 |
6.2.3 情景分析
以“modbus_write_bits”函數為例,分析下圖的執行流程:
1. 初始化
2. 主設備發送請求
3. 從設備接收請求
4.從設備回應
6.2.4 常用接口函數
下面分析 libmodbus開發庫提供的所有接口API函數。其主要對象文括 modbus.h 和 modbus.c ,接口函數大致可分為3類,以下分別進行介紹。
1. 各類輔助接口函數
MODBUS_API int modbus_set_slave(modbus t * ctx,int slave)
此函數的功能是設置從站地址,但是由于傳輸方式不同而意義稍有不同。
- RTU模式 :
如果 libmodbus應用于 主站設備端,則相當于定義 遠端設備ID ;如果libmodbus應用于從站設備端 ,則相當于定義 自身設備 ID ;在 RTU 模式下參數 slave 取值范圍為 0~247 ,其中 0(MODBUS_BROADCAST_ADDRESS) 為廣播地址。
- TCP模式:
通常,TCP 模式下此函數不需要使用。在某些特殊場合,例如串行 Modbus設備轉換為 TCP模式傳輸的情況下,此函數才被使用。此種情況下,參數 slave取值范圍為 0~247 ,0 為廣播地址;如果不進行設置,則 TCP 模式下采用默認值 MODBUS TCP SLAVE(OXFF) 。
下面的代碼以 RTU模式、主設備(MASTER)端為例:
modbus_t * ctx;
ctx=modbus_new_rtu("COM4",115200,'N',8,1);
if (ctx ==NULL)
{
fprintf(stderr,"Unable to create the libmodbus contextn");
return -1;
}
rc =modbus_set_slave(ctx,YOUR DEVICE ID);
if (rc==-1)
{
fprintf(stderr,"Invalid slave IDn");
modbus free(ctx);
return -1;
}
if (modbus connect(ctx)==-1)
{
fprintf(stderr,"Connection failed:sn",modbus strerror(errno));
modbus free(ctx);
return -1;
}
MODBUS_APIintmodbus_set_error_recovery(modbus_t*ctx,modbus_error_recovery_mode error_recovery):
此函數用于在連接失敗或者傳輸異常的情況下,設置錯誤恢復模式。有 3種錯誤恢復模式可選。
typedef enum
{
MODBUS_ERROR_RECOVERY_NONE =0, //不恢復
MODBUS_ERROR_RECOVERY_LINK =(1< 1), //鏈路層恢復
MODBUS_ERROR_RECOVERY_PROTOCOL =(1< 2) //協議層恢復
}modbus error recovery mode;
默認情況下,設置為 MODBUS_ERROR_RECOVERY_NONE ,由應用程序自身處理錯誤;若設置為 MODBUS_ERROR_RECOVERY_LINK ,則經過一段延時 libmodbus 內部自動嘗試進行斷開/連接;若設置為 MODBUS_ERROR_RECOVERY_PROTOCOL ,則在傳輸數據 CRC 錯誤或功能碼錯誤的情況下,傳輸會進入延時狀態,同時數據直接被清除。在 SLAVE/SERVER 端,不推薦使用此函數。
基本用法舉例:
modbus_set_error_recovery(ctx,MODBUS_ERROR_RECOVERY_LINK|MODBUS_ERROR_RECOVERY_PROTOCOL);
MODBUS_API int modbus_set_socket(modbus t * ctx,int s)
此函數設置當前 SOCKET 或串口句柄要用于多客戶端連接到單一服務器的場合。簡單用法舉例如下,后續介紹函數 modbus_tcp_listen() 時將會進一步介紹相關用法。
#define NB_CONNECTION 5
modbus_t * ctx;
ctx=modbus_new_tcp("127.0.0.1", 1502)
server_socket = modbus_tcp_listen(ctx,NB_CONNECTION);
FD_ZERO(&rdset);
FD_SET(server_socket,&rdset);
/* ... */
if (FD_ISSET(master_socket,&rdset))
{
modbus_set_socket(ctx,master_socket);
rc =modbus_receive(ctx,query);
if(rc!=-1)
{
modbus_reply(ctx,query, rc,mb_mapping);
}
}
MODBUS_API int modbus_get_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
MODBUS_API int modbus_set_response_timeout (modbus_t * ctx, uint32_t * to_sec, uint32_t * to_usec);
用于獲取或設置響應超時,注意時間單位分別是秒和微秒。
MODBUS_API int modbus_get_byte_timeout (modbus_t * ctx, uint32_t * to_sec,uint32_t * to_usec);
MODBUS_API int modbus_set_byte_timeout (modbus_t * ctx, uint32_t * to_sec,uint32_t * to_usec);
用于獲取或設置連續字節之間的超時時間,注意時間單位分別是秒和微秒。
MODBUS_API intmodbus_get_header_length (modbus_t * ctx);
獲取報文頭長度。
MODBUS_API int modbus_connect (modbus_t * ctx);
此函數用于主站設備與從站設備建立連接。
在 RTU 模式下,它實質調用了文件 modbus_rtu.c 中的函數 static int modbus_rtu_connect (modbus_t * ctx) ;在此函數中進行了串口波特率校驗位、數據位、停止位等的設置。
在 TCP 模式下,modbus_connect() 調用了文件 modbus_tcp.c 中的函數 static int_modbus_tcp_connect (modbus_t * ctx ) ;在函數 _modbus_tcp_connect() 中,對 TCP/IP 各參數進行了設置和連接。
MODBUS_API void modbus_close (modbus_t * ctx);
關閉 Modbus 連接。在應用程序結束之前,一定記得調用此函數關閉連接在 RTU 模式下,實質是調用函數 _modbus_rtu_close(modbus_t * ctx) 關閉串口句柄;在 TCP 模式下,實質是調用函數 _modbus_tcp_close(modbust * ctx) 關閉 Socket 句柄。
MODBUS_API void modbus_free (modbus_t * ctx);
釋放結構體 modbus_t 占用的內存。在應用程序結束之前,一定記得調用此函數
MODBUS_API int modbus_set_debug (modbust * ctx, int flag);
此函數用于是否設置為DEBUG模式。
若參數 flag 設置為TRUE,則進入 DEBUG模式。若設置為FALSE,則切換為非 DEBUG模式。在 DEBUG模式下所有通信數據將按十六進制方式顯示在屏幕上,以方便調試。
MODBUS_API const char * modbus_strerror (int errnum);
此函數用于獲取當前錯誤字符串。
2.各類Modbus功能接口函數
MODBUS_API int modbus_read_bits (modbus t * ctx, int addr, int nb, uint8_t * dest);
此函數對應于功能碼 01(0x01) 讀取線圈/離散量輸出狀態(Read Coil Status/DOs),其中,所讀取的值存放于參數 uint8_t * dest 指向的數組空間因此 dest 指向的空間必須足夠大,其大小至少為 nb * sizeof(uint8_t) 個字節。
用法舉例:
#define SERVER ID 1
#define ADDRESS START 0
#define ADDRESS END 99
modbus_t * ctx;
uint8_t * tab_rp_bits;
int rc;
int nb;
ctx=modbus_new_tcp("127.0.0.1",502);
modbus_set_debug(ctx,TRUE);
if (modbus_connect(ctx)==-1)
{
fprintf(stderr,"Connection failed:%sn", modbus_strerror(errno));
modbus free(ctx);
return -1;
}
//申請存儲空間并初始化
int nb = ADDRESS_END - ADDRESS_START;
tab_rp_bits = (uint8_t * ) malloc (nb * sizeof(uint8_t));
memset(tab_rp_bits, 0, nb * sizeof(uint8_t));
//讀取一個線圈
int addr =1;
rc =modbus_read_bits(ctx,addr,1,tab_rp_bits);
if (rc !=1)
{
printf("ERROR modbus_read_bits_single (%d)n", rc);
printf("address =%dn", addr);
}
//讀取多個線圈
rc =modbus_read_bits(ctx,addr,nb,tab_rp_bits);
if (rc !=nb)
{
printf("ERROR modbus_read_bitsn");
printf("Address =%d,nb =%dn", addr, nb);
}
//釋放空間關閉連接
free(tab_rp_bits);
modbus_close(ctx);
modbus_free(ctx);
MODBUS_API int modbus_read_input_bits (modbus_t * ctx, int addr, int nb,uint8_t * dest);
此函數對應于功能碼 02(0x02) 讀取離散量輸入值(Read Input Status/DIs),各參數的意義與用法,類似于函數 modbus_read_bits() 。
MODBUS_API int modbus_read_registers (modbus_t * ctx, int addr, int nb,uint16_t * dest);
此函數對應于功能碼 03(0x03) 讀取保持寄存器(Read Holding Register),其中,所讀取的值存放于參數 uint16_t * dest 指向的數組空間因此 dest 指向的空間必須足夠大,其大小至少為 nb * sizeof(uint16_t) 個字節。
當讀取成功后,返回值為讀取的寄存器個數;若讀取失敗,則返回-1。此函數調用依賴關系如下圖6-5所示。
用法舉例:
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:%sn", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
//從地址0開始連續讀取10個
rc =modbus_read_registers(ctx,0,10,tab_reg);
if (rc ==-1)
{
fprintf(stderr,"%sn",modbus_strerror(errno));
return -1;
}
for (i=0;i< rc;i++)
{
printf("reg[%d]=%d(0x%X)n",i,tab_reg[i],tab_reg[i]);
}
modbus_close(ctx);
modbus_free(ctx);
MODBUS_API int modbus_read_input_registers (modbus_t * ctx,int addr, int nb, uint16_t * dest );
此函數對應于功能碼 04(0x04) 讀取輸人寄存器(Read Iput Register),各參數的意義與用法,類似于函數 modbus_read_registers() 。
此函數的調用依賴關系如下圖 6-6 所示。
圖6-6函數 modbus_read input_registers()的調用依賴關系 :
MODBUS_API int modbus_write_bit (modbus_t * ctx, int coil_addr, int status):
該函數對應于功能碼 05(0x05) 寫單個線圈或單個離散輸出(Force SingleCoil)。其中參數 coil_addr 代表線圈地址;參數 status 代表寫值取值只能是TRUE(1)或 FALSE(0) 。
MODBUS_API int modbus_write_register (modbus_t * ctx,int reg_addr, int value):
該函數對應于功能碼 06(0x06) 寫單個保持寄存器(Preset Single Register)。
MODBUS_API int modbus_write_bits (modbus_t * ctx, int addr, int nb, const uint8_t * data):
該函數對應于功能碼 15(0x0F) 寫多個線圈(Force Multiple Coils)
參數 addr 代表寄存器起始地址,參數 nb 表示線圈個數,而參數 const uint8_t * data 表示待寫入的數據塊。一般情況下,可以使用數組存儲寫入數據,數組的各元素取值范圍只能是 TRUE(1)或 FALSE(0) 。
MODBUS_API int modbus_write_registers (modbus_t * ctx, int addr, int nb, const uint16_t * data):
該函數對應于功能碼 16(0x10) 寫多個保持存器(Preset MultipleRegisters)
參數 addr 代表寄存器起始地址,參數 nb 表示存器的個數而參數 const uint16_t * data 表示待寫人的數據塊。一般情況下,可以使用數組存儲寫入數據數組的各元素取值范圍是 0~0xFFFF 即數據類型 uint16_t 的取值范圍。
MODBUS_API int modbus_mask_registers (modbus_t * ctx, int addr, uint16_t and_mask, uint16_t or_mask ):
modbus_mask_write_register() 函數應使用以下算法修改遠程設備地址“addr”處的保持寄存器的值:
新值 = (current value AND ‘and’) OR (‘or’ AND (NOT ‘and’)) 。
該功能使用 Modbus 功能代碼 0x16(掩碼單個寄存器)。
MODBUS_API int modbus_write_and_read_registers (mobus_t * ctx ,
int writer_addr,
int writer_nb,
const uint16_t * src,
int read_addr,
int read_nb,
uint16_t * dest);
modbus_write_and_read_registers() 函數應將 write_nb 保持寄存器的內容從數組 “src” 寫入遠程設備的地址 write_addr ,然后將 read_nb 保持寄存器的內容讀取到遠程設備的地址 read_addr 。讀取結果作為字值(16 位)存儲在 dest 數組中。
必須注意分配足夠的內存來存儲結果 dest (至少 nb * sizeof(uint16_t))。該功能使用 Modbus 功能代碼 0x17(寫/讀寄存器)。
MODBUS_API int modbus_report_slave_id (modbus_t * ctx, int max_dest, uint8_t * dest):
該函數對應于功能碼 17(0x11) 報告從站ID。參數 max_dest 代表最大的存儲空間,參數 dest 用于存儲返回數據。返回數據可以包括如下內容:從站 ID狀態值(0x00= OFF狀態, 0xFF=ON狀態) 以及其他附加信息,具體的各參數意義由開發者指定。
用法舉例:
uint8_t tab_bytes[MODBUS_MAX_PDU_LENGTH];
...
rc =modbus_report_slave_id(ctx, MODBUS_MAX_PDU_LENGTH, tab_bytes);
if (rc >1)
{
printf("Run Status Indicator: %sn",tab_bytes[1] ?"ON":"OFF");
}
3. 數據處理的相關函數或宏定義
在libmodbus開發庫中,為了方便數據處理在 modbus.h 文件中定義了一系列數據處理宏。
例如獲取數據的高低字節序宏定義:
#define MODBUS_GET_HIGH_BYTE (data) (((data) > >8) & 0xFF)
#define MODBUS_GET_LOW_BYTE (data) ((data) & 0xFF)
對于浮點數等多字節數據而言,由于存在字節序與大小端處理等的問題,所以輔助定義了一些特殊函數:
MODBUS_API float modbus_get_float (const uint16_t * src);
MODBUS_API float modbus_get_float_abcd (const uint16_t * src);
MODBUS_API float modbus_get_float_dcba (const uint16_t * src);
MODBUS_API float modbus_get_float_badc (const uint16_t * src);
MODBUS_API float modbus_get_float_cdab (const uint16_t * src);
MODBUS_API void modbus_set_float (float f,uint16_t * dest);
MODBUS_API void modbus_set_float_abcd (float f,uint16_t * dest);
MODBUS_API void modbus_set_float_dcba (float f,uint16_t * dest);
MODBUS_API void modbus_set_float_badc (float f,uint16_t * dest);
MODBUS_API void modbus_set_float_cdab (float f,uint16_t * dest);
當然,可以參照 float 類型的處理方法,繼續定義其他多字節類型的數據例如int32_t、uint32_t、 int64_t、uint64_t 以及 double 類型的讀寫函數。
6.2.5 RTU/TCP關聯接口函數
在文件 modbus.h 的最后位置,有如下語句
#include "modbus-tcp.h"
#include "modbus-rtu.h"
可以發現,除了 modbus.h 包含的接口函數之外,modbus-rtu.h 和 modbus-tcp.h 也包含了必要的接口函數。
1. RTU模式關聯函數
MODBUS_API modbus_t * modbus_new_rtu (const char * device, int baud, char parity, int data_bit, int stop_bit):
此函數的功能是創建一個 RTU 類型的 modbus_t 結構體。參數 const char * device 代表串口字符串,在 Windows 操作系統下形態如 “COMx” ,有一點需要注意的是,對于串口1串口9來說,,傳遞 “COM1”“COM9” 可以 成功 ,但是如果操作對象為 COM10及以上端口 ,則會出現 錯誤。
產生這種奇怪現象的原因是:微軟預定義的標準設備中含有 “COM1” “COM9” 。所以,“COM1” “COM9” 作為文件名傳遞給函數時操作系統會自動地將之解析為相應的設備。但對于 COM10 及以上的串口,“COM10” 之類的文件名系統只視之為 一般意義上的文件,而非串行設備。為了增加對 COM10 及以上串行端口的支持,微軟規定,如果要訪問這樣的設備,應使用這樣的文件名(以COM10 為例):. COM10。
所以,使用時在代碼中可以如此定義:.
const char * device = “.COM10”;
在Linux操作系統下可以使用”/dev/ttySo”或”/dev/ttyUSB0”等形式的字符串來表示。而參數 int baud 表示串口波特率的設置值,例如:9600、19200、57600、115200等。
參數char parity 表示奇偶校驗位,取值范圍:
- ‘N’:無奇偶校驗;
- ‘E’:偶校驗;
- ‘O’:奇校驗。
參數 int data_bit 表示數據位的長度,取值范圍為 5、6、7和8。
參數int stop_bit 表示停止位長度,取值范圍為1或2。
用法舉例:
modbus t *ctx;
ctx=modbus_new_rtu("\.COM10",115200,'N',8,1);
if (ctx ==NULL)
{
fprintf(stderr,"Unable to create the libmodbus contextn");
return -1;
}
modbus_set_slave(ctx,SLAVE_DEVICE_ID);
if (modbus connect(ctx)==-1)
{
fprintf(stderr,"Connection failed:%sn",modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
MODBUS_API int modbus_rtu_set_serial_mode (modbus_t * ctx, int mode):
該函數用于設置串口為 MODBUS RTU RS232或MODBUSRTU_RS485模式,此函數只適用于 Linux 操作系統下。
MODBUS_API int modbus_rtu_set_rts (modbus_t * ctx, int mode)。
MODBUS_API int modbus_rtu_set_custom_rts (modbus_t * ctx, void ( * set_rts) (modbus_t * ctx, int on))。
MODBUS_API int modbus_rtu_set_rts_delay (modbus_t * ctx, int us)。
以上函數只適用于 Linux 操作系統下,RTS 即Request ToSend 的縮寫,具體的意義可通過網絡搜索,一般情況下,此類函數可忽略。
2. TCP模式關聯函數
*MODBUS_API modbus_t * modbus_new_tcp (const char ip_address, int port) 。
此函數的功能是創建一個TCP/IPv4 類型的modbus_t 結構體。
參數 const char * ip_address 為IP地址,port 表示遠端設備的端口號。
MODBUS_API int modbus_tcp_listen (modbus_t * ctx, int nb_connection)。
此函數創建并監聽一個 TCP/IPv4 上的套接字。
參數int nb_connection 代表最大的監聽數量,在調用此函數之前,必須首先調用modbus_new_tcp() 創建modbus_t結構體。
MODBUS_API int modbus_tcp_accept (modbus_t * ctx,int * s)。
此函數接收一個 TCP/IPv4 類型的連接請求,如果成功將進入數據接收狀態。
6.3 libmodbus移植與使用
6.3.1 移植方法
以串口為例,libmodbus支持了windows系統、Linux系統。如果要在Freertos或者裸機上使用libmodbus,需要移植libmodbus里操作硬件的代碼。
根據下圖的層次,要移植libmodbus的“后端”,就是構造自己的modbus_backend_t結構體:
后端modbus_backend_t結構體的成員含義如下:
成員 | 含義 |
---|---|
unsigned int backend_type; | 后端類型,是RTU還是TCP |
unsigned int header_length; | 頭部長度,比如RTU數據包前面需要有1字節的設備地址,頭部長度就是1 |
unsigned int checksum_length; | 校驗碼長度,RTU的校驗碼是2字節 |
unsigned int max_adu_length; | ADU(數據包)最大長度 |
set_slave | 設置從站地址 |
build_request_basis | 設置RTU請求包的基本數據,這些數據的格式是一樣的,比如req[0]是從設備地址,req[1]是功能碼,req[2]和req[3]是寄存器地址,req[4]和req[5]是寄存器數量 |
build_response_basis | 設置RTU回應包的基本數據,這些數據的格式是一樣的,比如req[0]是從設備地址,req[1]是功能碼 |
prepare_response_tid | 生產傳輸標識TID,在TCP中使用 |
send_msg_pre | 發送消息前的準備工作,對于RTU是填充CRC檢驗碼,對于TCP是填充頭部的Length |
send | 發送數據包 |
receive | 接收數據包 |
recv | 接收原始數據,receive會調用recv得到原始數據然后解析出數據包 |
check_integrity | 檢查數據包的完整性 |
pre_check_confirmation | 檢查響應數據包是否有效時,先執行pre_check_confirmation做一些簡單的檢查 |
connect | 硬件相關的連接,對于RTU就是打開串口、設置串口波特率等;對于TCP則是連接對端 |
is_connected | 判斷是否已經連接 |
close | 關閉連接 |
flush | 清空接收到的、未處理的數據 |
select | 阻塞一段時間以等待數據 |
free | 釋放分配的modbus_t等結構體 |
本節先寫出模板:
根據這個源碼:
改出:
6.3.2 使用USB串口作為后端
基于這2個程序:
第1步:合并上述2個源碼,并修改到能編譯成功(但是libmodbus里對USB串口的操作),結果放在如下目錄:
第2步,繼續修改上圖的代碼,實現USB串口作為后端,得到以下代碼:
USB串口的操作函數:
/* 發送數據 */
int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout);
/* 接收數據 */
int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout);
6.3.3 libmodbus從機實驗(USB串口)
本節源碼為:
參考“libmodbus-3.1.10testsunit-test-server.c”,把開發板當做從機,使用PC上Modbus Poll軟件讀寫開發板:控制LED。
要點:
① printf、fprintf、vfprintf都不能使用,改成空的宏
6.3.4 libmodbus主機實驗(USB串口)
本節源碼為:
參考“libmodbus-3.1.10testsunit-test-client.c”,把開發板當做主機,去讀寫PC上Modbus Slave軟件模擬的從機。
6.3.5 使用板載串口作為后端
學習本節課程前,先觀看《3.5.3 面向對象封裝UART》,并且觀看對應視頻《3-9-1_面向對象封裝UART_完善收發功能》、《3-9-2_面向對象封裝UART_實現結構體》。
本節代碼如下:
按照下圖連線:調試、供電、兩個485互連,使用CH1(左邊的RS485接口)作為主設備,訪問CH2(右邊的RS485接口):
1. 使用UART_Device
這2個視頻,是在開始本節課程之前才補錄的:《3-9-1_面向對象封裝UART_完善收發功能》、《3-9-2_面向對象封裝UART_實現結構體》。這兩個視頻里,把UART2、UART4的發送、接收功能都補全了,并且構造了對應的UART_Device結構體,里面實現了初始化、發送、接收一個自己的的函數,如下:
把UART2、UART4封裝為UART_Device的代碼為:“3_程序源碼?1_視頻配套的源碼3-9_面向對象封裝UARTuart_rtos_all_ok.7z”。需要把它的代碼移植到本節的工程里:
使用STM32CubeMX配置的過程如下:
- 使能DMA通道:
- 各個DMA通道的配置如下:
2. 用作后端
把UART2、UART4用作libmodbus后端時,只需要修改這幾個函數即可:
有兩個UART_Device,調用哪個UART_Device?在使用“modbus_new_st_rtu”創建modbus_t時,根據傳入的設備名在modbus_t結構體里記錄對應的UART_Device。_modbus_rtu_connect、_modbus_rtu_send、_modbus_rtu_recv這三個函數,就可以直接調用modbus_t結構體里的UART_Device函數了。
6.3.6 libmodbus實驗(板載串口)
本節源碼為:
按照下圖連線:調試、供電、兩個485互連:
創建一個ClientTask,使用CH2(右邊的RS485接口)對外通信。
創建一個ServerTask,使用CH1(左邊的RS485接口)讀寫從設備數據。
-
MODBUS
+關注
關注
28文章
1820瀏覽量
77308 -
操作系統
+關注
關注
37文章
6892瀏覽量
123742 -
開源
+關注
關注
3文章
3402瀏覽量
42711
發布評論請先 登錄
相關推薦
評論