1. 项目背景与核心需求
作为一名嵌入式开发者,我经常需要和各类串口设备打交道。调试过程中最头疼的就是找不到趁手的串口工具——要么功能太简单,要么界面复杂难用。去年开始接触QT框架后,我就萌生了自己开发一个调试助手的想法。这个系列文章记录了我从零开始用QT开发串口工具的全过程,今天这篇重点分享数据收发功能的实现细节。
串口调试工具的核心需求其实很明确:
- 稳定可靠的串口连接管理
- 灵活的数据收发功能
- 直观的数据显示界面
- 常用调试功能集成(如定时发送、数据记录等)
2. 开发环境准备
2.1 QT环境配置
我使用的是QT 5.15.2 LTS版本,这个版本稳定性好,社区支持完善。安装时记得勾选以下组件:
- Qt Creator 4.14.0
- Qt Charts(用于后期数据可视化)
- Qt SerialPort(核心串口模块)
注意:如果使用MSVC编译器,需要额外安装对应的Windows SDK。我推荐使用MinGW 8.1.0,对串口支持更友好。
2.2 工程创建与基础配置
在Qt Creator中新建Widgets Application项目时,记得在.pro文件中添加:
qmake复制QT += serialport widgets
这个配置会自动链接串口模块和界面组件库。
3. 串口通信核心实现
3.1 串口类封装
QT提供了QSerialPort类,但直接使用会比较繁琐。我封装了一个SerialPortHelper类,主要功能包括:
cpp复制class SerialPortHelper : public QObject {
Q_OBJECT
public:
explicit SerialPortHelper(QObject *parent = nullptr);
// 串口配置
bool openPort(const QString &portName,
QSerialPort::BaudRate baudRate,
QSerialPort::DataBits dataBits,
QSerialPort::Parity parity,
QSerialPort::StopBits stopBits);
void closePort();
// 数据收发
qint64 sendData(const QByteArray &data);
QByteArray readData();
// 状态查询
bool isOpen() const;
QString errorString() const;
private:
QSerialPort *m_serial;
};
3.2 数据收发实现
发送数据的核心代码:
cpp复制qint64 SerialPortHelper::sendData(const QByteArray &data) {
if(!m_serial || !m_serial->isOpen()) {
qWarning() << "Port not open!";
return -1;
}
qint64 bytesWritten = m_serial->write(data);
if(bytesWritten == -1) {
qWarning() << "Write failed:" << m_serial->errorString();
} else if(!m_serial->waitForBytesWritten(1000)) {
qWarning() << "Write timeout:" << m_serial->errorString();
}
return bytesWritten;
}
接收数据采用信号槽机制:
cpp复制// 在openPort中连接信号
connect(m_serial, &QSerialPort::readyRead,
this, &SerialPortHelper::handleReadyRead);
void SerialPortHelper::handleReadyRead() {
QByteArray data = m_serial->readAll();
while(m_serial->waitForReadyRead(10))
data += m_serial->readAll();
emit dataReceived(data);
}
3.3 性能优化技巧
- 缓冲区设置:
cpp复制m_serial->setReadBufferSize(1024 * 1024); // 1MB缓冲区
对于高速串口(如115200以上波特率),适当增大缓冲区可避免数据丢失。
- 定时发送实现:
cpp复制// 在界面类中
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, [this](){
QString text = ui->sendEdit->text();
if(!text.isEmpty()) {
m_port->sendData(text.toLocal8Bit());
}
});
// 启动定时发送
void MainWindow::on_timerSendCheck_clicked(bool checked) {
if(checked) {
int interval = ui->intervalSpin->value();
m_timer->start(interval);
} else {
m_timer->stop();
}
}
4. 界面设计与功能集成
4.1 主界面布局
使用QT Designer设计的主界面包含:
- 串口配置区(端口选择、波特率等)
- 数据发送区(文本输入、发送按钮)
- 数据接收区(显示窗口)
- 状态栏(连接状态、数据统计)
关键控件:
cpp复制// 接收显示使用QPlainTextEdit
ui->recvEdit->setReadOnly(true);
ui->recvEdit->setWordWrapMode(QTextOption::NoWrap);
// 波特率下拉框初始化
QList<qint32> baudRates = QSerialPortInfo::standardBaudRates();
foreach (qint32 baud, baudRates) {
ui->baudCombo->addItem(QString::number(baud), baud);
}
ui->baudCombo->setCurrentText("115200");
4.2 数据格式处理
支持多种格式显示和发送:
cpp复制// 十六进制发送
QByteArray MainWindow::formatSendData() {
QString text = ui->sendEdit->text();
if(ui->hexSendCheck->isChecked()) {
return QByteArray::fromHex(text.toLatin1());
}
return text.toLocal8Bit();
}
// 十六进制显示
void MainWindow::displayData(const QByteArray &data) {
QString displayText;
if(ui->hexDisplayCheck->isChecked()) {
displayText = data.toHex(' ').toUpper();
} else {
displayText = QString::fromLocal8Bit(data);
}
ui->recvEdit->appendPlainText(displayText);
}
5. 常见问题与解决方案
5.1 串口无法打开
可能原因及排查:
- 端口被占用:检查设备管理器,重启设备
- 权限问题(Linux/Mac):需要将用户加入dialout组
bash复制sudo usermod -a -G dialout $USER - 驱动问题:更新CH340/CP210x等常用转换芯片驱动
5.2 数据接收不完整
优化方案:
- 增加接收超时判断:
cpp复制while(m_serial->waitForReadyRead(50)) {
data += m_serial->readAll();
}
- 使用高精度定时器(QElapsedTimer)检测数据间隔
5.3 中文乱码处理
编码转换方案:
cpp复制// 发送时指定编码
QByteArray sendData = text.toUtf8(); // 或 toLocal8Bit()
// 接收时转换
QString decodedText = QString::fromUtf8(data);
// 或根据设备编码选择
// QString::fromLocal8Bit()
// QString::fromLatin1()
6. 功能扩展思路
6.1 数据记录功能
添加文件记录功能:
cpp复制void MainWindow::startRecording() {
m_logFile.setFileName(QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss.log"));
if(!m_logFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Open log file failed!";
return;
}
m_logStream.setDevice(&m_logFile);
}
void MainWindow::logData(const QByteArray &data) {
if(m_logFile.isOpen()) {
m_logStream << QDateTime::currentDateTime().toString("[hh:mm:ss.zzz] ");
m_logStream << data << "\n";
}
}
6.2 波形显示功能
利用QT Charts实现简单波形显示:
cpp复制// 初始化图表
QChart *chart = new QChart();
QLineSeries *series = new QLineSeries();
chart->addSeries(series);
// 数据更新
void updateChart(qreal value) {
static qreal x = 0;
series->append(x++, value);
if(series->count() > 100) {
chart->scroll(1, 0);
}
}
6.3 多端口监控
扩展SerialPortHelper支持多实例:
cpp复制QList<SerialPortHelper*> m_ports;
void MainWindow::addPortMonitor(const QString &portName) {
SerialPortHelper *port = new SerialPortHelper(this);
connect(port, &SerialPortHelper::dataReceived,
this, &MainWindow::handlePortData);
m_ports.append(port);
}
7. 实际调试心得
-
波特率适配:有些国产设备标称115200,实际可能需要57600才能正常通信,遇到乱码时可以尝试调整
-
流控设置:与某些PLC通信时需要启用硬件流控(RTS/CTS)
cpp复制m_serial->setFlowControl(QSerialPort::HardwareControl); -
线程安全:长时间大数据量收发建议放在子线程中,避免界面卡顿
cpp复制QThread *serialThread = new QThread; m_port->moveToThread(serialThread); serialThread->start(); -
性能测试:实际测试发现,在Windows平台下,QT的串口性能比原生API差约15%,但对大多数调试场景完全够用
-
跨平台注意:Linux下串口设备路径为"/dev/ttyS*"或"/dev/ttyUSB*",MacOS为"/dev/cu.*",需要做路径兼容处理
这个串口工具现在已经成了我的主力调试工具,后续还计划加入脚本自动化、协议解析等功能。开发过程中最大的体会是:好的工具不在于功能多复杂,而在于能否真正解决实际问题。建议大家在开发时先聚焦核心需求,再逐步扩展功能。