简介:基于STM32F103与OpenMV模组的无接触式红外体温监测与身份识别系统完整工程,面向嵌入式开发者和物联网方向学习者,适用于机场、学校、办公楼等场所的体温筛查与人脸识别场景。包内共一百九十八个文件,以C语言工程源码为主,包含众多头文件与源文件,以及编译生成的中间文件和目标文件,另有神经网络模型文件和Adaboost相关算法文件,压缩包整体八点零三兆,结构清晰便于二次开发。系统通过GY906DCI红外测温模块采集温度数据,经STM32F103处理实现实时体温监测;OpenMV模组负责采集人脸图像,利用神经网络模型精准识别身份,并结合Adaboost算法检测口罩佩戴情况。除完整源码外,还提供可烧录文件、初始化脚本和说明文档,方便快速验证算法效果与硬件功能;整套工程既可用于课程设计与毕业设计,也适合产品原型验证与小规模部署。目前已有六十二人学习下载,对开展嵌入式综合实训和智慧防疫应用开发具有较高参考价值。
1. 无接触式红外体温监测与身份识别:为什么是STM32F103和OpenMV这套组合
通道闸机、考勤机、会议签到处,最近两年几乎都要同时解决两件事:测体温,验身份。多数现场的做法是红外测温枪加人脸打卡机,两台设备各干各的,数据对不上。这个项目标题给了一条更紧凑的链路:STM32F103做主控和数据处理中枢,OpenMV模组负责摄像头画面里的人脸检测、口罩判定,GY906DCI红外测温模块无接触读取额头表面温度,最终把体温、人脸结果、口罩状态集中在OLED屏上输出,超阈值就告警。它最适合三类人:做课设或毕设的学生、想快速验证测温门禁样机的硬件工程师、手里恰好有蓝板子和OpenMV想拼出一个完整原型的人。这套组合不追云端大模型,用一颗Cortex-M3加一个机器视觉模组就能把闭环跑通,属于性价比很实在的嵌入式方案。
2. 硬件选型与最小系统搭建:STM32F103、GY906DCI与OpenMV的接线、供电和串口链路
2.1 STM32F103最小系统搭建与5V转3.3V电路
先不要急着接线,把最小系统弄干净。STM32F103要跑起来,最基础的四个部分:8MHz晶振(对应HSE)、BOOT0引脚通过10k电阻下拉到GND、NRST复位电路(典型用10k上拉加0.1uF对地电容)、以及VDDA/VSSA的滤波电容。很多第一次搭板子的人只接了VDD和GND,程序能下进去但一跑就hardfault,十有八九是VDDA没接或者晶振负载电容配错。负载电容选12~22pF,我用20pF用得最多,起振稳定,没有明显频偏。
供电链路是整个系统最容易翻车的地方。STM32F103和OpenMV、OLED都需要3.3V,而GY906DCI模块可以吃5V也可以吃3.3V。常见做法是用5V电源输入,经过AMS1117-3.3稳压得到3.3V总线。AMS1117价格低、外围就四个电容,压差0.8V左右给5V转3.3V足够。输入侧放一个10uF电解电容加0.1uF陶瓷电容,输出侧同样放10uF加0.1uF,位置靠近芯片引脚,不要拉长走线。这个电容配置不是玄学,AMS1117对ESR比较敏感,输出电容太小会自激振荡,导致3.3V输出在2.8V到3.6V之间跳,这时候I2C和串口全部乱掉。
| 模块 | 供电引脚 | 电压 | 最大电流 |
|---|---|---|---|
| STM32F103C8T6 | 3.3V | 3.0V~3.6V | 约80mA |
| OpenMV H7 | Vin或3.3V | 3.3V~5V | 约350mA |
| GY906DCI | VIN | 3.3V~5V | 约10mA |
| SSD1306 OLED | 3.3V | 3.0V~5V | 约20mA |
注意:OpenMV用USB供电时已经自己稳过压,再接到同一组3.3V总线容易造成两个LDO对拉。我的习惯是OpenMV单独用5V供电,F103这边单独用AMS1117,两边只共地,不共3.3V电源轨。
2.2 GY906DCI红外测温模块的I2C接线与地址配置
GY906DCI的核心是MELEXIS的MLX90614,ESF封装,输出红外热电堆信号,内部已经做了信号调理和温度线性化,对外只暴露一根SMBus接口(兼容I2C时序)。模块上通常引出四个脚:VIN、GND、SCL、SDA。它不是我之前用过的模拟输出型红外传感器那样读ADC,而是直接读数字寄存器,省掉了大量标定工作。
接线时把模块挂到STM32F103的I2C1上,默认引脚是PB6作SCL、PB7作SDA。模块的VIN可以直接接5V,它的板载稳压会转成内部3.3V,SCL/SDA电平也兼容3.3V。如果直接把模块的VIN接到3.3V也能工作,但注意模块上若已有稳压芯片,3.3V输入到某些LDO会因压差不够输出异常,所以VIN接5V更省心。
I2C地址是新手第一个坑。MLX90614出厂默认7位地址是0x5A,但在STM32的HAL库函数里,HAL_I2C_Master_Transmit()的DevAddress参数传的是8位地址,也就是0x5A左移一位得到0xB4。不少人在这里直接填0x5A,结果I2C始终返回超时。另外SCL和SDA需要上拉电阻,模块PCB上一般已经焊了4.7k上拉到VIN,如果你是自己买裸芯片而不是模块,记得外部补两个4.7k电阻到3.3V。缺少上拉时总线不会完全报错,但通信不稳定,表现为读十次有两次返回0xFF。
| GY906DCI引脚 | STM32F103引脚 | 说明 |
|---|---|---|
| VIN | 5V | 模块供电 |
| GND | GND | 必须共地 |
| SCL | PB6 | I2C1_SCL,外部上拉 |
| SDA | PB7 | I2C1_SDA,外部上拉 |
2.3 OpenMV与STM32F103的串口通信链路
OpenMV和STM32F103之间走UART,这是最稳妥的方案。OpenMV的P4(TX)和P5(RX)对应它的UART3,STM32这边用USART2,引脚PA2作TX、PA3作RX。连接要交叉:OpenMV的P4接到STM32的PA3,OpenMV的P5接到STM32的PA2,两边GND连在一起。电平都是3.3V,不需要电平转换芯片。
波特率我建议先用115200,别一上来就追求460800。OpenMV跑神经网络推理时帧率本来就不高,每秒传输的数据量也就几百字节,115200完全够。在STM32侧用CubeMX配置USART2,代码如下:
// CubeMX生成的HAL_UART_MspInit基础上,配置USART2参数 huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; HAL_UART_Init(&huart2);这段配置里BaudRate两边必须完全一致,差了哪怕1%在长帧数据下都会出现帧错误。WordLength是8位数据位,None校验,1位停止位,是最通用的8N1格式。HwFlowCtl不使能硬件流控,因为OpenMV那边没有RTS/CTS引出。初始化之后,接收用中断或DMA都行,但接收不定长帧最好配合空闲中断,这个在后面的帧协议里细说。
3. 温度读到能用:GY906DCI寄存器读取、多点采样与校准流程
3.1 通过I2C读取MLX90614的物体温度寄存器
MLX90614的数据手册里,RAM地址0x07保存的是物体温度,0x06保存的是环境温度,都是16位有符号数据,单位是开尔文,分辨率0.02°C,实际值要乘以0.02再减去273.15。读取流程是两个操作:先写一个字节的寄存器地址,再读连续三个字节,前两字节是温度数据低字节和高字节,第三个字节是PEC校验。
uint8_t reg = 0x07; uint8_t buf[3]; float temp_c; HAL_I2C_Master_Transmit(&hi2c1, 0xB4, ®, 1, 100); HAL_I2C_Master_Receive(&hi2c1, 0xB4, buf, 3, 100); uint16_t raw = (buf[1] << 8) | buf[0]; if (raw == 0 || raw == 0xFFFF) { temp_c = -999.0f; // 读值异常,标记为无效 } else { temp_c = raw * 0.02f - 273.15f; }逻辑说明:先发送要读取的寄存器地址,然后从同一地址连续读3个字节。HAL库的DevAddress传0xB4,是7位地址0x5A左移一位的结果。if判断raw等于0或0xFFFF是因为I2C总线异常时通常读到全0或全1,这时候不能把温度算成负数或几百摄氏度,必须返回一个无效值让上层逻辑去处理。超时时间设100ms,正常读一次在毫秒级,100ms足够且不会长时间卡住主循环。PEC校验字节读回来了但没有参与计算,这是有意的,目的是减少复杂度;如果读出来的温度偶尔跳变,再考虑把PEC加进来。
想进一步确认通信是否正常,可以先读一下MLX90614的ID寄存器,地址是EEPROM的0x20,能读出固定的8字节序列。这个寄存器值不会随温度变化,适合在初始化阶段做一次自检。具体的寄存器位定义可以直接翻ST的《STM32F103系列参考手册(RM0008)》和MLX90614数据手册,不必背下来,但要清楚I2C时序在F103上是通过片上外设完成的,剩下要调的只是地址和时序参数。
3.2 多点采样与滑动滤波:把温度波动压下去
GY906DCI非接触测温有一个固有弱点:目标距离、角度、环境辐射都会让读数抖。同一个额头,距离5cm和10cm读数可能差0.5°C。所以在软件层必须做两点:一是固定采样节奏,二是滤波。常见做法是每200ms读一次,读10次,去掉最大值和最小值后求平均。这个"去极值平均"比单纯滑动平均更抗偶尔的尖峰干扰。
#define SAMPLE_N 10 float buf[SAMPLE_N]; float temp_filtered = 0.0f; for (int i = 0; i < SAMPLE_N; i++) { buf[i] = read_obj_temp(); // 3.1节的读取函数 HAL_Delay(200); } // 冒泡找最大最小(样本少,不必优化) float min_v = buf[0], max_v = buf[0]; float sum = 0.0f; for (int i = 0; i < SAMPLE_N; i++) { if (buf[i] < min_v) min_v = buf[i]; if (buf[i] > max_v) max_v = buf[i]; sum += buf[i]; } temp_filtered = (sum - min_v - max_v) / (SAMPLE_N - 2);逻辑说明:采样间隔200ms是折中选择。MLX90614每次转换时间约90ms,换算到最高19Hz的刷新率,200ms的间隔能保证每次读到的都是完整的新数据,不会读到转换到一半的中间态。去掉最大最小再平均,是防止有人在测温瞬间从口袋里掏出手机之类的热源靠近传感器,造成单点跳变。这样处理之后,输出的温度曲线平滑很多,但代价是响应变慢,从开始测到出稳定结果大约要2秒,在门禁场景里完全能接受。
注意:如果发现滤波后温度变化过慢,比如对着冰凉金属板等3秒还没反应,优先检查采样间隔是不是太长,而不是改滤波算法。把HAL_Delay换成50ms、样本数降到5,响应时间能减到300ms左右,但抗干扰能力会弱一些。
3.3 温度阈值判定与告警逻辑
阈值不能写死成一个裸值。额温枪测的是体表温度,与腋下体温有偏差,常见做法是在软件里设一个可配置偏移量,比如模块出厂用黑体标定后偏移为+0.2°C,再叠加每个安装位置的环境差异。最终判定逻辑用连续N次超阈值来抑制误报:
#define TEMP_ALARM_THRESHOLD 37.3f #define TEMP_OFFSET 0.2f #define ALARM_COUNT_THRESHOLD 3 static uint8_t over_count = 0; static uint8_t alarm_state = 0; float body_temp = temp_filtered + TEMP_OFFSET; if (body_temp > TEMP_ALARM_THRESHOLD) { over_count++; if (over_count >= ALARM_COUNT_THRESHOLD) { alarm_state = 1; } } else { over_count = 0; alarm_state = 0; }参数说明:TEMP_ALARM_THRESHOLD取37.3°C是参考了常见测温设备的告警线,实际项目里应该做成可以在OLED菜单里修改的变量,而不是宏。TEMP_OFFSET是校准偏移量,出厂的测温模块通常会给一个标定差值,也可以用自己的额温枪对比20组数据之后算平均差填入。over_count连续计数的作用是过滤偶发抖动,比如人刚跑完步走过来,额头温度暂时偏高,如果只看单次很容易误报;连续三次确认超标才拉高alarm_state,稳定性和响应速度比较平衡。
4. OpenMV端的人脸识别与口罩检测:Adaboost级联与轻量卷积神经网络模型的分工
4.1 Adaboost人脸检测:Haar级联分类器在OpenMV上的应用
OpenMV的人脸检测用的是经典的Viola-Jones框架,也就是基于Haar特征和Adaboost训练出来的级联分类器。Adaboost的核心思想是把大量弱分类器按权重组合成一个强分类器,OpenMV里直接封装成了HaarCascade对象,自带一个frontalface的级联模型。这个模型不是神经网络,但它计算量小,在OpenMV上能跑到实时帧率,正好适合做人脸检测的"粗筛"。
import sensor, image sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(True) sensor.skip_frames(time=2000) face_cascade = image.HaarCascade("frontalface", stages=25) while True: img = sensor.snapshot() faces = img.find_features(face_cascade, threshold=0.5, scale_factor=1.35) for r in faces: img.draw_rectangle(r) print("face:", r.cx(), r.cy())逻辑说明:先把摄像头设置为灰度图QVGA(320x240),因为Haar特征在灰度图上计算最直接,RGB图反而增加计算量。find_features返回一个矩形列表,每个矩形是人脸候选框。threshold是判断阈值,越大越严格,0.5左右误检少,调到0.3会捡回更多人脸但也会把背景里的纹理误检成人脸。scale_factor是每轮扫描窗口的缩放步长,1.35表示每层放大1.35倍,数值越大扫描越快但可能漏掉尺寸变化快的人脸,数值越小越慢越准。
检测到人脸之后,可以用img.draw_rectangle画出人脸框,但更好的做法是只把RTTI数据(人脸框中心坐标和宽高)通过串口发给STM32F103,让单片机自己去画OLED提示或驱动云台。灰度图下OpenMV的帧率能到30fps以上,加上人脸检测也不会掉到不可用。
4.2 轻量卷积神经网络模型:口罩检测的部署方式
口罩检测没法用Haar级联硬做,因为口罩没有固定纹理特征,用神经网络分类更靠谱。但OpenMV的算力和内存有限,跑不了ResNet这种重量级网络,只能用轻量卷积神经网络模型。常见的做法是先用Edge Impulse或者Keras自己训练一个二分类模型,输入尺寸压到96x96或64x64,卷积层控制在3层左右,然后量化导出为tflite格式,拷贝到OpenMV的SD卡上。
import sensor, image, nn labels = ["unmasked", "masked"] net = nn.load('/mask_detect.tflite', load_to_fb=True) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) sensor.skip_frames(time=2000) while True: img = sensor.snapshot() for obj in net.classify(img, min_confidence=0.65): idx = obj.output().index(max(obj.output())) label = labels[idx] rect = obj.rect() img.draw_rectangle(rect) img.draw_string(rect.x(), rect.y(), label, color=(255, 0, 0)) if label == "unmasked": uart.write(b'\xA5\x5A\x03\x02\x01\xA9') # 发送未戴口罩状态参数说明:load_to_fb=True让模型加载到帧缓冲区,推理时不必反复读SD卡,速度更快,但会占掉大部分内存,所以输入分辨率只能到QQVGA(160x120)。min_confidence是置信度阈值,0.65意味着网络觉得至少有65%概率是某一类才输出结果,低于这个值丢弃,这样能压掉很多"不确定"的误判。obj.output()返回概率列表,比如[0.12, 0.88]表示未戴口罩12%、戴口罩88%,取最大值索引就是最终类别。
训练阶段有一件事值得提前做:把不同光照、不同肤色、不同口罩颜色的图片都放进训练集,并且加随机旋转、亮度抖动做数据增强。否则模型在自己电脑上测试准确率99%,到现场对着自然光一拍就翻车,这类现象在卷积神经网络模型落地时太常见了。
4.3 识别结果编码与串口帧协议
OpenMV和STM32之间传输的不能是一堆散的print字符串,要定义帧协议。我的习惯是:帧头0xA5 0x5A,长度字节,命令字,负载数据,最后一字节校验。校验用从长度到负载所有字节的累加和取低8位。
from pyb import UART import ustruct uart = UART(3, 115200, timeout_char=100) def send_frame(cmd, payload): length = len(payload) + 2 # cmd + checksum data = bytearray([0xA5, 0x5A, length, cmd]) data.extend(payload) checksum = sum(data[2:]) & 0xFF data.append(checksum) uart.write(data) # 示例:发送检测到人脸,未戴口罩,中心坐标(120, 80) payload = ustruct.pack("BBHH", 0x01, 0x00, 120, 80) send_frame(0x01, payload)这段代码里cmd=0x01表示识别结果帧,payload前两个字节分别是"是否检测到人脸"和"是否戴口罩",后两个字节是人脸中心x、y坐标。STM32端用状态机解析:收到0xA5且下一个字节是0x5A才开始累积,按长度字节接收完整个帧之后算一遍校验,对不上就丢弃。这样即使串口上偶尔有毛刺或乱码,也不会影响后续帧的解析,因为校验失败的数据根本不会被采用。
5. 联调避坑与常见问题排查:ST-Link调试、I2C读不到温度与串口乱码的五个现场
5.1 Keil MDK工程搭建与ST-Link调试全流程
工程搭建这块,建议直接用CubeMX生成HAL库工程,然后在Keil MDK里编译下载。STM32F103需要先装Device Family Pack,常见的坑是Pack版本和Keil版本不匹配,装好后在Device标签找不到STM32F103C8,解决方案是手动指定Pack文件路径或换个高版本Keil。ST-Link调试器接SWD四根线:SWDIO、SWCLK、GND、3.3V,如果只需要下载,接前三根就行。下载时报错No Target Connected,先量一下板子3.3V和GND是否短路,再看BOOT0是不是被外部电路拉高,最后检查ST-Link的SWDIO和SWCLK有没有接反。这一套流程走通之后,后面所有报错都能通过断点定位到具体代码行,强过printf盲猜。
5.2 I2C读不到GY906DCI:现象、原因与解决
现象:HAL_I2C_Master_Transmit返回HAL_TIMEOUT,或读回来的buf全为0xFF。
原因分三种:一是地址传错,把0xB4写成0x5A;二是SCL/SDA两根线没有上拉或者上拉电阻虚焊,导致电平拉不低;三是模块供电电压不够,VIN接在3.3V上且板载稳压压差不足。解决顺序:先用逻辑分析仪或示波器看SCL/SDA波形,确认有没有数据在跑;再用万用表量模块VIN对GND电压,必须稳定在3.3V以上;最后用I2C扫描程序把所有地址扫一遍,看0x5A地址能不能被枚举到。如果扫描不到,问题大概率在硬件链路而不是代码。
5.3 OpenMV串口乱码与帧丢失
现象:STM32串口助手收到的数据里夹杂乱字符,或者一条帧被截断成两段。
原因:波特率不一致、两边没共地、没有帧同步机制。OpenMV端UART默认8N1,如果有人把字长设成9位或者开了校验位,两边就完全对不上。帧丢失大多是解析方式不健壮,比如用固定长度接收,一旦中途少一个字节后面全错位。解决:加帧头+长度+校验,用状态机解析。接收中断里每来一个字节都进状态机,比对帧头、累计长度、最后算校验,校验通过才置标志位。这样即使连续几帧出错,下一帧帧头对了依然能恢复同步。
5.4 测温值忽高忽低或明显偏低
现象:对着人脸测出34°C,对着空气测出31°C,数据毫无规律。
原因:测量距离超过10cm。MLX90614是SMBus封装的被动红外传感器,能量收集角度有限,距离越远读数越低且越不稳定;另外传感器窗口如果沾了灰尘或油脂,红外透过率会下降。解决:把GY906DCI固定在一个支架上,让目标额头距离传感器窗口保持在5cm左右,垂直照射。软件上再叠加3.2节的采样滤波。还有一种很容易忽略的情况:如果传感器旁边有空调风口或者强红外辐射源,读数会被环境影响,安装时避开直吹。测温设备装好之后找几个人测一遍,把每个人和额温枪的差不固定在±0.3°C以内,才算是能交付的状态。
5.5 人脸识别误检率高与口罩状态乱跳
现象:OpenMV对着白墙偶尔画一个人脸框,口罩检测在戴与不戴之间来回跳。
原因:threshold设太低导致Haar把纹理误检成人脸;口罩模型训练数据太少或光照变化大;OpenMV的坐标和STM32显示坐标不一致导致框位置错位。解决:先把threshold往上调到0.55,提高误检成本;给OpenMV加一个ROI区域,只用画面中间一块做识别,排除背景干扰。口罩模型乱跳时可以print出obj.output()概率,看两个类别的概率是不是都在0.5附近,如果非常接近说明模型置信度低,需要补充训练数据。坐标错位则是OpenMV图像坐标系(左上角原点)和OLED显示或上位机之间的换算问题,统一先除以缩放系数再传给STM32。
6. 验证方法与参数调优:把测温精度和识别率做到可交付
系统能不能交付,全靠数据说话。测温验证方法:找一台经过计量校准的额温枪或黑体炉,和GY906DCI在同一环境下对同一目标各测20次,记录两组数据,计算平均误差和最大误差。我的习惯目标:平均误差在±0.3°C以内,最大误差不超过±0.5°C。如果偏差稳定在一个固定值,就把它直接写进TEMP_OFFSET,这是最快也最准的校准方式。
识别率验证分两部分。人脸检测率,让10个人站在离OpenMV约0.8米的位置,每人正对摄像头10秒,统计检测到人脸框的帧数占比,正常情况下应高于95%。口罩分类准确率,每个人戴与不戴各测20次,记录二分类结果,混淆矩阵里把"戴口罩识别成没戴"这一类看得比反面更重要,因为这在防疫场景里是漏报。漏报率高于10%就需要回炉补充训练数据。
参数调优上有几个具体技巧。OpenMV端scale_factor和threshold要一起调,先拿固定画面反复试,直到背景误检消失;STM32端的采样节奏可以用CubeMX的TIM3定时器中断来触发,每200ms置一个采样标志位,主循环检测到标志就读取一次温度,这样整个系统的采样节拍和串口发送间隔是稳定的,不会因为别的事情卡顿导致读数间隔漂移。调试阶段把温度和识别状态通过串口打印出来,用PC上位机记录一条时间线,对比实际行为,比盯着OLED看有效得多。
最后说一个我自己踩过的习惯:把GY906DCI和OpenMV用USB供电做测试时,一切正常;装到闸机上改用外部5V开关电源后,温度出现周期性跳变。查了半天是电源纹波顺便耦合到了I2C线上,给模块供电加了一颗100uF电解电容后彻底解决。后来所有测温和驱动步进电机的混合项目,我都会先在电源纹波上花时间检查,而不是急着调算法。这个细节也希望对你有帮助。
希望帮到你。
本文还有配套的精品资源,点击获取