简介:一套面向电子/嵌入式方向学生的智能家居门禁卡综合管理系统毕业设计与课程设计资料包,覆盖原理图、源码、部署到演示的完整流程。系统基于STM32单片机与RC522射频模块,实现密码开锁、指纹开锁、刷卡开锁;管理员可通过密码进入后台,添加或删除指纹、注册或注销门禁卡、修改密码,密码错误时有语音播报提示,兼具低成本与稳定性。资源共87个文件,以38个h头文件、36个c源码为主,另有2个mp4演示视频、原理图、门禁卡管理文档和工程配置文件,zip包约85.89MB,当前已有79人学习。内容预览显示工程采用CORE、SYSTEM、FWLIB、USER、HARDWARE等标准目录,并附演示视频与原理图,便于按模块理解继电器、显示、语音播报、矩阵按键等设计细节,适合需要快速完成课设/毕设并开箱即用的学生参考。
1. 智能家居门禁卡系统,STM32这套方案先拆开看
答辩现场最出效果的一幕通常是这样:一张IC卡靠近读卡模块,OLED屏上立刻弹出卡号,继电器“咔”一声吸合,电磁锁打开,三秒后自动落锁。整套系统的物理载体就是一块STM32最小系统板、一个RC522读卡模块、一块小显示屏和一个继电器模块。标题里的“综合管理系统”听起来复杂,拆开无非是三条链路:读卡、判权、执行,外加一个把三者串起来的主循环。
这类项目对上手者最大的价值,在于它把单片机外设调度串成了一条完整链路:SPI读卡、I2C显示、GPIO控锁、状态机收拢全局。真正花时间的不是RC522驱动移植,而是外设之间怎么配合、状态怎么迁移。适合正在做毕设开题、想直接跑通整套代码的本科生,也适合想弄明白一个带外设协作的嵌入式项目如何组织的初学者。先建立整体框架,再逐层往下拆,是打开这套代码的正确姿势。
2. 硬件选型和引脚分工:STM32F103与RC522的外设清单
门禁卡系统最常见的硬件组合是STM32F103C8T6加RC522射频模块,这个搭配在智能家居类毕业设计里基本属于默认方案。F103C8T6有64KB Flash、20KB RAM,主频72MHz,跑门禁管理逻辑绰绰有余;RC522支持ISO14443A协议,可以直接读M1卡(S50)的UID,模块便宜、例程多,资料到处都能找到。
为什么不选51单片机?51的SPI大多要靠IO口模拟,RC522的读写时序虽然不算苛刻,但模拟SPI会占用大量CPU时间。主循环里如果还要刷OLED、扫按键,响应会明显变慢。STM32把SPI和I2C通信全部卸给硬件外设,主循环只做状态判断,这才是“综合管理系统”和纯读卡Demo的根本区别。
2.1 外设引脚映射与JTAG复用引脚的处理
拿到源码第一步是打开引脚定义文件,把下面的映射表和自己的板子对照一遍。不同最小系统板的丝印略有差异,按表接线能少走一半弯路。
| 外设 | 信号 | STM32引脚 | 说明 |
|---|---|---|---|
| RC522 | SCK | PA5 | SPI1_SCK,速率2MHz以下 |
| RC522 | MISO | PA6 | 主入从出 |
| RC522 | MOSI | PA7 | 主出从入 |
| RC522 | CS | PA4 | 软件片选,低有效 |
| RC522 | RST | PB0 | 复位引脚,高电平复位 |
| OLED | SCL | PB6 | I2C1时钟线 |
| OLED | SDA | PB7 | I2C1数据线 |
| 继电器 | IN | PB1 | 高电平触发 |
| 蜂鸣器 | BEEP | PB10 | 有源蜂鸣器 |
| 按键注册 | KEY_ADD | PB3 | 按下进入发卡模式 |
| 按键删除 | KEY_DEL | PB4 | 按下进入删卡模式 |
PB3和PB4默认被JTAG占用,直接当普通IO用,寄存器配置不生效,按键读回来永远是高电平。需要在初始化最前面关闭JTAG,只保留SWD调试口。这也是很多同学程序下载正常、按键却没有反应的原因。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); GPIO_InitTypeDef gpio_init; gpio_init.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4; gpio_init.GPIO_Mode = GPIO_Mode_IPU; gpio_init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio_init);代码先开启GPIOB和AFIO时钟,再用GPIO_Remap_SWJ_JTAGDisable释放PB3、PB4、PA15,同时保留SWDIO和SWCLK,所以ST-Link下载调试不受影响。最后把PB3、PB4配成上拉输入,悬空保持高电平,按键按下接地读到低电平。
注意:如果调试器用的还是古老的JTAG四线模式,关闭JTAG后就会失效。现在市面上的ST-Link、DAP-Link默认都是SWD模式,不受影响。
2.2 电源与驱动电路:3.3V逻辑和5V继电器的衔接
RC522模块和OLED都是3.3V供电,直接接单片机3.3V即可。继电器模块常见两种:光耦隔离型和三极管驱动型。光耦隔离模块常见5V或12V版本,有跳线选择高电平触发或低电平触发,代码里的GPIO电平必须和跳线一致。
如果不用现成模块,而是自己在面包板上搭电路,会碰到一个经典问题:3.3V的单片机能不能驱动达林顿模块?答案是能,但不能拿GPIO直接推继电器。标准做法是GPIO串一个1k电阻接到NPN三极管(如S8050)基极,继电器线圈跨接在5V和集电极之间,线圈两端反向并联1N4007续流二极管。电磁锁是感性负载,断电瞬间反电动势可以到几十伏,没有续流二极管会直接把单片机电平拉乱,严重时整个系统复位,现象就是“一开锁就重启”。
2.3 上电检查清单
上电后按下面顺序检查,能筛掉大部分硬件问题:
- 3.3V稳压输出正常,电源指示灯常亮。
- 用万用表量RC522模块供电引脚,电压在3.2V到3.4V之间。
- 继电器模块电源指示灯亮,控制端悬空时不动作。
- ST-Link连接后能读到芯片ID。
如果RC522模块供电正常但指示灯不亮,优先检查模块背面的磁珠、电感焊点。这类模块出货量大,偶尔会有虚焊,拿放大镜看一眼比改半天代码有效得多。
3. 读卡与校验:RC522的SPI驱动和卡号比对流程
RC522驱动是所有门禁功能的起点。物理层是SPI,通信对象是一堆寄存器。初次接触的人容易被PCD_TRANSCEIVE、RC522_Command这些名字吓住,其实只要分三层看:底层SPI读写寄存器,中间层封装寻卡、防碰撞、选卡,上层拿卡号做认证。下面按这个层次展开。
3.1 SPI初始化的参数选择与上电时序
RC522的SPI接口最高能跑10Mbps,但实战中不需要往极限上靠。速率太高时,杜邦线稍长就会出现偶发读卡失败——这种“偶尔不灵”的问题在毕设验收现场非常致命。分频取32,72MHz主频下的实际SCK约2.25MHz,既保证速度又留足余量;如果接线超过20cm,直接降到64分频,1.125Mbps依然够用。
void SPI1_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_10MHz; GPIO_Init(GPIOA, &gpio); spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode = SPI_Mode_Master; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_CPOL = SPI_CPOL_Low; spi.SPI_CPHA = SPI_CPHA_1Edge; spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; spi.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spi); SPI_Cmd(SPI1, ENABLE); }几个关键参数逐个说:SPI固定为主模式,8位数据,高位先出;CPOL和CPHA都为0,即空闲时钟低电平、第一个边沿采样,对应MFRC522芯片手册要求的SPI Mode 0。片选用普通GPIO控制,读写前拉低,结束后拉高,比硬件NSS更可控。
RC522初始化还有一个容易忽略的步骤:配置完成后要把天线发射位打开,也就是TxControlReg的Bit0和Bit1置1,否则卡片根本感应不到,寻卡永远超时。不少移植例程在这里漏了一行,现象就是模块灯亮、程序在跑、但死活不读卡。
3.2 寻卡、防碰撞与选卡:拿到卡号的三步
M1卡的读卡流程是固定的:寻卡、防碰撞、选卡。寻卡让感应区内的卡片响应;防碰撞在多张卡同时靠近时选出一张,返回4字节序列号;选卡对序列号做校验并确认,之后才能对该卡继续操作。对应命令如下。
| 命令宏 | 值 | 作用 |
|---|---|---|
| PICC_REQALL | 0x52 | 寻卡,感应区内所有卡都响应 |
| PICC_REQIDL | 0x26 | 寻卡,只响应空闲状态卡 |
| PCD_TRANSCEIVE | 0x0E | 发送数据并等待接收应答 |
uint8_t RC522_GetCardUid(uint32_t *uid) { uint8_t status, len = 0, size = 0; uint8_t tag_type[2], snr[4]; uint8_t cmd[1]; // 第一步:寻卡,0x52请求感应区内所有卡响应 cmd[0] = PICC_REQALL; status = RC522_Command(PCD_TRANSCEIVE, cmd, 1, tag_type, &len); if (status != MI_OK) return 0; // 第二步:防碰撞,取回四字节卡序列号 status = RC522_AntiColl(snr); if (status != MI_OK) return 0; // 第三步:选卡,确认选中并返回卡容量 status = RC522_Select(snr, &size); if (status != MI_OK) return 0; *uid = ((uint32_t)snr[0] << 24) | ((uint32_t)snr[1] << 16) | ((uint32_t)snr[2] << 8) | snr[3]; return 1; }三层的返回状态各有含义:寻卡失败说明卡不在天线感应区,或者天线驱动没起来;防碰撞失败说明两张卡叠在一起;选卡失败多半是通信中断,回头查SPI接线和片选有没有被其它外设占用。卡号拼成uint32_t后,后面白名单比对就变成整数相等判断。需要注意的是,演示用的门禁卡大多是4字节UID的M1卡,如果换成CPU卡或身份证,UID长度不一定是4字节,这段代码就需要改成自适应长度解析。
3.3 白名单认证:把卡号变成权限
作为门禁系统,卡号不能只显示,还得校验。白名单的存储结构在第5章展开,这里先把认证逻辑独立出来看:
uint8_t Card_CheckAccess(uint32_t uid) { for (uint8_t i = 0; i < CARD_TABLE_MAX; i++) { if (card_table[i].valid == 0xFF && card_table[i].uid == uid) { return i + 1; // 返回卡号表中的位置,0表示未授权 } } return 0; }代码里返回的是“索引加1”,调用方拿到0就显示验证失败,拿到正数则取出该卡对应的用户名。这套校验依赖的是卡号本身,没有做M1扇区密钥验证,毕设场景完全够用。如果想让系统往产品级靠,至少还要补两步:用密钥读M1卡扇区数据确认卡的真伪,再对卡号做时间窗口防重放,避免同一张卡被异常频繁使用。
4. 主循环与联动执行:状态机、OLED显示与继电器控锁
读卡代码写完后,真正的难点是整合。很多人的第一个版本是顺序执行:读卡、开锁、延时3秒、锁闭。问题在于延时期间单片机被完全占用,此时再刷一张卡不会有任何响应,演示中很容易被判定为“卡死”。状态机是这类外设协作项目的标准解法:把当前所处的工作状态存下来,主循环只负责按事件切换状态。
4.1 状态定义与主循环框架
门禁行为可以拆成待机、验证、开锁、发卡、删卡五个状态。全局状态变量加一个switch主循环,逻辑一目了然。
typedef enum { STATE_IDLE, // 待机 STATE_VERIFY, // 验证卡号 STATE_UNLOCK, // 开锁状态,等待超时 STATE_ADD_CARD, // 注册新卡 STATE_DEL_CARD // 删除卡片 } sys_state_t; sys_state_t g_state = STATE_IDLE; uint32_t g_uid = 0; uint16_t g_unlock_ticks = 0; while (1) { switch (g_state) { case STATE_IDLE: if (KEY_ADD_IsPressed()) { g_state = STATE_ADD_CARD; } else if (RC522_GetCardUid(&g_uid)) { g_state = STATE_VERIFY; } break; case STATE_VERIFY: if (Card_CheckAccess(g_uid)) { Relay_On(); g_unlock_ticks = 0; g_state = STATE_UNLOCK; } else { Buzzer_Beep(150); OLED_ShowDenied(g_uid); g_state = STATE_IDLE; } break; case STATE_UNLOCK: if (++g_unlock_ticks >= UNLOCK_TIMEOUT_MS / LOOP_MS) { Relay_Off(); g_state = STATE_IDLE; } break; case STATE_ADD_CARD: OLED_ShowString("Add Card..."); if (RC522_GetCardUid(&g_uid)) { Card_Add(g_uid); g_state = STATE_IDLE; } break; case STATE_DEL_CARD: OLED_ShowString("Del Card..."); if (RC522_GetCardUid(&g_uid)) { Card_Delete(g_uid); g_state = STATE_IDLE; } break; } DelayMs(LOOP_MS); }主循环每一轮做的事很薄:读状态、处理事件、延时。UNLOCK_TIMEOUT_MS和LOOP_MS是宏,开锁时间3秒,实测比较合理,太短人还没走到门就锁了。开锁期间状态停留在STATE_UNLOCK,不再重复寻卡,避免继电器抖个不停。用计数代替DelayMs(3000),关键区别是STATE_UNLOCK期间主循环依然活着,按键可以中断开锁,OLED保持刷新,将来接蓝牙模块或WiFi模块,也能随时插入新状态而不改动主干。
状态迁移可以概括成下面这张表,写论文的时候这张表可以直接用。
| 当前状态 | 触发事件 | 输出动作 | 下一状态 |
|---|---|---|---|
| STATE_IDLE | 检测到卡 | 读取UID | STATE_VERIFY |
| STATE_VERIFY | 白名单命中 | 继电器吸合 | STATE_UNLOCK |
| STATE_VERIFY | 白名单未命中 | 蜂鸣器提醒,显示拒绝 | STATE_IDLE |
| STATE_UNLOCK | 3秒计时到 | 继电器释放 | STATE_IDLE |
| STATE_IDLE | 按下注册键 | 显示发卡界面 | STATE_ADD_CARD |
4.2 OLED显示:调程序时的“眼睛”
OLED用I2C接SSD1306,128x32或128x64都可以。初始化之后,调试期最常用的显示内容是卡号和当前状态。
OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, "UID:"); char buf[16]; sprintf(buf, "%08X", g_uid); // 卡号按8位十六进制显示 OLED_ShowString(0, 2, buf);坐标单位是页。128x32的屏幕一共4页,每页8像素高,纵坐标只能取0、1、2、3,取4就超出屏幕了;如果是128x64,纵坐标可以到0到7。sprintf格式化卡号要记得包含stdio.h,在Keil里勾选Use MicroLIB,否则这类库函数体积偏大,Flash剩得就不宽裕。
4.3 继电器与电磁锁:动作执行与保护电路
继电器的控制逻辑是最简单的,GPIO输出高电平,三极管导通,线圈吸合,常开触点闭合,电磁锁通电开锁。但简单电路对细节要求高。买模块时注意跳线选高电平触发,对应代码里的Relay_On();自己搭电路时,续流二极管和基极限流电阻都不能省。
电磁锁通常是12V供电,与单片机系统是两套电源,唯一公共点是GND。常见的坑是直接从单片机稳压器前端取电给电磁锁,开锁瞬间电流拉低电压,单片机随即复位。正确的接法是电磁锁电源从总电源单独拉一路,走线尽量粗,继电器触点按锁的规格选,12V、1A以下的电插锁用常见5V继电器模块也能顶住,但超过这个规格最好换成12V继电器模块或中间继电器。
5. 授权与记录:EEPROM上的卡号表和访客日志
毕业设计的评分表里,“综合管理”通常对应两个功能点:管理员能发卡、删卡,系统能记录最近的开锁记录。数据只放在内存里,断电就全丢,答辩老师一重启板子就露馅。所以必须把卡号表持久化存储,最省事的方案是I2C接口的EEPROM。
5.1 存储选型:为什么不用STM32片内Flash
STM32F103C8T6片内Flash有64KB,放卡号表和日志绝对够,但擦写寿命只有约1万次。卡号表写入不频繁,日志却是每次开锁都写,一天几十次,两年就到寿命边缘。外挂EEPROM的单字节擦写寿命在100万次量级,成本几毛钱,选型优先级明显更高。
常用芯片是AT24C16,容量2KB,支持16字节页面写。可以规划出30张授权卡和约200条访问记录:
| 区域 | 起始地址 | 长度 | 说明 |
|---|---|---|---|
| 卡号表 | 0x0000 | 30 × 8B = 240B | 最多30张授权卡 |
| 访问日志 | 0x00F0 | 200 × 8B = 1600B | 环形覆盖,保留最近记录 |
卡号表用固定结构体数组,删除就是标记失效,追加就是找空位覆盖,逻辑最简单,也最容易在论文里讲清楚。
5.2 卡号表结构设计
#define CARD_TABLE_MAX 30 #define CARD_TABLE_ADDR 0x0000 typedef struct { uint32_t uid; // 卡号,从RC522读出 uint8_t valid; // 0xFF表示有效,0x00表示空位 uint8_t user; // 用户编号,用于显示 } card_entry_t;valid单独占一个字节,而不是用uid等于0来判断空位,是因为M1卡虽然出厂UID通常不为0,但理论上存在全0的卡,拿0当空位标记,这类卡永远无法注册。用独立的标记字节,删除时只需把valid写成0,uid留在原地无妨,新卡注册时直接覆盖。
这里要注意结构体对齐:uint32_t占4字节,两个uint8_t在它之后,sizeof(card_entry_t)实际取到8字节而不是6字节。所有读写都用sizeof计算,不要手写长度。
5.3 EEPROM分页写入的边界处理
AT24C16每个页面16字节,页写不能跨页。8字节一条的卡号记录,如果起始地址在页内偏移大于8,这一条就会跨页。所以封装EEPROM驱动时,必须自己处理分页对齐:
void EEPROM_WriteBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { while (len > 0) { uint8_t page_left = 16 - (addr % 16); // 当前页剩余空间 uint8_t chunk = (len < page_left) ? len : page_left; // AT24C16的页选择位在器件地址里,不需要左移 EEPROM_PageWrite(0xA0 | ((addr >> 8) & 0x07), addr & 0xFF, buf, chunk); DelayMs(5); // 等待内部写周期完成 addr += chunk; buf += chunk; len -= chunk; } }逐行解释:page_left算的是当前页还能写多少字节,chunk取“剩余长度”和“页剩余空间”的较小值,保证每次PageWrite不跨页写。AT24C16和普通24C02的寻址不同,前者的A2、A1、A0引脚不存在,对应的三位被用作页选择,所以器件地址不是固定0xA0,而是从addr的高字节里取页号拼进去。(addr >> 8) & 0x07就是这三位的值,直接用0xA0按位或拼接即可,不要左移。
注意:每次写周期结束必须延时等待,AT24C16内部写周期典型值3到5ms。写完后立刻读回,读出来的数据和写入不一致,多半就是延时不够。
5.4 发卡、删卡与日志追加的实现要点
卡号注册的核心是先查重后追加:
uint8_t Card_Add(uint32_t uid) { for (uint8_t i = 0; i < CARD_TABLE_MAX; i++) { if (card_table[i].valid == 0xFF && card_table[i].uid == uid) { return 2; // 已存在,不重复注册 } } for (uint8_t i = 0; i < CARD_TABLE_MAX; i++) { if (card_table[i].valid == 0x00) { card_table[i].uid = uid; card_table[i].valid = 0xFF; EEPROM_WriteBuffer(CARD_TABLE_ADDR + i * sizeof(card_entry_t), (uint8_t *)&card_table[i], sizeof(card_entry_t)); return 1; } } return 0; // 卡表满了 }第一遍循环做查重,第二遍找空位。如果只用一次循环边查边找空位,卡号表前段全是有效卡时,直到遍历结束才发现卡已存在,空位信息早就丢了,追加逻辑会变复杂,两遍更清晰。删除逻辑相反,找到uid后把valid写成0再写回。日志区用环形覆盖策略,每条约8字节,按时间戳顺序写,写满后回到起始地址覆盖最旧记录,代码里就是维护一个log_write_index,每次写完后自增并取模。
6. 部署验证与排错:串口日志和调试器连接的三个坑
6.1 把串口日志当成最小示波器
代码收尾后不要急着接电磁锁,先把USART1用115200波特率输出日志,把状态机的每次转换打出来。完整的日志链长这样:
[INFO] RC522 init OK [EVENT] card detected, UID=0x1A2B3C4D [CHECK] access granted, user=3 [ACTION] relay ON [ACTION] relay OFF after 3000ms [EVENT] card detected, UID=0x99887766 [CHECK] access denied对照日志能快速定位问题层:没有card detected是RFID驱动或接线问题;有检测但access denied是存储或比对问题;有granted但电磁锁不动是执行层问题。这一条日志链能把故障范围从整套系统缩小到几个引脚。串口初始化时要重定向fputc到USART1,并在Keil里开启Use MicroLIB,否则printf输出不完整。
6.2 三个最容易踩的坑
| 现象 | 根因 | 解决方案 |
|---|---|---|
下载时报Error: No STM32 target found! If your product embeds debug authentication,程序无法烧录 | 目标板没供电,或SWDIO/SWCLK接反;少数情况是芯片进入低功耗模式或调试口被复用 | 确认板子有独立3.3V供电;只接SWDIO、SWCLK、GND三根线再试;按住复位键的同时点击下载;仍不通就用ST-Link Utility执行整片擦除 |
| Keil5里找不到STM32F103C8Tx器件 | 只装了MDK,没装芯片支持包 | 打开Pack Installer,安装Keil.STM32F1xx_DFP,完成后Device列表出现F1系列 |
设备管理器里STM32 Virtual Com Port显示黄色叹号 | 串口驱动缺失或版本不匹配 | 安装STSW-STM32050驱动包,重新插拔USB后确认枚举出COM口 |
“No STM32 target found”是毕设季出现频率最高的一类报错。如果目标板是单独供电的,先量一下3.3V是不是真的存在,再用最短的杜邦线重新接一次SWDIO和SWCLK——很多人就是这两根线在面包板上插反了。芯片锁死的情况,用ST-Link Utility连不上时可以试试把NRST对地短接一下,边短接边连接,成功率会高不少。
6.3 收尾前的小技巧
把所有延时和超时参数集中放到config.h里:LOOP_MS、UNLOCK_TIMEOUT_MS、I2C_SPEED、SPI_PRESCALER。答辩演示时想缩短开锁时间,改一行宏定义重新编译即可,不用满工程翻硬编码。刷完新固件后用串口回读配置参数,确认实际生效,防止Keil用了旧工程缓存,烧进去的还是上一版代码。给别人部署时,把这段串口日志格式说明写进交付文档,第二个人接手排查会轻松很多。
本文还有配套的精品资源,点击获取