Qt 串口类封装(基于QSerialPort )实战分享|附源码 + 使用经验
本文分享Qt 串口类封装(基于QSerialPort )实战经验,包含源码,各种使用小技巧
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
更多推荐


所有评论(0)