1. 项目缘起与整体方案拆解
1.1 为什么选STM32做多模态门禁
做单片机毕设,选题方向基本决定了后面三个月是顺风顺水还是天天熬夜。智能门禁这个题目每年都有人做,但大部分人的方案停留在“RC522刷卡+继电器开锁”这个层面,答辩的时候老师问一句“你这个和十年前的门禁有什么区别”就答不上来了。我这次做的方案是把人脸识别、指纹、刷卡、密码四种解锁方式整合到一块STM32主控板上,用OpenMV负责图像采集和人脸比对,主控做逻辑调度和人机交互,这样既有传统嵌入式的外设驱动工作量,又有机器视觉的亮点,答辩的时候可讲的东西足够撑满十五分钟。
选STM32F103ZET6作为主控,理由很直接:引脚多、资料全、价格便宜、学校实验室大概率有现成的开发板。这个芯片是Cortex-M3内核,主频72MHz,512KB Flash、64KB SRAM,跑FreeRTOS绰绰有余。有人会问为什么不选F407或者H7系列,性能确实更强,但对于门禁这个场景来说,F103的处理能力完全够用——人脸识别的计算在OpenMV那边完成,主控只需要接收比对结果然后驱动外设,不存在算力瓶颈。而且F103的标准库和HAL库资料铺天盖地,遇到问题搜一下基本都能找到答案,这对毕设来说太重要了。
整个系统的架构可以这样理解:STM32是“大脑”,负责统筹所有模块的工作节奏;OpenMV是“眼睛”,专职做人脸检测和识别;指纹模块是“手指”,负责指纹录入和比对;RC522是“读卡器”,处理IC卡;矩阵键盘和OLED是“交互界面”,让用户能输入密码、看到操作提示;电磁锁和继电器是“执行机构”,验证通过就开锁。每个模块各司其职,主控通过UART、SPI、I2C等接口把它们串起来,形成一个完整的闭环。
1.2 四种解锁方式的分工与优先级设计
四种解锁方式不是简单堆在一起就行,得考虑它们各自适合什么场景、响应速度如何、安全等级怎么排。我实际调试下来的感受是:
- 人脸识别:适合双手拎着东西回家的场景,无接触,体验最好。但受光照影响大,晚上或者逆光的时候识别率会下降。OpenMV的识别速度大概在300-500ms,加上串口传输和主控处理,整体响应在1秒左右。
- 指纹识别:适合日常进出,手指一按就行,速度快,大概200ms出结果。缺点是手指潮湿或者脱皮的时候容易失败,这个后面会详细说怎么处理。
- 刷卡:适合老人和小孩,不需要记密码也不需要指纹录入。RC522读卡大概100ms,是四种方式里最快的。但卡片容易丢,而且可以被复制,安全等级最低。
- 密码:作为兜底方案,其他方式都失效的时候用。矩阵键盘输入,主控比对,速度取决于用户输入速度。安全等级中等,但要注意防偷窥和键盘磨损问题。
优先级设计上,我让系统默认循环检测指纹和刷卡(这两个响应快、功耗低),人脸识别通过按键触发或者红外传感器触发(因为OpenMV一直开着功耗不小),密码通过键盘任意键唤醒。这样既保证了日常使用的便捷性,又不会让系统一直满负荷运行。
1.3 硬件选型清单与成本控制
毕设的预算通常有限,我把成本控制在300元以内,具体清单如下:
| 模块 | 型号 | 数量 | 参考单价 | 备注 |
|---|---|---|---|---|
| 主控板 | STM32F103ZET6最小系统板 | 1 | 35元 | 实验室可能有现成的 |
| 摄像头模块 | OpenMV Cam H7 | 1 | 180元 | 毕设里最贵的部分 |
| 指纹模块 | AS608光学指纹 | 1 | 45元 | 串口通信,资料多 |
| 读卡模块 | RC522 | 1 | 8元 | SPI接口 |
| 显示模块 | OLED 0.96寸 I2C | 1 | 12元 | 128x64分辨率 |
| 键盘 | 4x4矩阵键盘 | 1 | 6元 | 或者用独立按键 |
| 锁具 | 12V电磁锁+继电器 | 1 | 25元 | 继电器用5V驱动的 |
| 电源 | 12V 2A适配器+AMS1117-3.3 | 1 | 15元 | 注意电流要够 |
| 其他 | 杜邦线、洞洞板、蜂鸣器 | - | 20元 | 蜂鸣器做提示音 |
这里有个省钱的点:OpenMV可以用OpenMV Cam M7或者更便宜的H7版本,如果预算实在紧张,甚至可以用STM32F4加上OV7725摄像头自己搭,但那样软件工作量会大很多,不建议毕设期间折腾。另外指纹模块AS608比FPM10A贵一点,但AS608的识别率更高,而且支持串口和USB两种模式,调试更方便。
2. 核心模块的底层原理与驱动要点
2.1 OpenMV人脸识别的工作机制
OpenMV做人脸识别,底层用的是Haar Cascade或者LBP特征,H7版本还支持TensorFlow Lite的轻量级神经网络。我实际用的是Haar Cascade,因为速度快、资源占用少,虽然准确率不如深度学习方案,但对于门禁这种1:N(一个人对多个已注册人脸)的场景,只要注册的人脸不超过10个,识别率完全够用。
OpenMV的代码逻辑是这样的:先加载人脸检测模型,然后循环拍照,检测到人脸后提取特征,和已经保存在SD卡里的特征文件比对,如果相似度超过阈值就通过串口发送“识别成功”的指令给STM32。这里的关键参数是阈值,设太高会拒真,设太低会认假。我试过0.7、0.75、0.8三个值,最后定在0.75,实测下来误识率最低。
串口通信的协议我定义得很简单:OpenMV发送一个字节,0x01表示识别成功,0x02表示识别失败,0x03表示检测到人脸但未注册。STM32收到后根据当前状态决定下一步动作。这种简单的协议虽然不够优雅,但胜在稳定,不会出现解析错误。
注意:OpenMV的帧率设置很关键。如果设成30fps,处理不过来会丢帧;设成10fps又太慢,人站在门口等半天。我最后用的是15fps,配合一个简单的状态机,检测到人脸后暂停拍照,等比对完成再继续,这样既流畅又不卡顿。
2.2 AS608指纹模块的串口通信细节
AS608是光学指纹模块,内部有DSP芯片做图像处理和特征提取,STM32只需要通过串口发送指令包就行。指令包格式是固定的:包头(0xEF01)+ 芯片地址(4字节,默认0xFFFFFFFF)+ 包标识(1字节)+ 包长度(2字节)+ 指令内容 + 校验和(2字节)。
常用的指令有这几个:
- 录入指纹:0x01,需要按两次手指,第一次采集,第二次确认
- 搜索指纹:0x04,在已录入的指纹库中搜索当前手指
- 删除指纹:0x0C,删除指定ID的指纹
- 清空指纹库:0x0D,慎用,一清就全没了
我踩过的一个坑是校验和的计算。AS608的校验和是从包标识开始到指令内容结束的所有字节累加,取低16位。一开始我算错了范围,导致模块一直返回0x01(指令执行失败),查了半天才发现是校验和的问题。后来写了一个校验和计算函数,每次发指令前先算一遍,就再也没出过问题。
指纹录入的时候有个细节:模块要求手指按下去后保持稳定,如果中途移动了,采集的图像会模糊,导致录入失败。我在代码里加了一个判断,如果连续三次采集失败,就通过OLED提示用户“请保持手指不动”,这样用户体验好很多。
2.3 RC522读卡模块的SPI驱动
RC522是NXP的13.56MHz读卡芯片,支持Mifare S50/S70卡。STM32通过SPI接口和它通信,SPI速率我设的是4.5MHz,再高的话杜邦线长了容易出错。RC522的驱动分两层:底层是SPI读写寄存器,上层是寻卡、防冲突、选卡、读写块的操作。
寻卡指令是0x52,返回卡的类型;防冲突指令是0x93,返回卡的UID(唯一标识符)。UID是4字节或者7字节,我用的S50卡是4字节。拿到UID后和STM32 Flash里存储的授权UID列表比对,匹配就开锁。
这里有个经验:RC522的天线匹配很重要。如果读卡距离只有1-2厘米,大概率是天线匹配电容不对。我用的模块是已经匹配好的,读卡距离大概3-5厘米,够用了。如果要更远,可以换大天线或者调整匹配电容,但毕设没必要折腾这个。
2.4 矩阵键盘与OLED的协同交互
4x4矩阵键盘有8根线,4行4列,STM32用8个GPIO控制。扫描方法是逐行拉低,读列线状态,如果有列线被拉低,说明对应按键被按下。这里要注意消抖,我用的方法是检测到按键后延时20ms再读一次,如果还是按下状态就确认。
OLED用的是SSD1306驱动,I2C接口,地址0x78。显示内容分几行:第一行显示当前状态(如“请刷卡”),第二行显示操作提示(如“按#确认”),第三行显示系统信息(如“指纹库:5”)。刷新率不用太高,200ms刷一次就够了,太高反而会闪烁。
键盘和OLED的配合逻辑是这样的:系统空闲时OLED显示“请验证身份”,用户按任意键唤醒键盘,OLED切换到密码输入界面,用户输入6位密码后按#确认,STM32比对密码,正确就开锁,错误就提示“密码错误”并返回空闲状态。这个流程要写得顺畅,不能让用户等太久。
3. 系统联调与核心代码实现
3.1 主控状态机的设计思路
整个系统用一个状态机来管理,状态定义如下:
typedef enum { STATE_IDLE, // 空闲,循环检测指纹和刷卡 STATE_FACE, // 人脸识别模式 STATE_PASSWORD, // 密码输入模式 STATE_UNLOCK, // 开锁状态 STATE_ERROR // 错误状态 } SystemState_t;主循环里用switch-case处理不同状态。IDLE状态下,每100ms轮询一次指纹模块和RC522,如果有人脸触发信号(红外或者按键),切换到FACE状态。FACE状态下,等待OpenMV的串口数据,收到0x01就进入UNLOCK,收到0x02就返回IDLE。PASSWORD状态下,扫描键盘,收到完整密码后比对,正确进入UNLOCK,错误进入ERROR。
UNLOCK状态下,拉高继电器引脚,电磁锁通电打开,同时蜂鸣器响一声,OLED显示“开锁成功”。延时3秒后自动回到IDLE。ERROR状态下,OLED显示错误信息,蜂鸣器响两声,延时2秒后回到IDLE。
这个状态机的好处是逻辑清晰,每个状态只做一件事,不会出现多个模块同时抢资源的情况。我试过不用状态机,直接在while(1)里堆if-else,结果代码乱得没法维护,调试的时候改一处崩三处。
3.2 串口通信协议的定义与解析
STM32和OpenMV、AS608都通过串口通信,但协议不同。OpenMV用的是单字节指令,简单直接;AS608用的是标准指令包,需要解析。
STM32有三个串口:USART1接OpenMV,USART2接AS608,USART3预留调试用。USART1和USART2都开接收中断,在中断服务函数里处理数据。
OpenMV的数据处理很简单:
void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); if(data == 0x01) { face_result = FACE_SUCCESS; } else if(data == 0x02) { face_result = FACE_FAIL; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }AS608的数据处理复杂一些,因为要接收不定长数据包。我的做法是定义一个缓冲区,收到包头0xEF01后开始存储,根据包长度字段确定后续要收多少字节,收完后置一个标志位,主循环里检测到标志位就解析数据包。
提示:串口中断里不要做太耗时的操作,比如解析数据包、比对指纹,这些放到主循环里做。中断里只负责收数据、置标志位,这样不会阻塞其他中断。
3.3 指纹录入与比对的完整流程
指纹录入是门禁系统里比较繁琐的一步,因为要引导用户正确操作。我的流程是这样的:
- 用户在OLED上选择“录入指纹”(通过键盘输入特定指令进入)
- STM32发送录入指令给AS608,OLED提示“请按手指”
- 用户按下手指,AS608采集第一次图像,返回确认码
- OLED提示“请再按一次”,用户再次按下同一手指
- AS608比对两次图像,如果一致就生成特征模板并存储,返回成功
- OLED提示“录入成功”,分配一个ID(从1开始递增)
比对流程更简单:用户按下手指,STM32发送搜索指令,AS608在指纹库里搜索,找到匹配的就返回对应的ID,STM32根据ID判断是否是授权用户。
这里有个细节:AS608的指纹库容量是1000枚,但实际用不了那么多,我设的上限是50枚,超过就提示“指纹库已满”。另外,录入的时候要确保手指干燥清洁,如果手指太湿或者有污渍,采集的图像质量差,录入会失败。
3.4 人脸注册与识别的OpenMV端实现
OpenMV端的代码用MicroPython写,主要分两部分:注册和识别。
注册模式下,OpenMV拍照,检测人脸,把人脸区域裁剪出来,提取LBP特征,保存到SD卡的features文件夹里,文件名用编号命名(如1.lbp、2.lbp)。注册的时候要拍多张(我用的5张),从不同角度拍,这样识别率更高。
识别模式下,OpenMV循环拍照,检测到人脸后提取特征,和SD卡里所有特征文件比对,计算相似度。如果最高相似度超过阈值(0.75),就认为识别成功,通过串口发送0x01;如果检测到人脸但相似度都不够,发送0x02;如果没有检测到人脸,不发数据。
import sensor, image, time, os from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QVGA) sensor.set_framerate(15) uart = UART(3, 115200) face_cascade = image.HaarCascade("frontalface") while True: img = sensor.snapshot() faces = img.find_features(face_cascade, threshold=0.5, scale_factor=1.2) if faces: for f in faces: img.draw_rectangle(f) # 提取特征并比对 # ...省略比对代码... if match: uart.writechar(0x01) else: uart.writechar(0x02) time.sleep_ms(100)这段代码是简化版,实际用的时候还要加上特征提取和比对的部分。OpenMV的LBP特征提取用的是img.find_lbp(),比对用的是image.match_descriptor(),具体用法可以查OpenMV的官方文档。
4. 调试过程中踩过的坑与解决方案
4.1 电源干扰导致指纹模块频繁复位
这个问题折磨了我整整两天。现象是指纹模块用着用着就无响应,重新上电又好了,但过一会儿又挂。一开始以为是串口通信的问题,查了波特率、校验和、接线,都没问题。后来用示波器看电源波形,发现12V转5V的降压模块输出纹波很大,峰值有200mV,指纹模块对电源波动敏感,电压一抖就复位。
解决方案是加滤波电容。在指纹模块的电源引脚旁边并了一个100uF的电解电容和一个0.1uF的陶瓷电容,纹波立刻降到50mV以下,模块再也没复位过。这个经验告诉我,多模块系统里电源质量比什么都重要,宁可多花几块钱买好的降压模块,也不要在这上面省。
4.2 OpenMV与STM32串口通信丢包
OpenMV和STM32的串口通信偶尔会丢包,表现是OpenMV发了0x01但STM32没收到,或者收到了但解析成别的值。排查后发现两个原因:一是波特率不匹配,OpenMV默认是115200,STM32我一开始设的9600,改成115200后好了一些;二是串口线太长,杜邦线超过20cm后信号质量下降,换成短的屏蔽线后基本不丢了。
还有一个隐藏问题是OpenMV的串口发送没有缓冲,如果STM32正在处理其他中断,可能会错过数据。我的解决办法是在OpenMV端连续发送三次同样的指令,STM32收到一次就够,这样即使丢了一两次也不影响。虽然有点浪费带宽,但稳定性提升明显。
4.3 指纹识别率低的优化过程
一开始指纹识别率只有70%左右,十次里有三次失败。我做了以下几件事来优化:
- 调整采集参数:AS608的采集超时时间默认是5秒,我改成10秒,给用户更多时间调整手指位置。
- 增加重试机制:如果第一次搜索失败,自动重试两次,三次都失败才返回错误。
- 引导用户正确按压:OLED上显示“请将手指平放,稍用力按压”,并配一个简单的示意图。
- 定期清理指纹库:删除质量差的指纹模板,重新录入。
优化后识别率提升到95%以上,基本做到一次通过。这里的关键是站在用户角度想问题,不能假设用户知道怎么正确按指纹。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 指纹模块无响应 | 电源纹波大 | 示波器看电源波形 | 加滤波电容 |
| OpenMV串口丢包 | 波特率不匹配 | 检查双方波特率 | 统一设为115200 |
| RC522读卡距离短 | 天线匹配不良 | 换卡测试 | 换模块或调匹配电容 |
| OLED显示花屏 | I2C速率过高 | 降低I2C速率 | 设为100kHz |
| 电磁锁不动作 | 继电器驱动电流不足 | 测继电器线圈电压 | 加三极管驱动 |
| 系统频繁死机 | 堆栈溢出 | 查看HardFault | 增大堆栈或优化代码 |
| 人脸识别率低 | 光照条件差 | 在不同光照下测试 | 加补光灯或调阈值 |
| 密码输入无响应 | 键盘扫描频率低 | 提高扫描频率 | 改为10ms扫描一次 |
5. 系统优化与功能扩展思路
5.1 低功耗模式的实现
门禁系统通常需要24小时运行,功耗是个绕不开的问题。STM32F103本身功耗不高,但OpenMV一直开着就很费电。我的优化方案是让OpenMV在空闲时进入睡眠模式,通过红外传感器唤醒。
具体做法是:在门旁边装一个HC-SR501红外传感器,检测到有人靠近时输出高电平,STM32收到后通过串口发送唤醒指令给OpenMV,OpenMV从睡眠中醒来开始拍照。没人时OpenMV继续睡,功耗从120mA降到20mA左右。
STM32本身也可以用Stop模式,在IDLE状态下关闭不必要的外设时钟,只保留串口和外部中断。这样整机功耗可以控制在50mA以内,用12V 2A的适配器绰绰有余。
5.2 数据存储与掉电保护
指纹模板和授权卡UID需要掉电保存,我一开始想用AT24C02 EEPROM,但容量太小(2KB),存不了几个指纹模板。后来改用W25Q64 Flash,8MB容量,存几百个指纹模板都没问题。
W25Q64通过SPI接口和STM32通信,驱动比EEPROM复杂一点,但容量大、速度快。我划了三个区域:0x000000-0x00FFFF存指纹模板,0x010000-0x01FFFF存卡UID,0x020000-0x02FFFF存系统配置(如密码、阈值等)。
掉电保护方面,我在电源输入端加了一个大电容(1000uF),检测到掉电后STM32有大概50ms的时间把关键数据写入Flash。这个时间足够写完一条记录了。
5.3 与手机端联动的可能性
虽然毕设不要求联网,但我在调试的时候试过加一个ESP8266 WiFi模块,把开锁记录上传到手机端。实现方式是STM32通过串口发送AT指令给ESP8266,ESP8266连接MQTT服务器,手机端订阅主题接收消息。
这个功能加上去之后,答辩的时候老师明显更感兴趣了,问了好几个关于物联网安全的问题。不过要注意,联网功能会增加系统复杂度,如果时间紧张,建议先把基础功能做扎实,再考虑扩展。
5.4 结构设计与外壳制作
硬件调试完之后,外壳是个容易被忽视但影响观感的部分。我用的是亚克力板激光切割,设计了一个简单的盒子,前面板开孔装OLED、键盘、指纹模块和RC522,侧面开孔装电磁锁和电源接口。
如果学校没有激光切割机,可以用3D打印,或者直接买现成的塑料外壳自己开孔。关键是布局要合理,各个模块的接线要短,避免信号干扰。我第一版外壳没设计好,线缆乱七八糟,后来重新设计了一版,把线都藏在盒子内部,看起来整洁多了。
6. 答辩准备与文档整理
6.1 答辩时老师最可能问的问题
根据我和同学的答辩经验,老师的问题集中在以下几个方面:
- 为什么选这个方案:要能说清楚STM32、OpenMV、AS608各自的优缺点,以及为什么它们组合在一起是最优解。
- 人脸识别的原理是什么:至少要能说出Haar Cascade和LBP的基本概念,不用太深入,但不能一问三不知。
- 系统的安全性如何保证:要能说出多模态验证的优势,以及每种方式的安全等级。
- 如果指纹识别失败怎么办:要能说出重试机制和备用方案(密码、刷卡)。
- 代码量有多大,自己写了多少:要能说清楚哪些是移植的,哪些是自己写的,老师最反感全盘抄袭。
6.2 毕设文档的撰写要点
毕设文档通常包括绪论、方案设计、硬件设计、软件设计、调试与测试、总结与展望几个部分。我的经验是:
- 绪论不要抄太多,重点讲清楚为什么做这个题目,解决了什么问题。
- 方案设计要画系统框图,说明各模块的分工和接口。
- 硬件设计要附原理图和PCB图,如果用的是模块,就画模块间的连接图。
- 软件设计要附流程图和关键代码,代码不要贴太多,挑核心的贴。
- 调试与测试要附实测数据,比如指纹识别率、人脸识别率、响应时间等。
- 总结与展望要客观,不要吹牛,说清楚哪些做完了,哪些没做完,后续可以怎么改进。
6.3 演示视频的拍摄技巧
答辩通常要求提交演示视频,我的建议是:
- 视频长度控制在3-5分钟,太长老师没耐心看。
- 开头用10秒介绍系统功能,然后分模块演示。
- 每个功能演示一遍成功案例,再演示一遍失败案例(如指纹识别失败后重试)。
- 最后展示开锁动作,这是最直观的部分。
- 背景音乐不要加,加个简单的字幕说明就行。
拍摄的时候注意光线,人脸识别在光线好的地方演示效果更好。如果条件允许,用手机支架固定拍摄,画面稳定一些。
7. 个人实操心得与建议
做这个项目最大的感受是,嵌入式开发里“能跑通”和“稳定跑”之间隔着巨大的鸿沟。调试的时候经常遇到这种情况:单独测试每个模块都正常,但整合到一起就出问题。这时候不要慌,按照“电源→通信→逻辑”的顺序排查,先确保电源干净,再检查通信协议,最后看逻辑有没有漏洞。
另外,代码的模块化很重要。我把每个模块的驱动都写成独立的.c和.h文件,主函数里只调用接口,不直接操作寄存器。这样调试的时候可以单独测试每个模块,出问题也容易定位。我见过有些同学把所有代码堆在main.c里,几千行代码找一个bug能找一天。
最后说一个细节:OLED的显示内容要简洁明了,不要堆太多信息。用户站在门口等开锁,没耐心看小字。我最后定稿的界面只有三行:状态、提示、结果,字体调大,一眼就能看清。这个细节虽然小,但答辩的时候老师注意到了,还夸了一句“交互设计得不错”。
这个项目后续还可以扩展的方向很多,比如加个摄像头做录像存证、加个蜂鸣器做报警、加个蓝牙模块做近场通信。但毕设时间有限,先把核心功能做扎实,扩展功能量力而行。我见过有人贪多嚼不烂,最后基础功能都没调通,答辩的时候很被动。