简介:这份面向单片机课程设计与电子制作入门的 Word 文档,围绕 AT89C51 单片机电子密码锁展开,适合电子信息、自动化等专业学生及嵌入式初学者参考。内容以 AT89C51 最小系统为核心,串联 4×4 矩阵键盘、LCD1602 显示与报警模块,讲解六位用户密码的设置与修改、超次报警、超次锁定及密码错误提示等功能的实现思路,并涉及键盘扫描、密码校验流程、硬件调试与软件联调方法。文档包含绪论、总体方案、硬件设计、软件设计、调试、总结、参考文献及附录,附录中给出硬件原理图与源程序,便于读者对照理解电路连接与程序结构。整份资料为 1 个 doc 文件,压缩包约 1.01MB,目录结构清晰,已有约 400 人学习下载,可作为课程设计、毕业设计选题或单片机入门实践的参考资料。
1. AT89C51密码锁的难点不在算法,在4KB Flash和128字节RAM定下的约束
宿舍楼下的储物柜原先挂机械锁,钥匙丢过两次,后来换成了柜门上贴一块小键盘加一个12V电磁锁,键盘背后的主控就是AT89C51。这类题目年年出现在单片机课程设计和毕业设计的选题表里,被默认当成"简单题",但真正动手的人很快会撞到三堵墙:片内只有4KB Flash,代码稍带一点格式化显示就吃紧;只有128字节RAM,一个6位密码缓冲区加一屏显示缓冲就得反复权衡;AT89C51片内没有EEPROM,掉电以后所有变量清零,密码不能只放在数组里。这三条约束反过来决定了矩阵键盘怎么扫描、显示怎么分时复用、密码往哪里存,也决定了这套设计能不能从PROTEUS仿真走到实物。适合正在做51单片机课设的人,也适合想借这块经典芯片把IO口、定时器、软件模拟I2C一次摸清的工程师。
2. AT89C51最小系统、4x4矩阵键盘与电磁锁驱动的硬件分配
硬件这一层如果分配错了,后面写多少软件都救不回来。AT89C51一共32根IO,看起来宽裕,但矩阵键盘要占8根、LCD1602要占3根控制线加8根数据线、电磁锁和蜂鸣器各占1根、AT24C02占2根,算下来一点都不剩。所以先定引脚表,再动烙铁。
2.1 AT89C51最小系统里最容易接错的三只脚
第一只是31脚EA/VPP。用片内Flash时它必须接VCC,如果悬空或者误接GND,CPU会从外部程序存储器0000H取指,现象是上电后完全不动,很多人会误判成晶振没起振。第二只是9脚RST,常见做法是10μF电解电容正极接VCC、负极接RST,再并一只10k电阻到GND,上电瞬间RST被拉高足够长的时间完成复位;如果同时要按键复位,按键并在电容两端即可。第三只是18、19脚XTAL1/XTAL2,晶振两端各配一只30pF瓷片电容到地,电容取值偏大启动会变慢,偏小则容易在低温下不起振。
晶振频率的取舍直接决定后面定时器和串口的参数:
| 晶振 | 机器周期 | T0定时10ms初值 | 9600bps时TH1 | 适合场景 |
|---|---|---|---|---|
| 12MHz | 1μs | 0xD8F0 | 误差偏大 | 只需定时和显示 |
| 11.0592MHz | 1.085μs | 0xDBFF | 0xFD | 需要串口打印调试 |
| 6MHz | 2μs | 0xEC78 | — | 低功耗、抗干扰优先 |
如果计划用串口打印状态来排错,直接选11.0592MHz,省掉后面算波特率误差的麻烦;纯做键盘和显示,12MHz的计算更顺手。
2.2 4x4矩阵键盘与LCD1602的引脚分配表
键盘接P1口整字节,行线走高、列线读低,扫描时一次只拉低一行;LCD1602的数据线接P0,是因为P0可以整字节赋值,省掉拆位的代码。具体分配如下:
| 功能 | 端口 | 电气注意 |
|---|---|---|
| 矩阵键盘行线 | P1.0~P1.3 | 输出,扫描时拉低 |
| 矩阵键盘列线 | P1.4~P1.7 | 输入,读之前必须写1 |
| LCD1602数据 | P0.0~P0.7 | P0开漏,必须加10k排阻上拉 |
| LCD1602 RS/RW/E | P2.0/P2.1/P2.2 | 直接驱动 |
| 电磁锁驱动 | P2.3 | 经三极管或光耦 |
| AT24C02 SCL/SDA | P2.4/P2.5 | 4.7k上拉到VCC |
| 有源蜂鸣器 | P2.6 | 三极管驱动,加续流二极管 |
| 串口调试 | P3.0/P3.1 | 只用于开发阶段 |
提示:P0口内部没有上拉,直接接LCD1602数据线时高电平建立不起来,屏幕会显示乱码或者干脆黑屏,这是新手最容易忽略的一处硬件错误。
2.3 电磁锁与继电器驱动:端口宏定义和三极管参数
AT89C51单个IO的灌电流能力在10~20mA量级,拉电流更弱,直接去带12V电磁锁一定烧口。常见做法是IO口经1k基极电阻驱动S8050,集电极接继电器线圈,线圈两端反向并一只1N4148做续流,再用继电器触点切换电磁锁的12V回路。如果是3~5V的小型电磁锁,也可以用AO3400这类MOS管直接低边驱动,但续流二极管同样不能省。
#include <reg51.h> #include <intrins.h> sbit LOCK = P2^3; /* 电磁锁驱动,高电平吸合 */ sbit BEEP = P2^6; /* 有源蜂鸣器,低电平响 */ sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; sbit SCL = P2^4; /* AT24C02 时钟 */ sbit SDA = P2^5; /* AT24C02 数据 */ #define LOCK_ON() (LOCK = 1) #define LOCK_OFF() (LOCK = 0) #define BEEP_ON() (BEEP = 0) #define BEEP_OFF() (BEEP = 1)用宏而不是直接写位操作,是为了后面把驱动换成低电平有效时只改一行。P0口接LCD时记得在初始化里先写0xFF释放总线,否则读忙标志会一直被拉低。硬件通电前先用万用表量一遍P0排阻到VCC的阻值,确认是10k而不是1k,阻值太小会把IO口灌穿。
3. 定时器T0时基下的矩阵键盘扫描与LCD1602密码掩码显示
键盘和显示如果都用延时函数硬等,按键一多就会互相拖慢,密码输入时的响应会明显发涩。把T0做成一个10ms的软时基,消抖计数、显示刷新、开锁倒计时全部挂在这个时基上,主循环只负责状态机,思路会清爽很多。这也是蓝桥杯单片机赛题里矩阵键盘题的通用套路。
3.1 用T0做10ms时基:消抖、刷新与倒计时合一
unsigned char KeyTick = 0; /* 按键消抖计数 */ unsigned char LockTick = 0; /* 开锁保持、错误锁定时长 */ void T0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; /* T0 方式1:16 位定时器 */ TH0 = 0xDB; /* 11.0592MHz 下约 10ms */ TL0 = 0xFF; ET0 = 1; EA = 1; TR0 = 1; } void T0_ISR(void) interrupt 1 { TH0 = 0xDB; /* 方式1不自动重装,必须手动补 */ TL0 = 0xFF; KeyTick++; if (LockTick) LockTick--; }方式1不会自动重装初值,中断里忘了补TH0/TL0,周期会立刻跑偏。11.0592MHz下一个机器周期约1.085μs,10ms需要约9217个机器周期,65536减9217等于56319,也就是0xDBFF。如果换成12MHz,初值改成0xD8F0,代码里其他依赖时间的参数都要跟着重算,所以频率一旦定下来就别来回改。
3.2 行扫描法的C51实现与按键编码
#define KEY_NONE 0xFF unsigned char KeyScan(void) { unsigned char row, col, code = KEY_NONE; for (row = 0; row < 4; row++) { P1 = ~(0x01 << row); /* 当前行拉低,其余行和列线为高 */ _nop_(); _nop_(); /* 等电平稳定 */ col = (P1 >> 4) & 0x0F; /* 读列线,低电平有效 */ if (col != 0x0F) { /* 有键按下 */ switch (col) { case 0x0E: code = row * 4 + 0; break; case 0x0D: code = row * 4 + 1; break; case 0x0B: code = row * 4 + 2; break; case 0x07: code = row * 4 + 3; break; default: break; } break; /* 单键优先,扫到就退出 */ } } P1 = 0xFF; /* 恢复全高,避免影响下一次扫描 */ return code; }P1赋值时把列线一起写成了高,正好满足"读输入前先写1"的要求,不需要额外一条语句。返回的编码0~15对应16个键,0~9给数字,10给清除键,11给确认键,13给改密键。扫描函数本身不消抖,主循环里配合KeyTick判断,连续两次读到同一个编码且间隔不小于20ms才认定有效,这样能滤掉绝大多数抖动。矩阵键盘的按键排列如果和实物不一致,只需改switch里的映射,不用动扫描逻辑。
3.3 LCD1602初始化指令与密码掩码显示
void LCD_WriteCmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; P0 = cmd; LCD_E = 1; _nop_(); _nop_(); LCD_E = 0; DelayMs(2); /* 普通指令约 40us,留足余量 */ } void LCD_Init(void) { LCD_WriteCmd(0x38); /* 8位数据、2行显示、5x7点阵 */ LCD_WriteCmd(0x0C); /* 开显示、关光标、不闪烁 */ LCD_WriteCmd(0x06); /* 写入后地址自动加1,屏幕不滚动 */ LCD_WriteCmd(0x01); /* 清屏 */ }0x38和0x0C这两条是必配的,少一条屏幕要么只显示一行,要么光标乱闪。显示密码时不要直接写数字,用掩码代替,输入第几位就在对应位置写一个*,长度用变量控制,退格时把该位置写空格覆盖。状态机的转移关系可以固化成一张表:
| 当前状态 | 触发条件 | 动作 | 下一状态 |
|---|---|---|---|
| 待机 | 无按键 | 显示"INPUT PASSWORD" | 待机 |
| 输入 | 数字键 | 追加掩码星号 | 输入 |
| 输入 | 确认键 | 与存储密码比对 | 开锁或报警 |
| 开锁 | 比对通过 | 继电器吸合,LockTick置位 | 倒计时 |
| 报警 | 比对失败 | 蜂鸣器响,错误计数加一 | 待机 |
| 锁定 | 错误满3次 | 禁止键盘,倒计时60s | 待机 |
4. AT24C02存密码:AT89C51软件模拟I2C与开锁时序控制
4.1 为什么AT89C51必须外挂AT24C02
AT89C51片内是Flash程序存储器,不是数据存储器,运行期无法按字节写入,断电后RAM内容全部丢失。所以"修改密码后下次上电仍然有效"这个需求,必须靠外部非易失存储实现。常见的几条路线差别不小:
| 方案 | 存储介质 | 优点 | 代价 |
|---|---|---|---|
| AT89C51 + AT24C02 | 外置I2C EEPROM | 通用、擦写次数高 | 占2个IO,要软件模拟时序 |
| 换用STC89C52RC | 片内Data Flash | 省一颗芯片和2个IO | 芯片和烧写方式都要换 |
| AT89C51 + FM24C02 | 外置I2C FRAM | 无写等待,速度极快 | 单价明显更高 |
如果只是课设,外挂AT24C02最能体现对I2C时序的理解;如果已经把程序烧进STC单片机,直接用片内Data Flash更省事,但那是另一套寄存器操作。
4.2 软件模拟I2C的起始、字节写与读校验
void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void I2C_Start(void) { SDA = 1; SCL = 1; I2C_Delay(); SDA = 0; I2C_Delay(); /* SCL为高时SDA下降沿 = 起始 */ SCL = 0; } void I2C_Stop(void) { SDA = 0; SCL = 1; I2C_Delay(); SDA = 1; I2C_Delay(); /* SCL为高时SDA上升沿 = 停止 */ } void I2C_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SDA = (dat & 0x80) ? 1 : 0; dat <<= 1; SCL = 1; I2C_Delay(); SCL = 0; I2C_Delay(); } SDA = 1; /* 释放SDA读从机应答 */ SCL = 1; I2C_Delay(); SCL = 0; }AT24C02的写设备地址是0xA0,读是0xA1。字节写完整流程是起始、发0xA0、发字节地址、发数据、停止,然后必须等5ms让内部写周期结束,紧接着去读会得到旧数据,这是最典型的坑。密码建议存在0x00~0x05六个字节,0x06放一个简单的校验和,上电读取时先算一遍校验和,不一致就回退到默认密码,避免EEPROM被干扰后整机锁死。
4.3 密码比对、错误锁定与继电器开锁时序
比对不要用memcmp,C51里没有标准库的字符串比较,直接逐字节循环更可控。开锁动作由T0时基驱动,参数集中成一张表,改需求时只动这里:
| 参数 | 建议取值 | 说明 |
|---|---|---|
| 密码长度 | 6 | 存AT24C02前6字节 |
| 开锁保持 | 3s | 定时器计时到点自动断开 |
| 错误锁定 | 60s | 锁定期间键盘完全不响应 |
| 最大连错 | 3次 | 防止暴力试码 |
| 按键消抖 | 20ms | 两次采样一致才确认 |
| EEPROM写等待 | 5ms | 写完必须延时的死区 |
if (MatchPassword(input_buf)) { LOCK_ON(); LockTick = 300; /* 10ms × 300 = 3s */ ErrCount = 0; LCD_ShowStr(0, 1, "UNLOCKED"); } else { ErrCount++; BEEP_ON(); if (ErrCount >= 3) { LockTick = 6000; /* 10ms × 6000 = 60s */ LCD_ShowStr(0, 1, "LOCKED"); } }开锁保持时间和锁定时间都挂在同一个10ms时基上,主循环里判断LockTick是否归零即可,不需要再开一个定时器。继电器吸合和断开之间加一个几毫秒的间隔,可以明显减小对电源的冲击。
5. AT89C51密码锁从PROTEUS仿真到实物的联调顺序与三个抗干扰技巧
5.1 先仿真后实物的分段验证顺序
一次性把整机接好再上电,出问题几乎没法定位。稳妥的顺序是分段点亮:第一步只焊最小系统,写一段P1.0每500ms翻转的程序,用示波器或LED确认晶振和复位正常;第二步接LCD1602,只跑初始化和固定字符显示;第三步加矩阵键盘,用串口或LED把键码打出来核对;第四步接AT24C02,读写一个固定字节并回读校验;最后才接继电器和电磁锁。PROTEUS里可以把电磁锁换成LED先跑通逻辑,仿真通过再动实物,能省掉大量反复拆焊的时间。
5.2 串口调试打印与波特率参数
void UART_Init(void) { SCON = 0x50; /* 方式1,允许接收 */ TMOD &= 0x0F; TMOD |= 0x20; /* T1方式2,8位自动重装 */ TH1 = 0xFD; /* 11.0592MHz 下 9600bps */ TL1 = 0xFD; TR1 = 1; } void UART_SendByte(unsigned char c) { SBUF = c; while (!TI); TI = 0; }把每次按键编码、EEPROM读回值、当前状态机编号都通过串口打出来,比盯着1602猜问题快得多。注意T1被占用后就不能再拿它做别的定时,需要额外定时就再挂T0的软计数。
5.3 三个能救命的工程技巧
第一个是电磁锁反电动势。12V线圈断开的瞬间会产生几十伏的反压,轻则复位,重则打穿IO口。续流二极管要就近焊在线圈两端,条件允许再加一级光耦,把驱动地和单片机地分开,这一条比任何软件滤波都管用。
第二个是长按误判。有人按住数字键不放,扫描函数会每轮都返回同一个编码,密码被连续填入。解决办法是记录上一次的键码,只有"本次有键、上次无键"时才认为是一次有效按下,抬起沿清零状态。这个改动只有几行,效果立竿见影。
第三个是EEPROM写保护。改密后如果立刻断电,AT24C02内部的5ms写周期还没结束,数据可能只写了一半。写完延时后再回读一次,比对不一致就重写,最多重试三次。同时把默认密码写死在Flash里,上电校验和不通过时自动恢复,能避免整机变砖。
实物上电前,先用万用表确认P0排阻、SDA/SCL的4.7k上拉、继电器续流二极管三处都在位,再插上电磁锁,基本可以一次点亮。
本文还有配套的精品资源,点击获取