一、设计目的:
通过单片机应用产品的设计与调试过程,巩固课程所学理论知识,初步了解单片机应用系统设计与调试的方法。
二、设计要求:
设计一个以AT89S51单片机为核心的数字电子钟控制器,实现电子钟的时间、日期交替显示、闹钟功能,并可通过按钮开关或键盘切换显示内容、调整参数、设置闹钟,在单片机实验板上模拟调试实现控制器的功能。具体设计要求如下:
1.开机自检,检查相关接口及数码管显示器、指示灯、蜂鸣器等外设是否正常。
2.8位数码管显示器平常以一定的时间间隔、合适的格式显示时间和日期信息,时间显示时、分、秒;日期显示年(2000~2099)、月、日;设置闹钟功能时显示时、分、开/关状态。
3.可通过按键设定时间、日期、闹钟等参数、手动切换显示。按键可用独立式按键或行列式键盘实现。设定参数过程有合适的方式指示当前可修改的内容。
4.对开关量输入进行软件消抖动处理,参数的设定有容错处理,如:小时不能超过23,日期中每月最大天数、闰年等。
5.用Protel设计可实现上述功能的控制器的原理图(最小应用系统)。
扩展功能(选做):
1.可设置多次闹钟。
2.显示星期功能。
3.参数设定过程中,较长时间无操作,则自动恢复为正常显示方式。。
4.其它自选的扩展功能。
三、总体方案设计及说明
总体功能框图:
硬件:
8个LED采用动态扫描以节约驱动成本;
走时采用内部T0计时中断;
4x4矩阵键盘扫描采用线反转法,以中断扫描计数防止抖动;
……
软件:
采用C语言实现。
四、系统资源分配说明(接口、存储器分配)
1.接口:
89S51的P1口接8个LED小灯;
89S51的P3_2接蜂鸣器(低电平鸣响);
外扩一片8255:
89S51单片机的P0口是低8位地址与数据复用的,现在我们用74HC373分离出地址,89S51高位地址的P2_0(A8)接8255的片选端(/CS), 低位地址Q1Q0(A1A0)与8255的A1A0连接,数据位P0_7~P0_0分别接8255的D_7~D_0。 以此得到的8255端口的地址分别为:
PA:xxxxxxx0 xxxxxx00取0x0fefc; PB:xxxxxxx0 xxxxxx01取0x0fefd;
PC:xxxxxxx0 xxxxxx10取0x0fefd; CTL:xxxxxxx0 xxxxxx11取0x0feff;
8255的PA口控制LED数码管的8个显示段;PB口分别接8个LED数码管的共阳极;
PC口分别接4x4矩阵键盘的行线和列线。
2.存储分配:
struct{ //闹钟时、分、秒 ,共设6个闹钟(初始状态默认:00-00-F1)
uchar hour;
uchar minute;
uchar isON;
}alarm[6]={{0,0,0}};
uchar hour=12,minute=0,second=0;//时、分、秒
uchar temp_second; //用于立即切换显示时间/日期
uint year=2011;// 年
uchar month=12;// 月
uchar day=1; // 日
uchar week=6;// 星期
uchar Mdays[]={0,31,28,31,30,31,30,31,31,30,31,30,31};//各月天数
uchar alarm_isON=1; //闹钟总开关
uchar alarm_station=0; //闹钟状态
uchar ano; //闹钟号(当前时间到的闹钟号)
uchar start_minute;//开始响铃的时间(也就是所定闹钟的时间)
uint count_ms25=0; //软件计数器(计数40个25毫秒达1s)
uchar show_model=0; // 显示模式:[0]切换显示时间/日期 [1]切换显示日期/时间
const uchar fixtime=0x00;//时间修正量
uchar key=0xff;//获得的当前键值
uchar last_key=0xff; //最后一次扫描到的按键(非0xff)
uchar key_count=0;//扫描到同一按键的次数
uchar Edown=0; //闹钟开关键是否按下
uchar led_buf[8]={24,24,24,24,24,24,24,24}; //时间日期显示缓冲区
uchar code led_table1[]={0x0c0,0x0f9,0x0a4,0x0b0, 0x99,0x92,0x82,0x0f8,0x80,0x90,
0x88,0x83,0x0C6,0x0a1,0x86,0x8e,0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,
0x08,0x03,0x46,0x21,0x06,0x0e,0x7f,0x0bf,0xff};//数码管段码
uchar code KBTable[] = {'1','2','3','F','4','5','6','E','7','8','9','C','0','A','B','D'};//键值(可有可无)
五、软件流程图及说明
1.流程图:
2.主要程序段说明:
(1)显示:
动态显示:即各位数码管轮流点亮,对于显示器各位数码管,每隔一段延时时间循环点亮一次。利用人的视觉暂留功能可以看到整个显示,但须保证扫描速度足够快,人的视觉暂留功能才可察觉不到字符闪烁。显示器的亮度与导通电流、点亮时间及间隔时间的比例有关。调整参数可以实现较高稳定度的显示。动态显示节省了驱动和I/O口,降低了能耗。
void LED_show(uchar buf[])
{
uchar i,num,pLED=0x80;
for(i=0;i<8;i++)
{
num=buf[i];
PA=led_table1[num]; /*送字段码*/
PB=pLED; /*送字位码*/
pLED>>=1; /*右移一位*/
Delay(1); /*延时*/
}
}
(2)键盘(本次设计对下面两种扫描方式都进行了实现):
a.行扫描法:依次从第一至最末行线上发出低电平信号, 如果该行线所连接的键没有按下的话, 则列线所接的端口得到的是全“1”信号, 如果有键按下的话, 则得到非全“1”信号。
/*键盘扫描(行扫描法,延时消抖)********************************************************
uchar code KBTable[] = {
0xEE,'1',0xDE,'4',0xBE,'7',0x7E,'0',
0xED,'2',0xDD,'5',0xBD,'8',0x7D,'A',
0xEB,'3',0xDB,'6',0xBB,'9',0x7B,'B',
0xE7,'F',0xD7,'E',0xB7,'C',0x77,'D',
0x00,0xff};
uchar Get_key(void); // 获取最终键值
{ uchar i;
uchar line, row, k_value;
static uchar lastkey=0xff;
CTL=0x88; //CH输入,CL输出 10001000
PC=PC & 0xf0; // PC0~PC3输出0 , 输入PC4~ PC7(默认1无键按下)
if ((PC & 0xf0) == 0xf0)
{
lastkey=0xff;
return 0xff; //无键按下
}
row = PC;
Delay(4); //延时,消除抖动
if (row != PC)
{
lastkey=0xff;
return 0xff; //判为抖动
}
line=0xFE;
for (i=0;i<4;i++)
{ PC = line; //输出扫描信号
row=PC; //读键盘口
if ((row & 0xf0) != 0xf0)
break;
line=(line<<1)+1;
}
if (i==4)
{ lastkey=0xff; return 0xff; }
k_value = (row & 0xf0) | (line & 0x0f) ;
for (i=0; i<32; i+=2)
if (k_value == KBTable[i])
break;
if(lastkey==KBTable[i+1])
return 0xff;
lastkey=KBTable[i+1];
return KBTable[i+1];
}
b.线反转法:线反转法也是识别闭合键的一种常用方法, 该法比行扫描速度快, 但在硬件上要求行线与列线外接上拉电阻。先将行线作为输出线, 列线作为输入线, 行线输出全“0”信号, 读入列线的值, 那么在闭合键所在的列线上的值必为0;然后从列线输出全“0”信号,再读取行线的输入值,闭合键所在的行线值必为 0。这样,当一个键被按下时, 必定可读到一对唯一的行列值。再由这一对行列值可以求出闭合键所在的位置。
//一次键盘扫描(线反转法,中断扫描计数去抖)*********************************************************
uchar code KBTable[] = {'1','2','3','F','4','5','6','E','7','8','9','C','0','A','B','D'};
//key_index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
uchar key_scan(void) //返回 '0','1','2'...'E','F',0xff
{ uchar key_index,temp=0;
CTL=0x88; //CH输入,CL输出 10001000
PC=PC & 0xf0; //将低四位置0
if(PC!=0xF0) //判断按键是否按下 如果按钮按下 会拉低CH其中的一个端口
{
temp=PC; //读PC口
temp=temp&0xf0; //屏蔽低四位
temp=~((temp>>4)|0xf0); // 高四位取反值 ~(1111****)
if(temp==1) // pC.4 被拉低 1110 第一行
key_index=0;
else if(temp==2) // PC.5 被拉低 1101 第二行
key_index=4;
else if(temp==4) // PC.6 被拉低 1011 第三行
key_index=8;
else if(temp==8) // PC.7 被拉低 0111 第四行
key_index=12;
else //没有键按下
return 0xff;
CTL=0x81; //CL输入,CH输出 10000001
PC=PC & 0xf0; //CH输出0000 初始: 00001111 若有键按下,根据低4位哪一位被拉低确定第二维坐标
temp=PC; //读PC口
temp=temp&0x0f;
temp=~(temp|0xf0); // 低四位取反值 ~(1111****)
if(temp==1) //PC.0 被拉低 1110 第一列
key_index+=0;
else if(temp==2) //PC.1 被拉低 1101 第二列
key_index+=1;
else if(temp==4) //PC.2 被拉低 1011 第三列
key_index+=2;
else if(temp==8) //PC.3 被拉低 0111 第四列
key_index+=3;
else //没有键按下
return 0xff;
return KBTable[key_index];
}
else return 0xff;
}
c.消抖方法:
(1)硬件消抖法:就是在键盘中附加去抖动电路,从根上消除抖动产生的可能性。右图所示电路实际上是由R-S触发器构成的单脉冲电路。当按钮开关按下时Q端输出低电平,当开关松开时Q端恢复高电平,即输出一个负脉冲,以此消除抖动。
(2)软件消抖法:键按下的时间与操作者的按键动作有关,约为十分之几到几秒不等。而键抖动时间与按键的机械特性有关,一般为5~10ms不等。软件消抖法即是采用延时(一般延时10~20ms)的方法,以避开按键的抖动,即在按键已稳定地闭合或断开时才读出其状态。
(3)定时中断扫描计数消抖法:把扫描函数放在定时中断里调用,扫描有键值则保存至kast_key并计数(key_count++),下次中断时再扫描,键值与上次相同则计数累加,若键值不同则计数从1开始,……,直到相同的键值被计数>=6,则认为该键按下。
假设1s最快可以按5下,则每下约持续200ms, 每次25ms中断时就扫描一次键盘,在这8次(或更多)里若有6次以上检测到有同一键则判为有效键。
uchar Get_key(void)//返回 '0','1','2'...'E','F',0xff
{
if(key_count>=6)
{
key_count=0;
return last_key;
}
else return 0xff;
}
(3)中断服务子程序
/*25ms中断子函数**********************************************************************/
void Time_over(void) interrupt 1 using 2
{
uchar key_temp;
TH0 = 0x0A6; //晶振频率11.0592MHz下,定时25ms的计数初值
TL0 = 0x00+fixtime;//(2^16-X)*(12*1/11.0592)=25ms; -->X=42496=A600H
count_ms25++;
if(count_ms25>=40) //达到1秒
{
DateTime_change(); //走时更新时分秒等
count_ms25=0;
}
//每次25ms中断就扫描一次按键
key_temp=key_scan();
if((last_key==0xff)&&(key_temp!=0xff))
{
last_key=key_temp;
key_count++;
}
else
{
if(key_temp==last_key)
{key_count++;}
else
if(key_temp!=0xff){last_key=key_temp;key_count=1;}
}
}
六、系统功能与操作说明;
1.系统功能:
时间、日期(包括星期)、闹钟显示及其设置;
支持6个闹钟,可设置闹钟总开关及分开关;
完善的设置容错处理;
可自动/手动切换时间/日期显示模式;
设置位闪烁标识;
设置过程30s无操作,自动恢复为正常显示方式;
4x4矩阵键盘扫描采用线反转法,以中断扫描计数去抖;
开机自检功能;
......
2.显示格式说明:
时间显示:12-30-02 12时-30分-02秒
日期显示:11.01.01-62011年01月01日-星期六
闹钟设置显示:-123456- 等待选择要设置的闹钟号
06-00-E1 定时在06:00,闹钟1开(E开,F关)
闹钟总开关:时间显示时最后一位小数点亮代表总开,否则为关.
3.键盘操作说明:
(1)数字键“0”~“9”,用于设置时输入时间和日期,正常工作时无效。
(2)“时间设置”、“日期设置”、“闹钟设置”,用于进入相应功能的设置状态。
(3)“闹钟开关”在闹钟设置状态时,用于设置当前闹钟开或关,正常工作时,用于手动关闹铃的声音和设置闹钟总开关。
(4)“确认”用于设置参数的确认,并检查参数是否合理,如果符合要求,则参数有效;否则本次修改无效,保持原值。正常工作情况下按该键无操作。
(5)“显示切换”用于正常工作时手动切换时间、日期的显示。
七、调试记录(主要问题及解决方法)
1.在写设置模块特别是容错处理时比较耗时,要求对各个细节都进行仔细地调试,修修改改在所难免,都是些小问题,略过;
2.在实现功能模块的相互任意跳转时,起初只是直接调用功能入口函数fun_key(),出现递归调用的警告,因为要堆栈保护现场,而在任意跳转时多次递归调用必然会空间不足(即使通过reenstrant也不妥,也是这个问题),这样就有可能会造成数据覆盖使系统不稳定。对此,我在主函数里采用以下方法避免递归调用:
while(1)
{
Show_control();
alarm_scan();
key=Get_key();
while(key=='A'||key=='B'||key=='C') fun_key(); //功能跳转(可避免递归调用的处理)
fun_key();
}
3.在键盘的实验过程中,我几乎尝试了所有的键盘处理方法,包括行扫描法、线反转法、延时消抖、定时中断扫描计数消抖……其间曾导致LED动态刷新速度不足,闪烁严重,把LED_show()里的Delay(8)改为Delay(1)即可;条件key_count>=6判为有效也是针对我自己的程序根据实际情况调试得出的经验值。
听说有一种类似的按键扫描,可以很方便识别出短按、短按抬起、长按、长按抬起共4种按键动作,大概思路如下:
用定时器中断2mS扫描一次,如果按键被按下则计数累加,否则计数清零并记按键抬起动作。
1、累加到5次(10mS完成消斗)认为是短按;
2、累加超过5次且小于500次(1秒),并已产生按键抬起动作,认为是短按抬起;
3、累加超过500次,认为是长按;
4、累加超过500次,并已产生按键抬起动作,认为是长按抬起。
据此方法,我小试了下牛刀,具体实现未果,有空再试。
八、课程设计总结
其实以前就用Freescale单片机写过一个带温度显示和闹钟的数字时钟,只是实现走时、温度采集和键盘的基本功能,未考虑日期、容错处理、功能的完善性等细节问题。这次原打算尝试改用汇编实现,但后来迫于形式,时间有限,只好先用C顶着,也算是对这一类小产品的改进与完善吧。
至于时间的精确性,如果只用晶振是很难做到精确的,即使嵌入汇编精确计算或者调出一个最佳的时间偏差量、偏差函数,也难以确保其精确性,毕竟每个晶振的振荡频率与标称值存在误差也会老化。所以这里我没有做这方面的工作,目前普遍的做法是外接一个时钟芯片,比如说DS1302,价格也不贵。
九、附录:
1.Protel绘制的最小系统板原理图:
2.程序清单及详细注释
/*****************************************************
描述:
全功能数字电子钟
作者:Estrong 2011.12.12写于福建工程学院.
平台:Atmel 89S51 单片机
实现功能:
时间、日期(包括星期)、闹钟及其设置;
支持6个闹钟,可设置闹钟总开关及分开关;
完善的设置容错处理;
可自动/手动切换时间/日期显示模式;
设置位闪烁标识;
设置过程30s无操作,自动恢复为正常显示方式;
4x4矩阵键盘扫描采用线反转法,以中断扫描计数去抖;
开机自检功能;
......
******************************************************/
#include <reg51.h>
#include <absacc.h>
#define PA XBYTE[0x0fefc]
#define PB XBYTE[0x0fefd]
#define PC XBYTE[0x0fefe]
#define CTL XBYTE[0x0feff]
#define uchar unsigned char
#define uint unsigned int
//PA、PB、PC、CTL分别为8255的控制口
sbit P3_2=P3^2; //蜂鸣器引脚定义
struct{ //闹钟时、分、秒 ,共设6个闹钟(初始状态默认:00-00-F1)
uchar hour;
uchar minute;
uchar isON;
}alarm[6]={{0,0,0}};
uchar hour=12,minute=0,second=0;//时、分、秒
uchar temp_second; //用于立即切换显示时间/日期
uint year=2011;// 年
uchar month=12;// 月
uchar day=1; // 日
uchar week=6;// 星期
uchar Mdays[]={0,31,28,31,30,31,30,31,31,30,31,30,31};//各月天数
uchar alarm_isON=1; //闹钟总开关
uchar alarm_station=0; //闹钟状态
uchar ano; //闹钟号(当前时间到的闹钟号)
uchar start_minute;//开始响铃的时间(也就是所定闹钟的时间)
uint count_ms25=0; //软件计数器(计数40个25毫秒达1s)
uchar show_model=0; // 显示模式:[0]切换显示时间/日期 [1]切换显示日期/时间
const uchar fixtime=0x00;//时间修正量
uchar key=0xff;//获得的当前键值
uchar last_key=0xff; //最后一次扫描到的按键(非0xff)
uchar key_count=0;//扫描到同一按键的次数
uchar Edown=0; //闹钟开关键是否按下
uchar led_buf[8]={24,24,24,24,24,24,24,24}; //时间日期显示缓冲区
uchar code led_table1[]={0x0c0,0x0f9,0x0a4,0x0b0, 0x99,0x92,0x82,0x0f8,0x80,0x90,0x88,0x83,0x0C6,0x0a1,0x86,0x8e,0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,0x08,0x03,0x46,0x21,0x06,0x0e,0x7f,0x0bf,0xff};
// 0 1 2 3 4 5 6 7 89 a b c d e f0. 1. 2. 3. 4. 5. 6. 7. 8. 9. A. B. C. D. E. F. . - 全灭
// 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
/*函数声明*****************************************************************************/
void Init(void); //初始化
void fun_key(void); // 相应按键功能入口
void test(void); // 开机自检
void LED_show(uchar buf[]);// 8个数码管动态显示
void Show_time(void);// 时间送缓存
void Show_date(void); // 日期送缓存
void Show_alarm(uchar i);// 闹钟送缓存
uchar IsLeapYear(uint y); // 判断闰年
void Update_Feb(void);// 更新2月天数
void Show_control(void);// 时间日期切换显示控制
void DateTime_change(void);//走时改变时间和日期
void Delay(uchar k);// 延时约:60μs×k
uchar key_scan(void);// 键盘扫描(一次)
uchar Get_key(void); // 获取最终键值
void alarm_scan(void);// 闹钟扫描与响铃
void set_time(void);// 设置时间
void set_date(void);// 设置日期
void set_alarm(void);// 设置闹钟
/*主函数*****************************************************************************/
main()
{
Init();
Update_Feb(); //初始时更新2月天数
while(1)
{
Show_control();
alarm_scan();
key=Get_key();
while(key=='A'||key=='B'||key=='C') fun_key(); //功能跳转(可避免递归调用的处理)
fun_key();
}
}
/*键盘功能入口********************************************************************/
void fun_key(void)
{
switch(key)
{
case 'A': //按'A'键 设置时间
set_time();
break;
case 'B': //按'B'键 设置日期
set_date();
break;
case 'C': //按'C'键 设置闹钟
set_alarm();
break;
case 'E': //按'D'键 闹钟总开关,时间模式下最后一位小数点亮代表总开,否则为关
if(alarm_isON==0) alarm_isON=1;
else alarm_isON=0;
break;
case 'F': //按'F'键 时间/日期切换显示
show_model++;
if(show_model>1) show_model=0;
temp_second=0;
break;
default:break;
}
}
/*初始化************************************************************************/
void Init(void)
{
int i;
for(i=0;i<400;i++) Delay(8); /*硬件准备工作*/
CTL=0x80; /*8255控制字 方式0 ABC口皆设为输出*/
//计数器初始化:
TMOD=0x01; //GATE0=0,C/T=0,M1M0=01
TH0 = 0x0A6;//晶振频率11.0592MHz下,定时25ms的计数初值
TL0 = 0x00+fixtime; //(2^16-X)*(12*1/11.0592)=25ms; -->X=42496=A600H
test(); //开机自检
TR0=1; //启动T0计数
IE=0x82; //开放TF0中断
}
/*开机自检**********************************************************************/
void test(void)
{
uchar i,j,num,pLED=0x80;
P3_2=0;
for(i=0;i<10;i++)Delay(250);
P3_2=1;
for(i=0;i<8;i++)
{
num=led_buf[i];
PA=led_table1[num]; /*送字段码*/
PB=pLED; /*送字位码*/
for(j=1;j<=5;j++)Delay(250);
pLED>>=1; /*右移一位*/
Delay(10); /*延时*/
}
}
/*LED显示****************************************************************************/
void LED_show(uchar buf[]) //死循环里一般都要放该函数
{
uchar i,num,pLED=0x80;
for(i=0;i<8;i++)
{
num=buf[i];
PA=led_table1[num]; /*送字段码*/
PB=pLED; /*送字位码*/
pLED>>=1; /*右移一位*/
Delay(1); /*延时*/
}
}
/*更新显示缓存************************************************************************/
/*时间送缓存(12-00-00 时-分-秒)*/
void Show_time(void)
{
if(alarm_isON==0) led_buf[7]=second%10;
else led_buf[7]=second%10+16;//闹钟开时间最后一位显示有小数点
led_buf[6]=second/10;
led_buf[5]=33;
led_buf[4]=minute%10;
led_buf[3]=minute/10;
led_buf[2]=33;
led_buf[1]=hour%10;
led_buf[0]=hour/10;
}
/*日期送缓存(11.01.01-6,11年01月01日星期六)*/
void Show_date(void)
{ uint year_temp=year%100;
led_buf[1]=year_temp%10+16;
led_buf[0]=year_temp/10;
led_buf[3]=month%10+16;
led_buf[2]=month/10;
led_buf[5]=day%10;
led_buf[4]=day/10;
led_buf[6]=33;
led_buf[7]=week;
}
/*日期送缓存(2011.01.01,没有星期)
void Show_date()
{ uint year_temp=year;
led_buf[3]=year_temp%10+16;
year_temp=year_temp/10;
led_buf[2]=year_temp%10;
year_temp=year_temp/10;
led_buf[1]=year_temp%10;
year_temp=year_temp/10;
led_buf[0]=year_temp;
led_buf[5]=month%10+16;
led_buf[4]=month/10;
led_buf[7]=day%10;
led_buf[6]=day/10;
}
*/
/*闹钟显示送缓存(06-00-E1 闹钟1开,定时在06:00)*/
void Show_alarm(uchar i)
{
led_buf[7]=i;
if(alarm[i].isON) //闹钟开L6显示'E'
led_buf[6]=14;
else led_buf[6]=15; //闹钟关L6显示'F'
led_buf[5]=33;
led_buf[4]=alarm[i].minute%10;
led_buf[3]=alarm[i].minute/10;
led_buf[2]=33;
led_buf[1]=alarm[i].hour%10;
led_buf[0]=alarm[i].hour/10;
}
/*判断是否为闰年************************************************************************/
uchar IsLeapYear(uint y)
{
if( (y%4==0)&&(y%100!=0)||(y%400==0) )
return 1;
else
return 0;
}
/*更新2月天数**************************************************************************/
void Update_Feb(void)
{
if(IsLeapYear(year)) Mdays[2]=29; //闰年2月29天
else Mdays[2]=28; //平年2月28天
}
/*显示控制(时间日期切换显示)************************************************************/
void Show_control(void)
{
if(show_model==0)
{
if(temp_second/10%2==0) Show_time(); //切换模式:每隔10秒切换显示时间/日期
else Show_date();
}
else //if(show_model==1)
{
if(temp_second/10%2==0) Show_date(); //切换模式:每隔10秒切换显示日期/时间
else Show_time();
}
LED_show(led_buf);
}
/*走时改变时间和日期**********************************************************************/
void DateTime_change(void) //放中断,达1秒后执行
{
second++;temp_second++;
if(second>=60)
{minute++;second=0;}
if(temp_second>=60)
{temp_second=0;}
if(minute>=60)
{hour++;minute =0;}
if(hour>=24)
{day++;hour=0;week++;if(week>7) week=1;}
if(day>Mdays[month])
{month++;day=1;}
if(month>12)
{year++;month=1;Update_Feb();} //年份改变时更新2月天数
}
/*延时约:60μs×k ******************************************************************/
void Delay(uchar k)
{ uchar i;
while((k--)!=0)
for(i=0;i<25;i++);
}
/*键盘扫描(行扫描法,延时消抖)********************************************************
uchar code KBTable[] = {
0xEE,'1',0xDE,'4',0xBE,'7',0x7E,'0',
0xED,'2',0xDD,'5',0xBD,'8',0x7D,'A',
0xEB,'3',0xDB,'6',0xBB,'9',0x7B,'B',
0xE7,'F',0xD7,'E',0xB7,'C',0x77,'D',
0x00,0xff};
uchar Get_key(void); // 获取最终键值
{ uchar i;
uchar line, row, k_value;
static uchar lastkey=0xff;
CTL=0x88; //CH输入,CL输出 10001000
PC=PC & 0xf0; // PC0~PC3输出0 , 输入PC4~ PC7(默认1无键按下)
if ((PC & 0xf0) == 0xf0)
{
lastkey=0xff;
return 0xff; //无键按下
}
row = PC;
Delay(4); //延时,消除抖动
if (row != PC)
{
lastkey=0xff;
return 0xff; //判为抖动
}
line=0xFE;
for (i=0;i<4;i++)
{ PC = line; //输出扫描信号
row=PC; //读键盘口
if ((row & 0xf0) != 0xf0)
break;
line=(line<<1)+1;
}
if (i==4)
{ lastkey=0xff; return 0xff; }
k_value = (row & 0xf0) | (line & 0x0f) ;
for (i=0; i<32; i+=2)
if (k_value == KBTable[i])
break;
if(lastkey==KBTable[i+1])
return 0xff;
lastkey=KBTable[i+1];
return KBTable[i+1];
}
*/
//获取最终键值*********************************************************
uchar Get_key(void)//返回 '0','1','2'...'E','F',0xff
{
if(key_count>=6) //假设1s最快可以按5下,则每下约持续200ms, 每次25ms中断时就扫描一次键盘,在这8次(或更多)里若有六次以上检测到有同一键则判为有效键。
{
key_count=0;
return last_key;
}
else return 0xff;
}
//一次键盘扫描(线反转法,要求行列都要有上拉电阻,中断扫描计数去抖)*********************************************************
uchar code KBTable[] = {'1','2','3','F','4','5','6','E','7','8','9','C','0','A','B','D'};
//key_index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
uchar key_scan(void) //返回 '0','1','2'...'E','F',0xff
{ uchar key_index,temp=0;
CTL=0x88; //CH输入,CL输出 10001000
PC=PC & 0xf0; //将低四位置0
if(PC!=0xF0) //判断按键是否按下 如果按钮按下 会拉低CH其中的一个端口
{
temp=PC; //读PC口
temp=temp&0xf0; //屏蔽低四位
temp=~((temp>>4)|0xf0); // 高四位取反值 ~(1111****)
if(temp==1) // pC.4 被拉低 1110 第一行
key_index=0;
else if(temp==2) // PC.5 被拉低 1101 第二行
key_index=4;
else if(temp==4) // PC.6 被拉低 1011 第三行
key_index=8;
else if(temp==8) // PC.7 被拉低 0111 第四行
key_index=12;
else //没有键按下
return 0xff;
CTL=0x81; //CL输入,CH输出 10000001
PC=PC & 0xf0; //CH输出0000 初始: 00001111 若有键按下,根据低4位哪一位被拉低确定第二维坐标
temp=PC; //读PC口
temp=temp&0x0f;
temp=~(temp|0xf0); // 低四位取反值 ~(1111****)
if(temp==1) //PC.0 被拉低 1110 第一列
key_index+=0;
else if(temp==2) //PC.1 被拉低 1101 第二列
key_index+=1;
else if(temp==4) //PC.2 被拉低 1011 第三列
key_index+=2;
else if(temp==8) //PC.3 被拉低 0111 第四列
key_index+=3;
else //没有键按下
return 0xff;
return KBTable[key_index];
}
else return 0xff;
}
//闹钟扫描与响铃*****************************************************************
void alarm_scan(void) //死循环里一般都要放该函数
{ uint i;
if(alarm_isON==0) alarm_station=0;//闹钟总开关
else // if((alarm_isON==1)
{ for(ano=0;ano<5;ano++)
{if((alarm[ano].isON==1)&&(hour==alarm[ano].hour)&&(minute==alarm[ano].minute))
{ start_minute=minute;
alarm_station=1;
break;
}
else alarm_station=0;
}
}
if(alarm_station==0)Edown=0;
while( (alarm_station==1)&&(Edown==0)&&(minute==start_minute) )
{ //闹钟开关键(E)被按下或者响过一分钟就停
for(i=0;i<400;i++) /* 循环400次,嘀0.2秒钟 */
{
P3_2=0; /* P3.4=0,晶体管导通 */
Delay(2); /* 调延时函数,延时500μs */
P3_2=1; /* P3.4=1,晶体管截止 */
Delay(2); /* 调延时函数,延时500μs */
P1=0x00;
}
P1=0xff;
for(i=0;i<=50;i++) //延时作用
{
Show_control();
key=Get_key();
if(key=='E') {Edown=1;break;}
else continue;
}
Show_control();
key=Get_key();
if(key=='E') Edown=1;
else continue;
}
}
/*时间设置*****************************************************************/
void set_time(void)
{ uchar i,time1,count=0;
uchar key_buf[8];
time1=second;
Show_time();
for(i=0;i<8;i++) key_buf[i]=led_buf[i];
i=0;time1=second;
while(1)
{ if (time1!=second){count++;time1=second;}
if(count>=30) break; //长时间(30S,可变)无操作自动恢复正常显示
if(i==2) i=3;
else if(i==5) i=6;
if(count_ms25<20) led_buf[i]=key_buf[i]; //当前设置位闪烁(间隔0.5s)
else led_buf[i]=34;
key=Get_key();
if(key==0xff)
{LED_show(led_buf);alarm_scan();continue;}//没有键按下
else count=0;
if(key>='0'&&key<='9')
{
if(i==0&&key>'2') i--; //输入越界容错处理
else if(i==1&&key_buf[0]==2&&key>'3') i--;
else if((i==3||i==6)&&key>'5') i--;
else key_buf[i]=key-'0';
led_buf[i]=key_buf[i];
i++;
if(i>=8) i=0;
}
else if(key=='D') //确定
{
hour=key_buf[0]*10+key_buf[1];
minute=key_buf[3]*10+key_buf[4];
second=key_buf[6]*10+key_buf[7];
break;
}
else break;
}
}
/*日期设置*****************************************************************/
void set_date(void)
{ uchar i,temp,time1,count=0;
uchar key_buf[8];
time1=second;
Show_date();
for(i=0;i<8;i++) key_buf[i]=led_buf[i];
i=0;
while(1)
{ if (time1!=second){count++;time1=second;}
if(count>=30) break; //长时间(30S,可变)无操作自动恢复正常显示
if(i==6) i=7;
if(count_ms25<20) led_buf[i]=key_buf[i]; //当前设置位闪烁(间隔0.5s)
else led_buf[i]=34;
key=Get_key();
if(key==0xff) //没有键按下
{LED_show(led_buf);alarm_scan();continue;}
else count=0;
if((key>='0')&&(key<='9'))
{ if(i==0) key_buf[0]=key-'0';
else if(i==1) key_buf[1]=key-'0'+16;
else if(i==2) //月 输入越界容错处理 2:monthH
{
if(key>'1')i--;
else key_buf[2]=key-'0';
}
else if(i==3) //3:monthL
{
if(key_buf[2]==0)
{
if(key=='0') key_buf[3]=17; //最少01月
else key_buf[3]=key-'0'+16;
}
else
{
if(key_buf[2]==1&&key>'2') i--; //最多12月
else key_buf[3]=key-'0'+16;
}
}
//日 输入越界容错处理 4:dayH 5:dayL
else if(i==4)
{
if((key_buf[2]==0)&&((key_buf[3]-16)==2))
{
if(key>'2') i--;&nb