modbus协议的简介和教程网上有很多,这里根据个人思路制作一个快速上手版本,从底层到应用,只需一篇文章。
一、modbus协议简介
modbus协议分为很多种类,比如modbus-rtu,modbus-tcp,modbus-ascii,modbus-rtu over tcp等等,最常用的就是modbus-rtu和modbus-tcp两种类型了,其中modbus-rtu在OSI网络模型的物理层使用的大多是RS485,modbus-tcp使用的是RJ45也就是我们常说的水晶头。
为什么modbus协议搞这么麻烦,其实很简单,继续往下看。
二、modbus-rtu
这是modbus协议最开始的形态,其他版本都是基于这个版本的,理解了这个版本,其他版本就都差不多了。
1.modbus-rtu报文结构
modbus最终还是用来通信的协议,tcp通讯收发数据包,modbus收发报文,那么理解modbus报文格式将会是使用modbus协议的一项基本功。
modbus-rtu的报文分为以下几个部分:
从机地址 功能码 数据 CRC校验
8bit 8bit Nbit 16bit
2.modbus-rtu报文格式解析
现在你已经了解modbus的报文大概是什么结构,我们来看看具体怎么用。
从机地址:由于modbus协议采取一主多从的模式,故只有一个主机能主动发送报文,其他的从机只能回复收到的报文,所以在地址这里都是从机地址,主机不需要说明自己是谁。所有从机都能收到报文,但只有地址设置为报文中地址的从机会处理这条报文。由于这段数据长度规定为一字节,所以一条modbus上理论最多可以有0-255共256个从机。
功能码:这个字节用来告诉从机你准备让它干什么,常用的有1,2,3,4,5,6,15,16等,看到这么多数字可能会有点慌,但其实可以分为两个部分,很简单。其中01是用来读输出离散量或是1个bit的,02是用来读取输入离散量或是1个bit的,03是用来读取保持寄存器的,04用来读取输入寄存器,05用来写单个离散量,15用来写多个离散量,06用来写单个寄存器,16用来写多个寄存器。其中一个离散量就是1bit,一个寄存器大小是16bit。
数据:这里是最终你需要读或写的数据,不定长,最大250字节左右。
CRC校验:用来校验收到的数据是否受到损坏,如果损坏会回复错误,具体内容后面会讲,想详细了解校验方法自行百度。
上面说的这四部分就是modbus-rtu报文大致的结构,在实际使用中根据不同的功能只有较小的差距,下面举几个例子并说明。
读:
01,02,03,04发送的报文格式:
从站地址 功能码 地址高位 地址低位 数量高位 数量低位 CRC
8bit 8bit 8bit 8bit 8bit 8bit 16bit
01,02,03,04回复的报文格式:
从站地址 功能码 字节数 字节1 字节n CRC
8bit 8bit 8bit 8bit 8bit 8bit
这里发现为什么我说简单了吧,但是注意,01,02和03,04回复的解释方式是不一样的!
01,02功能码回复的字节是1bit对应一个离散量,而03,04回复的每两个字节才对应一个16位的寄存器。
现在你已经学会写所有的数据了,快来试试吧!
向地址255设备ID为2的输入寄存器写入666,用二进制或十六进制表示。
收到回复01 03 00 01 02 00 66 (十六进制,CRC校验省略),代表什么意思?
学会的朋友们可以把答案留在评论区。
写:
05,06发送的报文格式:
从站地址 功能码 地址高位 地址低位 写入高位 写入低位 CRC
8bit 8bit 8bit 8bit 8bit 8bit 16bit
这里又有一些区别,需要用一下脑子来记一记了,用05写离散量,写入数据得发送FF 00(十六进制)才代表1(true),00 00为0。而06写寄存器,高低位加起来默认是个int16。
05,06回复的报文格式:
从站地址 功能码 地址高位 地址低位 写入高位 写入低位 CRC
8bit 8bit 8bit 8bit 8bit 8bit 16bit
回复就简单了,正常回复就是直接复读发送的报文,哈哈。
15,16发送的报文格式:
从站地址 功能码 地址高位 地址低位 数量高位 数量低位 写入字节数 写入数据 CRC
8bit 8bit 8bit 8bit 8bit 8bit 8bit Nbit 16bit
这里15和16写入数据的区别跟01,03一样,一个是1bit代表一个离散量,一个是16bit一个寄存器。
15,16回复的报文格式:
从站地址 功能码 地址高位 地址低位 数量高位 数量低位 CRC
8bit 8bit 8bit 8bit 8bit 8bit 16bit
基本上也是复读,不过只复读到写入字节数之前。
异常回复:
当发送了错误的报文之后就会回复异常回复,异常回复格式为:
从站地址 异常功能码 错误码 CRC
8bit 8bit 8bit 16bit
异常功能码就是正常码加128,比如03请求出错,回复的异常功能码就是83H,错误码表如下:
异常码 | 名称 | 描述 |
---|---|---|
01 (0x01) | 非法功能码 | 从站设备不支持此功能码。 |
02 (0x02) | 非法数据地址 | 指定的数据地址在从站设备中不存在。 |
03 (0x03) | 非法数据值 | 指定的数据超过范围或者不允许使用。 |
04 (0x04) | 从站设备故障 | 从站设备处理响应的过程中,出现未知错误等。 |
05 (0x05) | 确认 | 从站设备已经接受请求,并且正在处理这个请求,但是需要长持续时间进行这些操作,返回这个响应防止在客户机(或主站)中发生超时错误,客户机(或主机)可以继续发送轮询程序完成报文来确认是否完成处理。 |
06 (0x06) | 从站设备忙 | 从站设备正在处理长持续时间的程序命令。 |
07 (0x07) | 否定确认 | 从站设备无法执行主站设备发送的请求。 |
08 (0x08) | 存储奇偶性差错 | 指示扩展文件区不能通过一致性校验。 |
10 (0x0A) | 不可用的网关路径 | 与网关一起使用,指示网关不能为处理请求分配输入端口值输出端口的内部通信路径。通常意味着网关是错误配置的或过载的。 |
11 (0x0B) | 网关目标设备响应失败 | 与网关一起使用,指示没有从目标设备中获得响应,通常意味着设备未在网络中。 |
小结:
到这里modbus的大部分东西就讲完了,但是还有很多细节没有写,这些在使用的过程中慢慢都会了解到,下面开始讲解modbus-tcp。
三、modbus-tcp
modbus-tcp和modbus-rtu很像,所以我只说有差别的部分,由于modbus-tcp的报文是嵌在tcp数据帧的数据段中,所以就不需要校验了,tcp协议做了这个工作。并且tcp已经用ip和端口号来确认从机地址了,所以就也不需要从机地址了。
1.modbus-tcp报文结构
事务处理标识符 协议标识符 长度 单元标识符 功能码 数据
16bit 16bit 16bit 8bit 8bit Nbit
后面的基本一样就是丢掉了校验,数据代表功能码后面所有其他数据,都是一样的,着重讲一下前面四个。
事务标识符:用于事务处理配对。回复报文会复读这一段数据。因为在以太网传输中存在一个问题,就是先发后至,我们可以利用这个事务处理标识符做一个id,来防止这种情况所造成的数据收发错乱(没人用,都是0000)。
协议标识符:modbus协议就是0000,固定的。
长度:后面的字节数
单元标识符:从站地址,基本没啥用,写错了也没事,原因前面讲过,很多设备的实现都会忽略这一段,但不排除有的设备非要你写对。
小结:
modbus-tcp和modbus-rtu的主要区别就是前面的一些数据,功能码后面的数据除了没有校验之外其他所有的都和rtu一样,故不再解释,下面给一个C语言收发报文的实现。
四、C语言实现收发modbus-tcp报文
#include <winsock2.h>
#pragma comment(lib, "Ws2_32.lib")
#include <ws2tcpip.h>
#include <stdio.h>
int main() {
// 初始化Winsock库
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "无法初始化Winsock库\n");
return EXIT_FAILURE;
}
// 创建TCP套接字
SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (tcpSocket == INVALID_SOCKET) {
fprintf(stderr, "无法创建TCP套接字: %d\n", WSAGetLastError());
WSACleanup();
return EXIT_FAILURE;
}
// 设置目标地址
sockaddr_in destAddr;
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(502);
if (inet_pton(AF_INET, "127.0.0.1", &destAddr.sin_addr) <= 0) {
fprintf(stderr, "无法转换目标IP地址\n");
closesocket(tcpSocket);
WSACleanup();
return EXIT_FAILURE;
}
// 连接到目标地址
if (connect(tcpSocket, (struct sockaddr*)&destAddr, sizeof(destAddr)) == SOCKET_ERROR) {
fprintf(stderr, "连接失败: %d\n", WSAGetLastError());
closesocket(tcpSocket);
WSACleanup();
return EXIT_FAILURE;
}
// 构造Modbus TCP 请求报文
unsigned char modbusRequest[] = {
0x00, 0x01, // 事务标识符
0x00, 0x00, // 协议标识符
0x00, 0x06, // 长度字段
0x01, // 单元标识符
0x03, // 功能码 (Read Holding Registers)
0x00, 0x00, // 起始寄存器地址
0x00, 0x01, // 寄存器数量
};
// 发送Modbus请求
if (send(tcpSocket, (char*)modbusRequest, sizeof(modbusRequest), 0) == SOCKET_ERROR) {
fprintf(stderr, "发送Modbus请求失败: %d\n", WSAGetLastError());
closesocket(tcpSocket);
WSACleanup();
return EXIT_FAILURE;
}
// 接收Modbus响应
char buffer[150];
int bytesRead = recv(tcpSocket, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
printf("接收到 %d 字节的数据:\n", bytesRead);
// 逐个打印接收到的数据
printf("接收到的数据内容:");
for (int i = 0; i < bytesRead; i++) {
printf("%02X ", (unsigned char)buffer[i]); // 以十六进制格式打印每个字节
}
printf("\n");
}
else if (bytesRead == 0) {
printf("连接已关闭\n");
}
else {
fprintf(stderr, "接收数据时发生错误: %d\n", WSAGetLastError());
}
// 关闭套接字
closesocket(tcpSocket);
// 清理Winsock库
WSACleanup();
return EXIT_SUCCESS;
}
可以看到其实很简单,就是发送一些数字,接收一些数字,这里只是举个例子,手写了一个报文,且并没有实现解析回复报文的功能。
总结:
modbus协议中常用的知识到此已经介绍完毕,阅读完这篇文章应该就能很轻易地开始使用modbus了,有收获可以点赞收藏,谢谢大家,后面有需要可能会增加一些新内容,但是应该没啥必要,剩下的一搜都有。