目录
一、Modbus协议通信过程
1.1 主机对从机写数据操作(0x06)
1.2 主机对从机读数据操作(0x03)
1.3 Modbus的CRC校验
二、在Qt中使用QModbus读写数据
2.1 部署准备——头文件中(.h)
2.2 部署准备——cpp文件中
2.3 modbus的使用——写函数
2.4 modbus的使用——读数据
2.5 额外——涉及的数据类型说明
Modbus能实现的功能较多,理解了如下2个操作,基本算掌握了Modbus 80%的使用了。在此基础上,能很快扩展到其他功能使用上。
这个功能的目的是:实现主机对从机 寄存器 的写入操作。
注意:是 主机 对 从机
说明:假如从机地址是 1,那么单片机接收到这串数据,根据CRC16进行校验判断数据是否正确,如果数据无误,就改变自己寄存器的值。
然后从机原封不动返回这句话,告诉主机通讯成功!!!
*****下面用 串口调试助手XCOM V2.0作为主机,Modbus Slave软件作为从机 ,通过串口数据监视软件AccessPort 捕获通讯中的数据,过程如下:
如下图所示,发送数据01 06 00 04 00 07 89 C9,接收到从机返回数据01 06 00 04 00 07 89 C9。说明通讯成功。
从串口数据监视软件AccessPort里也可以看出,如下图中红色框中所示,
对主机而言,发送01 06 00 04 00 07 89 C9后,接收了01 06 00 04 00 07 89 C9;
对从机而言,接收01 06 00 04 00 07 89 C9后,发送了01 06 00 04 00 07 89 C9。
这个功能的目的是:实现主机对从机 寄存器 的值的读操作。
注意:是 主机 对 从机
说明:假如从机地址是 1,那么单片机接收到这串数据,根据CRC16进行校验判断数据是否正确,如果数据无误,就发送相应的寄存器的值给主机!!!
注意:这里的数据字节个数,在主机下发数据那里可以变更,单位是不同的——对主机端,一个数据个数对应从机端两个字节的数据。
*****下面用 串口调试助手XCOM V2.0作为主机,Modbus Slave软件作为从机 ,通过串口数据监视软件AccessPort 捕获通讯中的数据,过程如下:
如下图所示,串口调试助手作为主机发送01 03 00 00 00 0A C5 CD,Modbus Slave作为从机,接收到主机的读信息后,发送01 03 14 78 67 00 00 00 05 00 00 00 00 00 00 00 06 00 00 00 00 00 08 80 AC作为回应。
主机端要求从从机0x01的寄存器0x0000开始读取10个数据;从机端接收到命令后,从寄存器0x0000,读取10个数据共20个字节,发送给主机。
从串口数据监视软件AccessPort里也可以看出,如下图中红色框中所示,
对主机而言,发送01 03 00 00 00 0A C5 CD后,接收了01 03 14 78 67 00 00 00 05 00 00 00 00 00 00 00 06 00 00 00 00 00 08 80 AC;
对从机而言,接收01 03 00 00 00 0A C5 CD后,发送了01 03 14 78 67 00 00 00 05 00 00 00 00 00 00 00 06 00 00 00 00 00 08 80 AC。
Modbus的CRC校验使用的是CRC16,输出两个字节校验结果。这个过程了解即可,Qt自带算CRC的算法。
CRC16校验的查表法 C++算法如下:
//CRC16校验算法 系数表
uchar auchCRCHi[]=
{
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40
};
uchar auchCRCLo[] =
{
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40
};
//查表法:Modbus的16位CRC校验
uint ModbusTool::N_CRC16(uchar *updata,uint len)
{
uchar uchCRCHi=0xff;
uchar uchCRCLo=0xff;
uint uindex;
while(len–)
{
uindex=uchCRCHi^*updata++;
uchCRCHi=uchCRCLo^auchCRCHi[uindex];
uchCRCLo=auchCRCLo[uindex];
}
return (uchCRCHi<<8|uchCRCLo);
}
我的软件环境作为参考,win7,win10下都可以。
需要包含如下头文件和QModbus类,至于定义Modbus对象,可以在头文件中,也可以在cpp中。
#include
#include
#include
class QModbusClient;
class QModbusReply;
//类中定义,我的类名叫ModbusTool
private:
QModbusClient *modbustoolDevice;//Modbus对象
private slots:
void readReady();//读下位机寄存器数据回调函数
public:
void write2client(unsigned int regStartAddr,unsigned int number);//上位机向下位机的某个寄存器写入数据
void readfromclient(unsigned int startAddress,int number);//从指定下位机寄存器处读取数据
定义modbus串口的打开和关闭函数
//当然,相应的.cpp和.h文件需要对应起来哈,别傻傻的不同的.h和.cpp中写入代码了
//再包含一个头文件
#include
//实例化Modbus对象
modbustoolDevice = new QModbusRtuSerialMaster(this);
//连接串口,这里是一个按钮
void ModbusTool::on_modbus_connect_clicked()
{
//判断modbus是否已经定义,没有则直接返回
if (!modbustoolDevice)
return;
//判断modbus是否已经连接
if (modbustoolDevice->state() != QModbusDevice::ConnectedState) {
//如果没有连接,则先设置串口的名称,串口奇偶校验位,串口波特率,串口数据位,串口停止位
modbustoolDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,
ui->modbus_searialport_name->currentText());
modbustoolDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,
QSerialPort::EvenParity);
modbustoolDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,
ui->modbus_searialport_btl->currentText().toInt());
modbustoolDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,
QSerialPort::Data8);
modbustoolDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,
QSerialPort::OneStop);
//再设置从机无响应时的动作
modbustoolDevice->setTimeout(1000);//从设备回复信息的超时时间
modbustoolDevice->setNumberOfRetries(2);//重复发送次数
//开始连接串口,并判断连接状态
if (!modbustoolDevice->connectDevice()) {
//如果连接失败,提示
statusBar()->showMessage(tr("连接失败: ") + modbustoolDevice->errorString(), 5000);
//FlagGetStatus=0;//连接失败,停止定时刷新
//ui->Status_SerialConnect->setStyleSheet("font-size:100px;border-radius:30px;\nbackground-color: red");
}else{
//如果连接成功,设置下面按钮的状态,同时给出提示
ui->modbus_connect->setEnabled(false);
ui->modbus_disconnect->setEnabled(true);
ui->send_commander->setEnabled(true);
statusBar()->showMessage(tr("连接成功!"), 5000);
//FlagGetStatus=1;//连接成功,定时刷新
//ui->Status_SerialConnect->setStyleSheet("font-size:100px;border-radius:30px;\nbackground-color: green");
}
} else {
//如果已经连接,则关闭串口
modbustoolDevice->disconnectDevice();
//ui->Status_SerialConnect->setStyleSheet("font-size:100px;border-radius:30px;\nbackground-color: rgb(205, 205, 205)");
}
}
//断开串口连接
void ModbusTool::on_modbus_disconnect_clicked()
{
//关闭串口
modbustoolDevice->disconnectDevice();
//设置下面按钮的状态,同时给出提示
ui->modbus_connect->setEnabled(true);
ui->modbus_disconnect->setEnabled(false);
ui->send_commander->setEnabled(false);
statusBar()->showMessage(tr("断开连接成功!"), 5000);
}
只需要这一个函数就能完成写操作,明确写入寄存器的起始地址,以及写入的数据,调用就能完成写操作。
/* * @FuncName 上位机向下位机的某个寄存器写入数据 * * @Function 向指定下位机寄存器写入数据 * * @param 参数a 下位机寄存器起始地址 * @param 参数b 写入值 * * @return 返回值无 */ void ModbusTool::write2client(unsigned int regStartAddr,unsigned int number) { //如果没有定义modbus串口,直接返回 if (!modbustoolDevice) { statusBar()->showMessage(tr("Modbus通讯失败,请检查串口连接!"),5000); return; }
//清除状态栏显示 statusBar()->clearMessage(); //从页面的输入框获取设备地址 unsigned int serverAddress=ui->modbus_addr->text().toInt(); if(serverAddress==0) { statusBar()->showMessage(tr("设备地址为空,写入失败!"),5000); return; } //定义Modbus的基本数据单元,这里设置了modbus为0x06写模式,寄存器地址,以及传输数据的大小 //QModbusDataUnit::QModbusDataUnit(RegisterType type, int address, quint16 size) QModbusDataUnit writeUnit(QModbusDataUnit::HoldingRegisters,regStartAddr,1);//06 //QModbusDataUnit writeUnit(QModbusDataUnit::Coils,regStartAddr,1);//05 //传输的数据为0,写入数据单元,这里还在做数据准备 //number = 0x08; //void QModbusDataUnit::setValue(int index, quint16 value) writeUnit.setValue(0,number); //发送Modbus格式的数据到从机地址为serverAddress的从机 //sendWriteRequest(const QModbusDataUnit &write, int serverAddress) if (auto *reply = modbustoolDevice->sendWriteRequest(writeUnit, serverAddress)) { if (!reply->isFinished()) { //如果接收到响应信息 connect(reply, &QModbusReply::finished, this, [this, reply]() { if (reply->error() == QModbusDevice::ProtocolError) { //接收到的响应信息有协议错误,则更新状态栏,错误码 statusBar()->showMessage(tr("写入数据错误: %1 (Mobus exception: 0x%2)") .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16), 5000); } else if (reply->error() != QModbusDevice::NoError) { //接收到的响应信息有其他错误,则更新状态栏,错误码 statusBar()->showMessage(tr("写入数据错误: %1 (code: 0x%2)"). arg(reply->errorString()).arg(reply->error(), -1, 16), 5000); }else{ //接收到的响应信息没有任何问题,更新状态栏 //注意,注意,注意,注意,注意,注意:这里解析了回应的信息,具体方法如下。一般没必要 //获取响应信息的数据到modbus基本数据QModbusDataUnit const QModbusDataUnit unit = reply->result(); //统计响应信息中数据的个数 QString showdata=tr("总数:%1 值为:").arg(unit.valueCount()); //依次展示响应信息中的数据 for (uint i = 0; i < unit.valueCount(); i++) { const QString entry=tr("%1 ").arg(unit.value(i)); showdata=showdata+entry; } //显示在界面中 ui->recv_area->setText(showdata); } reply->deleteLater(); }); } else { //发送多次后,没有响应数据,停止发送,更新状态栏信息 // broadcast replies return immediately reply->deleteLater(); } } else { //发送没有成功,更新状态栏信息 statusBar()->showMessage(tr("写入数据错误: ") + modbustoolDevice->errorString(), 5000); }
}
读操作包含两个函数。一个是发送读数据命令的函数,一个是处理读取的数据的函数。
/* * @FuncName 函数名:从指定下位机寄存器处读取数据 * * @Function 功能:读取下位机数据 * * @param 参数a 下位机寄存器起始地址 * * @return 返回值无 */ void ModbusTool::readfromclient(unsigned int startAddress,int number) { //如果没有定义modbus串口,直接返回 if (!modbustoolDevice) return; // ui->readValue->clear(); //清空状态栏的显示 statusBar()->clearMessage();
//定义Modbus的基本数据单元,这里设置了modbus为0x03读模式,寄存器地址,以及传输数据的大小 //QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters,startAddress,num); QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters,startAddress,number); //发送Modbus格式的数据到从机地址为serverAddress的从机,读取相应的数据 //if(auto* reply = Master->sendReadRequest(readUnit,ServerAddress)) //获取设备地址 unsigned int serverAddress=ui->modbus_addr->text().toInt(); if (auto *reply = modbustoolDevice->sendReadRequest(readUnit, serverAddress)) { if (!reply->isFinished()) //如果有响应信息了,进入readReady函数处理接收到的数据 connect(reply, &QModbusReply::finished, this, &ModbusTool::readReady); else //如果多次发送后没有响应,结束 delete reply; // broadcast replies return immediately } else { //如果发送失败,更新状态栏信息 statusBar()->showMessage(tr("读取失败: ") + modbustoolDevice->errorString(), 5000); }
}
/*
@FuncName 函数名:读下位机寄存器数据回调函数
@Function 功能:解析下位机上传的数据
@param 参数无
@return 返回值无
*/
void ModbusTool::readReady()
{
auto reply = qobject_cast
//如果没有响应数据,直接返回
if (!reply)
return;
if (reply->error() == QModbusDevice::NoError) {
//如果响应数据校验后,没有错误。解析数据
const QModbusDataUnit unit = reply->result();
QString showdata=tr("总数:%1 值为:").arg(unit.valueCount());
for (uint i = 0; i < unit.valueCount(); i++) {
const QString entry=tr("%1 ").arg(unit.value(i));
showdata=showdata+entry;
}
ui->recv_area->setText(showdata);
} else if (reply->error() == QModbusDevice::ProtocolError) {
//如果响应数据校验后,有协议错误,更新状态栏显示错误码
statusBar()->showMessage(tr("读取回应错误: %1 (Mobus exception: 0x%2)").
arg(reply->errorString()).
arg(reply->rawResult().exceptionCode(), -1, 16), 5000);
} else {
//如果响应数据校验后,有错误,更新状态栏显示错误码
statusBar()->showMessage(tr("读取回应错误: %1 (code: 0x%2)").
arg(reply->errorString()).
arg(reply->error(), -1, 16), 5000);
}
//结束响应过程
reply->deleteLater();
}
const QModbusDataUnit unit = reply->result();
这个代表接收到的响应信息的数据部分。
使用unit.value(i)依次读取就能得到各个数据。