MODBUS协议从理解到应用快速解析(附带C语言简单实现)

        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了,有收获可以点赞收藏,谢谢大家,后面有需要可能会增加一些新内容,但是应该没啥必要,剩下的一搜都有。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值