前言
第一次写博客,使用CSDN也好几年了,第一次作为创作者的身份去写博客,以前都是在印象里面做做笔记这样的,今天把我这次51实验的程序作为博客生涯的第一篇吧,加油。
目标:
通过51单片机设计一个简单的闹钟,具体为:可以实现基本时钟功能,按键还能切换日期,和闹钟,且能自由设置。后续添加支持新建闹钟,理论支持无限添加新的闹钟。
所需元件:
硬件部分:51单片机、数码管、蜂鸣器。
原理图
以上就是我们的硬件组成,关于基本的硬件原理、工作流程等相信大家耳熟能详,我这里不在赘述,而我们用到的k1-显示切换,k2-选中调整,k3按键加,k4-按键减。
软件部分:
基本的工程创建我就不介绍了,下面是工程目录结构,主要包含两个目录一个存放源文件,一个存放工具类文件,工具类文件夹里面是我在写其他的程序的时候,抽离出来的一些公共的方法,声明在头文件中,而头文件无需导入,只要在软件中指定其路径即可使用。
工程目录
程序流程图
关于工程设计的基本思想,在我们的流程图中得到体现,读者可以仔细研磨,这对后面讲解代码部分有帮助,而我们的代码讲解也会按照流程图顺序来介绍。
软件代码:
1、 定时器初始化。使用两个定时器,T0和T1都用到了,这在之前本来是不需要的,但是后来发现了一点问题,先说现象:当我按下按键进行模式选择的时候或者其他的按键的时候,发现秒钟不准了,一按按键,速度就不正常了,原因:后来发现我在按键扫描的软件防抖的时候,用的和我们系统一样的定时器,那就有问题了,后来发现了,然后换成使用两个定时器,一个作为系统时钟,一个是软防抖的,代码就不贴了,比较简单,读者可自行编写。
2、选择默认显示的参数。这里本来不需要单独作为一个流程的,但是如果是这样,后面的现象都解释不清楚,那他很简单了,定义两个一维数组,用来存放我们的时间和日期,然后定义一个二维数组存放闹钟,还有一个数组指针。在我们需要显示的时候把我们的指针指向谁,然后把指针传递给我们数码管显示,也就是说我们不直接操作我们定义的任何一个数组,而是间接的通过指针来访问(本来指针是直接访问)。
u16 Time[3] = {10 , 23 , 0 };
u16 Date[3] = {20 , 12 , 1 };
u16 AlarmClock[3] = {10 , 23 ,10};
u16 *TimOrDatOrClock;
sbit beef = P1^5;
3、按键判断。按键判断我这里为了严谨,使用的上升沿检测,按键按下数值不变,手松开按键,才视为一次按键事件,具体如下:
/*做到上升沿检测*/
u8 key_scan(void)
{
u8 status = null;
status =(Serial_Key & 0x0f) ;
if(status != 0x0f){
wait_ms(14);
if((Serial_Key & 0x0f) == 0x0f) return status;
}
return null;
}
注意:Serial_Key 我定义的是P3 口,因为P3口用到了串口和按键,这在原理图中可见,读者可返回参看。
4、检测闹钟时间是否到了。没有其他的,判断当前的时间是不是和闹钟的时间相等就可以做到,具体如下:
简单介绍:定义一个变量为3,当我们小时一致是减去1,再判断分钟一致时减去1,最后是秒,当我们的所有的全部一致时,我们的stat 应该是0。如果我们的stat是0的话应该和我们的is_beef 取反是一致的,is_beef 是我们用来标识蜂鸣器是不是在响,当我们有多个闹钟的时候,且时间在十秒之内,那就不会重复执行,tim是我们用来展示闹钟响铃的时间的,这里可以注意下,后面会讲到我们是如何用它来做一个逐渐急促的闹钟声的。
5、数码管显示。这个是我们比较重点的部分,因为我在这部分花费的时间是最多的,不是很难,但是结合控制闪烁,显示切换和其他一些要考虑的东西,那就显得十分繁琐了,基本思想我们清楚显示中有一部分不变,作为横杠的分割部分,下面直接看代码展示:
void is_Colck()
{
u8 stat = 3;
for(i = 0; i < 3 ; i++)
{
if(Time[i] == AlarmClock[i]) stat--;
}
if( is_beef = !stat ) tim = 249;
}
void Disp_Vue_Plus(u16 *vue)
{
u8 status = 8;
u8 j , i ;
if(k >= 1) k--;
for( j = 0 ; j < 3 ; j++) // 8 0 0
{
map[j*3] = (vue[j]/10) % 10;
map[j*3 + 1] = vue[j] % 10;
}
for(i = 0; i < status ;i ++ )
{
j = 350;
Dis_chip_Sel = chip_sele[status- i-1];
if(i == 2 || i == 5) DISP = num[10];
else //不是分割部分
{
if( IS != 0 )//没有任何值被选中
{
if(i == (IS-2) || i == (IS-3))//判断是否选中当前值
if(k < 80)
{
DISP = num[map[i]];
if(k< 2) k = 200; //控制闪烁关键部分
} else continue;
}
DISP = num[map[i]]; //显示数值
}
while(j--);
DISP = 0x00;
}
}
读者是不是很困惑,对,使用了大量的IF判断,因为也是需要大部分的条件需要判断,具体是哪一些,听我一一道来。
首先是我们开头定义的 status 本来做为我们控制显示位数的变量的,后来放弃了这个想法,因为没有必要。然后走到第一个 For 循环,他的作用就是对我们的传入的时间日期进行分割,个位和十位分开用一个数组的连续两位来标识,方便后面我们在对其显示的时候取值。
第二个For循环,开始直接确定该哪个数码管显示,这是不需要犹豫的,然后是我们要对其所显示的值做判断了,首先判断当前显示的是不是第2位和第5位,规定了是显示“–”的。然后判断当前数码管是不是被选中,不是的话,也可以直接显示,不做判断;如果是的话,我们就要让他闪烁显示,再次判断它是不是我们真的显示的那两个数码管,减2是因为,我按下一次按键iS是加3的,加三的原因是3、6、9刚好可以和我们数码管在数组中定义的下标有一定的联系,他具体表现为 小时占据两位(6和7)、分钟占据两位(3和4),秒钟(0和1),按键数值减去3和2即可得到上述数码管的数组下标:时钟 = 9 - 3 和 9 - 2;如果以上条件均满足,那就是满足闪烁条件了,定义一个变量,理解:其他数刷新30遍,其才刷新1遍,30是假设,读者可分析,另外,此段代码可以精简,感兴趣的读者,可以尝试,欢迎和我讨论。
1 - 1、 按键扫描结果:之前讲到按键扫描,具体对他的处理,相信读者心中应该有一个宏图了,这很好,说明您是学习到了的,我们继续,代码如下:
while(1)
{
CarryBit(Time ,2,2,2);
switch(key_scan())
{
case Key_Mode: //调整
IS += 3;
if(IS >= 10) IS = 0;
break;
case Key_: //按键减
Decide(0);
break;
case Key_Add:
Decide(1);
if(IS == 0) ;
break;
case Key_TXD:
Change(Tor);
Tor ++;
if(IS == 0) ;
}
// if( !Tor ) TimOrDatOrClock = Time; else TimOrDatOrClock = Date;
Disp_Vue_Plus( TimOrDatOrClock ); //Tor
is_Colck();
}
}
分析:用一个switch接收到我们得到的按键,分别是四个按键,我们把我们需要和硬件定义的值做了封装,便于后期更改,养成来良好的代码习惯很重要,这一点读者需要学习,这在以后的任何的开发中是十分常见的,定义在后面放出图片方便对比观看。
之前介绍到模式按键是对IS加3的操作,想一想我们之前将数码管的部分,想不起来?我来给读者复述一下:本来 IS 是 0 的,0 减去 2 和减去 3 不会对任何数码管选中;然后按下了一次,IS 为 3,判断 IS - 2 和 IS - 3 的值那就是 1和 0 了对应得数组下标的值的数码管就相当于选中,这个IS变量十分关键,贯穿了我们整个工程,后面我们判断是否有按键按下了(选中了),当前选中的是哪一个(通过 IS 减 1 或 2 得到),十分重要,需要理解。
Key_ 表示的是我们按键减按下执行的部分,这里使用的不是函数,而是宏定义(Decide(0))万物皆可宏定义,感兴趣的读者可以查阅资料。
细心的读者,应该发现我后面有一句if(IS == 0)
未作任何处理,判断的就是有没有任何按键按下,有没有选中的值,判断的原因方便我们做后续功能拓展,比如说,我们没有按下模式,直接按下加或者减键那这个状态是不是富余出来的了,好的利用起来,在我们的程序流程图中已经做好了功能定义,但是作者未能有时间去实现,有精力的读者朋友可以帮我完善,欢迎改进、和我讨论。
#define Serial_Key P3 //把串口和按键定义出来
#define Key_Mode 0x0e
#define Key_TXD 0x0d
#define Key_ 0x0b
#define Key_Add 0x07
#define Dis_chip_Sel P2
还有我们宏定义部分:
#define Decide(OV) for( i = 0; i < 3; i++) \
if(OV){ \
if(IS == ((i+1)*3)) \
TimOrDatOrClock[i]++; \
}else { \
if(IS == ((i+1)*3) && TimOrDatOrClock[i] > 0) \
TimOrDatOrClock[i]--; \
}
定义很简单,IS判断也作为了我们重要判断的依据之一,总的代码会循环3次表示全部判断一次,加和减的部分基本一致,需要注意的是语法:”\”宏定义不能分行用它来表示该行是不分段的,注意” \”后面不能有任何符号,空格也不行,否则会报错,逻辑简单,就不具体分析了,代码就代表我的思路。
最后是我们的模式切换按键了,分别显示时间、日期和闹钟
#define Change(EX) switch(EX) { \
case 1: TimOrDatOrClock = AlarmClock; break; \
case 2: TimOrDatOrClock = Time; break; \
case 3: TimOrDatOrClock = Date; break; \
default: EX = 0; }
EX为我们定义的一个值,每次按下加1,到3归0 ;
2-1 闹钟时间判断部分。之前讲到怎么判断闹钟时间到了,这次我们讲一下当我们时间到了的时候,怎么实现蜂鸣器做有急促的响声,代码如下:
void is_Colck()
{
u8 stat = 3;
for(i = 0; i < 3 ; i++)
{
if(Time[i] == AlarmClock[i]) stat--;
}
if( is_beef = !stat ) tim = 249;
}
void irq_Time() interrupt 1
{
Tim_s(); //定时器赋予初值
times++;
if( times == 20) //一秒时间
{
times = 0;
Time[2] ++;
}
if(tim < 250) { if((tim % (tim/40)) == 0)beef = !beef; tim--; }
if(tim == 0) tim = 250;
}
我在之前想到,每次定时器都有大量的中断,并且时间固定50ms,如果不不利用起来太可惜了,然后就加上了闹钟的功能,当然还可以有其他的操作可以实现,读者可以大发想象之门。tim在我们闹钟时间到了之后被赋值为250,可以改作为闹铃时间的长短,当它小于250表示闹钟时间到,蜂鸣器该响了,然后tim为40的倍数的时候取反一次,因为我们tim是在不断减小的,所以为零的情况会越来越多,表现出来就是蜂鸣器越来越急促的响声。
3、进位扫描。在我们while(1)
循环中还有重要的一环,那就是进位检测,判断每一位的进位条件是否满足,代码如下:
/* 进位扫描 */
void CarryBit(u16 *arr , u16 MaxHeiBit ,u16 MaxConBit ,u16 MaxLowBit )
{
u8 j = 0;
if((MaxHeiBit | MaxConBit | MaxLowBit) == null)
{ //还没实现以后有时间 ha
}
else
{
for( j = 2 ; j > 0 ; j--)
if(arr[j] >= 60) { arr[j] = 0; arr[j-1]++; }
}
注意的是有一点:传入的参数,我开始的想法是做成一个模板,我解释一下,读者就明白了
参数 | 作用 |
---|---|
*arr | 需要做检测的主体 |
MaxHeiBit | 最高位的最大值, 例如:小时的最大位是 12 |
MaxConBit | 次高位的最大值,例如:分钟的最大位是 60 |
MaxLowBit | 低位的最大值, 例如:秒钟的最大位是 60 |
这样可以在主函数用一个变量做12小时制和24小时制的切换,遗憾的是我没有实现它。
现象:
本次图片拍摄于,在我开始写这篇博客之前,显示的是日期;
本篇博客就到这里,有什么问题、疑问,欢迎在下方评论区留言讨论。