STM32+RFID+蓝牙智能门锁开发全攻略:从选型到联调
2026/9/14 21:47:20 网站建设 项目流程

简介:这是一份基于STM32+RFID+蓝牙的智能门锁系统项目,集成指纹识别、人脸识别、RFID刷卡、密码解锁及蓝牙解锁五种开锁方式,功能覆盖较全,适合毕业设计、课程设计、工程实训、项目开发及嵌入式竞赛等场景。压缩包共187个文件,大小约27.73MB,核心为86个.h头文件与74个.c源文件组成的STM32裸机工程,涵盖驱动、外设模块和主逻辑;同时配有Keil工程文件、PDF说明、HTML/Markdown使用文档及TXT说明等,目录结构清晰,方便按需查看、编译与烧录。目前已有307人浏览学习。资源内包含可直接编译的完整工程与源码,配合说明文档,可快速复现智能门锁功能;拿到后既可按引脚定义用面包板+杜邦线方式连接模块验证,也可在原架构上扩展更多功能,适合需要高效落地的单片机开发者。无论新手还是进阶开发者,均可依据包内文档快速定位问题,节省调试时间。

1. 从毕设题目到完整产品链路:这套门锁到底在做什么

很多人的毕设选题是“基于STM32+RFID+蓝牙设计的智能门锁系统”,但真正动手后发现它并不是“把三个模块插在一起”那么简单。它背后是一条完整的嵌入式链路:RFID负责近场刷卡鉴权,蓝牙负责远程指令通道,STM32作为决策中枢统一处理读卡事件、指令解析、继电器控制和状态反馈。三个器件单独拿出来都容易跑通,联调才是分水岭——电平匹配、电源纹波、通信时序、嵌入式状态机设计,任何一个环节出问题,门锁就表现为“没反应”“误开锁”或者“开机白屏”。这篇文章会从选型开始,逐步讲到硬件接线、驱动移植、业务协议和排错技巧,目标是让你在实验室里能完整复现,并且答辩时讲得出每个参数为什么这样设。

2. 器件选型和系统架构:为什么是STM32,RFID和蓝牙各承担什么角色

2.1 系统拓扑:三个模块的分工边界

这套门锁系统的信息流非常简单:RFID 模块通过 SPI 接口把读到的卡号上报给 STM32,STM32 查询预存的授权卡表,匹配成功则拉高继电器控制引脚,驱动电磁锁开锁;同时蓝牙模块通过串口接到 STM32,手机端发送指令可以远程开门、添加卡片、删除卡片。两路输入共用同一套决策逻辑,这意味着你要在代码里定义一个清晰的状态机,而不是在 while(1) 里写两段轮流判断的程序——否则会出现“刷卡读到一半时蓝牙指令插进来,门锁直接卡死”的问题。

整个系统的关键点在于:门锁是一个实时性要求较高的事件驱动系统,读卡事件的发生是随机的,用户在门外不会等到你系统忙完才刷卡。所以 STM32 的主循环必须是非阻塞式的,RFID 轮询和蓝牙接收都要用中断加标志位的方式去处理,而不是靠 delay 延时去等待。

2.2 STM32 选型参数:C8T6 还是 RBT6

最常见的方案是 STM32F103C8T6,原因是生态成熟、教程多、价格便宜,而且 20KB 的 RAM 和 64KB 的 Flash 对于门锁这个场景完全够用。如果你计划在门锁里加掉电保存、OTA 升级、或者 LCD 屏幕显示菜单,那么建议直接上 STM32F103RBT6,Flash 翻倍到 128KB,就不用中期换型。

从外设资源角度看,这套方案需要 1 路 SPI、1 路 USART、1 个外部中断引脚、3~4 个普通 GPIO,C8T6 的资源配置绰绰有余。要注意的是,蓝牙模块建议挂在 USART1 上,因为 USART1 的时钟来自 APB2,理论上可以跑到较高波特率,后面如果改 BLE 模块更从容;RC522 挂 SPI1,时钟同样来自 APB2,读卡事务响应更快。

2.3 RFID 读卡模块:RC522 与 MFRC522 的细节差异

市面上大量“RC522 模块”实质上用的都是 NXP 的 MFRC522 芯片,之所以叫 RC522 是因为早期国内厂商直接印了 RC522 的丝印。它支持 ISO/IEC 14443A 协议,工作频率 13.56MHz,通信接口有 SPI、I2C 和串行 UART 三种,绝大多数模块把它们引出为排针,板上有跳线或电阻选择。我们使用 SPI 模式,接口参数如下表:

参数典型值说明
工作电压3.3V模块板载稳压,也可 5V 供电但逻辑电平不稳
最大功耗约 20mA读写操作时会有起伏
SPI 频率建议 ≤ 4MHz频率过高容易读错数据
读卡距离2~5cm取决于天线线圈和电源质量
支持卡型Mifare 1K / 4K即常见的门禁卡

读卡距离是个容易被忽略的参数。模块供电电压低、天线周围有金属、或者杜邦线过长都会导致读卡距离缩短到 1cm 以内。如果遇到这种情况,优先检查电源,而不是怀疑代码。

2.4 蓝牙模块:经典 SPP 的 HC-05 与 BLE 方案对比

蓝牙部分的选择会直接改变手机 App 的开发量。最省事的方案是 HC-05,它实现的是蓝牙 2.0 + EDR 下的 SPP 协议(串口透传),手机装一个“蓝牙串口助手”App 就能直接收发数据,不需要自己写 Android 蓝牙 Socket 层。它的供电电压范围在 3.6V 到 6V 之间,5V 供电后板载稳压输出给芯片,因此和 STM32 的串口通信存在电平不对称的问题,最好用分压电阻把 HC-05 的 TX 降到 3.3V。

如果想走 BLE(低功耗蓝牙)路线,需要选择 JDY-08、nRF52832 这类模块,但对应的手机端必须自己开发小程序或 App,开发周期至少多两周。对于毕设和课设来说,时间是硬约束,HC-05 能以最低成本覆盖“远程开锁”这个演示点。唯一的坑是:HC-05 的默认波特率一般是 9600,但有些卖家出厂改成 38400 或 115200,买回来第一步是用 AT 指令查清楚。

2.5 电源与门锁执行机构:继电器加电磁锁的常见翻车点

门锁执行机构有两种常见选型:电磁锁(也叫电插锁)和舵机驱动的机械锁。电磁锁工作电流较大,通常需要 12V 或 24V 供电,不能直接从 STM32 的 3.3V 取电,必须用继电器隔离。毕设演示可以先用一个 5V 的微型电磁锁(工作电流 1A 以内),配合 SRD-05VDC-SL-C 继电器,STM32 的 GPIO 输出高电平驱动三极管(如 S8050)去导通继电器线圈。

系统供电可以统一采用 12V 适配器输入,经过降压模块得到 5V 给门锁和蓝牙,再经 AMS1117-3.3 给 STM32 和 RC522。但注意 AMS1117 的输入输出压差不能太大,12V 直接降到 3.3V 会导致芯片发热严重,长期工作不安全,中间必须用 DC-DC 降压到 5V 再进入 LDO。

这里有一个接线细节:继电器线圈两端必须反向并联一个 1N4007 二极管。继电器断开瞬间会产生感生电动势,方向与电源相反,如果没有续流二极管,这个反向尖峰可能直接击穿三极管甚至损坏 STM32 的引脚。不少人的门锁在开锁几次后就失灵,原因不是代码,而是继电器缺少续流保护。

3. STM32 驱动层实现:GPIO、SPI、串口配置与最小驱动验证

3.1 引脚规划表和初始化代码

先把引脚规划写死在代码注释里,这是联调时最省事的做法。下面是一份完整的引脚分配表:

功能引脚说明
RC522 SPI1 SCKPA5时钟输出
RC522 SPI1 MISOPA6模块到 MCU 数据
RC522 SPI1 MOSIPA7MCU 到模块数据
RC522 NSSPA4片选,低有效
RC522 RSTPA3复位信号
HC-05 TXDPA10接 STM32 USART1 RX
HC-05 RXDPA9分压后接 STM32 USART1 TX
继电器控制PB0高电平开启继电器
蜂鸣器/指示灯PB1可选状态输出

初始化代码用标准外设库写,老项目移植最容易的是“库函数版本”,新工程建议直接用 STM32CubeMX 生成底片,再把应用层代码叠上去。我这里给出关键初始化片段,适配的时钟是 72MHz:

void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 使能相关 GPIO 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); /* PB0 推挽输出,控制继电器 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); /* PB1 推挽输出,控制蜂鸣器 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); PA2_3_Config(); /* PA2 为 RC522 复位引脚输出,PA3 为继电器 此处略去重复的 RCC 与 GPIO 结构体赋值 */ }

SPI1 配置为主机模式,数据帧 8 位,时钟极性 CPOL=0、相位 CPHA=0,这是 MFRC522 兼容模式所要求的:

void SPI1_Config(void) { SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

注意 SPI 预分频取 8,72MHz 主频下对应的 SCLK 是 9MHz,虽然 RC522 手册标称支持到 10MHz,但实际走杜邦线时高频信号衰减明显,建议保持预分频 8 或 16。代码里的SPI_NSS_Soft表示软件控制片选,也就是把 PA4 当成普通 GPIO 拉低拉高,而不是交给 SPI 外设硬件管理。这样做的好处是片选时序控制更直接,读卡轮询失败时能用逻辑分析仪单步排查。

3.2 MFRC522 的移植:把读卡程序拆成四个函数

网上流传最广的 MFRC522 库有几百行,核心逻辑其实可以拆成四个函数:复位、写寄存器、读寄存器、读卡号。第一次移植时不要贪多,先把“能不能读到卡号的 UID”作为验收标准。

unsigned char MFRC522_Read(unsigned char reg) { unsigned char value; CS_LOW(); SPI_SendByte(0x80 | ((reg << 1) & 0x7E)); // 读命令,地址左移一位 value = SPI_SendByte(0x00); CS_HIGH(); return value; } unsigned char MFRC522_Write(unsigned char reg, unsigned char value) { CS_LOW(); SPI_SendByte((reg << 1) & 0x7E); // 写命令,最高位为 0 SPI_SendByte(value); CS_HIGH(); return SUCCESS; }

读函数里命令字节的最高位为 1,写函数最高位为 0,地址之所以要左移一位,是 MFRC522 的命令格式规定的:命令字节由 1 位读/写标志位 + 6 位寄存器地址 + 1 位保留位组成。这一点很多新手会搞错,直接按网上抄来的地址对照表操作但结果全是 0xFF,十有八九就是移位逻辑不对。

读卡号的核心是请求寻卡和防碰撞两步。平常说的“读卡号”指的是读 M1 卡 4 字节的 UID,不是读卡内扇区数据。完整代码如下:

unsigned char MFRC522_Request(unsigned char reqMode, unsigned char *tagType) { unsigned char status; unsigned int backBits; MFRC522_Write(MFRC522_REG_BIT_FRAMING, 0x07); tagType[0] = reqMode; status = MFRC522_ToCard(PCD_TRANSCEIVE, tagType, 1, tagType, &backBits); if ((status == MI_OK) && (backBits == 0x10)) { return MI_OK; } return MI_ERR; } unsigned char MFRC522_Anticoll(unsigned char *serNum) { unsigned char status; unsigned char i; unsigned char serNumCheck = 0; unsigned int unLen; MFRC522_Write(MFRC522_REG_BIT_FRAMING, 0x00); serNum[0] = PCD_ANTICOLL; serNum[1] = 0x20; status = MFRC522_ToCard(PCD_TRANSCEIVE, serNum, 2, serNum, &unLen); if (status == MI_OK) { for (i = 0; i < 4; i++) { serNumCheck ^= serNum[i]; } if (serNumCheck != serNum[4]) { return MI_ERR; } } return status; }

MFRC522_Request里的reqMode传入 0x26(寻卡请求),返回的tagType在这个模式下无实际意义,主要作用是确认卡片进入天线场区。MFRC522_Anticoll中的防碰撞命令 0x93 是“级联级别 1”,针对 UID 长度为 4 字节的标准 M1 卡。最后一个字节是校验值,计算方法是前 4 字节逐位异或,这也是代码里serNumCheck的作用——如果校验不过,宁可丢弃这帧数据重读,也不要进入后续流程。

3.3 蓝牙 HC-05 的接线和 AT 指令配置清单

HC-05 模块有 6 个排针引脚,其中 STATE 状态脚可以直接接到 STM32 的 GPIO 用来判断蓝牙是否已连接,但最简单可靠的连接方式是把模块的 TXD 接到 STM32 的 PA10(USART1 RX),模块的 RXD 经过两个电阻分压后接到 PA9(USART1 TX)。分压比例是 10K 和 20K,得到 5V 的三分之二约 3.3V,直接对接有烧坏模块的风险。

模块默认处于透传模式,想进入 AT 命令模式要在上电前按住模块上的按键,再给模块供电。这时模块以 38400 波特率运行(HC-05 的 AT 模式固定 38400,而透传模式波特率由 AT+UART 指令决定),用 USB 转 TTL 连接电脑串口助手,发送以下指令逐条确认:

AT 指令预期返回说明
ATOK通信正常
AT+NAME=SmartDoorOK修改蓝牙名称为 SmartDoor
AT+PSWD=1234OK配对密码
AT+UART=9600,0,0OK设置透传波特率 9600,停止位 1,无校验
AT+ROLE=0OK设置为从机模式

其中AT+ROLE=0很关键,如果模块被配置成主机模式,它不会等待手机连接,而是主动去搜索配对,门锁收不到任何指令。另外注意,LED 指示灯的闪烁频率也能判断状态——未连接时快速闪烁,连接成功变成慢闪,这个特征在联调时要比看串口日志更直觉。

3.4 蓝牙透传数据的最小验证程序

配置好模块后,写一段最小程序验证“手机发什么,STM32 串口中断收什么”。这道验证优先级最高,因为后续的指令解析协议全靠这条链路:

void USART1_IRQHandler(void) { uint8_t rx_data; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { rx_data = USART_ReceiveData(USART1); ring_buffer_push(rx_data); // 写入环形缓冲区 rx_flag = 1; // 通知主循环处理 } }

ring_buffer_push是自实现的环形缓冲写入函数,缓冲区大小为 64 字节。使用环形缓冲而不是直接在中断里解析指令,是为了防止长数据帧在解析过程中被新数据覆盖。主循环只需要轮询rx_flag,读到后把缓冲区内容原样通过 USART1 发回去,手机串口助手能看到回显即代表链路打通。这一步通过之后再往上叠指令解析逻辑,不要一上来就写完整协议。

4. 业务逻辑层:门锁状态机、鉴权流程与指令协议设计

4.1 状态机建模:从 IDLE 到 OPENING 的六种状态

嵌入式门锁系统最忌讳的写法是在主循环里堆 if-else。刷卡、蓝牙指令、超时、故障这些事件交织在一起时,状态机是唯一能保证行为可预测的建模方式。我把门锁设计为六个状态,每个状态有明确的进入条件和触发事件:

状态说明迁出条件
IDLE待机,RC522 持续寻卡检测到卡片 / 收到蓝牙指令
READING正在读取卡片 UID 和校验读取成功或超时
AUTHING查询卡号是否在授权表中命中或未命中
OPENING继电器吸合,等待开门超时延时结束
FAIL鉴权失败,蜂鸣器响延时结束
BT_CMD处理来自蓝牙的指令指令执行完成

状态机用 switch-case 实现,状态迁移放在主循环中执行,代码结构如下:

void DoorFsm_Process(void) { switch (door_state) { case IDLE: if (MFRC522_Request(PICC_REQIDL, g_tagType) == MI_OK) { door_state = READING; } else if (bt_cmd_ready_flag) { door_state = BT_CMD; } break; case READING: if (MFRC522_Anticoll(g_uid) == MI_OK) { g_uid_len = 4; MFRC522_Halt(); door_state = AUTHING; } else { door_state = IDLE; // 读卡失败,立即回到待机 } break; case OPENING: case FAIL: case BT_CMD: // 见后续小节,按各自延时和结果处理 break; } }

注意READING状态下如果防碰撞失败,不要停留在当前状态反复重试,直接回到IDLE重新寻卡。原因是若卡片没放好或者抗碰撞失败,继续读下去只会占用系统时间,而用户在门外已经重新刷卡了。

4.2 卡号存储方案:内部 Flash 掉电保存与卡容量规划

鉴权的核心数据是授权卡号集合。存储位置两个选择:一是用 STM32 内部的 Flash 最后一页,二是外接 AT24C02 存储器。对门锁场景,卡号只是一个 4 字节数组,一张表 20 张卡也才 80 字节,用内部 Flash 完全足够,省一个器件并减少一个 I2C 硬件排查点。

用内部 Flash 的注意事项是它的擦写寿命只有约 1 万次,而且删除整页重写速度慢。常见做法是每次修改卡表时执行“擦除一页(1KB)再写回全部数据”,平均耗时约几十毫秒,开机可以接受。但不能在掉电瞬间执行写入,因为电压跌落会导致 Flash 写入失败、数据损坏。正确方案是在AUTHING状态确认“添加卡片”指令后,先写 Flash 再回 ACK,而不掉电处理。

void Flash_EraseAndWriteCardTable(uint8_t *card_list, uint8_t count) { uint32_t addr = CARD_TABLE_BASE_ADDR; // 0x0800FC00,最后一页起始 FLASH_Unlock(); FLASH_ErasePage(CARD_TABLE_BASE_ADDR); FLASH_ProgramHalfWord(addr, (uint16_t)(count & 0xFF)); for (uint8_t i = 0; i < count; i++) { addr += 2; FLASH_ProgramHalfWord(addr, (uint16_t)(card_list[i * 4] << 8 | card_list[i * 4 + 1])); addr += 2; FLASH_ProgramHalfWord(addr, (uint16_t)(card_list[i * 4 + 2] << 8 | card_list[i * 4 + 3])); } FLASH_Lock(); }

Flash 地址0x0800FC00位于 64KB Flash 的最后一页,需确保这段空间没有被编译后的代码占用。你可以查看生成的.map文件确认程序体积未超过 63KB,否则写入会破坏代码段。FLASH_ProgramHalfWord一次写半个字(16 位),因为 STM32F103 的 Flash 不支持按字节写入,最小单位是半字。上面代码把两个字节拼成一个半字写入,读取时反解即可。注意每次上电都要从 Flash 读出卡表到 RAM,主循环中的鉴权操作只在 RAM 里进行,避免频繁访问 Flash 外设。

4.3 蓝牙指令协议:帧头、长度、命令码与校验

蓝牙链路不可靠,手机 App 发送的数据可能粘包和断包。设计一个带帧头和校验的指令协议是门锁系统能否稳定工作的关键。常见做法是定义 5 字节的短帧:1 字节帧头 0xA5,1 字节命令码,2 字节数据(放卡号高 8 位和低 8 位),1 字节校验(全部字节求异或)。

typedef enum { CMD_UNLOCK = 0x01, // 远程开锁 CMD_LOCK, // 远程闭锁 CMD_ADD_CARD, // 添加卡片, data 字段为卡号 CMD_DEL_CARD, // 删除卡片 CMD_QUERY_STATE // 查询门锁状态 } BT_CMD_t;

接收端在串口中断中每次只收一个字节,放入缓冲区后由主循环调用协议解析函数,按状态机方式逐字节匹配:第一个字节必须是 0xA5,否则丢弃并重新搜索帧头。收到第 5 字节后校验异或结果,通过则把帧内容放入待处理队列。

这个过程不使用串口空闲中断来断帧,是因为 HC-05 的透传数据流没有很可靠的时间间隔语义,且部分手机端 App 发送数据时可能会分包。以固定长度帧 + 等待填充超时的策略更稳妥,比如每收到一字节启动 50ms 看门狗定时器,超时未收满即丢弃当前帧。以下是解析函数的核心:

uint8_t ble_rx_buf[5]; uint8_t ble_rx_index = 0; void BLE_ParseByte(uint8_t byte) { if (ble_rx_index == 0 && byte != 0xA5) { return; /* 寻找帧头 */ } ble_rx_buf[ble_rx_index++] = byte; if (ble_rx_index == 5) { uint8_t checksum = 0; for (uint8_t i = 0; i < 4; i++) { checksum ^= ble_rx_buf[i]; } if (checksum == ble_rx_buf[4]) { BLE_ProcessCommand(ble_rx_buf[1], (ble_rx_buf[2] << 8) | ble_rx_buf[3]); } ble_rx_index = 0; memset(ble_rx_buf, 0, sizeof(ble_rx_buf)); } }

校验算法选择异或而不是 CRC16,是因为门锁指令长度短、错误概率低,而 CRC16 的计算代码对毕设项目来说增加了阅读负担。当然如果有额外的传感器数据要上报,帧长度变长时建议换成 CRC8 或 CRC16,异或的抗干扰能力在长帧下弱一些。BLE_ProcessCommand内部也是一个 switch 语句,对CMD_ADD_CARD这类需要 Flash 操作的命令,可以置一个pending_flash_op标志,回到主循环时执行,避免在中断上下文里直接调用 Flash 写函数。

4.4 鉴权失败策略:连续五次失败锁定读卡器

门锁系统的保护逻辑不只是“鉴别卡号”,还包括防止暴力枚举。在普通 M1 卡的 UID 空间(4 字节)中穷举虽然不现实,但有人会拿着大量空白卡一一试,这时需要在固件中加入失败计数:连续 5 次未通过鉴权后,门锁进入 30 秒锁定状态,在此期间不响应任何读卡操作和蓝牙开锁命令,只能等待系统计时结束。这个策略同时防范了“拿着手机蓝牙连续尝试”的滥用,是门锁产品化的底线功能。

uint8_t fail_count = 0; uint16_t lock_remain_time = 0; void Auth_FailHandler(void) { fail_count++; if (fail_count >= 5) { lock_remain_time = 30000; /* 单位毫秒,30 秒锁定 */ door_state = LOCKED; /* LOCKED 为新状态,不响应读卡 */ } else { door_state = FAIL; } }

LOCKED状态的主循环只做一件事:递减lock_remain_time,到 0 后把fail_count清零并回到IDLE。注意锁定状态必须单独设在状态机里,如果用FAIL状态带超时替代,会在短暂蜂鸣后恢复待机,无法实现真正的时间窗隔离。演示时也可以通过蓝牙发送CMD_QUERY_STATE获取门锁状态码,方便答辩时展示异常处理逻辑。

5. 联调方法、常见故障定位与可演示的进阶功能

5.1 串口日志分级:黄灯的问题用打印而不是猜测

门锁系统是黑盒设备,没有串口日志就没有联调效率。我建议把所有调试信息从 USART2 输出,波特率 115200,用一根 USB 转 TTL 线连接电脑。日志分为三级:LOG_ERR打印状态机异常迁移、读卡超时、Flash 写入失败;LOG_EVT打印刷卡成功、开锁动作、蓝牙指令接收帧;LOG_DBG默认关闭,需要调试时用宏打开。这样一个体系的好处是,你在现场演示时不会被大量调试杂讯干扰,系统出问题时又能快速定位到异常产生的位置。

#define LOG_LEVEL 2 void DoorLog(uint8_t level, const char *fmt, ...) { if (level > LOG_LEVEL) return; printf("[%s] ", level == 0 ? "ERR" : level == 1 ? "EVT" : "DBG"); /* 可变参数格式化后通过 USART2 发送 */ }

这里printf需要重定向到 USART2,方法是在工程里实现fputc函数向发送寄存器写数据。注意如果使用 Keil MDK,需要在工程选项里勾选 “Use MicroLIB”,否则printf会导入完整 C 库导致 Flash 空间紧张。日志输出本身会阻塞 CPU,所以真正的产品代码会把它关掉或改成 DMA 发送。

5.2 三种典型故障的定位路径

故障现象一:刷卡无反应,RC522 的 LED 不闪。优先检查天线场区是否存在。把手机开启 NFC 检测功能,贴近模块天线先确认射频场是否在;如果手机能感应到模块发出的场,说明硬件供电和天线匹配没问题,问题在 SPI 通信层。用逻辑分析仪抓 SCK、MOSI、MISO,在MFRC522_Request调用时应该能看到约 30 个时钟周期的波形。如果 SCK 有波形而 MISO 一直为高,说明 RC522 未进入 SPI 从机模式,最可能是 NSS 引脚时序不对,排查 CS 是否拉低。

故障现象二:蓝牙连上了但发送指令无响应。先通过 HC-05 的 STATE 引脚(高电平表示已连接)确认链路状态,然后检查手机端发送的字节是否包含\r\n换行符,有些 App 默认会在数据后加换行,而协议解析器对多余字节兼容性差,会破坏帧结构。可以临时用循环回环测试——把 STM32 收到的原文发送回蓝牙,看手机是否收到,若收不到则检查分压电路电平是否符合预期。

故障现象三:继电器接通但门锁不动作。按照 5V 继电器 + 12V 电磁锁的接线,常见问题有:继电器触点电流小于门锁工作电流,需要查看门锁标称的启动电流是否超过继电器额定值;续流二极管方向接反,导致继电器全时吸合或烧毁三极管;电磁锁本身需要一个上电冲击电流,而电源适配器容量不足触发限流保护,此时更换 2A 以上的 12V 适配器。

5.3 可演示的进阶:手机小程序代替串口调试助手

如果你展示时不想用“蓝牙串口助手”这种工业味太重的界面,可以花一个周末写一个微信小程序。小程序端使用微信的蓝牙 API,扫描到 HC-05 后连接、写在特征值、监听通知回调。但注意微信小程序只支持 BLE(低功耗蓝牙),并不兼容 HC-05 的经典蓝牙 SPP 协议。要在小程序里演示,需要把硬件换成支持 BLE 的 AT 指令蓝牙模块(如 JDY-08),并把 STM32 的串口初始化保持不变,因为 JDY-08 同样以串口透传的方式和 MCU 通信,底层协议差异被模块封装掉了。

这个改动只影响硬件选型,上报指令协议完全复用上一章设计的帧结构。小程序端收到鉴权结果后展示“门锁已开启,请推门进入”的动画,演示效果远好于电脑串口。如果你有时间,可以在固件里增加一个 0x06 命令码上报当前状态(开/关/锁定中),小程序首页轮询显示,这是评委最容易留下印象的一个点。

5.4 读卡距离缩水的排查次序

RC522 读卡距离低于 2cm 时不要直接怀疑芯片坏,按顺序排查:先测模块 VCC 电压,正常 3.3V 条件下读卡距离应在 4cm 左右,如果降到 2.5V 以下则读卡距离会骤降到贴近天线才感应的水平;再检查天线是否被外壳金属遮挡,天线周围 1cm 内不能有金属和铜柱;最后查天线走线谐振电容是否匹配,这种情况只发生在自制天线时,买成品模块不用管。如果只是因为杜邦线太长导致 MISO 信号质量差,把 SPI 预分频从 8 调整到 16,或换短线(小于 10cm 的杜邦线),能立竿见影。最终确认读卡距离的测量方法是用一把塑料直尺,从天线表面为 0 点垂直测试卡片能被感应的最大距离,而不是凭手感。

5.5 参数速查:一份可以贴在工位上的配置表

项目做到最后,很多时间花在“忘记了波特率”“找不到引脚定义”这种琐碎事上。把关键配置集中在一个表格里,连同固件版本号一并放到代码文件的头部注释里:

配置项备注
系统主频72MHzPLL 倍频,外部 8MHz 晶振
RC522 SPI 速率9MHz(预分频 8)信号不佳时改预分频 16
USART1(蓝牙)波特率9600与 HC-05 透传波特率严格一致
USART2(日志)波特率115200仅调试使用
继电器闭合时间2000ms超时自动断电,避免持续吸合
失败锁定阈值5 次 / 30 秒连续鉴权失败后锁定
授权卡上限20 张受 Flash 页大小限制,可扩展

最后一条值得多说一句:授权卡上限 20 张不是 Flash 存储不了更多,而是卡表每次变更需要整页擦除重写,卡越多擦写时间越长。如果项目扩展需求增加,可以预留 4KB 的 Flash 区域存卡表,容量扩大到 100 张卡,但代码里必须保证卡表结构体对齐,否则从 Flash 读回的数据字节序会错位。调这类问题时多看一眼结构体声明中的#pragma pack设置,比反复清 Flash 重烧更快。

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

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

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

立即咨询