天天看點

基于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

繼續閱讀