第2章:Qt串口模块概览——QSerialPort类体系、QSerialPortInfo枚举器与编程核心流程
串口编程,说白了就是让Qt程序跟硬件设备“打电话”。你得知道拨给谁、用什么协议、数据怎么包装。这一章,我把Qt串口模块的底裤给你翻出来看看。
2.1 QSerialPort类体系——串口操作的核心
QSerialPort是Qt串口编程的“心脏”。它继承自QIODevice,所以读写操作跟文件、网络套接字很像。我刚开始用的时候,心想这不就是个文件操作吗?后来发现坑不少。
核心继承链: QObject → QIODevice → QSerialPort
QSerialPort本身不直接操作硬件,它通过平台相关的后端(如Windows的COM API、Linux的termios)完成实际通信。
QSerialPort提供的主要功能可以归纳为三类:
| 功能类别 | 关键方法/属性 | 说明 |
|---|---|---|
| 端口配置 | setPortName(), setBaudRate(), setDataBits(), setParity(), setStopBits(), setFlowControl() | 配置串口参数,必须在open()之前或之后重新配置时调用 |
| 打开/关闭 | open(QIODevice::ReadWrite), close() | 打开模式通常用ReadWrite,只读或只写场景较少 |
| 数据读写 | read(), readAll(), write(), waitForReadyRead(), waitForBytesWritten() | 同步读写方法,配合信号使用更高效 |
| 信号 | readyRead(), bytesWritten(), errorOccurred() | 异步事件驱动,避免阻塞主线程 |
嗯,这里要注意:QSerialPort不是线程安全的。如果你要在工作线程里操作串口,得用信号槽跨线程通信,或者加锁保护。我曾经在一个多线程项目里直接在线程函数里操作同一个QSerialPort对象,结果数据乱得一塌糊涂。
2.2 QSerialPortInfo枚举器——发现你的设备
QSerialPortInfo是个“侦察兵”。它帮你扫描系统里有哪些可用串口,以及它们的详细信息。我个人习惯在程序启动时先调用它,把可用端口列出来让用户选。
它的核心静态方法就一个:
QList<QSerialPortInfo> availablePorts = QSerialPortInfo::availablePorts();
foreach (const QSerialPortInfo &info, availablePorts) {
qDebug() << "端口名称:" << info.portName();
qDebug() << "描述:" << info.description();
qDebug() << "制造商:" << info.manufacturer();
qDebug() << "序列号:" << info.serialNumber();
qDebug() << "系统位置:" << info.systemLocation();
qDebug() << "产品标识符:" << info.productIdentifier();
qDebug() << "供应商标识符:" << info.vendorIdentifier();
qDebug() << "是否可用:" << info.isBusy();
}
你想想看,这些信息有什么用?
- portName():Windows上是"COM3"这种,Linux上是"ttyUSB0"。
- description():比如"USB Serial Port",帮你识别设备类型。
- vendorIdentifier() / productIdentifier():USB设备的VID/PID,可以用来精确匹配特定设备。
- isBusy():判断端口是否被其他程序占用。我曾经在调试时发现端口打不开,一查是别的工具占着。
小技巧: 如果你要自动连接某个已知设备,可以用VID/PID组合来过滤。比如某个USB转串口模块的VID是0x1A86,PID是0x7523,你就可以只显示这个设备,避免用户选错。
2.3 串口编程核心流程——三步走
串口编程的流程其实很固定,我总结成三步:配置 → 打开 → 读写。每一步都有讲究。
第一步:配置串口参数
配置参数必须在打开端口之前或之后重新设置。常见的配置代码:
QSerialPort *serial = new QSerialPort(this);
serial->setPortName("COM3");
serial->setBaudRate(QSerialPort::Baud115200);
serial->setDataBits(QSerialPort::Data8);
serial->setParity(QSerialPort::NoParity);
serial->setStopBits(QSerialPort::OneStop);
serial->setFlowControl(QSerialPort::NoFlowControl);
这里有个坑:波特率必须跟设备匹配。我遇到过好几次,设备手册写的是115200,结果实际是9600,折腾了半天才发现。
第二步:打开端口
if (!serial->open(QIODevice::ReadWrite)) {
qDebug() << "打开失败:" << serial->errorString();
return;
}
打开失败的原因很多:端口不存在、被占用、权限不足。在Linux上,普通用户可能没有/dev/ttyUSB0的权限,需要加udev规则或者用sudo。
警告: 打开端口后,记得检查error()。有些错误是临时的,比如设备刚插上还没稳定,可以等几秒重试。我曾经在产线上遇到过USB转串口模块插拔后需要500ms才能稳定识别。
第三步:读写数据
读写有两种方式:同步和异步。我推荐用异步,因为不会阻塞UI。
异步读取(推荐):
connect(serial, &QSerialPort::readyRead, this, [=]() {
QByteArray data = serial->readAll();
// 处理接收到的数据
});
同步写入:
QByteArray cmd = "AT\r\n";
qint64 bytesWritten = serial->write(cmd);
if (bytesWritten == -1) {
qDebug() << "写入失败";
} else {
serial->waitForBytesWritten(1000); // 等待1秒确保写完
}
你想想看,为什么不用同步读取?因为waitForReadyRead会阻塞线程,如果设备一直不发数据,你的程序就卡死了。异步信号则不会。
2.4 核心流程SVG图
下面这张图把整个串口编程流程串起来了,你对照着看会更清楚:
2.5 常见陷阱与避坑指南
做串口编程,有些坑是新手必踩的。我列几个最常见的:
- 端口被占用不释放: 程序崩溃后,端口可能还处于打开状态。下次启动时isBusy()返回true。解决办法:在程序退出时确保close()被调用,或者用RAII封装。
- 数据粘包: 串口是流式协议,没有消息边界。你发"AT\r\n",可能分两次收到"AT"和"\r\n"。我习惯在接收缓冲区里自己拼包,根据换行符或固定长度切分。
- 波特率不匹配: 设备端和Qt端波特率不一致,收到的全是乱码。用示波器或者逻辑分析仪看一眼波形,马上就能确认。
- 信号槽连接泄漏: 如果你在堆上创建QSerialPort,记得在析构时断开所有连接。否则对象销毁后信号还可能触发,导致野指针崩溃。
我的经验: 调试串口程序时,先用串口调试助手(比如sscom、Putty)确认设备能正常通信。排除硬件问题后,再排查代码。这样可以节省大量时间。
2.6 本章小结
这一章我们聊了QSerialPort的类体系、QSerialPortInfo的枚举功能,以及串口编程的三步流程。说白了,配置参数要准、打开端口要稳、读写数据要异步。这些基础打牢了,后面讲驱动交互和故障定位时你才能跟上。