简介:面向电子类专业课程设计与毕业设计的电子密码锁仿真资源包,基于51单片机完成软硬件设计,适合学习矩阵键盘、LCD显示、密码校验与报警逻辑等知识点的读者参考。包内共18个文件,以C语言源程序、Proteus仿真工程(DSN/PDS工程)和Keil工程文件为主,同时包含HEX烧录文件、备份文件及编译过程生成的列表文件,整体体积仅148KB。已有2336人学习下载。资源提供了完整可运行的电子密码锁设计与仿真方案,使用者可对照源程序与仿真原理图,理解单片机如何读取键盘输入、比较密码并控制开锁输出;也可直接打开工程查看电路连接,或借助HEX文件在Proteus中运行验证,作为课程设计或毕业设计模板进行二次开发。
1. 单片机课程设计里绕不开的电子密码锁系统设计
电子密码锁是单片机课程设计里出现频率最高的题目之一,因为它把输入、存储、显示、输出四件事全部串了起来。真动手做会发现,键盘抖动、密码掉电丢失、开锁后状态乱跳,每个问题都能卡住半天。这套系统设计真正的骨架是四件事:51单片机负责状态调度,矩阵键盘负责采集输入,EEPROM承担密码掉电保存,继电器与电磁锁完成最终动作。下面从选型讲到Proteus仿真,再到防护加固,把一条能直接复现的路径讲完。适合正在做课设或毕设的读者,也适合想把手上那块51开发板变成能开门的小系统的开发者。
2. 硬件选型与接线:51单片机、矩阵键盘和LCD1602怎么搭
2.1 主控选型:STC89C52比AT89S52更适合课程设计
STC89C52和AT89S52都是8051内核,实验室里STC的板子更多:内部8KB Flash、512B RAM,支持ISP串口下载,一条USB转TTL线就能烧程序,不用额外买烧录器。密码锁程序编译出来一般两三KB,Flash和RAM都不是瓶颈,真正影响选择的是下载是否方便、资料是否好找。STC还有个优势:内部看门狗,第5章的防护逻辑会用到。
这里要提醒一个常见误选:有人为了省IO选AT89C2051这种20脚芯片,但P0口和P2口被砍掉后,LCD1602和继电器就得和键盘挤同一个P1口,扫描和显示互相干扰,排错成本反而更高。密码锁题目原则上选40脚封装的89C52,P0、P1、P2三口分开干三件事,逻辑清晰,后续扩展继电器和蜂鸣器也不冲突。
2.2 矩阵键盘扫描:4x4按键只用P1一个口
键盘按行列接法接到P1口,4根行线接P1.0-P1.3,4根列线接P1.4-P1.7。扫描思路是分时复用:每次只拉低一列,读行线电平,就能确定哪个键被按下。对应按键扫描函数是这样:
#include <reg52.h> #define KEY_PORT P1 unsigned char KeyScan(void) { unsigned char row, i; unsigned char code col_mask[4] = {0xEF, 0xDF, 0xBF, 0x7F}; unsigned char code key_tab[4][4] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; for (i = 0; i < 4; i++) { KEY_PORT = col_mask[i]; // 依次把一列拉低 row = KEY_PORT & 0x0F; // 读四根行线状态 if (row != 0x0F) { // 出现低电平,说明有键按下 delay_ms(10); // 软件消抖,过滤机械抖动 row = KEY_PORT & 0x0F; if (row != 0x0F) { while ((KEY_PORT & 0x0F) != 0x0F); // 等待释放,防连按 if (row == 0x0E) return key_tab[0][i]; else if (row == 0x0D) return key_tab[1][i]; else if (row == 0x0B) return key_tab[2][i]; else if (row == 0x07) return key_tab[3][i]; } } } return 0xFF; // 无键按下,返回无效值 }这段代码把消抖、判定、防连按放在一个函数里完成。col_mask依次将P1.4到P1.7之一置低,其余三列保持高电平;如果行线上有低电平,就说明该列与某行的交点被按下。返回值直接是按键字符,后续密码比对时比较ASCII码即可。delay_ms(10)过滤掉机械按键按下瞬间的抖动,如果省掉,一次物理按下可能被识别成多次输入。
扫描函数的阻塞特性要留意:while循环会一直等按键释放才返回。密码锁这种交互节奏慢的场景,阻塞反而省心,主循环不会收到重复键值。如果以后要增加长按功能,就得改成非阻塞扫描,每10ms读一次,只记录电平边沿变化。
引脚分配如下,这套接法对应第4章的Proteus仿真连线:
| 信号 | 51引脚 | 方向 | 说明 |
|---|---|---|---|
| 行线R0-R3 | P1.0-P1.3 | 输入 | 键盘行状态读取 |
| 列线C0-C3 | P1.4-P1.7 | 输出 | 逐列拉低扫描 |
| LCD1602数据D0-D7 | P0 | 双向 | 8位并行数据 |
| LCD1602 RS | P2.5 | 输出 | 寄存器选择 |
| LCD1602 RW | P2.6 | 输出 | 读写选择 |
| LCD1602 EN | P2.7 | 输出 | 使能信号 |
| 继电器控制 | P2.1 | 输出 | 高电平吸合开锁 |
| 蜂鸣器 | P2.0 | 输出 | 低电平触发 |
2.3 LCD1602的P0上拉与继电器驱动电路
LCD1602支持8位和4位两种并行模式。密码锁系统里数据口P0已经占满,直接用8位模式最省事;4位模式要先发高4位再发低4位,代码多一层打包逻辑,还容易在时序上出错。不论仿真还是实物,都要记住P0口是开漏输出,必须外接上拉电阻才能输出可靠的高电平。实际做的时候我一般用10kΩ排阻。
注意:P0口必须外接10kΩ上拉电阻,Proteus里不加仿真也能跑,实物上会把LCD打出一片乱码。
驱动端的电流缺口更大。单片机IO口输出电流只有几百微安,而常见5V继电器线圈吸合需要30-80mA,直接驱动带不动。常见做法是三极管放大:把P2.1接基极,发射极接地,集电极接继电器线圈,线圈并联续流二极管吸收断电反电动势。控制逻辑代码很简单:
sbit RELAY = P2^1; RELAY = 1; // 三极管导通,继电器吸合 delay_ms(3000); // 保持3秒 RELAY = 0; // 释放,锁体复位开锁保持时间不是固定3秒。普通电磁锁1-3秒就可以,电控锁带闭锁机构的需要5秒以上。把时间抽成宏定义放在程序开头,实物调试时按锁体规格改一处就行。继电器触点选型也要注意:交流220V的锁具用10A/250VAC规格,直流低压锁用5A/30VDC的继电器足够,别把耐压选小了。
3. 密码存储与校验:EEPROM掉电保存和键盘输入怎么配合
3.1 密码为什么存AT24C02,而不是写死在Flash
把密码固定写死在程序里,用户改密码就得重新烧录固件,这不符合电子密码锁的基本需求。AT24C02是2Kbit的EEPROM,I2C接口,掉电不丢数据,允许反复擦写,对密码锁这种"偶尔改一次密码"的使用频率绰绰有余。器件地址是0xA0(A2/A1/A0全接地时),这个地址头在写代码时要背下来。
I2C时序里,起始和停止条件要严格按协议生成:
sbit SCL = P3^6; sbit SDA = P3^7; void I2C_Start(void) { SDA = 1; SCL = 1; delay_us(5); // 保证两条线稳定在高电平 SDA = 0; // SCL为高时SDA拉低,产生起始信号 delay_us(5); SCL = 0; } void I2C_Stop(void) { SDA = 0; SCL = 1; delay_us(5); SDA = 1; // SCL为高时SDA拉高,产生停止信号 delay_us(5); }主节点的延时决定I2C速率,标准模式100kHz下SCL半个周期约5微秒,STC89C52在12MHz晶振下用delay_us(5)正合适。如果换用支持400kHz快速模式的器件,延时可以缩到2微秒。
初学最容易在应答信号上翻车。AT24C02每收到一个字节后会把SDA拉低作为应答,主机在第9个时钟周期必须释放SDA总线,否则应答被破坏。等待应答的函数要单独提出来:
bit I2C_WaitAck(void) { unsigned char t = 0; SDA = 1; // 释放SDA,让从器件能拉低 delay_us(2); SCL = 1; delay_us(2); while (SDA) { // 一直高电平说明从器件不应答 if (++t > 200) { SCL = 0; return 1; // 应答超时 } } SCL = 0; return 0; // 应答正常 }返回1时多半是器件地址错误、SDA/SCL接反或者缺少上拉电阻。发送字节和读取字节的底层函数也要自己写:
void I2C_SendByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SCL = 0; if (dat & 0x80) SDA = 1; else SDA = 0; dat <<= 1; SCL = 1; // SCL高电平期间,SDA数据必须保持稳定 } SCL = 0; } unsigned char I2C_ReadByte(void) { unsigned char i, dat = 0; SDA = 1; // 释放总线,准备接收 for (i = 0; i < 8; i++) { SCL = 1; dat <<= 1; if (SDA) dat |= 0x01; SCL = 0; } return dat; }3.2 EEPROM地址分配与断电保存初始化
密码数据在EEPROM里的分布要有固定规划,不然改密码时新旧数据容易混。我常用的分配方式:
| EEPROM地址 | 作用 | 说明 |
|---|---|---|
| 0x00 | 初始化标志 | 0xA5表示已写入过密码 |
| 0x01-0x06 | 6位密码 | 以ASCII码存储 |
上电先读标志字节。如果不是0xA5,就按默认密码123456写入,再补写标志;是0xA5说明EEPROM里已有用户密码,把这6个字节搬到内存数组。对应初始化函数:
unsigned char pwd[6]; void LoadPassword(void) { unsigned char flag, i; unsigned char code default_pwd[6] = {'1','2','3','4','5','6'}; flag = AT24C02_ReadByte(0x00); if (flag != 0xA5) { for (i = 0; i < 6; i++) { AT24C02_WriteByte(0x01 + i, default_pwd[i]); } AT24C02_WriteByte(0x00, 0xA5); for (i = 0; i < 6; i++) pwd[i] = default_pwd[i]; } else { for (i = 0; i < 6; i++) { pwd[i] = AT24C02_ReadByte(0x01 + i); } } }逐字节读回6位密码在课程设计里完全够用。AT24C02支持页读页写,一次操作能搬多个字节,但密码锁的读写频率低,追求页模式只会让代码可读性变差。函数里的标志判断是这套系统设计里的关键防护:不加这个标志,每次上电都会用默认密码覆盖用户改过的密码。
注意:AT24C02每次写入后有约5ms的写周期,期间不响应器件地址,延时要留足10ms再发起下一次操作。
3.3 按键输入缓冲与密码比对逻辑
输入环节不要逐位实时匹配,那样会打乱显示和后续的退格逻辑。先维护一个输入缓冲数组和计数器,数字键填充缓冲,LCD上显示“*”而不是明文,防止旁边人记下密码。按满6位自动进入校验,另外给个“#”确认键,用户按得慢时不用等满6位:
unsigned char input_buf[6]; unsigned char input_cnt = 0; void HandleKey(unsigned char key) { if (key >= '0' && key <= '9') { // 只处理数字键 if (input_cnt < 6) { input_buf[input_cnt++] = key; LCD_ShowChar('*'); } if (input_cnt == 6) { VerifyPassword(); // 输入满6位,自动校验 } } else if (key == '*') { // 退格:删除最后一位 if (input_cnt > 0) { input_cnt--; LCD_Backspace(); } } else if (key == '#') { // 确认:立即校验 VerifyPassword(); } }比对函数不用strcmp,原因是密码定长6字节,写成定长比较逻辑更直接,也不会因字符串结束符提前返回:
unsigned char VerifyPassword(void) { unsigned char i; bit match = 1; for (i = 0; i < 6; i++) { if (input_buf[i] != pwd[i]) { match = 0; break; } } if (match) { RELAY = 1; // 开锁 delay_ms(3000); RELAY = 0; LCD_ShowString("OPEN"); err_cnt = 0; } else { LCD_ShowString("ERROR"); err_cnt++; // 错误次数累计,供锁定逻辑使用 } input_cnt = 0; // 无论成功还是失败都清空缓冲 return match ? 1 : 0; }成功或失败后清空input_cnt是必须的。如果不清空,用户输错后继续按键,新旧输入会拼在一起,出现“按两三位就自动校验”的诡异现象。err_cnt这个变量目前只负责累加,第5章会接入锁定状态机。
4. 系统状态设计与Proteus仿真:把键盘、显示和开锁动作串成闭环
4.1 状态机:待机、输入、校验、锁定四态
直接在主循环堆if-else也能跑,但加需求时很容易变成补丁摞补丁,比如后面要加“连续输错锁定30秒”,状态一多就乱了。更稳的写法是把系统抽象成几个状态,用状态机管理:
typedef enum { STANDBY, // 待机,等待首个按键 INPUT, // 输入密码中 VERIFY, // 校验中 LOCKOUT // 锁定,禁止输入 } SysState; SysState state = STANDBY; unsigned char err_cnt = 0; unsigned char lock_timer = 0; void SystemTick(void) { unsigned char key; key = KeyScan(); switch (state) { case STANDBY: if (key >= '0' && key <= '9') { // 首个数字键进入输入态 input_cnt = 0; state = INPUT; } break; case INPUT: if (key != 0xFF) HandleKey(key); // 正常收集按键 break; case VERIFY: break; case LOCKOUT: if (lock_timer > 0) { lock_timer--; // 定时器每1秒触发一次更新 } else { err_cnt = 0; state = STANDBY; } break; } }状态迁移表如下,把触发条件和动作集中放一起,联调时对着表查问题比读代码更直观:
| 当前状态 | 触发条件 | 迁移到 | 动作 |
|---|---|---|---|
| STANDBY | 按下数字键 | INPUT | 清空输入缓冲,LCD提示输入密码 |
| INPUT | 输入满6位或按“#” | VERIFY | 读取当前密码,准备比对 |
| VERIFY | 密码匹配 | STANDBY | 继电器吸合,显示OPEN,错误计数清零 |
| VERIFY | 密码不匹配 | INPUT | 显示ERROR,错误计数加1 |
| INPUT | 连续错3次 | LOCKOUT | 锁定30秒,蜂鸣器间歇报警 |
LOCKOUT状态里主循环忽略所有按键,这才是锁定的实质。如果只在输入判断里加一个计数,锁定期间按键还是会被接受,密码已经被人试出去了,系统却没有拦截。51单片机资源有限,状态枚举只占几个字节,switch跑一遍的开销比if-else链还低,在12MHz晶振下没有任何实时性压力。
4.2 Proteus仿真搭建与两个高频坑
Proteus是验证这套系统最快的路径,不用焊接就能同时观察键盘扫描和LCD时序。把AT89C52、KEYPAD-SMALLCALC、LM016L、AT24C02、RELAY和LED拖到画布,按第2章的引脚表连线。晶振设12MHz,把Keil生成的hex文件加载到AT89C52的Program File属性里,点运行就能看到效果。
仿真里排在前两位的坑,都跟外设配置有关。第一个是LCD不显示:LM016L的RS、RW、EN三个控制脚接错任意一根,数据都进不了寄存器。调试时先固定RW为低电平,只做写操作,能在LCD上显示固定字符串再继续。第二个是AT24C02读出全是0xFF:检查SCL和SDA有没有上拉电阻,在Proteus的属性面板把两条线设为10k上拉,不然总线上电平不确定,器件永远不应答。
仿真的模型响应和真实器件有差异,AT24C02的写周期在Proteus里常常表现为“好像不用等”,这会导致在仿真里正常、烧到板子上就丢密码。无论仿真现象如何,WriteByte后保留10ms延时,让实物和仿真行为保持一致,这是教材里不太会提的细节。
4.3 实物调试顺序:先模块后联调
拿实物直接跑完整程序,出了问题要同时排查五六个模块,效率很低。分四步走:
- 烧一个只点亮LCD的程序,确认显示字符正确;
- 单独测矩阵键盘,按下的键值用数码管或串口打印出来;
- 单独验证EEPROM:写一个字节再读回来,和原值对比;
- 最后才把键盘、存储、继电器拼到一起,跑完整的密码校验逻辑。
每一步先在Proteus里仿真通过再上实物,多数时间耗在排线上,模块本身的问题反而少。调试期间最好留一路串口输出,把状态机的跳转日志发到上位机。51的P3.0、P3.1是串口引脚,和AT24C02用的P3.6、P3.7不冲突,可以同时挂着用,调试完再关掉。
5. 51单片机电子密码锁加固:锁定、改密码和初始化验证
5.1 连续输错锁定30秒,用看门狗兜底
光有校验逻辑,门禁场景根本不够用。错误计数err_cnt在VerifyPassword里累计,满3次进入LOCKOUT状态,锁定期间按键全部忽略,蜂鸣器每秒响一次提醒。锁定时间先写30秒,实际部署可以扩展成阶梯式:第一次30秒,第二次60秒,第三次5分钟,自动试密成本会快速上升。
STC89C52内部看门狗也要开。程序一旦跑飞,系统会在1.6秒内复位,对挂在门上的设备,死机等于开不了门:
WDTRST = 0x1E; // 使能看门狗 WDTRST = 0xE1;WDTRST是STC89C52的看门狗复位寄存器,原版reg52.h里没有定义,用Keil编译前先声明sfr WDTRST = 0xA6;。主循环里每1000ms喂一次狗。注意开锁那3秒的delay_ms(3000)会阻塞主循环,超过喂狗间隔,系统会被看门狗复位,看到的现象就是“一开门锁就重启”。要把开锁计时改成非阻塞,用定时器中断计数,主循环只检查状态标记,既照顾了看门狗,又不阻塞键盘扫描。
5.2 修改密码:原密码确认与新密码双次输入
改密码的逻辑不能是“输入新密码直接写”。至少要分两步:先验原密码,再收新密码,新密码还要输入两次确认一致。两次不一致就回到原密码校验,不写EEPROM。真实使用场景里,新用户输错一次的概率不低,这个确认流程是体验兜底。新密码确认一致后再整体写入,避免反复擦写EEPROM,同时把内存里的pwd数组同步更新,防止重启后又用旧密码。
5.3 初始化引脚:忘记密码后的最后手段
电子密码锁最尴尬的故障就是忘记密码。预留一个初始化策略,比返厂靠谱。在P3.2接一个测试按钮,按下时把EEPROM的0x00标志改写,重启后LoadPassword会认为从未初始化,重新写入默认密码。日常固件验证时短接一下再上电,恢复到出厂状态,整个过程不涉及烧录器。另外把初始化按钮放在外壳内侧,平时摸不到,需要恢复出厂设置时打开外壳按一下就行。
本文还有配套的精品资源,点击获取