1.前言
使用modbus有些時間了,期間使用過modbus RTU也使用過modbus TCP,通過博文和大家分享一些MODBUS TCP的東西。在嵌入式中實現TCP就需要借助一個以太網協議棧,在這里我選擇最簡單的uIP協議棧。uIP協議棧簡單易用方便上手,相比於LwIP無論是移植還是使用難度都低些,這樣就可以把更多的精力花在modbus tcp協議本身而不必花大量的時間研究以太網協議棧。modbus協議棧為freemodbus
【其他有用的博文】
【1】
uIP學習筆記
【2】
MODBUS協議整理——匯總
【工程代碼】
示例代碼托管於GitHub——【
Github Clone】
如果有問題我會及時更新。
【使用說明】
【1】工具鏈為IAR 6.5
【2】從機IP為固定IP 192.168.1.15,請保證從機和路由器位於同一個網段中。
【3】modbus tcp的偵聽端口號為502
2.MODBUS TCP注意點
2.1 主機和從機、服務端和客戶端

圖1 MODBUS請求響應模型
【在modbus協議中】
主機發送modbus請求,從機根據請求內容向主機返回響應。在modbus協議中,主機總是主動方,從機總是被動方。
【在網絡應用中】
在網絡應用中存在客戶端和服務器端,客戶端(例如瀏覽器)發送請求到服務器,服務器向客戶端返回內容(例如HTML文本)。
【在modbus tcp中】
主機是客戶端,而從機是服務器端。千萬不要以為服務器端重要,主機也重要。
【在modbus rtu中】
主機(Master)是客戶端(Client),而從機(Slave)是服務器端(Server)。
2.2 是否可以多主機
通過前面的分析,主機為客戶端那么modbus tcp支持多個主機,在一個局域網中可存在多個主機和多個從機。從機的連接能力(連接主機的數量)由uIP的最大TCP連接個數決定。
2.3 modbus TCP協議簡述
modbus TCP和modbus RTU基本相同,但是也存在一些區別
【1】從機地址變得不再重要,多數情況下忽略。從某種意義上說從機地址被IP地址取代
【2】CRC校驗變得不再重要,甚至可以忽略。由於TCP數據包中已經存在校驗,為了不重復造輪子,modbus TCP干脆取消了CRC校驗。
modbus TCP和modbus RTU的區別可使用下圖概括

圖2 modbus TCP數據包和modbus RTU數據包比較
在modbus TCP中包含一個MBAP頭,該頭包含以下幾個部分
區域 |
長度
|
描述
|
客戶端
|
服務器
|
傳輸標志
|
2字節
|
MODBUS 請求和響應傳輸過程中
序列號
|
客戶端生成 |
應答時復制該值
|
協議標志 |
2字節
|
Modbus協議默認為0
|
客戶端生成
|
應答時復制該值
|
長度
|
2字節
|
剩余部分的長度 |
客戶端生成
|
應答時由服務器端生成
|
單元標志
|
1字節
|
從機標志(從機地址) |
客戶端生成
|
應答時復制該值
|
【1】傳輸標志可理解為序列號,防止MODBUS TCP通信錯位,例如后發生的響應先到了主機,而早發生的響應后到主機
【2】單元標志可理解為從機地址,此時已經不再重要
2.4 modbus tcp 和 TCP IP的關系
modbus TCP可以理解為發生在TCP上的應用層協議,既然是TCP協議那么一個完整的MODBUS TCP報文必然包括TCP首部,IP首部和Ethernet首部。
下面就通過uIP協議棧來實現modbus TCP
3.代碼實現
3.1 偵聽502端口
- BOOL
- xMBTCPPortInit( USHORT usTCPPort )
- {
- BOOL bOkay = FALSE;
- USHORT usPort;
- if( usTCPPort == 0 )
- {
- usPort = MB_TCP_DEFAULT_PORT;
- }
- else
- {
- usPort = (USHORT)usTCPPort;
- }
- // 偵聽端口 502端口
- uip_listen(HTONS(usPort));
- bOkay = TRUE;
- return bOkay;
- }
【代碼說明】
【1】uip_listen(HTONS(usPort)) 偵聽502端口,注意大小端變化。
3.2 uIP循環處理——porttcp.c
- void uip_modbus_appcall(void)
- {
- if(uip_connected())
- {
- PRINTF("connected!\r\n");
- }
- if(uip_closed())
- {
- PRINTF("closed\r\n");
- }
- if(uip_newdata())
- {
- PRINTF("request!\r\n");
- // 獲得modbus請求
- memcpy(ucTCPRequestFrame, uip_appdata, uip_len );
- ucTCPRequestLen = uip_len;
- // 向 modbus poll發送消息
- xMBPortEventPost( EV_FRAME_RECEIVED );
- }
- if(uip_poll())
- {
- if(bFrameSent)
- {
- bFrameSent = FALSE;
- // uIP發送Modbus應答數據包
- uip_send( ucTCPResponseFrame , ucTCPResponseLen );
- }
- }
- }
【代碼說明】
【1】uip_newdata()返回為True表示存在新的數據
【2】復制uip_appdate中的數據到ucTCPRequestFrame,該變量為全局變量,通過該全部變量”中轉“到modbus處理的緩沖區中。然后向modbus協議棧發送消息,消息內容為EV_FRAME_RECEIVED 。由於沒有使用操作系統
【3】如果處理完成則通過uip_send發送響應。
【4】ucTCPRequestFrame和ucTCPResponseFrame均為全局數組,用於和modbus緩沖區交換數據。
static UCHAR ucTCPRequestFrame[MB_TCP_BUF_SIZE];static USHORT ucTCPRequestLen;static UCHAR ucTCPResponseFrame[MB_TCP_BUF_SIZE];static USHORT ucTCPResponseLen;
3.3 modbus 接收處理
- BOOL
- xMBTCPPortGetRequest( UCHAR ** ppucMBTCPFrame, USHORT * usTCPLength )
- {
- *ppucMBTCPFrame = &ucTCPRequestFrame[0];
- *usTCPLength = ucTCPRequestLen;
- /* Reset the buffer. */
- ucTCPRequestLen = 0;
- return TRUE;
- }
【代碼說明】
【1】** ppucMBTCPFrame為一個指向數據的指針,而*ppucMBTCPFrame可以指向一個數組,在這里可把ucTCPRequestFrame復制給該變量,配合usTCPLength,那么從uIP接收到的內容就”轉移“到freemodbus中。
3.4 modbus 發送處理
- BOOL
- xMBTCPPortSendResponse( const UCHAR * pucMBTCPFrame, USHORT usTCPLength )
- {
- memcpy( ucTCPResponseFrame , pucMBTCPFrame , usTCPLength);
- ucTCPResponseLen = usTCPLength;
- bFrameSent = TRUE; // 通過uip_poll發送數據
- return bFrameSent;
- }
【代碼說明】
【1】把傳入的內容 pucMBTCPFrame復制給ucTCPResponseFrame,並設置bFrameSent為True,那么在下一次uip_poll時便會把響應發送會主機。
4.測試與分析
【1】連接從機
選擇IP地址為192.168.1.15,端口號為502

圖3 打開modbus tcp連接
【2】嘗試讀出保持寄存器

圖4 讀取保持寄存器
【3】抓包分析
請使用ip.addr == 192.168.1.15 表達式過濾報文,其中192.168.1.100為PC機,此處為modbus 主機
【簡單分析】
【1】115行為modbus主機請求,此時傳輸標志為25.
【2】116行為modbus從機給出的TCP應答,TCP應答為TCP協議規定的內容,TCP應答中不包含modbus 響應
【3】117行為modbus從機響應,此時傳輸標志依然為25.
【4】118行為modbus主機 TCP應答,同16行。

圖5 抓包分析
轉摘:http://blog.csdn.net/xukai871105/article/details/21652287