资讯动态

STM32+RFID宿舍门禁系统:从硬件到Android联调全解析

发布时间:2026/9/16 16:52:05 来源:尧图企业网站定制
简介基于STM32单片机与射频识别技术实现的宿舍门禁系统配套完整的安卓端手机应用源码和毕业设计资料适合嵌入式、物联网、软件工程等专业学生直接用于毕业设计、课程设计或项目初期演示。整个压缩包共包含五十六个文件体积仅一点三一兆字节其中主要文件有九个Java源文件、十七个可扩展标记语言界面与配置描述、十张图片、若干构建脚本、数字签名文件以及一个可直接安装到手机的应用程序安装包另附有打包好的工程文件压缩包目录结构安排清楚便于按需求查阅和二次开发。目前已经有一百四十八人学习下载。整套源码涵盖了从安卓客户端到STM32硬件控制端的完整通信流程射频识别刷卡、门锁控制等核心环节都有对应代码实现并配有详细的设计说明文档讲清了模块划分和验证方法既能帮助读者快速搭建门禁演示系统也能为理解手机与单片机之间的数据交互提供直观范例后续还可以扩展考勤、远程控制等实用功能。1. 为什么宿舍门禁偏偏选 STM32 单片机 RFIDAndroid APP 只做管理端宿舍门禁绝对不算复杂设备可工程上的坑一点也不少。直接在门锁旁放一个 RF 读卡器底层读卡、白名单判断交给 STM32Android 手机只做管理员配置和门禁记录查看是毕业设计里最容易被还原到真实场景的方案。它把单片机时序、RFID 协议和手机端的蓝牙权限问题切成了三块每块都可以单独调试。RC522 这类模块十几块钱就能买到STM32 的最小系统板和普通蓝牙串口模块加起来不贵正好覆盖“控制器 读卡器 手机管理端”的完整链路。这篇文章沿着 STM32 单片机、RFID 读卡链路、Android APP 源码这条线讲清楚RC522 该怎么接、白名单状态机怎么设计、手机和单片机之间的数据帧怎么定、蓝牙为什么比 WiFi 更适合演示以及联调时最容易卡住的问题。无论是准备答辩还是接手同类项目都能直接拿这套思路当骨架再往里填自己板子的引脚和协议细节。2. STM32 与 RFID 读卡硬件选型和白名单状态机门禁系统的核心不是 Android 界面而是“卡号能不能被可靠地读出来”这一环。选错读卡器型号后面所有软件工作都会白做选对之后STM32 端的代码其实就是 SPI 初始化和一张 UID 白名单。2.1 13.56MHz 的 Mifare 卡为什么是宿舍门禁默认选项宿舍场景里最常见的卡有两类125kHz 的 ID 卡和 13.56MHz 的 IC 卡。ID 卡一般只有一串固定 UID门禁终端没法往里写数据卡片丢失后只能换新卡后台也没法做延期、退宿等操作。IC 卡内部有扇区除了 UID 还能存用户身份、宿舍号、有效期所以多数学校一卡通选的是 Mifare Classic 系列也就是习惯上说的 M1 卡。RC522 是读 M1 卡最常用的一颗芯片支持 ISO/IEC 14443A通过 SPI、I2C、UART 和 MCU 通信。STM32 的 SPI 资源非常充裕常见做法是走 SPI 模式速度可控代码也好查。选型时可以先用这张表把几种常见方案框住对比项125kHz EM410013.56MHz Mifare Classic超高频 UHF典型容量64 bit UID1KB / 4KB更大容量读写功能通常只读 UID可读可写扇区可读可写读卡距离2~10cm2~8cm几十厘米到米级STM32 接入专用 125k 模块RC522 / RC523多为串口透传模组这里要特别提醒宿舍门禁用 M1 卡不代表它绝对安全只是因为它廉价、驱动库多、手机端生态好。只读卡号 UID 的方案在工程里够跑通后面第 5 章我会专门说它的风险边界。2.2 STM32 与 RC522 的 SPI 接线与初始化参数RC522 模块引脚不多和 STM32 的连接基本都是这 6 根线SDA片选、SCK、MOSI、MISO、RST外加电源地。IRQ 脚不是必需门禁这种低频读卡场景可以悬空。以 STM32F103 的 SPI1 为例常用映射是RC522 模块引脚STM32 引脚说明SDA / CSPA4软件片选低电平有效SCKPA5SPI 时钟MOSIPA7主出从入MISOPA6主入从出RSTPA8复位普通 GPIO 控制VCC / GND3.3V / GND必须共地供电不能超过 3.6V初始化时我一般先拉高片选再配置 SPI1 主模式。RC522 的标准 SPI 相位极性是 mode 0即时钟空闲为低电平、第一个边沿采样。建议先把分频系数调大例如SPI_BAUDRATEPRESCALER_64让 SPI 时钟降下来确认能读卡后再逐步提速。SPI_HandleTypeDef hspi1; void RC522_SPI_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); gpio.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, gpio); gpio.Pin GPIO_PIN_4; gpio.Mode GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, gpio); hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_64; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }这段初始化里GPIO_AF5_SPI1是 F1 系列 SPI1 的重映射别的系列要查数据手册改。片选用软件控制所以NSS配成SOFT每次通信前自己拉低、结束后拉高。CLKPolarity和CLKPhase对应 mode 0这是 RC522 数据手册里推荐的值。SPI 速率不要一上来就拉满很多“读卡失败”其实都是握手太快导致 RC522 没来得及响应。2.3 白名单状态机先读 UID 再做动作读卡端和门禁动作之间应该有一个清晰状态机而不是在 while 循环里读到卡就直接开锁。常见状态是空闲、防碰撞成功、读取 UID、校验白名单、执行开锁、上报 Android。这样可以避免同一张卡放在读卡器上时每几百毫秒触发一次开锁。当前状态触发条件动作下一状态IDLE检测到天线场内有卡调用寻卡和防碰撞CARD_READCARD_READ成功读出 UID与白名单比较CHECK_PASS / CHECK_FAILCHECK_PASSUID 命中白名单驱动继电器开锁 0.8 秒SEND_EVENTSEND_EVENT串口发送成功延时后回到 IDLEIDLE白名单最简单的方式是写一个二维数组把 4 字节 UID 直接铺开const uint8_t whitelist[][4] { {0x12, 0x34, 0x56, 0x78}, {0xAB, 0xCD, 0xEF, 0x01}, }; void DoorControl_OnCard(const uint8_t *uid, uint8_t len) { if (len ! 4) return; for (uint8_t i 0; i sizeof(whitelist) / sizeof(whitelist[0]); i) { if (memcmp(uid, whitelist[i], 4) 0) { HAL_GPIO_WritePin(LOCK_GPIO_Port, LOCK_Pin, GPIO_PIN_SET); HAL_Delay(800); HAL_GPIO_WritePin(LOCK_GPIO_Port, LOCK_Pin, GPIO_PIN_RESET); break; } } }这段代码把继电器开锁时间做成了硬延时比较简单适合演示。实际项目里更稳妥的做法是用定时器避免HAL_Delay卡住主循环。另外需要留意sizeof(whitelist) / sizeof(whitelist[0])只有在白名单数组定义在当前编译单元才能正确算出条数如果把表挪到外部 flash 文件就要显式传长度。3. STM32 与 Android 的串口蓝牙链路数据帧怎么定STM32 判定卡号有效之后Android 端必须收到“门被打开”的事件。宿舍门到管理端之间没有现成网线最常见的做法是串口蓝牙透传。重点不在蓝牙本身而是双方怎么约定一帧数据。3.1 蓝牙、BLE、串口 WiFi 怎么选手机连 STM32 的路径有三条经典蓝牙串口模块、BLE 低功耗蓝牙、串口 WiFi。宿舍场景下管理 APP 只需要在有人刷卡时收到一条事件数据量极小按可靠性排我一般会选经典蓝牙身材的透传模块比如 HC-05 或 HC-06。方案优点缺点适合场景经典蓝牙透传连接方式简单串口透明传输配对麻烦Android 权限复杂毕设演示、本地管理BLE 模块省电、连接快要写 GATT 服务和特征值调试成本高电池供电、长期在线ESP8266 WiFi可远程访问需要热点/路由器演示现场容易断多设备联网管理HC-06 一般是纯从机HC-05 可以配置主从模式毕设选 HC-05 更灵活。Android 端用传统蓝牙的 RFCOMM 通道连接UUID 固定为串口服务00001101-0000-1000-8000-00805F9B34FB。3.2 门禁事件数据帧帧头、命令和累加和串口本质上是字节流不能靠换行判断一条数据完整。如果直接用printf(card:%s)发给 Android粘包半包问题会很难查。解决办法是定一个固定长度的二进制帧。我这里用一个 10 字节帧做例子字段如下偏移字段长度值0帧头 110xAA1帧头 210x552命令10x01 读卡 / 0x02 开锁结果3结果10x00 成功 / 0x01 无权限4UID 长度145~8UID4卡号9累加和1前 9 字节相加取低 8 位STM32 端发送函数可以这样写void DoorEvent_Send(uint8_t cmd, const uint8_t *uid, uint8_t result) { uint8_t frame[10]; uint8_t sum 0; frame[0] 0xAA; frame[1] 0x55; frame[2] cmd; frame[3] result; frame[4] 4; memcpy(frame[5], uid, 4); for (uint8_t i 0; i 9; i) { sum frame[i]; } frame[9] sum; HAL_UART_Transmit(huart1, frame, sizeof(frame), 100); }这里用累加和而不是 CRC16是因为帧长只有 10 字节累加和足够发现大部分串口噪声代码也直观。HAL_UART_Transmit最后一个参数是超时毫秒数阻塞发送时 100ms 足够如果蓝牙模块波特率和 STM32 不一致手机端最多只会收到乱码不会导致发送函数阻塞。3.3 Android 端识别帧边界状态机解析而不是等换行Android 从InputStream读到的是不定长片段可能一次收到半帧也可能一次收到两帧。解析时不能用indexOf(\n)要维护一个缓冲区状态机先找0xAA 0x55再等满 10 字节最后核对累加和。class FrameParser { private val buf ByteArray(1024) private var len 0 fun push(data: ByteArray): ListDoorEvent { System.arraycopy(data, 0, buf, len, data.size) len data.size val events mutableListOfDoorEvent() var index 0 while (len - index 10) { if (buf[index] ! 0xAA.toByte() || buf[index 1] ! 0x55.toByte()) { index continue } val sum (0 until 9).fold(0) { acc, i - acc buf[index i] } if (((sum and 0xFF) (buf[index 9].toInt() and 0xFF))) { events.add( DoorEvent( cmd buf[index 2], result buf[index 3], uid buf.copyOfRange(index 5, index 9) ) ) } index 10 } if (index 0) { System.arraycopy(buf, index, buf, 0, len - index) len - index } return events } }这段代码里len是缓冲区有效长度每次收到数据先追加再循环处理完整帧。index是找帧头时的滑动窗口处理完整帧后跳 10 字节。最后把剩余字节搬到头部保证半帧不会丢。这里没有用BufferedReader因为二进制帧可能包含 0x0A、0x0D按行读会把数据截断。4. Android 端 APP 源码里的三个核心模块Android 端不用做 RFID 底层焦点应该放在三个模块蓝牙连接、帧解析、门禁记录展示。读懂这三个模块哪怕下载下来的源码命名风格很怪也能在半小时内定位到关键位置。4.1 蓝牙权限Android 6 到 13 的声明差异传统蓝牙权限是 Android 里变化最大的部分之一。Android 12 开始新增了BLUETOOTH_SCAN和BLUETOOTH_CONNECT旧的BLUETOOTH权限只对 Android 11 及以下生效。如果不做兼容经常出现手机能配对却连不上 socket 的怪问题。权限最低 API用途BLUETOOTHAPI 23~30发起连接需要BLUETOOTH_ADMINAPI 23~30扫描设备需要BLUETOOTH_SCANAPI 31扫描设备BLUETOOTH_CONNECTAPI 31连接设备ACCESS_FINE_LOCATIONAPI 23传统蓝牙扫描需要Manifest 里可以同时声明新旧权限系统会自动忽略低版本不认识的高版本权限uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /注意ACCESS_FINE_LOCATION需要运行时动态申请只写在 Manifest 里还不够。Android 系统要求蓝牙扫描结果可能与位置相关所以定位权限在 Android 11 及以下必须显式授权。4.2 用 BluetoothSocket 连接 STM32 串口蓝牙的最小代码连接 HC-05 的过程是获得已配对设备然后用 SPP UUID 建立 RFCOMM socketval uuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB) val socket: BluetoothSocket device.createRfcommSocketToServiceRecord(uuid) socket.connect() thread(start true) { val buffer ByteArray(256) while (socket.isConnected) { val len socket.inputStream.read(buffer) if (len 0) { parser.push(buffer.copyOf(len)) } } }createRfcommSocketToServiceRecord是串口服务标准做法。connect()不能放在主线程否则会触发NetworkOnMainThreadException类似的 ANR 风险。读线程里read是阻塞的返回 -1 时表示连接断开代码里最好加异常捕获并把 socket 置空。4.3 门禁记录的本地存储SQLite 还是 DataStore门禁 APP 的数据最多就是几千条记录用 Room 或 DataStore 都不算错但源码结构里最常出现的其实是直接操作 SQLite。这里我建议使用 Room因为它把 SQLiteOpenHelper 的样板代码收掉而且便于以后把日志接口换成网络上传。一张门禁记录表至少要有时间、UID、操作结果、设备名四个字段。展示最近记录时直接查昨天之后的数据SELECT card_uid, result, created_at FROM door_events WHERE created_at datetime(now, -1 day) ORDER BY id DESC LIMIT 100;datetime(now, -1 day)是 SQLite 自带的相对时间计算不需要在 Android 代码里手动拼时间字符串。ORDER BY id DESC比ORDER BY created_at DESC更可靠因为时间字段可能写入相同秒值。如果 Room 的 DAO 用Query注解把这段 SQL 原样贴进去就能跑通。5. 联调时 STM32、RFID 和 Android 的 5 个实际问题把 PCB、模块和 APP 串起来的阶段才是真正耗时的地方。下面这些问题是我在类似项目里几乎每次都遇到的按出现频率排序。5.1 ST-Link 报 error: no stm32 target found这个报错在 STM32 工程里最常见。第一步先查 SWD 四根线SWDIO、SWCLK、GND以及目标板供电。ST-Link 的 SWDIO 和 SWCLK 不能接反也不能悬空。第二步看目标板是不是自供电很多最小系统板用 USB 供电时 SWD 口也能探测到但如果 USB 座接触不良会间歇性报 no target。st-info --probe这个命令能列出 ST-Link 当前连接的芯片和 IDCODE。如果返回空说明 ST-Link 固件或 USB 驱动有问题如果能列出芯片但还是连不上优先怀疑目标板复位引脚被外设拉低。再有一种情况是芯片进入读保护需要用 ST-Link Utility 执行全擦除但这会清掉整个 flash毕设里的固件要提前备份。5.2 APP 报“RFID 数据连接错误”时先查哪里APP 提示“RFID 数据连接错误”并不代表 RFID 天线坏了。这个提示通常是手机端没在超时时间内收到串口帧问题可能出在蓝牙链路、STM32 死循环、RC522 初始化失败。排查顺序应该是先用串口调试助手连接蓝牙模块看不插卡时是否有数据再用 STM32 的调试器看MFRC522_Request返回值。RC522 读卡失败常见原因是卡片类型不对。宿舍里的 CPU 卡或身份证属于非接触式 CPU 卡RC522 只支持 ISO14443A 中的部分指令读不出来很正常。还有的 M1 卡做过加密尽管 UID 能读后面的扇区数据会一直校验失败。调试时把卡换一张空白 M1 卡是最快的排除方法。5.3 手机搜不到蓝牙模块或者连不上HC-05 上电后 LED 慢闪表示待连接状态快闪表示 AT 模式或已配对。如果手机搜不到首先看模块是否真的进入可发现模式部分模块需要按住按键上电。第二个坑是手机打开了蓝牙但定位开关没开Android 11 及以下版本会扫描不到传统蓝牙设备。模块加密默认 PIN 码通常是 1234 或 0000。连上后如果收到的全是乱码先确认两边波特率一致。常见 HC-05 出厂波特率是 9600但有的山寨模块是 38400可以用 AT 指令查看AT ATUART? ATNAME?进入 AT 模式的方法一般是按住模块按键再上电这时串口以 38400 波特率工作。ATUART9600,0,0可以改成 9600。注意改完之后STM32 的huart1初始化也必须改成同样数值否则发送的帧全部无效。5.4 晶振电容计算和 HSE_VALUE 不一致导致串口乱码STM32 的串口波特率由外设时钟分频得到如果固件里宏定义的 HSE_VALUE 与实际晶振不一致串口数据看起来就是乱码。比如板子上焊的是 8MHz 晶振程序里却写成 12MHz那么 USART 的波特率会发生约 33% 的偏差。先用逻辑分析仪量一下晶振管脚或者直接看启动文件里的时钟配置。外部晶振的负载电容需要匹配CL (C1 * C2) / (C1 C2) Cstray如果芯片数据手册要求负载电容 20pF杂散电容按 4pF 估算那么两个匹配电容各取 32pF 左右。实际工程里常用两个 33pF因为电容本身有精度误差算出来的 20.5pF 已经足够接近。代码里的HSE_VALUE一定改成实际晶振频率#define HSE_VALUE ((uint32_t)8000000)这个宏定义在stm32f1xx_hal_conf.h里而不是 main.c。很多工程从别处复制过来晶振换成了 25MHz宏定义忘了改结果就是 USB 枚举失败和串口乱码一起出现。5.5 只读 UID 的门禁方案有什么风险毕业设计里最常被评委问的问题是只读 UID 的 Mifare 卡能防复制吗答案是不能。UID 是公开的M1 卡的 UID 甚至不需要密钥就能读出来普通 NFC 手机也能模拟。只读 UID 适合做功能演示但论文里必须写明它的安全边界。更稳妥的做法是选一个扇区比如扇区 2用密钥把宿舍编号写进去读卡时先验证扇区数据再开锁。这样即使 UID 被复制缺少密钥或扇区数据也过不了校验。不过要提醒不要在网上找复制卡的教程写进论文也不要在演示时展示复制过程这会直接影响答辩评价。合理的方式是给出一个“UID 扇区验证”的状态机设计并在论文里说明密钥管理由服务器负责。6. 把毕业设计从“能跑”变成“可展示”的收尾技巧功能跑通只是起点答辩或演示时真正加分的是“稳定可复现”。下面这三个技巧能让系统在别人手里也能顺畅演示。6.1 用 Python 脚本在 PC 上先验证 Android 解析帧每次烧录固件验证解析器非常慢。更好的做法是先用 PC 串口发模拟帧给 Android APP 喂数据。只要把 HC-05 换成 USB 转 TTL接在电脑上就能用 Python 把门禁事件帧发出去import serial port serial.Serial(COM3, 9600, timeout1) frame bytearray.fromhex(AA 55 01 00 04 12 34 56 78 5E) port.write(frame)AA 55 01 00 04 12 34 56 78 5E这一帧的 UID 是12 34 56 78命令是 0x01结果是 0x00。末尾的5E是前 9 个字节的和0xAA 0x55 0x01 0x00 0x04 0x12 0x34 0x56 0x78 0x25E取低 8 位得到0x5E。这样可以在不触发继电器的情况下验证手机端蓝牙解析和 UI 刷新。6.2 给门禁记录加一条可展示的 SQL 查询演示时经常要打开手机展示“今天有谁刷过卡”。Room 的 DAO 里可以直接写时间范围查询SELECT card_uid, CASE WHEN result 0 THEN 允许 ELSE 拒绝 END AS status, created_at FROM door_events WHERE created_at BETWEEN :startTime AND :endTime ORDER BY id DESC把startTime和endTime用LocalDate.now()转换成当天的起始和结束时间传进去。这样演示时不用翻几十条旧记录现场就能看到刚刷的卡按时间倒序出现在第一行再配上 RecyclerView 的刷新动画观感会明显好。6.3 把蓝牙断线重连加进主界面可见回调演示现场最大的不确定因素是蓝牙模块断电或手机走进干扰区。建议在主界面的onStart里检查连接状态断开时自动重连最后一次成功的设备而不是让用户去设置页手动点连接override fun onStart() { super.onStart() if (!bluetoothService.isConnected()) { reconnect(lastDeviceAddress) } }reconnect()里要先把旧 socket 关闭再创建新的 RFCOMM socket否则第二次connect()可能抛出Socket is closed异常。注意重连间隔至少留 500ms避免 HC-05 还在恢复时就发连接请求。这个细节放在答辩现场非常有用评审老师看到的会是一个重新打开 APP 就能自动恢复的完整系统。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价