-
1.起源
Modbus由Modicon公司于1979年開發,是一種工業現場總線協議標準。
Modbus通信協議具有多個變種,其中有支持串口,以太網多個版本,其中最著名的是Modbus?RTU(通信效率最高,基于串口)、Modbus?ASCII和Modbus?TCP(基于以太網)三種
其中Modbus?TCP是在施耐德收購Modicon后1997年發布的。
2.分類
1)Modbus?RTU
運行在串口上的協議,采用二進制表現形式以及緊湊的數據結構,通信效率較高,應用比較廣泛
2)Modbus?ASCII
運行在串口上的協議,采用ASCII碼進行傳輸,并且每個字節的開始和結束都有特殊字符作為標志,傳輸效率遠遠低于Modbus?RTU,一般只有通訊量比較少時才會考慮它。
3)Modbus?TCP
運行在以太網上的協議
3.優勢:
免費、簡單、容易使用
4.應用場景:
Modbus協議是現在國內工業領域應用最多的協議,不只PLC設備,各種終端設備,比如水控機、水表、電表、工業秤、各種采集設備
二、Modbus?tcp協議
1.特點
1)采用主從問答式通信
(一個主機可以對應多個從機,可以發出請求采集數據,發出請求控制設備)
2)Modbus?TCP是應用層協議,基于傳輸層的TCP進行通信的
3)Modbus?TCP端口號默認502
2.組成
ModbusTcp協議包含三部分:報文頭、功能碼、數據????????
報文頭7個字節,功能碼1個字節,Modbus?TCP/IP協議最大數據幀長度為260字節,最大252,(1?byte?=?8?bit)
1)報文頭
包含一個7字節報文頭
事物處理標識符:沒有限制,主機按照啥發,從機按照什么回
協議標識符:為啥是4個0?寫一個0行不行?每一位都是16進制數,?一位十六進制由4位2進制組成,8位2進制為一個字節
長度:
單元標識符:從機id
注意:報文頭包含內容個數順序均不可調換
2)寄存器
Modbus?TCP通過寄存器的方式存儲數據。
一共有四種類型的寄存器,分別是:離散量輸入、線圈、輸入寄存器、保持寄存器。
離散量和線圈其實就是位寄存器(每個寄存器數據占1字節),工業上主要用于控制IO設備。輸入和保持寄存器是字寄存器(每個寄存器數據占2個字節),工業上主要用于存儲工業設備的值。
1)?離散量和線圈其實就是位寄存器(每個寄存器數據占1字節),工業上主要用于控制IO設備
線圈寄存器,類比為開關量,每一個bit都對應一個信號的開關狀態。所以一個byte就可以同時控制8路的信號。比如控制外部8路io的高低。?線圈寄存器支持讀也支持寫,寫在功能碼里面又分為寫單個線圈寄存器和寫多個線圈寄存器。
離散輸入寄存器,離散輸入寄存器就相當于線圈寄存器的只讀模式,他也是每個bit表示一個開關量,而他的開關量只能讀取輸入的開關信號,是不能夠寫的。比如我讀取外部按鍵的按下還是松開。
2)?輸入和保持寄存器是字寄存器(每個寄存器數據占2個字節),工業上主要用于存儲工業設備的值。
保持寄存器,這個寄存器的單位不再是bit而是兩個byte,也就是可以存放具體的數據量的,并且是可讀寫的。比如我我設置時間年月日,不但可以寫也可以讀出來現在的時間。寫也分為單個寫和多個寫
輸入寄存器,這個和保持寄存器類似,但是也是只支持讀而不能寫。一個寄存器也是占據兩個byte的空間。類比我我通過讀取輸入寄存器獲取現在的AD采集值?
3)功能碼(16進制)
寄存器PLC地址和寄存器的對應關系:
00001-09999?:線圈
10001-19999:離散量輸入
30001-39999:輸入寄存器
40001-49999:保持寄存器
點亮一個燈:05
讀溫濕度:04?03
具體協議分析:
實例分享 | ModbusTCP報文詳解
4)總結
讀數據:
主機--》從機:
報文頭--功能碼--起始地址--數量
從機--》主機:
報文頭(長度可能發生變化)--功能碼(不變)--字節計數--數據
在讀數據和寫單個的時候,字節長度都是0x06(?一個字節的單元標識符,一個字節的功能碼,兩個字節的地址,兩個字節的數據)
寫數據:
寫單個:
主機--》從機:
報文頭--功能碼--地址--數據/斷通標志(保持寄存器:數據:線圈:斷通標志)
從機--》主機:
原文返回
寫多個:
主機--》從機:
報文頭----功能碼----起始地址----數量----字節計數----數據
從機--》主機:??
原文返回
5)練習
練習一
主機詢問數據流
00?03?00?00?00?06?01 03 00?63 00?02
00?00:事務處理標識符?
00?00:協議標識符
00?06?:字節?長度
03:功能碼(保持寄存器)
0063:起始地址:6*16+3=99,40100
0002:寄存器的數量??40100-40101
00?03?00?00?00?07?01 03 04 05?83 00?76
00?00:事務處理標識符
00?00:協議標識符
00?07:字節長度
03:功能碼
04:字節計數
0583:40100寄存器的數據
0076:40101寄存器的數據
練習2
1.讀傳感器數據,讀1個寄存器數據,寫出主從數據收發協議。
00?00?00?00? 00?06 01 03 00?60 00?01
00?00?00?00? 00?05 01 03 02 00?62
- 寫出控制IO設備開關的協議數據,操作1個線圈,置1。
線圈單個置零:0000?置一:FF00
00?00?00?00? 00?06 01 05 00?60 FF?00
00?00?00?00? 00?06 01 05 00?60 FF?00
- 工具安裝
- Modbus?Salve/Poll
1)安裝過程:軟件默認安裝
2)破解:點擊connection-》connect,輸入序列號(序列號在SN.txt)
3)使用
從機:
先設置,先打開salve端,
再連接:點擊connection-》connect
主機:
-
- 網絡調試助手
-
- Wireshark
捕獲器選擇:
windows如果連接有線網絡,選擇本地連接/以太網
?????如果連接無線網絡,選擇WLAN
如果只是在本機上的通信,選擇NPCAP?Loopback?apdater
或Adapter?for?loopback?traffic?capture
過濾條件:
過濾端口:tcp.port==502
過濾IP:ip.addr?==?192.168.3.11(windows?的ip)
- 練習
在虛擬機寫程序實現poll端功能,編寫客戶端實現和Slave通信。
發送:發送Modbus?TCP協議???03功能碼
發送:uint8_t?data[12]={0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x03,0x00,0x00,0x00,0x02};
接收:recv
#include?<stdio.h>
#include?<stdlib.h>?//?atoi
#include?<sys/types.h>
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<netinet/ip.h>
#include?<arpa/inet.h>
#include?<unistd.h>int main(int?argc, char const *argv[])
{uint8_t?data[12] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06,0x01, 0x03, 0x00, 0x00, 0x00, 0x03};uint8_t?buf[32] = {0};//1.創建套接字int?sockfd?= socket(AF_INET,?SOCK_STREAM, 0);if (sockfd?< 0){perror("socket?err");return -1;}//2.填充結構體struct sockaddr_in?caddr;
????caddr.sin_family?=?AF_INET;
????caddr.sin_port?= htons(502);
????caddr.sin_addr.s_addr?= inet_addr(argv[1]);//3.鏈接if (connect(sockfd, (struct sockaddr *)&caddr, sizeof(caddr)) < 0){perror("connect?err");return -1;}//4.發送send(sockfd,?data, sizeof(data), 0);//5.接收recv(sockfd,?buf, sizeof(buf), 0);for (int?i?= 0;?i?<?buf[8];?i++)printf("%#x?",?buf[9+i]);putchar(10);//6.關閉close(sockfd);return 0;
}
作業:
- 復習今天內容
- 完成05功能碼??,寫一個線圈
- 封裝函數。。
/*客戶端創建代碼?*/
#include?<stdio.h>
#include?<sys/types.h>?/*?See?NOTES?*/
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<netinet/ip.h>?/*?superset?of?previous?*/
#include?<arpa/inet.h>
#include?<unistd.h>
#include?<stdlib.h>
#include?<string.h>
int?sockfd;
uint8_t?buf[32];
void?writecoil(uint8_t?*p,?int?addr,?int?num)
{
????p[7]?=?0x05;
????p[8]?=?addr?>>?8;???//寄存器高位
????p[9]?=?addr?&?0xff;?//寄存器低位
????if?(num?==?1)
????{
????????p[10]?=?0xff;?//置1
????}
????else?if?(num?==?0)
????{
????????p[10]?=?0x00;?//置0
????}
????p[11]?=?0x00;
????for?(int?j?=?0;?j?<?12;?j++)
????{
????????printf("%#x?",?*(p+j));
????}
????send(sockfd,?p,?12,?0);
????recv(sockfd,?buf,?sizeof(buf),?0);
????for?(int?i?=?0;?i?<?12;?i++)
????{
????????printf("%#x?",?buf[i]);
????}
????putchar(10);
}
void?readregster(uint8_t?*p,?int?addr,?int?num)
{
????p[7]?=?0x03;
????p[8]?=?addr?>>?8;???//寄存器高位
????p[9]?=?addr?&?0xff;?//寄存器低位
????p[10]?=?num?>>?8;???//數量高位
????p[11]?=?num?&?0xff;?//數量低位
????for?(int?j?=?0;?j?<?12;?j++)
????{
????????printf("%#x?",?*(p?+?j));
????}
????putchar(10);
????send(sockfd,?p,?12,?0);
????recv(sockfd,?buf,?sizeof(buf),?0);????for?(int?i?=?0;?i?<?buf[8];?i++)
????{
????????printf("%#x?",?buf[9?+?i]);
????}
????putchar(10);
}
void?set_selve_id(uint8_t?*p)
{
????p[6]?=?0x01;
}
int?main(int?argc,?char?const?*argv[])
{
????if?(argc?<?3)
????{
????????printf("input?err\n");
????????return?-1;
????}
????//創建套接字
????sockfd?=?socket(AF_INET,?SOCK_STREAM,?0);
????if?(sockfd?<?0)
????{
????????perror("socket?err.");
????????return?-1;
????}
????//填充結構體
????struct?sockaddr_in?caddr;
????caddr.sin_family?=?AF_INET;
????caddr.sin_port?=?htons(atoi(argv[2]));
????caddr.sin_addr.s_addr?=?inet_addr(argv[1]);
????socklen_t?len?=?sizeof(caddr);
????//鏈接
????if?(connect(sockfd,?(struct?sockaddr?*)&caddr,?len)?<?0)
????{
????????perror("connect?err.");
????????return?-1;
????}
????//?uint8_t?data[12]?=?{0x00,?0x00,?0x00,?0x00,?0x00,?0x06,?0x01,?0x01,?0x00,?0x07,?0xFF,?0x00};
????//?uint8_t?data1[12]?=?{0x00,?0x00,?0x00,?0x00,?0x00,?0x06,?0x01,?0x01,?0x00,?0x00,?0x00,?0x03};
????uint8_t?data[12]?=?{0x00,?0x00,?0x00,?0x00,?0x00,?0x06};
????int?th?=?sizeof(data);
????int?addr?=?0x0101;
????int?num?=?0x0000;
????set_selve_id(data);
????//?readregster(data,?0,?2);
????writecoil(data,?6,?1);
????return?0;
}