STM32智能门禁系统:多身份验证与状态机设计
2026/9/17 12:44:59 网站建设 项目流程

简介:面向电子信息、自动化与嵌入式方向本科生的毕业设计文档,围绕以STM32F407ZGT6为核心、集成密码键盘、指纹识别与RFID读卡模块的智能门禁系统展开,覆盖硬件选型与电路连接、软件流程调度、用户信息增删与门禁记录管理等关键环节,可用于论文撰写参考、课程设计选题与嵌入式入门实践。文档以生物识别与卡片识别双重验证为主线,兼顾安全与通行便利,并涉及数据加密存储、隐私保护与网络防护等设计思路。压缩包共1个文件,为863KB的docx毕业论文,包含摘要、研究背景与意义、研究目的、系统总体方案及软硬件设计等章节,结构完整,可直接借用论文框架,也可据此拆解STM32外设驱动与模块联调任务。目前已有178人学习下载,适合需要理清门禁类课题思路、补齐论文结构或对照实现细节的读者。

1. 从答辩被问懵说起:STM32 门禁系统到底在做什么

有个现象挺有意思:每年毕业季,STM32 门禁类课题的实物能跑通,但答辩一问“你这个身份验证的状态机怎么设计的”,很多人就卡住了。问题不在焊板子,在于没把系统当成一个完整的信息流来看。这套基于 STM32 的智能门禁系统,本质上是一个以单片机为中枢、把三种身份凭据(密码、指纹、RFID 卡)统一收敛到一套权限模型的嵌入式控制系统。它的输入层是 4×4 矩阵键盘、AS608 指纹模块、RC522 射频读卡器,输出层是 OLED 状态显示和 SG90 舵机模拟的门锁执行机构,中间靠 STM32F103C8T6 做调度。适合已经会点亮 LED、会写串口收发,但没做过完整多模块协同项目的人,也适合被“毕设怎么从散装例程变成一套系统”困住的开发者。

2. STM32F103C8T6 主控选型与外设资源分配

2.1 为什么这颗芯片够用,以及哪里会不够

STM32F103C8T6 是 Cortex-M3 内核、LQFP48 封装、72MHz 主频、64KB Flash、20KB SRAM。门禁系统的外设需求算下来并不重:OLED 走 I2C 占 2 根线,RC522 走 SPI 占 4 根线(NSS、SCK、MISO、MOSI),AS608 走 UART 占 2 根线,矩阵键盘占 8 根 GPIO,舵机占 1 个定时器通道,蜂鸣器占 1 根。加起来 GPIO 占用在 20 根以内,C8T6 的 37 个可用 IO 完全兜得住。

真正吃紧的是 SRAM。AS608 的指纹特征模板一个用户约 512 字节,如果不在本地存模板、只存指纹 ID,10 个用户也就几十字节;但如果你打算把指纹图像缓冲区(AS608 单幅图像约 36864 字节)也塞进片内,20KB 直接爆。常见做法是让 AS608 自己管模板库,STM32 只存“指纹 ID → 权限等级”的映射表,这样 SRAM 消耗可以压到 2KB 以内。

外设模块接口方式占用引脚对应外设资源
0.96 寸 OLEDI2CPB6/PB7I2C1
RC522SPIPA4-PA7SPI1
AS608 指纹UARTPA9/PA10USART1
4×4 矩阵键盘GPIO 扫描PB0-PB7GPIOB
SG90 舵机PWMPA0TIM2_CH1
蜂鸣器GPIOPC13GPIO 输出

提示:PA9/PA10 同时也是烧录后常用的串口调试口,接 AS608 之后就没法直接用串口打印调试信息了。我一般会把调试口换到 USART2(PA2/PA3),避免拆线才能看日志。

2.2 时钟树与启动配置

F103 默认走内部 8MHz RC(HSI),直接跑会飘。外部晶振 8MHz 经 PLL 9 倍频到 72MHz,这是标准配置,必须在SystemInit之前把 HSE 使能,否则定时器算出来的 PWM 脉宽全错。下面是用标准库配置时钟的关键片段:

// system_stm32f10x.c 中的时钟配置,确认 HSE_VALUE 为 8000000 #define HSE_VALUE ((uint32_t)8000000) // PLL 倍频 9 倍,得到 72MHz 系统时钟 RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); // 等待 PLL 锁定 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() != 0x08); // 等待切换完成

逻辑说明:先等 PLL 就绪再切系统时钟源,顺序反了会导致切到未锁定的 PLL 上,程序跑飞。参数说明:RCC_PLLMul_9决定了最终主频,改成RCC_PLLMul_4就是 32MHz,舵机 PWM 的预分频值要跟着重算。

2.3 舵机的 PWM 参数怎么算才不抖

SG90 的周期是 20ms(50Hz),脉宽 0.5ms 对应 0 度,2.5ms 对应 180 度。F103 的 TIM2 挂在 APB1 上,时钟 72MHz。预分频PSC=71得到 1MHz 计数频率,自动重装值ARR=19999得到 20ms 周期。这样每个计数单位是 1μs,脉宽直接写微秒数就行:

// TIM2 初始化:72MHz / (71+1) = 1MHz 计数频率 TIM_TimeBaseInitTypeDef t; t.TIM_Prescaler = 71; // 1us 一个计数 t.TIM_Period = 19999; // 20ms 周期,对应 50Hz t.TIM_ClockDivision = 0; t.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &t); // 设置 90 度(脉宽 1.5ms) TIM_SetCompare1(TIM2, 1500); // 单位是 us

参数说明:TIM_SetCompare1的值就是脉宽微秒数,500 对应 0 度,2500 对应 180 度。门禁开锁常用 0→90 度,关门 90→0 度。注意舵机上电瞬间会往某个角度猛冲,机械门锁的限位要留余量,否则堵转电流会把 USB 供电拉垮,表现为单片机复位——这个坑我踩过,换独立 5V 2A 供电就好了。

3. 三种身份验证模块的驱动与状态机设计

3.1 矩阵键盘扫描与密码输入的防抖处理

4×4 矩阵键盘 16 个键,需要 8 根线,比 16 个独立按键省一半 IO。原理是行线输出低电平、列线配置为上拉输入,逐行拉低读取列状态,哪一列为低就说明哪一行那一列的键被按下。扫描周期设在 10ms 左右,配合软件消抖。

// 行线 PB0-PB3 推挽输出,列线 PB4-PB7 上拉输入 const uint8_t keymap[4][4] = { {'1','2','3','A'}, // A 键切换密码模式 {'4','5','6','B'}, // B 键切换指纹模式 {'7','8','9','C'}, // C 键切换刷卡模式 {'*','0','#','D'} // # 确认,* 取消,D 进管理员 }; uint8_t KeyScan(void) { for (uint8_t row = 0; row < 4; row++) { GPIO_WriteBit(GPIOB, 0x0F, (uint16_t)~(1 << row)); // 逐行拉低 for (uint8_t col = 0; col < 4; col++) { if (!GPIO_ReadInputDataBit(GPIOB, 1 << (col + 4))) { delay_ms(10); // 消抖 if (!GPIO_ReadInputDataBit(GPIOB, 1 << (col + 4))) return keymap[row][col]; } } } return 0; // 无按键 }

逻辑说明:GPIO_WriteBit的掩码只操作低 4 位,不会误改列线状态。参数说明:delay_ms(10)是消抖延时,太短会连击,太长会漏键,10~20ms 是经验区间。

注意:如果键盘和 OLED 共用 GPIOB,扫描时会短暂改变 PB4-PB7 状态,OLED 若也接在 I2C 的 PB6/PB7 上就会花屏。这是硬件规划阶段就该避开的重叠,别等调不出来才查。

3.2 AS608 指纹模块的指令帧格式与权限分级

AS608 走 UART,默认波特率 57600。它用固定帧格式通信:帧头 2 字节0xEF 0x01,然后是地址(默认0xFF 0xFF 0xFF 0xFF)、包标识、包长度、指令码、参数、校验和。校验和 = 包标识 + 包长度高字节 + 包长度低字节 + 指令 + 参数,逐字节累加取低 16 位。

// 采集指纹并搜索模板库,返回匹配到的指纹 ID uint8_t FingerSearch(void) { uint8_t cmd[] = {0xEF,0x01,0xFF,0xFF,0xFF,0xFF,0x01,0x00,0x03,0x01,0x00,0x05}; // 最后两字节 0x00 0x05 是校验和,由 send_cmd 内部重算 uart_send(cmd, 12); delay_ms(200); // AS608 比对需要时间 if (uart_read(buf, &len) == ACK_OK) { return buf[10]; // 返回模板 ID } return 0xFF; // 未匹配 }

逻辑说明:帧里的0x01是搜索指令码,参数0x00是缓冲区号。参数说明:delay_ms(200)要覆盖模块比对耗时,AS608 搜索 1000 个模板约 300ms,模板少时可以缩短,但别低于 100ms。

权限分级用一张映射表实现,指纹 ID 0~4 为管理员,5~9 为普通用户。管理员指纹只能通过“管理员进入流程”录入,普通用户不能修改权限表,这是整个系统安全性的下限。

3.3 RC522 的 SPI 读卡流程与扇区结构

RC522 支持 SPI、I2C、UART 三种接口,SPI 最常用。读卡分四步:寻卡(REQA)、防冲突(防多卡同时响应)、选卡、读取扇区。每张 Mifare S50 卡有 16 个扇区、每扇区 4 个块,块 0 是厂商信息,块 3 是密钥块,用户数据存在块 1、块 2。门禁系统一般只在块 1 存一个 4 字节卡号或者用户编号。

uint8_t ReadCardUID(uint8_t *uid) { if (PcdRequest(PICC_REQIDL, buf) != MI_OK) return ERR_NO_CARD; // 寻卡 if (PcdAnticoll(buf) != MI_OK) return ERR_CONFLICT; // 防冲突,拿到 4 字节 UID memcpy(uid, buf, 4); // 用 UID 前 4 字节做比对,无需读扇区,速度更快 return MI_OK; }

逻辑说明:UID 是卡的唯一标识,直接比对 UID 比读扇区更快,也不涉及密钥验证。参数说明:PICC_REQIDL表示寻感应区内所有未休眠的卡,如果要读休眠卡需要发PICC_REQA

3.4 统一身份验证状态机

三个模块各自为政会写出一堆 if-else,维护成本极高。我一般抽出一个状态机,把“等待输入身份 → 验证 → 判定权限 → 执行动作”四个阶段固定下来:

状态触发条件动作下一步
IDLE系统上电OLED 显示主菜单WAIT_INPUT
WAIT_INPUT按键选择模块切换当前验证方式VERIFY
VERIFY输入提交调用对应模块校验CHECK_PERM
CHECK_PERM校验通过判断管理员/用户权限UNLOCK 或 ADMIN
UNLOCK权限为普通用户舵机开锁,蜂鸣器短鸣IDLE
ADMIN权限为管理员进入设置菜单IDLE

这个结构让新增一种验证方式只需在 WAIT_INPUT 里加分支,校验和权限逻辑不用动。

4. 断电保存、错误锁定与实测排错

4.1 用 Flash 模拟 EEPROM 存用户数据

F103C8T6 没有片内 EEPROM,密码、指纹 ID 映射、卡号列表都得存 Flash 或者外挂芯片。最省事的方案是用片内 Flash 的最后一页(Page 63,地址 0x0800FC00)当存储区。写之前必须先擦除整页,擦除后是 0xFF,再按半字(16 位)写入。

#define PARA_ADDR 0x0800FC00 void SaveParams(Params *p) { FLASH_Unlock(); FLASH_ErasePage(PARA_ADDR); // 必须整页擦除 uint16_t *src = (uint16_t *)p; for (uint16_t i = 0; i < sizeof(Params)/2; i++) { FLASH_ProgramHalfWord(PARA_ADDR + i*2, src[i]); } FLASH_Lock(); }

逻辑说明:FLASH_ErasePage擦除 2KB 一页,写入数据不能超过这个大小。参数说明:sizeof(Params)/2是因为按半字写,结构体大小必须是偶数。注意 Flash 擦写寿命约 1 万次,别在按键循环里频繁保存,只在退出设置时存一次。

4.2 连续错误锁定的实现与解锁路径

密码连续错 3 次转管理员验证,管理员再错 3 次锁定;刷卡连续错 3 次同理。这个逻辑的核心是计数器要区分“当前会话”和“累计错误”,会话级计数器在成功验证后清零,锁定标志写进 Flash 保证断电后仍然锁定。

if (WrongCount >= 3) { if (CheckAdmin(pwd)) { WrongCount = 0; // 管理员兜底,计数器清零 UnlockDoor(); } else { LockFlag = 1; SaveParams(&params); // 锁定状态持久化 OLED_ShowString(0, 2, "SYSTEM LOCKED"); } }

注意:锁定后必须有硬件级恢复路径,我一般留一个隐藏组合键(比如上电时长按 D 键 5 秒)触发恢复出厂设置,否则演示时锁死了只能重新烧录。

4.3 实测中最容易遇到的四个问题

第一,OLED 显示乱码。八成是 I2C 地址写错,SSD1306 的地址是 0x78(写)或 0x7A(读),如果驱动里写的 0x3C,那是 7 位地址没左移。第二,指纹模块无响应。先量波特率,用逻辑分析仪抓帧头是不是0xEF 0x01,如果不是就是波特率不匹配,AS608 支持 9600 到 115200。第三,读卡距离太短。RC522 天线是板载 PCB 天线,周围有金属或者锡箔会严重衰减,卡片贴合到 1cm 内才响应是正常的,想改善得换大天线版本。第四,舵机一转单片机就复位。这就是前面说的供电问题,舵机堵转电流能到 1A 以上,USB 口给不了,独立供电是最快的排查手段。

5. 从能跑到好用:把状态机改成表驱动

原型能跑之后,下一层痛点是状态转移逻辑散落在各模块的 if-else 里,加一个“临时密码”或者“防尾随延时上锁”就得动好几处代码。我习惯把手写状态机改成表驱动,用一张常量表描述所有转移,执行引擎只有一个switch

typedef struct { uint8_t cur; // 当前状态 uint8_t event; // 触发事件 uint8_t next; // 目标状态 void (*action)(void); // 转移动作 } Trans; const Trans fsm[] = { {IDLE, EV_KEY_A, WAIT_INPUT, SwitchToPwd}, {IDLE, EV_KEY_B, WAIT_INPUT, SwitchToFinger}, {VERIFY,EV_OK, CHECK_PERM, NULL}, {VERIFY,EV_FAIL, IDLE, IncWrongCount}, {CHECK_PERM, EV_IS_ADMIN, ADMIN, EnterAdmin}, }; void FSM_Run(uint8_t event) { for (uint8_t i = 0; i < sizeof(fsm)/sizeof(Trans); i++) { if (fsm[i].cur == g_state && fsm[i].event == event) { if (fsm[i].action) fsm[i].action(); g_state = fsm[i].next; return; } } // 未匹配到转移,忽略该事件 }

逻辑说明:事件由按键扫描和模块回调统一产生,引擎只负责查表,新增功能就是加一行表项,不动引擎代码。参数说明:sizeof(fsm)/sizeof(Trans)是表项数量,表越大线性查找越慢,条目过百可以按cur做分段索引,但对门禁这种十几个状态的系统没必要。

验证这张表对不对,可以写一个简单的自动化脚本,把状态转移序列打印成日志:上电后依次触发 A 键、输入错误密码三次、触发 D 键、输入管理员密码,检查日志里的状态序列是不是IDLE → WAIT_INPUT → VERIFY → IDLE → ... → ADMIN。手工点按键验证容易漏分支,用串口日志回放能覆盖到边角情况。

调试阶段还可以把 AS608 的返回码全部映射成可读字符串打出来,0x00是成功,0x09是未搜索到模板,0x0A是合并模板失败,0x1C是图像太干,排错时一眼就能定位是手指问题还是通信问题。这类返回码映射表在开发后期比任何断点都好用,尤其是现场演示不能接调试器的时候。

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

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

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

立即咨询