STM32+QT构建无人超市系统:硬件交互与软件逻辑的工程实践
2026/9/14 11:09:12 网站建设 项目流程

简介:本资源是一套面向嵌入式开发与物联网应用学习者的完整无人超市消费系统实战项目,基于STM32与Qt跨平台协同设计,解决硬件控制与上位机交互融合的典型工程问题,适用于高校课程设计、电子竞赛备赛及嵌入式工程师能力进阶。压缩包共164个文件,含STM32底层驱动源码(C语言)、Qt上位机工程(含UI界面、多角色权限管理)、可执行程序(exe)、本地化翻译文件(qm)、编译中间文件(o/d)及关键设计文档(PDF/DOC),整体容量77.95MB,结构清晰,模块解耦明确。已有1087人学习下载,配套B站讲解视频与CSDN系列设计文档,覆盖RFID读写、步进电机闸机控制、会员全生命周期管理(注册/充值/消费/挂失)及商品电子标签扫码结算全流程。读者可直接部署运行,快速掌握STM32外设驱动开发、Qt信号槽机制、串口通信协议设计及软硬协同调试方法。

1. 项目缘起:为什么选择STM32+QT来构建无人超市消费系统?

最近几年,无人零售的概念从风口逐渐沉淀为一种可行的商业模式,尤其是在一些封闭或半封闭的社区、园区和办公场景里。我手头这个项目,就是为一个高校的创业孵化园设计的无人超市消费系统。甲方爸爸的核心诉求很明确:成本可控、运行稳定、交互直观,并且能快速部署和迭代。在评估了树莓派、安卓工控板、纯PC方案后,我们最终拍板了STM32 + QT这个组合。

很多人第一反应可能是:STM32做前端交互?这不是自找麻烦吗?确实,STM32通常给人的印象是跑在设备里默默控制电机、采集传感器的“幕后英雄”。但在这个项目里,它的角色很清晰:担任整个系统的“边缘智能终端”和“硬件交互枢纽”。具体来说,它负责几件关键事:通过串口或SPI连接RFID读卡器,识别用户身份卡;控制继电器阵列,管理电磁锁,实现货柜门的开关;采集重量传感器(我们用的是HX711模块)的数据,进行商品重量变化计算;通过串口屏或简单的TFT屏,提供基础的本地操作反馈(比如“请刷卡”)。

那么,为什么不用树莓派这类更强大的Linux板子一杆子捅到底呢?原因有三。第一是成本,对于需要部署几十上百个货柜节点的场景,STM32F103系列芯片加上必要的外设,BOM成本可以压得非常低。第二是实时性与稳定性,STM32裸机或RTOS的程序,对货柜门开关、重量采集这种需要毫秒级响应的任务,确定性远比运行着完整操作系统的Linux要高,也避免了系统死机、卡顿导致货柜门锁异常的风险。第三是功耗与维护,STM32系统功耗低,适合7x24小时运行,而且程序烧录进去基本就不用管了,没有系统升级、依赖库冲突这些烦心事。

QT,则扮演了“大脑”和“脸面”的角色。我们在一台部署在超市内的工控机(其实就是一台迷你PC)上运行QT开发的桌面应用程序。这个程序负责所有“高级”任务:通过TCP/IP网络与各个STM32终端通信,汇总状态;运行基于重量的商品识别算法(这是核心,后面细说);管理SQLite本地数据库,记录商品信息、库存和交易流水;最重要的是,提供一个非常友好、直观的触摸屏操作界面,供用户浏览商品、确认购物车和完成支付。

QT选型用的是QT5.15.2 LTS版本,搭配MSVC2017 64位编译器。选择这个版本是因为它在Windows平台下足够稳定,对触摸屏的支持成熟,而且其信号槽机制、多线程模型以及强大的UI控件库,能让我们快速构建出一个响应迅速、界面美观的桌面应用。整个架构可以概括为:STM32作为分布在各个货柜的“手脚”,负责精准执行和采集;QT应用作为中央“大脑”,负责复杂的逻辑判断、界面展示和数据管理。两者通过简单的自定义串口协议(后来升级为TCP Socket)进行通信,各司其职,扬长避短。

2. 系统核心架构与通信协议设计

确定了技术栈,接下来就是搭架子。整个系统的物理架构很简单:一台工控机作为服务器,通过一个交换机,连接多个货柜节点。每个货柜节点就是一块STM32核心板加上我们自制的扩展板。

2.1 硬件节点(STM32端)设计要点

STM32我们选用的是STM32F103ZET6,属于F1系列的增强型,有足够的GPIO、串口和SPI资源。外设连接如下:

  • RFID模块:用的是MFRC-522,通过SPI接口连接。这里有个坑,MFRC-522的库网上很多,但有些在连续读卡时不稳定。我们最终优化了读卡流程,加入了防冲突处理和读卡间隔延时,保证了在多人快速刷卡时也能准确识别UID。
  • 重量传感器:HX711,24位高精度ADC,通过两个GPIO模拟时序通信。这是实现“即拿即走”的关键。每个货柜有四个称重传感器(货柜四角),STM32需要实时读取四路HX711的数据,计算总重量。这里的关键是滤波和去皮。我们采用了滑动平均滤波,并且设计了一个“空载标定”流程:每次货柜门关闭后,自动记录当前重量作为皮重基准值。
  • 门锁控制:通过ULN2003驱动板控制12V电磁锁。逻辑很简单,收到开锁指令后,给一个500ms的高电平脉冲,然后释放。但安全考虑,我们加入了门磁开关检测,STM32会持续监测门状态,如果门被异常打开或超过设定时间未关闭,会立即上报异常事件给QT服务器。
  • 通信模块:早期我们用串口转以太网模块(如W5500),后来为了简化,直接用了ESP8266(AT指令模式)作为Wi-Fi透传模块,让STM32通过串口就能接入网络。STM32端实现了一个简单的状态机,负责打包数据帧、发送心跳包、解析服务器指令。

2.2 软件中枢(QT端)模块划分

QT端的应用程序,我们采用经典的分层设计:

  1. 通信管理层:继承自QObject,使用QTcpSocketQTcpServer。为每个连接的STM32终端维护一个ClientHandler对象,负责数据的接收、解析和指令下发。协议我们设计得很简单:帧头(0xAA 0x55)+ 数据长度 + 命令字 + 数据区 + CRC16校验。这个模块也负责心跳检测,超时未响应的终端会被标记为离线。
  2. 业务逻辑层:这是最核心的部分。它接收来自通信层的“重量变化”事件。当用户打开柜门,拿走商品再关门后,STM32会上报关门后的新重量。业务层需要计算重量差值(新皮重 - 旧皮重,注意是负值),然后根据预设的商品重量数据库进行匹配。

    注意:这里不能简单做等值匹配。我们为每个商品预设了一个重量范围(比如一罐可乐是355±5克)。匹配算法会遍历数据库,找到差值落在哪个商品的重量范围内。如果匹配成功,则将该商品加入用户的虚拟购物车;如果匹配失败(比如拿了两件不同商品),则会标记为“待确认”,并在界面上提示用户手动选择。

  3. 数据访问层:使用QT自带的QSqlDatabase操作SQLite数据库。建了三张核心表:商品表(ID,名称,单价,标准重量,重量容差,图片路径)、用户表(卡号,姓名,余额)、交易流水表(时间,卡号,商品ID,数量,金额,货柜编号)。
  4. UI展示层:使用QML结合传统Widget。主界面是一个商品浏览网格,点击商品可以看详情。侧边栏是实时更新的购物车。支付界面支持余额支付(从数据库扣款)和扫码支付(我们集成了支付宝的当面付API)。所有界面都为触摸操作做了优化,按钮够大,反馈明确。

2.3 关键通信协议帧示例

为了让STM32和QT端能对话,我们定义了几种关键的命令帧:

1. 终端上报状态(心跳 & 重量)STM32 -> QTAA 55 0C 01 00 01 00 00 12 34 00 00 13 88 XX XX

  • 0x01:命令字(状态上报)
  • 00 01:终端编号(1号柜)
  • 00 00 12 34:当前重量值(十六进制,单位0.1克,这里是466.0克)
  • 00 00 13 88:皮重值(500.0克)
  • XX XX:CRC16校验

2. 服务器下发开锁指令QT -> STM32AA 55 05 02 00 01 00 00 XX XX

  • 0x02:命令字(开锁)
  • 00 01:终端编号(1号柜)
  • 00 00:开锁时长(0表示使用默认值500ms)

3. 终端上报异常事件STM32 -> QTAA 55 06 03 00 01 01 00 XX XX

  • 0x03:命令字(事件上报)
  • 00 01:终端编号
  • 0x01:事件类型(0x01表示门超时未关)
  • 0x00:事件参数

这套协议虽然简单,但足够可靠。我们在QT端为每种命令字都建立了对应的解析函数,并在ClientHandlerreadyRead信号槽里进行数据帧的拼接和完整性判断,有效处理了TCP的粘包问题。

3. 核心难点:商品识别算法与重量数据处理

“无人超市”听起来很酷,但技术落地的核心就卡在“如何知道用户拿走了什么”这个问题上。纯视觉方案成本高、受光照影响大。我们选择的“重力感应”方案,成本低、可靠性高,但挑战巨大。

3.1 重量传感器的噪声与漂移

HX711虽然精度高,但读数是“活”的。环境温度变化、货柜结构应力释放、甚至风扇的震动都会导致读数缓慢漂移。直接拿一次读数做判断,绝对翻车。

我们的处理流程如下:

  1. 硬件滤波:在HX711的模拟电源输入端加上了LC滤波电路,并在数据线(DT)和时钟线(SCK)上加了上拉电阻,这是稳定读数的基础。
  2. 软件滤波:STM32端,我们连续读取20次HX711,去掉最大最小值,然后取平均,得到一个“瞬时值”。这个瞬时值每100ms更新一次。
  3. 动态皮重跟踪:这是最关键的一步。我们不是只在初始化时记录皮重。我们定义了一个“稳定状态”:货柜门关闭超过10秒,且连续30个“瞬时值”的标准差小于一个阈值(比如2克)。当系统进入“稳定状态”,我们就用当前的重量平均值,去平滑更新皮重值。公式是:新皮重 = 旧皮重 * 0.9 + 当前平均重量 * 0.1。这个一阶低通滤波算法,让皮重能够缓慢地跟随环境漂移,避免了长期运行后累积误差过大。
  4. 变化量检测:当货柜门关闭时,STM32会记录关门瞬间的重量作为关门重量。等待3秒(让货品放稳)后,进入“稳定状态”计算出的新皮重,与关门重量进行比较。如果差值超过一个“最小可识别重量”(我们设为5克),则认为发生了商品变动,将差值(通常是负值,表示重量减少)上报给服务器。

3.2 QT端的商品匹配策略

QT服务器收到一个负的重量差值,比如-355.3克。接下来就是“猜”用户拿走了什么。

  1. 精确匹配:首先在商品数据库中查找标准重量为355克,且容差为±5克的商品。如果找到唯一商品(如“可口可乐330ml”),则直接匹配成功。
  2. 容差匹配:如果精确匹配失败,则查找所有商品,看差值是否落在[标准重量-容差, 标准重量+容差]区间内。如果仍只有一个商品命中,也视为成功。
  3. 多商品匹配(最复杂的情况):如果差值同时匹配了多个商品(比如一瓶水和一包饼干的总重,恰好和一瓶饮料的重量接近),或者差值不等于任何单件商品重量(用户可能一次拿了多件),系统就会进入“待确认”状态。
    • 在UI上,会弹出一个列表,显示所有可能的商品组合(通过组合算法计算,限制在2件商品以内)。
    • 同时,我们会调取该货柜内的商品陈列图(我们在数据库里为每个货位存了一张参考图片),在界面侧边栏显示,供用户对照确认。
    • 用户可以通过触摸屏,从列表中选择自己实际拿取的商品。这个选择结果会被记录,并用于优化后续的匹配算法(例如,如果某个商品组合被频繁手动确认,可以将其作为一个“虚拟组合商品”加入数据库,并赋予一个组合重量和容差)。

3.3 应对“恶意”操作的策略

无人值守,必须考虑防损。

  • 重量增加处理:如果检测到重量增加(差值>0),系统会认为用户放回了商品。这会触发一个“取消上次购买”的逻辑,从购物车中移除最近一次匹配的商品。同时,系统会记录该异常操作。
  • 开门超时:STM32检测到门打开超过30秒(可配置),会立即上报事件。QT端会在界面显示警告,并记录该次开门事件。连续超时,该货柜可能被临时锁定。
  • 网络中断处理:如果STM32与服务器断连,STM32会进入“离线模式”。在此模式下,它仍然可以读卡开门,并在本地Flash中记录交易事件(卡号、时间、重量变化)。等网络恢复后,再将缓存的事件批量上传给服务器进行结算。这保证了即使网络波动,交易数据也不会丢失。

4. QT客户端开发中的实用技巧与避坑指南

QT端的开发是整个项目的门面,也是用户体验的关键。这里分享几个我们趟过坑才得到的经验。

4.1 多线程与界面更新的正确姿势

QT的GUI操作必须在主线程。但我们的通信模块QTcpServer在接收到数据后,需要立刻解析并更新业务数据,这可能会阻塞主线程。同时,像“匹配商品”这种计算,也可能耗时。

我们的做法是:

  • 通信线程:我们并没有为每个Socket单独开线程,而是利用了QT的事件循环。QTcpServer在主线程监听,当有新连接时,创建的QTcpSocket对象也属于主线程。但是,我们在一个单独的QThread里运行了一个DataProcessor对象。ClientHandler收到完整数据帧后,并不处理业务,而是通过信号槽(记得设置连接类型为Qt::QueuedConnection)将原始数据发送给DataProcessor线程。
  • 业务计算DataProcessor线程负责重量差值计算、商品匹配等耗时逻辑。计算完成后,它再通过信号槽,将结果(如“用户A从1号柜拿走了可乐”)发送回主线程。
  • 界面更新:主线程接收到结果信号后,去更新购物车列表、用户余额等UI元素。这样,界面始终保持流畅响应。
// 示例:跨线程传递数据 // 在DataProcessor类(工作在子线程)中定义信号 signals: void productMatched(const QString &cardId, int cabinetId, const QString &productName); // 在主窗口类中连接信号槽 connect(dataProcessorThread, &DataProcessor::productMatched, this, &MainWindow::onProductMatched, Qt::QueuedConnection);

提示:Qt::QueuedConnection确保了槽函数会在接收者对象所属的线程(这里是主线程)的事件循环中被调用,这是跨线程更新UI的安全方式。

4.2 数据库操作与性能优化

一开始我们每次更新购物车都直接操作数据库,在压力测试时界面明显卡顿。

优化方案:

  1. 连接池:虽然SQLite是文件数据库,但多线程同时读写也需要管理。我们使用了QSqlDatabase::cloneDatabase来为每个需要长时间访问数据库的模块创建独立的连接,避免连接冲突。
  2. 批量写与事务:用户的购物车操作,我们先更新在内存中的一个QMap结构里。只有当用户点击“结算”时,才开启一个数据库事务,将购物车QMap里的所有商品一次性插入交易流水表,并更新用户余额和库存。这大大减少了数据库写操作的次数。
  3. 异步查询:对于商品图片加载这种操作,我们使用QtConcurrent::run在后台线程中读取图片文件路径,加载到QPixmap,再通知主线程更新QLabel

4.3 界面美化与触摸适配

QT Widgets默认风格比较老旧。我们使用了QSS(QT样式表)来美化界面,效果立竿见影。

/* 示例:美化按钮 */ QPushButton { background-color: #4CAF50; /* 绿色 */ border: none; color: white; padding: 15px 32px; text-align: center; font-size: 18px; border-radius: 8px; } QPushButton:pressed { background-color: #45a049; /* 按下时更深的绿色 */ }

对于触摸屏,关键点是:

  • 控件尺寸:所有按钮的最小尺寸设置为60x60像素,确保手指容易点中。
  • 反馈延迟:为按钮的pressedreleased状态设置明显的颜色变化,并考虑添加轻微的触觉反馈(如果硬件支持)。
  • 禁用长按:在可能的地方,避免使用需要长按才能触发的操作,或者提供明显的替代方式。

4.4 打包与部署

开发是在Windows上用MSVC完成的,但最终部署的工控机环境可能不同。我们使用windeployqt工具来收集所有依赖的DLL。

windeployqt --release --no-compiler-runtime --no-angle --no-opengl-sw MyApp.exe

然后,将可执行文件、依赖库、数据库文件、图片资源等一起拷贝到一个目录下。我们还写了一个简单的批处理脚本,用于设置运行路径和启动程序。

数据库的初始化脚本也集成在程序里。首次运行时,如果检测到数据库文件不存在,会自动执行建表SQL语句并插入初始商品数据。

5. STM32固件开发的调试心得

STM32端的代码逻辑相对单纯,但硬件调试的坑一点不少。

5.1 传感器数据读取的稳定性

HX711的读数偶尔会跳变。除了前面说的硬件滤波,在代码里我们加了“野值剔除”逻辑。连续读5次,如果某次读数与前一次的平均值偏差超过3倍标准差,则丢弃该次读数。此外,给HX711的电源一定要稳定,最好用LDO单独供电,数字地和模拟地之间用0欧电阻或磁珠连接。

RFID读卡,在RC522_RequestRC522_Anticoll函数之间,增加了HAL_Delay(50),给卡片足够的响应时间,大大降低了漏读率。

5.2 使用FreeRTOS还是裸机?

我们一开始用了FreeRTOS,创建了三个任务:读卡任务、称重任务、通信任务。好处是逻辑清晰,但后来发现,对于F103来说,任务切换的开销和栈空间的占用,在资源上有点紧张。特别是称重任务需要高优先级和精确的周期,用RTOS的vTaskDelayUntil不如裸机的定时器中断来得直接。

最终我们退回到了裸机+状态机的模式。主循环是一个大状态机,定时器中断负责以100ms为周期触发重量采集和滤波计算,串口中断处理通信。RFID读卡放在主循环中轮询。代码体积小了,响应也更确定。对于这种逻辑不极端复杂的嵌入式前端,裸机状态机往往是更简单可靠的选择。

5.3 高效的日志调试法

STM32调试,光靠LED闪烁是不够的。我们利用一个空闲的串口(UART3)连接了一个USB转TTL模块,在代码里大量使用printf重定向到串口。

// 重定向printf到串口 int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart3, (uint8_t*)ptr, len, 1000); return len; } // 在代码中打印关键变量 printf("[Weight] Raw: %ld, Filtered: %ld, Tare: %ld\r\n", raw_val, filtered_val, tare_val);

这样,通过串口助手就能看到实时的重量数据、状态切换和事件报告,排查问题效率倍增。产品发布时,只需要将日志输出宏定义为空即可。

5.4 固件升级(OTA)的预留

虽然当前项目没有要求,但我们为未来留了后路。我们修改了链接脚本,将Flash分为Bootloader区、App1区、App2区(备份)和参数区。Bootloader通过串口接收新的固件包(bin文件),校验后写入App2区,再跳转执行。在QT管理端,我们也做了一个简单的固件上传工具。这样,未来如果发现某个货柜的逻辑有问题,可以远程批量升级,无需人工一个个去烧录,运维成本会大大降低。

从零开始构建这套系统,最大的体会就是:无人零售的技术核心不在于“无人”,而在于“可信”。用户要相信系统能准确识别商品,运营者要相信系统能稳定运行、准确结算。STM32+QT的组合,通过清晰的职责划分,一个确保底层交互的可靠,一个确保上层逻辑和体验的友好,共同搭建起了这份“可信”的基石。每一个传感器数据的滤波算法,每一次网络通信的异常处理,每一行UI代码的触摸优化,都是在为这个目标添砖加瓦。

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

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

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

立即咨询