基于STM32的多模态智能门禁系统:RFID、蓝牙与人脸识别的开锁仲裁实现
2026/9/13 1:16:01 网站建设 项目流程

简介:这是一份基于STM32的智能门禁系统完整项目,面向嵌入式方向的高校学生与开发者,适合作为毕业设计或期末大作业参考。系统涵盖人脸识别、RFID刷卡、蓝牙App控制、密码锁四类门禁方式,源码已本地编译验证可运行,评审分达95分,难度适中,内容经助教老师审定,能帮助理解STM32外设驱动、通信协议与多模块整合思路。资源包共254个文件,以C源码、头文件、Keil工程文件、编译中间文件及HEX烧录文件为主,源码覆盖STM32F10x标准外设库的定时器、Flash、ADC、I2C、CAN等驱动模块,HEX文件可直接烧录,配合详细文档便于理解门禁系统设计与实现。压缩包仅8.67MB,目录清晰,便于导入调试。目前已有229人学习下载,需完整项目方案、源码解析、实验参考的读者可放心使用。

1. 多模态门禁为什么绕不开STM32

做门禁类项目最容易被问倒的一句话是“如果别人偷拍了你的密码、复读了你的门禁卡,怎么办?”单模式方案在答辩或者实际部署中都很被动:密码锁怕窥视,RFID卡怕复制,蓝牙App在楼层隔断后不稳定,人脸识别单独用成本又偏高。把这几种方式压到同一台设备上,让它们互为补充,才是这类智能门禁系统真正的价值点,而STM32在其中负责最关键的协调工作。基于STM32的智能门禁系统源码里,主控芯片需要同时处理RC522读卡、矩阵键盘密码、蓝牙透传指令以及人脸识别串口结果,并将这些不同来源的事件统一映射成“开锁”或“拒绝”两个动作。对于正在做毕业设计或嵌入式期末大作业的人来说,这套思路比单纯点灯跑马灯更能体现工程能力。

2. STM32外设分配与四路开锁信号接线规划

2.1 模块选型和接口选择

拿到这类项目以后,先别急着打开Keil刷代码,而是要把板级接口想清楚。门禁系统里有四种输入方式,它们在STM32上占用的资源完全不同。RC522 RFID模块走SPI,13.56MHz读卡对时序要求高,但STM32的SPI1时钟可以跑到18MHz,完全够用。蓝牙模块用HC-05这种串口透传模块,接一组USART即可。人脸识别模块我习惯选择带UART接口的成品模块,比如OpenMV或者TX510这种,识别算法在模块内部完成,STM32只需要解析串口返回的结果。密码锁则通过矩阵键盘读到按键值,走GPIO加扫描算法。

这里有一个关键选择:不要把RFID和蓝牙挂在同一个SPI或同一组UART上。RC522在连续读卡时会产生频繁的中断,如果和蓝牙共用中断优先级,会导致蓝牙丢帧或指令延迟。分开接不同的外设总线,既隔离故障,又方便单独调试。电源方面,我一般使用AMS1117-3.3给RC522和蓝牙模块供电,继电器和电磁锁用外部5V电源单独驱动,地线必须和STM32共地,否则串口通信会出现乱码甚至损坏引脚。

2.2 引脚分配表

以下是一份适用于STM32F103C8T6的引脚分配方案,可以作为最小开发板的外接参考:

模块接口类型STM32引脚复用功能电平
RC522 RFIDSPI1PA5(SCK),PA6(MISO),PA7(MOSI),PA4(CS)软件控制CS,CPOL=0,CPHA=03.3V
蓝牙模块USART2PA2(TX),PA3(RX)波特率9600,8N13.3V
人脸识别模块USART3PB10(TX),PB11(RX)波特率115200,8N13.3V
矩阵键盘GPIO输入PC0~PC3上拉输入,低电平有效3.3V
电磁锁继电器GPIO输出PC13推挽输出,高电平开锁外部5V

注意RC522的IRQ引脚不需要接,轮询寻卡比中断寻卡更容易排查问题。矩阵键盘不要用模拟输入模式,否则多键按下时无法正确判断扫描结果。另外,PC13在STM32F103上默认是RTC相关引脚,用它作为继电器控制时,要确保没有复用RTC功能。

2.3 串口和GPIO初始化代码

我用标准外设库初始化两组串口和一个输入、一个输出。这里给出最重要的一段初始化代码:

void UART_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART3, ENABLE); /* USART2 TX PA2 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* USART2 RX PA3 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); /* USART3 TX PB10 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); /* USART3 RX PB11 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_InitStructure.USART_BaudRate = 115200; USART_Init(USART3, &USART_InitStructure); USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); USART_Cmd(USART3, ENABLE); }

这段代码把USART2的波特率定为9600,用于蓝牙模块;USART3定为115200,用于人脸识别模块。因为蓝牙模块出厂配置一般是9600,而人脸模块为了快速传输特征数据和识别结果,普遍使用115200。初始化后,必须在中断服务函数里读取USARTx->DR,否则RXNE置1后不会再接收后续数据。我见过不少人在这里只开了时钟没开中断,导致串口完全收到数据但不动作。

3. RC522读卡与密码锁的权限校验实现

3.1 RC522读卡流程分四步

RC522读取一张M1卡,需要依次执行寻卡、防冲突、选卡、读序列号四个动作。寻卡是向天线场发送请求,拿到卡片类型;防冲突用于处理多张卡同时出现的场景;选卡成功后,就可以从缓冲区拷贝出卡号UID。代码如下:

uint8_t RFID_ReadCardID(uint8_t *card_id) { uint8_t status; unsigned char buffer[16] = {0}; status = RC522_Request(PICC_REQIDL, buffer); if (status != MI_OK) return 1; status = RC522_Anticoll(buffer); if (status != MI_OK) return 2; memcpy(card_id, buffer, 4); RC522_Halt(); return 0; }

在实际测试中,RC522_Request返回错误多数是因为天线线圈没有调好,或者卡片不在感应区。很多人拿到代码后直接把返回值改成一个整体判断,不出错还好,一旦出错根本不知道是寻卡失败还是防冲突失败。所以我把每一段都返回不同错误码,比如1是寻卡失败,2是防冲突失败,这样配合串口打印可以快速缩小问题范围。另外,RC522_Halt这步不能省,否则卡片会一直处于激活状态,下一次读取时容易出错。

3.2 权限表设计:把密码和RFID放进同一个结构体

门禁系统到了一定复杂度,不能再用几个全局变量分别存密码和卡号,那样权限很容易错乱。我自己的做法是定义一张统一的权限表,把RFID的UID、密码哈希、有效期、权限等级放在同一个结构体中:

typedef struct { uint8_t uid[4]; // 4字节UID uint16_t uid_crc; // UID校验值 uint32_t password_hash; // 密码哈希值,不存明文 uint32_t expire_time; // 过期时间戳,0表示永久 uint8_t auth_level; // 1普通用户,2管理员 uint8_t valid; // 记录是否有效 } AccessItem; #define MAX_ACCESS_ITEMS 32 AccessItem access_table[MAX_ACCESS_ITEMS];

这张表的意义在于:刷卡时查uid字段,输入密码时查password_hash字段,两种方式共用同一个权限查询函数。管理员在App端新增一张卡,只需要往这张表里插入一条记录,同时把uid_crc算好;删除用户时把valid置0即可。评审老师问起数据持久化,也可以直接指着这个结构体讲内部Flash存储方案。

3.3 密码不能明文存储,RFID注意防复制

我见过很多项目在代码里直接写#define PASSWORD 12345,虽然演示起来没问题,但只要有人用串口工具翻一下程序Flash就能看到密码,安全上完全站不住脚。推荐的做法是:把原始密码和一个固定的盐字节异或,再做一次CRC16,得到password_hash。校验密码时重新计算再比对。至于“rfid怎么复制”这个常见问题,普通M1卡的UID确实可以被某些读写设备直接改写,所以门禁项目里至少要加一条uid_crc校验,防止卡号被篡改后绕过权限表。如果做产品级门禁,建议直接换用CPU卡或SOC卡,那种卡内部有密码校验,复制难度会高得多。

4. 蓝牙App串口指令帧与状态机解析

4.1 为什么不用裸字符串

蓝牙模块给人的感觉很简单:App发一个字节,STM32收一个字节。但实际门禁系统里,App除了开锁,还要下发“添加卡号”“删除用户”“查询记录”这类带参数的指令。如果用字符串解析,像“card:1234”和“card:5678”的区别就要靠字符串函数处理,效率低且容易漏数据。我习惯定义一帧定长或者带长度字节的协议:

帧头长度命令字数据段校验和
0xAA 0x551字节1字节可变长度1字节

例如开锁指令就是AA 55 03 01 00 FA,其中长度03表示后面“01 00 FA”三个字节,01是开锁命令,00是未知额外参数,FA是校验和。添加卡号指令AA 55 06 02 12 34 56 78 C7,数据段就是4字节UID。这种帧结构拿到任何嵌入式平台都能通用,换到ESP32上也只需要改串口初始化。

4.2 串口状态机解析代码

解析程序放在USART2的中断服务函数中,逐字节进入状态机:

#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 enum { STATE_WAIT_HEAD1, STATE_WAIT_HEAD2, STATE_WAIT_LEN, STATE_WAIT_DATA }; uint8_t rx_buffer[32]; uint8_t rx_len = 0; uint8_t rx_idx = 0; uint8_t rx_state = STATE_WAIT_HEAD1; void Parse_BluetoothByte(uint8_t byte) { switch (rx_state) { case STATE_WAIT_HEAD1: if (byte == FRAME_HEAD1) rx_state = STATE_WAIT_HEAD2; else rx_state = STATE_WAIT_HEAD1; break; case STATE_WAIT_HEAD2: if (byte == FRAME_HEAD2) rx_state = STATE_WAIT_LEN; else rx_state = STATE_WAIT_HEAD1; break; case STATE_WAIT_LEN: rx_len = byte; if (rx_len > 32) { rx_state = STATE_WAIT_HEAD1; break; } rx_idx = 0; rx_state = STATE_WAIT_DATA; break; case STATE_WAIT_DATA: rx_buffer[rx_idx++] = byte; if (rx_idx >= rx_len) { rx_buffer[rx_idx] = 0; Process_BluetoothFrame(rx_buffer, rx_len); rx_state = STATE_WAIT_HEAD1; } break; default: rx_state = STATE_WAIT_HEAD1; break; } }

这个状态机的核心是用状态代替流程,防止串口中断被长时间占住。STATE_WAIT_LEN里的if (rx_len > 32)是一个边界保护,避免恶意数据把rx_buffer写穿。Process_BluetoothFrame里需要先验证校验和,再根据命令字执行开锁、加卡、删卡等动作。注意蓝牙模块在开机时会发一串AT回显或者厂商信息,这些数据没有帧头,状态机直接丢弃,不会影响后续指令。

4.3 App端实现要点

App端的逻辑其实很直接,就是通过蓝牙Socket发送上面的帧。如果自己写Android程序,推荐参考官方BluetoothChat里连接、读写的部分;如果想快速验证,可以用“Serial Bluetooth Terminal”这类通用串口工具,按帧格式发十六进制数据。这种蓝牙App控制嵌入式设备的方式,和网上常见的“蓝牙app控制esp32”是一个套路,本质都是串口透传,区别只在于另一方跑的是ESP32还是STM32。需要提醒的是,不要把开锁命令直接写死在App代码里,否则反编译后就能看到执行逻辑。让App只发送“请求码”,由STM32判断是否需要二次授权,这样权限还是捏在设备端手里。

5. 人脸识别模块接入与多模式开锁仲裁

5.1 串口型人脸模块的接入方式

在STM32上直接跑人脸识别算法不是不行,而是没有必要。更稳妥的方案是外接一个串口型人脸识别模块,让模块内部完成人脸检测、特征提取和比对,只通过UART输出识别结果。比如OpenMV或者TX510,它们的典型输出是:识别成功后发一帧AA 55 02 01 0C,其中01表示识别成功,0C是校验。STM32端接收并解析:

void Process_USART3_FaceResult(uint8_t *data, uint8_t len) { if (len < 5) return; if (data[0] != 0xAA || data[1] != 0x55) return; if (data[2] != 0x02) return; /* 校验和:前4字节按位异或 */ uint8_t checksum = data[0] ^ data[1] ^ data[2] ^ data[3]; if (checksum != data[4]) return; if (data[3] == 0x01) { face_match_ok = 1; face_user_id = data[4]; /* 这里可以扩展为用户ID */ } }

之所以不让模块直接开锁,就是要把决策权集中到STM32。人脸识别只是提供“是谁”的结论,具体这个人有没有权限,还要看权限表和当前时间。我调过很多这种人脸模块,最常遇到的问题是串口过来的数据帧偶尔会多一个字节,如果校验不过就扔,不要因为一帧错误就直接禁止开锁,否则实际使用中开锁率会很低。可以在代码里加一个连续错误计数器,超过5次再报警。

5.2 多模式开锁仲裁状态机

当一个项目同时具备人脸、RFID、密码、蓝牙四种方式后,最怕的就是逻辑混乱。比如用户刷脸成功的同时,手指按到了密码键盘,或者管理员远程开锁后,用户又刷了一次RFID卡。我设计了一个优先级仲裁表:

优先级来源判定条件动作
1人脸识别face_match_ok == 1直接开锁
2密码+RFID双重认证密码正确且当前卡号匹配直接开锁
3RFID单卡卡号存在且auth_level>=1开锁并记录
4蓝牙远程授权管理员确认后的开锁帧开锁并触发超时

对应的伪代码可以用一个简单的循环实现:

void Access_Arbitration(void) { if (face_match_ok) { Door_Unlock(); face_match_ok = 0; } else if (rfid_input_valid && password_input_valid) { Door_Unlock(); rfid_input_valid = 0; password_input_valid = 0; } else if (rfid_input_valid) { if (AccessTable_FindByUID(scaned_uid) != NULL) { Door_Unlock(); } rfid_input_valid = 0; } else if (bluetooth_unlock_req) { if (admin_confirmed) Door_Unlock(); bluetooth_unlock_req = 0; } }

这段仲裁代码的要点在于“每个标志位只保持一个事件周期”。我一般在开锁后立即清零所有标志位,否则残留的标志会在下一次主循环中再次触发开锁。除此之外,电磁锁吸合需要几百毫秒,这期间应该忽略新的人脸识别结果,我习惯加一个unlock_busy计数器,在计数器倒计时结束前直接跳过仲裁。

5.3 阈值调整和安全边界

人脸识别模块的阈值直接决定误识别率与拒真率的平衡。把置信度阈值调低,会让照片和视频都通过,给人脸识别门禁机做渗透测试时一打一个准;调得太高,本人化妆或者光线暗一点就开不了门。项目文档里通常会给出设置阈值的AT指令,但真正要做的不是写死,而是在App里增加一个调试页面,动态读取识别置信度并调整。比如在默认楼道逆光环境下,把阈值调到比出厂值高5%左右,再经过十几次真人测试和照片攻击测试,才能确定最终参数。

6. Flash掉电保存与门禁调试三板斧

6.1 用内部Flash保存权限表

权限表如果只放在SRAM里,断电重启后管理员增加的卡、修改的密码全都会丢失。STM32F103内置Flash自带了存储能力,不需要外挂EEPROM。我通常把权限表放在主程序占用空间之后的几个扇区,比如C8T6是64KB Flash,末尾4KB就是0x0800F000。写入前必须先擦除整个扇区,Flash的擦除操作会将它所有位置为0xFF,然后再写入对应值:

void Flash_WriteAccessTable(void) { uint32_t addr = FLASH_BASE + 65536 - 4096; /* 末尾4KB */ uint16_t i; FLASH_Unlock(); FLASH_ErasePage(addr); for (i = 0; i < MAX_ACCESS_ITEMS; i++) { FLASH_ProgramHalfWord(addr + i * sizeof(AccessItem), ((uint16_t*)&access_table[i])[0]); FLASH_ProgramHalfWord(addr + i * sizeof(AccessItem) + 2, ((uint16_t*)&access_table[i])[1]); } FLASH_Lock(); }

严格来说,这种直接往Flash写结构体的做法需要确保结构体是2字节对齐的,否则ProgramHalfWord写的位置会错位。我自己的做法是先把整个结构体memcpy到一个uint8_t数组,再按半字循环写入。写Flash期间必须关闭串口中断,否则程序会死在Flash等待期间。

6.2 验证方法:串口日志分级

调门禁系统,不要只会在Keil里打断点。我习惯把所有调试信息按级别输出到串口:错误级、事件级、数据级。错误级打印“RFID_Request fail”,事件级打印“card 12 34 56 78 unlock”,数据级打印接收到的原始蓝牙帧和人脸置信度。用逻辑分析仪抓取USART引脚波形,可以确定蓝牙模块是否真的工作在9600波特率。如果身边没有逻辑分析仪,直接用两个USB转TTL模块,把STM32的TX接到PC串口,原样转发数据,也能达到类似效果。

6.3 避免误触发:继电器测试跳线

最后分享一个特别实用的技巧:在继电器开锁回路中串联一个手动测试跳线。调试时先断开跳线,此时即使仲裁逻辑误判开锁,电磁锁也不会真正吸合,使用万用表量一下继电器输出端有没有高电平,就能确认是逻辑问题还是执行机构问题。等所有模式都验证通过后再接上跳线,正式演示时也会更稳妥。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询