本篇将总结Qt的串口编程。Qt4中并没有串口模块支持,但自Qt5.1开始,官方加入了serialport模块,使得我们可以轻松使用该模块实现跨平台串口操作。
概述
Qt Serial Port提供了基本的串口操作接口函数。包括配置,I/O操作,获取和设置RS-232控制管脚信号等。使用时需要添加模块支持并在调用相关接口的文件中添加头文件支持:
QT += serialport
#include <QtSerialPort/QtSerialPort>
涉及到的类主要由两个:
类 |
描述 |
QSerialPort |
Provides functions to access serial ports |
QSerialPortInfo |
Provides information about existing serial ports |
串口操作
打开串口的操作需要设置串口名字,设置波特率以及一些其他配置。这些配置也可以通过QSerialPortInfo类设置。然后调用open函数即可。QSerialPort::ReadWrite显示了该串口可读写,类似的控制还可以有read-only (r/o), write-only (w/o)。需要注意的是,串口只能被打开一次,也即无法打开已经被打开的串口,所以,最好在open之前判断其是否被打开。
QSerialPort *m_serialPort; if (m_serialPort->isOpen())
{
m_serialPort->clear();
m_serialPort->close();
}
m_serialPort->setPortName("COM1");
m_serialPort->setBaudRate(QSerialPort::Baud115200);
m_serialPort->setStopBits(QSerialPort::OneStop);
m_serialPort->setDataBits(QSerialPort::Data8);
m_serialPort->setParity(QSerialPort::NoParity);
m_serialPort->setFlowControl(QSerialPort::NoFlowControl);
m_serialPort->setReadBufferSize(1024);
if (m_serialPort->open(QSerialPort::ReadWrite))
{
qDebug() << "COM1 SeriPort open sucess!";
return true;
}
else
{
qDebug() << " COM1 SeriPort open failed!";
return false;
}
一旦串口设置完成并且已经被打开,那么就可以进行数据读写了。由于QSerialPort类继承于QIODevice类,所以IO操作的接口函数适用于串口类。可以通过调用read() or write()函数分别读写,也可以调用readLine() and readAll()来读取。在读取时可以调用waitForReadyRead()接口以等待有新的数据可读取:
int numRead = 0, numReadTotal = 0;
char buffer[50];
for (;;) {
numRead = serial.read(buffer, 50);
// Do whatever with the array
numReadTotal += numRead;
if (numRead == 0 && !serial.waitForReadyRead())
break;
}
如果waitForReadyRead返回false,表示串口已断开或者出现了某些错误。同样可以在发送数据时调用waitForBytesWritten()函数以等待数据发送。
int numSend = 0;
char buffer[50];
numSend = serial.write(buffer, 50);
if (numSend == 0 && !serial. waitForBytesWritten ())
return;
当串口发生错误时,会触发errorOccurred()信号,我们可以响应该信号,调用error()接口获取具体错误信息。
Constant |
Value |
Description |
QSerialPort::NoError |
0 |
No error occurred. |
QSerialPort::DeviceNotFoundError |
1 |
An error occurred while attempting to open an non-existing device. |
QSerialPort::PermissionError |
2 |
An error occurred while attempting to open an already opened device by another process or a user not having enough permission and credentials to open. |
QSerialPort::OpenError |
3 |
An error occurred while attempting to open an already opened device in this object. |
QSerialPort::NotOpenError |
13 |
This error occurs when an operation is executed that can only be successfully performed if the device is open. This value was introduced in QtSerialPort 5.2. |
QSerialPort::ParityError |
4 |
Parity error detected by the hardware while reading data. This value is obsolete. We strongly advise against using it in new code. |
QSerialPort::FramingError |
5 |
Framing error detected by the hardware while reading data. This value is obsolete. We strongly advise against using it in new code. |
QSerialPort::BreakConditionError |
6 |
Break condition detected by the hardware on the input line. This value is obsolete. We strongly advise against using it in new code. |
QSerialPort::WriteError |
7 |
An I/O error occurred while writing the data. |
QSerialPort::ReadError |
8 |
An I/O error occurred while reading the data. |
QSerialPort::ResourceError |
9 |
An I/O error occurred when a resource becomes unavailable, e.g. when the device is unexpectedly removed from the system. |
QSerialPort::UnsupportedOperationError |
10 |
The requested device operation is not supported or prohibited by the running operating system. |
QSerialPort::TimeoutError |
12 |
A timeout error occurred. This value was introduced in QtSerialPort 5.2. |
QSerialPort::UnknownError |
11 |
An unidentified error occurred. |
另外,Qt发送的数据也可以用QTextStream and QDataStream的流对象进行,这意味着可以用过operator<<() and operator>>()进行赋值。
阻塞与非阻塞
上述调用了waitForReadyRead和waitForBytesWritten ()接口的IO操作是阻塞的编程形式。我们也可以不调用这两个函数,直接调用read和write,通过QIODevice::readyRead,QIODevice::bytesWritten信号检测状态,这种方式为非阻塞方式。
阻塞的串口编程和非阻塞的串口编程有较大的不同。非阻塞的串口不需要单独的线程,只需要在主线程中new串口对象,用信号和槽关联读取数据connect(serial, &QSerialPort::readyRead, this, &MainWindow::readData),读取和发送数据只需要短短的一句话:QByteArray data = serial->readAll()和serial->write(data)。如果你要开发一个GUI图形用户界面的程序,阻塞的串口编程必须在一个非UI线程中使用,来避免阻塞界面反应。这涉及Qt多线程编程,这里先给出简单的示例,Qt多线程的知识将在后续文章中总结。
在实际工程应用中发现串口与网络不同,粘包现象比较严重,大多需要根据协议进行组包和截包处理。为了避免数据丢失,我个人建议采用阻塞的方式收发数据。若在非阻塞中发送数据,建议在允许的前提下加上一定延时。
下面给出一个阻塞用法,但是为了不影响GUI使用,我们采用了多线程技术:
//mater.h
class MasterThread : public QThread
{
Q_OBJECT
public:
explicit MasterThread(QObject *parent = nullptr);
~MasterThread();
void transaction(const QString &portName, int waitTimeout, const QString &request);
void run() Q_DECL_OVERRIDE;
signals:
void response(const QString &s);
void error(const QString &s);
void timeout(const QString &s);
private:
QString portName;
QString request;
int waitTimeout;
QMutex mutex;
QWaitCondition cond;
bool quit;
};
这是一个串口收发中的Master主机模块,它将向slave端发送数据,并接收slave上传的数据。设计将串口收发放进了独立线程里,即由QThread派生MasterThread类。transaction()函数用以启动一个新的串口访问通道。response()信号将回送数据。error() or timeout()将在错误发生时触发。由于transaction()将在UI的线程中调用,但是response()是在子线程中发送的,MasterThread类的成员变量将会在两个不同的线程中读写,可能会出现同步问题。因此,我们用QMutex类实现数据保护。(具体见后面的文章总结)
void MasterThread::transaction(const QString &portName, int waitTimeout, const QString &request)
{
QMutexLocker locker(&mutex);
this->portName = portName;
this->waitTimeout = waitTimeout;
this->request = request;
if (!isRunning())
start();
else
cond.wakeOne();
}
transaction()函数保存了串口的名字,超时时间和请求request数据。mutex可以通过QMutexLocker保护它们。然后调用start()函数线程开始工作,即调用run函数。
void MasterThread::run()
{
bool currentPortNameChanged = false;
mutex.lock();
QString currentPortName;
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
}
int currentWaitTimeout = waitTimeout;
QString currentRequest = request;
mutex.unlock();
在run函数中,首先lock QMutex对象,然后获取类的成员变量:串口名称,超时和request数据,之后,QMutex lock被释放。
QSerialPort serial;
if (currentPortName.isEmpty()) {
emit error(tr("No port name specified"));
return;
}
while (!quit) {
创建一个QSerialPort临时对象,我们要保证所有的串口操作在run函数中执行以保证一次有且仅有一个对象在执行。如果检测到串口名发生了变化,那么就要重新注册串口:
if (currentPortNameChanged) {
serial.close();
serial.setPortName(currentPortName);
if (!serial.open(QIODevice::ReadWrite)) {
emit error(tr("Can't open %1, error code %2")
.arg(portName).arg(serial.error()));
return;
}
}
run函数中继续将发送数据,直到数据发送完毕:
// write request
QByteArray requestData = currentRequest.toLocal8Bit();
serial.write(requestData);
if (serial.waitForBytesWritten(waitTimeout)) {
//waitForBytesWritten()显示了阻塞传输,如果超时(超过了waitTimeout),则发送一个超时信号出去:
} else {
emit timeout(tr("Wait write request timeout %1")
.arg(QTime::currentTime().toString()));
}
之后,等待slave发送来的数据,进行接收,为了保证数据不丢失,这里用while循环来叠加数据,直到10ms内无数据过来,则将接收到数据发送出去。否则,发送接收超时信号。
// read response
if (serial.waitForReadyRead(currentWaitTimeout)) {
QByteArray responseData = serial.readAll();
while (serial.waitForReadyRead(10))
responseData += serial.readAll();
QString response(responseData);
emit this->response(response);
} else {
emit timeout(tr("Wait read response timeout %1")
.arg(QTime::currentTime().toString()));
}
之后,线程休眠,直到下一次transaction()。线程将被唤醒,重新读取新的数据,并重新运行run函数。
mutex.lock();
cond.wait(&mutex);
if (currentPortName != portName) {
currentPortName = portName;
currentPortNameChanged = true;
} else {
currentPortNameChanged = false;
}
currentWaitTimeout = waitTimeout;
currentRequest = request;
mutex.unlock();
}
例子
这里附上两个例子,分别是阻塞和非阻塞实现的具体工程代码,
请大家到本人下载频道下载:https://download.csdn.net/download/bjtuwayne/12080230