使用按键、LCD1602、EEPROM (AT24C02)做一个简易密码锁程序。
(1)用户可以通过输入 6 位数字密码来打开保险箱。
(2)当密码正确时,保险箱柜门打开(步进电机正转 90 度) ;当密码不正确时,保险箱
柜门不打开;保险箱柜门打开后,若按下关门键,则保险箱柜门关上(步进电机反转 90 度)。
(3)用户密码可以自行修改(密码输入 2 次,第 2 次和第 1 次相同才有效)。
(4)有相应的密码输入显示窗口,输入数字用相应符号替代以免被偷窥。
我是通过实物完成的。
我的工程下有5个文件:
1.主函数.c
#include <reg52.h>
#define uchar unsigned char
#define ulong unsigned long
signed long beats = 0;
extern void InitLcd1602();
extern void LcdShowStr(uchar x, uchar y, uchar *str);
extern void E2Read(uchar *buf, uchar addr, uchar len);
extern void E2Write(uchar *buf, uchar addr, uchar len);
extern void KeyDriver();
extern void KeyScan();
extern void LcdFullClear();
extern void LcdAreaClear(uchar x,uchar y,uchar len);
void ConfigTimer1(uchar ms);
void StartMotor(signed long angle);
void LongToString(uchar *str, ulong dat);
void KeyAction(uchar keycode);
void NumberAction(uchar n);
void Reset();
void save();
void pass();
void HidePass(uchar *str, ulong password);
bit pass_the = 0;//这个值判断是否校验密码通过,通过时才能关箱门和修改密码
uchar step = 0;//0表示修改密码,1表示第二次输入,2表示完成后清零,3表示输入
uchar count = 0;//计算输入的位数
ulong password1 = 0;//六位密码
ulong password2 = 0;
uchar password[6];//password只要三位存储,便于显示
uchar str[20];
uchar T1RL;
uchar T1RH;
ulong real_pass;
void main()
{
uchar buf[6];
EA = 1;//使能总中断
TMOD = 0x01;//设置T0为模式1
TH0 = 0xf8;//为T0赋初值0xF8CD,赋值2ms
TL0 = 0xcd;
ET0 = 1;//使能T0中断
TR0 = 1;//启动T0
InitLcd1602();
ConfigTimer1(1);
E2Read(buf, 0x8e, sizeof(buf));
real_pass = buf[0] * 100000 + buf[1] * 10000 + buf[2] * 1000 + buf[3] * 100
+ buf[4] * 10 + buf[5] * 1;//读取真实的密码
LcdShowStr(0, 0, "Input:");//显示到液晶
while(1)
{
KeyDriver();//调用按键驱动函数
}
}
void StartMotor(signed long angle)
{
//在计算前关闭中断,完成后再打开,以避免中断打断计算过程造成错误
EA = 0;
beats = (angle * 4076) / 360;
EA = 1;
}
void HidePass(unsigned char *str, unsigned long dat)
{
signed char i = 0;
uchar buf[12];
do {
buf[i++] = dat % 10;
dat /= 10;
} while (dat > 0);
while(i-->0)
{
*str++ = '*';
}
*str = '\0';
}
void Reset()
{
uchar i;
for(i=0; i<6; i++)
password[i] = 0;
step = 0;
password1 = 0;
password2 = 0;
count = 0;
LcdFullClear();
if (pass_the == 0)
LcdShowStr(0, 0, "Input:");
else if (pass_the == 1)
LcdShowStr(0, 0, "Modify:");
}
void save()
{
uchar buf[6];
password[0] = password2 / 100000;
password[1] = password2 / 10000 % 10;
password[2] = password2 / 1000 % 10;
password[3] = password2 / 100 % 10;
password[4] = password2 / 10 % 10;
password[5] = password2 % 10;
E2Write(password, 0x8e, sizeof(password));
LcdShowStr(0, 0, "Password saved");
LcdAreaClear(0, 1, 16);
E2Read(buf, 0x8e, sizeof(buf));//重新读取密码
real_pass = buf[0] * 100000 + buf[1] * 10000 + buf[2] * 1000 + buf[3] * 100
+ buf[4] * 10 + buf[5] * 1;
step = 4;//应该有一个新的状态
}
void pass()
{
if(password1 == real_pass)
{
LcdShowStr(0, 0, "Up close");
LcdShowStr(0, 1, "Down modify");
StartMotor(90);//正转90度打开箱门
pass_the = 1;//校验通过,这个值将在修改密码和关箱门后清零
step = 3;
}
else
{
LcdShowStr(0, 0, "Fail pass");
step = 2;
}
}
void KeyAction(uchar keycode)//存在一个按键锁定问题,比如在输入密码后不准按数字键
{
if((keycode>='0') && (keycode<='9') && step != 3)//step=3时要锁住
{
NumberAction(keycode - '0');
}
else if(keycode == 0x1b)//退出键重置
{
if(step < 3)
{
if(pass_the == 1)
{
pass_the = 0;
StartMotor(-90);
}
Reset();
}
else
{
LcdFullClear();
step = 3;//回退一个状态
LcdShowStr(0, 0, "Up close");
LcdShowStr(0, 1, "Down modify");
}
}
else if(keycode == 0x26 && pass_the == 1 && step==3)//通过后关箱门
{
StartMotor(-90);
pass_the = 0;
step = 2;
LcdFullClear();
LcdShowStr(0, 0, "Input:");
}
else if(keycode == 0x28 && pass_the == 1 && step == 3)//进入修改密码环节
{
step = 2;//进入修改密码阶段
LcdFullClear();
LcdShowStr(0, 0, "Modify:");
}
else if (keycode == 0x0d && count < 6)//小于6个数
{
LcdAreaClear(0, 0, 16);
LcdShowStr(0, 0, "Continue to 6:");
}
else if (keycode == 0x0d && count > 6)//超出留个数
{
LcdAreaClear(0, 0, 16);
LcdShowStr(0, 0, "over 6!");
step = 2;
}
else if (keycode == 0x0d && step == 0 && pass_the == 0)//校验密码
{
LcdFullClear();
pass();
}
else if (keycode == 0x0d && step == 0 && pass_the == 1)//修改密码
{
step = 1;
count = 0;
LcdFullClear();
LcdShowStr(0, 0, "Input again:");
}
else if (keycode == 0x0d && step ==1 && password1 == password2 && pass_the == 1)//回车键,存储密码
{
save();//保存后要Reset(),回退到上一个页面,强制只有关箱门才能退出
LcdShowStr(0, 1, "Press esc");
}
else if (keycode == 0x0d && step ==1 && password1 != password2 && pass_the == 1)//回车键,两次输入密码不相等
{
LcdFullClear();
LcdShowStr(0, 0, "Different");
step = 2;
}
}
void NumberAction(uchar n)
{
if (step == 0)//输入密码时要隐藏密码
{
password1 = password1 * 10 + n;
HidePass(str, password1);
LcdShowStr(0, 1, str);
count++;
}
else if(step == 1)
{
password2 = password2 * 10 + n;
HidePass(str, password2);
LcdShowStr(0, 1, str);
count++;
}
else if(step == 2)
{
Reset();
password1 = n;
HidePass(str, password1);
LcdShowStr(0, 1, str);
count = 1;
}
}
void TurnMotor()
{
unsigned char tmp;
static unsigned char index = 0;
unsigned char code BeatCode[8] = {0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6};
if (beats != 0)//节拍数不为0则产生一个驱动节拍
{
if (beats > 0)//节拍数大于0时正转
{
index++;//正转时节拍输出索引递增
index = index & 0x07;//用&操作实现到8归零
beats--;//正转时节拍计数递减
}
else
{
index--;
index = index & 0x07;
beats++;
}
tmp = P3;
tmp = tmp & 0xF0;
tmp = tmp | BeatCode[index];
P3 = tmp;
}
else
{
P3 = P3 | 0x0F;
}
}
//T0中断服务函数,用于驱动步进电机旋转
void InterruptTimer0() interrupt 1
{
TH0 = 0xf8;
TL0 = 0xcd;
TurnMotor();
}
void ConfigTimer1(uchar ms)
{
ulong tmp;
tmp = 11059200 / 12;
tmp = (tmp * ms) / 1000;
tmp = 65536 - tmp;
tmp = tmp + 13;
T1RH = (uchar)(tmp >> 8);
T1RL = (uchar)tmp;
TMOD &= 0x0f;
TMOD |= 0x10;
TH1 = T1RL;
TL1 = T1RL;
ET1 = 1;
TR1 = 1;
}
void InterruptTimer1() interrupt 3
{
TH1 = T1RH;
TL1 = T1RL;
KeyScan();
}
2.I2C.c:用于单片机和EEPROM通信。
②I2C.c
#include <reg52.h>
#include <intrins.h>
#define uchar unsigned char
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
extern void LcdShowStr(uchar x, uchar y, uchar *str);
//产生总线起始信号
void I2CStart()
{
I2C_SDA = 1;//首先确保SDA, SCL都是高电平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0;//先拉低SDA
//起始信号的条件是SCL为高电平期间,SDA由高电平向低电平产生一个下降沿
I2CDelay();
I2C_SCL = 0;//再拉低SCL
}
//产生总线停止信号
void I2CStop()
{
I2C_SCL = 0;//首先要确保SDA, SCL都是低电平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1;//先拉高SCL
I2CDelay();
I2C_SDA = 1;//再拉高SDA
//停止信号的条件是SCL为高电平期间,SDA由低电平向高电平产生一个上降沿
I2CDelay();
}
//I2C总线写操作,dat-待写入字节,返回值-从机应答位的值
bit I2CWrite(uchar dat)
{
bit ack;
uchar mask;//用于探测字节内某一位值的掩码变量
//mask=1000 0000向右进1,即为1100 0000
for (mask = 0x80; mask!=0; mask>>=1)//从高位到低位依次进行
{
if ((mask&dat) == 0)//该为输出到SDA上
I2C_SDA = 0;
else
I2C_SDA = 1;
I2CDelay();
I2C_SCL = 1;
I2CDelay();
I2C_SCL = 0;
}
I2C_SDA = 1;//8位数据发送完后,主机释放SDA,以检测从机应答
I2CDelay();
I2C_SCL = 1;//拉高SCL
ack = I2C_SDA;//读取此时的SDA值,即为从机的应答值
I2CDelay();
I2C_SCL = 0;//再拉低SCL完成应答位,并保持住总线
return (~ack);//返回从机应答值
}
//I2C总线读操作,并发送非应答信号,返回值-读到的字节
uchar I2CReadNAK()
{
uchar mask;
uchar dat;
I2C_SDA = 1;//首先确保主机释放SDA
for (mask=0x80;mask!=0;mask>>=1)//从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1;//拉高SCL
if (I2C_SDA == 0)//读取SDA的值
dat &= ~mask;//为0时,dat对应位清零
else
dat |= mask;//为1时,dat对应位置1
I2CDelay();
I2C_SCL = 0;//再拉低SCL以使从机发出下一位
}
I2C_SDA = 1;//8位数据发送完以后,拉高SDA,产生非应答信号
I2CDelay();
I2C_SCL = 1;//拉高SCL
I2CDelay();
I2C_SCL = 0;//再拉低SCL完成非应答为位,并保持住总线
return dat;
}
//I2C总线读操作,并发送应答信号,返回值-读到的字节
uchar I2CReadACK()
{
uchar mask;
uchar dat;
I2C_SDA = 1;//首先确保主机释放SDA
for (mask=0x80;mask!=0;mask>>=1)//从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1;//拉高SCL
if (I2C_SDA == 0)//读取SDA的值
dat &= ~mask;//为0时,dat对应位清零
else
dat |= mask;//为1时,dat对应位置1
I2CDelay();
I2C_SCL = 0;//再拉低SCL以使从机发出下一位
}
I2C_SDA = 0;//8位数据发送完以后,拉高SDA,产生应答信号
I2CDelay();
I2C_SCL = 1;//拉高SCL
I2CDelay();
I2C_SCL = 0;//再拉低SCL完成非应答为位,并保持住总线
return dat;
}
3.EEPROM.c写入数据:
#include <reg52.h>
#define uchar unsigned char
extern void I2CStart();
extern void I2CStop();
extern uchar I2CReadACK();
extern uchar I2CReadNAK();
extern bit I2CWrite(uchar dat);
//E2读取函数,buf-数据接收指针,addr-E2中的起始地址,len-读取长度
void E2Read(uchar *buf, uchar addr, uchar len)
{
do {
I2CStart();
if(I2CWrite(0x50<<1))
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr);//写入起始地址
I2CStart();//发送重复启动信号
I2CWrite((0x50<<1)|0x01);//寻址器件,后续为读操作
while(len>1)//连续读取len-1个字节
{
*buf++ = I2CReadACK();//最后字节之前为读取操作+应答
len--;
}
*buf = I2CReadNAK();//最后一个字节为读取操作+非应答
I2CStop();
}
//写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度
void E2Write(uchar *buf, uchar addr, uchar len)
{
while(len > 0)
{
//等待上次写入操作完成
do {
I2CStart();
if (I2CWrite(0x50<<1))
{
break;
}
I2CStop();
} while(1);
//按页模式连续写入字节
I2CWrite(addr);
while (len > 0)
{
I2CWrite(*buf++);
len--;
addr++;
if ((addr&0x07) == 0)
{
break;
}
}
I2CStop();
}
}
//将一段内存数据转换为十六进制格式字符串,str-字符串指针,
//src源数据地址,len-数据长度
void MemToStr(uchar *str, uchar *src, uchar len)
{
uchar tmp;
while(len--)
{
tmp = *src >> 4;//先取高4位
if (tmp <= 9)
*str++ = tmp + '0';
else
*str++ = tmp - 10 + 'A';
tmp = *src & 0x0f;//再取低4位
if (tmp <= 9)
*str++ = tmp + '0';
else
*str++ = tmp - 10 + 'A';
//*str++ = ' ';//转换玩每一个字节添加一个空格
src++;
}
*str = '\0';//添加字符串结束符
}
4.Keyborad.c:键盘驱动程序:
#include <reg52.h>
#define uchar unsigned char
sbit key_in_1 = P2^4;
sbit key_in_2 = P2^5;
sbit key_in_3 = P2^6;
sbit key_in_4 = P2^7;
sbit key_out_1 = P2^3;
sbit key_out_2 = P2^2;
sbit key_out_3 = P2^1;
sbit key_out_4 = P2^0;
uchar code KeyCodeMap[4][4] = {
{'1', '2', '3', 0x26},//向上键
{'4', '5', '6', 0x25},//向左键
{'7', '8', '9', 0x28},//向下键
{'0',0x1B, 0x0D, 0x27}//ESC键,回车键,向右键
};
uchar pdata KeySta[4][4] = {
{1,1,1,1}, {1,1,1,1}, {1,1,1,1}, {1,1,1,1}
};
extern void KeyAction(uchar keycode);
void KeyDriver()
{
uchar i, j;
static uchar pdata backup[4][4] = {
{1,1,1,1}, {1,1,1,1}, {1,1,1,1}, {1,1,1,1}
};
for (i=0; i<4; i++)
{
for (j=0; j<4; j++)
{
if(backup[i][j] != KeySta[i][j])
{
if (backup[i][j] != 0)//即按键按下时
{
KeyAction(KeyCodeMap[i][j]);//调用响应的动作函数
}
backup[i][j] = KeySta[i][j];//刷新备份
}
}
}
}
//按键扫描函数
void KeyScan()
{
uchar i;
static uchar keyout = 0;//矩阵按键扫描输出索引
static uchar keybuf[4][4] = {
{0xff,0xff,0xff,0xff}, {0xff,0xff,0xff,0xff},
{0xff,0xff,0xff,0xff}, {0xff,0xff,0xff,0xff}
};
//将一行的四个键值移入缓冲区
keybuf[keyout][0] = (keybuf[keyout][0] << 1) | key_in_1;
keybuf[keyout][1] = (keybuf[keyout][1] << 1) | key_in_2;
keybuf[keyout][2] = (keybuf[keyout][2] << 1) | key_in_3;
keybuf[keyout][3] = (keybuf[keyout][3] << 1) | key_in_4;
//消抖后更新按键状态
for (i=0; i<4; i++)
{
if((keybuf[keyout][i] & 0x0f) == 0x00)
{
KeySta[keyout][i] = 0;
}
else if((keybuf[keyout][i] & 0x0f) == 0x0f)
{
KeySta[keyout][i] = 1;
}
}
//执行下一次的扫描输出
keyout++;
keyout &= 0x03;
switch(keyout)
{
case 0:key_out_4 = 1;key_out_1 = 0;break;
case 1:key_out_1 = 1;key_out_2 = 0;break;
case 2:key_out_2 = 1;key_out_3 = 0;break;
case 3:key_out_3 = 1;key_out_4 = 0;break;
default: break;
}
}
5.LCD1602.c屏幕显示:
#include <reg52.h>
#define uchar unsigned char
#define lcd1602_db P0
sbit lcd1602_rs = P1^0;
sbit lcd1602_rw = P1^1;
sbit lcd1602_e = P1^5;
void LcdWaitReady()
{
uchar sta;
lcd1602_db = 0xff;
lcd1602_rs = 0;
lcd1602_rw = 1;
do {
lcd1602_e = 1;
sta = lcd1602_db;
lcd1602_e = 0;
} while(sta & 0x80);//只是检测是否忙没有执行任何操作,1表示在忙,0表示空闲
}
//向液晶写入一行命令,cmd-待写入命令值
void LcdWriteCmd(uchar cmd)
{
LcdWaitReady();
lcd1602_rs = 0;//0为命令
lcd1602_rw = 0;//0表示写入
lcd1602_db = cmd;
lcd1602_e = 1;//产生高脉冲
lcd1602_e = 0;
}
//向液晶写入一字节数据,dat-待写入数据值
void LcdWriteDat(uchar dat)
{
LcdWaitReady();
lcd1602_rs = 1;//1为数据
lcd1602_rw = 0;//0表示写入
lcd1602_db = dat;
lcd1602_e = 1;//产生高脉冲
lcd1602_e = 0;
}
//设置显示RAM起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标
void LcdSetCursor(uchar x, uchar y)
{
uchar addr;
if(y==0)
addr = 0x00 + x;//这意味着第一个显示字符x=0
else
addr = 0x40 + x;
LcdWriteCmd(addr | 0x80);//设置RAM地址
}
void LcdShowStr(uchar x,uchar y,uchar *str)
{
LcdSetCursor(x, y);
while(*str != '\0')//通过长度来改变,而静态显示时是通过判断最后一个字符是否为/0,while(*str!='\0')
{
LcdWriteDat(*str++);
}
}
//打开光标闪烁效果
void LcdOpenCursor()
{
LcdWriteCmd(0x0f);
}
//关闭光标显示
void LcdCloseCursor()
{
LcdWriteCmd(0x0c);
}
//区域清除,清除从(x,y)坐标起始的len个字符位
void LcdAreaClear(uchar x,uchar y,uchar len)
{
LcdSetCursor(x, y);
while(len--)
{
LcdWriteDat(' ');
}
}
//整屏清除
void LcdFullClear()
{
LcdWriteCmd(0x01);
}
//初始化1602液晶
void InitLcd1602()
{
LcdWriteCmd(0x38);//写入
LcdWriteCmd(0x0c);//显示器开,光标关闭
LcdWriteCmd(0x06);//文字不动,地址自动+1
LcdWriteCmd(0x01);//清屏操作
}
密码锁部分显示设置了几个步骤,在不同的步骤有不同的操作,相同的按键响应也不同,后来我的设置是:第零步,没有通过密码检验,第零步,已经通过密码检验,第一步,第二次输入密码后,第二步,开始修改密码阶段,第三步,进入电机反转90度还是修改密码选择阶段。这样程序更加清楚,相关功能也更加好实现。
要锁住按键,在电机反转90度还是修改密码选择阶段,因此这是应该让输入锁住,禁止输入。
还有一个问题是,就是一开始如果选择修改秘密时没有关上让电机反转90度,在实际应用中相当于箱门一直打开,而如果下次输入正确密码后箱门又正转90度,这显然是错误的,因此我设置成了修改密码后回到选择让电机反转90度还是继续修改密码阶段,这样不选择关上反转90度是无法退出,再次重新输入密码的,比较符合实际的应用,当然读者也可以按照自己的想法修改。
还有一些问题没有弄好,就是初始时输入密码,假设这是一个保险柜,在卖给用户后应该要设置一个初始化密码阶段,或是告诉用户初始密码。还有在用户忘记密码时,应该有办法找回密码,这是可以改进的部分。