51单片机蓝牙密码锁完整实现:HC-05透传与AT24C02掉电保存
简介这套基于51单片机的蓝牙密码锁学习资源面向电子设计竞赛选手、单片机初学者及智能门禁DIY爱好者。内容以完整实物项目为主线提供原理图、PCB、程序源码、系统框图、软件流程图、元器件清单与文档资料并配套视频讲解便于从硬件搭建到程序烧录全流程参考。压缩包共72个文件覆盖C语言源码、电路设计文件、芯片手册、演示视频、Excel清单及烧录工具等整体约98.91MB目录分类清晰适合分模块检索。已有1914人学习下载。项目重点展示矩阵键盘、DS1302时钟、DS18B20测温、AT24C02存储和蓝牙模块的协同使用可实现密码输入、重新设置、一键关门、时间显示等逻辑并能了解EEPROM数据掉电保存、串口通信与手机端蓝牙调试方法是巩固51单片机外设驱动与系统联调能力的实用参考。1. 基于51单片机蓝牙密码锁从HC-05透传到掉电保存的完整实现做一个基于51单片机的蓝牙密码锁核心不是把蓝牙打通而是把“密码校验”这个状态机设计清楚。STC89C52这类增强型51内核芯片主频12MHz时单周期指令约1µs跑一个轻量级状态机绰绰有余HC-05蓝牙模块负责透明传输手机端用任意串口工具就能替代专用APP省去安卓和iOS双端适配的麻烦。这篇文章按硬件选型、AT指令配置、密码存储、状态机流转、安全加固的顺序给出一个可直接抄作业的方案重点放在密码不变式、EEPROM掉电保存和防暴力破解这三件事上。需要提醒的是51系列没有内部EEPROM的型号必须外挂AT24C02如果选STC15W408AS这类带内部EEPROM的芯片电路还能再精简两层。适合的人群是正在做课程设计或入门嵌入式安全的开发者已经会点亮LED、会跑串口收发但对蓝牙模块配置和密码认证体系还不熟悉。下面的方案默认你用的是STC89C52RC加HC-05硬件上只需一个USB转TTL模块烧录程序手机端装“蓝牙串口助手”这类工具就能完成全部验证。2. 蓝牙密码锁的硬件选型与引脚分配51单片机怎么接HC-05和矩阵键盘2.1 为什么HC-05比HC-06更适合做密码锁HC-05和HC-06最本质的区别是角色切换能力。HC-05支持AT指令配置主从模式既能当从机被手机连接也能当主机主动连接其他蓝牙设备HC-06则是固定从机上电就只能被搜索。对密码锁场景HC-05的另一个隐藏优势是支持绑定指定蓝牙地址可以限制只有特定手机能连接这对课程设计答辩时的“安全性”演示非常有用。HC-05的典型工作状态有两套指示灯逻辑上电后LED快闪约2Hz表示未配对慢闪约0.5Hz表示已连接。硬件上需要注意HC-05的TXD和RXD是3.3V电平51单片机的P3.0RXD和P3.1TXD是5V电平两者直接相连虽然多数情况下能工作但长时间运行有烧毁蓝牙模块的风险。稳妥做法是在HC-05的RXD串联一个1kΩ电阻分压或者用两个三极管搭电平转换电路。2.2 密码锁按键输入矩阵键盘与独立键盘的取舍矩阵键盘省I/O口4x4只用8个引脚就能读16个按键独立键盘占用引脚多但程序简单。从实际使用体验看密码锁至少需要0-9十个数字键、确认键、退格键、修改密码键一共13个功能独立键盘需要13个I/O口对51单片机来说压力不小。4x4矩阵键盘加功能切换键是更合理的方案用P1口低四位做行扫描高四位做列检测通过翻转法读取按键值。2.2.1 矩阵键盘的引脚分配表功能引脚说明矩阵键盘行0-3P1.0 - P1.3输出低电平扫描信号矩阵键盘列0-3P1.4 - P1.7检测电平变化HC-05 TXDP3.0 (RXD)蓝牙发送到单片机HC-05 RXDP3.1 (TXD)单片机发送到蓝牙AT24C02 SDAP2.0I2C数据线需接4.7kΩ上拉AT24C02 SCLP2.1I2C时钟线电磁锁驱动P2.2经ULN2003驱动继电器蜂鸣器P2.3按键反馈与报警LCD1602数据P0口需接10kΩ上拉排阻LCD1602控制P2.4 - P2.7RS、RW、EN及背光控制这个分配方案的核心思路是把串口、I2C、键盘扫描、显示刷新全部错开在不同端口避免中断冲突和总线争用。P0口内部没有上拉电阻接LCD1602时必须外部加上拉排阻否则显示花屏是最常见的低级错误。2.3 电磁锁驱动电路继电器与ULN2003的配合电磁锁工作电流通常在几百毫安到1安培51单片机P2.2引脚最大灌电流约20mA直接驱动必烧引脚。ULN2003是达林顿管阵列内部集成续流二极管正好适合驱动继电器线圈。电路连接方式为P2.2接ULN2003的1B输入ULN2003的1C输出接继电器线圈一端线圈另一端接VCC继电器常开触点串联在电磁锁供电回路中。这里的工程细节是续流二极管方向。ULN2003虽然内部集成了续流二极管但如果继电器线圈供电电压超过5V比如用12V驱动电磁锁建议在继电器线圈两端再并一个1N4007方向为阴极接正电源、阳极接线圈另一端否则继电器断电瞬间的反向电动势可能击穿ULN2003内部晶体管。3. HC-05蓝牙模块AT指令配置与51单片机串口通信透传模式的建立3.1 用USB转TTL模块完成HC-05的AT指令配置HC-05进入AT指令模式有两种方法按住模块上的按键再上电或者把PIO11引脚拉高再上电。配置时必须用USB转TTL模块连接HC-05不能经过51单片机因为单片机程序会抢占串口数据。AT指令模式下HC-05的波特率固定为38400而透传模式下默认波特率是9600这两个波特率要区分清楚。以下是最小配置序列在串口助手中逐条发送每条指令以回车换行结尾返回OK表示成功ATNAMEBLE-LOCK # 设置蓝牙名称为BLE-LOCK ATROLE0 # 设置为从机模式0从机1主机 ATPSWD1234 # 设置配对密码为1234 ATUART9600,0,0 # 设置透传波特率9600停止位1无校验 ATBIND0000,0000,0000 # 清空绑定地址允许任意设备连接参数说明ROLE参数如果设成1HC-05会主动搜索周围设备并发起连接此时模块发热量会变大对密码锁没有意义PSWD是配对码真实项目中应该改成随机数字并记录在案答辩时当场演示配对过程比直接连上更有说服力。3.2 51单片机侧串口驱动波特率与定时器配置的对应关系STC89C52使用定时器1作为串口波特率发生器工作在8位自动重装模式。晶振11.0592MHz下9600波特率的TH1初值为0xFD这个组合的波特率误差为0是51单片机最经典的配置。串口初始化代码如下void UART_Init(void) { SCON 0x50; // 串口模式18位UART允许接收 TMOD 0x0F; // 清零T1控制位 TMOD | 0x20; // 设置T1为模式28位自动重装 TH1 0xFD; // 波特率9600 TL1 0xFD; TR1 1; // 启动定时器1 ES 1; // 开启串口中断 EA 1; // 开启总中断 }这段代码的关键是TMOD寄存器的高四位控制T1低四位控制T0。如果前面已经用T0做了定时扫描必须先把TMOD 0x0F 清零高四位再用或运算设置模式否则会破坏T0的配置。串口中断服务函数里需要处理接收缓存密码锁场景下数据量不大一个字节的环形队列就够用。3.2.1 串口接收中断中的溢出判断串口接收中断标志位RI由硬件置1必须在中断服务程序中软件清零。如果接收数据过快导致SBUF中的数据还没被读取就被新数据覆盖硬件会置位SM0标志SCON.5程序中应检查这个位并做丢包处理。对密码锁来说手机端每次只发6-8字节密码溢出概率极低但程序健壮性需要这个判断。void UART_ISR(void) interrupt 4 { if (RI) { RI 0; if (SM0) { // 检查溢出标志 SM0 0; return; } rx_buffer[rx_head] SBUF; rx_head (rx_head 1) % RX_BUFFER_SIZE; } if (TI) { TI 0; // 发送完成标志清零 } }3.3 蓝牙透传的数据格式设计自定义协议而不是裸发密码很多初学者直接让手机发送“123456”字符串单片机收到后和存储的密码比较。这种做法有个致命问题如果密码是“123”和“123456”的前缀关系先发“123”再发“456”单片机无法区分是两次输入还是一次输入。更可靠的做法是定义简单的帧协议typedef struct { unsigned char header; // 帧头固定0xAA unsigned char length; // 数据长度 unsigned char cmd; // 命令字0x01开锁0x02改密 unsigned char data[8]; // 密码ASCII码 unsigned char checksum; // 校验和所有字节累加取低8位 } Bluetooth_Frame;手机端发送开锁指令时组帧为AA 06 01 31 32 33 34 35 36 校验和。单片机收到帧头0xAA后进入接收状态机按length字段收满后续字节计算校验和通过后再解析命令字和密码。这个协议虽然增加了手机端的编码工作量但避免了边界条件下的一切歧义同时为后续扩展遥控断电、远程报警等功能预留了命令字空间。4. 密码存储与掉电保存AT24C02在51单片机密码锁中的写入策略4.1 STC89C52为什么必须外挂AT24C02STC89C52RC内部没有EEPROM只有Flash程序存储器和SRAM数据存储器。SRAM在掉电后数据全部丢失Flash虽然掉电不丢但写入需要擦除整个扇区不适合频繁修改密码。AT24C02是2Kbit256字节的I2C接口EEPROM写寿命约100万次性价比极高是51单片机课程设计中的标准配置。AT24C02的I2C地址固定为0xA0写和0xA1读A0-A2引脚接地。使用时注意SDA和SCL必须接4.7kΩ上拉电阻到VCCI2C总线是开漏结构没有上拉电阻时通信必失败。写入时序要求每个字节后有一个5ms的写周期延时期间芯片不响应任何指令连续写入时如果忽略这个延时会导致数据丢失。4.2 密码在EEPROM中的存储布局与校验冗余直接把密码明文存在EEPROM第0地址是最常见的做法但缺少校验机制如果写入过程中掉电读回来的是不完整数据系统可能永久锁死。密码锁必须三冗余存储配合校验字节布局如下EEPROM地址内容说明0x00 - 0x07密码副本1ASCII码存储6位密码加2字节保留位0x08副本1校验字节密码所有ASCII码异或结果0x10 - 0x17密码副本2与副本1相同0x18副本2校验字节异或结果0x20开锁失败计数器连续错误次数上限5次0x21 - 0x27修改记录时间戳可选记录最近修改时间读取密码时先读副本1并计算校验失败则读副本2两个副本都损坏就恢复默认密码123456并置标志位这样系统永远不会因为存储损坏而锁死。写入时先写副本2再写副本1保证任何时刻至少有一个有效副本。4.2.1 AT24C02写入函数的页写实现AT24C02支持8字节页写即连续写入8字节只需要一次写周期。但页写的边界条件是地址低3位不能跨页从0x07地址连续写2字节会跨到下一页实际执行时只有本页的1字节写入成功。下面代码实现安全的跨页连续写入void EEPROM_WriteBytes(unsigned char addr, unsigned char *buf, unsigned char len) { unsigned char i; while (len 0) { // 计算当前页剩余空间AT24C02页大小8字节 unsigned char page_left 8 - (addr % 8); unsigned char chunk (len page_left) ? len : page_left; I2C_Start(); I2C_SendByte(0xA0); // 设备地址写命令 I2C_WaitAck(); I2C_SendByte(addr); // 写入起始地址 I2C_WaitAck(); for (i 0; i chunk; i) { I2C_SendByte(buf[i]); I2C_WaitAck(); } I2C_Stop(); Delay5ms(); // 写周期延时 addr chunk; buf chunk; len - chunk; } }参数说明chunk变量是本次页写能容纳的字节数由当前地址距页边界的位置决定。比如地址0x07时page_left为1只能写1字节下次循环从0x08开始又是一个新页。这种写法比直接一次性写8字节更稳健byte预算从65字节增加到约80字节但可靠性大幅提升。4.3 密码校验的时间恒定写法密码校验的朴素写法是逐字节比较遇到不匹配就返回失败。这在工程上有个隐患攻击者可以通过测量响应时间推断密码位数是否匹配。虽然蓝牙密码锁的安全等级不需要对抗计时攻击但养成时间恒定的写法是好习惯bool Password_Verify(unsigned char *input) { unsigned char i; unsigned char diff 0; unsigned char stored[8]; EEPROM_ReadBytes(0x00, stored, 8); // 读副本1 for (i 0; i 8; i) { diff | (stored[i] ^ input[i]); // 异或或运算 } return (diff 0); }这段代码的关键在于用diff累加异或结果只有全部字节都相等diff才为0。不管密码在第几位开始不匹配函数执行时间都相同同时避免了提前return导致的时序泄露。5. 51单片机密码锁主循环状态机与蓝牙指令解析从按键到开锁的完整流程5.1 密码锁的5个核心状态定义与切换条件密码锁逻辑本质是状态机用枚举类型定义状态在主循环中用switch-case驱动。设计5个状态就能覆盖全部场景待机等待输入STATE_IDLE、接收键盘密码输入STATE_KEY_INPUT、接收蓝牙密码输入STATE_BT_INPUT、修改密码流程STATE_CHANGE_PWD、开锁成功或失败后的反馈STATE_FEEDBACK。状态触发条件退出条件IDLE上电或反馈结束键盘有按键按下或蓝牙收到帧头0xAAKEY_INPUT键盘数字键按下按确认键触发校验按退格键删除末位BT_INPUT蓝牙帧头匹配帧接收完成、校验和正确后自动切换校验逻辑CHANGE_PWD连续按两次修改键新密码两次输入一致后写入EEPROMFEEDBACK校验通过或失败延时3秒后自动回IDLE状态机的核心设计原则是每个状态只处理自己该处理的事件键盘扫描和蓝牙接收互不干扰。键盘扫描用主循环轮询方式蓝牙接收靠串口中断填充缓冲区两种事件通过标志位通知状态机切换避免在中断服务函数中执行耗时逻辑。5.2 主循环中键盘扫描与蓝牙帧解析的协同主循环的伪代码逻辑如下注意键盘扫描函数和蓝牙帧检查函数都是非阻塞设计保证任何情况下的实时响应void main(void) { unsigned char key; UART_Init(); Timer0_Init(); // 用于延时和超时判断 EEPROM_Init(); LCD_Init(); while (1) { key MatrixKey_Scan(); // 非阻塞扫描无按键返回0xFF if (key ! 0xFF) { HandleKeyPress(key); // 根据当前状态处理按键 } if (Bluetooth_FrameReady()) { // 帧接收完成标志 ProcessBluetoothFrame(); // 解析并执行开锁/改密 } UpdateDisplay(); // 刷新LCD显示 FeedDog(); // 喂狗防程序跑飞 } }这段主循环的执行频率取决于LCD刷新耗时如果LCD_Init和UpdateDisplay包含较长延时语句会导致键盘扫描节拍变慢。常见优化是把延时函数改用定时器轮询或者降低LCD刷新频率。矩阵键盘的扫描必须加上消抖处理检测到按键后延时10ms再确认电平状态否则一次按键会被当成多次触发。5.3 蓝牙帧解析函数的实现细节蓝牙数据通过串口中断逐字节存入缓冲区帧解析函数在完成一帧接收后执行。关键是帧头的抖动问题如果手机端发送数据时中间有停顿单片机可能先收到0xAA字节认为是帧头然后等待后续数据超时造成状态错乱。因此帧解析必须配合超时机制unsigned char BT_ParseFrame(void) { static unsigned char state 0; static unsigned char index 0; static unsigned char length 0; static unsigned char checksum 0; unsigned char byte; if (rx_count 0) return 0; byte rx_buffer[rx_head_read]; rx_head_read (rx_head_read 1) % RX_BUFFER_SIZE; rx_count--; switch (state) { case 0: // 等待帧头 if (byte 0xAA) { state 1; checksum 0xAA; } break; case 1: // 接收长度 length byte; checksum byte; index 0; state 2; break; case 2: // 接收命令字 frame.cmd byte; checksum byte; state 3; break; case 3: // 接收数据 if (index length - 2) { // 长度字段含命令字和校验字节 frame.data[index] byte; checksum byte; index; } if (index length - 2) { state 4; } break; case 4: // 校验字节 state 0; return (byte checksum); } return 0; }这段代码的状态变量是static类型意味着帧解析进度在函数调用间保留。字节从环形缓冲区排队取出不被中断服务函数打断分析。状态4返回校验结果后立即回状态0保证下一帧可以连续处理。5.4 开锁执行逻辑继电器吸合、超时自动断开与蜂鸣器反馈校验通过后执行结构分为三步置位P2.2驱动继电器吸合电磁锁、开启3秒延时后自动断开、蜂鸣器短鸣三声提示成功。开锁动作必须做成一次性触发而不是持续置位防止锁体长时间通电发热void Unlock(void) { P2_2 1; // 继电器吸合 Buzzer_Beep(3, 100); // 鸣3声每声100ms Timer0_DelayMs(3000); // 持续开锁3秒 P2_2 0; // 自动闭合锁体 // 开锁成功后清除失败计数 EEPROM_WriteByte(0x20, 0); }参数说明延时用定时器计数而不是空循环Delay原因是空循环占用CPU导致键盘扫描和蓝牙监听停滞用户可能觉得锁“死机了”。3秒开锁窗口可以让用户拉开门把又不会让电磁锁长时间通电发热。如果使用带反馈的电磁锁带门磁检测还可以扩展成检测到门关闭后再断锁这是课程设计拿到高分的一个加分项。6. 51单片机蓝牙密码锁的防暴力破解与掉电保护最后一道防线怎么设计6.1 连续错误锁定的实现策略电磁锁本身没有机械锁芯暴力破解的方式就是不断尝试密码组合。6位数字密码的排列组合是100万种理论上穷举空间够大但如果攻击者用手机App自动发送随机密码每秒钟可以尝试几十次这就需要在固件层加入锁定机制。一种常见的做法记录连续错误次数到AT24C02的地址0x20每次校验失败加1达到5次后触发3分钟锁定。锁定期间不论蓝牙还是键盘输入都忽略蜂鸣器以1Hz频率鸣叫报警。错误计数存储在EEPROM而不是SRAM保证掉电重启后锁定状态不丢失。void OnPasswordFailed(void) { unsigned char fail_count; fail_count EEPROM_ReadByte(0x20); fail_count; if (fail_count 5) { LockScreen(180); // 锁定180秒期间所有输入无效 fail_count 0; // 锁定结束后清零也可以不清零加重惩罚 } EEPROM_WriteByte(0x20, fail_count); }这里有个设计选择锁定结束后要不要清零错误计数课程设计要求“演示方便”清零更合适真实项目倾向不清零采用递增惩罚时间第1次锁定3分钟、第2次10分钟、第3次1小时直接杜绝了暴力破解的可能。6.2 修改密码的双重确认与EEPROM掉电保护修改密码的完整流程是按下修改键进入CHANGE_PWD状态输入新密码后按确认系统提示“再次输入”第二次输入一致才写入EEPROM。这个流程虽然增加了一次输入操作但能防止误触导致密码被意外更改。写入过程中的掉电保护依赖三冗余布局但更关键的是写入顺序。先写副本2地址0x10确认写入成功后再写副本1地址0x00最后更新校验字节。如果写入副本2后、写入副本1前掉电重启后读副本1还是旧密码校验通过系统正常工作反之先读副本2也只能是旧密码或新密码两者都完整不会出现“密码一半新一半旧”的中间态。6.3 真实场景下还能加的安全补丁出厂后想要安全等级更高下面三个补丁按成本从低到高排序限制蓝牙配对设备在HC-05上把BIND地址设置为管理员手机的蓝牙MAC地址只有该设备能连接其他手机根本进不了透传阶段。缺点是换手机后需要重新用USB配置模块。密码加盐存储EEPROM里存的不应该是密码本身而是密码加固定盐值的哈希。51单片机上跑完整的SHA-256比较吃力但可以用简单散列算法如CRC16或异或移位防止直接用串口工具读EEPROM芯片就能看到明文密码。增加看门狗喂狗逻辑STC89C52没有硬件看门狗但STC15系列有。用带看门狗的51变体在主循环中周期性喂狗程序跑飞后会强制复位保证蓝牙协议栈异常时锁不会处于“既不能开也不能关”的僵死状态。6.4 一个串口层面的验证手段用手机日志反向确认固件状态开发调试时最痛苦的阶段是蓝牙通了、但密码校验不生效分不清问题出在协议还是比对逻辑。常见做法是在固件中预留调试指令收到特定的0xFF字节时通过蓝牙回传当前状态信息包括密码错误计数、EEPROM读取状态、当前所在状态机编号。手机端的串口调试工具可以直接看到这些日志。static void SendDebugInfo(void) { unsigned char buffer[16]; buffer[0] 0xAA; // 帧头 buffer[1] 0x05; // 长度 buffer[2] 0x08; // 调试命令字 buffer[3] current_state; buffer[4] EEPROM_ReadByte(0x20); // 错误计数 buffer[5] password_valid_flag; buffer[6] 0; // 预留 // 计算校验和并发送 UART_SendString(buffer, 7); }这个调试通道在生产版本中要删除或关闭否则任何人都能通过蓝牙获取锁的状态信息反而成了信息泄露路径。丁级输入频率和代码逻辑关系表与指针参数✿补充定时器初始化中波特率误差的量化对比晶振频率12MHz配9600波特率时定时器初值需要TH10xF4实际波特率为10417误差约8.5%长时间通信会产生帧错误11.0592MHz晶振下误差为0所以“一律用11.0592MHz晶振配9600波特率加0xFD初值”这句话在蓝牙锁场景不绝对但安全。如果手里只有12MHz晶振可以把串口波特率改成4800并同步修改HC-05的ATUART参数这比靠误差容忍更可靠。本文还有配套的精品资源点击获取