QT开发技术【串口和C++20协程,实现循环发送、暂停、恢复、停止】
引言
在嵌入式开发、工业控制等诸多领域,串口通信是极为常见的通信方式。Qt 作为一个强大的跨平台应用程序开发框架,提供了 QSerialPort 类来方便地实现串口通信。而 C++20 引入的协程特性,为异步编程带来了极大的便利,能让代码以更简洁、直观的方式处理异步任务。本文将详细介绍如何结合 Qt 的串口功能与 C++20 协程,实现串口数据的循环发送,并具备暂停、恢复和停止的功能。
一、C++20 协程介绍
1. 什么是协程
协程(Coroutine)是一种比线程更轻量级的并发编程概念。与线程不同,线程由操作系统进行调度,而协程由程序自身控制调度。协程可以在执行过程中暂停(suspend),并在合适的时候恢复(resume)执行,这使得编写异步代码变得更加简洁和直观,避免了传统回调函数带来的嵌套问题,也就是所谓的“回调地狱”。
2. C++20 协程的实现
C++20 正式将协程纳入标准库,主要通过三个新的关键字 co_await、co_yield 和 co_return 来实现。
2.1 co_await
co_await 用于暂停协程的执行,直到等待的操作完成。它可以作用于任何实现了协程等待器(Awaitable)接口的对象。协程等待器需要实现三个特定的成员函数:await_ready、await_suspend 和 await_resume。
#include <iostream>
#include <coroutine>
#include <future>// 简单的协程等待器
struct Awaitable {bool await_ready() const noexcept { return false; }void await_suspend(std::coroutine_handle<> h) {std::cout << "Suspending coroutine" << std::endl;// 模拟异步操作完成后恢复协程h.resume();}void await_resume() const noexcept {std::cout << "Resuming coroutine" << std::endl;}
};// 协程函数
struct Task {struct promise_type {Task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() {}};
};Task coroutineFunction() {co_await Awaitable{};std::cout << "Coroutine continues execution" << std::endl;
}int main() {coroutineFunction();return 0;
}
在上述代码中,Awaitable 是一个简单的协程等待器,coroutineFunction 是一个协程函数,使用 co_await 暂停执行,直到 Awaitable 的异步操作完成。
2.2 co_yield
co_yield 用于将协程的控制权返回给调用者,同时保留协程的状态,以便后续恢复执行。它通常用于实现生成器(Generator)。
#include <iostream>
#include <coroutine>
#include <optional>// 生成器类
template<typename T>
struct Generator {struct promise_type {T value_;std::exception_ptr exception_;Generator get_return_object() { return Generator{Handle::from_promise(*this)}; }std::suspend_always initial_suspend() { return {}; }std::suspend_always final_suspend() noexcept { return {}; }std::suspend_always yield_value(T value) {value_ = value;return {};}void unhandled_exception() { exception_ = std::current_exception(); }void return_void() {}};using Handle = std::coroutine_handle<promise_type>;Handle coro_;Generator(Handle h) : coro_(h) {}~Generator() { if (coro_) coro_.destroy(); }bool moveNext() {coro_.resume();return !coro_.done();}T currentValue() { return coro_.promise().value_; }
};// 生成器协程函数
Generator<int> numberGenerator() {for (int i = 0; i < 5; ++i) {co_yield i;}
}int main() {auto gen = numberGenerator();while (gen.moveNext()) {std::cout << gen.currentValue() << std::endl;}return 0;
}
在这个例子中,numberGenerator 是一个生成器协程函数,使用 co_yield 逐个返回整数。
2.3 co_return
co_return 用于终止协程的执行,并返回一个值(如果需要)。
#include <iostream>
#include <coroutine>
#include <future>// 协程返回任务
struct Task {struct promise_type {int result_;Task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_value(int value) { result_ = value; }void unhandled_exception() {}};
};// 协程函数
Task coroutineFunction() {co_return 42;
}int main() {// 这里只是示例结构,实际获取返回值需要更完善的设计coroutineFunction();return 0;
}
3. C++20 协程的优势
代码简洁:协程可以让异步代码以同步的方式编写,避免了复杂的回调嵌套,提高了代码的可读性和可维护性。
轻量级:协程的创建和销毁开销比线程小得多,适合处理大量并发任务。
灵活调度:协程的调度由程序自身控制,可以根据需要在合适的时机暂停和恢复执行。
4. C++20 协程的应用场景
异步 I/O 操作:如网络编程、文件读写等,使用协程可以避免阻塞线程,提高程序的并发性能。
生成器:实现按需生成数据的迭代器,节省内存空间。
游戏开发:处理游戏中的异步事件,如动画播放、资源加载等。
5. 注意事项
内存管理:协程在暂停时会在堆上分配内存,需要确保在协程结束时正确释放这些内存。
异常处理:协程中的异常需要正确处理,避免程序崩溃。
性能开销:虽然协程比线程轻量,但频繁的暂停和恢复操作也会带来一定的性能开销。
二、 QT串口和C++20协程
2.1 协程等待器
// 协程等待器,用于异步等待一段时间
struct AwaitableDelay {QTimer timer;bool await_ready() const noexcept { return false; }void await_suspend(std::coroutine_handle<> h) {QObject::connect(&timer, &QTimer::timeout, [h]() { h.resume(); });timer.start();}void await_resume() const noexcept {}AwaitableDelay(int ms) { timer.setSingleShot(true); timer.setInterval(ms); }
};
AwaitableDelay 是一个协程等待器,利用 QTimer 实现定时功能。await_ready 返回 false 表示协程需要暂停,await_suspend 连接定时器的超时信号到协程的恢复函数,await_resume 在协程恢复时调用。
2.2 异步发送任务
// 异步发送数据的协程
struct AsyncSendTask {struct promise_type {AsyncSendTask get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() {}};
};
AsyncSendTask 定义了协程的承诺类型,用于管理协程的生命周期。
2.3 主窗口类
class CQtTest : public QWidget {Q_OBJECT
public:CQtTest(QWidget* parent = nullptr);~CQtTest();private slots:void on_pushButton_Start_clicked();void on_pushButton_Stop_clicked();void on_pushButton_Pause_clicked();void on_pushButton_Resume_clicked();void on_pushButton_Send_clicked();private:std::unique_ptr<Ui::CQtTest> ui;QSerialPort serialPort;std::atomic<bool> isCoroutineRunning{false}; // 协程运行标志位std::atomic<bool> isCoroutinePaused{false}; // 协程暂停标志位AsyncSendTask asyncSendData(QSerialPort* serialPort);
};
2.4 异步发送数据协程
AsyncSendTask CQtTest::asyncSendData(QSerialPort* serialPort) {if (!serialPort->isOpen()) {qDebug() << "Serial port is not open";co_return;}isCoroutineRunning = true;while (isCoroutineRunning) {// 检查是否暂停while (isCoroutinePaused) {co_await AwaitableDelay(100); // 短暂等待,避免 CPU 占用过高}// 发送 aabbQByteArray data1 = QByteArray::fromHex("aabb");qint64 bytesWritten1 = serialPort->write(data1);if (bytesWritten1 == -1) {qDebug() << "Error writing data aabb to serial port:" << serialPort->errorString();} else {qDebug() << "Data sent:" << data1.toHex();}// 等待 1 秒co_await AwaitableDelay(1000);// 检查是否暂停while (isCoroutinePaused) {co_await AwaitableDelay(100);}// 发送 ccddQByteArray data2 = QByteArray::fromHex("ccdd");qint64 bytesWritten2 = serialPort->write(data2);if (bytesWritten2 == -1) {qDebug() << "Error writing data ccdd to serial port:" << serialPort->errorString();} else {qDebug() << "Data sent:" << data2.toHex();}// 等待 1 秒co_await AwaitableDelay(1000);}isCoroutineRunning = false;
}
asyncSendData 协程函数在串口打开的情况下,循环交替发送 aabb 和 ccdd,每次发送前后检查暂停标志位,若暂停则等待。
2.5 槽函数实现
void CQtTest::on_pushButton_Start_clicked() {if (serialPort.isOpen() && !isCoroutineRunning) {isCoroutinePaused = false;asyncSendData(&serialPort);}
}void CQtTest::on_pushButton_Stop_clicked() {isCoroutineRunning = false;isCoroutinePaused = false;
}void CQtTest::on_pushButton_Pause_clicked() {isCoroutinePaused = true;
}void CQtTest::on_pushButton_Resume_clicked() {isCoroutinePaused = false;
}void CQtTest::on_pushButton_Send_clicked() {if(serialPort.isOpen())serialPort.write(QByteArray::fromHex("aabb"));
}
on_pushButton_Start_clicked:启动协程。
on_pushButton_Stop_clicked:停止协程。
on_pushButton_Pause_clicked:暂停协程。
on_pushButton_Resume_clicked:恢复协程。
on_pushButton_Send_clicked:手动发送 aabb 数据。
三、完整代码
#pragma once#include "ui_QtTest.h"
#include <QWidget>
#include <memory>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QTimer>
#include <QDebug>
#include <coroutine>
#include <future>namespace Ui {class CQtTest;
}// 协程等待器,用于异步等待一段时间
struct AwaitableDelay {QTimer timer;bool await_ready() const noexcept { return false; }void await_suspend(std::coroutine_handle<> h) {QObject::connect(&timer, &QTimer::timeout, [h]() { h.resume(); });timer.start();}void await_resume() const noexcept {}AwaitableDelay(int ms) { timer.setSingleShot(true); timer.setInterval(ms); }
};// 异步发送数据的协程
struct AsyncSendTask {struct promise_type {AsyncSendTask get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() {}};
};class CQtTest : public QWidget
{Q_OBJECTpublic:explicit CQtTest(QWidget *parent = nullptr);virtual ~CQtTest() = default;private:AsyncSendTask asyncSendData(QSerialPort* serialPort);private slots:void on_pushButton_Start_clicked();void on_pushButton_Stop_clicked();void on_pushButton_Send_clicked();void on_pushButton_Pause_clicked();void on_pushButton_Resume_clicked();private:std::unique_ptr<Ui::CQtTest> ui;QSerialPort serialPort;std::atomic<bool> isCoroutineRunning{ false }; // 协程运行标志位std::atomic<bool> isCoroutinePaused{ false }; // 协程暂停标志位
};
#include "QtTest.h"#include <iostream>
#include <vector>CQtTest::CQtTest(QWidget* parent): QWidget(parent), ui(std::make_unique<Ui::CQtTest>())
{ui->setupUi(this);serialPort.setPortName("COM1"); // 根据实际情况修改串口名serialPort.setBaudRate(500000); // 根据实际情况修改波特率serialPort.setDataBits(QSerialPort::Data8);serialPort.setParity(QSerialPort::NoParity);serialPort.setStopBits(QSerialPort::OneStop);serialPort.setFlowControl(QSerialPort::NoFlowControl);if (serialPort.open(QIODevice::ReadWrite)) {qDebug() << "Serial port opened successfully:" << serialPort.portName();}else {qDebug() << "Failed to open serial port:" << serialPort.portName();}}void CQtTest::on_pushButton_Send_clicked()
{if(serialPort.isOpen())serialPort.write(QByteArray::fromHex("aabb"));
}void CQtTest::on_pushButton_Pause_clicked()
{isCoroutinePaused = true;
}void CQtTest::on_pushButton_Resume_clicked()
{isCoroutinePaused = false;
}void CQtTest::on_pushButton_Start_clicked() {if (serialPort.isOpen() && !isCoroutineRunning) {isCoroutinePaused = false;asyncSendData(&serialPort);}
}void CQtTest::on_pushButton_Stop_clicked() {isCoroutineRunning = false;isCoroutinePaused = false;
}AsyncSendTask CQtTest::asyncSendData(QSerialPort* serialPort) {if (!serialPort->isOpen()) {qDebug() << "Serial port is not open";co_return;}isCoroutineRunning = true;while (isCoroutineRunning) {// 检查是否暂停while (isCoroutinePaused) {co_await AwaitableDelay(100); // 短暂等待,避免 CPU 占用过高}// 发送 aabbQByteArray data1 = QByteArray::fromHex("aabb");qint64 bytesWritten1 = serialPort->write(data1);if (bytesWritten1 == -1) {qDebug() << "Error writing data aabb to serial port:" << serialPort->errorString();}else {qDebug() << "Data sent:" << data1.toHex();}// 等待 1 秒co_await AwaitableDelay(1000);// 检查是否暂停while (isCoroutinePaused) {co_await AwaitableDelay(100);}// 发送 ccddQByteArray data2 = QByteArray::fromHex("ccdd");qint64 bytesWritten2 = serialPort->write(data2);if (bytesWritten2 == -1) {qDebug() << "Error writing data ccdd to serial port:" << serialPort->errorString();}else {qDebug() << "Data sent:" << data2.toHex();}// 等待 1 秒co_await AwaitableDelay(1000);}isCoroutineRunning = false;
}
三、效果和总结
通过结合 Qt 的 QSerialPort 类和 C++20 协程特性,我们实现了一个功能丰富的串口数据发送程序,具备循环发送、暂停、恢复和停止的功能。C++20 协程让异步代码的编写更加简洁直观,避免了传统回调函数带来的复杂性。同时,使用原子标志位可以方便地控制协程的运行状态。在实际开发中,可以根据具体需求对代码进行扩展,如添加数据接收功能、错误处理等。