news 2026/9/12 0:41:51

基于STM32的智能门禁系统程序设计:从状态机到Flash存储

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32的智能门禁系统程序设计:从状态机到Flash存储

简介:面向嵌入式开发者与电子类毕设学生,STM32智能门锁硬件端工程实现了密码与指纹解锁、虚位密码、超时锁定以及通过Wi-Fi/MQTT接入阿里云平台的完整逻辑。资源包共135个文件,以75个.h头文件和46个.c源文件为核心,覆盖HAL库外设驱动、FatFs文件系统、中文字库、SPI/UART/SD卡等模块,另有5个json配置文件、少量txt说明、png电路或界面截图及md文档,压缩包整体约4.49MB。当前已有325人浏览学习。工程将密码与指纹特征加密存储于外部Flash中,并支持门锁状态上报、开门通知、钥匙修改通知和门未锁好提醒等事件推送,便于理解嵌入式端如何与阿里云物联网平台完成双向交互。需要留意的是,压缩包仅包含门锁硬件端程序,安卓应用与云端配置不在其内;对正在准备毕业设计或开发实际门锁产品的开发者,可直接复用其屏幕交互、错误提示与自动锁屏机制,有效缩短固件开发与联调周期。

1. 把“智能门禁”拆成程序,才看得见 stm32 真正要做的事

门禁硬件的单价,现在已经被各种模块厂商压得很低,刷卡头、矩阵键盘、继电器输出焊在一起不过十几根飞线。真正让“基于 stm32 的智能门禁系统程序”从实验室里的“能开门”变成现场挂机半年不出错的,从来不是硬件选型,而是程序里对输入可靠性、掉电行为和异常恢复的处理。矩阵键盘按一次进来两个数字、RC522 偶尔读卡超时、继电器吸合的一瞬间单片机复位,这三类问题消耗的调试时间,比开门逻辑本身多得多。

这套程序的本质,是用状态机把输入验证、锁控输出、异常恢复串成一个闭环,用统一的定时节拍驱动扫描和去抖,再用片内 Flash 把密码、白名单和开锁日志留住。读卡、按键、门磁这些输入可以被任意顺序打断,但程序行为必须始终聚焦在“空闲、校验、开锁、报警”这几个状态上。做基于 stm32 的毕业设计,或者在一个 stm32 项目里新开一个门禁分支,都值得先把这套骨架搭正确,再往上面加功能。

下面按我平时落地的顺序展开:先定引脚和状态机,再写双因子鉴权链,随后处理掉电保存与锁控驱动,最后用三件工具把程序验证到可以交付的状态。

2. 先定输入输出再写状态机:stm32智能门禁的系统骨架

2.1 引脚分配与最小电路:哪些脚不能省

我一般先用 CubeMX 把时钟、GPIO 和外设排好,再动飞线。控制器用 STM32F103C8T6 就足够,64KB Flash 放几百条白名单和一大段日志绰绰有余。RC522 走 SPI1,占用 PA5/PA6/PA7,片选和复位单独分 PA4、PA3,不与其它外设共享;矩阵键盘行列各四个,占 PB0 到 PB7;继电器用 PB8 输出,门磁接 PB9;UART1 的 PA9/PA10 留给上位机管理。这个分配不是唯一解,但有几个引脚一定要慎重:PA13/PA14 是 SWD 调试口,PB3/PB4 默认复用 JTAG,把它们当普通 GPIO 用不是不行,代价是下载调试变慢,严重时烧录器直接连接失败。

外部晶振按 8MHz 配两个 22pF 负载电容,有条件的用负载电容公式 CL=(C1×C2)/(C1+C2)+Cstray 估算,Cstray 按 3 到 5pF 算,保证等效负载落在晶振手册范围内。这个参数偏了,初期仪表看不出问题,跑几个月后 RTC 时间戳和定时器节拍会慢慢漂。另一个隐藏点:用 CubeMX 后期把芯片型号从 C8T6 换到 RBT6 很方便,但迁移后必须重新核对所有引脚的复用功能,不能只改型号就编译下载。

功能块接口推荐引脚关键说明
RC522SPI1 + CS/RSTPA5/PA6/PA7、PA4/PA3SPI 时钟降到 1Mbps 左右更稳
矩阵键盘4 行输出 / 4 列输入PB0~PB3、PB4~PB7行推挽、列上拉,禁止列下拉
继电器开锁GPIO 推挽输出PB8经 ULN2003 或三极管驱动,线圈并联续流二极管
门磁检测GPIO 上拉输入PB9门体开合检测,配合软件滤波
调试与管理口UART1PA9/PA10日志输出、白名单管理、协议回放

2.2 用状态机代替散排逻辑,运行时序才能一屏讲清

门禁的输入种类并不多:键盘按下、刷卡、门磁动作、超时,但如果把这些事件用 if 散落在各自的回调里,验证顺序一乱,程序行为立刻不可预测。我通常把系统限制成四个状态:空闲、校验、开锁、报警,每个状态只响应自己该响应的事件,非法事件直接丢弃。状态机不仅让代码可读,更重要的是让“锁定时拒绝一切输入”这种安全策略有了明确的落地位置。

调度不建议上 RTOS,门禁任务的实时性要求只有毫秒级,超级循环加定时器节拍足够,还少一层任务切换的心智负担。主循环按 10ms 周期调用一次状态机,SysTick 用 HAL_GetTick() 提供时间基准,需要精确时序的蜂鸣器鸣叫再用 TIM 定时器单独控制。核心状态机如下:

typedef enum { ST_IDLE, ST_VERIFY, ST_UNLOCK, ST_ALARM } door_state_t; static door_state_t g_state = ST_IDLE; static uint32_t g_enter_tick = 0; static uint8_t g_fail_count = 0; void door_fsm_run(void) { switch (g_state) { case ST_IDLE: // 只有空闲态才接受新的鉴权输入 if (key_event_pending() || rfid_event_pending()) { g_state = ST_VERIFY; g_enter_tick = HAL_GetTick(); set_panel_led(LED_VERIFY, ON); } break; case ST_VERIFY: if (auth_result == AUTH_PASS) { g_fail_count = 0; g_state = ST_UNLOCK; g_enter_tick = HAL_GetTick(); set_relay(RELAY_ON); } else if (auth_result == AUTH_FAIL && ++g_fail_count >= 3) { // 连续失败 3 次,进入报警锁定 g_state = ST_ALARM; g_enter_tick = HAL_GetTick(); } else if (HAL_GetTick() - g_enter_tick > VERIFY_TIMEOUT_MS) { g_state = ST_IDLE; set_panel_led(LED_VERIFY, OFF); } break; case ST_UNLOCK: // 开锁持续时间到后自动回锁 if (HAL_GetTick() - g_enter_tick > UNLOCK_KEEP_MS) { set_relay(RELAY_OFF); g_state = ST_IDLE; } break; case ST_ALARM: // 锁定 30 秒,期间键盘和刷卡一律不收 if (HAL_GetTick() - g_enter_tick > LOCK_DURATION_MS) { g_fail_count = 0; g_state = ST_IDLE; } break; } }

这段代码的关键在于状态迁移记录 g_enter_tick,所有超时判定都基于它与当前 HAL_GetTick 的差值,不需要为每个状态单独开定时器。VERIFY_TIMEOUT_MS 通常取 5000,UNLOCK_KEEP_MS 取 3000 到 5000,LOCK_DURATION_MS 取 30000,这些宏集中放在配置头文件里,现场交付时只需要调这一处。注意 door_fsm_run 只能放在主循环轮询,不能进中断;中断回调只负责置事件标志,鉴权和状态迁移全部留在主循环完成,避免 SPI 读卡和 Flash 写入在中断上下文里卡死。

3. 刷卡与密码并存的双因子鉴权:stm32门禁程序的完整鉴权链

3.1 矩阵键盘扫键与按键去抖:先解决“按一次进来两字”

矩阵键盘电路设计本身不复杂,四行四列的机械开关本质是一堆抖动源。没有软硬件去抖时,一次按键经常被采样成多个脉冲,轻则密码输入重复,重则直接触发重试锁定。硬件上可以给每个按键并联 104 电容,但更可靠的是软件处理:扫描到低电平后不立即确认,间隔 10ms 连续扫描两次,两次结果一致才上报按键。门禁键盘不追求游戏手柄的微秒级响应,20ms 的处理延迟完全无感。

扫描代码用行推挽、列上拉的标准接法:

const uint8_t row_pin[4] = { GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3 }; const uint16_t col_mask = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; uint8_t key_scan(void) { for (uint8_t r = 0; r < 4; r++) { // 先把四行全部拉高,再把当前行拉低 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, row_pin[r], GPIO_PIN_RESET); // 读取列口,取反后获得按下位 uint16_t raw = (~HAL_GPIO_ReadPin(GPIOB, col_mask)) & col_mask; if (raw) { for (uint8_t c = 0; c < 4; c++) { if (raw & (1U << (4 + c))) { return r * 4 + c; // 返回 0~15 的键值 } } } } return 0xFF; // 无按键 }

这里先置高所有行线,再把当前扫描行拉低,目的是避免上一行扫描残留的电平影响本次判断。列口配置为上拉输入,无键按下时读到全高,取反后为 0;有键闭合时对应列被拉低,取反后对应位置 1。raw 与 col_mask 做位与,排除掉行口的干扰。实际工程中不把去抖逻辑揉进 key_scan,而是在主循环里每 10ms 调用一次,连续两次返回相同键值才向状态机投递按键事件,返回值 0xFF 直接过滤。

3.2 RC522 过 SPI 读卡号:先验证版本号,再谈寻卡

RC522 和 stm32 的通信,在 HAL 库环境下我习惯拆成三步:第一步只验证总线,第二步再寻卡,第三步才是读 UID 和鉴权。网上不少例程把三步全包在一个大函数里,一旦读不到卡,根本分不清是 SPI 没通还是天线场没建立。验证方式很直接:读取 RC522 的版本寄存器,地址 0x37,读回常见值是 0x92 或 0x91。如果读回 0xFF 或 0x00,先查 CS 片选是否悬浮、SPI 极性和相位是否配置为模式 0,而不是急着改寻卡重试次数。

CubeMX 里配置 SPI1 时,分频系数往大了调,实践下来 1Mbps 左右的时钟在飞线环境下最稳,RC522 模块规格虽然支持更高速度,但杜邦线和模块插座带来的寄生电容会把波形边沿拖垮。防碰撞和选卡流程走完后,拿到 4 字节 UID,白名单匹配用 memcmp;这里有个细节:memcmp 的第三个参数必须固定传 4,不能依赖外部传入的长度字段,防止恶意构造的卡号长度绕过比较。每步流程的状态码通过 UART 打印,现场调试时串口能看到“寻卡成功、防碰撞成功、选卡成功”到哪一步断掉。

RC522 状态表示含义处理建议
0x26 应答成功检测到卡片进入射频场继续防碰撞流程
防碰撞成功已取得 4 字节 UID进入选卡和验证
超时错误卡片未在场或天线未启间隔 100ms 重新检测
位冲突错误多张卡同时靠近延时几毫秒后重试

3.3 鉴权结果处理:三次失败进报警,防止暴力枚举

鉴权链最容易犯的设计错误是“密码对就开门,密码错就提示”,缺少失败后的抑制策略。6 位数字密码空间只有一百万种,交给暴力脚本按每秒十次的节奏试,一天之内必然被跑穿,门禁程序必须内置失败计数和锁定窗口。上面的状态机里已经预留了 g_fail_count,鉴权失败一次就加一,连续达到三次直接进入 ST_ALARM 状态,这个状态下矩阵键盘和 RC522 全部停止响应,持续 30 秒。只有管理员串口指令能提前解除锁定,这条口令在程序里单独写死,不能与用户密码共用一套存储结构。

不管是键盘输入的密码还是刷卡得到的 UID,最后都收敛成同一个鉴权接口,例如 auth_check_uid(uint8_t *uid, uint8_t len) 和 auth_check_password(uint8_t *buf, uint8_t len)。两个函数内部都对长度做上限校验,密码长度不足四位或者超过八位直接返回失败,不进入比较逻辑。卡片鉴权还有一个常见坑:多张卡放到读卡器上时,RC522 会返回位冲突,程序容易误判成非法卡,正确做法是原卡保持不动、延迟重试,而不是立刻累加失败次数。

4. 掉电保存与锁控输出:stm32门禁里最容易翻车的三个细节

4.1 密码、白名单、事件日志写进片内 Flash

授权数据必须掉电保持。外部 AT24C02 是一种选择,但 F103 片内 Flash 在典型门禁项目中往往还空着一大半,直接用 Flash 能省一路 I2C,也少一个器件失效点。F103 的 Flash 是页擦除架构,64KB 型号共 64 页、每页 1KB,写入粒度是半字或字。存密码和白名单前先规划布局,我的习惯是:密码哈希单独占一页,白名单从下一页开始顺序存放,事件日志再往后放并做环形覆盖。任何整页擦除动作都要先把这页内容读进 RAM 缓冲区,构造好新数据再擦除回写,否则中途掉电整页变 0xFF。

这里给出一个带备份思想的存储函数示例:

#define PIN_PAGE_0 0x0800C000 // 密码区主备份页 #define PIN_PAGE_1 0x0800C400 // 密码区副备份页 void flash_store_pin(uint32_t *hash, uint32_t len) { uint32_t page_err = 0; FLASH_EraseInitTypeDef erase_cfg; HAL_FLASH_Unlock(); __disable_irq(); // 写 Flash 期间关中断 for (int copy = 0; copy < 2; copy++) { uint32_t dest = (copy == 0) ? PIN_PAGE_0 : PIN_PAGE_1; erase_cfg.TypeErase = FLASH_TYPEERASE_PAGES; erase_cfg.PageAddress = dest; erase_cfg.NbPages = 1; HAL_FLASHEx_Erase(&erase_cfg, &page_err); for (uint32_t i = 0; i < len; i += 4) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, dest + i, hash[i / 4]); } } __enable_irq(); HAL_FLASH_Lock(); }

这段代码把同一份哈希写入两个物理页,启动时分别读出并比对 CRC,一页损坏就用另一页恢复。HAL_FLASH_Unlock 必须在擦写前调用,HAL_FLASH_Lock 在结束后调用;写 Flash 期间关闭中断,是因为擦写操作需要占用指令总线,中断服务程序里若有 Flash 访问会产生总线忙等待。密码不能明文存储,至少做一次 FNV 或 CRC 混淆,工程上可以升级为带盐的摘要,但毕业设计阶段先保证“不是明文落盘”。

存储方案擦写方式寿命与代价适用场景
片内 Flash页擦除、字写入约 1 万次擦写,零成本白名单变化不频繁的普通门禁
AT24C02字节写、页写约 100 万次,需 I2C日志频繁追加、需要高耐用
W25Q64 外挂 SPI Flash扇区擦除约 10 万次,成本低需要存大量开门记录

4.2 继电器驱动与续流二极管:一类典型的“吸合即复位”

继电器吸合瞬间把 stm32 复位,是门禁项目里最经典的玄学问题,根因几乎都出在驱动电路上。GPIO 直接驱动继电器线圈,电流不够是小事,更大的问题是线圈断开瞬间会产生反向感应电动势,电压尖峰可以直接打穿 GPIO 或让电源跌落。常规做法是 GPIO 先接三极管或 ULN2003 放大电流,继电器线圈两端反向并联一只 1N4148 或 1N4007 续流二极管,让反电动势在二极管和线圈的环路里消耗掉。电磁锁的功率更大,最好连光耦一起上,PC817 输入端接 GPIO,输出端单独供电给继电器。

这里有一个接线细节:续流二极管方向不能接反,阴极接继电器线圈正极、阳极接负极,接反就变成短路路径,继电器一上电就烧驱动管。锁控输出最好能有独立的 5V 电源轨,不要从 stm32 的 3.3V LDO 后面取,瞬时电流会拉低 MCU 供电电压。现场如果已经出现吸合复位的现象,先用示波器抓继电器供电引脚在动作瞬间的跌落波形,再检查续流管方向,不要一上来就怀疑程序跑飞去加大喂狗频率。

4.3 低功耗唤醒:STOP 模式、门磁中断与电池电压检测

电池供电门禁要把待机电流压下来,F103 最常用的是 STOP 模式,典型待机电流几十微安级别。进入 STOP 前把不用的外设时钟关闭,把矩阵键盘的列线和门磁接成 EXTI 外部中断,任意一路信号变化都可以唤醒。唤醒后先重新初始化时钟树,再恢复外设,注意 F103 从 STOP 退出后默认运行在 HSI 上,如果主频是 72MHz,必须重新配置 PLL,否则定时器节拍和串口波特率全部错位,表现为“唤醒后第一次按键失灵或者串口乱码”。

低电量告警可以顺带用 ADC 做:电阻分压后采样电池电压,主循环里每 10ms 判断一次,低于阈值时只允许开门三次并点亮欠压指示灯。ADC 这里不需要开 DMA,门禁不采集连续波形,单次转换足够,省掉 DMA 配置带来的外设关联复杂度。待机电流测试要断开调试器,因为 st-link 的参考电压会通过 SWD 接口反向供电,测出来的数据比真实值虚高不少,这是低功耗验证里最容易出现的假数据来源。

5. 三件工具把 stm32 门禁程序验证到可以交付

5.1 逻辑分析仪抓 SPI 时序,定位读卡异常

RC522 的调试别靠猜。逻辑分析仪四个通道分别接 CS、SCK、MOSI、MISO,触发沿设置在 CS 下降沿,先看读版本号的波形:主机发出命令后,MISO 上必须回来一段数据。采样率不用太高,10M 左右足够覆盖 1Mbps 的 SPI 时钟。如果 MISO 一直是高电平,问题多半在片选配置或者 MISO 引脚复用;如果波形断断续续,检查杜邦线长度和 SPI 分频系数。键盘扫描同样能用逻辑分析仪验证:行线拉低、对应列线被拉低的过程,直接证明键值计算没有接反行列。

5.2 串口回放协议帧,把状态迁移变成可重复测试

把按键、刷卡、门磁统一封装成串口协议帧,等于给状态机装了一台信号发生器。常见做法是定义$KEY,05*C7$RFID,1234ABCD*5A这样的短帧,帧尾固定一个校验和。主循环里解析到合法帧后,直接调用对应的输入处理函数,不经过真实的 GPIO 扫描。测试时用支持循环发送的串口工具,预置二十条正常帧加两三条非法帧循环发送,同时开启板子的状态迁移日志,观察 IDLE 到 VERIFY 到 UNLOCK 的轨迹是否符合预期。非法帧必须包含长度错误和校验错误两类,确认这些帧被丢弃且不触发失败计数,才能证明协议层能挡住构造数据。

5.3 打开 IWDG 做压测,让复位问题无处遁形

看门狗不是用来兜住 Bug 的,但门禁设备挂墙后没人天天去断电重启,IWDG 作为最后一道防线仍然必要。在 CubeMX 里开启 IWDG,溢出时间设 1 秒,主循环末尾喂狗;喂狗代码必须放在状态机函数之后,因为状态机在 ST_ALARM 锁定期间也要正常返回,否则锁定 30 秒内看门狗会误复位整个系统。压测方法是对继电器连续开关 100 次,同时统计 stm32 的复位次数,如果喂狗间隔正常但仍然出现复位,优先怀疑电源跌落而不是程序死循环。IWDG 一旦使能就只能复位关闭,所以它不能替代驱动电路整改,只用来保证异常发生后系统自动恢复到空闲状态。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 0:40:28

ESP32-S3语音助手接入大模型:基于ESP-IDF的完整实现

简介&#xff1a;这是一份基于乐鑫ESP-IDF框架打造的国产开源小智AI机器人完整工程源码&#xff0c;主要面向物联网嵌入式开发者、机器人创客以及AI应用学习者&#xff0c;解决在ESP32系列芯片上快速接入大模型并实现自然语音对话的关键问题&#xff0c;帮开发者少走弯路。资源…

作者头像 李华
网站建设 2026/9/12 0:35:08

写CRUD三年,如何突破瓶颈成为真正的架构师?

写了三年CRUD&#xff0c;很多人会陷入一种隐秘的焦虑&#xff1a;每天在Controller、Service、Mapper之间来回穿梭&#xff0c;需求一个接一个&#xff0c;代码越写越熟&#xff0c;却感觉离“架构师”越来越远。增删改查本身没有错&#xff0c;错的是我们只把系统当成一张张表…

作者头像 李华
网站建设 2026/9/12 0:29:33

基于深度学习的图像修复实战:从掩码生成到GAN与注意力机制

简介&#xff1a;一套基于深度学习的图像修复系统Python实现与项目文档&#xff0c;面向计算机专业毕业设计、课程设计以及需要完整实战项目的机器学习爱好者。项目采用卷积神经网络与对抗式训练策略&#xff0c;可对图像划痕、噪点、局部遮挡等损伤进行智能补全&#xff1b;代…

作者头像 李华
网站建设 2026/9/12 0:26:05

CNN模型Web部署实战:PyTorch转ONNX+FastAPI服务化

简介&#xff1a;本资源是一个基于卷积神经网络&#xff08;CNN&#xff09;实现的猫狗图像识别Web应用完整工程包&#xff0c;面向深度学习初学者与Web部署实践者&#xff0c;解决图像分类模型训练、封装与本地化部署的一体化学习需求。资源共218个文件&#xff0c;涵盖44个Py…

作者头像 李华
网站建设 2026/9/12 0:24:08

damo_link:Rust编写的32位单片机烧录与串口调试一体化工具

1. 项目概述&#xff1a;为什么一个“二合一”工具能解决32位单片机开发中最痛的两个环节&#xff1f;在嵌入式开发一线干了十多年&#xff0c;我经手过从8051到RISC-V的上百款MCU&#xff0c;也踩过无数烧录失败、串口乱码、波特率错配、COM端口消失的坑。直到去年用上damo_li…

作者头像 李华