单片机密码锁课程设计:基于状态机的Proteus仿真与实现
简介一份面向单片机课程设计的密码锁完整方案适合计算机、电子类专业学生完成综合性实践作业。项目基于8051单片机展开通过PROTEUS仿真环境实现电路布局与功能验证涵盖密码输入、存储、校验及显示等关键环节可直接用于理解单片机的硬件控制与C语言/汇编编程流程。资源包共20个文件体积约108KB包含C语言源文件、头文件、HEX烧录文件、DSN仿真工程以及工程配置与调试记录等类型便于对照代码与仿真电路进行学习。目前已有2239人浏览学习。从内容构成看资料不仅提供可运行的密码锁仿真与代码还涉及24C02外部存储器、键盘处理、显示驱动等模块设计有助于系统掌握嵌入式开发中的存储协议、按键扫描与程序烧录思路为后续更复杂的单片机项目打下坚实基础。1. 单片机课程设计里的密码锁比的不是锁是状态机同样一个“单片机课程设计——密码锁PROTEUS仿真代码”的题目有人两个晚上交差有人一周还在调按键。区别通常不在硬件原理图上而在代码里有没有把密码锁当成一台“状态机”来写。密码锁的完整行为只有四个阶段等待输入、收集按键、比对结果、决定开锁还是报警。这四个阶段互相切换的条件非常清晰天然适合用有限状态机建模而矩阵键盘扫描、显示刷新、蜂鸣器控制都是挂在这个状态机上的外围动作。这个课设真正考察的是三件事状态如何迁移、外设如何被状态驱动、以及Proteus仿真环境下如何验证这套逻辑。适合正在做课设的本科生、想快速搭一套可答辩工程的在职读者以及准备把Proteus仿真移植到实物板子上的单片机初学者。把下面这套流程走完你会发现仿真通过只是起点真正有价值的是那段能移植到任何单片机上的状态机骨架。2. 密码锁的逻辑模型与硬件选型矩阵键盘、存储与Proteus元件2.1 密码锁的四个状态直接决定代码结构先别急着写代码把密码锁的行为画成一张状态表。第一版设计不需要花哨功能四个状态足够锁定态、输入态、校验态、报警态。锁定态就是刚上电或者开锁成功后的状态系统等待用户按“开始”键进入输入态。输入态负责接收数字按键通常按固定长度收集比如6位密码收满后自动进入校验态。校验态比对输入与预设密码一致就开锁并回到锁定态不一致就把错误次数加一连续错满三次进入报警态。报警态下蜂鸣器鸣叫键盘全部锁定持续10到30秒后回到锁定态。这个模型的好处在于每一段代码都对应一个明确的触发条件。开锁继电器、错误提示灯、蜂鸣器都只是状态迁移的副作用。后续改需求比如增加“管理员密码”“修改密码功能”只需要在状态表里增加新的状态和迁移条件而不会把整个代码搅乱。Proteus仿真里调试时状态机的优势更明显每个状态对应一组引脚电平用示波器或者电压探针一看就知道当前系统卡在哪一步。手头没有实物板时用Excel或者文本画一张状态迁移表列清楚“当前状态、触发事件、动作、下一状态”。这张表既是写代码的依据也是答辩时的设计文档。多数课程设计评分里这一张表的价值比仿真截图高得多。2.2 按键输入选型矩阵键盘还是独立按键密码锁至少需要10个数字键和若干功能键。如果用独立按键一个按键占一个IO10个数字键加确认、取消、清除就是13个引脚AT89C51总共32个IO还能勉强撑住但显示和继电器控制会变得非常紧张。所以课设和实际产品里几乎都选4x4矩阵键盘8个引脚解决16个按键多出来的按键可以分配给“确认”“取消”“退格”“修改密码”这些辅助功能。矩阵键盘的扫描原理不复杂把8个引脚分成4行4列行引脚接输出列引脚接输入。扫描时逐行拉低读回列电平哪一列变低就说明该行该列的按键被按下。这里有个容易踩的坑按键按下时列引脚被拉低但释放时引脚状态不确定必须靠上拉电阻保证默认高电平。Proteus仿真里矩阵键盘元件默认带了内部上拉模型但实物移植时很多初学者忘了开单片机内部上拉或者忘了接外部上拉电阻结果按一次触发好几次。在Proteus里仿真通过不代表实物没问题这一点后面还会展开。如果选用STC系列单片机IO内部有弱上拉直接接矩阵键盘也能工作。但AT89C51的P0口是开漏输出必须外接上拉电阻这在Proteus里经常被忽略。后面第四章会专门讲接线。2.3 Proteus元件选型AT89C51还是STC还是STM32Proteus 8 Professional里最常用的51内核元件是AT89C51配套的元件库路径是“Microprocessor ICs”分类下的AT89C51直接拖进画布即可。做课设默认选它就够了原因是它的外设模型完整P0口开漏、P1到P3口准双向、内部无EEPROM这些特性和教材完全一致答辩时老师问到的细节都能对上。想用STC系列的同学要注意Proteus的元件库里没有STC官方模型常见的做法是用AT89C51替代仿真等实物烧录时再换成STC89C52。两者引脚兼容但STC的EEPROM和看门狗在仿真里是不存在的代码里要用条件编译或者抽象层隔离。如果你的题目加了“掉电保存密码”这类要求Proteus里可以选AT24C02配合I2C总线也可以直接选带内部EEPROM的单片机模型。但Proteus对单片机内部EEPROM的仿真支持非常有限更可靠的做法是用外部EEPROM芯片总线时序看得见摸得着排错也更直观。STM32F103系列在Proteus 8里也有模型但需要手动导入芯片库默认元件列表里搜不到新手阶段不建议在课程设计里用STM32做密码锁时钟树和库函数会占据大量调试时间状态机本身的逻辑反而被冲淡。2.4 密码存储方案EEPROM的三种写法密码存储是答辩时老师最喜欢追问的点。最简陋的做法是把密码硬编码在程序里改密码要重新烧录这只能得及格分。正常课设要求“能修改密码”那就必须把密码放在可擦写的存储器里。三种常见方案按推荐度排序第一种是外部EEPROM芯片AT24C02通过I2C接口读写2Kbit容量存6位密码绰绰有余。Proteus里能完整仿真I2C时序用逻辑分析仪可以直接看SCL和SDA线上的数据。第二种是STC单片机内部EEPROM本质上是用IAP技术把Flash划分出一块区域模拟EEPROM写入前必须先擦除按扇区操作寿命约10万次。第三种是在仿真阶段直接用“内建变量文件导入”的方式把密码放在一个全局数组里每次修改后把数组内容打印到串口终端让答辩老师看到修改效果。这种方式不能掉电保存但胜在代码量少适合赶时间的人。实际工程里我一般用第二或第三种因为AT24C02的I2C时序在Proteus里调试挺浪费时间但它最能体现“工程设计能力”如果时间充裕建议做第一种。三种方案并不冲突可以在代码里封装一个PwdStore_Read和PwdStore_Write接口层底层实现随便换上层状态机不用改。3. 可复制的代码骨架矩阵键盘扫描、状态机与掉电保存3.1 矩阵键盘扫描与松开检测的完整写法矩阵键盘扫描的代码网上很多但大多有个通病只检测“按下”不检测“松开”导致一次按键被误判成多次。课程设计答辩现场按一次密码蹦出两个字符的情况多半就是这里的问题。下面这段是能直接用的扫描函数带松开检测#define KEY_PORT P1 // 返回键值 0-15无按键返回 0xFF unsigned char Key_Scan(void) { unsigned char row, col, key_value; unsigned char temp; KEY_PORT 0x0F; // 低4位拉高作为输入高4位输出低 if ((KEY_PORT 0x0F) ! 0x0F) { // 有按键按下 DelayMs(20); // 20ms软件去抖 if ((KEY_PORT 0x0F) ! 0x0F) { for (row 0; row 4; row) { KEY_PORT ~(0x10 row); // 逐行拉低 temp KEY_PORT 0x0F; if (temp ! 0x0F) { // 某列为低说明该行该列按键按下 col 0; while (temp 0x01) { // 找列号 col; temp 1; } key_value row * 4 col; // 等待按键释放 while ((KEY_PORT 0x0F) ! 0x0F); return key_value; } } } } return 0xFF; }逻辑上分为三段先整盘扫描判断有没有按键落下有就延时20ms过滤抖动然后逐行拉低找出具体键值最后死循环等待释放回到高电平。注意KEY_PORT ~(0x10 row)这句0x10 row是把某一行输出低其它行输出高配合初始的KEY_PORT 0x0F实现行扫描。列号的计算用了移位计数法比查表省一点Flash空间。while ((KEY_PORT 0x0F) ! 0x0F)的释放等待看似简单但在状态机里会阻塞整个主循环下面会讲怎么改造成非阻塞版本。如果手感觉得响应慢可以把DelayMs(20)改成10但低于5ms后机械按键的抖动没滤干净松手时容易误触。Proteus仿真里按键模型不带机械抖动这段代码看起来是冗余的实际到实物板上就能体会到它的价值。3.2 状态机的核心结构switch-case封装所有密码逻辑密码锁的状态机不建议用if-else嵌套状态一多代码就变成一团。用枚举加switch-case每种状态一个case状态迁移一目了然typedef enum { SM_LOCKED, // 锁定态等待功能键 SM_INPUT, // 输入态收集密码 SM_VERIFY, // 校验态比对密码 SM_ALARM // 报警态锁定30秒 } PwdState; PwdState current_state SM_LOCKED; unsigned char pwd_buf[6] {0}; // 当前输入 unsigned char pwd_len 0; // 已输入位数 unsigned char err_count 0; // 连续错误次数 const unsigned char pwd_saved[6] {1,2,3,4,5,6}; // 初始密码 void Pwd_StateMachine(unsigned char key) { switch (current_state) { case SM_LOCKED: if (key KEY_ENTER) { // 按确认键开始输入 pwd_len 0; current_state SM_INPUT; Display_Clear(); } break; case SM_INPUT: if (key 0 key 9) { // 数字键收集 if (pwd_len 6) { pwd_buf[pwd_len] key; Display_Append(key); // 数码管显示不用取反 } } else if (key KEY_CANCEL) { pwd_len 0; // 取消输入回到锁定态 current_state SM_LOCKED; Display_Clear(); } else if (key KEY_ENTER) { current_state SM_VERIFY; // 输入结束进入校验 } break; case SM_VERIFY: // 校验逻辑独立成函数便于测试 if (Pwd_Compare(pwd_buf, pwd_saved, pwd_len)) { Relay_On(); // 开锁动作 DelayMs(3000); // 保持3秒 Relay_Off(); err_count 0; Display_OpenSuccess(); } else { err_count; Display_OpenFail(); if (err_count 3) { current_state SM_ALARM; Alarm_Start(); } else { current_state SM_LOCKED; } } pwd_len 0; break; case SM_ALARM: // 报警期间任何按键不响应 if (Alarm_Timeout()) { // 30秒计时结束 Alarm_Stop(); err_count 0; current_state SM_LOCKED; } break; } }这个结构最舒服的地方是校验动作被放进了SM_VERIFY这个瞬时状态里按键处理、延时、显示都在各自的位置不会互相污染。报警态里Alarm_Timeout()用的是非阻塞计时方式而不是死等30秒这样数码管还能刷新蜂鸣器也能被打断。答辩时老师问“你的报警期间能不能按键解锁”直接回答“报警态忽略了所有按键事件只处理计时溢出”就够了。Pwd_Compare建议写成普通函数而不是在状态机里逐位比较方便单独测试。比较时还要处理长度不一致的情况防止数组越界。3.3 掉电保存STC内部EEPROM的抽象接口如果选用STC单片机做实物内部EEPROM的写入逻辑需要擦除扇区再写入不能像RAM那样直接改。下面是封装后的读写接口Proteus仿真里用宏切换实现#ifdef SIMULATION // 仿真模式下直接用RAM数组模拟 unsigned char pwd_storage[6] {1,2,3,4,5,6}; void PwdStore_Write(unsigned char *data, unsigned char len) { unsigned char i; for (i 0; i len; i) { pwd_storage[i] data[i]; } } void PwdStore_Read(unsigned char *data, unsigned char len) { unsigned char i; for (i 0; i len; i) { data[i] pwd_storage[i]; } } #else // STC89C52内部EEPROM地址从0x2000开始 #define EEPROM_BASE 0x2000 void PwdStore_Write(unsigned char *data, unsigned char len) { unsigned char i; IAP_CONTR 0x80; // 使能IAP IAP_CMD 0x02; // 字节写模式 for (i 0; i len; i) { IAP_ADDRL (EEPROM_BASE i) 0xFF; IAP_ADDRH (EEPROM_BASE i) 8; IAP_DATA data[i]; IAP_TRIG 0x5A; // 触发时序1 IAP_TRIG 0xA5; // 触发时序2 while (!(IAP_CONTR 0x01)); // 等待写完成 } IAP_CONTR 0x00; // 关闭IAP } void PwdStore_Read(unsigned char *data, unsigned char len) { unsigned char i; IAP_CONTR 0x80; IAP_CMD 0x01; // 字节读模式 for (i 0; i len; i) { IAP_ADDRL (EEPROM_BASE i) 0xFF; IAP_ADDRH (EEPROM_BASE i) 8; IAP_TRIG 0x5A; IAP_TRIG 0xA5; data[i] IAP_DATA; } IAP_CONTR 0x00; } #endif#ifdef SIMULATION这个技巧在课设里非常好用同一份代码既能进Proteus仿真又能烧进实物只需要在编译器里定义或者取消宏。注意STC的IAP操作要严格遵循数据手册里的触发时序两次对IAP_TRIG的写操作中间不能有别的语句否则写入不生效。仿真模式下没有真实EEPROM直接用RAM数组代替掉电数据会丢但这只是为了跑通逻辑答辩时说明白即可。修改密码的功能在状态机里增加一个SM_CHANGE_PWD状态收集两次新密码输入校验一致后调用PwdStore_Write。注意写入前最好先关闭中断因为IAP时序对时序敏感中断干扰会导致写失败。3.4 显示刷新与按键扫描的时序配合密码锁通常用数码管显示输入位数用LCD1602显示提示信息。两套方案的代码差别很大。数码管需要动态扫描每隔5到10ms刷新一位否则会闪烁。LCD1602不需要持续刷新只在内容变化时写入。推荐课设用LCD1602它自带字符库能显示“PLEASE INPUT”这种完整提示答辩演示效果远好于数码管的几个数字。但LCD1602的初始化时序有一堆细节Proteus里经常出现“只亮不显示字”的情况多半是初始化代码里延时不够。主循环的典型结构是先扫描键盘拿到键值有键值才喂给状态机然后刷新显示最后执行一次蜂鸣器和继电器的延时计时。这个循环不能用DelayMs阻塞否则键盘扫描期间数码管会熄灭。要么把显示刷新放进定时器中断要么把主循环的时间片分给显示和扫描。51单片机资源紧张我习惯用定时器0做1ms基准中断里做数码管位选扫描和计时累加主循环只负责键盘和状态机这样各司其职不容易出现按键无响应。下面是定时器中断里的片段假设P0接数码管段码P2低4位做位选void Timer0_ISR(void) interrupt 1 { static unsigned char tick 0; unsigned char code seg_code[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; TH0 0xFC; // 1ms重装值12MHz晶振 TL0 0x66; tick; if (tick 5) { // 每5ms切换一位 tick 0; display_index (display_index 1) % 4; P2 ~(0x01 display_index); // 选中当前位 P0 seg_code[display_buf[display_index]]; // 送段码 } }注意P2 ~(0x01 display_index)这条语句的含义低电平选中对应位的三极管或数码管共阴端所以要把位选值取反。段码表用的是共阴数码管的编码如果实物是共阳要把整个表取反Proteus仿真里选错型号就会出现显示乱码的现象。4. PROTEUS仿真联调加载hex、接线检查与常见仿真坑4.1 从Keil编译到Proteus加载hex的完整路径先把工具链理顺。Keil C51工程里设置好输出hex文件路径在“Options for Target - Output - Create HEX File”勾选。编译通过后在Proteus画布里双击单片机元件弹出属性窗口在Program File一栏选择编译生成的hex文件点击OK。接着点击画布左下角的运行按钮仿真就开始了。如果点了运行但单片机不干活最常见的原因是没设置晶振频率双击单片机把Clock Frequency改成12MHz和Keil里延时函数的计算值保持一致。Proteus 8 Professional的界面比旧版更复杂第一次使用容易找不到元件库。左侧面板的Component Mode按钮能打开元件选择弹窗搜索框输入AT89C51、BUTTON、LED-YELLOW、RESISTOR、7SEG-BCD等关键词逐一放置。如果搜索不到某个元件先确认Proteus安装的库版本旧版自带库默认路径为C:\Program Files (x86)\Labcenter Electronics\Proteus 8 Professional\DATA\LIBRARY某些第三方元件需要手动复制到该目录。接线遵循一个原则先放电源和地再放单片机和外围电路最后连接信号线。Proteus仿真里不接VCC和GND通常也能跑因为数字模型默认有电源但实物不是这样所以仿真图上一定要画出VCC符号和GND符号养成好习惯。4.2 电源、上拉、晶振与复位电路的标准接法P0口是开漏输出驱动LED或者作为数据总线时必须外接上拉。Proteus里使用RESPACK-8排阻最省空间一端接P0口另一端接VCC阻值选4.7k到10k之间。注意排阻的方向公共端要接VCC否则整排都不亮。晶振电路在Proteus里接不接都能仿真但实物必须接建议画上保持电路完整。复位电路用经典的10uF电容加10k电阻RST引脚接高电平复位这个电路在仿真里效果不明显但它是课程设计原理图评分的一部分。矩阵键盘的接线4根行线接P1.0-P1.34根列线接P1.4-P1.7行列交叉处放置按键元件BUTTON。Proteus的BUTTON模型默认两端短接按下时导通无需外接上拉也能读到低电平但为了和实物行为一致建议在列线到VCC之间加4个10k上拉电阻。这样仿真和实物的行为才对得上否则实物移植时会出现键值错乱。电路模块元件/参数常见错误晶振12MHz 两个30pF电容晶振频率与延时函数不匹配复位10uF电容 10k电阻RST脚接法反了P0口上拉4.7k或10k排阻忘记上拉导致LCD不显示矩阵键盘行接P1.0-P1.3列接P1.4-P1.7行列接反导致键值错乱继电器驱动PNP三极管或ULN2003直接用IO口驱动电流不够4.3 仿真通过但实物不动的四个排查点排查点一晶振不起振。Proteus里双击晶振元件看它的属性窗口有没有显示当前频率仿真运行时晶振两端电压应该是正弦波用电压探针一测便知。排查点二单片机没有加载hex。启动仿真后如果单片机引脚全部保持高电平八成是Program File没选对检查路径里有没有中文。排查点三P0口上拉缺失。LCD1602的数据线接在P0口时没上拉会导致数据读不回去最典型的症状是屏幕只有第一行黑色小方块。排查点四仿真速度太慢。Proteus的CPU占用率与动画显示有关把“Animation Options”里的帧率调低配合DelayMs延时的仿真模型能明显加快运行速度。有个容易被忽略的坑Proteus 8同时打开多个仿真工程时pds.exe会占用大量内存有时候元件拖不进画布是因为软件卡死先保存工程重启Proteus再试比反复关闭窗口有效。还有人会遇到“元件库搜索不到AT89C51”的情况多半是装了精简版或绿色版Proteus元件库被裁剪过。这种版本问题不好解决直接换官方安装包是唯一可靠的路。5. 答辩验收前值得做的三个验证从密码锁到可复用状态机这套代码如果只是用来过检其实不需要再改。但如果你想在答辩时多拿几分或者以后做毕业设计复用这段逻辑建议做三件事掉电保存验证、连续错误锁定验证、非阻塞按键验证。掉电保存验证的方法是把默认密码改成别的值运行仿真时拉掉单片机的电源端子再重新上电直接输入刚才设置的密码看能不能开锁。STC内部的IAP写入时序如果不对通常表现为第一次写入成功、第二次失败或者复位后读出来是0xFF。用Proteus自带的虚拟终端配合串口输出在每次写入后打印校验值能快速定位问题。注意STC的EEPROM写入地址是按扇区管理的跨扇区连续写会白耗时间6位密码直接放在同一个扇区里即可。连续错误锁定验证是检查err_count在报警结束后有没有清零。很多人报错状态退出后忘了重置错误计数导致第四次输入正确密码也被锁住。正确做法是报警超时退出时清空err_count同时把pwd_len清0防止残留的输入数据干扰下一次校验。用纸记录一个测试序列错误、错误、错误、正确逐次验证行为是否符合预期。最后一个建议是把按键扫描函数改成可重入的非阻塞版本。把释放等待改成“检测到按下时返回键值并记录按下时刻主循环在10ms后再次检测是否仍为低电平直到变高才认为一次有效按键结束”。这样状态机在等待释放期间还能刷新数码管整体响应感会好很多。改动量不大但说明你对“前后台系统”的理解比同组人深一层。这个技巧同样可以用在矩阵键盘扫描、红外遥控解码、旋转编码器计数等场景密码锁只是它的第一个载体。本文还有配套的精品资源点击获取