天天看点

基于Tcp通讯实现自定义的modbusTcp软协议(QT/C++ for android)概述代码总结

概述

modbus作为工业通用协议,应用极广且非常成熟,大部分的编译器支持modbus并会封装成模块供使用者调用,我自己用的是QT,本身也是有一个seriousbus的模块,专门封装了modbus的相关函数,调用起来非常方便,但是,在QT5.12之后 ,seriousbus模块从QT自带的android编译器中移除了。而我恰好得写一个用在android系统里的通讯项目,这就很尴尬了。。。虽然QT提供了 qseriousbus-everywhere-master的外部库,但是我并不会安装(有会装这玩意的能不能留言给个教程链接)。所以只能自己写个Tcp和上位机通讯,然后按照modbus的格式,进行数据交互。

代码

tcp构建

用QT建立tcp的client非常简单,网上资源多得是,我这里就不贴代码了,端口号502,ip自己配好,测一下能连上,就ok了。这里主要讲一下,报文的生成和数据的解析。

报文生成

tcp连接建立之后,要进行数据通讯,就必须发送符合modbus格式的报文给上位机,这里用的是modbusTcp协议,不需要Rtu协议的CRC校验部分,报文格式和详解网上资源很多,这里简单提一下,最下面会给一个《modbus协议详解中文版》。

简单来说,modbus报文由2位16进制数构成,一个2位16进制数1字节。

00 00 :消息号,自己随便定义,返回相同的消息号;

00 00 :识别号,00 00 表示这里是一个modbus协议;

00 06 :后续字节数,表示06之后还有6个字节;

00 : 从站号,这东西一般设置plc的时候可以设置,如果实在不知道,可以先用QT封装好的modbus函

数写一个简单通讯,然后用wireShark抓个包,看看发出和接收的站号是多少。

06: 功能码,06表示写入单个寄存器,功能码具体的功能网上很多,协议详解里也有,不多赘述。

F7 00:地址,你要读取或写入的地址,注意区分高位和低位。

00 01:如果是读取,这里表示读取的数量,如果是写入,这两个字节表示写入的数值,数值需区分高低位

(modbusTcp没有CRC校验)

即:00 00 00 00 00 06 00 06 F7 00 00 01 这样一个报文就是一个有效的报文。

接下来,贴上生成报文的函数代码:

首先是读取报文的生成:

void MymodebusTcp::readrequestData(int wid, int address, QByteArray &datamsg)   //读取报文的生成
{
//输入十进制int,转为16进制QString,前两字符为高位,后两个字符为低位。
    QString n_address = QString("%1").arg(address,4,16,QLatin1Char('0'));  //转16进制
    QStringList adl;
    n_address = n_address.trimmed();  

    for (int i = 0; i < 4; i++)
    {
        adl << n_address.at(i);
    }
    
    QString hadr = "0x" + adl[0] + adl[1];  //高位string
    QString ladr = "0x" + adl[2] + adl[3];  //低位string
    
    int n_hadr = hadr.toInt(nullptr, 16);   //高位int
    int n_ladr = ladr.toInt(nullptr, 16);   //低位int

    datamsg[0] = 0x01;        //消息号
    datamsg[1] = 0x2E;
    datamsg[2] = 0x00;        //modbus标识
    datamsg[3] = 0x00;
    datamsg[4] = 0x00;        //后续字节数
    datamsg[5] = 0x06;
    datamsg[6] = 0x00;        //站号
    datamsg[7] = (char)wid;     //功能码
    datamsg[8] = (char)n_hadr;  //高位地址
    datamsg[9] = (char)n_ladr;  //低位地址
    datamsg[10] = 0x00;         //每次读一个
    datamsg[11] = 0x01;
}
           

读取部分的功能码,这里直接写成每次读一个,毕竟这个协议肯定不如modbus正规协议成熟,应用范围窄,没必要实现所有功能,要想批量读取,外部直接写一个地址的自增,循环读好了。

下面是写入报文的生成:

void MymodebusTcp::writerequestData(int wid, int address, int data, QByteArray &datamsg) //写入报文生成
{      
//地址部分的高低位处理,同上
    QString n_address = QString("%1").arg(address,4,16,QLatin1Char('0'));  
    n_address = n_address.trimmed();  
    QStringList adl;

    for (int i = 0; i < 4; i++)
    {
        adl << n_address.at(i);
    }

    QString hadr = "0x" + adl[0] + adl[1];
    QString ladr = "0x" + adl[2] + adl[3];
 
    int n_hadr = hadr.toInt(nullptr, 16);
    int n_ladr = ladr.toInt(nullptr, 16);
    
    if(wid == 16 || wid == 06)     //写寄存器的功能码,23好像也是但是用得不多,需要用23的可以自己加上
    {
    //写入的数据,和地址一样需要进行高低位的处理
        QString n_data = QString("%1").arg(data,4,16,QLatin1Char('0'));  
        n_data = n_data.trimmed();  
        QStringList datalist;

        for (int i = 0; i < 4; i++)
        {
            datalist << n_data.at(i);
        }

        QString h_data = "0x" + datalist[0] + datalist[1];
        QString l_data = "0x" + datalist[2] + datalist[3];

        int hdata = h_data.toInt(nullptr, 16);
        int ldata = l_data.toInt(nullptr, 16);

        datamsg[0] = 0x01;
        datamsg[1] = 0x2E;
        datamsg[2] = 0x00;
        datamsg[3] = 0x00;
        datamsg[4] = 0x00;
        datamsg[5] = 0x06;
        datamsg[6] = 0x00;
        datamsg[7] = (char)wid;
        datamsg[8] = (char)n_hadr;
        datamsg[9] = (char)n_ladr;
        datamsg[10] = (char)hdata;
        datamsg[11] = (char)ldata;
    }
    else           //写入线圈的操作(线圈应该都是bool吧,有例外的自己改一下)
    {
        QString s = QString::number(data);
        int hdata,ldata;
        int indata = s.toInt(nullptr, 16);
       //修改线圈通断的输入时设定好的, 0000 表示断,FF00表示通
        if(indata == 0)  //断
        {
            hdata = 0x00;
            ldata = 0x00;
        }
        else         //通 
        {
            hdata = 0xFF;
            ldata = 0x00;
        }

        datamsg[0] = 0x01;
        datamsg[1] = 0x2E;
        datamsg[2] = 0x00;
        datamsg[3] = 0x00;
        datamsg[4] = 0x00;
        datamsg[5] = 0x06;
        datamsg[6] = 0x00;
        datamsg[7] = (char)wid;
        datamsg[8] = (char)n_hadr;
        datamsg[9] = (char)n_ladr;
        datamsg[10] = (char)hdata;
        datamsg[11] = (char)ldata;
    }
}
           

我只贴了int型和bool型数据的读写报文生成,float型的我倒是也写了,但是有float型接口的机器人不在身边,测不了,所以这里就不贴了先,如果有需要,下面留言说一下,我机器上测好之后再贴上来。

然后是通过报文发送请求指令的代码:

bool MymodebusTcp::mymodbus_read(int wid, int address, QList<quint16> &backmsg)  //读取
{
    QByteArray datamsg;
    QByteArray back;
    readrequestData(wid, address, datamsg);
    if (datamsg.length() < 0)
    {
        qDebug() << "报文错误";
        return false;
    }

    QByteArray datamsg2;
    if (!tcp_write(datamsg, datamsg2))
    {
        qDebug() << "tcp写入错误";
        return false;
    }

//返回数据的处理
    QString strMessage = datamsg2.toHex();
    int len = strMessage.length();
    QVector<int> readM;
    for(int i=0;i<len/2;i++)
    {
        readM.push_back(strMessage.mid(2*i,2).toInt(nullptr,16));
    }

    for (int j = 9; j < readM.length(); j++)  //得到返回值主体部分(去掉报头7字节,功能码1字节,数量1字节)
    {
        backmsg.append(quint16(readM[j]));
    }
    return true;
}
           

float数据处理

出差把浮点数据的测试做好了,数据处理这部分也贴出来吧。

bool MymodebusTcp::mymodbus_read(int wid, int address, QList<float> &backmsg)  //支持底层地址连续读取
{
    QByteArray datamsg;
    QByteArray back;
    readrequestData(wid, address, datamsg);
    if (datamsg.length() < 0)
    {
        qDebug() << "报文错误";
        return false;
    }
    qDebug() << tr("mymodbustcp  wid=%1, address=%2").arg(wid).arg(address);

    QByteArray datamsg2;
    if (!tcp_write(datamsg, datamsg2))
    {
        return false;
    }

    QVector<QString> singledata;  //单个数据,双寄存器
    QVector<QString> singlereg;   //单个寄存器,双字节
    QVector<QString> readM;       //单个字节
    QByteArray m_byte;

    for (int i = 0; i < datamsg2.length()-9; i++)  //去除返回值报头,得到数据部分
    {
        m_byte[i] = datamsg2[i+9];
    }

    QString m_str = m_byte.toHex();
    QString temp;

    qDebug() << "m_str =  " << m_str;

    //区分单个数据,双寄存器
    for(int i = 0; i < m_str.length()/8; i++)
    {
        singledata.append(m_str.mid(i*8, 8));
        qDebug() << "singledata =  " << singledata.at(i);

        if (singledata.at(i).length() != 8)
        {
            qDebug() << "the " << i << " data is error!!!" ;
        }
    }

    //区分单个数据内的两个寄存器
    for (int i = 0; i < singledata.count(); i++)
    {
        for(int j = 0; j < singledata.at(i).length()/4; j++)
        {
            singlereg.append(singledata.at(i).mid(4*j, 4));
            if (singlereg.at(j).length() != 4)
            {
                qDebug() << "the " << j << " regisiter is error!!!" ;
            }
        }
        qDebug() << "singlereg =  " << singlereg;
    }

    //交换两个寄存器的数据(数据上来说,应该第二个寄存器在先)
    for (int i = 0; i < singlereg.count(); i++)
    {
        temp = singlereg.at(i);
        singlereg[i] = singlereg[i+1];
        singlereg[i+1] = temp;
        i++;
    }
    qDebug() << "changed singlereg =  " << singlereg;

    //得到每个具体的字节
    for (int i =0; i < singlereg.count(); i++)
    {
        for (int j = 0; j < singlereg.at(i).length()/2; j++)
        {
            readM.append(singlereg.at(i).mid(2*j,2));
        }
    }
    qDebug() << "readM =  " << readM;

    //数据转换: string ==》int ==》 char ==> float
    uchar tt[4];
    int len = readM.count();
    QString ns;
    int stoi;

    for (int i = 0,j; i <len;)
    {
        for (j = i; j < i+4; j++)
        {
            ns = "0x" + readM[j];
            stoi = ns.toInt(nullptr, 16);
            tt[j-i] = (char)stoi;
        }
        backmsg << get_float_from_4u8(tt);
        i += 4;
    }
    qDebug() << "mymodbustcp backmsg == " << backmsg;
    return true;
}

           

float读取的报文生成的函数我就不贴了,和之前的没什么区别,无非是读取数量变成两个寄存器,这里贴出来的是把循环读取写在外部,底层每次读一个的写法,但是运行效率来说,还是底层循环较快,所以有优化强迫症的兄弟,在报文生成的函数,加个表示读取数量的num变量,然后一层一层写上去就可以了,我自己也优化了一下,但是懒得再贴了,没多大变化。

write请求的发送我就不贴了,无非是返回值处理这边不一样,可以随便一点,反正写入的返回就是你发送过去的报文本身,也没必要读取,随便写个判错就ojbk了。

我这个函数,生成报文之后,直接调用了tcp_write,这个函数我也不贴了,因为不全是我写的,有部分是拷贝公司大佬以前写好的代码,反正就是tcp通讯的一个写入,socket–>write(……),读取返回值的 readAll() 我也在这个函数里实现了,所以贴出来的只有处理返回值的部分,最终返回的backmsg在外部是转bool还是保留int数据,看你们需要了。

总结

主要是贴出来tcp报文的生成和read返回值的解析两部分代码,tcp通讯部分,自力更生。如果需要搞通讯协议这块,建立你们下个抓包的wireShark(我自己的版本太低,懒得下新的,也没必要放出来了)和一个类似于串口调试助手的软件,用来模拟报文发送和数据接收,可以大大提高开发速度(我用的这玩意也是别人给的,不知道开源不开源,不敢随便放出来,要用的网上自己找找吧)。

附:

modbus协议中文版:https://pan.baidu.com/s/1VtDfP2B1S2heSvg4s_02PQ,提取码:d6lz

继续阅读