1. 项目需求与硬件选型拆解
1.1 这个项目到底要解决什么问题
这类门禁考勤Demo的技术链路其实很典型:摄像头采集画面,本地完成人脸检测和识别,把“谁来了”这个结论通过串口发给下位机,下位机根据结果决定开不开锁。整个链路里,最容易出问题的往往不是识别算法本身,而是“识别完怎么把数据可靠地递给MCU”。我见过太多项目死在通信这一层,不是发出去乱码,就是下位机一收就丢帧。
选型阶段我认真纠结过。树莓派性能确实强,但成本高、功耗大、启动要好几秒,用于门禁多少有点杀鸡用牛刀;纯MCU方案根本带不动神经网络,哪怕跑个最轻量的人脸模型也够呛;云端识别虽然算法丰富,但依赖网络,画面还要传出去,在门禁场景里既不及时也有隐私层面的顾虑。最后定了MaixCam,主要看中它这几点:K210芯片自带KPU硬件加速器,跑轻量级人脸识别模型完全不需要外部算力;板上直接带了摄像头模组,省去选型匹配的麻烦;原生引出UART串口,跟MCU通信零障碍;开发用MicroPython语法,改代码烧固件调试都很高效。
1.2 MaixCam方案的优势边界在哪里
顺便把MaixCam的边界说清楚,避免有人上手就期望过高。它能干的:轻量人脸检测、关键点提取、特征比对,实际跑下来帧率在每秒20到30帧之间,完全够门禁这种低速场景;除此之外还能做物体分类、目标检测、颜色识别这些。它干不了的:跑大型深度学习模型,处理高分辨率图像流,或者同时挂很多外设跑复杂逻辑。所以我的定位是把它当成“边缘小脑”——图像理解的活它包了,控制决策的活交给下位机。这种分工在项目落地时特别重要,别指望一块K210把所有事全干了,贪多必然两头都不讨好。
2. 人脸识别从模型到识别的完整落地
2.1 固件烧录与开发环境准备
拿到MaixCam第一件事就是刷固件。不同固件版本的人脸识别API有过调整,我建议选一个发布比较稳定的版本,后续所有代码都基于这个版本调,不要频繁升级。刷完固件,连上IDE,能看到摄像头实时画面,说明环境和驱动都正常。这一步卡住的人不多,但注意烧录时别选错芯片型号,选错之后板子开不了机只能重新烧,不算大问题,就是浪费时间。
还有一个容易被忽略的点:供电。K210满载工作时的电流不小,有些USB口供电不稳,会导致固件偶尔异常重启或者识别时死机。做这个项目时我直接用一个单独5V/2A的电源给MaixCam供电,识别稳定性提升很明显。环境问题提前处理干净,后面调试会省一大堆事。
2.2 人脸数据采集与特征库构建
人脸识别的核心逻辑不是“训练”,而是“提特征+比对”。K210资源有限,没法像PC那样现场训练神经网络,所谓的人脸注册,本质就是保存这个人脸的特征向量,识别的时候重新提取特征,然后和库里所有特征做距离比对。
我的注册流程是这样的:先拍一张人脸照片,检测模型框出人脸区域,然后用特征提取模型把这张人脸转成128维的特征向量,最后把特征向量和名字、ID一起存进SD卡上的文件里。这里有个关键细节——注册时采集的图像质量直接决定后续识别体验。拍照时光照要均匀,人脸在画面中占比要大,角度尽量正对镜头,别戴夸张配饰。我一开始图省事,随手拍了几张模糊的注册照片,后来识别阶段怎么调阈值都不理想,重新采集之后效果立刻变好。
特征库文件建议用结构化格式保存,每条记录包含版本号、ID、姓名、特征长度、特征数据,同时给整个文件加一个CRC校验,防止掉电写入损坏后加载了一堆垃圾数据而不自知。
2.3 识别主流程的代码实现
识别循环就做四件事:取帧、检测人脸、提取特征、遍历特征库比对。核心代码思路如下:
import sensor, image, lcd from maix import KPU # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(True) sensor.run(1) lcd.init() # 加载检测模型和特征提取模型 detector = KPU() detector.load_kmodel("/sd/face_detect.kmodel") extractor = KPU() extractor.load_kmodel("/sd/face_recognize.kmodel") # 从SD卡加载已注册的特征库 features = load_features_from_sd("/sd/features.bin") names = ["张三", "李四"] THRESHOLD = 0.75 # 距离阈值,需实测标定 while True: img = sensor.snapshot() faces = detector.face_detect(img) if faces: face = faces[0] feature = extractor.face_extract(img, face) best_id = -1 best_dist = 9999 for i, ref in enumerate(features): dist = compute_distance(feature, ref) if dist < best_dist: best_dist = dist best_id = i if best_id >= 0 and best_dist < THRESHOLD: print("识别成功:", names[best_id], "距离:", best_dist) else: print("未识别人员") lcd.display(img)这里必须说明一下,MaixPy的API在不同固件版本里细节有差异,比如检测函数的返回格式、特征提取函数的参数,我上面这段是按通用思路写的伪代码,核心是“检测框拿到后提特征,再和特征库比对”这条主线。实际编码时,打开你固件版本对应的官方例程,把检测和特征提取的例子各跑一遍,再拼起来,比我直接给你一段代码靠谱得多。
2.4 识别参数调优的关键动作
识别效果不理想,先别急着怀疑模型,按这个顺序排查:第一,光线;第二,人脸在画面中的尺寸;第三,角度;第四,特征库质量。光线方面,太强容易过曝,太暗提不出足够特征,侧光会导致人脸一半亮一半暗,这些都会让特征向量飘。人脸尺寸方面,QVA分辨率下,人脸宽度建议占画面四分之一以上,太远的人脸检测到了也提不出可靠特征。角度方面,正对镜头识别率最高,偏转超过30度之后特征偏差会明显加大。
阈值标定是我每次做人脸项目都花时间做的一件事。拿个真实场景拍上一段视频,统计“本人每次识别的距离值”和“别人误触发的距离值”,找一个能明显区分两者的临界点,留出余量。阈值太大容易误放行,阈值太小则漏识别,实际项目里宁可偶尔多问一次,也不能把不认识的人放进去。
3. 串口数据协议设计与实现
3.1 为什么在这个场景里选串口
识别结果要传给下位机,可选项有WiFi、蓝牙、串口。门禁这个场景有一个特点:下位机大概率是单片机,串口是标配外设,驱动成熟、稳定可靠、延迟极低,调试起来也方便——USB转TTL一插,串口助手直接看数据。WiFi和蓝牙虽然无线体验好,但增加了配对流程、断线重连、协议栈复杂度,对“门锁开不开”这种简单的控制逻辑来说,性价比太低。
在开始设计协议前,我先确认了一件事:信道的可靠性边界。串口是字节流,本身不提供“帧”的概念,所以协议层最大的任务就是让接收方知道“一段数据从哪里开始、到哪里结束、内容对不对”。这个前提明确了,后面设计就有方向了。
3.2 帧格式设计思路与字段说明
这个项目我设计的帧格式如下,它兼顾了解析简便性和扩展性:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2字节 | 0xAA 0x55,标志数据开始 |
| 类型 | 1字节 | 0x01识别成功 / 0x02识别失败 / 0x03注册成功 / 0x04心跳 |
| 长度 | 1字节 | 数据区长度N,范围0-255 |
| 数据区 | N字节 | 业务数据,由类型字段决定含义 |
| CRC16 | 2字节 | 从帧头到数据区末尾的CRC16校验值 |
| 帧尾 | 2字节 | 0x0D 0x0A,方便工具观察边界 |
有人会问,有了长度字段还需要帧尾吗?我的经验是保留。帧尾的目的是给调试者一个明确的“本帧结束”标记,在人眼观察串口助手输出时帮助很大。长度字段存在的另一个价值是,即使数据区里恰好出现0xAA 0x55,接收方也不会误判新帧开始——因为有长度约束,先收够数据再判断下一个帧头。
第一版协议其实没有长度字段,靠帧尾判断结束,结果数据区里出现和帧尾相同的字节时,解析就乱了。后来加上长度字段,问题彻底解决。这就是协议迭代的真实过程,往往是踩了坑才想到更稳的方案。
3.3 识别结果的数据区定义
识别成功时数据区我定义为定长4字节,字段如下:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0-1 | 用户ID | 小端序2字节,最大支持65535人 |
| 2 | 置信度 | 0-255整数,0.9映射为230 |
| 3 | 保留 | 置0,留作扩展 |
有人可能会问,为什么不用JSON或者文本格式?在MaixCam这种嵌入式环境下,把payload拼成JSON字符串再发,接收端单片机要引入JSON解析库,内存和Flash都被吃掉一大块,解析还有失败的可能。二进制定长格式,MCU端直接把接收缓冲区对应地址的字节memcpy到结构体变量里,一次到位。传输协议和业务格式分开考虑,是我在这个项目里比较深的一个体会:传输层保证帧完整,业务层保证数据含义,两者别混在一起。
3.4 MaixCam发送端代码实现
发送端代码先做引脚映射,再初始化串口,最后封装发送函数。参考实现如下:
from machine import UART from fpioa_manager import fm import board_info # 引脚映射,具体引脚号以你板子上的丝印为准 fm.register(board_info.PIN10, fm.fpioa.UART1_TX, force=True) fm.register(board_info.PIN11, fm.fpioa.UART1_RX, force=True) uart = UART(UART.UART1, 115200, bits=8, parity=None, stop=1) def crc16(data): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc.to_bytes(2, 'little') def send_frame(cmd, payload): head = bytes([0xAA, 0x55]) middle = bytes([cmd, len(payload)]) body = head + middle + payload frame = body + crc16(body) + bytes([0x0D, 0x0A]) uart.write(frame)实际使用中,识别成功就立刻组帧发送,识别失败也发一帧,这样下位机能随时知道MaixCam还在正常工作。发送频率不要过高,连续发帧之间留几毫秒间隔,给下位机串口接收缓冲留出处理时间。这个细节我在联调时深有体会,一开始K210上识别循环跑得飞快,结果下位机老是丢帧,加了一点延时就好了。
3.5 MCU端状态机解析流程
下位机收到的是一串连续字节流,必须用状态机做逐字节解析。推荐按这个流程实现:空闲状态等待0xAA,收到后进入帧头1状态;下一个字节是0x55则进入帧头2状态,否则回到空闲;然后依次收类型、长度,再循环收数据区;数据区收完后收CRC,校验通过后确认帧尾,一帧完整数据就算到手了。任何一个环节出错都回到空闲状态重新同步。
状态机的核心价值在于“重新对齐”能力:串口通信偶尔丢一个字节是很正常的事,如果丢的是长度字段,整个帧的边界就乱了。有了帧头匹配机制,下一帧过来时又能重新咬合。实测中,我在115200波特率下连续发送一万帧,只要供电稳定,丢包率基本可以忽略。
3.6 应答与重传要不要做
这个项目的数据方向是单向的:MaixCam只发,MCU只收,所以不需要应答机制。但如果你做的是更严肃的系统,识别结果不能丢,那就要加上确认和重传:MCU收到合法数据后回一个ACK帧,MaixCam超时没收到ACK就重发同一帧。这里有一个坑必须提前说:加重传之后,下位机可能重复收到同一帧,如果重复控制开锁,门就会连续触发两次,所以业务上要做幂等处理,比如同一个ID几秒内只执行一次动作。我在门禁Demo里故意没加重传,原因是门禁场景宁可丢一帧,也不能让门锁莫名其妙来回触发两下。
4. 常见问题与排查记录
4.1 问题速查表
把这次调试中遇到的和预想到的常见问题整理成一张表,方便大家对照处理:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 串口收到乱码 | 波特率或数据位不一致 | 统一为115200/8N1 |
| 能检测到人脸但经常识别失败 | 特征库采集质量差 | 重新注册,规范光照角度 |
| 识别结果不稳定时好时坏 | 阈值设置不当或光线变化 | 重新标定阈值,保证光照稳定 |
| 下位机偶尔收不到完整帧 | 发送太快或状态机有漏洞 | 帧间隔加5-10ms,检查丢字节后的重新同步逻辑 |
| 上电后特征库读取异常 | 文件写入不完整 | 保存文件时加CRC,加载时校验 |
| 画面卡顿帧率低 | 分辨率设太高 | 识别场景QVGA足够,不必用VGA |
| K210频繁重启 | 供电不足 | 用独立5V/2A电源,避免USB口供电 |
4.2 容易被忽略的三个连线细节
第一个是共地。MaixCam和MCU之间除了TX接RX、RX接TX之外,两地一定要连在一起,否则两边的电平参考点不一致,数据就会乱飞。第二个是发送引脚不要接错。有人会把两个TX接一起,两个发送端打架,数据完全发不出去,这个错误用串口助手一测就能发现。第三个是电平匹配。K210的UART是3.3V TTL电平,如果对接的是5V单片机系统,要注意电平容忍度,最好是两边都确认过再连线;如果对接的是RS232之类的总线设备,还需要额外的电平转换模块,不要拿TTL信号直接往RS232口上捅。
4.3 联调顺序与排查思路参考
联调时我的习惯是分层排查,绝不一上来就板对板接好直接跑。先把MaixCam单独接USB转TTL到电脑串口助手,确认发送的每一帧数据格式正确、CRC正确;再把MCU端的解析程序用电脑串口助手模拟发数据,确认下位机解析正确;最后才让两端真正对接。这样一旦出问题,就能迅速判断是MaixCam侧故障还是MCU侧故障,而不是两头一起查。逻辑分析仪在这里也很有用,串口波形的电平、波特率误差都能直接看到,比猜快多了。
5. 个人实操体会与扩展建议
这个项目做完,我最大的体会是:方案越简单越可靠。能用串口解决的不用WiFi,能本地识别的不上云,能定长二进制传完的不用JSON。不是新技术不好,而是嵌入式场景里,链路上每多一个环节就多一个故障点,越简单的方案越容易稳定跑上几年。
另一个体会是人脸识别项目的体验上限,其实在注册数据采集阶段就决定了。算法和框架只是在用数据做比对,特征库质量差,后面做多少调优都是浪费。所以我在每个项目里都会反复强调:注册阶段宁可多花时间,也不要偷懒拍几张模糊照片了事。
最后分享一个小技巧:在MaixCam的屏幕上实时显示识别结果和距离值,联调时会直观很多。你看到的不是一串枯燥的调试打印,而是每一次判断的依据,问题在哪一层很快就清楚了。这个项目做完之后,我又顺手接了一个语音播报模块,识别成功时喊一声名字,现场效果立刻不一样。基础链路跑通之后,扩展起来其实非常快,关键是把第一步走扎实。