本页内容

阻塞式接收器

演示如何在非GUI线程中使用QSerialPort 的同步API。

“阻塞接收器”演示了如何在非GUI线程中使用QSerialPort 的同步API来创建一个串行接口应用程序。

Qt Serial Port 阻塞式接收器

QSerialPort 支持两种常见的编程方法:

  • 异步(非阻塞)方法。操作会在控制权返回 Qt 事件循环时被调度并执行。当操作完成时,QSerialPort 会发出一个信号。例如,QSerialPort::write() 会立即返回。当数据发送至串行端口时,QSerialPort 会发出bytesWritten() 信号。
  • 同步(阻塞)方法。在非GUI和多线程应用程序中,可以调用waitFor...() 函数(即QSerialPort::waitForReadyRead()),以使调用线程暂停,直到操作完成。

本示例演示了同步方法。Terminal示例则演示了异步方法。

本示例旨在演示一种模式,您可利用该模式简化串行编程代码,同时不影响用户界面的响应性。 使用 Qt 的阻塞式串行编程 API 通常会使代码更简单,但由于其阻塞行为,应仅在非 GUI 线程中使用,以防止用户界面冻结。但与许多人的想法相反,在QThread 中使用线程并不一定会给您的应用程序带来难以管理的复杂性。

本应用程序是一个接收端(Receiver),用于演示其与发送端应用程序“阻塞式发送器(Blocking Sender)”示例配合工作的场景。

“接收器”应用程序通过串口从“发送器”应用程序接收请求,并向其发送响应。

我们将从 ReceiverThread 类开始,该类负责处理串行编程代码。

class ReceiverThread : public QThread
{
    Q_OBJECT

public:
    explicit ReceiverThread(QObject *parent = nullptr);
    ~ReceiverThread();

    void startReceiver(const QString &portName, int waitTimeout, const QString &response);

signals:
    void request(const QString &s);
    void error(const QString &s);
    void timeout(const QString &s);

private:
    void run() override;

    QString m_portName;
    QString m_response;
    int m_waitTimeout = 0;
    QMutex m_mutex;
    bool m_quit = false;
};

ReceiverThread 是QThread 的子类,它提供了一组 API 用于接收来自 Sender 的请求,并具有用于发送响应和报告错误的信号。

应调用 startReceiver() 方法来启动接收器应用程序。该方法将配置和启动串行接口所需的参数传递给 ReceiverThread。当 ReceiverThread 从发送器接收到任何请求时,会发出 request() 信号;若发生任何错误,则会发出 error() 或 timeout() 信号。

需要注意的是,虽然 startReceiver() 是在主线程(GUI 线程)中调用的,但响应数据和其他参数将通过 ReceiverThread 所属的线程进行访问。由于 ReceiverThread 的数据成员会由不同线程并发读写,因此建议使用QMutex 来同步访问。

void ReceiverThread::startReceiver(const QString &portName, int waitTimeout, const QString &response)
{
    const QMutexLocker locker(&m_mutex);
    m_portName = portName;
    m_waitTimeout = waitTimeout;
    m_response = response;
    if (!isRunning())
        start();
}

startReceiver() 函数存储串口名称、超时时间和响应数据,而QMutexLocker 通过锁定互斥锁来保护这些数据。随后,如果线程尚未运行,则启动该线程。QWaitCondition::wakeOne() 的用法将在后文讨论。

void ReceiverThread::run()
{
    bool currentPortNameChanged = false;

    m_mutex.lock();
    QString currentPortName;
    if (currentPortName != m_portName) {
        currentPortName = m_portName;
        currentPortNameChanged = true;
    }

    int currentWaitTimeout = m_waitTimeout;
    QString currentRespone = m_response;
    m_mutex.unlock();

在 run() 函数中,首先获取互斥锁,从成员变量中获取串口名称、超时时间和响应数据,然后再次释放互斥锁。 在任何情况下,都不应在一个进程正在获取这些数据的同时调用startReceiver() 方法。QString 虽然是可重入的,但并非线程安全的,因此不建议从一个进程的初始化、调用及超时或响应数据中读取另一个进程的串口名称。ReceiverThread 每次只能处理一个初始化。

我们在进入循环之前,在 run() 函数中于栈上构造的QSerialPort 对象:

    QSerialPort serial;

    while (!m_quit) {

这使我们能够在循环运行期间仅创建一个对象,同时也意味着该对象的所有方法都将在 run() 线程的上下文中执行。

在循环中,检查当前启动的串口名称是否发生变化。如果发生了变化,则重新打开并重新配置该串口。

        if (currentPortName.isEmpty()) {
            emit error(tr("Port not set"));
            return;
        } else if (currentPortNameChanged) {
            serial.close();
            serial.setPortName(currentPortName);

            if (!serial.open(QIODevice::ReadWrite)) {
                emit error(tr("Can't open %1, error code %2")
                           .arg(m_portName).arg(serial.error()));
                return;
            }
        }

        if (serial.waitForReadyRead(currentWaitTimeout)) {

循环将持续等待请求数据:

            // read request
            QByteArray requestData = serial.readAll();
            while (serial.waitForReadyRead(10))
                requestData += serial.readAll();

警告: 采用阻塞方式时,应在每次调用 read() 之前使用 waitForReadyRead()方法, 因为该方法 会处理所有 I/O 例程,而非由 Qt 事件循环处理。

如果读取数据时发生错误,将发出 timeout() 信号。

        } else {
            emit timeout(tr("Wait read request timeout %1")
                         .arg(QTime::currentTime().toString()));
        }

读取成功后,尝试发送响应并等待传输完成:

            // write response
            const QByteArray responseData = currentRespone.toUtf8();
            serial.write(responseData);
            if (serial.waitForBytesWritten(m_waitTimeout)) {
                const QString request = QString::fromUtf8(requestData);
                emit this->request(request);

警告: 在阻塞模式下,每次调用 write() 之后都应使用 waitForBytesWritten()方法, 因为该方法 会处理所有 I/O 例程,而非由 Qt 事件循环处理。

如果在写入数据时发生错误,将发出 timeout() 信号。

            } else {
                emit timeout(tr("Wait write response timeout %1")
                             .arg(QTime::currentTime().toString()));
            }

写入成功后,会发出包含从发送方应用程序接收到的数据的 `request()` 信号:

                emit this->request(request);

接下来,线程切换到读取串行接口的当前参数(因为这些参数可能已被更新),并从头开始运行循环。

        m_mutex.lock();
        if (currentPortName != m_portName) {
            currentPortName = m_portName;
            currentPortNameChanged = true;
        } else {
            currentPortNameChanged = false;
        }
        currentWaitTimeout = m_waitTimeout;
        currentRespone = m_response;
        m_mutex.unlock();
    }

运行示例

您可以通过以下方式运行示例:

示例项目 @ code.qt.io

另请参阅 “串行终端”和“阻塞式发送器”。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.