Qt 串口类封装(基于QSerialPort )实战分享|附源码 + 使用经验

1. 前言

在嵌入式开发过程中,串口通信是最常见、最基础的通信方式之一。无论是调试模块、配置指令,还是进行数据采集与控制,串口几乎是默认首选。

结合 Qt 框架进行 GUI 工具开发,不仅能快速构建项目,还能自定义调试工具,提高开发效率。比如:

  • 构建串口测试平台、协议验证工具
  • 实现指令发送、回包解析、实时日志展示
  • 模拟主机行为做功能验证,压力测试

本文将分享我封装的 Qt 串口类源码与调试经验,适合需要使用串口高效集成项目,或者开发上位机调试工具的嵌入式开发者。


2. QSerialPort 简介

QSerialPort 是 Qt 提供的串口通信类,封装了串口大部分的基本操作:

  • 打开/关闭串口
  • 设置波特率、数据位、停止位、校验位
  • 发送/接收字节流数据
  • 异步事件处理(readyRead()

常用 API 示例:

port->setPortName("COM3");                  // Windows
port->setBaudRate(QSerialPort::Baud115200); 
port->setDataBits(QSerialPort::Data8);      
port->setParity(QSerialPort::NoParity);     
port->setStopBits(QSerialPort::OneStop);    
port->open(QIODevice::ReadWrite);           

3. 为什么重新封装成一个类?

虽然 Qt 本身提供了 QSerialPort,但在实际开发中,直接在业务代码中使用会产生以下问题:

  • 接收数据在 readyRead() 触发时并不完整
  • 粘包、拆包问题处理不方便
  • 多处使用串口逻辑会导致代码分散难维护

因此我将其封装成一个类,核心思路:

  • 内部缓存接收数据
  • 引入定时器超时机制判断一帧数据是否接收完成
  • 使用信号槽将完整数据向外传递
  • 提供接口函数实现发送、连接、断开等操作

4. 源码分享与调用方式

4.1源码

uartlite_drv.h
#ifndef UARTLITE_DRV_H
#define UARTLITE_DRV_H

#include <QObject>
#include <QByteArray>
#include <QString>

class QSerialPort;
class QTimer;

/**
 * @brief uartlite_drv 串口通信类,封装 Qt 串口操作
 */
class uartlite_drv : public QObject
{
    Q_OBJECT

public:
    /**
     * @brief 构造函数
     */
    explicit uartlite_drv(QObject *parent = nullptr);

    /**
     * @brief 连接串口设备
     * @return true 连接成功,false 失败
     */
    bool connect_dev();

    /**
     * @brief 断开串口设备
     * @return true 总是返回 true
     */
    bool disconnect_dev();

    /**
     * @brief 设置串口名称
     */
    void set_com_name(const QString &name);

    /**
     * @brief 获取串口连接状态
     */
    bool is_connected() const;

signals:
    /**
     * @brief 串口接收数据完成信号
     * @param buff 接收到的完整数据
     */
    void rev_uart_buf_signal(QByteArray buff);

public slots:
    /**
     * @brief 串口数据发送接口
     * @param send_buff 待发送数据
     */
    void send_data_slot(QByteArray send_buff);

private slots:
    /**
     * @brief 串口接收槽函数(readyRead)
     */
    void rev_data_slot();

    /**
     * @brief 接收超时槽函数
     */
    void rev_data_time_out();

private:
    QSerialPort *uart_dev = nullptr;      ///< 串口对象
    QTimer *rev_timer = nullptr;      ///< 接收超时定时器
    QByteArray rev_buf;               ///< 接收缓冲区
    QString com_name;                 ///< 串口名称
    bool connected = false;           ///< 连接状态标志
};

#endif // UARTLITE_DRV_H

uartlite_drv.cpp
#include "uartlite_drv.h"

#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
#include <QTimer>
#include <QDebug>

uartlite_drv::uartlite_drv(QObject *parent) : QObject(parent)
{
    rev_timer = new QTimer(this);
	rev_timer->setTimerType(Qt::PreciseTimer);/*精确定时*/
    connect(rev_timer, &QTimer::timeout, this, &uartlite_drv::rev_data_time_out, Qt::QueuedConnection);

    uart_dev = new QSerialPort(this);
}

bool uartlite_drv::connect_dev()
{
    uart_dev->setPortName(com_name);
    qDebug() << "串口名:" << com_name;

    if (!uart_dev->open(QIODevice::ReadWrite)) {
        qDebug() << "串口打开失败!";
        connected = false;
        return false;
    }

	/*以下设置的是最常用的串口配置,有其他需求可直接修改*/
    uart_dev->setBaudRate(QSerialPort::Baud115200);//波特率
    uart_dev->setDataBits(QSerialPort::Data8);//8位数据
    uart_dev->setParity(QSerialPort::NoParity);//无校验位
    uart_dev->setStopBits(QSerialPort::OneStop);//1位停止
    uart_dev->setFlowControl(QSerialPort::NoFlowControl);//无数据流控制

    connect(uart_dev, &QSerialPort::readyRead, this, &uartlite_drv::rev_data_slot);//连接接收的槽函数

    connected = uart_dev->isOpen();
    qDebug() << (connected ? "串口打开成功!" : "串口状态异常!");
    return connected;
}

bool uartlite_drv::disconnect_dev()
{
    if (uart_dev && uart_dev->isOpen()) {
        uart_dev->close();
        connected = false;
    }
    return true;
}

void uartlite_drv::set_com_name(const QString &name)
{
    com_name = name;
}

bool uartlite_drv::is_connected() const
{
    return connected;
}

void uartlite_drv::send_data_slot(QByteArray send_buff)
{
    if (!connected) {
        qDebug() << "串口未连接,无法发送数据!";
        return;
    }
    uart_dev->write(send_buff);//115200 波特率发送128byte数据耗时约11~12ms
}

void uartlite_drv::rev_data_slot()
{
    QByteArray rev_data = uart_dev->readAll();
    rev_buf.append(rev_data);/*数据缓存*/

    /*启动接收超时定时器,串口空闲无数据一定时间后认为单条数据已经结束*/
	/*可以根据自己指令长短,波特率,使用场景等适当调整*/
    rev_timer->start(5);/*调用start会重装载计时器值,这里设置5ms*/
	
}

void uartlite_drv::rev_data_time_out()
{
	/*超时机制触发,串口总线上暂无数据,认为指令包完整,暂停计时器*/
    rev_timer->stop();
    emit rev_uart_buf_signal(rev_buf);/*指令包发送*/
	//qDebug()<<rev_buf.size()<<rev_buf.toHex() ;/*调试使用*/
    rev_buf.clear();
}

4.2 调用示例

uartlite_drv uart;
uart.com_name = "COM3";        // 设置端口号(Windows)
uart.connect_dev();            // 打开串口
uart.send_data_slot(data);     // 发送数据
connect(&uart, &uartlite_drv::rev_uart_buf_signal, [](QByteArray data){
    qDebug() << "接收到数据:" << data.toHex();
});

如何在Qt中获取端口:

QStringList m_portNameList;
foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts()){
    m_portNameList << info.portName();
    qDebug()<<"port :"<<info.portName();
}

4.3 C语言写多了后遗症。。。

由于贴主平常主要从事嵌入式开发,以艹C语言为主,尽管到了C++下,还是忍不住用C的写法,就是喜欢每个byte都被我列出来单独罚站的感觉。这里贴一些C的写法,大家轻喷

/*接收后数据处理*/
#define max_recv_len 128
typedef struct{
    uint8_t  RevBuf[max_recv_len];
    uint16_t  Len;
}recv_data;
/*指令解析槽函数*/
void stm32_cmd::rev_cmd(QByteArray buff)
{
    if (buff.size() > max_recv_len ) {
        qDebug() << "buff.size over = " << buff.size();
        return;
     }
     
    recv_data *uartdata = new recv_data;
    uartdata->Len = buff.size();
    
    for (uint16_t i = 0; i < uartdata->Len; ++i) {
        uartdata->RevBuf[i] = static_cast<uint8_t>(buff.at(i));
    }

	/*以下就是个人发挥了,根据自己的指令来*/
    uint8_t cmd_type;
    unsigned short cmd_addr;
    if ((uartdata->RevBuf[0] == 0x5A) && (uartdata->RevBuf[1] == 0xA5)){
     /*do something*/
    }

    delete uartdata; // 确保释放 uartdata 的内存
}


/*数据发送,根据自己的指令格式封装*/
	uint8_t  SendData[128];
	/*个人发挥,根据自己的指令来*/
	SendData[0] = 0x5A; /* 帧头 */
	SendData[1] = 0xA5; /* 帧头 */
	/***自我发挥吧***/
	int send_buff_len = 128; /* 最终发送的数据总长度 */
	
	QByteArray send_buff;
	send_buff.append((char*)SendData, send_buff_len);
	
	uart->send_data_slot(send_buff);

4.4 .pro 文件注意添加:

pro文件内记得添加,不然编译器无法识别编译

QT += serialport

4.5 关于为什么用这个数据接收机制

	这里解释一下为什么采用这种机制
	
	这个方法适用于大部分的指令场景,数据对发的时候双方一般也会增加一定的指令间隔
	
	普通情况下,如果对指令及时性要求不是特别严格,建议使用队列机制,也可以做指令分级,搭建高级别指令抢占先发机制。
	
	其实使用串口作为指令通信,主机指令发出到从机指令解析到做出响应,几毫秒~十几毫秒响应已经足够了
	
	更高实时性要求情况下,串口包装指令不是最优解,或者需要搭建简洁的指令格式
	
	对数据包完整性有较高要求的,建议编辑指令格式时,数据位增加停止位作为分包依据
	

5. 在 Linux 下使用 Qt 串口的注意事项

5.1 串口设备命名不同

系统 设备命名 说明
Windows COM1, COM3 虚拟串口名
Linux /dev/ttyS0 板载串口
/dev/ttyUSB0 USB 转串口(CH340 等)
/dev/ttyACM0 CDC 类型串口(STM32 VCP)
uart.com_name = "/dev/ttyS0";

5.2 查看串口设备

ls /dev/ttyS*
ls /dev/ttyUSB*
ls /dev/ttyACM*

6. 其他妙用

  • 使用 QTextBrowser 显示接收数据,搭建自己的串口助手:
/*utf8Togb2312*/
std::string utf8Togb2312(std::string utf8_str)
{
    QTextCodec* utf8Codec = QTextCodec::codecForName("utf-8");
    QTextCodec* gb2312Codec = QTextCodec::codecForName("gb2312");

    QString strUnicode = utf8Codec->toUnicode(utf8_str.data());        //无编码
    QByteArray gb2312= gb2312Codec->fromUnicode(strUnicode);       //无编码转换

    return std::string(gb2312.data());
}
/*gb2312Toutf8*/
std::string gb2312Toutf8(std::string gb2312_str)
{
    QTextCodec* utf8Codec = QTextCodec::codecForName("utf-8");
    QTextCodec* gb2312Codec = QTextCodec::codecForName("gb2312");

    QString strUnicode = gb2312Codec ->toUnicode(gb2312_str.data());    //无编码
    QByteArray utf8 = utf8Codec ->fromUnicode(strUnicode);     //无编码转换

    return std::string(utf8.data());
}
/*结合textBrowser界面显示*/
void data_window::rev_printf_slot(QByteArray buff)
{
    std::string str = buff.toStdString();
    std::string str_1 =  gb2312Toutf8(str);//字符转码
    QString data =  QString::fromStdString(str_1);
    //qDebug()<<data;
    ui->textBrowser->append(data);
    ui->textBrowser->moveCursor(QTextCursor::End);/*始终保持最后一行*/

}

演示:
串口调试助手

  • 使用十六进制显示、搭建指令调试器
connect(this->uart_dev, QOverload<QByteArray>::of(&uartlite_drv ::rev_uart_buf_signal),
            this, [this](QByteArray data) {
                // 转换数据为16进制字符串
                QString hexString = data.toHex(' ').toUpper();
                QString displayString = QString("Recv: %1").arg(hexString);

                // 获取 QTextCursor 对象
                QTextCursor cursor = ui->cmd_Browser->textCursor();
                // 将光标移动到文本末尾
                cursor.movePosition(QTextCursor::End);

                // 创建 QTextCharFormat 对象来设置文本格式
                QTextCharFormat format;
                format.setForeground(Qt::blue); // 设置字体颜色,例如蓝色
                format.setFontWeight(QFont::Normal); 

                // 插入带格式的文本和换行符
                cursor.insertText(displayString, format);
                cursor.insertBlock(); // 插入一个新的段落,相当于换行

                // 确保光标位置正确
                ui->cmd_Browser->setTextCursor(cursor);
            });

演示:
指令窗口

搭配上Qt,玩法很多,大家自行发挥吧


7. 调试经验总结分享

  • 串口通信是流式数据传输,并没有天然的帧边界。在实际项目中,我采用了 “接收空闲时间超时机制” 来判断一条数据是否接收完成,这个方法简单、粗暴、但非常有效:通常指令之间本身就会有间隔(主机或MCU自然存在指令发送节拍间隔),你可以根据波特率、数据包长度动态调整这个时间,比如 115200 波特率下,5ms 可容纳约 57 字节

  • 串口通信适用于大多数指令交互、配置参数、获取状态等需求,几毫秒~十几毫秒的响应延迟通常是可以接受的。但如果你对实时性要求非常高(比如亚毫秒级),串口可能不是最佳方案,建议考虑:更高性能的总线(如 SPI、USB HID)简化协议,尽量减少字段、压缩响应时间…不同总线各有各的优点缺点,实际还是要按照项目需求走

  • 建议使用接收队列与优先级机制,对于有复杂交互逻辑的系统,可以设计一个 接收队列 + 指令优先级调度的模型:将接收数据封装成任务,统一放入队列中处理,为不同指令设置优先级,甚至支持抢占

  • 如果你希望更加精确地判断接收数据是否完整,推荐在自定义协议中添加:固定帧尾(如 0x7E, \n, 0x0D 0x0A),数据长度字段(用于验证接收数据量)校验码(如 CRC8、CRC16)这些方式都比“超时判断”更精确,但代码实现复杂度也会增加。

  • 另外,大家使用串口收发器的时候,比如ttl转usb,232转usb等等,需要注意收发器的质量,质量不佳极有可能造成,粘包,拆包,错包的情况,推荐 FTDI/CP2102/CH340 等

结语

如果你觉得本文对你有所帮助,欢迎点赞、收藏或评论交流。


作者:Jafi


Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐