简介
Hi,大家好,这里是ikat易卡,最近花了几天的时间做了一个三合一的智慧门禁系统,可通过刷卡指纹扫脸解锁。其以STM32F103C8T6为主控,其中包含了MFRC522射频识别模块可通过刷卡解锁,AS608指纹模块可通过指纹解锁,K210用于人脸识别可通过刷脸解锁。成功解锁后通过控制舵机转动来模拟门禁打开。并做了断电存储的功能,可保证断电后录入的卡、指纹、人脸不丢失。
此为:三合一智慧门禁系统
大家可用于 课程设计
或 毕业设计
这一套下来,成本在三百元以内。
功能演示
B站演示视频连接: https://www.bilibili.com/video/BV1eM4m197vr/
关键模块简介
1.RC522模块简介
RC522是一种非接触式读写卡芯片,底层采用SPI
模拟时序,可以应用于校园一卡通、水卡充值消费、公交卡充值消费设计、门禁卡等。具体的工作原理我就不讲了,若有需要请查阅其他博主的文章。
2.AS608指纹识别模块简介
AS608模块是一种指纹识别模块,它可以用于身份验证、门禁系统、考勤系统等应用场景。采用串口UART
通讯,模块提供了很多接口,且自带存储可存储指纹库,便于用户调用开发。具体的工作原理我就不讲了,若有需要请查阅其他博主的文章。
3.k210人脸识别模块简介
K210是一款由中国芯片设计公司寒武纪(Kendryte)推出的低功耗、高性能的人工智能处理器。它采用了RISC-V架构,拥有双核心的处理器和硬件加速器,能够实现图像识别、语音识别、物体检测等多种人工智能应用。由此可见k210比stm32f103单片机强太多了,所以这里我用MaixPy(一个用python开发的开发环境,其提供了许多库能快速地开发智能应用)来开发k210用于人脸识别
,并采用串口UART
与stm32通讯。
我买的是以下的套件,其中包含了配套的屏幕,摄像头,到手插上就能用。还需有一张小于等于32G的SD卡与读卡器。
架构设计
硬件设计
以上是STM32F103的引脚分配图,如图所见一共使用了3个串口,5个GPIO分配给RC522,4个GPIO分配给按键,2个GPIO分配给0.96寸OLED屏幕,1个GPIO用于控制舵机。
以下是具体功能列表:
- 串口1用于与AS608指纹解锁模块通讯,详细接法看硬件连接原理图
- 串口2用于与K210通讯,K210的GPIO9连接stm32的PA3
- 串口3用作调试串口,打印信息至电脑上位机
- PA5,6,7,PB0,1连接RC522,用GPIO口模拟SPI时序与其通讯
- PB6,7连接0.96寸OLED屏幕,用GPIO口模拟I2C时序与其通讯
- PB12-15为按键输入口
- PB9为舵机PWM输出口
硬件连接原理图
连接方式:
由于各模块相对独立简单且没有大功率模块,所以我没有画PCB连接板而是采用洞洞板相互连接,洞洞板背面用飞线相连。然后stm32的5V得连接K210的5V引脚,3.3V就可以不用相连接了,记住5V引脚一定要相互连接,不然仅依靠3.3V是驱动不了K210的。
软件设计
STM32F103部分
我几乎没用cubemx生成初始化代码,cubemx我一般当做图形化引脚分配的工具使用。
关于系统设计框图,我有点懒,暂时先不画框图了,代码开源了你们自己看吧。后续可能会补上。
我讲讲stm32代码大致执行思路:
- 首先初始化各种模块。
- 然后根据全局变量判断OLED显示哪一页,这一页能执行什么功能。
- 通过按键改变全局变量来改变页面显示与操作。
- 在接受到k210通过串口发来的数据后在中断里改变对应的全局变量,并在之后的循环里执行相应的代码。
K210人脸识别部分
我用的是MaixHub的现成模型,这模型需自行下载,因为是加密模型需填写机器码且每个板子的机器码不一样,如果机器码错误则会无法运行。所以得获取板子的机器码,下载模型的网址有获取机器码的教程。这里我遇到了个坑,详情请看 踩坑记录
。
以下是MaixHub中人脸识别模型的网址:https://maixhub.com/model/zoo/60
以下是使用K210人脸识别的几个步骤。默认你已经会了基本的k210使用方法。如果不会请自行搜索。
- K210加密模型获取。
- 烧录支持
kmodelv4
的固件。 - 使用读卡器插入SD至电脑,用下载的模型替换掉我代码仓库里的K210SD中的三个模型。
- 然后保存至SD卡中,电脑弹出SD,将其插入K210里,上电运行即可。
- 以下是K210全部代码。
import sensor,image,lcd # import 相关库
import KPU as kpu #模型库
import time #定时库
import ubinascii #二进制库
import uos #文件库
from Maix import FPIOA,GPIO #GPIO库
from fpioa_manager import fm #标准库
from machine import UART #串口库
from machine import Timer #定时器库
from machine import WDT #看门狗库
fm.register(9,fm.fpioa.UART1_TX)#串口引脚映射
fm.register(10,fm.fpioa.UART1_RX)
fm.register(15, fm.fpioa.GPIO0)
#fm.register(9, fm.fpioa.GPIOHS9)
com = UART(UART.UART1, 115200, timeout=50, read_buf_len=4096)#构建串口对象
#GPIO9 = GPIO(GPIO.GPIOHS9, GPIO.OUT)
check = 0
save = 0
#看门狗回调函数
def on_wdt(self):
return
def on_timer(timer): #回调函数
global check
global save
data = []
#data = com.read(2)
if data!=None:
if data == b'A':
check = 1#代表存储人脸特征
elif data == b'B':
check = 1
save = 1 #存到SD卡中
elif data == b'C':
save = 2 #清除人脸
#定时器中断初始化
tim = Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_ONE_SHOT, period=500,
unit=Timer.UNIT_MS,callback=on_timer, arg=on_timer,start=False)
#从SD卡中加载模型
task_fd = kpu.load("/sd/FaceDetection.smodel") # 加载人脸检测模型
task_ld = kpu.load("/sd/FaceLandmarkDetection.smodel") # 加载人脸五点关键点检测模型
task_fe = kpu.load("/sd/FeatureExtraction.smodel") # 加载人脸196维特征值模型
clock = time.clock() # 初始化系统时钟,计算帧率
key_pin=16 # 设置按键引脚 FPIO16
fpioa = FPIOA()
fpioa.set_function(key_pin,FPIOA.GPIO7)
key_gpio=GPIO(GPIO.GPIO7,GPIO.IN)
last_key_state=1
key_pressed=0 # 初始化按键引脚 分配GPIO7 到 FPIO16
def check_key(): # 按键检测函数,用于在循环中检测按键是否按下,下降沿有效
global last_key_state
global key_pressed
val=key_gpio.value()
if last_key_state == 1 and val == 0:
key_pressed=1
else:
key_pressed=0
last_key_state = val
lcd.init() # 初始化lcd
lcd.rotation(0)
sensor.reset() #初始化sensor 摄像头
sensor.set_pixformat(sensor.RGB565) #设置摄像头像素
sensor.set_framesize(sensor.QVGA) #设置窗口为配套屏幕大小
sensor.set_hmirror(0) #设置摄像头镜像
sensor.set_vflip(2) #设置摄像头翻转
sensor.set_auto_gain(1,0) #摄像头自动增益
sensor.run(1) #使能摄像头
#使用官方库人脸检测算法
#anchor for face detect 用于人脸检测的Anchor
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
#standard face key point position 标准正脸的5关键点坐标 分别为 左眼 右眼 鼻子 左嘴角 右嘴角
dst_point = [(44,59),(84,59),(64,82),(47,105),(81,105)]
#初始化人脸检测模型
a = kpu.init_yolo2(task_fd, 0.5, 0.3, 5, anchor)
img_lcd=image.Image() # 设置显示buf
img_face=image.Image(size=(128,128)) #设置 128 * 128 人脸图片buf
a=img_face.pix_to_ai() # 将图片转为kpu接受的格式
record_ftr=[] #空列表 用于存储当前196维特征
record_ftrs=[] #空列表 用于存储按键记录下人脸特征, 可以将特征以txt等文件形式保存到sd卡后,读取到此列表,即可实现人脸断电存储。
names = ['Mr.1', 'Mr.2', 'Mr.3', 'Mr.4', 'Mr.5', 'Mr.6', 'Mr.7', 'Mr.8', 'Mr.9' , 'Mr.10'] # 人名标签,与上面列表特征值一一对应。
#写入特征点到SD卡(转换为二进制)
def save_feature(feat):
with open('/sd/data.txt','a') as f:
record =ubinascii.b2a_base64(feat)
f.write(record)
#清除人脸
def save_clear():
record_ftr = []
record_ftrs = []
with open("/sd/data.txt","w") as f:
f.write("")
f.close()
#打开文件进行读取,如果有特征点信息,将其导入存储数组中
with open('/sd/data.txt','rb') as f:
s = f.readlines()
for line in s:
#print(ubinascii.a2b_base64(line))
record_ftrs.append(bytearray(ubinascii.a2b_base64(line)))
# check = 0
# save = 0
while(1): # 主循环
#GPIO9.value(1)
check_key() #按键检测
tim.start()
img = sensor.snapshot() #从摄像头获取一张图片
clock.tick() #记录时刻,用于计算帧率
code = kpu.run_yolo2(task_fd, img) # 运行人脸检测模型,获取人脸坐标位置
# if save == 2:
# save = 0
# save_clear()
# #使用看门狗进行软件复位
# wdt0 = WDT(id=1, timeout=1000, callback=on_wdt, context={})
if code: # 如果检测到人脸
for i in code: # 迭代坐标框
# Cut face and resize to 128x128
a = img.draw_rectangle(i.rect()) # 在屏幕显示人脸方框
face_cut=img.cut(i.x(),i.y(),i.w(),i.h()) # 裁剪人脸部分图片到 face_cut
face_cut_128=face_cut.resize(128,128) # 将裁出的人脸图片 缩放到128 * 128像素
a=face_cut_128.pix_to_ai() # 将裁出图片转换为kpu接受的格式
#a = img.draw_image(face_cut_128, (0,0))
# Landmark for face 5 points
fmap = kpu.forward(task_ld, face_cut_128) # 运行人脸5点关键点检测模型
plist=fmap[:] # 获取关键点预测结果
le=(i.x()+int(plist[0]*i.w() - 10), i.y()+int(plist[1]*i.h())) # 计算左眼位置, 这里在w方向-10 用来补偿模型转换带来的精度损失
re=(i.x()+int(plist[2]*i.w()), i.y()+int(plist[3]*i.h())) # 计算右眼位置
nose=(i.x()+int(plist[4]*i.w()), i.y()+int(plist[5]*i.h())) #计算鼻子位置
lm=(i.x()+int(plist[6]*i.w()), i.y()+int(plist[7]*i.h())) #计算左嘴角位置
rm=(i.x()+int(plist[8]*i.w()), i.y()+int(plist[9]*i.h())) #右嘴角位置
a = img.draw_circle(le[0], le[1], 4)
a = img.draw_circle(re[0], re[1], 4)
a = img.draw_circle(nose[0], nose[1], 4)
a = img.draw_circle(lm[0], lm[1], 4)
a = img.draw_circle(rm[0], rm[1], 4) # 在相应位置处画小圆圈
# align face to standard position
src_point = [le, re, nose, lm, rm] # 图片中 5 坐标的位置
T=image.get_affine_transform(src_point, dst_point) # 根据获得的5点坐标与标准正脸坐标获取仿射变换矩阵
a=image.warp_affine_ai(img, img_face, T) #对原始图片人脸图片进行仿射变换,变换为正脸图像
a=img_face.ai_to_pix() # 将正脸图像转为kpu格式
#a = img.draw_image(img_face, (128,0))
del(face_cut_128) # 释放裁剪人脸部分图片
# calculate face feature vector
fmap = kpu.forward(task_fe, img_face) # 计算正脸图片的196维特征值
feature=kpu.face_encode(fmap[:]) #获取计算结果
reg_flag = False
scores = [] # 存储特征比对分数
for j in range(len(record_ftrs)): #迭代已存特征值
score = kpu.face_compare(record_ftrs[j], feature) #计算当前人脸特征值与已存特征值的分数
scores.append(score) #添加分数总表
max_score = 0
index = 0
for k in range(len(scores)): #迭代所有比对分数,找到最大分数和索引值
if max_score < scores[k]:
max_score = scores[k]
index = k
if max_score > 80: # 如果最大分数大于80, 可以被认定为同一个人
a = img.draw_string(i.x(),i.y(), ("%s :%2.1f" % (names[index], max_score)), color=(0,255,0),scale=2) # 显示人名 与 分数
#串口发送数据,用于控制舵机
com.write("1")
#拉低GPIO9
#GPIO9.value(0)
#屏幕显示文字识别成功
a = img.draw_string(10,50, ("successly identify"), color=(0,255,0),scale=3)
#刷新屏幕
a = lcd.display(img)
#延迟1s
time.sleep(1)
#拉高GPIO9
#GPIO9.value(1)
else:
a = img.draw_string(i.x(),i.y(), ("X :%2.1f" % (max_score)), color=(255,0,0),scale=2) #显示未知 与 分数
if key_pressed == 1:
key_pressed = 0
record_ftr = feature
record_ftrs.append(record_ftr) #将特征点添加到比较数组中
save_feature(record_ftr) #存到SD卡
break
a = lcd.display(img) #刷屏显示
#kpu.memtest()
#a = kpu.deinit(task_fe)
#a = kpu.deinit(task_ld)
#a = kpu.deinit(task_fd)
关键代码
1.STM32初始化代码
int main(void)
{
HAL_Init();
SystemClock_Config(); // 8M外部晶振,72M主频
MX_GPIO_Init(); // 初始化输入引脚
MX_TIM4_Init(); // 定时器4初始输出pwm控制舵机
MX_USART1_UART_Init(); // 串口1配置,PA9-> USART1_TX,PA10-> USART1_RX ,57600波特率,8位数据,1位停止位,无校验
MX_USART2_UART_Init(); // 与k210通讯串口,串口通讯.
MX_USART3_UART_Init(); // 调试串口
printf("Demo1");
OLED_Init(); // OLED初始化
OLED_ShowString(1, 1, " Welcome Home! ");
OLED_ShowString(2, 1, " loading... ");
RFID_Init(); // RFID初始化
Key_Init(); // 按键初始化
Servo_Init(); // 舵机初始化
while (GZ_HandShake(&AS608Addr)) // 初始化指纹模块
{
HAL_Delay(100); // 等待1秒
printf("finger init again\r\n");
}
Read_Card_Flash(); // 读取flash中的卡片ID
HAL_Delay(1000);
printf("所有模块已初始化成功\r\n");
OLED_ShowString(2, 1, " INIT SUCCESS! ");
HAL_Delay(1000);
//接下来进入while(1)主循环
2.cardID 4字节数据存入单片机flash,做到断电保存。
// 将cardID 4字节 存入flash,判断ID 0-3
void Add_Card_Flash()
{
uint16_t cardID[2];
cardID[0] = UID[0] << 8 | UID[1];
cardID[1] = UID[2] << 8 | UID[3];
if (ID_select == 0)
{
stmflash_write(0X08009000, cardID, 2);
}
else if (ID_select == 1)
{
stmflash_write(0X08009004, cardID, 2);
}
else if (ID_select == 2)
{
stmflash_write(0X08009008, cardID, 2);
}
else if (ID_select == 3)
{
stmflash_write(0X0800900C, cardID, 2);
}
}
3.指纹录入代码,这里有点赶时间代码没去优化,写得很烂,有7层嵌套了,可读性极差。(我好好反省
// 录入指纹
ensure = GZ_GetImage();
if (ensure == 0x00)
{
ensure = GZ_GenChar(CharBuffer1); // 生成特征
if (ensure == 0x00)
{
ensure = GZ_GetImage();
if (ensure == 0x00)
{
ensure = GZ_GenChar(CharBuffer2); // 生成特征
if (ensure == 0x00)
{
ensure = GZ_Match();
if (ensure == 0x00)
{
ensure = GZ_RegModel();
if (ensure == 0x00)
{
ensure = GZ_StoreChar(CharBuffer2, ID_select); // 储存模板
if (ensure == 0x00)
{
printf("录入成功");
GZ_ValidTempleteNum(&ValidN); // 读库指纹个数
printf("指纹库中有%d个指纹", ValidN);
OLED_Clear();
OLED_ShowString(2, 1, " Add finger");
OLED_ShowString(3, 1, " success");
HAL_Delay(3000); //
menu_page = 1;
ID_select = 0;
break;
}
}
}
}
}
}
}
4.识别指纹。
5读取RFID 卡ID。
还有很多业务代码我就不展示了,要完整的详细的还是自行查看下载源码查看吧。代码仓库连接在文章末尾。
踩坑记录
1.移植代码时一定要注意引脚定义,我当时移植RC522时有两个引脚写反了死活通讯不了。当然接线也是,请务必多次检查。
2.串口2中断回调函数这里,我遇到了中断一次卡死不退出但第二次中断后能退出到主循环里的问题,我试了很多种解决方式,有效的方法居然是:在中断回调函数里多添加了 HAL_UART_Transmit(&huart2, (uint8_t *)"OK", 2, 0xffff);
这串代码就好了,有够逆天的。难道是中断回调执行地太快导致的?还是串口2的发送与接受寄存器存在某种联系?
//串口中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
}
else if (huart->Instance == USART2)
{
//多了以下代码就不会出现中断一次卡死的问题,
HAL_UART_Transmit(&huart2, (uint8_t *)"OK", 2, 0xffff);
// 清楚接收中断标志位
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
// 打印接收到的数据是否为'1',若是则解锁成功
if (g_rx2_buffer[0] == 0x31)
{
face_flag = 1;
// HAL_UART_Transmit(&huart2, (uint8_t *)"unlock success!\r\n", 17, 0xffff);
}
while (HAL_UART_Receive_IT(&huart2, (uint8_t *)g_rx2_buffer, RXBUFFERSIZE_UART2) == HAL_OK) // 重新打开中断
;
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
}
}
3.关于K210机器码的获取问题,由于我买的K210的型号是Sipeed Maix Bit ( with Mic )
不像其他的k210一样插上USB能出现两个串口,我手上的k210开发板只会出现一个串口,这个串口是用来下载与连接MaixPy的。烧录key_gen_v1.2.bin
固件复位后不会从USB的串口中打印机器码,而是从GPIO9
你没听错是从GPIO9打印出k210的机器码,所以说我还得连接杜邦线到TTL,很坑爹。而且网上的教程全是通过USB的串口打印的,根本没有提到还得用杜邦线单独引出,我当时是用杜邦线插上k210GPIO口的排针一个一个试出来的(悲)。