首页 / 资讯中心 / 文章详情

Qt串口模块qtserialport源码解析:跨平台实现与避坑指南

Qt串口模块qtserialport源码解析:跨平台实现与避坑指南 ★ FEATURED ARTICLE
简介这份资源是面向 Qt 开发者的 qtserialport 串口库源码包主要解决 Qt4 环境缺少原生串口支持的问题。Qt4 本身不提供串口模块进行串口通信往往需要依赖第三方库或手动集成 qtserialport本包正好提供了完整的源码与工程文件方便开发者自行编译并接入项目。压缩包共 148 个文件约 408KB以 cpp 与 h 源文件为核心配合多个 pro、pri 工程配置及 qdoc 文档、png 图标、ui 界面等辅助内容覆盖了跨平台串口实现所需的目录结构。使用时可将头文件放入 Qt 的 include 目录、库文件放入 lib 目录并在 pro 中追加 CONFIG serialport 即可调用。目前已有 602 人学习下载适合需要在 Qt4 下完成串口通信、希望理解底层实现或进行二次开发的工程师参考。1. 从一次串口调试翻车说起这个源码包到底能干什么去年帮一个做工业数据采集的朋友排查问题他的上位机用 Qt 写跑在 Windows 上好好的换到一台国产 Linux 工控机上就死活收不到数据。串口打开成功write 也返回了字节数但 read 就是空的。折腾了一下午最后发现是串口参数里Parity和StopBits的默认值在两边平台行为不一致而他用的是 QSerialPort 的默认构造。那次之后我把 qtserialport 的源码翻了一遍才发现这个模块看着简单底下藏着的平台差异和事件循环机制比想象中多。这份 qtserialport 源码包就是 Qt 官方串口模块的完整实现。它对外暴露的 API 很薄——QSerialPort、QSerialPortInfo、QSerialPortInfoPrivate 这几个类但底下针对不同操作系统做了各自的私有实现Windows 走的是 Win32 的CreateFileReadFile/WriteFile加 OVERLAPPED 异步 IOLinux 走的是opentermios配置加QSocketNotifier监听文件描述符macOS 则用IOKit那一套。如果你正在做串口通信相关的上位机、嵌入式调试工具、或者需要跨平台跑的数据采集软件这份源码值得放在手边。它不只是一个能用的库更是一份「Qt 如何封装平台相关 IO」的参考实现。2. 模块架构拆解从 QSerialPort 到平台私有实现的调用链2.1 整体分层结构qtserialport 的源码目录结构很清晰核心分三层qtserialport/ ├── src/serialport/ │ ├── qserialport.cpp # 对外主类API 入口 │ ├── qserialport.h │ ├── qserialportinfo.cpp # 串口枚举与信息查询 │ ├── qserialportinfo.h │ ├── qserialport_p.h # 私有头持有 QSerialPortPrivate │ ├── qserialportinfo_p.h │ └── qserialportglobal.h # 导出宏、类型定义 ├── src/serialport/ 下的平台子目录 │ ├── qserialport_win.cpp # Windows 实现 │ ├── qserialport_win_p.h │ ├── qserialport_unix.cpp # Linux/macOS 通用 Unix 实现 │ ├── qserialport_unix_p.h │ └── qserialportinfo_unix.cpp └── examples/ # 官方示例 ├── terminal/ # 终端模拟 ├── blockingmaster/ # 阻塞式主站 ├── blockingslave/ # 阻塞式从站 ├── master/ # 异步主站 └── slave/ # 异步从站最上层是QSerialPort继承自QIODevice所以它能直接塞进QTextStream、QDataStream里用。QSerialPort本身不干活它持有一个QSerialPortPrivate指针所有实际动作都转发给这个私有类。私有类再根据编译平台在qserialport_win.cpp或qserialport_unix.cpp里实现具体逻辑。这种「公有类薄、私有类厚、平台实现分文件」的做法是 Qt 处理跨平台 IO 的典型套路。2.2 一个 open 调用背后的完整链路拿最常用的open()举例看看从调用到落地经历了什么// qserialport.cpp 中的简化逻辑 bool QSerialPort::open(OpenMode mode) { Q_D(QSerialPort); if (isOpen()) { qWarning(QSerialPort::open: Already open); return false; } // 设置底层 QIODevice 的打开模式 setOpenMode(mode); // 转发给私有实现 if (d-open()) { return true; } setOpenMode(NotOpen); return false; }Q_D是 Qt 的私有类访问宏展开后就是拿到d_ptr并转成QSerialPortPrivate*。真正的open()在平台文件里// qserialport_unix.cpp 中的核心片段 bool QSerialPortPrivate::open() { // 1. 根据端口名拼出设备路径比如 /dev/ttyUSB0 QByteArray portName this-portName.toLocal8Bit(); // 2. 以非阻塞方式打开避免 open 本身卡住 fd ::open(portName.constData(), O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd -1) { setError(QSerialPort::DeviceNotFoundError); return false; } // 3. 用 termios 配置波特率、数据位、校验位等 if (!setTermios()) { ::close(fd); fd -1; return false; } // 4. 创建 QSocketNotifier 监听可读事件 notifier new QSocketNotifier(fd, QSocketNotifier::Read, this); // 5. 连接信号数据到达时触发 _q_completeAsyncCommunication QObject::connect(notifier, SIGNAL(activated(int)), q, SLOT(_q_completeAsyncCommunication())); return true; }这里有几个关键点值得注意。第一O_NONBLOCK是必须的否则open在某些串口设备上会阻塞到有数据为止。第二QSocketNotifier是 Qt 事件循环和文件描述符之间的桥梁它让串口读事件能像 socket 一样被select/poll机制捕获从而融入 Qt 的事件驱动模型。第三setTermios()里才是真正配置串口参数的地方baudRate、dataBits、parity、stopBits、flowControl都在这里映射成 termios 的c_cflag位。2.3 QSerialPortInfo 的枚举逻辑QSerialPortInfo负责列出系统上可用的串口。在 Linux 上它扫描/sys/class/tty/目录过滤掉没有device子目录的项那些是虚拟终端再读取driver符号链接判断驱动类型。在 Windows 上则通过 SetupAPI 枚举设备接口。这部分代码在qserialportinfo_unix.cpp和qserialportinfo_win.cpp里逻辑不复杂但平台细节多比如 Linux 下要区分ttyS*原生串口和ttyUSB*USB 转串口因为两者的device路径结构不同。// qserialportinfo_unix.cpp 中枚举串口的简化逻辑 QListQSerialPortInfo QSerialPortInfo::availablePorts() { QListQSerialPortInfo ports; // 遍历 /sys/class/tty 下的所有条目 QDir ttyDir(QStringLiteral(/sys/class/tty)); const auto entries ttyDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString entry : entries) { // 检查是否存在 device 子目录没有则跳过 QString devicePath ttyDir.filePath(entry QStringLiteral(/device)); if (!QFileInfo::exists(devicePath)) continue; // 读取 driver 信息用于填充 description QString driverPath devicePath QStringLiteral(/driver); QFileInfo driverInfo(driverPath); QString driverName driverInfo.symLinkTarget().section(/, -1); QSerialPortInfo info; info.d_portName entry; info.d_description driverName; ports.append(info); } return ports; }这段代码里symLinkTarget().section(/, -1)是取符号链接目标路径的最后一段也就是驱动名。比如/sys/bus/usb-serial/drivers/ftdi_sio取出来就是ftdi_sio。这个信息在调试时很有用能快速判断当前串口用的是哪个芯片的驱动。3. 把源码跑起来编译、示例运行与最小验证工程3.1 编译 qtserialport 的两种方式如果你只是想用 Qt 自带的串口模块通常不需要自己编译安装 Qt 时勾选 Qt Serial Port 组件即可。但既然拿到了源码自己编一遍能帮你确认版本、打补丁、或者交叉编译到嵌入式平台。方式一作为 Qt 子模块编译。把qtserialport目录放到 Qt 源码树的qtbase同级然后# 进入 qtserialport 目录 cd qtserialport # 用 qmake 生成 Makefile注意指定 Qt 安装路径 /path/to/Qt/5.15.2/gcc_64/bin/qmake qtserialport.pro # 编译并安装 make -j$(nproc) make install方式二独立编译。如果不想动 Qt 安装目录可以只编译出库文件然后在自己的工程里手动链接mkdir build cd build qmake ../qtserialport.pro CONFIGrelease make -j$(nproc) # 产物在 lib/ 目录下得到 libQt5SerialPort.so编译时如果报Project ERROR: Unknown module(s) in QT: serialport说明你的 Qt 安装里没有串口模块需要先装libqt5serialport5-devLinux或重新运行 Qt 安装器勾选对应组件。3.2 跑通官方 terminal 示例源码包里examples/serialport/terminal是最直观的示例它模拟了一个串口终端能收发数据、切换参数。编译运行cd examples/serialport/terminal qmake terminal.pro make # 运行需要指定串口设备 ./terminal /dev/ttyUSB0如果手头没有真实串口设备可以用socat创建一对虚拟串口来验证# 创建一对虚拟串口pty 模式 socat -d -d pty,raw,echo0 pty,raw,echo0 # 输出会显示两个设备路径比如 /dev/pts/3 和 /dev/pts/4 # 一个给 terminal 用另一个用 echo 或 cat 发数据这个示例的代码结构值得一看MainWindow负责 UISettingsDialog负责参数配置Console负责显示。核心的串口读写逻辑在MainWindow::openSerialPort()里void MainWindow::openSerialPort() { SettingsDialog::Settings p settingsDialog-settings(); // 设置端口名 serial-setPortName(p.name); // 设置波特率 serial-setBaudRate(p.baudRate); // 设置数据位 serial-setDataBits(p.dataBits); // 设置校验位 serial-setParity(p.parity); // 设置停止位 serial-setStopBits(p.stopBits); // 设置流控 serial-setFlowControl(p.flowControl); // 以读写模式打开 if (serial-open(QIODevice::ReadWrite)) { // 连接 readyRead 信号到读取槽 connect(serial, QSerialPort::readyRead, this, MainWindow::readData); // 连接 errorOccurred 信号处理错误 connect(serial, QSerialPort::errorOccurred, this, MainWindow::handleError); } }readyRead是QIODevice的信号当有数据到达时触发。errorOccurred是 Qt 5.8 之后新增的替代了旧的error信号避免和QSerialPort::error方法重名。这两个信号是异步模式下最常用的入口。3.3 最小验证工程从零写一个串口收发官方示例功能全但代码多我一般会先写一个最小工程验证环境。新建一个 Qt Widgets 项目.pro里加一行QT serialport然后main.cpp里写#include QCoreApplication #include QSerialPort #include QSerialPortInfo #include QDebug int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 列出所有可用串口 const auto ports QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : ports) { qDebug() Port: info.portName() Description: info.description() Manufacturer: info.manufacturer(); } QSerialPort serial; serial.setPortName(QStringLiteral(/dev/ttyUSB0)); serial.setBaudRate(QSerialPort::Baud115200); serial.setDataBits(QSerialPort::Data8); serial.setParity(QSerialPort::NoParity); serial.setStopBits(QSerialPort::OneStop); serial.setFlowControl(QSerialPort::NoFlowControl); if (!serial.open(QIODevice::ReadWrite)) { qCritical() Open failed: serial.errorString(); return -1; } // 连接数据到达信号 QObject::connect(serial, QSerialPort::readyRead, [serial]() { QByteArray data serial.readAll(); qDebug() Received: data; }); // 发送一条测试数据 serial.write(hello\r\n); return a.exec(); }这个工程跑起来后如果串口有回环TX 接 RX就能看到自己发的数据被读回来。如果没有回环可以用socat虚拟串口对一端跑这个程序另一端用echo发数据。验证通过后再往工程里加协议解析、超时重传这些逻辑心里就有底了。4. 避坑与排查串口开发中那些让人抓狂的瞬间4.1 打开成功但收不到数据现象open()返回 truewrite()也返回了字节数但readyRead信号永远不触发或者触发后readAll()返回空。原因最常见的是流控配置不对。如果设备端开了硬件流控RTS/CTS而代码里设的是NoFlowControl数据可能被设备端挂起。另一个原因是QSocketNotifier没有正确关联到事件循环比如在非 GUI 线程里创建了QSerialPort但没有跑exec()。解决先确认flowControl设置和设备端一致。然后在open()之后手动调一次waitForReadyRead(100)如果这个能读到数据而readyRead不触发说明事件循环有问题。检查QSerialPort对象是否在拥有事件循环的线程里创建。4.2 Linux 下权限不足现象open()返回 falseerrorString()显示Permission denied。原因Linux 下串口设备默认属于dialout组普通用户没有读写权限。解决把当前用户加入dialout组sudo usermod -aG dialout $USER # 需要重新登录生效或者临时用sudo chmod 666 /dev/ttyUSB0但重启后会失效。嵌入式环境里更常见的做法是写 udev 规则在/etc/udev/rules.d/下加一条KERNELttyUSB*, MODE06664.3 Windows 下端口名超过 COM9 打不开现象在 Windows 上setPortName(COM10)之后open()失败但 COM1 到 COM9 都正常。原因Win32 API 对 COM10 及以上的端口名要求用\\.\COM10这种特殊路径格式直接传COM10会被当成普通文件名。解决qtserialport 的 Windows 实现里已经处理了这个问题但如果你在代码里手动拼路径需要加前缀。检查qserialport_win.cpp里的portName处理逻辑确认你用的版本已经包含这个修复。如果用的是很老的 Qt 版本升级到 5.9 以上。4.4 关闭串口时程序卡死现象调用close()后程序无响应或者析构QSerialPort对象时卡住。原因在readyRead槽函数里直接调close()而QSocketNotifier还在触发状态导致递归或死锁。另一个可能是waitForBytesWritten在关闭过程中被调用。解决不要在信号槽里直接关闭串口用QMetaObject::invokeMethod投递到事件循环下一轮再关。关闭前先disconnect所有信号再clear()缓冲区最后close()。4.5 虚拟串口对收不到数据现象用socat创建了虚拟串口对一端写另一端读不到。原因socat的pty模式默认会回显而且两端都是raw模式时数据不会自动刷新。另外如果程序里设了Baud115200但socat没指定速率某些系统上会不匹配。解决用socat -d -d pty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1创建固定路径的虚拟串口对echo0关掉回显。测试时先用cat /tmp/ttyV1确认另一端能收到原始数据再跑 Qt 程序。5. 二次开发改造从源码里挖出可复用的设计5.1 给 QSerialPort 加一个超时重传层qtserialport 本身不提供协议层的重传机制但它的异步模型很适合在上面包一层。我一般会写一个SerialTransport类持有QSerialPort内部维护一个发送队列和超时定时器class SerialTransport : public QObject { Q_OBJECT public: explicit SerialTransport(QObject *parent nullptr) : QObject(parent), m_serial(new QSerialPort(this)) { // 超时定时器单次触发 m_timeoutTimer.setSingleShot(true); connect(m_timeoutTimer, QTimer::timeout, this, SerialTransport::onTimeout); // 数据到达时先攒到缓冲区 connect(m_serial, QSerialPort::readyRead, this, SerialTransport::onReadyRead); } bool send(const QByteArray frame, int retries 3) { if (!m_serial-isOpen()) return false; m_pendingFrame frame; m_retriesLeft retries; return doSend(); } private slots: void onTimeout() { if (m_retriesLeft-- 0) { // 重传 doSend(); } else { emit sendFailed(m_pendingFrame); } } void onReadyRead() { m_rxBuffer.append(m_serial-readAll()); // 这里做帧解析解析到完整帧后停掉超时定时器 // ... } private: bool doSend() { qint64 written m_serial-write(m_pendingFrame); if (written m_pendingFrame.size()) { // 启动超时等待应答 m_timeoutTimer.start(500); return true; } return false; } QSerialPort *m_serial; QTimer m_timeoutTimer; QByteArray m_pendingFrame; QByteArray m_rxBuffer; int m_retriesLeft 0; };这个类的关键在于发送后启动一个 500ms 的超时定时器如果在超时前收到了完整应答帧就停掉定时器否则重传重传次数用完后发sendFailed信号。帧解析部分需要根据具体协议来写常见做法是用状态机或者查找固定帧头帧尾。5.2 跨平台编译时怎么处理平台差异如果你要把 qtserialport 交叉编译到嵌入式 Linux.pro文件里需要根据目标平台调整# 在 qtserialport.pro 或你的工程文件里 linux { # 嵌入式 Linux 可能需要指定 sysroot QMAKE_CFLAGS --sysroot$$SYSROOT QMAKE_CXXFLAGS --sysroot$$SYSROOT # 链接 termios 库 LIBS -ltermios } win32 { # Windows 下需要链接 setupapi LIBS -lsetupapi }qtserialport 的源码里已经用#ifdef Q_OS_WIN、#ifdef Q_OS_LINUX做了条件编译你不需要改源码只需要确保编译时Q_OS_*宏定义正确。交叉编译时最容易翻车的地方是QSocketNotifier在目标平台上的实现——有些嵌入式 Qt 版本裁剪了QSocketNotifier需要确认QT_CONFIG(socketnotifier)是开启的。5.3 一个验证改造是否成功的小技巧改完源码或者包了一层之后怎么确认没引入新问题我的习惯是写一个「回环测试」用socat创建虚拟串口对一端跑被测程序另一端跑一个 Python 脚本模拟设备应答。Python 脚本用pyserial读数据收到特定帧后回复预设应答并记录时间戳。这样能同时验证发送、接收、超时重传三条路径。# 模拟设备端的 Python 脚本 import serial import time # 打开虚拟串口的另一端 ser serial.Serial(/dev/pts/4, 115200, timeout1) while True: data ser.read(1024) if data: print(f[{time.time():.3f}] RX: {data.hex()}) # 简单应答原样返回 ser.write(bACK: data) print(f[{time.time():.3f}] TX: ACK:{data.hex()})跑通这个回环后再改参数、改协议、改重传策略心里都有底。从那以后我每次动串口相关的代码都强制走一遍「虚拟串口对 回环脚本」的验证流程比接真实设备省事得多也不会因为手头没硬件就拖着不测。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站