单片机课程设计密码锁:Proteus仿真与状态机实现详解

单片机课程设计密码锁:Proteus仿真与状态机实现详解 简介单片机课程设计中的密码锁项目以PROTEUS仿真与源码为核心面向计算机、电子工程专业学生帮助完成电路设计、编程调试和功能验证等实践环节。资源共20个文件压缩包约108KB包含PROTEUS仿真工程.dsn、Keil工程.uv2、C语言主程序.c、汇编启动文件.a51、头文件.h以及编译生成的.hex、.lst、.obj等文件目录结构清晰可直观查看整套项目从源码到仿真的完整链路。当前已有2239人下载学习。通过该方案读者可以掌握单片机I/O控制、矩阵键盘检测、24C02存储密码、显示反馈与报警电路等关键知识点利用仿真环境反复验证密码设置、校验、错误锁定等流程。尤其适合课程设计参考、毕设起步或嵌入式入门练习在无需硬件条件下即可完成系统级的综合性训练也是理解I2C协议与状态机编程的良好范例。1. 单片机课程设计密码锁为什么先从PROTEUS仿真开始单片机课程设计里密码锁是点名率最高的题目之一。原因很直接功能边界清晰按键、显示、控制都看得见摸得着代码量又足够撑起一篇课程报告。但很多同学拿到题目后的第一反应是买一块开发板把矩阵键盘和数码管往板子上一插然后被杜邦线松动、数码管段码闪烁、按键抖动这类硬件问题拖掉整整一个周末。我反而建议先打开Proteus把原理图、HEX文件和调试环境全部跑通再回头整理实物。因为仿真阶段暴露的逻辑问题能占到总调试量的七八成而硬件问题其实就那么几种等逻辑稳定后处理起来反而快。Proteus仿真并不等于纸上谈兵它自带虚拟示波器、逻辑分析仪和虚拟终端可以实时观察单片机引脚电平也能随时修改参数重新运。对课程设计而言你需要的不是极致的实物还原而是在有限时间内把单片机 按键 数码管 继电器这套核心逻辑做对、讲清楚。这篇文章就按一个典型的密码锁课设来拆从键盘扫描、密码校验到Proteus联调给出可直接改用的代码结构和仿真注意事项。新手能照着步骤做有经验的人也能从中看到一些边界条件。2. 密码锁的硬件电路与矩阵键盘扫描怎么搭2.1 单片机选型51还是STM32课设和实践的差别密码锁的逻辑复杂度不高对主频和内存几乎没有要求所以芯片选型非常开放。如果是校内课程设计最常见的是AT89C51或STC89C52理由有三个Proteus元件库直接内置不需要额外导入Keil工具链成熟范例多代码量小报告好写。如果题目没有强制指定不建议在课设里用STM32因为Proteus仿真STM32需要额外装芯片库而且GPIO、时钟、中断的初始化代码比整个密码锁逻辑还长投入产出比不高。下面这张表是我在选择密码锁仿真方案时会对比的几个维度对比项AT89C51 / STC89C52STM32F103Proteus元件支持内置直接放置需要手动添加芯片库开发工具Keil STC-ISPKeil STM32CubeMX键盘扫描直接P1口读写延时消抖需要配置GPIO模式和时钟密码存储外部EEPROM或常量片内Flash/外部EEPROM课程设计代码量约200行至少400行以上选51还有一个隐藏问题Proteus里的AT89C51没有片内EEPROM如果你打算做“密码掉电保存”要么换一个带EEPROM的51型号要么在外部挂一片AT24C02。这一点在验收时经常被问到提前决定好方案能省很多事。另外晶振选择也要注意密码锁本身对波特率不敏感但如果后面要接串口调试最好选11.0592MHz而不是12MHz不然串口波特率误差偏大。2.2 矩阵键盘四线扫描逐列拉低与行读取密码锁的输入一般用4×4矩阵键盘10个数字键之外剩下的键可以定义成“确认”和“取消”。矩阵键盘为什么省IO口4×4键盘如果用独立按键要占16个引脚而矩阵接法只用8个引脚51单片机的P1口刚好一组。扫描的原理可以归纳成一句话先将所有列线置高电平再逐列拉低同时读取行线状态如果某根行线变为低电平就说明该行与当前拉低的列交叉处的按键被按下。#define KEYPAD P1 code unsigned char col_code[4] {0xFE, 0xFD, 0xFB, 0xF7}; unsigned char keyscan(void) { unsigned char col, row, tmp; for (col 0; col 4; col) { KEYPAD col_code[col]; // 将当前列拉低其余列输出高 tmp KEYPAD 0xF0; // 只读高四位即行线 if (tmp ! 0xF0) { // 说明有按键按下 delay_ms(10); // 软件消抖 tmp KEYPAD 0xF0; if (tmp ! 0xF0) { while ((KEYPAD 0xF0) ! 0xF0); // 等待按键释放 switch (tmp) { case 0xE0: row 0; break; case 0xD0: row 1; break; case 0xB0: row 2; break; case 0x70: row 3; break; default: return 0xFF; } return row * 4 col; // 返回按键坐标 } } } return 0xFF; }这段代码的核心是KEYPAD col_code[col]将P1口低四位中的一位输出低电平其余为高然后立刻读回P1口的高四位。P1口内部自带弱上拉行线没被按下时读回来是1按下后会被拉低到0。row * 4 col只是一个位置编号并不是最终的键值。比如位置0可能是数字1也可能是数字0取决于你的键盘布线。所以在实际工程里我会再加一张二维映射表code unsigned char key_map[4][4] { {1, 2, 3, 10}, // 10 确认键 {4, 5, 6, 11}, // 11 取消键 {7, 8, 9, 12}, // 12 预留功能键 {13, 0, 14, 15} // 根据实际排布调整 };这样keyscan返回的坐标再查一次表就能得到真正的键值。注意KEYPAD col_code[col]这行背后的一个坑如果按键没有释放下一次主循环又调用keyscan会重复返回同一个键。上面代码用while等待释放虽然简单但在按住不放时会阻塞整个程序。更稳妥的方式是检测“按下沿”即只处理从未按下到按下的那一刻松开时不处理这个放到第3章状态机里一起讲。2.3 数码管动态显示与蜂鸣器驱动电路密码锁的输出一般分三部分数码管显示输入状态、蜂鸣器反馈按键、继电器控制电磁锁。Proteus里最常用的是7SEG-MPX4-CA也就是共阳四位一体数码管。共阳还是共阴决定了段码表很多人在这里踩坑。共阳数码管要让某个段亮必须给该段低电平因此数字0对应的段码是0xC0共阴则刚好相反需要高电平数字0是0x3F。code unsigned char seg_code[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 };显示时不能把四个位同时点亮要靠动态扫描。人眼视觉暂留大约20ms只要轮流点亮每一位每个位停留1~2ms看起来就是四个数字同时亮。unsigned char display_buf[4]; void display_scan(void) { unsigned char i; for (i 0; i 4; i) { P2 ~(1 i); // 位选低电平选中 P0 seg_code[display_buf[i]]; // 段选输出 delay_ms(1); } }位选信号接到P2口低四位段选接到P0口。注意P0口是开漏输出内部没有上拉电阻仿真里不接排阻也能亮但会造成段码亮度不一致真实硬件上必须接一个10kΩ左右的上拉排阻。蜂鸣器驱动也要分有源和无源有源蜂鸣器给高电平就响无源蜂鸣器需要提供频率方波。Proteus库里的SOUNDER多半是有源如果代码里用了for循环翻转IO却发现声音不对先确认元件类型不要盲目调频率。继电器控制建议不要直接接单片机引脚而是用三极管或ULN2003做驱动仿真中看不出来实物上51单片机的IO口驱动能力只有几十毫安直接带继电器很容易把引脚烧掉。3. 密码锁程序设计与状态机实现3.1 密码存储方案常量数组、EEPROM与掉电保存课程设计对密码存储的要求一般有三种级别。最低要求是固定密码直接写成一个常量数组编译时烧到程序存储区断电不会丢失但无法修改。中等要求是支持修改密码密码存在外部EEPROM里每次开机读出来改密码时写回去。最高要求在课设里很少见是把密码加密后存储防止直接读EEPROM内容看到明文。对于多数课设做到第二级就够了。用51单片机做“可修改密码”最标准的方案是外挂AT24C02。AT24C02是2Kbit的EEPROM256字节存4位密码绰绰有余。它用I2C协议通信51本身没有硬件I2C外设需要用软件模拟SCL和SDA时序。初次接触I2C会觉得坡度陡但AT24C02只有两种操作写一个字节、读一个字节。下面是代码框架sbit SCL P2^0; sbit SDA P2^1; void i2c_start(void) { SDA 1; SCL 1; delay_us(5); SDA 0; delay_us(5); SCL 0; } void i2c_stop(void) { SDA 0; SCL 1; delay_us(5); SDA 1; delay_us(5); } void i2c_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SDA (dat 0x80) ? 1 : 0; dat 1; SCL 1; delay_us(5); SCL 0; } }上电时从AT24C02读出密码覆盖默认的password[]数组。修改密码时按顺序写入四个地址然后立即读回验证一遍。这里有个容易忽视的细节AT24C02写入一个字节后内部需要大约5ms的写周期这期间不响应I2C请求。所以每写完一个字节要加延时否则紧接着写下一个字节会失败。3.2 状态机定义与主循环结构密码锁最忌讳的是用一堆if去判断“能不能按”“按了会怎样”。我习惯把整个系统切成几个状态每个状态只做自己的事靠状态值驱动。下面是一个四状态模型状态状态值当前允许的操作退出条件IDLE0输入数字、确认、取消确认键按下后进入VERIFYVERIFY1不允许输入只做密码比对比对完成进入UNLOCK或LOCKEDUNLOCK2保持继电器吸合显示OPEN延时结束回到IDLELOCKED3拒绝所有按键倒计时结束回到IDLE主循环的写法要避免“阻塞式延时”。例如开锁后保持5秒如果直接用delay_ms(5000)这5秒内单片机不扫描键盘也不会刷新数码管。更专业的做法是用定时器维护一个全局计时变量主循环不断查询时间差是否到达。看下面的代码结构unsigned char sys_state 0; unsigned char input_buf[4]; unsigned char input_len 0; unsigned int sys_tick 0; // 定时器中断中递增 void main(void) { unsigned char key; system_init(); while (1) { key keyscan(); if ((key ! 0xFF) (sys_state 0)) { if ((key 0) (key 9)) { if (input_len 4) { input_buf[input_len] key; display_buf[input_len - 1] key; } } else if (key 10) { // 确认 sys_state 1; } else if (key 11) { // 取消 input_len 0; clear_display(); } } switch (sys_state) { case 1: verify_password(); break; case 2: relay_on(); if (sys_tick - unlock_start 500) { // 500 5秒 relay_off(); sys_state 0; input_len 0; clear_display(); } break; case 0: display_scan(); break; default: break; } } }这里sys_tick由定时器中断每10ms加1500就是5秒。相比delay_ms这个写法不会阻塞键盘扫描在开锁期间按取消键也能响应。注意sys_tick - unlock_start用无符号减法即使发生回绕也是安全的这是嵌入式里很常见的技巧。3.3 密码验证与错误锁定逻辑密码验证不是简单的字符串比较而是逐字节比较缓冲区与存储密码。缓冲区必须满4位才允许验证否则按确认键应该忽略。错误处理上我一般设置3次输入机会超过3次进入LOCKED状态并在倒计时结束前禁止任何按键操作。unsigned char error_count 0; void verify_password(void) { unsigned char i; if (input_len ! 4) { // 输入未满4位不验证 sys_state 0; return; } for (i 0; i 4; i) { if (input_buf[i] ! password[i]) { break; } } if (i 4) { // 比对通过 error_count 0; unlock_start sys_tick; sys_state 2; beep_ok(); } else { error_count; beep_error(); if (error_count 3) { sys_state 3; lock_remain 60; // 锁定60秒 display_lock(); } else { input_len 0; sys_state 0; clear_display(); } } }注意lock_remain最好也由定时器更新不要在主循环里写for死等。LOCKED状态下keyscan仍然会被调用但主循环会过滤掉所有按键只有倒计时归零才允许跳出锁定。错误计数器的清零时机要合理首次输错3次锁定如果用户按取消键应该把错误计数清零否则取消键形同虚设。3.4 修改密码与超时自动清屏加了“可修改密码”后原来的四状态模型要扩展一个CHANGE_PWD状态。常见流程是先输入原密码并验证通过然后输入新密码两次两次一致才写入EEPROM。我倾向于把整个修改过程固化成一个线性序列而不是增加更多状态变量第1步验证原密码失败则直接退回IDLE。第2步输入新密码4位按确认第3步再输入一次新密码4位按确认第4步比对两次新密码一致则写入EEPROM不一致则提示后回到IDLE。这里需要两个缓冲区new_pwd1[4]和new_pwd2[4]同时要有一个步骤标识change_step。用状态机很好实现只需在IDLE状态里检测一个change_flag。另外用户输入过程中很可能中途走开所以还要加“超时自动清屏”每次按键记录last_key_time sys_tick如果主循环发现sys_tick - last_key_time 3000且当前还在IDLE就自动清空缓冲区。这个功能虽然不是硬性要求但答辩时提到“防止误操作残留缓存”非常加分。4. PROTEUS仿真完整搭建原理图、HEX烧录与调试点4.1 元件清单与最小系统电路Proteus新建工程后第一个操作是放置元件。密码锁仿真涉及的元件不多打开元件库搜索即可。下面这张清单可以直接照着放元件名称搜索关键词数量作用AT89C51AT89C511主控芯片4位共阳数码管7SEG-MPX4-CA1显示输入/状态按键BUTTON16组成4×4键盘排阻RESPACK-82P0口上拉/位选上拉有源蜂鸣器SOUNDER1声音反馈继电器RELAY1控制电磁锁NPN三极管2N22221驱动继电器晶振CRYSTAL112MHz电容CAP2晶振负载电容电解电容CAP-ELEC1复位电容电阻RES若干限流/分压最小系统电路包含三部分电源VCC和GND、复位电路、时钟电路。AT89C51的复位引脚是RST上电瞬间通过10μF电容充电使RST保持短暂高电平完成复位正常运行时RST要拉低。时钟电路就是18、19引脚接晶振和两个30pF电容Proteus仿真对晶振频率不敏感但实物必须接所以仿真里也画上能让原理图更规范。4.2 引脚分配与Proteus连线我常用的引脚分配方案如下功能模块信号单片机引脚矩阵键盘行0~3P1.4~P1.7矩阵键盘列0~3P1.0~P1.3数码管段码 a~g, dpP0.0~P0.7数码管位选 S1~S4P2.0~P2.3蜂鸣器控制端P3.7继电器控制端P3.6I2C EEPROMSCLP2.0I2C EEPROMSDAP2.1注意P2.0、P2.1既用来做位选又用来做I2C这显然会冲突。为了避免接线混乱建议在方案中预留出专门引脚如果要用AT24C02就把位选改用P2.0~P2.3之外的空闲脚比如P2.4~P2.7。规划引脚时最好先画一张引脚占用表把P0到P3所有引脚用途列出来再开始连线。否则画到一半发现引脚被占返工成本高。4.3 HEX烧录与仿真调试常见问题在Keil中编译生成HEX文件前必须勾选“Create HEX File”路径最好不要有中文。然后回到Proteus双击AT89C51在Program File里选择编译好的HEX文件芯片频率改成12MHz。第一次仿真常见的报错和现象如下“Legacy PROTEUS VSM Model cannot simulate a 12.0MHz 8051 device”之类提示优先把晶振和单片机频率改成12MHz或者换成AT89C52。仿真运行后数码管全灭先暂停程序用电压探针点P0口看是否有电平翻转如果没有检查程序是否执行到显示函数。按键按下去没反应先用虚拟终端打印按键值确定是扫描函数的问题还是显示函数的问题。继电器不动作检查三极管基极有没有电压继电器一侧有没有接VCC和GND。有人说Proteus里芯片不接复位电路也能运行确实如此但课程设计验收时老师会看原理图缺了复位电路等于硬件设计不完整。正确做法还是一步一步把最小系统画全。4.4 用虚拟终端打印按键值定位问题按键扫描的时序问题很难靠肉眼排查我一般会在代码里临时加一段串口输出把所有按键位置号打印出来。Proteus的虚拟终端可以模拟串口接收。先在单片机程序里初始化串口波特率9600然后把key_map[row][col]直接发送到SBUF虚拟终端就能收到字符。这样按下一个键你能立刻看到返回的是数字还是0xFF从而快速区分是按键接线错误还是代码错误。虚拟终端放在TXD引脚上注意接地和电源。如果打印出来全是乱码多半是晶振频率和波特率不匹配。用11.0592MHz晶振9600波特率的误差几乎为零。调试完成后这段串口代码可以保留也可以删除不影响密码锁功能。5. 密码锁进阶掉电保存与仿真自检清单5.1 AT24C02实现密码掉电保存如果你决定给密码锁加上“改密码后断电不丢失”AT24C02是性价比最高的方案。Proteus里放置AT24C02把A0~A2接地器件地址是0xA0写地址SDA、SCL各接一个4.7kΩ上拉电阻到VCC。写入密码时A10分别访问四个字节地址每写一个字节后延时5msvoid eeprom_write_pass(unsigned char *pwd) { unsigned char i; for (i 0; i 4; i) { i2c_start(); i2c_write_byte(0xA0); // 器件写地址 i2c_write_byte(i); // 密码字节地址 i2c_write_byte(pwd[i]); i2c_stop(); delay_ms(5); // 等待内部写周期 } }上电时按相同地址读四个字节读操作先发送器件地址和寄存器地址再发送读地址0xA1。要注意Proteus里AT24C02断电后内容是否保留取决于模型设置有的版本重新仿真后RAM清零所以“掉电保存”必须在实物上验证才算数。5.2 错误锁定倒计时的时间片写法前面的锁定功能我用了一个lock_remain变量。在定时器中断中每满1秒就把lock_remain减1主循环只检查它是否为0。这样锁定期间可以让按键不响应同时数码管显示剩余秒数。定时器0初始化代码可以先设置50ms中断一次用计数器累加20次生成1秒时间基准。锁定期内按键扫描照常执行但主循环在最顶层过滤if (sys_state 3) { if (lock_remain 0) sys_state 0; // 这里不处理任何按键 continue; }5.3 仿真验收自检清单最后给出一份可以直接用来验收的清单照着逐项打勾上电后数码管显示默认状态无残留数字。按数字键1数码管第一位显示1再按2第二位显示2。输入正确密码1234按确认继电器吸合蜂鸣器响一声。输入错误密码连续3次数码管显示锁定标志此时按任何键无效。锁定倒计时结束后自动回到待输入状态错误次数清零。进入修改密码流程后原密码验证失败不能进入下一步新密码两次输入不一致会提示。修改密码后重新仿真密码仍为新密码如果使用EEPROM且仿真支持保持。如果你发现“仿真通过、实物异常”优先检查晶振频率、上拉电阻和IO口驱动能力这三个变量多数“仿真正常实物冒烟”的问题都能落到它们头上。本文还有配套的精品资源点击获取