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