简介这是一套面向嵌入式初学者与课程设计需求的STM32F1智能门锁完整项目资料基于STM32F103C8T6主控整合RFID读卡器、OLED屏幕、按键、蓝牙通信、蜂鸣器与舵机等模块配套上位机与下位机双向交互方案适合作为单片机课程设计、毕业设计或电子DIY练手项目。资源包共237个文件约8.13MB以C源码、头文件、编译中间文件及工程配置为主另含hex烧录文件与Keil工程文件覆盖从源码到可执行固件的完整链路。目前已有17081人学习下载热度较高。项目功能划分清晰密码模式下支持输入开锁、错误提示、连续三次输错触发蜂鸣器报警并将密码保存至FLASH实现掉电不丢失IC卡模式可读取身份信息并完成开锁或报警上位机则负责密码设置与重置、错误报警弹窗以及IC卡用户信息的增删管理。整体方案兼顾硬件选型、软件逻辑与远程控制便于读者快速理解智能门锁的完整实现思路并二次开发。1. 从一块 F103 最小系统板到一把能刷卡的锁这套资源到底值不值得拆手头攒着几块 STM32F103C8T6 最小系统板一直想找个能把 GPIO、SPI、UART、中断和 Flash 读写串起来的综合练手项目。翻了不少 stm32 课程资源要么是点灯加串口打印的拼盘要么是直接甩一个 HAL 库工程让你自己悟。这套基于 STM32F1 的智能门锁项目不太一样——它把 RC522 读卡、舵机开锁、矩阵键盘密码、OLED 状态显示、掉电存储和远程控制指令解析揉进了一个完整流程里还配了全套视频教程。说白了它解决的是“学完外设不知道怎么组合成一个能跑的产品原型”这个断层。适合已经能独立写 GPIO 和串口收发、想往综合项目迈一步的嵌入式新手也适合需要快速搭一个门禁 Demo 去验证上位机协议的熟手。我花了一个周末把工程拆开跑通下面把选型逻辑、接线参数、代码骨架和踩过的坑一次讲清。2. 硬件选型与接线为什么是 RC522 加舵机这套组合2.1 主控与外设的匹配逻辑STM32F103 系列跑 72MHz片上 64KB Flash、20KB RAM对于门锁这种状态机加少量数据存储的场景绰绰有余。选 F1 而不是 F4 的原因很直接RC522 走 SPI 最高 10MbpsF1 的 SPI1 完全喂得饱舵机只需要一路 50Hz PWM通用定时器随便出矩阵键盘占 7 个 GPIOOLED 走 I2C 占 2 个剩下的引脚还能挂个蜂鸣器做按键反馈。整套 BOM 成本压得很低适合批量做课程设计或小规模门禁节点。常见做法是把 RC522 接 SPI1SCK 在 PA5MISO 在 PA6MOSI 在 PA7片选 NSS 用 PA4 软件控制复位 RST 用 PB0。这里有个细节——很多教程把 NSS 接硬件 SPI 的 NSS 引脚结果多机通信时片选时序对不上读卡偶尔失败。我一般强制用软件片选初始化时把 PA4 配成推挽输出通信前拉低、通信后拉高时序完全可控。舵机信号线接 PA8对应 TIM1_CH1配置成 PWM 模式周期 20ms脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。门锁场景只需要两个位置关锁 0 度、开锁 90 度所以实际占空比只在 5% 和 7.5% 之间切换。矩阵键盘用 4x4 薄膜键盘行接 PB1 到 PB4列接 PB5 到 PB8全部配成上拉输入和推挽输出交替扫描。OLED 用 0.96 寸 I2C 屏SCL 接 PB10SDA 接 PB11地址一般是 0x78 或 0x7A取决于模块背面的电阻跳线。2.2 供电与电平匹配的坑RC522 模块标称 3.3V 供电但市面上不少模块的 SPI 引脚没有电平转换直接接 5V 主控会烧。F103 的 IO 是 3.3V 电平和 RC522 直连没问题但舵机必须单独供 5V且电流要够——SG90 堵转电流能到 700mA如果和主控共用一路 LDO开锁瞬间主控会复位。血泪经验舵机电源和主控电源分开走共地即可。我在面包板上第一次跑的时候舵机一转 STM32 就重启查了半天以为是看门狗最后发现是电源跌落。接线确认后先别急着写业务逻辑。用下面这段代码把 RC522 的版本号读出来能返回 0x92 或 0x91 就说明 SPI 通了// rc522_check.c // 读取 RC522 的 VersionReg验证 SPI 通信是否正常 #include rc522.h #include spi.h uint8_t RC522_Check(void) { uint8_t version; RC522_Init(); // 初始化 SPI 和 GPIO version RC522_ReadReg(0x37); // VersionReg 地址固定为 0x37 if (version 0x92 || version 0x91) { return 1; // 通信正常 } return 0; // 通信失败检查接线和片选 }逻辑说明RC522_Init 里先拉低 RST 再拉高给模块一个复位脉冲然后配置 SPI 为模式 0、时钟分频到 4.5MHz 左右。ReadReg 的地址格式是 (reg 1) | 0x80读操作最高位置 1。参数说明0x37 是 RC522 数据手册里固定的版本寄存器地址返回值 0x92 对应 v2.0 芯片0x91 对应 v1.0两者都算正常。如果返回 0x00 或 0xFF先查 NSS 有没有拉低、MISO 有没有接反。3. 核心功能实现刷卡、密码、存储与远程指令解析3.1 RC522 寻卡与防冲突流程RC522 读卡不是一步到位标准流程是寻卡、防冲突、选卡、读写。寻卡用 PICC_REQALL 命令返回卡的类型防冲突用 PICC_ANTICOLL拿到 4 字节 UID选卡用 PICC_SELECTTAG把 UID 发给卡确认。这三步走完才能读扇区数据。很多课程资源只贴寻卡代码结果两张卡同时靠近就读不出来就是因为没做防冲突。// rc522_card.c // 完整读卡流程寻卡 - 防冲突 - 选卡 - 读 UID uint8_t RC522_ReadCard(uint8_t *uid) { uint8_t status; uint8_t buffer[5]; status PcdRequest(PICC_REQALL, buffer); // 寻卡buffer[0..1] 返回卡类型 if (status ! MI_OK) return status; status PcdAnticoll(buffer); // 防冲突buffer[0..3] 返回 UID if (status ! MI_OK) return status; memcpy(uid, buffer, 4); // 保存 4 字节 UID status PcdSelect(uid); // 选卡用 UID 锁定这张卡 return status; }逻辑说明PcdRequest 内部会拉低 NSS、发送命令字、等待中断引脚响应超时时间一般设 200ms。PcdAnticoll 返回的 UID 是低位在前比较时要注意字节序。参数说明PICC_REQALL 会寻所有卡如果只想寻未进入休眠的卡可以用 PICC_REQIDL。选卡成功后后续读写扇区就不会被其他卡干扰。3.2 舵机开锁与状态机设计门锁的状态不能乱。我一般用一个枚举把状态定死LOCKED、UNLOCKING、UNLOCKED、LOCKING、ALARM。刷卡或密码正确后状态从 LOCKED 跳到 UNLOCKING舵机转到 90 度延时 3 秒后自动跳 LOCKING 转回 0 度。如果连续 3 次密码错误跳 ALARM蜂鸣器响 5 秒并锁定键盘 30 秒。// lock_fsm.c // 门锁状态机核心逻辑 typedef enum { LOCKED, UNLOCKING, UNLOCKED, LOCKING, ALARM } LockState; void Lock_Update(LockState *state, uint8_t event) { switch (*state) { case LOCKED: if (event EVT_CARD_OK || event EVT_PASS_OK) { Servo_SetAngle(90); // 开锁 *state UNLOCKING; } else if (event EVT_PASS_FAIL) { if (fail_count 3) { Buzzer_On(); *state ALARM; } } break; case UNLOCKING: if (delay_3s_done()) { // 非阻塞延时用 systick 计数 Servo_SetAngle(0); // 关锁 *state LOCKING; } break; case ALARM: if (alarm_5s_done()) { Buzzer_Off(); *state LOCKED; fail_count 0; } break; default: break; } }逻辑说明状态机每 10ms 调一次event 由按键扫描和读卡任务置位。非阻塞延时用 SysTick 的毫秒计数做差不能用 delay_ms 死等否则键盘和读卡全卡住。参数说明fail_count 是全局变量密码正确时清零舵机角度 90 度对应开锁实际机械结构可能需要微调到 85 或 95看锁舌行程。3.3 掉电存储与远程指令解析UID 白名单和密码要存 FlashF103 的 Flash 页大小 1KB擦写寿命 1 万次左右。不能每次刷卡都写我一般只在管理员模式下修改白名单时才擦写。存储结构用简单的 TLV 格式先存一个 magic number 0x5A5A再存密码 6 字节再存白名单数量和白名单 UID 数组。上电时先读 magic不对就加载默认密码。远程控制这块项目里留了 UART 接口上位机发指令格式是CMD:UNLOCK\n或CMD:ADD:UID\n。解析时用环形缓冲区收字节遇到换行符就取出一帧用 strncmp 匹配命令头。注意别用 scanf 或 gets会阻塞主循环。常见做法是串口中断收一个字节进 buffer主循环里查有没有完整帧。// remote_cmd.c // UART 环形缓冲区收帧与命令解析 #define RX_BUF_SIZE 64 static uint8_t rx_buf[RX_BUF_SIZE]; static uint16_t rx_head 0, rx_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { // 缓冲区未满 rx_buf[rx_head] ch; rx_head next; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void CMD_Process(void) { static char line[32]; static uint8_t idx 0; while (rx_tail ! rx_head) { char c rx_buf[rx_tail]; rx_tail (rx_tail 1) % RX_BUF_SIZE; if (c \n) { line[idx] \0; if (strncmp(line, CMD:UNLOCK, 10) 0) { Lock_Update(lock_state, EVT_REMOTE_UNLOCK); } else if (strncmp(line, CMD:ADD:, 8) 0) { Add_UID_To_Flash((uint8_t *)line 8); } idx 0; } else if (idx sizeof(line) - 1) { line[idx] c; } } }逻辑说明中断里只做收字节和移动 head不做解析避免中断时间过长。主循环调 CMD_Process 逐字节取遇到换行符才解析。参数说明RX_BUF_SIZE 取 64 够用如果指令带长 UID 可以加到 128。strncmp 的长度要精确匹配命令头否则CMD:UNLOCKX也会被误判。4. 避坑与排查从读卡失败到舵机抖动的五个真实记录4.1 现象RC522 寻卡返回 MI_ERR串口打印全是 0xFF原因SPI 模式配错。RC522 要求 SPI 模式 0CPOL0CPHA0时钟空闲低电平、第一个边沿采样。很多教程用模式 3读出来全是 0xFF。另外 NSS 如果接硬件片选初始化顺序不对也会导致 MISO 一直被拉高。解决在 SPI_Init 里把 CPOL 和 CPHA 都设成 0NSS 用软件模式。初始化时先拉高 NSS再拉低 RST 延时 100ms再拉高 RST 延时 100ms最后发软复位命令 0x0F。用示波器看 SCK 和 MOSI确认第一个字节 0x80 的波形正常。4.2 现象刷卡能读到 UID但比对白名单永远不匹配原因UID 字节序搞反了。PcdAnticoll 返回的 buffer[0] 是 UID 最低位但很多白名单存储时按人类阅读顺序写成了高位在前。比如卡上印的 UID 是A1 B2 C3 D4实际读出来是D4 C3 B2 A1。解决统一在存储和比对时都用读出来的原始顺序或者写一个字节翻转函数。我一般在上位机录入 UID 时就把顺序翻好下位机不做转换减少一处出错点。4.3 现象舵机转到一半突然回弹门锁咔咔响原因PWM 占空比在状态切换时被其他定时器中断打断或者舵机电源和主控共用导致电压跌落。另外舵机从 0 度到 90 度如果直接跳变机械冲击大齿轮容易扫齿。解决PWM 更新放在定时器更新中断里保证占空比寄存器原子写入。舵机角度用渐变方式每 10ms 加 5 度分 18 步走到 90 度。电源单独供 5V主控和舵机共地但不共电源正极。4.4 现象Flash 存了白名单断电重启后数据丢了原因F103 的 Flash 写之前必须先擦除整页擦除后是 0xFF。如果直接写数据没擦除写进去的还是旧值。另外擦除时如果没关中断中断向量表可能被误擦。解决写 Flash 前先解锁擦除目标页再按半字写入。擦除和写入期间关全局中断写完再开。存储结构加 magic number 和 CRC 校验上电读取时校验失败就加载默认值避免读到半截数据。4.5 现象远程指令发过去没反应但本地刷卡正常原因串口中断优先级低于读卡任务或者环形缓冲区溢出。如果读卡任务里用了 delay_ms 死等串口中断虽然能进但主循环一直不调 CMD_Process帧就堆在缓冲区里。解决读卡任务改成状态机轮询不用死等。串口中断优先级设成比 SysTick 低但比读卡高保证收字节及时。缓冲区满了就丢弃最旧数据并在串口打印溢出警告方便定位。5. 进阶技巧把门锁日志写进 Flash 并做循环覆盖项目跑通之后我习惯加一个日志功能记录每次开锁的时间戳、方式和 UID。F103 没有 RTC 电池的话时间戳可以用上电后的毫秒数代替或者外挂一个 DS1302。日志区单独占一页 Flash每条记录 16 字节4 字节序号、4 字节时间、4 字节 UID、1 字节方式、3 字节保留。写满一页后从头覆盖序号递增这样永远保留最近 64 条记录。// flash_log.c // 循环日志写满一页后从头覆盖 #define LOG_PAGE_ADDR 0x0801FC00 // 最后一页1KB #define LOG_ENTRY_SIZE 16 #define LOG_MAX_ENTRY (1024 / LOG_ENTRY_SIZE) void Log_Write(uint32_t uid, uint8_t method) { static uint32_t seq 0; uint32_t addr; uint8_t entry[LOG_ENTRY_SIZE]; seq Log_ReadLastSeq() 1; // 读上一条序号加一 addr LOG_PAGE_ADDR (seq % LOG_MAX_ENTRY) * LOG_ENTRY_SIZE; if (seq % LOG_MAX_ENTRY 0) { // 写满一轮擦除整页 FLASH_Unlock(); FLASH_ErasePage(LOG_PAGE_ADDR); FLASH_Lock(); } *(uint32_t *)entry seq; *(uint32_t *)(entry 4) Get_Tick_Ms(); *(uint32_t *)(entry 8) uid; entry[12] method; entry[13] entry[14] entry[15] 0; FLASH_Unlock(); for (int i 0; i LOG_ENTRY_SIZE; i 2) { FLASH_ProgramHalfWord(addr i, *(uint16_t *)(entry i)); } FLASH_Lock(); }逻辑说明seq 从 Flash 里读最后一条记录的序号加一后对 LOG_MAX_ENTRY 取模得到写入偏移。写满一轮先擦整页再写保证空间循环利用。参数说明LOG_PAGE_ADDR 选最后一页是因为 F103C8T6 只有 64KB Flash前 63 页留给代码最后一页做日志。Get_Tick_Ms 返回上电后的毫秒数精度够用。读日志时按 seq 从大到小遍历遇到 0xFFFFFFFF 就停。验证方法连续刷卡 70 次断电重启用串口把日志全打出来看序号是否连续、最早的记录是否被覆盖。如果发现某条记录 UID 是 0xFFFFFFFF说明擦除后没写成功检查 FLASH_ProgramHalfWord 的返回值。从那以后我每次做带存储的项目都强制先写一个读写测试函数把整页擦掉、写满、读回、比对确认无误再往上叠业务逻辑。这个习惯帮我省了至少三次“数据丢了但不知道哪一步丢”的后悔药。希望帮到你。本文还有配套的精品资源点击获取