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