
简介本资源是一套完整的红外人脸测温系统开发方案面向嵌入式初学者与智能硬件开发者解决非接触式体温筛查场景下的软硬协同设计问题。系统以STM32为下位机核心配合红外测温传感器与QT框架开发的上位机实现人脸检测、实时测温、异常告警、语音播报及人脸图像自动存档等功能适用于校园、社区、办公场所等轻量级防疫部署场景。压缩包共687个文件含191个hpp/h头文件OpenCV与QT接口定义、70个C源码STM32底层驱动与协议解析、68个DLL动态库OpenCV 3.4.7视觉处理支持、16个可执行文件含上位机主程序与调试工具及8份PDF使用文档整体大小76.66MB。已有1016人学习下载提供从串口通信协议定义、Haar级联人脸检测集成、温度阈值联动逻辑到异常拍照命名规则等完整工程实践细节代码结构清晰、模块划分明确具备直接编译运行与二次开发基础。 红外人脸测温仪这类项目我在好几个技术群里见过不下十次从大创结题到毕设答辩再到工厂考勤机的原型验证都有它的身影。说它是“单片机综合应用”的经典题目一点都不夸张——STM32负责采集和逻辑控制红外传感器负责非接触测温摄像头配合OpenMV或者DSP做简单人脸定位再加一个QT写的上位机完成可视化和数据存储。就这么一套组合把嵌入式开发里最常用的几块知识点全串起来了串口通信协议、ADC/IIC外设驱动、传感器标定、GUI事件循环、数据可视化。网上流传的版本很多有的功能全一些有的阉割得只剩个温度显示但整体的骨架都差不多。这篇就按我自己折腾过的方案把从硬件选型到上位机联调的全过程拆开揉碎讲一遍。1. 红外人脸测温仪到底“测”的是什么先想清楚技术路线做这个项目之前最容易犯的错是把它当“人脸识别项目”来做。一上来就想搞深度学习、跑卷积神经网络还有人问我要不要上OpenCV DNN模型。实际上工程需求远没有这么复杂——它要做的是“检测到人脸然后对人脸区域做非接触式测温”重点在于测温方案的稳定性和数据链路的通畅性而不是算法的先进性。所以第一步得先把技术路线定下来。1.1 核心需求拆解测温准、响应快、显示直观任何一个测温仪最终要回答三个问题温度测出来是多少测的是不是人脸区域显示给用户看不看得懂反过来对应到硬件和软件层面至少需要下面这几样东西红外测温传感器现在用得最多的是Melexis的MLX90614IIC接口出厂校准精度在±0.5℃左右测人体温度完全够用。进阶一点可以用MLX90640那种32x24的热成像阵列能出热力图但价格翻了不止十倍而且数据处理复杂度也上了一个台阶初期不建议碰。人脸检测模块STM32F103这种级别的单片机跑传统人脸检测算法很吃力所以常规做法是挂一个OpenMV摄像头模块IDE里内置了Haar Cascade人脸检测代码直接把检测结果通过串口发给STM32。不想用OpenMV的话用独立的USB摄像头配合主机上位机做人脸检测也可以但那样“单片机参与感”就弱了很多毕设答辩容易被问住。主控MCUSTM32F103C8T6是绝对的主力性价比高资料多IIC和串口资源都够用。如果项目要求同时驱动LED屏、语音播报、多个传感器建议换STM32F407主频高外设更丰富。这块最容易踩的坑是把“测环境温度”和“测人体温度”混为一谈。MLX90614默认的测温模式有两类——SMBus模式下的To和Ta两个寄存器分别代表“目标物体温度”和“环境温度”而人体测温场景真正关心的是To。如果上位机拿错寄存器显示出来的温度会带明显偏差冬季大概会低个五六度夏天又会偏高。调试的时候先用万用表或者另一只测温枪去校准别上来就怀疑算法。1.2 为什么选STM32QT组合聊聊方案选型的底层逻辑这个项目用STM32QT本质上是一种“嵌入式采集 桌面端应用解耦”的思路。STM32要干的是实时采集和前端交互比如按键切换、蜂鸣器报警、OLED屏显示这些活儿对实时性要求高而且要在没有操作系统的裸机环境下稳定跑起来。QT上位机则负责大数据量的展示和存储比如实时温度曲线、历史查询、报表导出这些要是全塞给单片机做会非常痛苦。有人问为啥不用Python写上位机开发速度多快啊。关键点是很多高校的课程体系里QT依然是主流GUI框架而且QT的信号槽机制特别适合串口这种异步接收数据的场景。串口一发进来一个完整帧触发一个readyRead信号槽函数里解析、更新曲线、刷新界面逻辑非常清晰。配合QCustomPlot这类第三方图表库画温度趋势图也就是几行代码的事。相比之下Python用pyqt或pyside写起来虽然也快但打包分发依赖太大在Windows老机器上跑还偶尔会出兼容性问题对于工程类项目反而不够省心。从调试和验收的角度看STM32固件和QT上位机分开开发、分开测试也是效率最高的方式。先不要给上位机发真实数据直接用串口助手模拟帧数据灌给QT验证解析逻辑等上位机稳定了再用单片机发真实温度这时候定位问题会非常明确——要么是数据根本没发出来要么是协议匹配不上不会有那种“界面和硬件一起瞎跑”的绝望状况。2. 硬件搭建与传感器接线把所有外设先转起来项目能不能跑通硬件的稳定程度往往占了六成。很多同学烧录完固件发现温度读不出来第一反应是怀疑代码逻辑实际上用示波器或者万用表量一下IIC引脚的电平和时序五分钟就能找到问题。硬件这部分我按“主控、传感器、显示交互”三个部分来拆解。2.1 核心器件清单与关键引脚分配先列一份我实际调试通过的物料清单大家照着买基本不会踩坑器件型号/规格数量说明主控板STM32F103C8T6最小系统板1蓝板或者红板都行注意区分3.3V供电红外测温模块MLX90614GY-9061注意买3.3V版本5V版本也能用但要注意电平匹配人脸识别模块OpenMV4 CamH71也可以买OpenMV3 M7性能足够显示设备0.96寸OLEDIIC1用于本地显示实时温度可选交互按键轻触按键 x22一键测温、温度单位切换声光提示蜂鸣器 指示灯各1提示体温异常通信接口CH340 USB转串口模块1连接PC和STM32调试上位机平台PC QT 5.151Windows/Linux均可引脚分配建议这样规划保证IIC总线上不同设备地址不冲突MLX90614接STM32的PB6I2C1_SCL、PB7I2C1_SDA供电接3.3VOLED也挂在I2C1上通过地址区分MLX90614地址默认0x5AOLED常见地址0x3C或0x3DIIC协议天然支持一总线多设备只要地址不撞就行OpenMV通过串口2PA2-TX、PA3-RX与STM32通信波特率建议设115200因为图像检测后的坐标数据量不算大但这个波特率能保证帧率稳定蜂鸣器接PA4按键接PA5/PA6OLED用软件IIC或者硬件IIC都行但STM32的硬件I2C在F103上口碑不稳定我后来直接改用模拟IIC了省得被咬线卡住耽误时间。2.2 MLX90614不是插上就能读数必须搞懂SMBus协议MLX90614底层走的是SMBus协议它在IIC协议的基础上做了些限制。网上很多代码直接把IIC读取函数拿过来用结果读到FF或者永远都是0多半是没有处理好PEC校验和寄存器地址的问题。要注意MLX90614的读取流程是主机发送START信号发送器件地址0x5A写方向紧接着发送需要读取的寄存器地址比如0x07是To物体温度重启START信号发送器件地址0x5B读方向读取两个字节的16位温度数据注意温度是8位小数精度所以完整温度值 16位整数值 * 0.02 - 273.15最后读取PEC校验字节如果不做PEC校验至少也要把地址和数据位都检查一遍避免在长线传输或者电源波动时读到乱码。实操经验直接用STM32的硬件I2C外设容易在连续读多个寄存器的时序上被硬件状态机卡住所以我后来在工程里用的是模拟IIC实现反正速度也不要求特别快测温频率5Hz就足够。关键代码段大致是这样float MLX90614_GetTemp(uint8_t reg) { uint8_t buf[3] {0}; uint16_t data_raw 0; float temp 0; IIC_Start(); IIC_SendByte(0x5A); // 写地址 IIC_WaitAck(); IIC_SendByte(reg); // 寄存器地址 0x07 IIC_WaitAck(); IIC_Start(); // 重复起始 IIC_SendByte(0x5B); // 读地址 IIC_WaitAck(); buf[0] IIC_ReadByte(); // 数据低字节 IIC_SendAck(0); buf[1] IIC_ReadByte(); // 数据高字节 IIC_SendAck(0); buf[2] IIC_ReadByte(); // PEC IIC_SendNak(); IIC_Stop(); data_raw (uint16_t)((buf[1] 8) | buf[0]); temp data_raw * 0.02 - 273.15; return temp; }很多人问为什么算出来温度显示成45℃大概率是读取到了非目标寄存器或者环境干扰导致IIC数据错位。建议先读一遍RAM里的全部数据打印出来和手册对照确认注释说的寄存器地址在代码里没有偏移。2.3 OpenMV人脸检测只往外发坐标不做复杂运算OpenMV端的人脸检测代码几乎是固定的它的价值在于把“人脸存在性”和“人脸的像素坐标”通过串口传出来剩下的判断逻辑全丢给STM32。既然主控跑的是裸机逻辑就不能让它去处理图像必须彻底分工。我实际用的OpenMV代码很简单import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(30) sensor.set_auto_whitebal(False) uart UART(3, 115200) uart.init(115200, bits8, parityNone, stop1) face_cascade image.HaarCascade(frontalface, stages25) clock time.clock() while(True): img sensor.snapshot() faces img.find_features(face_cascade, threshold0.75, scale_factor1.5) if faces: x, y, w, h faces[0] img.draw_rectangle((x, y, w, h)) frame F:%d,%d,%d,%d\n % (x, y, w, h) uart.write(frame.encode()) else: uart.write(N:0,0,0,0\n.encode()) clock.tick()帧格式设计得越简单越好。我用的这个带字母前缀的结构F表示检测到人脸N表示没检测到。STM32串口中断里收字符缓存在数组里检测到换行符就解析。之所以不用二进制帧是因为调试的时候直接拿串口助手看字符流就能判断问题省去一层转换。3. 固件层设计温度采集、串口协议和逻辑控制硬件转起来之后就到了这个项目的灵魂环节——STM32固件怎么把多个外设的数据源有序地汇集成一帧完整的数据再通过串口送给上位机。这里有一个很重要的编程思路不要把所有逻辑都堆在主循环里而是要分模块、分状态机来处理。3.1 主循环的多种任务调度怎么避免互相卡顿如果主循环里又跑IIC读温度又跑串口解析又去扫描按键一个不小心就出现“OLED显示卡住半秒测温频率忽高忽低”的鬼现象。我最后采用的方式是“前后台结构”系统节拍用SysTick定时器每1ms进一次中断置标志位然后主循环里按不同的时间片去处理不同任务。一个典型的调度片段如下while (1) { if (flag_10ms) { flag_10ms 0; Key_Scan(); // 按键扫描 } if (flag_100ms) { flag_100ms 0; mlx90614_val MLX90614_GetTemp(0x07); // 读温度 if (current_mode MODE_AUTO) { // 自动模式下才做人脸检测数据的联动判断 } } if (flag_500ms) { flag_500ms 0; OLED_Refresh(mlx90614_val); // 刷新本地显示 if (temp_alarm) { Buzzer_On(50); } } UART_Process(); // 串口数据解析不用等靠中断接收 }这个结构看着平平无奇但实践里非常顶用。每个任务都有自己独立的节拍优先级高的事情比如接收串口靠中断抢断优先级低的事情比如刷新OLED就慢慢排不会出现IIC读个寄存器的几十微秒把整个系统堵死的情况。温度采集的上限其实没必要设很高人体测温5Hz到10Hz已经完全够用了再快反而是浪费CPU。3.2 串口通信协议从裸发字符串到带校验的二进制帧很多初版作品喜欢直接拿printf打印“Temperature:36.5\r\n”上位机再用字符串切割解析。这在功能演示里能跑但扛不住真实使用环境——串口任意一个字节出错、或者上位机在中间时刻打开串口导致半包数据都会解析出乱码。所以我还是建议用结构化帧格式。我自己用的帧格式设计如下帧头命令字数据长度数据区校验和1字节 (0xAA)1字节1字节N字节1字节累加和取反具体举个例子一条实时温度数据帧大概长这样AA 01 05 01 DC 01 C8 00 8B数据区里我放了4个字段人脸检测状态1字节目标温度整数部分1字节目标温度小数部分1字节乘以100环境温度2字节。十六进制和十进制混合用不直观但对于解析代码来说是最高效的。校验和计算方式很简单从“命令字”开始把每一字节累加然后取反加1当作第八字节发出去。上位机收到后做一次同样的累加比对不相等就丢掉整帧等下一帧。为什么加校验因为实际使用中USB转串口、长线传输、蓝牙透传都可能产生干扰不加校验会出现一个极诡异的问题某一次显示的温度是18℃但过了两秒又恢复36℃。就是偶发的一个字节被干扰成0x00了。加上校验和之后这类异常数据会被直接过滤。3.3 温度超限报警和人脸联动逻辑怎么落自动测温模式下逻辑是“只有检测到人脸才认为当前温度有效”。这一点非常关键——不然有人从旁边经过传感器指向墙壁温度读出来30℃就会产生误报。OpenMV传回来的数据帧里包含一个人脸状态位STM32收到F状态时才把当前MLX90614读到的温度作为一次有效测温结果缓存下来并更新上位机的显示。没有检测到人脸时上位机的温度显示置成“--”或者保留上次值但绝对不能强制突变。报警逻辑用迟滞比较器思路#体温超过37.3℃进入报警状态蜂鸣器响报警之后必须低于37.0℃才能解除报警。这个0.3℃的迟滞区间是为了防止临界温度附近频繁触发/释放实际体验好很多。有个细节因为红外测温受环境温度影响明显可以在代码里加一个“温度补偿参数”比如MLX90614装在设备里内部发热会被自身电路影响实测在开机半小时后读数会比刚开机高0.2℃上下这个补偿可以在标定阶期测出来然后写死进固件里。4. QT上位机的设计与实现曲线、状态、数据一个都不能少QT上位机这块我见的最多的翻车现场是串口能打开就是收不到数据然后一查串口接收区的字符编码不对或者协议解析那里边界条件没处理好。上位机本身就是个“数据加工厂”底层逻辑不复杂但工程化的习惯得从一开始就养成。4.1 用串口类和信号槽搭出数据接收通道QT5自带的QSerialPort类已经封装得非常好了不必自己再去搞第三方库。项目里添加串口功能需要在.pro文件里加上QT serialport charts如果要用QCustomPlot绘制曲线还需要下载qcustomplot.h和qcustomplot.cpp放入项目目录然后加一句include(./qcustomplot/qcustomplot.pri)。这里多提一句QCustomPlot比QT Charts更灵活画实时曲线时双缓冲区翻滚更方便性能也更好。我后来一直用的是QCustomPlot原因是它的绘制逻辑是直接基于QPainter的二次开发上限高很多。串口接收最核心的代码其实就是用readyRead信号触发读取然后自行根据帧协议拆包。我习惯把解析逻辑独立成一个函数避免在槽函数里堆太多业务代码// 串口接收槽函数 void MainWindow::onSerialReadyRead() { QByteArray data serial-readAll(); rxBuffer.append(data); // 查找帧头提取一条完整帧 while (rxBuffer.size() 8) { int headIdx rxBuffer.indexOf(char(0xAA)); if (headIdx 0) { rxBuffer.clear(); break; } if (headIdx 0) { rxBuffer rxBuffer.mid(headIdx); } if (rxBuffer.size() 8) break; int dataLen static_castuchar(rxBuffer.at(2)); if (rxBuffer.size() 3 dataLen 1) { QByteArray frame rxBuffer.mid(0, 3 dataLen 1); rxBuffer rxBuffer.mid(3 dataLen 1); parseFrame(frame); } else { break; } } }核心思路就一句话永远不要假设串口一次把所有字节都发进来数据是“流式”到达的。所以设计成“找帧头 → 判断长度 → 截取完整帧 → 继续找下一帧”的循环解析方式才能应对分包、粘包的各种真实情况。4.2 实时温度曲线与数据刷新的几个关键参数画实时曲线时我见过不少人直接把所有点全画出来CPU占用直接飙到20%以上界面还卡顿。正确做法是用“滚动窗口”思路只保留最近300~500个数据点每次新点到达时移除最早的点再调用replot刷新。时间轴上是5分钟或10分钟之内的数据这样一条清晰美观的曲线就出来了。曲线绘制有一点值得注意QCustomPlot里可以把温度轴的范围设为“自动适配”和“固定范围”两种模式。固定范围比如32℃~40℃能更直观地看到细小波动自动范围则适合环境温度跨度大的场景。我倾向于先固定因为测温仪的量化指标就是“分辨出0.1℃的变化”固定范围粗网格线方便观察。再来说说刷新频率。上位机串口通信的波特率一般115200数据帧长度8个字节如果每100ms发一帧实际占用带宽不到1%非常充裕。建议在上位机上做一个“接收计数”的显示标签每收到一帧加1用户可以直观看到数据链路是否通畅。如果计数卡住不动了不必怀疑上位机逻辑优先检查串口号和接线。4.3 数据记录与导出从历史查询到CSV报表测温仪如果没有数据存储功能在项目验收时是会被扣分的。但QT里做数据库无非是引入SQLite或者直接写CSV文件。对于这种轻量级的项目我建议直接写CSV简单、透明、能用Excel打开。每当解析到一帧有效的人脸测温数据时就把时间戳精确到分钟或秒、目标温度、环境温度、是否报警追加到CSV行。CSV的写入不要每帧都fopen/fclose否则文件IO会把界面卡顿正确的做法是维护一个QFile对象保持打开状态每次写入后flush一次。为了稳定可以设置每天或每小时切换一次文件避免文件无限增大。这种方案写起来也就二十来行代码但演示效果非常加分——评测老师问你“有没有历史记录功能”你直接打开CSV文件给他展示数据趋势比单纯说“我能画曲线”要硬气得多。另外可以在QT界面上做一个“导出报表”按钮点击后把当前内存中缓冲的温度数据再生成一份完整的CSV文件。这算是个很讨巧的功能因为不管你之前实时记录了多少数据导出的都是完整可靠的数据文件不容易被挑毛病。5. 联调路上的那些坑以及排查思路项目做到这一步开始进入最磨人心的联调阶段。串口、数据、逻辑、界面任何一个环节出问题都需要一套稳定的排查方法而不是瞎猜乱试。5.1 温度读数乱跳/明显偏低的常见原因读到的温度在25℃和36℃之间反复横跳大概率不是MLX90614坏了而是IIC读时序不稳。先打开调试串口把这几次读回来的原始12位温度值打印出来。如果能看到明显的0或65535这类极值说明读取过程中数据线受干扰了。解决办法是在IIC线路上加上拉电阻一般4.7kΩ缩短连接线长度并且保证电源纹波不要太大。MLX90614对电源噪声是比较敏感的最好给它单独加一个100nF去耦电容放在传感器引脚附近。还有一种常见情况温度整体偏低1~2℃。这不一定就是代码的问题可能是测温距离不同导致的。MLX90614的红外测温是视角范围内所有物体的平均温度如果装在一个空旷的位置传感器视野很大周围环境的低温也会被平均进结果里。可以给传感器前端加一个遮光管比如黑胶管或3D打印的聚光筒把视场角限制在5~10度内对着人脸方向测读数准确度会明显提升。我实测过不加聚光筒时额头测温36.2℃加上后能到36.5℃左右和医用温度计差距缩小到0.2℃以内。5.2 QT上位机收不到数据第一步先查协议还是查波特率这个问题我见得太多了。优先顺序应该是检查串口号和波特率再用串口助手对比调测最后才怀疑代码解析。因为QT里打开了错误的串口或者串口的波特率/数据位/校验位与单片机不一致时数据就会以乱码形式出现或根本不被触发readyRead。快速定位方法把STM32固件里的发送函数单独抽出来上电后每隔一秒主动发一条固定帧用“友善串口助手”这类工具先验证底层发送链路。如果串口助手能收到完整数据说明发端没问题问题在QT端的参数配置或者解析逻辑。如果串口助手也收不到那就去查单片机串口有没有初始化成功、会不会被调试器占用了、有没有接错TX/RX——TX接RXRX接TX这个口诀我已经说过无数遍了但每隔几天还是有人问。调试技巧在QT里加一个“接收十六进制”的显示切换开关。平时的字符显示模式下如果看到一堆乱码说明波特率不匹配如果十六进制看到数据里有AA开头的完整链路说明协议对得上。这个开关在实际联调时比任何调试日志都好使。5.3 界面卡死/曲线不同步的软件层面的坑曲线不同步最常见的原因是主线程里做了耗时操作比如文件写入、大段数据处理把Qt事件循环给阻塞了。串口数据到来时如果UI线程正在忙信号槽就会排队延迟表现出来就是曲线一顿一顿地跳。遇事不决先把耗时操作丢到子线程或者QTimer异步任务里让UI线程始终保持在毫秒级的响应水平。QCustomPlot的replot其实比较消耗CPU如果一秒钟刷新几十次加上Widget本身的绘制开销在低配电脑上确实会卡。我实测下来5Hz的数据刷新配1Hz~2Hz的重绘视觉效果已经很顺滑了。还有个常见问题上位机和单片机上电顺序不一致导致数据错乱。比如上位机先打开串口单片机后上电启动两者刚刚开始通信时有几帧半包数据是正常的协议解析逻辑应该具备“丢弃不完整帧”的能力。如果因为头几帧错乱导致界面直接崩溃那多半是解析代码里对非法数据的防御没做到位访问字符串或数组越界了。这里务必养成习惯所有缓冲区操作先判断长度所有解析先校验再使用。6. 从能用演示到稳定落地再多补充几点项目做完了功能演示也通过了但如果你要拿去比赛答辩或者真正部署到现场有些细节还能再打磨打磨。首先上电自检和异常状态提示得做出来。上电时检测MLX90614是否正常通信读不到地址就显示“传感器错误”并蜂鸣报警。不然设备放在那传感器线松了自己不知道测出来全是乱码客户体验极差。其次是断线重连机制。上位机在串口被拔出后要能自动识别并提示重新插入时能手动重连别动不动就白屏、崩溃。这些容错处理在演示环节最容易被老师突然袭击准备好应对得分率能高很多。再讲一个提升档次的技巧把温度曲线和OpenMV的检测状态联动起来。当人脸离开检测区域时上位机的曲线可以暂停记录不把无效数据点画进去等下次检测到人脸再继续。这个功能虽然只是判断了一个状态位但在演示时给评审看的效果非常直观——“看没有人脸时温度曲线不更新检测到人脸时曲线立刻恢复”专业感直接从“做了个demo”升到“做了个产品原型”。扩展方面实际上这个项目的架构扩展性很好。把MLX90614换成MLX90640热成像阵列固件层只需要改传感器驱动和数据处理上位机增加一个热力图矩阵渲染就能变成一台廉价的红外热成像仪。如果想做多人同时测温甚至可以再加一个小米人体传感器或压力传感器来做触发联动逻辑也完全复用现有的状态机思路。从学习角度来看这个项目最大的价值在于掌握了一套“传感器采集 → MCU处理 → 协议组帧 → 上位机解析 → 可视化展示 → 数据存储”的完整开发闭环。以后你再做任何涉及硬件和上位机的项目哪怕换个传感器、换个GUI框架核心的架构思维是不变的。这也是为什么很多嵌入式工程师面试时会被问“你怎么设计一个设备的上位机通信协议”——因为真正干活的时候协议设计、状态机划分、容错处理这些能力才是拉开差距的地方。本文还有配套的精品资源点击获取