目录
[2. 分类:](#2. 分类:)
[3. 优势:](#3. 优势:)
[4. 应用场景:](#4. 应用场景:)
[二、 ModbusTCP的协议](#二、 ModbusTCP的协议)
[1. 报文头](#1. 报文头)
[2. 寄存器](#2. 寄存器)
[1. 线圈(Coils)](#1. 线圈(Coils))
[2. 离散量输入(Discrete Inputs)](#2. 离散量输入(Discrete Inputs))
[3. 输入寄存器(Input Registers)](#3. 输入寄存器(Input Registers))
一:modbus起源
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,一般只有通讯量比较少时才会考虑它。
注:在ASCII模式下,每个8位的字节被拆分成两个ASCII字符进行发送,比如十六进制0xAF(1010 1111),会被分解成ASCII字符"A"(0100 0001)和"F"(0100 0110)进行发送,其发送量显然比RTU增加一倍。
3)Modbus TCP
运行在以太网上的协议
3. 优势:
免费、简单、容易使用
4. 应用场景:
Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备。
5.ModbusTCP特点(掌握):
1)采用主从问答式通信
2)Modbus TCP是应用层协议,基于传输层的TCP进行通信的
注:更好的理解网络模型的分层特点:
各层之间独立,每一层不需要知道下一层如何实现
当任何一层发生变化时,只要层间接口关系保持不变,则这层以上或以下层不受影响。
3)Modbus TCP端口号默认502
二、 ModbusTCP的协议
ModbusTcp协议包含三部分:报文头、功能码、数据
MBAP:Modbus Application Protocol (modbus报文头)
PDU:Protocol Data Unit(协议数据单元)
Modbus TCP/IP协议最大数据帧长度为260字节
1. 报文头
包含7个字节
2. 寄存器
1. 线圈(Coils)
- 定义:线圈在Modbus协议中通常被类比为开关量,每一个bit都对应一个信号的开关状态。它主要用于控制IO设备的开关状态,如继电器、阀门等。
- 访问类型:可读可写。通过发送特定的功能码(如01H、05H、0FH),可以读取或修改线圈的状态。
- 应用场景:用于控制外部设备的开/关状态,如控制灯的亮灭、电机的启停等。
2. 离散量输入(Discrete Inputs)
- 定义:离散量输入寄存器相当于线圈寄存器的只读模式,每个bit表示一个开关量,但只能读取输入的开关信号,不能修改。
- 访问类型:只读。通过发送功能码02H,可以读取离散量输入寄存器的状态。
- 应用场景:用于读取外部设备的状态,如按钮是否被按下、开关是否处于打开状态等。
3. 输入寄存器(Input Registers)
- 定义:输入寄存器与保持寄存器类似,但它是只读的。每个输入寄存器占据两个byte的空间,可以存储16位的数据。
- 访问类型:只读。通过发送功能码04H,可以读取输入寄存器的值。
- 应用场景:用于读取工业设备的模拟量输入值,如温度、压力、流量等传感器的读数。
- 保持寄存器(Holding Registers)
- 定义:保持寄存器是Modbus协议中最重要的数据类型之一,它既可以读取也可以修改。每个保持寄存器同样占据2个byte的空间,可以存储16位的数据。
- 访问类型:可读可写。通过发送功能码03H、06H或10H,可以读取或修改保持寄存器的值。
- 应用场景:用于存储和修改设备的设定值、状态值等,如设置温度控制器的目标温度、读取设备的运行时间等。
总结
数据类型 | 定义 | 访问类型 | 功能码 | 应用场景 |
---|---|---|---|---|
线圈 | 开关量,每个bit对应一个信号的开关状态 | 可读可写 | 01H, 05H, 0FH | 控制外部设备的开/关状态 |
离散量输入 | 类似线圈的只读模式,每个bit表示一个开关量 | 只读 | 02H | 读取外部设备的状态 |
输入寄存器 | 类似保持寄存器的只读模式,每个寄存器占两个byte | 只读 | 04H | 读取工业设备的模拟量输入值 |
保持寄存器 | 既可以读取也可以修改,每个寄存器占两个byte | 可读可写 | 03H, 06H, 10H | 存储和修改设备的设定值、状态值等 |
3.功能码
01功能码分析
05功能码分析
0F功能码分析
练习:封装函数实现03,05功能码的作用
cs
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
void read_registers(uint8_t *p, uint16_t addr, uint16_t num, uint8_t *dest, int sockfd)
{
p[8] = addr >> 8;
p[9] = addr;
p[10] = num >> 8;
p[11] = num;
send(sockfd, p, 12, 0);
int ret = recv(sockfd, dest, 24, 0);
if (ret < 0)
{
perror("recv err");
close(sockfd);
return;
}
else
{
for (int i = 0; i < ret; i++)
printf("%02x ", dest[i]);
}
printf("\n");
}
void write_coil(uint8_t *p, uint16_t addr, int op, uint8_t *dest, int sockfd)
{
p[8] = addr >> 8;
p[9] = addr;
p[11] = 0;
if (op == 1)
{
p[10] = 0xFF;
}
else
{
p[10] = 0;
}
send(sockfd, p, 12, 0);
int ret = recv(sockfd, dest, 24, 0);
if (ret < 0)
{
perror("recv err");
close(sockfd);
return;
}
else
{
for (int i = 0; i < ret; i++)
printf("%02x ", dest[i]);
}
printf("\n");
}
int main(int argc, char const *argv[])
{
char buf[128] = {0};
// 1.创建套接字(socket)------------》有手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 2.指定(服务器)网络信息--------》有对方的号码
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(502);
saddr.sin_addr.s_addr = inet_addr(argv[1]);
// 3.连接(connect)-------------------》拨打电话
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("connect err");
return -1;
}
printf("connect okk\n");
uint8_t data_reg[12] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03};
uint8_t data_reg1[24] = {};
uint16_t first;
int num;
printf("请输入读取保持寄存器起始地址:");
scanf("%hx", &first);
printf("请输入要查询的寄存器个数:");
scanf("%d", &num);
read_registers(data_reg, first, num, data_reg1, sockfd);
uint8_t data_coil[12] = {0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05};
uint8_t data_coil1[24] = {};
printf("请输入写入线圈起始地址:");
scanf("%hx", &first);
printf("请选择设置通断状态:");
scanf("%d", &num);
write_coil(data_coil, first, num, data_coil1, sockfd);
close(sockfd);
return 0;
}