背景
近期才开始接触QT编程,以前都是使用MFC编写上位机软件,经常需要通过串口与RS485设备通讯。之所以开始学习QT,是因为看到网上有个人说自从接触了QT之后,就觉得MFC可以放弃,于是我也有了学习QT的冲动。
买了一本QT入门的书籍《Qt Creater快速入门》,简单学习了按钮等基本控件的使用、信号与槽、以及多线程的知识后,开始编写一个简单的串口程序与RS485设备通讯。
这本书上没有关于串口知识,不过好在现在的网络这么发达,随便一搜就有一大堆。
开始
(1)创建一个Qt widgets application应用,包含以下头文件:
#include <QtSerialPort/QtSerialPort>
#include <QList>
#include <QtSerialPort/QSerialPortInfo>
(2)界面上放一个下拉列表comboBox,使用下面几句代码可以获取电脑上可用的串口,并显示在comboBox控件中,简简单单几句话,就列出了所有串口,感觉好方便啊,瞬间觉得好喜欢。
QList<QSerialPortInfo> list = QSerialPortInfo::availablePorts();
foreach (const QSerialPortInfo & info, list) {
ui->comboBox->addItem(info.portName());
}
(3)界面上放一个下拉列表控件comBaud,添加几种波特率
ui->comBaud->addItem(tr("9600"));
ui->comBaud->addItem(tr("4800"));
ui->comBaud->addItem(tr("19200"));
(4)创建一个串口对象:
QSerialPort port;
(5)界面上放置一个按钮openCom,用于打开串口,转到槽添加如下代码,
void Widget::on_openCom_clicked()
{
QString name = ui->openCom->text();//获取按钮上的文字
if(name == tr("打开串口"))
{
port.setPortName(ui->comboBox->currentText());//选择串口号
if( !port.open(QIODevice::ReadWrite))//打开串口
{
QMessageBox::information(this, tr("提示"),
tr("打开串口失败,请查看串口是否被占用"), QMessageBox::Ok);
return;
}
ui->openCom->setText(tr("关闭串口"));//改变按钮上的文字
port.setBaudRate(ui->comBaud->currentText().toInt());
port.setDataBits(QSerialPort::Data8);//8位数据位
port.setParity(QSerialPort::NoParity);//无检验
port.setStopBits(QSerialPort::OneStop);//1位停止位
port.setFlowControl(QSerialPort::NoFlowControl);//无硬件控制
ui->comboBox->setDisabled(true);//串口号下拉列表变灰
ui->comBaud->setDisabled(true);//波特率下接列表变灰
}else
{
port.close();//关闭串口
ui->comboBox->setEnabled(true);//串口号下拉列表变亮
ui->comBaud->setEnabled(true);//串口号下拉列表变亮
ui->openCom->setText(tr("打开串口"));
}
}
(6)添加一个按钮detect,用于点击之后开始与RS485设备通信,添加一个列表控件listWidget,用于显示通讯结果,转到槽添加如下代码
void Widget::on_detect_clicked()
{
if(!port.isOpen())
{
QMessageBox::critical(this, tr("错误"),
tr("请先打开串口"), QMessageBox::Ok);
return;
}
ui->listWidget->clear();//清空列表内容
MyThread *th = new MyThread(this, &port);//启动线程,在线程内通讯
th->start();
}
(7)添加一个C++类,类名MyThread,继承自QThread,添加一个信号sendResult,用于将通讯结果反馈给主程(暂时这么干吧,毕竟我才刚入门)。
mythread.h文件:
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <widget.h>
#include <QtSerialPort/QtSerialPort>
class MyThread : public QThread
{
Q_OBJECT
public:
MyThread(Widget * parent, QSerialPort *port);
protected:
void run();
protected:
QSerialPort * port;
Widget * parent;
signals:
void sendResult(int code, QString str);
};
#endif // MYTHREAD_H
mythread.cpp文件:
#include "mythread.h"
MyThread::MyThread(Widget * parent, QSerialPort *port)
{
this->parent = parent;//传入父对象,以便给它发信号
this->port = port;//传入串口对象,以便在线程中使用
}
//CRC校验码计算
unsigned short CRC16(unsigned char *pBuf,unsigned short len)
{
unsigned short uCRC=0xFFFF;
int i,j;
for(i=0;i<len;i++)
{
uCRC^=(*(pBuf+i));
for(j=0;j<8;j++)
{
if((uCRC&0x0001)==0x0001)
{
uCRC=(uCRC>>1);
uCRC^=0xA001;
}
else
uCRC=(uCRC>>1);
}
}
return uCRC;
}
void MyThread::run()
{
bool bRet = false;
unsigned char buf[6];
//关联信号与槽,用于给父对象信号,告知通讯结果
QObject::connect(this, SIGNAL(sendResult(int, QString)),
parent, SLOT(result(int, QString)));
//与43个RS485设备通讯,检查设备是否有回应
for(unsigned char addr = 1; addr <= 43 ; ++addr)
{
buf[0] = addr;
buf[1] = 0x03;
buf[2] = 0xA0;
buf[3] = 0x06;
unsigned short crc = CRC16(buf, 4);
buf[4] = static_cast<unsigned char>(crc);
buf[5] = static_cast<unsigned char>(crc >> 8);
port->write(reinterpret_cast<const char *>(buf), 6);//发送数据
port->flush();//让数据立刻从串口发送出去,不要在缓冲区里墨迹
msleep(200);//等待200ms
//检查串口接收到了多少数据
int num = static_cast<int>(port->bytesAvailable());
if(num != 6)
{
QString str = QString().sprintf("addr=%02d,设备没有响应, 返回数据量为%d",
addr, static_cast<int>(num));
emit sendResult(0, str);
}else
{
// 接收到了6个字节,表时RS485设备有响应
QByteArray array = port->readAll();
QString str = QString().sprintf("addr=%02d, 发现设备,返回数据量为%d",
addr, static_cast<int>(num));
emit sendResult(0, str);
}
msleep(800);
}
}
(8)测试结果如下:43个设备除41号设备外(该设备RS485通讯线没有连接)都有回应,完成了预期目标。
小结
(1)往串口写数据的时候,write()后面要加一句flush()才能把数据立刻通过串口发送出去。
(2)刚开始用的是异步读取数据方式,感觉怪怪的,因为发送数据和接收数据不在同一个函数里,而且readyRead信号槽中接收到的数据经常不完整,往往要多次拼接数据,这对于RS485通讯来说很不好操作,后来改用同步操作,使用waitForReadyRead()来等待响应数据,结果在waitForReadyRead()函数这里折腾了小半天,根据手册,waitForReadyRead()返回值为true时表明已经接收到数据了,返回false表示接收超时,结果明明有接收到数据,却还是会超时返回false,造成先是readyRead信号槽提示接收到数据,然后是waitForReadyRead()返回false提示接收超时。后来进到QIODevice::waitForReadyRead()中查看,里面说到使用connect关联了readyRead信号,那么waitForReadyRead()将不会接收到readyReady信号返回true,这下明白了,把connect信号的那句注释掉,waitForReadyRead()果然返回了true,于是就有了上面的程序,先发送数据,等等待一个固定时间再读数据,不要用waitForReadyRead()来等待响应数据,因为它接收到的数据不完整。