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