简介:基于STM32F103与OpenMV模组的无接触式人体温度测量与身份识别系统,是一份面向嵌入式开发学习者、电子设计竞赛参赛者及智能防疫项目开发者的完整工程资料。系统融合了GY906DCI红外测温、体温异常报警、口罩佩戴检测、人脸身份学习与识别,以及实时OLED显示界面等技术,从单片机底层驱动到机器视觉算法均有完整覆盖。压缩包共198个文件,约8.03MB,包含大量的C语言与头文件源码、Python脚本、训练好的TensorFlow Lite模型、工程配置文件以及Word文档说明等,另有编译生成的烧录文件,便于直接查阅、二次开发与模块复用。项目源自电子设计竞赛赛题场景,目录结构完整,适合学习多传感器协同、串口通信、图像处理与嵌入式界面设计。目前已有75人学习,可作为课程设计、毕业设计或竞赛备赛的实用参考。
1. 从“滴一声”到“看一眼”:为什么体温检测需要身份与口罩信息一起做
车站安检口那台“滴”一下报体温的黑匣子,只回答“发烧没发烧”,但大多数场景真正需要的是一次交互里同时拿到三个答案:体温是否异常、有没有戴口罩、这个人是谁。把这三个问题分开做,两台设备三个流程,现场人员要盯三块屏;合到一套“基于STM32F103与OpenMV模组”的系统里,主控做逻辑与显示,视觉模组做人脸与口罩判断,GY-906做非接触测温,一次抓拍就同步归档——这才是这套设计存在的理由,也是毕设和产品原型里最常被抄的骨架。
这套方案的受众很明确:手里有STM32F103开发板、想给课设或作品加“智能感”的学生,以及要快速验证测温考勤一体机原型的硬件工程师。它的门槛不在传感器,而在两片处理器之间的数据协议设计,以及OLED上那套实时GUI怎么把温度、口罩状态、人脸ID塞进一块0.96寸屏幕而不显得乱。下面按从选型到联调的路径拆开讲,最后把最容易让新手熬夜的坑集中排一遍。
2. 分清楚谁干活:STM32F103做主控、OpenMV负责看的架构边界
2.1 为什么不把测温、人脸识别全塞进STM32F103
STM32F103系列主打的是控制与接口,Cortex-M3内核在72MHz下跑RTOS、驱动外设、解析协议都很稳,但让它跑人脸识别就强人所难了——H7系列都未必舒服,何况F103连硬件浮点都没有。OpenMV模组自带摄像头、SD卡槽和MicroPython解释器,内置的人脸检测、特征学习、口罩分类示例代码几乎开箱即用,是低成本视觉方案里比较省事的选择。常见做法是OpenMV只管“看”,把识别结果通过串口发给STM32F103,主控负责读GY-906的I2C温度数据、做阈值判断、驱动OLED与蜂鸣器。
这个分工带来的另一个好处是调试解耦。OpenMV的IDE能实时看帧率和识别框,STM32侧只看串口日志就行,不用两台设备一起跑仿真。实践中我一般会把OpenMV的串口波特率固定为115200bps,8位数据、1位停止位,和STM32的USART配置对齐——两边的奇偶校验都设为无,否则粘包率会明显上升。处理器分界不是性能攀比,而是让擅长的人干擅长的事。
2.2 GY-906的I2C时序与STM32F103的HAL库配置
GY-906模组用的是MLX90614芯片,默认I2C地址是0x5A(7位地址是0x5A,读写时分别左移拼接方向位),上电后需要大约0.5秒稳定。和STM32F103通信时,我习惯用CubeMX把PB6、PB7配成I2C1,速率设为100kHz标准模式,因为MLX90614对总线时序要求不算苛刻,但400kHz快模式下某些模块会偶尔丢ACK。HAL库驱动OLED的代码网上很多,但I2C上挂两个设备时要注意:GY-906和0.9寸OLED共用I2C总线需要确认OLED的地址是0x3C还是0x3D(0.9寸OLED对I2C兼容问题大多出在地址位或SA0电平上),两块设备的地址不冲突才能共用。
读取温度的核心代码段如下:
// 读取MLX90614的RAM地址0x07(物体温度)和0x06(环境温度) uint8_t read_reg = 0x07; HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&hi2c1, 0x5A << 1, read_reg, 1, (uint8_t *)&raw_temp, 3, 100); if (status == HAL_OK) { // 原始数据低字节在前,高字节在后,共16位;右移两位后乘0.02得到摄氏度 uint16_t data = (raw_temp[1] << 8) | raw_temp[0]; float object_temp = (data >> 2) * 0.02f - 0.01f; }这段代码的关键在两点:一是HAL_I2C_Mem_Read的MemAddressSize参数设成1字节,MLX90614的寄存器地址就是8位寻址;二是返回的3个字节里第3个字节是PEC校验,如果不需要可以忽略,但读取长度必须写3,否则部分库的DMA模式会读到错误数据。PEC校验关闭时(模块默认状态),0.01的偏移补偿可以视校准结果决定保留还是去掉,不影响读数稳定性。环境温度从0x06读,用来判断探头自身是否被阳光晒热——这个数值异常偏高时,测出来的体温可信度就要打问号。
2.3 OpenMV串口协议:一帧数据把“人、脸、口罩、温度”打包
OpenMV侧要发的信息至少有四类:检测到的人脸坐标框、人脸ID编号、口罩佩戴状态(1表示戴了,0表示没戴)、以及当前帧是否有人在。把这些全用文本拼成逗号分隔串,STM32解析虽然容易但容易出错——比如人脸ID是两位数时分隔符匹配就乱了。我建议用固定头+定长字段的二进制帧结构,避免这个坑。帧格式可以这样定:帧头0xAA 0x55、数据长度1字节、数据类型1字节、数据体若干字节、校验和1字节,STM32状态机按“找帧头-收长度-收数据-校验”四步走。
import sensor, image, time, ustruct from machine import UART uart = UART(3, 115200, timeout_char=1000) sensor.reset(); sensor.set_pixformat(sensor.RGB565); sensor.set_framesize(sensor.QVGA); sensor.skip_frames(30) face_cascade = image.HaarCascade("frontalface", stage=2) while True: img = sensor.snapshot() faces = img.find_features(face_cascade, threshold=0.7, scale_factor=1.25) # 假设有独立函数判定口罩与人脸ID mask_state = 1 if check_mask(img) else 0 face_id = get_face_id(img) if faces else 255 # 帧结构:0xAA55 + 长度 + 类型 + 数据(4字节) + 校验 data = ustruct.pack("<BB", mask_state, face_id) # 小端: 口罩状态 + 人脸ID payload = ustruct.pack("<BB", 0xAA, 0x55) + ustruct.pack("<B", len(data)) + \ ustruct.pack("<B", 0x01) + data checksum = sum(payload) & 0xFF uart.write(payload + ustruct.pack("<B", checksum))代码里的HaarCascade阈值与scale_factor直接影响检测距离和误检率。threshold设太高会漏检,设太低会把背景纹理当人脸;我一般先从0.7起步,实测后发现OpenMV模组在QVGA分辨率下距离1.2米左右的人脸检测最稳,太近反而因为脸超出画面而失败。人脸ID学习的过程通常是把人脸关键点特征存到OpenMV的SD卡里,每次开机load特征库,识别时返回最接近特征的ID,255表示未知。如果只是做原型,可以先让每个受测者按下学习键采集10帧特征建立档案。
3. 把数据变成界面:OLED实时GUI的设计与状态机
3.1 0.9寸OLED显示界面布局与汉字取模
OLED显示模块的痛点不在硬件上——I2C接口四根线(VCC、GND、SCL、SDA)接错才会烧,正常情况屏本身很皮实——真正麻烦的是显示内容。0.9寸屏分辨率128x32,只够显示两行汉字或四行数字,参考“stm32f103c8t6的oled”相关代码能发现绝大多数字库都自带ASCII字符,但汉字要自己取模。用PCtoLCD2002软件逐字生成16x16点阵数组,再把数组按页写入SSD1306的GRAM。这个取模时注意“逐行式”还是“逐列式”,要和驱动代码的写GRAM顺序对齐,差一个方向字就全是镜像的。
我的布局习惯是:第一行走图标+状态栏,比如“人脸ID: 01”,第二行走“T: 36.5C”,第三行显示“口罩: OK”或“请佩戴口罩”,第四行留给告警条。128x32按4行、每行高度8像素来切分,不足的部分用反色小方块做进度条或角标。
3.2 状态机驱动GUI:温度握手、人脸识别、超温告警的三态切换
GUI不要写成顺序执行的代码块,否则串口数据一断整个界面就卡死。用一个简单的枚举状态机:
typedef enum { GUI_WAIT_FACE, // 等待OpenMV上报人脸 GUI_MEASURING, // 已见人脸,正在读温度 GUI_ALARM // 体温越限,触发告警 } gui_state_t;主循环里每10ms轮询一次状态:GUI_WAIT_FACE时如果串口收到合法帧且face_id != 255,切到GUI_MEASURING并打拍子启动一次GY-906读取;GUI_MEASURING里等待温度稳定,连续读5次取平均,超过37.3℃就进GUI_ALARM并拉高蜂鸣器引脚;GUI_ALARM保持到操作员按键确认或OpenMV上报人脸消失。这样做的好处是逻辑清晰,后续加“按按键重测”也就是在状态机里多插一个转移条件。
4. 体温异常报警与口罩检测:阈值标定与联动逻辑
4.1 测温报警的分级阈值和3个校准锚点
体温异常报警不是“大于37.0就响”。红外测温受距离、环境温度、皮肤发射率影响,直接拿默认阈值会天天误报。我通常会做三个锚点校准:一是黑体(或恒温杯)校准,把GY-906读数与已知温度比对得到偏移量;二是环境温度补偿,把读到的环境温度作为查表索引;三是距离修正,实际部署时固定安装距离(比如30cm),这个距离下误差是可控的。报警阈值软件里做成两个变量:T_alarm_low和T_alarm_high,分别对应37.3℃的“关注”和38.5℃的“异常”,前者亮黄图标,后者红图标加蜂鸣器。
实测中典型的坑是“额头温度偏低、手心温度偏高”,如果用额温枪的标准去set阈值会让腋下测温的场景全报绿。如果设备是手持贴额的,阈值按36.5-37.2为正常区间比较贴实际;如果是闸机远距,阈值就要放宽到37.8再触发告警,因为远距测温读数普遍偏低。
4.2 口罩检测联动测温:先看戴没戴,再读温度
联动的顺序是个隐性设计点。如果先测体温再检测口罩,一个没戴口罩的人会因为靠近设备而被读到高温——这个人到底该按“发烧”处理还是按“未戴口罩”处理?常见做法是先看OpenMV的mask_state,如果为0就直接在OLED显示“请佩戴口罩”,不启动GY-906读取;只有mask_state为1才进入测温流程。这样两个告警不会互相打架,而且逻辑上符合现场防疫的基本认知:先阻断未防护者进入,再筛查发热者。
5. 避坑:串口粘包、OLED花屏和测温漂移
5.1 串口粘包与丢帧:现象是温度偶尔显示成“--”,原因是对帧边界不敏感
现象:OLED上的温度值偶尔跳成“--”,或人脸ID在几秒内乱跳。 原因:OpenMV发送频率高于STM32解析速度,多个帧被缓冲在DMA里,一次性读出后如果只按固定位置解析,第二个帧的帧头就成了数据位。 解决:解析代码用状态机,完整实现“找0xAA 0x55同步字→读长度→读数据体→校验”,校验不通过就丢弃当前帧并回到找帧头状态,不会因为一个坏帧让后续所有帧错位。
5.2 OLED显示残影或全亮:I2C地址和复位时序是头号嫌疑
现象:刚上电时OLED全亮几秒,之后显示乱码或两边亮度不均。 原因:SSD1306在I2C地址0x3C和0x3D之间选择了错误的地址,或者初始化序列里缺少开显示指令,也可能VCC和GND接反烧了板载稳压(少数模块没有反接保护)。 解决:先单独用I2C扫描工具确认设备地址;再检查初始化序列是否包含0xAF开显示;最后测量模块VCC引脚的3.3V是否稳定,不要从STM32的3.3V引脚直接拉大电流,我一般OLED和GY-906都用外部3.3V LDO单独供电。
5.3 GY-906温度漂移:探头自身发热是隐藏变量
现象:设备开机连续运行20分钟后,体温读数比刚开机时稳定偏高0.2℃-0.4℃。 原因:MLX90614靠近主控板和OpenMV的发热区域。模组的测温能力是对温差敏感的,探头自己温度升高,而内部算法补偿的参考点固定时,输出就会正向漂移。 解决:把GY-906通过排线引出,探头悬空靠近检测位置,避免贴在电路板或OpenMV外壳上;软件里做开机5秒的“预热采集”,把这时的环境温度作为基线,后续每次都减去基线与开机环境温度的变化量。
5.4 OpenMV学习特征偶尔认不出人:光照和角度比算法影响更大
现象:同一人上午能识别,下午换了顶灯或换了眼镜就显示未知ID。 原因:OpenMV的LBP特征对光照变化和遮挡比较敏感,学习时如果只采集了一个角度,识别时稍有偏转就匹配不上。 解决:建人脸档案时执行“旋转录制”,让被测者左右转15度各拍10帧,并把这10帧的平均特征写入SD卡;识别时用帧率上限的一半(比如25fps),留出更多时间给算法对比特征库。
5.5 蜂鸣器长鸣停不掉:告警状态没加手动复位
现象:超过阈值后蜂鸣器一直响,温度已经降下来也不停。 原因:状态机停在GUI_ALARM里,缺少退出条件。 解决:给告警状态增加“温度回落到阈值以下并持续3秒”的自动复位,同时还监听一个按键引脚作为手动确认。两条路径都要有,现场才不会被一个卡死状态锁住。
6. 联调验证技巧:用一张A4纸和一台手机就能完成的可靠性测试
联调完不是跑了demo就交付,需要三个维度的实际验证:测温精度、识别鲁棒性、协议可靠性。我的做法很简单,打印一张40cm x 40cm的棋盘格纸贴在墙上,用手机支架固定OpenMV在同一高度,让人站到距设备30cm处,把串口监视器和OpenMV IDE同时拉开,人工记录50次检测结果,统计三个数据:漏检率(有面无人框)、误报率(无人却显示ID)、测温标准差。50次里漏检超过3次就要调HaarCascade阈值或光照;测温标准差超过0.15℃就要重做校准。
另一个容易被忽略的验证是串口压力测试。把OpenMV的帧率从25fps调到60fps,让串口数据跑到接近带宽上限,看STM32的OLED是否依旧流畅刷新、日志有没有乱码。这一步能提前暴露DMA缓冲和解析状态机的竞争问题。如果一切正常,再跑一个8小时连续上电的老化测试——GY-906的漂移和OLED的老化都是这么发现的,别等到正式演示时屏上出现一条死线。
这套方案的落地成本不高,F103开发板加OpenMV加GY-906加OLED总共不到300元,但设计的核心从来不是堆料,而是把“两核通信、传感器校准、GUI状态机”这三件事理顺。先照着文中协议在桌面跑通串口互发,再上传感器调阈值,最后再碰界面,顺序反了会让问题叠加成黑匣子。希望这篇拆解能帮你在搭这套方案时少走几步弯路。
(全文完)
本文还有配套的精品资源,点击获取