ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

51单片机电子密码锁:从Proteus仿真到Keil C51状态机实现

51单片机电子密码锁:从Proteus仿真到Keil C51状态机实现 简介一套基于51单片机的电子密码锁系统设计工程资料面向电子信息类课程设计与毕业设计场景围绕微控制器密码锁的仿真实现展开。资源包含完整的源程序和仿真设计在Keil环境下使用C语言编写主控逻辑在Proteus中绘制原理图并完成系统功能验证可作为电子密码锁项目的直接参考或二次开发基础。压缩包共18个文件主要涵盖.c源文件、.hex烧录文件、.dsn仿真图以及Keil和Proteus的工程配置文件.uvproj、.pdsprj等文件类型覆盖从编写、编译到仿真的完整流程包体仅148KB结构紧凑。目前已有2336人学习浏览适合需要快速理解51单片机密码锁设计思路的读者。借助这些文件可获取完整的程序设计方案、仿真电路连接关系及调试辅助文件如列表文件、内存映射文件等大幅减少自行搭建环境的时间便于对照学习按键输入、密码校验、开锁控制等关键逻辑。1. 课设密码锁拿到手先别急着改代码电子密码锁是51单片机课设里被做烂也最容易做糊的题目。这套资源包里既有Keil的C51源程序001.c、Proteus仿真原理图001.DSN也有编译好的001.hex理论上解压后打开仿真就能跑。但真正把它变成自己能讲清楚的作品需要搞明白三件事矩阵键盘的行列扫描怎么映射到P1口LCD1602显示和密码输入状态机怎么配合以及上电后密码为什么还能留在AT24C02里。很多人直接烧录别人的代码仿真一转就以为完成结果答辩提问一问就卡壳。所以下面从电路接法到C语言状态机按实际调试顺序重走一遍适合电子信息类课设、毕设以及想复习51单片机引脚复用的工程师。2. 硬件设计与Proteus电路搭建矩阵键盘、LCD1602与负载驱动2.1 接口规划与器件清单打开001.DSN排在最前面的通常是单片机最小系统一块AT89C51或STC89C52、12MHz晶振、两个30pF负载电容和上电复位电路。密码锁的外围器件不多但接口规划直接影响后续编程。常见做法是P0口整口接LCD1602的D0~D7P2.0~P2.2接RS、RW、ENP1口接4×4矩阵键盘P3.4和P3.5接AT24C02的SDA和SCLP2.3控制继电器P2.4控制蜂鸣器。这个分配在多个51单片机课程设计里都能通用接口明细如下表所示。模块引脚51单片机连接说明LCD1602D0~D7P0.0~P0.78位数据需要10k排阻上拉LCD1602RS、RW、ENP2.0、P2.1、P2.2寄存器选择、读写、使能矩阵键盘行P1.0~P1.3扫描输出行矩阵键盘列P1.4~P1.7输入检测列AT24C02SDA、SCLP3.4、P3.5I2C数据线和时钟线继电器INP2.3经三极管驱动蜂鸣器INP2.4经三极管驱动P0口数据总线必须接上拉这是51单片机硬件设计里最常见的一个坑。Proteus仿真里如果不接上拉电阻LCD1602要么白屏要么显示几个高亮方块。AT24C02的SDA和SCL也需要4.7kΩ上拉到VCC仿真中漏掉之后读回来的数据全是0xFF。安排接口时尽量把矩阵键盘放在P1口因为P1口内部有上拉扫描代码更简单把LCD数据线放在P0口则符合51单片机扩展总线的习惯也方便以后改成总线方式访问。2.2 矩阵键盘接法与扫描代码的准备4×4矩阵键盘用8根线组合出16个按键行接P1.0~P1.3列接P1.4~P1.7。扫描方式为逐行拉低读列。Proteus里放置按键可以用BUTTON元件四个一组手工排列也可以直接用Keypad组件但要小心方向旋转后引脚标签换了。推荐手工排列因为按键和引脚关系一目了然烧录出现错位时很快能排查。#define KEY_PORT P1 #define ROW1 P1_0 #define ROW2 P1_1 #define ROW3 P1_2 #define ROW4 P1_3 #define COL1 P1_4 #define COL2 P1_5 #define COL3 P1_6 #define COL4 P1_7 #define KEY_NULL 0xFF #define KEY_ENTER 0x0E #define KEY_DEL 0x0D把行线和列线单独命名后面写扫描函数时只需要操作行和列不需要记忆具体引脚。KEY_NULL表示无键按下值0xFFKEY_ENTER和KEY_DEL是ASCII码对应键盘上的#和*。为什么用ASCII因为密码本身以字符形式比较按键返回值用字符后面buf数组可以直接存字符校验函数省一层转换。在Proteus里照以下步骤重连一遍比较稳妥先将AT89C51、LCD1602、AT24C02、矩阵键盘、继电器等元件拖入图纸连接最小系统XTAL1/XTAL2接12MHz晶振和两个30pF电容RST经10uF电容到VCC、10k电阻到GND按表格接好其余信号线AT24C02的A0~A2接GNDWP接GND双击单片机加载Keil生成的001.hex。仿真图虽然已经画好但自己按这个顺序走一遍后续定位问题会快很多。2.3 负载驱动电路继电器和蜂鸣器不能直连IO51单片机IO口输出高电平时的驱动能力很弱一个IO最多提供几毫安电流直接驱动继电器线圈可能把单片机拉复位。常见做法是继电器通过PNP三极管或ULN2003达林顿管驱动。Proteus仿真里用NPN三极管2N2222就够基极串2.2k电阻接到P2.3发射极接地继电器线圈接在VCC和集电极之间线圈两端反并联一个1N4148二极管。蜂鸣器同样用三极管驱动有源蜂鸣器接在集电极回路上程序只要给高/低电平就能响无源蜂鸣器需要给方波脉冲注意源程序里如果使用的是有源蜂鸣器换成无源后声音会异常。继电器模块的12V电源要和单片机5V共地不然开关信号没有参考地仿真里虽然能跑实物接线会莫名其妙失效。三极管基极电阻的取值原则是保证IO口输出高电平时三极管能进入饱和区一般1k~10k都行阻值太大继电器吸合无力测试时表现为电磁锁“嗒”一声又不吸住。这部分直接关系到系统设计能不能从仿真过渡到实物也是答辩经常追问的“为什么要用三极管”的回答关键。3. Keil C语言下的按键扫描与密码校验状态机实现3.1 行列扫描的正确顺序拿到001.c第一件事不是读完整代码而是找到KeyScan函数。这个函数是密码锁的入口出问题后边全部白搭。硬件已经把行线放在P1.0~P1.3列线放在P1.4~P1.7扫描函数按“先整体判断是否有键按下再逐行拉低读列”的顺序实现。整体判断的好处是减少无谓的行循环在主循环里可以快速跳过没有按键的时间片。unsigned char KeyScan(void) { unsigned char row, col; const unsigned char key_map[4][4] { {1, 2, 3, A}, {4, 5, 6, B}, {7, 8, 9, C}, {*, 0, #, D} }; P1 0x0F; // 低四位输出0高四位作为输入 if ((P1 0xF0) 0xF0) // 无键按下 return KEY_NULL; DelayMs(10); // 去抖 if ((P1 0xF0) 0xF0) return KEY_NULL; for (row 0; row 4; row) { P1 ~(1 row); // 第row行拉低 Delay10us(); for (col 0; col 4; col) { if ((P1 (0x10 col)) 0x00) { while ((P1 (0x10 col)) 0x00); // 松手检测 return key_map[row][col]; } } } return KEY_NULL; }第一次读P1高4位等于0xF0说明所有列都是高电平没有任何键按下。两次判断之间加10ms消除按键按下瞬间的机械抖动。扫描阶段~(1 row)会把当前行置0其他行置1对应行线输出低电平。列判断里0x10 col表示从P1.4到P1.7逐列检测如果读到0说明这一列和当前行交叉的键被按下。最后的while等待松手避免一次长按返回几十次同一个键值这点在密码输入时尤其重要。如果仿真中行列接反表现是按下1显示其他字符优先检查仿真图里按键交叉点的行列位置而不是检查代码。3.2 状态机从输入到校验的状态迁移密码锁的程序核心不是某个算法而是一个状态机。这里把系统分成ST_IDLE、ST_INPUT、ST_CHECK、ST_UNLOCK、ST_LOCK五个状态状态迁移关系可以用下表表示。状态事件动作下一状态ST_IDLE按下数字键清空计数器显示输入掩码ST_INPUTST_INPUT按下数字键保存字符显示*ST_INPUTST_INPUT按下#检查输入位数ST_CHECK 或 ST_INPUTST_INPUT按下*退格删除最后一位ST_INPUTST_CHECK校验成功继电器吸合显示OPENST_UNLOCKST_CHECK校验失败错误计数加1显示ERRORST_INPUT 或 ST_LOCK这个枚举覆盖了密码锁的基本业务流程。ST_LOCK用于连续错误锁死达到次数后回到ST_IDLE。实际源程序可能把锁死实现为一个标志位但用状态枚举更容易读代码也方便扩展管理员密码功能。typedef enum { ST_IDLE 0, ST_INPUT, ST_CHECK, ST_UNLOCK, ST_LOCK } SYS_STATE;主循环的关键片段如下void main(void) { unsigned char key; unsigned char buf[6]; unsigned char cnt 0; SystemInit(); LcdInit(); while (1) { key KeyScan(); if (key KEY_NULL) continue; switch (sys_state) { case ST_INPUT: if (key 0 key 9) { if (cnt 6) { buf[cnt] key; LcdWriteChar(*); } } else if (key #) { if (cnt 6) { sys_state ST_CHECK; } else { LcdShowString(LEN ERR); cnt 0; } } else if (key *) { if (cnt 0) { cnt--; LcdMoveCursorLeft(); LcdWriteChar( ); LcdMoveCursorLeft(); } } break; case ST_CHECK: if (CheckPassword(buf) 0) { LcdShowString(OPEN); RelayOn(); DelayMs(2000); RelayOff(); sys_state ST_IDLE; } else { LcdShowString(ERROR); cnt 0; sys_state ST_INPUT; } break; } } }输入6位密码后按#进入校验校验函数从AT24C02读密码并与buf逐字节比较。显示用星号隐藏真实密码避免旁人从屏幕上看到键值。退格处理需要LCD光标配合先左移一位写空格再左移。如果省略这一步界面上会残留星号用户不知道到底删了几个。ST_CHECK分支里继电器吸合2秒模拟开锁这个延时期间不响应下一次按键课程设计足够用想做得更完善可以用定时器计时但会引入非阻塞状态管理代码量会明显增加。3.3 错误次数锁定最简单可靠的做法连续错误3次要锁定否则密码锁失去防爆破意义。最直接的做法是定义错误计数器每次错误加1达到3次进入ST_LOCK状态调用锁死延时。void EnterLock(void) { unsigned int i; sys_state ST_LOCK; for (i 0; i 30000; i) { DelayMs(1); // 约30秒 } err_cnt 0; sys_state ST_INPUT; }阻塞延时的缺点是锁定期内不响应任何按键但在课设展示场景中反而能防止误操作。若要用定时器实现非阻塞锁定只需在定时中断里递增状态位主循环查询剩余时间不占用CPU。尤其注意锁死期间LCD应显示“LOCK”而不是黑屏否则使用者无法区分是死机还是锁定。4. 密码掉电保存与修改AT24C02的I2C读写实践4.1 为什么数据不能只放在数组里前面校验密码时用到CheckPassword它通常从AT24C02读取密码。如果密码用unsigned char code default_pwd[]定义程序重刷后密码立即恢复默认值没有“修改密码”的意义。AT24C02容量256字节地址0x00~0x05可以存6位密码字符0x10放一个校验标志断电后依靠EEPROM保持数据。51单片机没有硬件I2C外设P3.4和P3.5用普通IO模拟SCL、SDA时序正确率取决于延时是否给够。4.2 I2C起始、停止与字节收发I2C通信的四个基本动作是起始、停止、发送字节、接收字节。起始条件是SCL高电平时SDA由高变低停止是SCL高电平时SDA由低变高。数据位在SCL低电平时改变高电平时保持稳定从机在SCL高电平期间采样。sbit SCL P3^5; sbit SDA P3^4; void I2C_Delay(void) { volatile unsigned char i 4; while (i--); } void I2C_Start(void) { SDA 1; SCL 1; I2C_Delay(); SDA 0; I2C_Delay(); SCL 0; } void I2C_Stop(void) { SDA 0; SCL 1; I2C_Delay(); SDA 1; I2C_Delay(); SCL 0; } 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; I2C_Delay(); } SCL 0; SDA 1; // 释放总线等待从机应答 SCL 1; I2C_Delay(); SCL 0; } unsigned char I2C_RecvByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { SCL 1; I2C_Delay(); dat 1; if (SDA) dat | 0x01; SCL 0; I2C_Delay(); } SCL 0; SDA 1; return dat; } void I2C_SendAck(bit ack) { SCL 0; SDA ack; // 0ACK1NACK SCL 1; I2C_Delay(); SCL 0; }起始和停止时序是I2C的“身份标识”从设备靠这个判断通信开始和结束。发送字节时高位在前每改变一位SDA前先拉低SCL等SDA稳定后再拉高SCL。这里用volatile变量做短延时避免编译器把空循环优化掉。接收字节同理方向相反。整个通信过程的关键点可以用下表概括。信号条件电平变化起始SCL1SDA由1变0数据位SCL0SDA改变并稳定采样SCL1从机读取SDA应答SCL1主机释放SDA从机拉低停止SCL1SDA由0变14.3 密码写入与校验标记AT24C02的器件地址由固定前缀1010和A2/A1/A0组成A0~A2接地后写地址是0xA0读地址是0xA1。写入时先发送器件地址再发送存储地址然后连续写数据。下面这个函数把6位密码写到0x00地址并在0x10地址写入验证标志。#define EEPROM_WADDR 0xA0 #define EEPROM_RADDR 0xA1 #define PWD_ADDR 0x00 #define FLAG_ADDR 0x10 #define FLAG_VALUE 0x5A bit EepromWritePassword(const unsigned char *pwd) { unsigned char i; I2C_Start(); I2C_SendByte(EEPROM_WADDR); I2C_SendByte(PWD_ADDR); for (i 0; i 6; i) { I2C_SendByte(pwd[i]); } I2C_SendByte(FLAG_VALUE); I2C_Stop(); DelayMs(10); // 等待AT24C02内部写周期完成 return 1; }8位地址模式下设置地址只需要发送一个字节PWD_ADDR。连续写入6个密码字节加1个标志字节共7个字节没有超过AT24C02的8字节页大小因此不用考虑跨页回卷。如果密码长度改成8位就必须在写入前判断字数防止写到页末尾回卷覆盖旧数据。读取密码函数如下。读到最后一个字节时发送NACK告诉从机“不再读”随后产生停止条件。如果最后一次也应答AT24C02会继续输出下一个地址的数据导致多读一个字节。bit EepromReadPassword(unsigned char *buf) { unsigned char i; I2C_Start(); I2C_SendByte(EEPROM_WADDR); I2C_SendByte(PWD_ADDR); I2C_Start(); I2C_SendByte(EEPROM_RADDR); for (i 0; i 6; i) { buf[i] I2C_RecvByte(); I2C_SendAck(i 5); // 最后一个字节回NACK } I2C_Stop(); return 1; }写后回读是EEPROM工程里最便宜的校验方式。写入正常时读出来的6位密码应该和new_pwd完全一致回读失败说明总线时序或上拉电阻有问题外部程序可以显示“SAVE ERROR”并保留旧密码。4.4 修改密码流程中的命令顺序在状态机中增加修改密码分支很多课设方案用功能键C进入修改模式。这里给出一种可用的命令顺序输入原密码加#验证再输入新密码加#再次输入新密码加#确认两步确认能够避免误设密码这是产品化密码锁的基本要求答辩时提到这个设计往往比单纯实现开锁更得分。case ST_CHG_OLD: if (key #) { if (CheckPassword(buf) OK) { sys_state ST_CHG_NEW; } } break; case ST_CHG_NEW: if (key #) { memcpy(new_pwd, buf, 6); sys_state ST_CHG_CFM; cnt 0; } break; case ST_CHG_CFM: if (key #) { if (memcmp(buf, new_pwd, 6) 0) { if (ChangePassword(buf)) { LcdShowString(SAVED); } else { LcdShowString(SAVE ERR); } sys_state ST_IDLE; } } break;memcpy和memcmp需要包含string.h如果觉得内存操作对51来说太“重”手写for循环也一样。修改密码过程中每一屏都要显示当前状态比如“OLD PWD”“NEW PWD”“AGAIN”否则用户只能靠猜。ChangePassword内部先写后读保证密码真正落到了EEPROM里。5. 仿真联调中的常见故障与定位技巧5.1 加载hex后没反应先检查最小系统双击Proteus里的单片机在Program File中选择001.hex晶振频率改成12MHz。如果LCD没反应先用示波器电压表测晶振引脚和RST引脚很多proteus仿真51单片机资料里AT89C51默认没有显式连接电源端子需要把VCC和GND补上。这是仿真不运行最常见的原因。5.2 矩阵键盘扫描异常先看列线电平在很多proteus仿真51单片机课程设计里矩阵键盘列线直接接P1口也能跑因为P1口内部有上拉。如果程序用了“输出低电平、读高电平”的扫描方式列线就不能在外部再加下拉。判断方法运行后不按键用虚拟逻辑分析仪看P1.4~P1.7正常应全部为高读到低电平说明外部下拉或按键短路。5.3 故障速查表现象可能原因定位方法LCD白屏P0口缺上拉排阻用电压表测P0引脚电压按键错位行列线接反对照KeyScan中的引脚宏定义输入正确却开不了锁AT24C02数据错误回读EEPROM地址0x00~0x05修改密码后重启丢失标志位没写对检查FLAG_ADDR处是否为0x5A继电器不吸合IO驱动能力不足改用ULN2003或三极管驱动一个更实用的验证技巧把默认密码从000000改成123456断电重启再试新密码同时验证EEPROM写入、回读和状态机切换。如果重新上电后还能用新密码打开这套系统从仿真到实物移植的把握就大了很多。本文还有配套的精品资源点击获取
返回列表