有一个项目需要PC和PLC通信,PLC通信协议是Modbus协议。前两天研究了一下,QT源码也有例程,不过源码读了有点懵,参考了别人的博客,实现了一个简单的通信Demo,测试可以对PLC内部寄存器和中间继电器读写。
QT:5.12.12
Kits:MSVC2017
PLC:信捷XD5
软件预览:
QT += core gui serialbus serialport
主要对串口的参数进行配置,包括串口号、波特率、数据位、停止位、校验位、超时时间和重试次数。刷新按钮实现对PC串口的检测,使用QSerialPortInfo::availablePorts();获取串口信息,函数放到初始化中。
QList<QSerialPortInfo> port_list = QSerialPortInfo::availablePorts();
ui->comboBox_com->clear();
foreach(const QSerialPortInfo & info,port_list)
{
// qDebug() << info.portName(); //串口号 COM1 COM2-----
// qDebug() << info.systemLocation(); //串口存在的系统位置是个路径
// qDebug() << info.description();//返回串口描述字符串(如果可用);否则返回空字符串
// qDebug() << info.manufacturer();//返回串口制造商字符串(如果可用);否则返回空字符串
// qDebug() << info.serialNumber();//返回串口序列号字符串(如果可用);否则返回空字符串
ui->comboBox_com->addItem(info.portName());
}
用结构体存储串口设置参数
//串口参数
struct Settings {
QString serialPort;
int parity;
int baud;
int dataBits;
int stopBits;
int responseTime;
int numberOfRetries;
};
每次打开串口时重新刷新参数
//初始化串口参数信息
m_settings.serialPort = ui->comboBox_com->currentText();
m_settings.parity = ui->comboBox_parity->currentIndex();
if (m_settings.parity > 0)
m_settings.parity++;
m_settings.baud = ui->comboBox_baud->currentText().toInt();
m_settings.dataBits = ui->comboBox_databits->currentText().toInt();
m_settings.stopBits = ui->comboBox_stopbits->currentText().toInt();
m_settings.responseTime = ui->spinBox_timeout->value();
m_settings.numberOfRetries = ui->spinBox_retries->value();
先获取设置的串口参数,然后实例化串口设备对象,这里使用串口线连接,使用QModbusRtuSerialMaster,最后设置参数连接串口,连接成功设置按钮状态。
//获取串口数据
getComParameter();
if (modbusDevice)
{
modbusDevice->disconnectDevice();
delete modbusDevice;
modbusDevice = nullptr;
}
modbusDevice = new QModbusRtuSerialMaster(this);
connect(modbusDevice, &QModbusClient::errorOccurred, [this](QModbusDevice::Error) {
qDebug() << "modbus Error:" << modbusDevice->errorString();
});
//配置串口参数
modbusDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,m_settings.serialPort);
modbusDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,m_settings.parity);
modbusDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,m_settings.baud);
modbusDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,m_settings.dataBits);
modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,m_settings.stopBits);
modbusDevice->setTimeout(m_settings.responseTime); // 配置请求超时时间
modbusDevice->setNumberOfRetries(m_settings.numberOfRetries); // 配置失败重试次数
if(!modbusDevice->connectDevice())
{
qDebug()<<tr("Connect failed: %1").arg(modbusDevice->errorString());
}
else
{
qDebug() << "Modbus open Success!";
ui->pushButton_openCom->setEnabled(false);
ui->pushButton_closeCom->setEnabled(true);
ui->pushButton_refreshCom->setEnabled(false);
ui->comboBox_com->setEnabled(false);
ui->comboBox_baud->setEnabled(false);
ui->comboBox_databits->setEnabled(false);
ui->comboBox_stopbits->setEnabled(false);
ui->comboBox_parity->setEnabled(false);
ui->spinBox_timeout->setEnabled(false);
ui->spinBox_retries->setEnabled(false);
}
断开串口连接,析构串口设备对象,设置按钮状态。
if (!modbusDevice)
return;
modbusDevice->disconnectDevice();
delete modbusDevice;
modbusDevice = nullptr;
qDebug() << "Modbus close Success!";
ui->pushButton_openCom->setEnabled(true);
ui->pushButton_closeCom->setEnabled(false);
ui->pushButton_refreshCom->setEnabled(true);
ui->comboBox_com->setEnabled(true);
ui->comboBox_baud->setEnabled(true);
ui->comboBox_databits->setEnabled(true);
ui->comboBox_stopbits->setEnabled(true);
ui->comboBox_parity->setEnabled(true);
ui->spinBox_timeout->setEnabled(true);
ui->spinBox_retries->setEnabled(true);
设置一个下拉框来选择读写的数据存储类型,例如线圈或者寄存器。读写数据的时候按照需要选择
这里只用测试了Coils和Holding Registers,其他两个没有测试。
//值类型
ui->comboBox_valueType->addItem(tr("Coils"), QModbusDataUnit::Coils);
ui->comboBox_valueType->addItem(tr("Discrete Inputs"), QModbusDataUnit::DiscreteInputs);
ui->comboBox_valueType->addItem(tr("Input Registers"), QModbusDataUnit::InputRegisters);
ui->comboBox_valueType->addItem(tr("Holding Registers"), QModbusDataUnit::HoldingRegisters);
写串口数据需要对方的设备id,和要写入的寄存器或线圈地址,和写入的数据,
代码支持同时写入多个连续位置,数据按照空格区分,写入的数据是十进制的。对于进制我没有过于追究。
if (!modbusDevice)
{
QMessageBox::information(NULL, "提示", "请先连接设备");
return;
}
//获取要写入的寄存器数据
QList<quint16> values;
QStringList values_list = ui->lineEdit_writeValue->text().split(" ");
for(int i = 0 ; i < values_list.size(); i++)
{
values.append(values_list.at(i).toUInt());
}
int id = ui->lineEdit_id->text().toInt(); //设备地址
int addr = ui->lineEdit_addr->text().toInt(); //寄存器地址
//组合写数据帧 table写入的数据类型 寄存器或线圈
const auto table =
static_cast<QModbusDataUnit::RegisterType> (ui->comboBox_valueType->currentData().toInt());
QModbusDataUnit writeUnit = QModbusDataUnit(table,
addr, values.size());
for(int i=0; i<values.size(); i++)
{
writeUnit.setValue(i, values.at(i));
}
//id 发生给slave的ID
if (auto *reply = modbusDevice->sendWriteRequest(writeUnit,id))
{
if (!reply->isFinished())
{
connect(reply, &QModbusReply::finished, this, [this, reply]()
{
if (reply->error() == QModbusDevice::ProtocolError)
{
qDebug() << QString("Write response error: %1 (Mobus exception: 0x%2)")
.arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
}
else if (reply->error() != QModbusDevice::NoError)
{
qDebug() << QString("Write response error: %1 (code: 0x%2)").
arg(reply->errorString()).arg(reply->error(), -1, 16);
}
reply->deleteLater();
});
}
else
{
reply->deleteLater();
}
}
else
{
qDebug() << QString(("Write error: ") + modbusDevice->errorString());
}
读串口数据需要对方的设备id,和要读取的寄存器或线圈地址,读取的个数,支持读取连续的多个寄存器或线圈,读取数值按照空格区分。
读数据要先发送数据帧给从机,然后等待从机返回数据。
if (!modbusDevice)
{
QMessageBox::information(NULL, "提示", "请先连接设备");
return;
}
//清除读窗口信息
ui->lineEdit_readValue->clear();
//获取设备信息
int id = ui->lineEdit_id->text().toInt(); //设备地址
int addr = ui->lineEdit_addr->text().toInt(); //寄存器地址
int readNum = ui->lineEdit_readNum->text().toInt(); //读取寄存器个数
//组合写数据帧 table写入的数据类型 寄存器或线圈
const auto table =
static_cast<QModbusDataUnit::RegisterType> (ui->comboBox_valueType->currentData().toInt());
QModbusDataUnit readUint = QModbusDataUnit(table,
addr, readNum);
//读取数据
if (auto *reply = modbusDevice->sendReadRequest(readUint, id))
{
if (!reply->isFinished())
connect(reply, &QModbusReply::finished, this, &Widget::readReady);
else
delete reply;
}
else
{
qDebug() << "Read error: " << modbusDevice->errorString();
}
在槽函数readReady();获取读到的数据。
auto reply = qobject_cast<QModbusReply *>(sender());
if (!reply)
return;
if (reply->error() == QModbusDevice::NoError)
{
const QModbusDataUnit unit = reply->result();
if(unit.valueCount() == ui->lineEdit_readNum->text().toUInt())
{
QString send_buff;
for (uint i = 0; i < unit.valueCount(); i++)
{
const QString entry = tr("Address: %1, Value: %2").arg(unit.startAddress() + i)
.arg(QString::number(unit.value(i),
unit.registerType() <= QModbusDataUnit::Coils ? 10 : 16));
send_buff.append(QString::number(unit.value(i),
unit.registerType() <= QModbusDataUnit::Coils ? 10 : 16) + " ");
}
//读取的数据
ui->lineEdit_readValue->insert(send_buff);
}
}
else if (reply->error() == QModbusDevice::ProtocolError)
{
qDebug() << QString("Read response error: %1 (Mobus exception: 0x%2)").
arg(reply->errorString()).
arg(reply->rawResult().exceptionCode(), -1, 16);
}
else
{
qDebug() << QString("Read response error: %1 (code: 0x%2)").
arg(reply->errorString()).
arg(reply->error(), -1, 16);
}
reply->deleteLater();
代码发送数据帧和接收数据帧是没有做显示,这里参考通过重定向打印功能将将我们需要的数据显示到缓冲区显示。使用QLoggingCategory
这部分没什么用,我也没有研究过,就不做说明。
代码参考自:https://blog.csdn.net/qq_43581670/article/details/128466920
我代码我放到github上了需要自取,基于QT5版本
链接:https://github.com/Mr-zhao765/Qt-Project
以上就是这个项目的介绍了,逻辑比较简单代码也比较少,我注释也比较多,相比于源码比较容易理解多了。
个人比较懒不爱总结!第一次写博客,加油!