STM32与QT串口通信实战:无人超市系统设计与实现
2026/9/2 11:36:32 网站建设 项目流程

简介:本资源是一套完整的无人超市消费系统软硬件协同设计方案,面向嵌入式开发初学者与课程设计实践者,解决RFID会员管理、商品电子标签识别、闸机联动控制等典型物联网应用场景问题。压缩包共164个文件,含Qt上位机源码(.cpp/.ui/.pro)、STM32固件工程(.c/.h/.uvproj/.hex)、可执行程序(.exe)、多语言翻译文件(.qm)、设计文档(.pdf/.doc)及调试依赖库(.dll),整体大小77.95MB,结构清晰,便于分模块学习与移植。目前已有1087人学习下载,配套B站讲解视频与CSDN系列设计文档,覆盖从硬件驱动(RC522读卡、步进电机控制)、协议解析到QT界面逻辑(管理员/顾客双角色、商品上架、充值消费挂失全流程)的完整实现路径,代码注释充分,关键模块如RFID_RC522.c、motor.c、main.c等均具典型参考价值。

1. 项目概述:当STM32遇见QT,一个无人超市的雏形如何诞生

最近在整理过往的项目资料,翻到了去年做的一个“无人超市消费系统”的Demo。这个项目在当时算是一个比较综合的练手项目,它把嵌入式端的硬件控制、数据采集和PC端的上位机软件界面、网络通信给串了起来,形成了一个从“扫码开门”到“自动结算”的完整闭环。项目核心是STM32作为下位机,负责处理各种传感器和执行器,而QT则作为上位机,提供了一个可视化的管理界面和数据库交互层。今天,我就把这个项目的设计思路、实现细节以及踩过的那些坑,系统地梳理一遍,希望能给想做类似物联网或消费电子项目的朋友一些参考。

简单来说,这个系统模拟了一个无人值守的购物场景。顾客通过手机扫码(模拟)打开货柜门,选取商品后关门,系统通过重量传感器(模拟商品被取走)识别拿取的商品,并自动计算消费金额,从顾客的账户(模拟)中扣款,最后在QT上位机上更新库存、交易记录等信息。整个过程无人干预,核心难点在于硬件的可靠识别上下位机之间稳定、高效的通信

2. 系统整体架构与通信协议设计

2.1 为什么选择STM32+QT的组合?

在做方案选型时,我主要考虑了以下几点:

  1. 下位机(STM32)的可靠性:无人超市的硬件终端需要7x24小时稳定运行,对异常情况(如传感器故障、通信中断)要有基本的处理能力。STM32系列MCU,特别是F1或F4系列,外设丰富、实时性强,且有成熟的HAL库和丰富的社区资源,开发效率和稳定性都有保障。它能够轻松驱动RFID读卡器、重量传感器、电磁锁、显示屏等模块。
  2. 上位机(QT)的跨平台与界面优势:作为管理终端,需要一个图形界面来展示商品信息、库存、交易流水,并可能需要进行简单的数据管理。QT的跨平台特性(Windows/Linux/macOS)为后期部署提供了灵活性,其强大的信号槽机制非常适合处理异步事件(如串口数据到达),并且界面设计工具(Qt Designer)能快速搭建出专业的UI。
  3. 通信方式的权衡:对于这种小范围、点对点的通信,串口(UART)是最简单、最可靠的选择。它不需要复杂的网络协议栈,接线简单,在STM32和PC(通过USB转串口模块)上都极易实现。虽然速度不是最快,但对于传输一些控制指令和传感器数据(几十个字节)完全够用。如果未来需要多个终端组网,可以在串口基础上改用RS485,或者升级为以太网、Wi-Fi(如STM32+ESP8266),但当前Demo阶段,串口足以验证核心逻辑。

注意:在实际商用环境中,通信安全(如数据加密)和通信冗余(如心跳包、重发机制)必须重点考虑。Demo中为了简化,可能没有加入,但这是产品化路上必须补上的一课。

2.2 系统工作流程与数据流

整个系统的运行遵循一个清晰的状态机流程:

  1. 待机状态:STM32控制货柜门锁关闭,显示屏显示欢迎语或二维码。QT上位机界面显示当前货柜状态为“空闲”,库存数据为最新。
  2. 扫码/开锁阶段:顾客使用手机APP扫描二维码(该二维码可包含一个临时的开锁指令编码)。STM32通过串口收到来自QT上位机转发或直接生成的“开锁指令”。验证通过后,STM32控制电磁锁打开,并开始监测门磁传感器和重量传感器。
  3. 购物识别阶段:顾客开门,取放商品。这里的关键是商品识别。Demo中常用两种方式模拟:
    • RFID识别:每个商品贴有RFID标签,取放时读写器识别。成本较高,但识别准确。
    • 重量变化识别(本项目采用):每个货格下方安装高精度称重传感器(如HX711模块)。STM32周期读取重量值。当系统检测到门被打开,并记录下此时的初始重量。关门后,再次读取重量,与初始值比较。通过预设的“商品重量-商品ID”映射表,来判断用户拿取或放回了哪个商品。这种方法成本较低,但需要处理商品摆放位置、轻微震动等带来的干扰。
  4. 结算与通信阶段:关门后,STM32计算出重量变化对应的商品ID和数量,生成一条包含“交易ID、商品ID、数量、时间戳”的消费数据帧。通过串口发送给QT上位机。
  5. 上位机处理阶段:QT程序收到数据帧后,进行解析:
    • 校验数据完整性(如CRC校验)。
    • 根据商品ID查询本地数据库(如SQLite)中的商品单价。
    • 计算消费金额,并从该顾客的账户余额(模拟)中扣款。
    • 更新数据库中的库存数量和交易记录表。
    • 在UI界面上实时刷新显示本次交易详情、最新库存和账户余额。
    • 可选的,通过TCP/IP将交易数据上传至云端服务器(本Demo可选功能)。
  6. 反馈与结束:QT上位机将结算结果(成功/失败)回传给STM32。STM32在显示屏上显示消费金额和“谢谢惠顾”等信息,然后回归待机状态。

3. STM32下位机核心功能实现详解

3.1 硬件选型与电路设计要点

  • 主控MCU:我选用的是STM32F103C8T6(蓝色药丸),性价比高,资源足够(64KB Flash, 20KB RAM,多个定时器、UART、ADC)。如果需要更复杂的UI(如彩屏TFT)或文件系统,建议升级到F4系列。
  • 称重模块HX711是24位高精度ADC专用于称重传感器,与STM32通过2个GPIO(SCK, DT)进行通信。关键在于传感器的标定。你需要用已知重量的砝码(如500g标准砝码)进行两点标定,获取斜率(scale)和偏移(offset),并将这些参数保存在STM32的Flash中,防止掉电丢失。
  • 门锁与门磁:电磁锁由一个大电流MOS管驱动,STM32的GPIO通过MOS管控制通断。门磁传感器(干簧管或霍尔传感器)连接到一个GPIO,用于检测门开合状态,记得配置上拉电阻。
  • 显示模块:为了显示简单信息(如金额、状态),使用0.96寸OLED(SSD1306驱动,I2C接口)就足够了。如果需要显示二维码,则需要选用SPI接口的TFT屏幕。
  • 通信接口CH340G USB转TTL串口模块是连接STM32与PC的桥梁。STM32的USART1的TX、RX引脚连接CH340的RX、TX。

实操心得:STM32的GPIO驱动能力有限,驱动电磁锁、继电器等感性负载时,务必使用三极管或MOS管进行隔离驱动,并在电磁锁线圈两端并联续流二极管,防止反向电动势击穿MCU。

3.2 软件设计:状态机与数据解析

STM32的程序核心是一个有限状态机(FSM),这能让程序逻辑清晰,易于维护和调试。

// 简化版状态机示例 typedef enum { STATE_IDLE, // 空闲待机 STATE_UNLOCKED, // 已解锁,门未开 STATE_SHOPPING, // 门已开,购物中 STATE_CALCULATING, // 门已关,计算中 STATE_REPORTING // 上报结算数据 } SystemState_t; SystemState_t gSystemState = STATE_IDLE;

main函数的超级循环中,根据当前状态执行相应的任务:

while (1) { switch (gSystemState) { case STATE_IDLE: displayWelcomeMsg(); if (receivedValidOpenCmd()) { // 收到开锁指令 unlockDoor(); gSystemState = STATE_UNLOCKED; recordTareWeight(); // 记录皮重(空载重量) } break; case STATE_UNLOCKED: if (isDoorOpened()) { gSystemState = STATE_SHOPPING; } break; case STATE_SHOPPING: // 持续监测门状态,此处可加入购物超时判断 if (isDoorClosed()) { gSystemState = STATE_CALCULATING; } break; case STATE_CALCULATING: currentWeight = getStableWeight(); // 获取稳定后的重量 weightChange = currentWeight - gTareWeight; identifyGoods(weightChange); // 根据重量变化识别商品 generateTransactionData(); gSystemState = STATE_REPORTING; break; case STATE_REPORTING: if (sendDataViaUART()) { gSystemState = STATE_IDLE; } else { // 发送失败,重试或进入错误处理 } break; } HAL_Delay(10); // 适当延时,避免CPU空转 }

串口通信协议设计: 为了确保数据可靠传输,需要自定义一个简单的应用层协议。一个经典的帧结构如下:

帧头(2字节)命令字(1字节)数据长度(1字节)数据(N字节)校验和(1字节)帧尾(2字节)
0xAA, 0x55CMDLENDATA[0~N-1]SUM0x0D, 0x0A
  • 帧头/帧尾:用于在数据流中定位一帧的开始和结束。
  • 命令字:区分不同类型的指令或数据,例如0x01代表开锁指令,0x02代表上报消费数据。
  • 校验和:通常是对命令字数据长度和所有数据字节进行累加和(或异或和),用于检测传输过程中是否发生错误。

在STM32端,使用串口空闲中断(IDLE)来接收不定长数据是高效的做法。HAL库提供了相应的回调函数HAL_UARTEx_RxEventCallback,当串口总线空闲一段时间后,会触发此中断,此时可以处理接收缓冲区中的数据,进行解帧。

4. QT上位机软件设计与实现

4.1 界面布局与数据库设计

使用Qt Designer进行界面拖拽布局非常高效。主界面可以包含以下几个区域:

  • 状态显示区:显示货柜连接状态(如“已连接COM3”)、当前交易状态。
  • 商品信息区:以表格(QTableWidget)形式展示商品ID、名称、单价、当前库存。
  • 交易流水区:以表格或列表形式实时滚动显示最近的交易记录。
  • 控制与日志区:包含手动开锁、清空记录等按钮,以及一个文本浏览器(QTextBrowser)显示系统运行日志。

数据库选用轻量级的SQLite,无需安装数据库服务器。使用QT自带的QSqlDatabaseQSqlQuery类进行操作。至少需要两张表:

-- 商品表 CREATE TABLE goods ( id INTEGER PRIMARY KEY, -- 商品ID name TEXT NOT NULL, -- 商品名称 price REAL NOT NULL, -- 单价 stock INTEGER NOT NULL, -- 库存 weight REAL NOT NULL -- 标准重量(用于重量识别匹配) ); -- 交易记录表 CREATE TABLE transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, trade_time DATETIME DEFAULT CURRENT_TIMESTAMP, goods_id INTEGER, quantity INTEGER, amount REAL, FOREIGN KEY (goods_id) REFERENCES goods (id) );

4.2 串口通信与数据解析

QT中通过QSerialPort类来实现串口通信。核心步骤:

  1. 发现和配置串口

    QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts(); foreach (const QSerialPortInfo &info, ports) { ui->comboBoxPort->addItem(info.portName()); } // 配置参数 serialPort->setPortName(portName); serialPort->setBaudRate(QSerialPort::Baud115200); serialPort->setDataBits(QSerialPort::Data8); serialPort->setParity(QSerialPort::NoParity); serialPort->setStopBits(QSerialPort::OneStop); serialPort->setFlowControl(QSerialPort::NoFlowControl);
  2. 连接信号与槽:这是QT的精华所在。

    connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::handleSerialData); connect(serialPort, &QSerialPort::errorOccurred, this, &MainWindow::handleSerialError);

    当串口有数据可读时,readyRead信号会触发handleSerialData槽函数。

  3. 数据接收与解帧:在handleSerialData中,需要实现一个状态机解析器来应对数据粘包、断包问题。逻辑与STM32端对应:

    void MainWindow::handleSerialData() { QByteArray data = serialPort->readAll(); m_buffer.append(data); // 追加到缓冲区 while (m_buffer.size() >= MIN_FRAME_LEN) { // 1. 寻找帧头 int headIndex = m_buffer.indexOf(FRAME_HEADER); if (headIndex == -1) { m_buffer.clear(); // 没有帧头,清空无效数据 break; } m_buffer = m_buffer.mid(headIndex); // 丢弃帧头前的数据 // 2. 检查长度是否足够 if (m_buffer.size() < sizeof(FrameHeader) + 1) break; // 假设帧头后第一个字节是长度 quint8 dataLen = m_buffer.at(sizeof(FrameHeader)); // 3. 检查一帧数据是否完整到达 int totalFrameLen = sizeof(FrameHeader) + 1 + dataLen + 1 + sizeof(FRAME_TAIL); // 帧头+长度+数据+校验+帧尾 if (m_buffer.size() < totalFrameLen) break; // 数据不完整,等待下次接收 // 4. 提取一帧数据 QByteArray oneFrame = m_buffer.left(totalFrameLen); m_buffer = m_buffer.mid(totalFrameLen); // 从缓冲区移除已处理帧 // 5. 校验帧尾和校验和 if (!checkFrameTailAndSum(oneFrame)) { qDebug() << "帧校验失败!"; continue; } // 6. 解析有效数据 processValidFrame(oneFrame); } }

4.3 多线程与界面刷新

一个常见的坑:在串口数据接收槽函数handleSerialData中直接进行复杂的数据库操作和界面更新。这会导致主界面在数据处理时卡顿甚至无响应。

正确做法:使用生产者-消费者模型和多线程。

  1. 工作线程(消费者):创建一个继承自QThread的类(如SerialWorker)。将串口对象moveToThread到这个工作线程中。所有耗时的操作(如数据解析、数据库查询与更新)都在这个线程中进行。
  2. 主线程(生产者/UI线程):只负责界面的显示和用户交互。当工作线程完成数据处理后,通过信号槽机制,将需要显示的结果(如交易信息)以信号的方式发送给主线程,主线程的槽函数接收到信号后,安全地更新UI。
// 在工作线程中 void SerialWorker::processValidFrame(const QByteArray &frame) { // ... 解析数据 ... TransactionInfo trans; trans.goodsId = goodsId; trans.quantity = qty; // ... 计算金额,更新数据库 ... // 发出信号,通知主线程更新UI emit transactionProcessed(trans); } // 在主窗口类中连接信号槽 connect(serialWorker, &SerialWorker::transactionProcessed, this, &MainWindow::onTransactionProcessed, Qt::QueuedConnection); // 使用队列连接,确保跨线程安全 void MainWindow::onTransactionProcessed(const TransactionInfo &trans) { // 这里可以安全地操作UI控件 ui->tableTransaction->insertRow(0); // ... 更新表格 ... }

5. 系统联调与核心问题排查实录

将STM32下位机和QT上位机连接起来调试,是问题集中爆发的阶段。下面记录几个典型问题及解决方法。

5.1 通信不稳定,数据错乱或丢失

  • 现象:QT端偶尔收到乱码,或者STM32发送的数据帧不完整。
  • 排查步骤
    1. 检查物理连接:确保USB转串口线连接牢固,TX/RX没有接反。可以尝试更换一个USB口或串口模块。
    2. 统一波特率等参数:这是最常犯的错误。务必确认STM32程序中的串口初始化(波特率、数据位、停止位、校验位)与QT程序中QSerialPort的设置完全一致。常用115200波特率,8N1(8数据位,无校验,1停止位)。
    3. 使用调试工具:在PC端使用串口调试助手(如XCOM、SSCOM)直接连接STM32,发送和接收数据,先排除STM32程序的问题。同样,用另一个串口助手模拟STM32向QT程序发送数据,排除QT解析逻辑的问题。
    4. 检查缓冲区与解析逻辑:确保QT端的缓冲区管理正确,能够处理粘包(多个帧连在一起)和断包(一帧数据分多次到达)的情况。上文提到的状态机解析法是必须的。
    5. 注意流控制:STM32和PC端都不要启用硬件流控制(RTS/CTS),除非你明确知道如何接线和配置。

5.2 重量传感器数据跳动大,识别不准

  • 现象:关门结算时,计算出的重量变化值波动大,无法稳定匹配到某个商品。
  • 解决方法
    1. 硬件滤波:确保HX711和称重传感器的连接线短而粗,并远离电源等干扰源。在传感器供电脚并联一个100uF的电解电容和一个0.1uF的瓷片电容进行滤波。
    2. 软件滤波:在STM32端对读取的原始AD值进行软件滤波。中位值平均滤波法非常有效:连续采样N次(如10次),去掉最大值和最小值,对剩下的值求平均。
      long HX711_Read_Average(uint8_t times) { long sum = 0; long values[times]; for (uint8_t i=0; i<times; i++) { values[i] = HX711_Read(); } // 这里可以加入排序,去掉头尾再求平均的代码 // 简化版:直接求和平均 for (uint8_t i=0; i<times; i++) { sum += values[i]; } return sum / times; }
    3. 稳定判断:在购物结束关门后,不要立即读取重量。可以延时1-2秒,然后连续读取多次重量,判断其波动是否在一个极小阈值内(如±5个AD值),认为稳定后再进行结算。
    4. 标定与容差:商品重量标定要准确。在匹配时,不要要求完全相等,而是设置一个合理的容差范围(比如±10g)。对于重量非常接近的不同商品,这种方案本身识别就有局限,需要考虑引入RFID或视觉辅助。

5.3 QT界面卡顿,特别是接收数据时

  • 现象:串口数据快速到来时,整个QT界面失去响应。
  • 根源:在readyRead的槽函数中执行了耗时操作(如复杂的字符串处理、数据库插入、UI大量更新),阻塞了QT的主事件循环。
  • 根治方案:如前文所述,必须使用多线程。将串口对象和数据处理逻辑移到一个单独的QThread中。这是QT开发中处理I/O密集型任务的黄金法则。

5.4 数据库操作失败或慢

  • 现象:插入交易记录时程序崩溃,或随着记录增多查询变慢。
  • 排查
    1. 数据库连接:确保数据库文件路径正确,且有读写权限。每次执行SQL前检查数据库连接是否打开。
    2. 事务(Transaction):当需要插入多条记录(如一次购物有多件商品)时,务必使用事务。这能大幅提升性能和数据一致性。
      QSqlDatabase::database().transaction(); // 开始事务 // 执行多条INSERT语句... if (/* 所有操作成功 */) { QSqlDatabase::database().commit(); // 提交 } else { QSqlDatabase::database().rollback(); // 回滚 }
    3. 索引优化:对于transactions表,如果经常按trade_time查询,可以为其创建索引以加速。
      CREATE INDEX idx_time ON transactions (trade_time);

6. 项目扩展与优化思考

这个Demo完成了从硬件感知到软件管理的闭环,但距离真正的商用还有距离。如果你在此基础上继续深入,可以考虑以下几个方向:

  1. 身份识别升级:将简单的“扫码开锁”升级为更安全的人脸识别刷掌识别。可以在STM32端集成一个摄像头模组(如OV7670),但计算能力有限;更合理的架构是,STM32将图像数据上传给QT/服务器,由算力更强的上位机或云端完成识别,再将结果下发给STM32。

  2. 通信协议强化

    • 加密:在自定义协议的数据段加入加密(如简单的AES加密),防止数据被篡改或伪造。
    • 应答与重传:设计“命令-应答”机制。上位机发送的每条指令,下位机都必须回复一个ACK(确认)或NACK(否认)。如果上位机超时未收到应答,则进行重传。同理,下位机上报数据也应有应答。
  3. 本地存储与断网续传:STM32端增加SPI Flash或SD卡,用于在通信中断时临时存储交易记录。待网络恢复后,再将缓存的数据补发给上位机。这保证了数据的完整性。

  4. QT上位机功能增强

    • 数据可视化:利用Qt Charts模块,绘制商品销售趋势图、库存预警图等。
    • 网络通信:增加TCP客户端模块,将本地的交易数据同步到远程云服务器,实现多网点集中管理。
    • 配置管理:提供一个友好的配置界面,允许管理员动态添加/删除商品、修改价格和库存阈值,而无需直接操作数据库文件。
  5. 低功耗设计:如果设备是电池供电,STM32需要在空闲时进入停止(Stop)模式待机(Standby)模式,通过门磁传感器等外部中断来唤醒,以极大延长待机时间。

这个项目麻雀虽小,五脏俱全。它串联了嵌入式开发、PC软件编程、硬件交互、通信协议、数据库等多个知识点。把其中每一个环节吃透、做稳,尤其是处理好那些“坑”,你的工程能力会得到实实在在的锻炼。最后,代码和文档的规范性至关重要,清晰的注释、模块化的设计,会让你在后期调试和扩展时事半功倍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询