目录
1.modbus
Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种
1.1 分类
1) Modbus RTU:
运行在串口上的协议,采用二进制表现形式以及紧凑型数据结构,通信效率高,应用广泛。
2) Modbus ASCII:
运行在串口上的协议,采用ASCII码传输,并且利用特殊字符作为其字节的开始与结束标识,其传输效率要远远低于Modbus RTU协议,一般只有在通信数据量较小的情况下才考虑使用Modbus ASCII通信协议
3) Modbus TCP:
运行在以太网上的协议
1.2 特点
1)采用主从问答式通信
2)Modbus TCP是应用层协议,基于传输层TCP协议实现
3)Modbus TCP端口号默认 502
应用层协议实质是应用发送消息的规定格式。
2. modbus TCP
2.1 组成
ModbusTcp协议包含三部分:报文头、功能码、数据
2.2 报文头
包含7个字节,分别是:
事务处理标识符:主机发什么,从机回什么,没有限制
协议标识符:00 00 (十六进制) 占 2字节
长度:占两个字节,需要 4位 16进制来表示
单元标识符:从机 ID 一个字节
3. 寄存器
包含四种:离散量输入、线圈、输入寄存器、保持寄存器
1)离散量和线圈其实就是位寄存器(每个寄存器数据占1字节),工业上主要用于控制IO设备。
线圈寄存器,类比为开关量,每一个bit都对应一个信号的开关状态。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。
读功能可以分为读单个和读多个,但是通过一个功能码实现。
对应上面的功能码也就是:0x01 0x05 0x0f
离散输入寄存器,离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。
比如我读取外部按键的按下还是松开。对应功能码,读单个或多个。
2)输入和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值
保持寄存器,这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写功能分为单个写和多个写。读单个或多个。
所以功能码有对应的三个:0x03 0x06 0x10
输入寄存器,这个和保持寄存器类似,但是也是只支持读而不能写。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值
对应的功能码也就一个 0x04
总结:
读都是可以一个功能码读多个或单个的。而写是分为写单个和写多个需要两个功能码。
所以对于只读的寄存器都只有一个功能码,而可读可写的寄存器都有三个功能码。
4. 功能码
4.1 功能码代码
15(十进制)转换为十六进制:0x0f
16(十进制)转换为十六进制:0x10
寄存器PLC地址和寄存器的对应关系:
线圈: 00001-09999
离散量输入:10001-19999
输入寄存器:30001-39999
保持寄存器:40001-49999
4.2 寄存器操作
总结模板:
读数据:
主机-->从机:报文头(7)+ 功能码(1)+ 起始地址(2)+ 数量(2)
从机-->主机:报文头(7)+ 功能码(1)+字节计数(1)+数据(?)
写单个:
主机-->从机:报文头(7)+ 功能码(1)+ 地址(2)+ 断通标志或数据(2)
写多个:
主机-->从机:报文头(7)+功能码(1)+起始地址(2)+数量(2) +字节计数(1) +数据(?)
例如1:
主机->从机:02 25 00 00 00 06 01 03 00 63 00 02
02 25 :事务处理标识符
00 00 :协议标识符
00 06: 字节长度
03:功能码
00 63:起始地址 0063 ---》 0~99 ----》 40100
00 02 : 数量
也就是读两个保持寄存器,40100 与 40101
从机->主机:02 25 00 00 00 07 01 03 04 32 13 30 08
02 25 :事务处理标识符
00 00 :协议标识符
00 07 :字节长度
03:功能码
04:返回的字节计数
32 13:40100 的数值
30 08:40101 的数值
例2. 控制IO设备开关的协议数据,操作1个线圈,置1。
主机:0000 0000 0006 01 05 0022 FF00
事务标识符:00 00
协议标识符:00 00
字节数量:00 06 六个字节
功能码:05 写线圈
起始地址:00 22
通断标识:FF 00
5. 练习
练习一
实现03功能,读单个或多个保持寄存器
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// 1.创建流式套接字
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd < 0)
{
perror("clientfd socket error");
return -1;
}
printf("client socket scuess\n");
// 2.指定服务器的网络信息 struct sockaddr_in
// 定义结构体变量 server_addr
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // ipv4族
server_addr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
server_addr.sin_port = htons(atoi(argv[2])); // 端口
// 3.请求链接服务器 connect()
socklen_t addrlen = sizeof(server_addr); // 长度
// 客户端的socket连接服务器
int ret = connect(clientfd, (struct sockaddr *)&server_addr, addrlen);
if (ret < 0)
{
perror("connect err");
return -1;
}
printf("connect scuess\n");
// 发送03命令
// 读取0000地址开始的三个寄存器的值
uint8_t buf[32] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x05};
ret = send(clientfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("send error");
return -1;
}
recv(clientfd, buf, sizeof(buf), 0);
for (int i = 0; i < buf[8]; i++)
{
printf("%02x ", buf[9 + i]);
}
printf("\n");
uint8_t buf2[32] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x10, 0x00, 0x00, 0x00, 0x05};
close(clientfd);
return 0;
}
练习二
封装函数,实现03、05功能
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int clientfd;
// 初始化服务器
void set_server_id(struct sockaddr_in *p)
{
char buf1[32] = {0};
char buf2[32] = {0};
printf("请输入ip地址: ");
scanf("%s", buf1);
printf("请输入端口号: ");
scanf("%s", buf2);
p->sin_family = AF_INET; // ipv4族
p->sin_addr.s_addr = inet_addr(buf1); // ip地址
p->sin_port = htons(atoi(buf2)); // 端口
}
// 读保持寄存器
// *p:功能码 addr 起始地址 num 读取的个数
void read_registers(uint8_t id, uint8_t g, uint16_t addr, uint16_t num)
{
int res;
uint8_t data[128] = {0};
uint8_t buf[32] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, id, g, addr >> 8, addr, num >> 8, num};
for (int i = 0; i < 12; i++)
{
printf("%02x ", buf[i]);
}
printf("\n");
res = send(clientfd, buf, 12, 0);
if (res < 0)
{
perror("send read error");
return;
}
res = recv(clientfd, data, sizeof(data), 0);
if (res < 0)
{
perror("recv read error");
return;
}
for (int i = 0; i < data[8]; i++)
{
printf("%02x ", data[9 + i]);
}
printf("\n");
}
// 写线圈寄存器
// id: 从机id g: 功能码 addr: 起始地址 op:1,0
void write_coil(uint8_t id, uint8_t g, uint16_t addr, int op)
{
int res;
uint8_t data[128] = {0};
uint8_t buf[32] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, id, g, addr >> 8, addr};
if (op == 1)
{
buf[10] = 0xFF;
}
else if (op == 0)
{
buf[10] = 0x00;
}
buf[11] = 0x00;
for (int i = 0; i < 12; i++)
{
printf("%02x ", buf[i]);
}
printf("\n");
res = send(clientfd, buf, sizeof(buf), 0);
if (res < 0)
{
perror("send write error");
return;
}
res = recv(clientfd, data, sizeof(data), 0);
if (res < 0)
{
perror("recv write error");
return;
}
for (int i = 0; i < 12; i++)
{
printf("%02x ", data[i]);
}
printf("\n");
}
int main(int argc, char const *argv[])
{
// int clientfd;
// 1.创建流式套接字
clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd < 0)
{
perror("clientfd socket error");
return -1;
}
printf("client socket scuess\n");
// 2.指定服务器的网络信息 struct sockaddr_in
// 定义结构体变量 server_addr
struct sockaddr_in server_addr;
set_server_id(&server_addr);
// 3.请求链接服务器 connect()
socklen_t addrlen = sizeof(server_addr); // 长度
// 客户端的socket连接服务器
int ret = connect(clientfd, (struct sockaddr *)&server_addr, addrlen);
if (ret < 0)
{
perror("connect err");
return -1;
}
printf("connect scuess\n");
// 发送命令
int id;
printf("请输入从机id: ");
scanf("%x", &id);
read_registers(id, 0x03, 0x0000, 0x0002);
write_coil(id, 0x05, 0x0000, 1);
close(clientfd);
return 0;
}
标签:00,addr,clientfd,0x00,TCP,modbus,寄存器,include
From: https://blog.csdn.net/weixin_67273669/article/details/140618943