目录
[二、单片机与 Qt 上位机的通信方式](#二、单片机与 Qt 上位机的通信方式)
[(一)使用 QT 上位机和 STC 单片机实现串口通信](#(一)使用 QT 上位机和 STC 单片机实现串口通信)
[三、单片机 Qt 上位机的优势](#三、单片机 Qt 上位机的优势)
[(一)高效便捷的 USB 通信上位机解决方案](#(一)高效便捷的 USB 通信上位机解决方案)
[(二)学习上位机软件对 C 语言开发的启示](#(二)学习上位机软件对 C 语言开发的启示)
[(三)选择 C# 还是 Qt 作为上位机开发工具](# 还是 Qt 作为上位机开发工具)
[四、单片机 Qt 上位机案例](#四、单片机 Qt 上位机案例)
[(一)简单的串口上位机控制单片机 LED](#(一)简单的串口上位机控制单片机 LED)
[(二)使用 QT 制作单片机 bootloader 上位机实现 IAP](#(二)使用 QT 制作单片机 bootloader 上位机实现 IAP)
[(三)QT 上位机 + STC 单片机实现串口通信](#(三)QT 上位机 + STC 单片机实现串口通信)
[五、单片机 Qt 上位机常见问题及解决方法](#五、单片机 Qt 上位机常见问题及解决方法)
[(一)基于 Qt 的上位机制作过程中遇到的问题及解决 ------TCP 通讯的建立](#(一)基于 Qt 的上位机制作过程中遇到的问题及解决 ——TCP 通讯的建立)
[(三)在使用 QT 制作上位机界面时,遇到的常见问题](#(三)在使用 QT 制作上位机界面时,遇到的常见问题)
一、引言
单片机与 Qt 上位机的结合在嵌入式系统开发中具有重要意义。Qt 作为一种强大的跨平台图形用户界面开发框架,可以为单片机系统提供直观、便捷的人机交互界面,实现对单片机的控制和数据监测。本文将详细介绍单片机与 Qt 上位机的通信方式、优势、案例以及常见问题及解决方法。
通信方式
单片机与 Qt 上位机之间的通信主要通过串口实现。串口通信是一种简单、稳定、可靠的通信方式,其原理是通过串口发送端口将数据转换为串行数据,并将其发送到接收端口,接收端口将接收到的串行数据还原成原始数据。在单片机与 Qt 上位机之间进行串口通信时,需要确定好使用的波特率、数据位、停止位和校验位等参数,以保证数据传输的正确性。
优势
- 强大的人机交互界面:Qt 上位机可以提供丰富的图形界面和交互功能,使操作人员能够直观地了解单片机系统的运行状态,并进行方便的控制操作。
- 跨平台性:Qt 支持多种操作系统,使得开发的上位机软件可以在不同的平台上运行,提高了软件的通用性和可移植性。
- 高效的数据处理能力:Qt 上位机可以利用计算机的强大计算能力,对从单片机接收到的数据进行快速处理和分析,为系统的决策提供支持。
- 易于开发和维护:Qt 提供了丰富的开发工具和库函数,使得上位机软件的开发变得更加简单和高效。同时,Qt 的良好封装性和模块化设计也使得软件的维护更加容易。
案例
- QSerialPort 实现上位机和单片机串口通信模块
-
- 背景知识:项目需要上位机控制单片机执行任务,单片机会发送心跳包和任务指令到上位机,因此采用全双工模式,使用 Qt 自带 QSerialPort 实现。
-
- 设计思路:将通信模块的发送和数据接收实现在次线程,数据解析另起线程,处理数据后将处理结果发送到 UI 线程。UI 线程发送指令信号,次线程槽函数中将指令转变为通信协议数据,放置于发送数据队列,利用该线程中的定时器定时发送;QSeriealPort 接收数据的槽函数负责接收数据,并在其实现内将接收到的数据进行解析。设计了 DeviceManager、DeviceControl、DeviceDataRecete 三个类。
-
- 调试运行:UI 线程通过 DeviceManager 发送任务命令信号,DeviceControl 在次线程中执行命令转换为通信协议数据,通过定时进行发送,同时接收串口数据,并在 DeviceControl 的接收槽函数的读取并推入 DeviceDataRecete 的数据队列,DeviceDataRecete 对数据进行解析,并将结果通过 DeviceControl 发送到 DeviceManager 所在的 UI 线程。
-
- 结果分析:测试显示可正常运行,该模式适合实时通信,在效率方面可以,DeviceDataRecete 线程可以使用条件变量。
- STM32 单片机如何处理 QT 上位机串口中发过来的数据?
-
- 配置串口通信参数:定义串口参数变量,设置串口的参数,配置串口中断。
-
- 接收数据:编写串口接收数据的代码,启动串口接收中断,每当接收到一帧数据时,就会自动触发回调函数,并将接收到的数据存储在 uart_rx_data 变量中。
-
- 处理数据:读取接收缓冲区中的数据,判断接收到的数据类型,根据数据类型进行不同的操作。
-
- 发送数据:编写串口发送数据的代码,在需要发送数据时,调用串口发送函数即可。
- 串口接收数据分包问题处理(QT 上位机 / 单片机等)
-
- 设计思路:数据的格式是头 0XA4 0X4A,尾部是 0X3C 0X3C,里面包含数据数据长度和帧校验等相关定义。采用每来一次数据,识别当前数据是否有尾部,如果有,则认为当前数据是一包,则再将缓冲数据进行头部识别,识别不到头,证明是错误数据,不进行处理。识别成功,则调用相应处理函数。若数据中没有尾部,则将数据放入缓存。若数据最后一位是 0X3C,则做好标记,下一包数据来临时优先判断第一个字节是不是 0X3C。若单次接收包含了多包数据也应能处理。
-
- 代码参考:给出了 QT 代码参考,包括多个函数的实现,用于处理串口接收数据分包问题。
- qt 串口通信上位机遇到的问题
-
- 问题描述:自己尝试用 qt 写了一个串口,可以接受数据,但是发送数据的时候遇到了一点问题,发送时数据不能到单片机,但是在用 xcom 试的时候,之前在自己写的串口上位机中发送的数据也会在 xcom 中显示出来。
-
- 可能原因及解决方法:检查串口设置是否正确,包括波特率、数据位、校验位、停止位等;确认数据格式是否正确,发送的数据格式是否与单片机接收的格式一致;检查程序逻辑是否正确,针对程序整体进行调试和分析,确认程序逻辑是否有错误。同时给出了一个简单的 Qt 串口通信上位机示例代码。
- 【QT 自研上位机 与 ESP32 下位机联调>>>串口控制 GPIO - 基础样例】
-
- 概述:作为新手入门相关设备,打算出三章内容,像之前调试 STM32 单片机一样,本次第三章是和上位机进行联合调试。包括串口基础篇、结合 GPIO 进行外围控制、与上位机联合调试。
-
- 实验环境:介绍了 ESP32 的相关信息,包括芯片特点、硬件信息、调试环境等。
-
- 自我总结:有了调试串口和 IO 的经验,将两者组合起来,就像学习东西一块一块学,以小见大。同时提到了之前的联合文章经历。
-
- 实验过程:验证上位机 QT 程序,包括下载样例代码、修改 qt 程序、运行测试验证;验证下位机 ESP32 程序,包括下载样例代码、更改 ESP32 代码,编译下载、验证;联合调试 ESP32 和 qt 上位机,包括硬件连接、验证。
-
- 代码连接和细节部分:给出了代码连接,介绍了常见错误解决办法、ESP32 工程下载后先清理、逻辑分析仪的使用等细节部分。
-
- 总结:总结了本次实验的内容和意义。
- 单片机真的不如上位机吗?
-
- 单片机和上位机在各自的领域都有着不可替代的作用,不能简单地进行优劣比较。单片机具有体积小、成本低、功耗低等特点,适合对尺寸和成本有严格限制的场合;上位机则凭借其更强大的计算能力和丰富的软件资源,适用于需要大规模数据处理和复杂控制的场景。在实际应用中,单片机和上位机通常是相辅相成的。
- 高效便捷:基于 QT 5 与 STM32 的 USB 通信上位机解决方案
-
- 项目介绍:提供了一个基于 QT 5 的上位机程序,用于与 STM32 单片机进行 USB 通信。能够实时显示从单片机发送的命令,实现高效的数据交互和监控。
-
- 项目技术分析:介绍了技术栈、技术优势和应用场景。技术栈包括 QT 5、STM32 和 USB 通信;技术优势有实时性、易用性、兼容性;应用场景包括嵌入式系统开发、物联网设备调试和工业自动化控制。
-
- 项目特点:高效通信、动态显示、用户友好、广泛适用。
-
- 结语:期待用户的使用和反馈,共同推动技术的进一步发展。
- 基于 GD32E503 的直流数控电源
-
- 作品设计:分为硬件设计和软件设计,硬件设计主要分为 MCU 控制板模块、DC-DC 模块和快充模块三个部分,软件设计分为逻辑代码的设计和上位机设计。上位机采用 QT 编写,可以与 GD32 单片机进行交互,单片机将电压、电流等信息通过串口发送给上位机,上位机进行接收并实时绘制动态波形。
-
- 作品创新:具有恒压、恒流输出,纹波小,精度高,采用补偿算法和中位值滤波算法减小误差,做到了百分之百国产化替代,增加了快充、物联网、存储功能。
-
- 测试分析:包括实物展示、性能测试、快充测试和获奖评语。
- QT 上位机 + STC 单片机实现串口通信
-
- 未详细描述具体内容。
- Qt5 实现与单片机 ATS89S51 通信
-
- 测试环境:开发环境为 Qt5.96 Mingw32-bit 和 keil3。
-
- 项目目标:实现下位机基于 STC 单片机控制 LED 灯模块、独立键盘模块,实现基于 Qt 的上位机与下位机进行串口通信,通过上位机发送指令对下位机进行控制,并通过界面方式显示。
-
- 实现效果:展示了参数配置窗口、上位机控制界面等效果。
-
- 关键通信类 QSerialport:介绍了这个类的作用和使用注意事项,给出了单片机参考代码和上位机通信代码的获取方式。
- #4 使用 QT 制作单片机 bootloader 上位机实现 IAP
-
- QT 介绍:QT 是一个跨平台的 C++ 图形用户界面库,支持所有 Linux/Unix 系统和 Windows 平台。开发上位机需要熟悉 QT 的基本操作、C++ 基础语法等。
-
- QT 分解 HEX 文件:介绍了 HEX 文件的格式含义和分解思路,给出了具体的代码实现,包括读取和解析 HEX 文件、将 HEX 文件的每一行读入二维数组、提取需要的信息、加入自定义指令等。
常见问题及解决方法
- 串口通信问题
-
- 发送数据时数据不能到单片机:检查串口设置是否正确,包括波特率、数据位、校验位、停止位等;确认数据格式是否正确,发送的数据格式是否与单片机接收的格式一致;检查程序逻辑是否正确,针对程序整体进行调试和分析,确认程序逻辑是否有错误。
-
- 串口接收数据分包问题:采用合适的分包处理方法,如每来一次数据,识别当前数据是否有尾部,如果有,则认为当前数据是一包,则再将缓冲数据进行头部识别,识别不到头,证明是错误数据,不进行处理。识别成功,则调用相应处理函数。若数据中没有尾部,则将数据放入缓存。若数据最后一位是 0X3C,则做好标记,下一包数据来临时优先判断第一个字节是不是 0X3C。若单次接收包含了多包数据也应能处理。
- 晶振频率问题
-
- 单片机的晶振频率不是 11.0592MHz,导致发送信息出现误差:使用串口误差计算器计算,调整单片机的定时计数器的溢出值,确保上位机和单片机的波特率一致。
- 文件解析问题
-
- 在使用 QT 制作单片机 bootloader 上位机实现 IAP 时,分解 HEX 文件出现问题:理解 HEX 文件的格式含义,按照正确的思路进行文件解析,注意 QT 库函数的作用,确保代码正确实现。
二、单片机与 Qt 上位机的通信方式
(一)使用 QT 上位机和 STC 单片机实现串口通信
下位机完成数据生成和发送,上位机存储和处理数据。通过光敏和热敏传感器进行 A/D 采集,将数据封装成数据包发送到上位机。
在使用 QT 上位机和 STC 单片机进行串口通信时,通常采用以下步骤实现:
1. 硬件连接
确保 STC 单片机与计算机正确连接,可以通过 USB 转串口线等方式进行连接。
2. 配置串口参数
对于下位机 STC 单片机,需要进行串口配置。例如,设置串口工作方式、波特率、数据位、停止位和校验位等。常见的配置如下:
void UsartConfiguration()
{
SCON = 0X50; // 设置为工作方式10101 0000
TMOD = 0X20; // 设置计数器工作方式2
PCON = 0X80; // 波特率加倍,波特率加倍加快传输速率但是如果232总线太长时有可能出现丢帧
TH1 = 0XF4; // 计数器初始值设置,注意波特率是4800的
TL1 = 0XF4;
REN = 1; // 允许接受
ES = 1; // 打开接收中断
EA = 1; // 打开总中断
TR1 = 1; // 打开计数器 T1
}
对于上位机 QT,也需要配置相应的串口参数,确保与单片机的参数一致。可以通过以下方式设置:
m_serialPort = new QSerialPort();
m_serialPort->setPortName("COM8");
m_serialPort->setBaudRate(QSerialPort::Baud9600);
m_serialPort->setDataBits(QSerialPort::Data8);
m_serialPort->setParity(QSerialPort::NoParity);
m_serialPort->setStopBits(QSerialPort::OneStop);
m_serialPort->setFlowControl(QSerialPort::NoFlowControl);
m_serialPort->setReadBufferSize(40960);
m_serialPort->open(QIODevice::ReadWrite);
3. 数据发送与接收
下位机 STC 单片机可以通过以下方式发送数据:
void Uart_Send_Byte(unsigned char uartData)
{
SBUF = uartData;
while(!TI);
TI = 0;
}
上位机 QT 可以通过以下方式接收数据:
connect(ui->lineEdit, SIGNAL(editingFinished()), this, SLOT(send_data()));
void Widget::send_data()
{
// 将数据转换为 QByteArray 类型并发送
uint8_t data_10 = ui->lineEdit->text().toInt();
QString s = QString::number(data_10, 16);
if(s.length() == 1)
{
s = '0' + s;
}
QByteArray ba = QString2Hex(s);
m_serialPort->write(ba);
}
同时,上位机也可以通过定时器定时接收数据,并进行数据解析。例如:
// 串口接收数据操作
// 串口接收数据帧格式为:帧头'*' 帧尾'#' 数字间间隔符号',' 符号全为英文格式
void Widget::Read_Date()
{
int bufferlens = 0; // 帧长
QString str = ui->Receive_text_window->toPlainText();
timerserial->stop();
QByteArray bufferbegin = "*"; // 帧头
int index = 0;
QByteArray bufferend = "#"; // 帧尾
int indexend = 0;
QByteArray buffercashe;
index = buffer.indexOf(bufferbegin, index); // 查找帧头
indexend = buffer.indexOf(bufferend, indexend); // 查找帧尾
if((index < buffer.size()) && (indexend < buffer.size()))
{
bufferlens = indexend - index + 1; // 计算帧长度
buffercashe = buffer.mid(index, bufferlens); // 截取出数据帧
}
char recvdata[buffercashe.size()];
memset(recvdata, 0, sizeof(recvdata));
memcpy(recvdata, buffercashe.data(), bufferlens - 1);
recvdata[buffercashe.size() - 1] = 35;
if(recvdata[0] == '*' && recvdata[buffercashe.size() - 1] == '#') // 二次帧检查
{
str_to_num(recvdata); // 更新数据并缓存到保存区
str += "succeed:"; // 在文本窗口给出提示
str += tr(buffercashe);
str += " ";
ui->Receive_text_window->clear();
ui->Receive_text_window->append(str);
}
else
{
str += "error!";
}
}
4. 注意事项
在进行串口通信时,需要注意以下几点:
- 确保上位机和单片机的波特率一致。如果单片机的晶振频率不是标准的 11.0592MHz,可能会导致发送信息出现误差。此时可以使用串口误差计算器计算,调整单片机的定时计数器的溢出值,确保上位机和单片机的波特率一致。
- 注意数据格式的正确性。发送的数据格式应与接收方的格式一致,避免出现数据解析错误。
- 处理好串口接收数据分包问题。可以采用合适的分包处理方法,如每来一次数据,识别当前数据是否有尾部,如果有,则认为当前数据是一包,则再将缓冲数据进行头部识别,识别不到头,证明是错误数据,不进行处理。识别成功,则调用相应处理函数。若数据中没有尾部,则将数据放入缓存。若数据最后一位是 0X3C,则做好标记,下一包数据来临时优先判断第一个字节是不是 0X3C。若单次接收包含了多包数据也应能处理。
三、单片机 Qt 上位机的优势
(一)高效便捷的 USB 通信上位机解决方案
基于 QT 5 与 STM32 的 USB 通信上位机具有诸多优势,能够满足多种场景需求。
1. 实时性:上位机能够动态显示从单片机发送的命令,确保数据的及时性和准确性,让用户可以实时了解系统状态,便于及时做出决策和调整。
2. 易用性:QT 5 框架提供了直观的用户界面,操作简单易行。即使是非专业人士也能轻松上手,降低了使用门槛,提高了工作效率。
3. 兼容性:支持多种 STM32 单片机型号,适应不同的嵌入式系统需求。具有广泛的适用性,无论是在嵌入式系统开发、物联网设备调试还是工业自动化控制等领域,都能发挥重要作用。
4. 应用场景广泛:
- 在嵌入式系统开发过程中,开发者需要频繁地与底层硬件进行交互。通过本上位机程序,开发者可以实时监控单片机的状态,快速调试和优化系统性能。
- 物联网设备的调试往往涉及大量的数据传输和监控。本项目提供的 USB 通信解决方案,能够帮助开发者高效地进行数据采集和分析,提升调试效率。
- 在工业自动化控制领域,实时监控和数据交互是确保系统稳定运行的关键。通过本项目,工程师可以轻松实现对工业设备的远程监控和控制,提高生产效率。
(二)学习上位机软件对 C 语言开发的启示
上位机软件的开发思路对 C 语言开发有很多启示。
1. 展示强大的单片机软件:上位机可以将强大的单片机软件展示出来,让用户更直观地了解系统的功能和性能。在工业自动化控制等场景中,上位机软件能够呈现出单片机系统的运行状态,方便用户进行监控和操作。
2. 方便软件调试时读写参数:在软件调试过程中,上位机软件可以提供便捷的参数读写功能。例如,当进行运动控制软件调试时,PID 参数可能需要在现场实时调整。如果使用上位机软件,就可以轻松地修改参数,无需一遍遍烧写程序或通过调试软件不断地发协议,提高了调试效率。
3. 学习高级语言的面向对象设计思想:上位机开发总离不开学习高级语言,虽然很多做底层的人可能对高级语言有偏见,但不可否认的是,高级语言确实有很重要的思想在里面,其中 "面向对象" 的设计思想尤为重要。如果掌握了这种思想,并将其应用于 C 语言的开发中,对 C 语言开发会非常有帮助。
(三)选择 C# 还是 Qt 作为上位机开发工具
在选择上位机开发工具时,需要考虑多个因素,根据具体需求和偏好做出选择。
1. 跨平台支持:
- 如果应用程序需要在多个操作系统上运行,Qt 可能是更好的选择。Qt 具有强大的跨平台能力,可以帮助开发具备一致性和可移植性的应用程序,适用于 Windows、Linux、macOS 等多个平台。
2. GUI 开发:
- C# 拥有 Windows 窗体应用程序和 WPF 等功能强大的 GUI 工具,可以快速构建用户界面。C# 语法简洁,易于学习和使用,特别是对于.NET 框架的初学者。
- Qt 则提供了丰富的 GUI 组件和工具包,可用于构建漂亮且跨平台的界面。Qt 的 QML 集成支持使用 QML 来构建现代、动态的用户界面,可以实现复杂的 UI 设计和动画效果。
3. 学习曲线和经验:
- 如果你已经熟悉 C# 或 C++,选择相应的框架可能更容易上手。如果你在 C# 或 C++ 中有特定的偏好或经验,可以根据自己的喜好选择。
- C# 语法简洁,对于初学者来说可能更容易学习。而 Qt 使用 C++ 作为主要的编程语言,如果你对 C++ 比较熟悉,那么 Qt 可能更适合你。
4. 生态系统和资源:
- C# 和 Qt 都拥有庞大的开发者社区和丰富的资源。考虑查阅相关的教程、文档和论坛,了解它们的生态系统和可用资源,这有助于你在学习和开发过程中获得支持和帮助。
- C# 在 Windows 平台上有强大的 IDE 支持,如 Visual Studio,提供了智能感知、调试工具等功能。Qt 也有丰富的开发工具和库函数,以及详细的文档支持。
5. 性能要求:
- 如果你对性能有较高要求,C++ 通常比 C# 更接近底层,可能在某些情况下具有优势。Qt 使用 C++ 作为主要的编程语言,因此在性能方面可能更有优势。
- 不过,在实际应用中,性能差异可能并不明显,具体取决于应用程序的需求和使用场景。
综上所述,选择 C# 还是 Qt 作为上位机开发工具需要权衡以上因素,并根据具体需求和偏好做出选择。你也可以考虑根据项目需要,综合使用 C# 和 Qt,例如使用 C# 开发 Windows 平台上的应用程序,或使用 Qt 进行跨平台开发。
四、单片机 Qt 上位机案例
(一)简单的串口上位机控制单片机 LED
学 QT 时写的一个简单串口上位机,用来控制单片机上的 2 个 LED,一个只有开和关状态,一个可以调节亮度。
通过 Qt 的串口通信功能,可以实现与单片机的交互,从而控制 LED 的状态。对于只有开和关状态的 LED,可以通过发送特定的指令让单片机控制其亮灭。对于可调节亮度的 LED,可以发送不同的指令来调整其亮度级别。
(二)使用 QT 制作单片机 bootloader 上位机实现 IAP
QT 可用于开发跨平台的上位机,介绍了 QT 分解 HEX 文件的方法以及与 MCU 的通信流程。
QT 是一个强大的 C++ 图形用户界面库,支持多种操作系统。在开发单片机 bootloader 上位机实现 IAP(In-Application Programming)时,首先需要熟悉 QT 的基本操作和 C++ 基础语法。
QT 分解 HEX 文件时,先将 HEX 的每一行都读入一个二维数组,一维是 HEX 数据,二维是总的 HEX 行数。然后将不需要的数据删掉,如冒号和最后两位校验码,因为要加入自定义指令,所以用自己封装的校验函数计算校验码。接着将需要的信息,如地址段、data 长度和 data 都提取出来,顺便再加入自定义指令。
在与 MCU 通信时,确定好通信格式,发送前加入自定义指令,并调用校验函数计算出这一包的校验和,发给 MCU。MCU 处理后提取出地址值和 data 段向 FLASH 写入,当最后一包写完时 MCU 即可跳转 app。
(三)QT 上位机 + STC 单片机实现串口通信
第一个上位机小项目,实现 QT 与 STC 单片机的串口通信,可读取温度等数据。
通过 Qt 的 serialport 模块,可以实现与 STC 单片机的串口通信。下位机完成数据生成和发送,上位机存储和处理数据。例如,可以通过光敏和热敏传感器进行 A/D 采集,将数据封装成数据包发送到上位机。
在使用 QT 上位机和 STC 单片机进行串口通信时,需要进行硬件连接和配置串口参数。确保 STC 单片机与计算机正确连接,可以通过 USB 转串口线等方式进行连接。对于下位机 STC 单片机,需要进行串口配置,设置串口工作方式、波特率、数据位、停止位和校验位等。对于上位机 QT,也需要配置相应的串口参数,确保与单片机的参数一致。
数据发送与接收可以通过特定的函数实现。下位机 STC 单片机可以通过发送函数发送数据,上位机 QT 可以通过接收函数接收数据,并进行数据解析。同时,上位机也可以通过定时器定时接收数据,并进行数据解析。
在进行串口通信时,需要注意确保上位机和单片机的波特率一致,注意数据格式的正确性,处理好串口接收数据分包问题。
五、单片机 Qt 上位机常见问题及解决方法
(一)基于 Qt 的上位机制作过程中遇到的问题及解决 ------TCP 通讯的建立
在进行基于 Qt 的上位机制作时,通讯协议的选择至关重要。本部分将详细讲解 TCP 通讯的搭建过程,包括头文件设置、UI 界面设计以及功能实现等方面。
1. 上位机功能与通讯协议选择
该上位机可能用于多种场景,如工业控制、数据监测等。在选择通讯协议时,需要考虑数据传输的可靠性、实时性以及系统的具体需求。一开始可能计划使用 UDP 进行通讯,因为对于某些场景下的数据精度要求不高,UDP 协议相对更高效。然而,由于所使用的设备(如 LiDAR)型号可能不支持 UDP 连接方式,最后决定使用 TCP 进行通讯。
2. TCP 通讯的搭建
2.1 创建类
首先,对项目右键选择 "添加新文件",然后选择 Qt 中的 Qt 设计器界面类,这里可以根据习惯选择,比如选择 Main Window。点击下一步后,输入类名时要注意命名规范,避免后续出现混淆。创建成功后会生成三个文件,分别是头文件(.h)、源文件(.cpp)和界面文件(.ui)。
2.2 头文件设置
进入头文件,需要进行以下操作:
- 引用必要的头文件,如#include <QTcpSocket>,用于 TCP 套接字的操作。
- 创建一个接收信息的函数,函数名可以自定义,例如void receiveMessage(),用于接收 TCP 连接传来的消息。
- 建立一个指针变量QTcpSocket* TcpSocket,以便在后续的代码中使用。
2.3 UI 界面
设计一个大致的 UI 界面,为了测试功能可以先简单布局。在设计 UI 时,建议给每个控件起好对应的名字,尤其是在创建槽函数之前,这样可以避免在控件较多时出现混乱。
2.4 功能实现
2.4.1 建立连接
右键点击建立连接的按钮,选择 "转到槽",选择 "clicked ()" 信号并确定。这样会生成一个由按钮名字和 "_clicked" 组成的函数,例如void tcpSocket::on_pushButton_buildConnect_clicked()。在这个函数中实现建立 TCP 连接的功能:
void tcpSocket::on_pushButton_buildConnect_clicked()
{
QString ipAddress = "192.168.0.1"; //雷达的 IP 地址
quint16 port = 2112; //雷达的端口
TcpSocket->connectToHost(ipAddress, port); //建立连接
if (TcpSocket->waitForConnected(3000)) //等待连接,如果超时则连接失败
{
qDebug() << "Connected to LiDAR";
}
else
{
qDebug() << "Failed to connect to LiDAR";
}
}
在实际使用中,可以先用其他方法验证 IP 是否正确,例如在命令提示符中输入 "ping 你要连接的 ip 地址",如果能 ping 通表示可以连接,但实际连接时还要注意端口号,一般在雷达的官方文档中有写。
2.4.2 发送信息
和建立连接类似,右键点击发送信息的按钮,选择 "转到槽",生成相应的函数。在发送信息的函数中,从控件中读取信息,进行必要的格式转换后,使用TcpSocket->write()函数发送信息。
void tcpSocket::on_pushButton_send_clicked()
{
QString message = ui->textEdit_sendData->toPlainText(); //从控件中读取信息
// 去除空格和换行符
message.remove(" ");
message.remove("\n");
// 将 HEX 字符串转换为字节数组
QByteArray hexData = QByteArray::fromHex(message.toUtf8());
TcpSocket->write(hexData); //发送信息
qDebug() << "发送指令(HEX):" << hexData.toHex(); //验证
}
2.4.3 接收信息
在主函数中添加以下代码,建立readyRead信号和receiveMessage函数的连接,当收到readyRead信号时,执行receiveMessage函数。
connect(TcpSocket, &QTcpSocket::readyRead, this, &tcpSocket::receiveMessage);
receiveMessage函数的代码如下:
void tcpSocket::receiveMessage()
{
// 读取数据
QByteArray response = TcpSocket->readAll();
// 处理接收到的数据
QString messageUTF = QString::fromUtf8(response);
// 显示在文本框中
ui->textEdit_receiveData->append(messageUTF);
}
(二)串口接收数据分包问题处理
在 QT 上位机、单片机等使用情况下,串口数据分包问题是一个常见的难题。本部分将介绍处理串口数据分包问题的思路和代码参考。
1. 设计思路
数据的格式通常有特定的头尾标志,例如头为 0XA4 0X4A,尾部是 0X3C 0X3C,里面还包含数据长度和帧校验等相关定义。处理思路如下:
- 每来一次数据,识别当前数据是否有尾部,如果有,则认为当前数据是一包,则再将缓冲数据进行头部识别。识别不到头,证明是错误数据,不进行处理。识别成功,则调用相应处理函数。
- 若数据中没有尾部,则将数据放入缓存。若数据最后一位是 0X3C,则做好标记,下一包数据来临时优先判断第一个字节是不是 0X3C。
- 若单次接收包含了多包数据也应能处理。
2. 代码参考
以下是 QT 代码参考:
class MainWindow:public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void setupPlot();
void setCVMode();
void setCCMode();
void initActionsConnections();
private slots:
void updateView();
void updateLCDNumber();
void handleTimeout();
void showStatusMessage(const QString &message);
void about();
void openSerialPort();
void closeSerialPort();
void writeData(const QByteArray &data);
void readData();
void SerialRecv_solt();
void process_serial_recv(char*pBuf,int len);
void ComSend(unsigned char cmd1,int var1);
int get_tail(unsigned char*pbuf,int len);
int get_head(unsigned char*pbuf,int len);
int send_copy_from_read(unsigned char*pbuf,int len);
int process_pack(unsigned char* pbuf,int len);
void on_doubleSpinBox_vol_valueChanged(double arg1);
void on_doubleSpinBox_cur_valueChanged(double arg1);
void on_radioButton_cv_clicked();
void on_radioButton_cc_clicked();
void on_pushButton_verify_clicked();
void on_verticalSlider_vol_valueChanged(int value);
void on_verticalSlider_cur_valueChanged(int value);
void on_actionCtrl_triggered(bool checked);
void on_actionUpdate_triggered();
private:
Ui::MainWindow *ui;
QSerialPort *serial;
SettingsDialog *settings;
QVector<double> vol, cur;
QTimer *m_pTimer;
unsigned char data_recv[1024];
int len_pack=0;
int maybe_tail;
};
int MainWindow::get_tail(unsigned char*pbuf,int len)
{
int i=0;
int pos;
for(i=0;i<len;i++)
{
if(pbuf[i]==0xc3)
{
if(i==(len-1))
{
return 0; //疑似拿到了尾部一个字节
}
else
{
if(pbuf[i+1]==0xc3)
pos=i+2; //第几个字节出现了尾部
return pos; //完整的拿到了尾部
}
}
}
return -1; //没有拿到尾部任何信息
}
int MainWindow::get_head(unsigned char*pbuf,int len)
{
int i=0;
for(i=0;i<len;i++)
{
if((pbuf[i]==0xA5)&&(i<(len-1)))
{
if(pbuf[i+1]==0x5A)
return i;
}
}
return -1; //没有头部信息
}
int MainWindow::process_pack(unsigned char* pbuf,int len)
{
int pos_head=0;
memcpy(data_recv+len_pack,pbuf,len);
len_pack+=len;
pos_head=get_head(data_recv,len_pack);
if(pos_head>=0) //找到了头
{
send_copy_from_read(data_recv+pos_head,len_pack-pos_head);
qDebug() << "hello";
}
//相关标志位清零
maybe_tail=0;
len_pack=0;
}
void MainWindow::SerialRecv_solt()
{
QByteArray data;
data.resize(serial->bytesAvailable());
serial->read(data.data(),data.size());
int size_now=0;
int pos_data=0;
int data_num=0;
size_now=data.size();
if((maybe_tail==1)&&((unsigned char)data.data()[0]==0xc3)) //识别到结束位
{
process_pack((unsigned char*)data.data(),1); //将当前包剩余的数据赋值给下一包
if(data.size()>1)
{
memcpy(data_recv+len_pack,&data.data()[1],(data.size()-1));
len_pack+=(data.size()-1);
}
}
else
{
maybe_tail=0;
size_now--;
while(size_now>0)
{
pos_data=data.size()-size_now;
data_num=get_tail((unsigned char*)data.data()+pos_data,size_now);
if(data_num>0) //识别到结束位
{
process_pack((unsigned char*)data.data()+pos_data,data_num);
size_now=data.size()-data_num;
}
else //没有识别到结束位
{
if(data_num==0)
maybe_tail=1;
if((len_pack+data.size())<1024) //限制缓冲大小为 1024 个字节
{
memcpy(data_recv+len_pack,data.data()+pos_data,size_now);
len_pack+=size_now;
}
else
{
len_pack=0; //从头开始
}
break;
}
}
}
}
(三)在使用 QT 制作上位机界面时,遇到的常见问题
在使用 QT 制作上位机界面时,可能会遇到一些常见问题,本部分将针对这些问题进行分析并提供解决方法。
1. 实现上位机界面可拖拽变大变小功能
- 使用布局功能,这些布局功能可嵌套使用。
- 在布局功能的基础上,若想让控件排列整齐,可以在控件间放置特定控件,在属性界面设置高和宽,从而达到对齐效果。
- 布局完毕后,在对象或 UI 设计界面右键选中最外围控件,选择布局 -> 栅格布局。运行生成的界面就可以使用鼠标拖拽功能来改变上位机界面的大小了。
- 若想控件大小自适应,可以在属性中,将 sizePolicy 中的水平策略与垂直策略设置成 Expanding,在设计一个最小尺寸与最大尺寸。
2. 解决最大化按钮灰色问题
在 Qt Designer 中将 maximumSize 的值设置为 16777215x16777215 即可使窗口打开时最大化按钮可用。具体代码如下:
const QSize MAIN_SIZE_MAX = QSize(16777215,16777215);
this->setMaximumSize(MAIN_SIZE_MAX);
this->setWindowFlag(Qt::WindowMaximizeButtonHint, true);
3. 处理不同分辨率电脑显示效果差异问题
在 QT 的 main 函数中,在 "QApplication a (argc, argv);" 之前加上如下几句语句,即可实现 QT 上位机软件适配不同分辨率的电脑。
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true);
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
4. 给图像添加图标
- 首先选择一张图片,将其转化为.ico 文件,可以通过在线工具进行转化。
- 右键选中项目 ->Add New,选择 Qt Resource File,生成一个.qrc 文件。点击 Add Prefix,会在下面生成一个文件,在属性的前缀处可修改路径。然后点击 Add Files 将.ico 图标添加进来。上位机界面的其他控件的图标(如菜单栏、工具栏里的图标),也可以放到这个位置。
- 在程序中,添加如下程序:setWindowIcon(QIcon(":/icon/logo.ico")),并在当前项目目录下创建一个.rc 结尾的文件,在该文件中输入IDI_ICON1 ICON DISCARDABLE "logo.ico"。在该项目的.pro 文件里,添加RC_FILE += logo.rc