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 #include #include // 简单的协程等待器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 #include #include // 生成器类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 #include #include // 协程返回任务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_OBJECTpublic: 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 占用过高 } // 发送 aabb QByteArray 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); } // 发送 ccdd QByteArray 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 #include #include #include #include #include #include #include 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 #include 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 占用过高 } // 发送 aabb QByteArray 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); } // 发送 ccdd QByteArray 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 协程让异步代码的编写更加简洁直观,避免了传统回调函数带来的复杂性。同时,使用原子标志位可以方便地控制协程的运行状态。在实际开发中,可以根据具体需求对代码进行扩展,如添加数据接收功能、错误处理等。