概述
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