一年前有个朋友把一摊子活丢给我做一套“能开锁的东西”不要指纹、不要NFC要的是手机能开、本地密码也能开、断电之后密码不能丢。我当时想这不就是个串口发字符串控制继电器的小玩具嘛真正动手才发现从器件选型到通信协议再到防暴力破解每一步都藏着值得较真的细节。这套基于单片机的蓝牙无线密码锁就是我把这些细节全部走通之后的完整方案。如果你正在做课程设计、毕业设计或者单纯想给自己宿舍门装一把能手机控制的锁这篇文章应该能帮你省下不少弯路。我尽量把硬件选型思路、电路设计取舍、蓝牙通信协议、密码管理逻辑以及联调时踩过的坑一次性讲清楚。1. 系统框架与器件选型先想清楚再做板子1.1 这套锁到底要完成哪些事很多人上来就画PCB、写代码结果做到一半发现功能漏了、器件不够用。我建议先把需求写死哪怕只是用手机备忘录记几行。我最后确定的功能清单是这样的手机蓝牙连接后输入密码开锁、本地矩阵键盘输入密码开锁、修改密码后掉电不丢失、连续输错密码自动锁定、 OLED屏幕显示锁状态和剩余锁定时间、预留一个继电器输出口控制电磁锁或舵机。看上去功能不多但每一项都牵扯到硬件资源和软件逻辑如果前期不列清楚后面很容易返工。这个清单里最容易被忽略的是“连续输错密码自动锁定”和“掉电不丢失”。前者涉及安全逻辑后者涉及EEPROM或Flash存储这两块恰恰是蓝牙密码锁区别于普通蓝牙玩具的核心。1.2 关键器件选型与对比主控的选择通常有两种思路51单片机或者STM32。如果你只是交个课程设计用51完全够STC89C52之类芯片便宜、资料多、老师也熟。但如果想真正做点带工程味道的东西我更推荐STM32F103C8T6。原因很简单串口数量够用、内部Flash可以模拟EEPROM、主频72MHz跑状态机和显示刷新都从容关键是价格也不贵。蓝牙模块的选择比较关键。市面上常见的几款模块我之前都试过直接看表格更直观模块型号协议主从模式默认波特率能不能连iPhone适合场景HC-05经典蓝牙SPP主从一体9600部分底板为38400不支持安卓手机、PC串口调试HC-06经典蓝牙SPP仅从机9600不支持安卓、低成本从机方案JDY-31经典蓝牙SPP仅从机9600不支持便宜、可直接AT配置JDY-08/GZ-08低功耗BLE 4.0主从一体默认连接参数可配支持手机App、低功耗场景我最后用的是STM32F103C8T6加HC-05的组合。原因不是为了性能而是HC-05的资料最全AT指令配置方式也标准网上随便一搜就能解决90%的问题。调试阶段先跑通后续想换BLE模块只改串口协议这一层底层逻辑不用动。1.3 为什么不顺手做指纹或人脸方案指纹模块大概30到60块人脸识别模块更贵而且算法和存储逻辑会吃掉大量开发时间。对于大多数场景蓝牙密码锁的意义在于把“无线鉴权”和“本地控制”这条链路完整打通密码本身就是一种凭证。等你把密码锁的权限管理、状态存储、异常锁定这套逻辑跑明白了后面加指纹模块上去其实就是加一个传感器输入源难度并不高。所以我对这套项目的定位是用最低成本把无线鉴权的完整链路吃透之后想扩展任何生物识别都是水到渠成的事。2. 硬件电路设计里的几个关键取舍2.1 供电设计蓝牙模块不能直接怼5V供电看着简单踩坑率却最高。蓝牙模块的板载稳压电路通常可以接受5V输入但模块输出的串口TX电平是3.3V如果MCU的RX引脚是5V电平长期运行容易出边界问题。另外经典蓝牙模块工作时电流波动挺大配对上电瞬间可能抽走40到50毫安如果和继电器共用一条细杜邦线电压跌落可能导致MCU复位。我实际用的供电拓扑是USB 5V进来后一路直接给MCU和继电器模块另一路经过AMS1117-3.3稳压后给蓝牙模块和OLED供电。AMS1117便宜可靠而且输出端只需要两个10uF电容做去耦。唯一要注意的是AMS1117的压差大概1V输入5V输出3.3V没问题但电池直接供电的话得算好电压余量。另外在5V入口和3.3V出口各加一个100uF电解电容能显著减少继电器吸合瞬间造成的电压跌落。2.2 电磁锁驱动三极管、继电器和续流二极管驱动电路是另一块容易糊弄的地方。如果你用继电器模块很多成品模块自带了三极管和续流二极管接个信号线就能用。但如果你是自己焊的继电器电路有件事必须做继电器线圈两端并联一个1N4007二极管方向要反着并也就是二极管阴极接电源正阳极接三极管集电极那一侧。这个二极管的作用是吸收线圈断电瞬间的反向电动势。没有它线圈产生的反向高压可能直接打坏三极管严重时会让MCU复位或者串口通信乱掉。基极电阻也要算一下拿S8050三极管举例假设继电器线圈电流是70毫安三极管放大倍数取100基极电流需要0.7毫安考虑1.2倍冗余基极电阻取1k到2k都是合理的直接用1k电阻最省事。控制逻辑上要注意继电器是高电平触发还是低电平触发。有些模块默认低电平触发MCU上电瞬间引脚处于高阻态或低电平锁可能会直接吸合一下。这个我后面专门踩过一次解决办法是MCU初始化时先把控制引脚拉到一个确定的安全电平再初始化继电器模块。2.3 蓝牙串口与单片机电平匹配蓝牙模块的TX输出是3.3V如果你用的STM32它本身就是3.3V电平直接连没问题。但如果你用51单片机5V电平的RX引脚对3.3V高电平其实也能识别因为51引脚的高电平阈值一般是0.7倍VCC也就是3.5V左右3.3V有点悬但不一定错。可靠的做法是电阻分压蓝牙TX到单片机RX之间串一个1k电阻单片机RX到GND接一个2k电阻这样3.3V分到单片机RX大约2.2V高于高电平阈值又不至于太高。实际上很多51板用3.3V串口直接接也跑得很稳但批量做或者要求可靠性的时候分压不能省。2.4 显示与键盘OLED I2C和矩阵键盘显示方案我强烈推荐0.96寸I2C接口的OLED也就是SSD1306驱动的屏只需要两根信号线加上电源比1602字符屏少用6个IO口看着还高级。I2C地址通常是0x3C偶尔是0x3D初始化时先扫描一遍地址避免焊完发现不亮。本地键盘建议用4x4矩阵键盘接8个IO口。如果你不想用那么多IO也可以用ADC按键方案一个模拟引脚串几个电阻能省7个引脚。矩阵键盘的扫描要点是行线设输入上拉、列线设输出逐列拉低再读行线。按键消抖用20到30毫秒的延时或者状态机方式都可以我偏向用状态机不阻塞主循环。OLED和键盘两个东西一加整块板子的人机交互就比较完整了开锁、改密码、看剩余锁定时间都看得见。3. 蓝牙通信协议把发过来的字符串变成能执行的动作3.1 裸字符串为什么不行如果只是做个演示直接发“123456”也能跑但你会发现几个问题第一次发没反应、第二次发却执行了、指令粘在一起、中间夹杂乱码时整个解析就崩了。根本原因是串口数据是字节流没有天然的分帧能力。手机上点一下发送蓝牙模块把字符串一个字节一个字节送到MCUMCU如果只是判断“收到的字符串是不是等于123456”那就得自己拼缓冲、处理半包和粘包。最简单的办法就是定义一套指令协议让每条指令有明确的帧头、帧尾和长度边界。3.2 帧格式与命令集定义我用的格式非常简单每一条指令都是纯文本行#[命令] [参数1] [参数2]\n帧头是#帧尾是换行符\n。为了让调试更省事\r\n和单独\n我都当作结束标志。解析器一旦收到帧头就认为一条指令开始收集到帧尾就交给指令处理函数。这样一来就算串口助手发送时自动带了回车换行也不会有问题。命令集定义如下指令示例含义UNLOCK#UNLOCK 123456\n普通密码开锁SETPWD#SETPWD 123456 888888\n验证旧密码后修改密码STATUS#STATUS\n查询当前锁状态ADMIN#ADMIN 999999\n管理员密码验证进入管理模式RESET#RESET 999999 123456\n管理员重置密码应答也同样简单粗暴OK\n ERR:WRONGPWD\n ERR:LOCKED\n ERR:FULL\n BUSY\n这其实是个行协议没有二进制帧不用计算校验和。乍一看不够“专业”但对51或者小容量STM32来说非常合适易懂、易调、不容易出错。密码本身是数字文本ASCII直接发解析端也省去字节转二进制的麻烦。3.3 串口中断里的状态机解析解析逻辑我放在串口接收中断里每次收到一个字节就喂给状态机。这样主循环不需要轮询等待数据解析过程也不阻塞。核心代码长这样typedef enum { IDLE, RECV } ParseState; static ParseState state IDLE; static uint8_t len 0; static uint8_t frame_buf[32]; // 放在USART接收中断里 void UART_ParseByte(uint8_t ch) { if (state IDLE) { if (ch #) { state RECV; len 0; } } else if (state RECV) { if (ch \n || ch \r) { frame_buf[len] \0; state IDLE; Command_Process(frame_buf); } else if (len sizeof(frame_buf) - 1) { frame_buf[len] ch; } else { state IDLE; // 缓冲区满了直接丢弃 } } }注意一点缓冲区满了不能无限往里面塞必须强制回到IDLE状态。之前有人图省事不检查长度最后发现密码验证偶尔失败其实就是缓冲区溢出覆盖了其他内存。串口中断里也不要直接调用printf或者大循环处理只负责收字节和置标志具体指令处理放在主循环里执行这样响应更稳。3.4 应答机制和超时重传协议里加入应答之后手机端就很容易判断指令是否成功。我实测的经验是手机发一条UNLOCK之后如果3秒内没收到以OK或ERR开头的回复基本可以判定这条指令丢失App端提示用户重试。但重试要克制不能每100毫秒就发一次。一方面容易把MCU串口缓冲区撑满另一方面如果锁因为某些原因卡在继电器动作中重复指令会造成锁舌反复跳动。我实际规定App端同一指令3秒内最多重试1次总共不超过3次。MCU端如果在执行继电器动作期间收到新指令直接回复BUSY让上层知道现在没办法处理。4. 密码管理与开锁控制软件层面的安全底线4.1 密码的存储与掉电保存密码存储优先选用EEPROM或者Flash。如果是STM32F103C8T6可以直接用内部Flash模拟EEPROM不需要外挂芯片。如果你用51用STC单片机的内部EEPROM或者外挂AT24C02都可以。我实际用的是AT24C02理由很简单I2C接口2KB容量单字节擦写寿命理论上有100万次容错思路清晰而且很多教程都是拿它做例子资料好找。硬件连接就是SDA和SCL两个引脚接上拉电阻模块一般在板子上已经贴了4.7k上拉省了很多事。存储结构建议用结构体typedef struct { uint8_t current_pwd[6]; // 当前密码固定6位 uint8_t admin_pwd[6]; // 管理员密码 uint8_t error_count; // 连续错误次数 uint16_t lock_until; // 锁定截止时间相对时间戳 uint8_t flag; // 初始化标志0xA5表示数据有效 } LockData;每次整体读写不要拆开写。如果拆成几个单独地址来写容易出现半新半旧的数据状态。flag这个字段非常重要首次上电时检测flag不是0xA5说明EEPROM是空的或者数据损坏这时写入默认密码123456管理员密码999999再把flag置成0xA5。4.2 校验逻辑与控制细节密码校验本身不复杂但有几个细节必须处理。输入密码后不管校验成功还是失败都要立刻把接收缓冲区和临时密码缓冲区清空。原因很简单如果单片机内部还留着刚才的明文密码有心人用调试接口或者串口嗅探就能把密码抓出来。另外一个细节是密码比较要使用固定长度比较也就是memcmp而不是strcmp避免有人发一个123456X之类带额外字符的字符串碰运气。开锁动作我设计为先输出一个高电平到继电器控制引脚保持1秒然后自动恢复关闭状态。保持时间不要超过5秒不然电磁锁一直通电发热也会白白耗电。开锁成功后OLED显示“OPEN”3秒后变回“LOCKED”状态。4.3 错误锁定与锁定状态不掉电防暴力破解的逻辑直接影响这套锁靠不靠谱。我的策略是连续错3次锁定30秒锁定期间所有开锁请求都返回ERR:LOCKED本地键盘也忽略密码输入。锁定状态必须存到EEPROM里不能只放在RAM变量中。不然别人断电重启一下错误次数清零暴力破解就直接绕过去了。我参考了指数退避的思路连续错误的锁定时间依次变成1秒、2秒、4秒、8秒、30秒、60秒上限5分钟。每次错误次数加1写回EEPROM下次上电重新读取。有一点要注意EEPROM虽然号称能写100万次但如果一直在同一地址写寿命消耗还是很快的。所以错误计数和锁定截止时间不要频繁写。比如每次输错密码就写一次暴力破解的人故意反复输错一天下来就能写上千次时间一久这块区域就废了。实际做法是错误计数在内存里累加只有当锁定状态切换或者达到某个阈值时才回写EEPROM。4.4 键盘和蓝牙输入统一走一套校验逻辑很多教程把键盘输入和蓝牙输入分开写两套函数结果就是键盘能开锁但蓝牙不能改密码或者两边的密码不共用同一个存储区域。我建议设计一个统一的输入抽象层。键盘和蓝牙本质上都是产生一段密码文本最终都送到同一个校验函数uint8_t Password_Verify(uint8_t mode, uint8_t *input) { LockData ld; EEPROM_Read(ld); if (mode MODE_USER) { return memcmp(ld.current_pwd, input, 6) 0; } else if (mode MODE_ADMIN) { return memcmp(ld.admin_pwd, input, 6) 0; } return 0; }这样不管是本地键盘输入、蓝牙指令输入还是以后加一个指纹模块都只是给这个函数喂数据校验逻辑永远只有一份。代码好维护也不会出现某个入口忘记校验密码的漏洞。5. 联调实测手机连不上的时候我在做什么5.1 蓝牙模块的AT配置HC-05的AT配置逻辑和其他模块不太一样需要按住模块上的小按键再上电此时模块进入AT模式LED慢闪。用USB转TTL模块连接时TX接RX、RX接TX波特率通常为38400。如果你用的是HC-06或者JDY-31不用按键直接进AT模式波特率默认9600。常用配置指令AT ATNAMEMyLock ATPSWD8888 ATUART115200,0,0 ATROLE0ATUART115200,0,0的意思是设置串口波特率115200停止位1位无校验。改完这一条之后单片机的串口波特率也必须同步改到115200不然双方通信立刻变乱码。有一个坑是部分HC-05底板出厂波特率是9600但模块芯片的默认回显是38400你如果按9600发AT可能没有任何回显。遇到这种情况先把USB转TTL波特率切到38400试一下再切回9600。5.2 第一次联调翻车现场我第一次联调时手机串口助手里输入123456点了发送锁纹丝不动。排查了半天发现是协议不一致手机端发的是123456\n而单片机端要求的是#UNLOCK 123456\n根本不是一个对话体系。第二个典型问题出现在串口助手的“发送新行”选项上。有的工具默认在发送内容后自动加\r\n导致实际发出来的字符串比预期多了两个字节。如果协议里固定了密码长度这个多余的换行就会让校验失败。解决办法有两个要么在协议解析时忽略行尾所有空白字符要么在打印数据时用十六进制模式看一下实际发送字节。把串口助手切到十六进制显示之后你看到的才是模块真正发到单片机里的字节。比如字符串123456\n显示出来是31 32 33 34 35 36 0A如果发现末尾有0D 0A就知道是自动加了回车换行足够判断问题出在哪了。5.3 iOS对经典SPP的限制如果你手里只有iPhoneHC-05这条路线就不用想了。iOS系统从系统层面不支持经典蓝牙SPP串口协议必须用BLE低功耗模块。JDY-08或者JDY-23这类BLE模块手机端用LightBlue这类通用工具就能看到服务和特征值。BLE的通信方式不再是串口透传那么简单需要你找到一个可写特征值把指令写进去然后监听另一个可读特征值拿应答。这也是为什么我在第1章选型时建议大家调试阶段先用安卓手机等协议调通再切BLE。当年我做的时候就是先买了HC-05结果用iPhone死活连不上最后借了一台安卓手机才完成联调。5.4 OLED显示乱码与串口复用问题OLED显示乱码的第一嫌疑是I2C地址不对。SSD1306的地址一般是0x3C但有的屏是0x3D如果库默认0x3C而你的屏是0x3D屏幕上就什么都显示不了。可以在初始化时写一个扫描I2C总线的函数把检测到的地址打印到调试串口。另一个容易出问题的点是串口复用如果你把调试打印printf也重定向到连接蓝牙模块的串口上那调试信息和蓝牙指令就会混在一起协议解析器偶尔会收到一串莫名其妙的字符出现“偶尔开锁偶尔失灵”的怪现象。我后来把调试信息输出到另一个串口也就是USART2蓝牙独占USART1两个互不打扰一切恢复正常。6. 安全加固无线密码锁不是一键解锁玩具6.1 防暴力破解指数退避锁定前面提到过指数退避这里给出更完整的思路。密码锁最怕的是有人连猜6位纯数字密码一共100万种组合如果不加限制每秒猜3次不到4天就能猜完。指数退避本质是用时间和错误次数做成本叠加让暴力破解的期望时间指数级增加。我实际采纳的策略前3次错误不锁定从第4次开始每次错误锁定时间翻倍。锁定期间不校验密码直接返回锁定错误。而且这个时间是用芯片内部定时器跑出来的系统不断电、时间继续走即使断电重启也能恢复。配合OLED显示的话锁定倒计时直接显示在屏幕上用户体验也好很多。不会出现“输了半天密码不知道怎么回事”的感觉。6.2 防重放攻击给指令加上随机数防重放是很多入门项目完全想不起来的问题。如果你的锁只校验“密码是否正确”那么别人拿一个无线抓包工具录下一段开锁指令过后重新播放一遍门照样开。这个攻击对SPP透明传输的模块来说非常容易实现。最简单的解决办法是让每一条开锁指令一次性有效办法是加入随机数或者时间戳。比如App端在发送UNLOCK之前先请求一个挑战值C-S: #CHALLENGE\n S-C: CHAL:12345\n C-S: #UNLOCK 123456 12345\n设备端在收到开锁指令时检查这个挑战值是不是自己刚刚发出去的而且一个挑战值只能使用一次。如果校验失败返回ERR:EXPIRED。这样即使开锁指令被完整录下来重放一遍也无效因为挑战值已经过期。这个方案不复杂却能把整个锁的安全性提升一个档次。唯一需要注意的是MCU端记录的挑战值不能太多只保留最近一个就行过期就丢弃。6.3 权限分级与使用场景我不建议所有人共用同一个密码。管理员密码和普通用户密码分开可以让管理员密码的权限只用于改密和特殊开锁普通用户密码只能开锁。这样即使普通用户密码泄露管理员也不会跟着失守。再进一步可以给普通用户密码分时效。不过对单片机这种资源受限的场景用定时器做每日时间戳比较麻烦通常需要加RTC芯片或者从手机端同步时间。我的建议是如果只做课程设计做到管理员密码和普通密码分级就够用了真正要做产品级再去加RTC和时效逻辑。6.4 结构安全与硬件加固软件做再好硬件结构上也不该留低级破绽。蓝牙模块的天线尽量朝外不要被金属外壳包住否则信号衰减严重手机在1米外就失联。锁体本身的强电线和信号线要分开走继电器驱动线不要和蓝牙天线平行过长距离避免开锁瞬间信号被干扰。还有一点可能被忽略MCU的调试接口比如SWD或者串口下载口在成品状态应该关闭或者物理断开。不然别人拆开后盖直接往调试口灌一段代码就能把EEPROM里的密码读出来软件那层加固全白搭。7. 我在调试中踩过的坑和后续扩展想法7.1 四条最值得写下来的坑第一条继电器吸合瞬间单片机复位。现象是蓝牙能连上、发送开锁指令后LED灭一下然后重启。原因基本就是电源电压跌落和继电器线圈反电动势。前者靠电源入口加大电容后者靠续流二极管两个都加上就再没出现过了。第二条EEPROM数据全部变成0xFF。AT24C02如果读到全0xFF大概率是WP引脚被拉高了写保护开了。看一下你的模块上WP引脚是不是被模块自己拉到高电平如果是要在代码里输出低电平才能正常写。第三条矩阵键盘偶尔连击。按键扫描的时候没有做松开检测导致一次按下产生多次触发。解决办法是检测到按键按下后等待松开再执行一次动作或者在确认键按下后加一个状态标志松开时才清掉标志。第四条蓝牙模块热插拔导致串口通信失效。上电顺序很重要应该先让MCU和模块的电源稳定再去接串口线。如果先接串口线再上电3.3V和5V之间的先后关系会乱严重时会把模块串口引脚烧坏。后来我做了一个小转接板把电源和串口的上下电顺序用逻辑控制省了很多事。7.2 可以继续扩展的方向整套系统跑通之后扩展方向非常多。换一颗带BLE的模块再接个小程序就能在iPhone上直接控制。如果换成带蓝牙和WiFi的芯片比如ESP32系列还可以把“远程开锁”做到公网层面人在外面也能看门锁状态。我自己比较推荐的下一步是加一个电容式指纹模块因为密码锁的校验框架已经抽象成统一的密码校验函数指纹模块采集到的特征值只要同样塞进校验逻辑整个系统就能支持“指纹开锁”改动量比你想象的小得多。再往后就是加开锁记录把每次开锁的时间存到EEPROM循环区手机连接后拉取最近几十条记录这套锁就算有了初步的审计能力。我个人在反复调试这套系统时最深的一个感受是无线入口远没有想象中那么简单。协议不设计、校验不做、密码明文存储这类问题书本上写一百遍也是“注意点”只有亲手做一遍才能真正变成习惯。最后再分享一个很土但百试百灵的调试技巧——把串口助手切到十六进制显示再回一条短信你就瞬间知道实际发出去了哪几个字节很多“单片机没反应”的玄学问题当场就能变成一眼看穿的确定性问题。
阅读完成 · 觉得有帮助?