modbus作为工业通用协议,应用极广且非常成熟,大部分的编译器支持modbus并会封装成模块供使用者调用,我自己用的是QT,本身也是有一个seriousbus的模块,专门封装了modbus的相关函数,调用起来非常方便,但是,在QT5.12之后 ,seriousbus模块从QT自带的android编译器中移除了。而我恰好得写一个用在android系统里的通讯项目,这就很尴尬了。。。虽然QT提供了 qseriousbus-everywhere-master的外部库,但是我并不会安装(有会装这玩意的能不能留言给个教程链接)。所以只能自己写个Tcp和上位机通讯,然后按照modbus的格式,进行数据交互。
用QT建立tcp的client非常简单,网上资源多得是,我这里就不贴代码了,端口号502,ip自己配好,测一下能连上,就ok了。这里主要讲一下,报文的生成和数据的解析。
tcp连接建立之后,要进行数据通讯,就必须发送符合modbu格式的报文给上位机,这里用的是modbusTcp协议,不需要Rtu协议的CRC校验部分,报文格式和详解网上资源很多,这里简单提一下,最下面会给一个《modbus协议详解中文版》(淘宝上花钱在csdn里代下载的,免费放出来,别和我说版权,这也不是某些人自己敲出来的,本人一直坚持开源共享,希望可以共同进步,没积分在csdn上下载的,可以来我这里)。
简单来说,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;
}
出差把浮点数据的测试做好了,数据处理这部分也贴出来吧。
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