多功能电子钟

我们课程到了这里,基本知识介绍完毕。如果同学们能够认真把前边的“降龙十八章”领悟透彻,那剩下的主要工作就是不断反复练习巩固了。本章我们首先介绍实际项目开发中的一些技巧和规范性的东西,然后带领大家一起来做一个真正的项目,把项目开发的整个流程都走一遍。
19.1 类型说明
C 语言不仅提供了丰富的数据类型给我们使用,而且还允许用户自己定义类型说明符,也就是说为了方便,给已经存在的数据类型起个“代号”,比如“ 9527 就是你的终身代号”,就用 9527 来代表某个人。在 C 语言中,使用 typedef 即可完成这项功能,定义格式如下:
typedef   原类型名   新类型名
typedef 语句并未定义一种新的数据类型,他仅仅是给已经有的数据类型取了一个更加简洁直观的名字,可以用这个新的类型名字来定义变量。在实际开发中,很多公司都会使用这个关键字来给变量类型取新名字,一是为了方便代码的移植,还有就是为了代码更加的简洁一些,比如以下的这几种类型定义方式。
typedef  signed    char    int8;    // 8 位有符号整型数
typedef  signed    int     int16;   //16 位有符号整型数
typedef  signed    long    int32;   //32 位有符号整型数
typedef  unsigned  char    uint8;   // 8 位无符号整型数
typedef  unsigned  int     uint16;  //16 位无符号整型数
typedef  unsigned  long    uint32;  //32 位无符号整型数
经过以上的这种类型说明后,今后我们在程序中就可以直接使用uint8 来替代 unsigned char 来定义变量。聪明的你,是否发现我们起的这个代号,无符号型的前边带一个 u ,有符号的不带 u int 表示整数的意思,后边的数字代表的是这个变量类型占的位数,这种命名方式很多公司都采用,大家也可以学着采用这种方式。
有的时候也有用宏定义代替 typedef 的功能,但是宏定义是由预处理完成的,而 typedef 则是在编译时完成的,后者更加灵活。我发现有的同学用这种定义方式:
#define  uchar  unsigned char
这种方式不建议大家使用,在这种应用下是没问题,但是当用到指针的时候,就有可能出错,在一些比较正规的公司如果写出这种形式可能会感觉写代码的人比较初级。下面我们就介绍一下 typedef #define  之间的区别。
#define 是预编译处理命令,在编译处理时进行简单的替换,不做任何正确性检查,不管含义是否正确都会被代入,比如:
#define  PI  3.1415926
有了这个宏,我们今后可以直接用 PI 来替代 3.1415926 了,比如我们写 area = PI*r*r 求圆的面积就会直接替换成 3.1415926*r*r 。如果我们不小心写成了 3.1415g26 ,编译的时候还是会代入。
typedef 是在编译时进行处理的,它是在自己的作用域内给一个已经存在的类型起一个代号,如果我们把前边的类型说明错误的写成:
typedef  unsinged  char    uint8;
编译器会直接报错。
对于 #define 来说,更多的应用是进行一些程序可读性、易维护的替换。比如:   
#define   LCD1602_DB  P0
#define   SYS_MCLK   (11059200/12)
在写 1602 程序的过程中,我们可以直接用LCD1602_DB 表示 1602 的通信总线,我们也可以直接用SYS_MCLK来作为我们单片机的机器周期,这样如果改动一些硬件,比如出于特定需要而换了其它频率的晶振,那么我们可以直接在程序最开始部分改一下即可,不用到处去修改数字了。
而对于类型说明,有的情况下 typedef #define 用法一样,有的情况就不一样了。
typedef  unsigned  char    uint8;       uint8  i, j;
#define  uchar  unsigned char           uchar  i, j;
这两种用法是完全相同的,等价的,没有区别,不过大家要注意 typedef 后边有分号,而 #define 后边是没有分号的。
typedef   int*  int_p;     int_p  i, j;
#define   int_p  int*      int_p  i,  j;
这两种用法得到的结果是不一样的,其中第一种无疑是定义了 i j 这两个 int 指针变量。而第二种呢?因为 define 是直接替换,实际上就是 int* i, j;  所以 i 是一个 int 指针变量,而 j 却是一个普通的 int 变量。
总之, typedef 是专门给类型重新起名的,而 #define 是纯粹替换的,大家记住其用法。
19.2 头文件
在前边的章节中,我们多次使用过文件包含命令 #include ,这条指令的功能是将指定的被包含文件的全部内容插到该命令行的位置处,从而把指定文件和当前的源程序文件连成一个源文件参与编译,通常的写法如下:
#include < 文件名 >           或者  #include ”文件名”
使用尖括号表示预处理程序直接到系统指定的“包含文件目录”去查找,使用双引号则表示预处理程序首先在当前文件所在的文件目录中查找被包含的文件,如果没有找到才会再到系统的“包含文件目录”去查找。一般情况下,我们的习惯是系统提供的头文件用尖括号方式,我们用户自己编写的头文件用双引号方式。
我们在前边用过很多次 #include <reg52.h> ,这个文件所在的位置是 keil 软件安装目录的\C51\INC这个路径内,大家可以去看看,在这个文件夹内,有很多系统自带的头文件,当然也包含了 <intrins.h> 这个头文件。当我们一旦写了#include <reg52.h> 这条指令后,那么相当于在我们当前的 .C 文件中,写下了以下的代码。
#ifndef __REG52_H__
#define __REG52_H__

/*  BYTE Registers  */
sfr P0    = 0x80;
sfr P1    = 0x90;
sfr P2    = 0xA0;
sfr P3    = 0xB0;
... ...

/*  BIT Registers  */
/*  PSW  */
sbit CY    = PSW^7;
sbit AC    = PSW^6;
sbit F0    = PSW^5;
sbit RS1   = PSW^4;
sbit RS0   = PSW^3;
sbit OV    = PSW^2;
sbit P     = PSW^0; //8052 only

/*  TCON  */
sbit TF1   = TCON^7;
sbit TR1   = TCON^6;
sbit TF0   = TCON^5;
sbit TR0   = TCON^4;
sbit IE1   = TCON^3;
sbit IT1   = TCON^2;
sbit IE0   = TCON^1;
sbit IT0   = TCON^0;
... ...

#endif
我们之前在程序中,只要写了#include <reg52.h> 这条指令,我们就可以随便使用 P0 TCON TMOD 这些寄存器和 TR0 TR1 TI RI 等这些寄存器的位,都是因为它们已经在这个头文件中定义或声明过了。
前边我们讲过,要调用某个函数,必须提前进行声明。而 Keil 自己做了很多函数,生成了库文件,我们如果要使用这些函数的时候,不需要写这些函数的代码,而直接调用这些函数即可,调用之前首先要进行声明一下,而这些声明也放在头文件当中。比如我们所用的_nop_();函数,就是在<intrins.h> 这个头文件中。
在我们前边应用的实例中,很多文件中的所要用到的函数,都是在其他文件中定义的,我们在当前文件要调用它的时候,提前声明一下即可。为了让我们程序的易维护性和可移植性提高,我们自己就可以编写我们所需要的头文件。我们自己编写的头文件中不仅仅可以进行函数的声明,变量的外部声明,一些宏定义也可以放在其中。
举个例子,比如我们在 main.c 这个文件中,配套写了一个 main.h 文件。新建头文件的方式也很简单,和 .c 是类似的,首先点击新建文件的那个图标,或者点击菜单 File->New ,然后点击保存文件,保存的时候命名为 main.h 即可。为了方便我们编写、修改维护,我们在 Keil 编程环境中新建一个头文件组,把所有的源文件放在一个组内,把所有的头文件放在一个组内,如图 19-1 所示。
 
图19-1  工程文件分组管理
大家注意, main.h 里除了要包含 main.c 所要使用的一些宏之外,还要在里边对 main.c 文件中所定义的全局变量,进行 extern 声明,提供给其他的 .c 文件使用,还要把 main.c 内的自定义类型进行声明,还要把 main.c 内所使用的全局函数进行声明,方便给其他文件调用。比如我们把 main.h 文件写成下边这样。
enum eStaSystem {  // 系统运行状态枚举
    E_NORMAL, E_SET_TIME, E_SET_ALARM
};

extern enum eStaSystem staSystem;

void RefreshTemp(uint8 ops);
void ConfigTimer0(uint16 ms);
首先大家注意,对于函数的外部声明, extern 是可以省略的,但是对于外部变量的声明是不能省略的。其次 enum 是一个枚举体,前边我们已经提到过了,大家可以再把书翻回去了解一下枚举体的作用和结构。我们在 main.c 当中定义的 staSystem 其他文件中要调用,在这里就要用 extern 声明一下。
头文件这样编写看似没问题,实际上则不然。首先第一个比较明显的问题,由于所有的源文件都有可能要包含这个 main.h ,同样 main.c 也会包含它,而 staSystem 这个枚举变量是在 main.c 中定义的,所以当 main.h main.c 包含时就不需要进行外部声明,而被其它文件包含时则应进行这个声明。此外,在我们的程序编写过程中,经常会遇到头文件包含头文件的用法,假设 a.h 包含了 main.h 文件, b.h 文件同样也包含了 main.h 文件,如果现在有一个 c 文件 1602.c 文件既包含了 a.h 又包含了 b.h ,这样就会出现头文件的重复包含,从而会发生变量函数等的重复声明,因此我们 C 语言还有一个知识点叫做条件编译。
19.3 条件编译
条件编译属于预处理程序,包括我们之前讲的宏,都是程序在编译之前的一些必要的处理过程,这些都不是实际程序功能代码,而仅仅是告诉编译器需要进行的特定操作等。
条件编译通常有三种用法,第一种表达式:
#if   表达式
     程序段  1
#else  
     程序段  2
#endif
作用:如果表达式的值为“真” ( 0) ,则编译程序段 1 ,否则,编译程序段 2 。在使用中,表达式通常是一个常量,我们通常事先用宏来进行声明,通过宏声明的值来确定到底执行哪段程序。
比如我们公司开发了同类的两款产品,这两款产品的功能有一部分是相同的,有一部分是不同的,同样所编写的程序代码大部分的代码是一样的,只有一少部分有区别。这个时候为了方便程序的维护,可以把两款产品的代码写到同一个工程程序中,然后把其中有区别的功能利用条件编译。
#define  PLAN   0
#if (PLAN == 0)
     程序段 1
#else
     程序段 2
#endif
这样写之后,当我们要编译款式 1 的时候,把 PLAN 宏声明成 0 即可,当我们要编译款式 2 的时候,把宏声明的值改为 1 或其它值即可。
第二种表达式和第三种表达式是类似的,使用哪一种完全看个人喜好,但是所有的程序最好统一。
表达式二:
#ifdef   标识符
       程序段 1
#else  
        程序段 2
#endif
表达式三:
#ifndef   标识符
        程序段 1
#else
        程序段 2
#endif
在本章的示例中我们使用到了表达式三,表达式三的作用是:如果标识符没有被 #define 命令所声明过,则编译程序段 1 ,否则则编译程序段 2 。此外,命令中的 #else 部分是可以省略的。表达式二和表达式三正好相反,大家自己看一下吧。其实 #ifndef 就是 if no define 的缩写。
在头文件的编写过程中,为了防止命名的错乱,我们每个 .c 文件对应的 .h 文件,除名字一致外,进行宏声明的时候,也用这个头文件的名字,并且大写,在中间加上下划线,比如我们这个 main.h 的结构,我们首先要这样写:
#ifndef _MAIN_H
#define _MAIN_H

    程序段 1

#endif
这样说明的意思是,如果这个_MAIN_H没有声明,那么我们就声明_MAIN_H,并且我们的程序段1 是有效的,最终结束;那么如果_MAIN_H已经声明过了,那么我们也就不用在声明了,同时程序段1 也就不必要再有效了。这样就可以有效的解决了 a.h 包含了 main.h 后, b.h 中既包含 main.h ,而 1602.c 既包含 a.h 又包含 b.h 所带来的尴尬。
第二个问题是, main.c 文件中定义的外部变量,在 main.c 中不需要进行外部声明。那么我们可以在我们的 main.c 程序中最开始的位置加上一句:
#define  _MAIN_C
然后在 main.h 内对这类变量进行声明的时候,再加上这样的条件编译语句:
#ifndef  _MAIN_C
     程序段 2
#endif   
这样处理之后,大家看一下,由于我们在 main.c 的程序中首先对_MAIN_C进行宏声明了,因此程序段2 中的内容不会参与到 main.c 的编译中去,而其他所有的包含 main.h 的源文件则会把程序段 2 参与到编译中,因此前边我们的 main.h 文件的整体代码如下所示。
#ifndef _MAIN_H
#define _MAIN_H

enum eStaSystem {  // 系统运行状态枚举
    E_NORMAL, E_SET_TIME, E_SET_ALARM
};

#ifndef _MAIN_C
extern enum eStaSystem staSystem;
#endif

void RefreshTemp(uint8 ops);
void ConfigTimer0(uint16 ms);

#endif
19.4 多功能电子钟
本章的重头戏就是我们要做的这个项目实践开发——多功能电子钟。当接到一个具体项目开发任务后,要根据项目做出框架规划,整理出逻辑思路,并且写出规范的程序,调试代码最终完成功能。[size=14.0000pt]19.4.1 硬件布局规划
作为电子钟,或者说万年历,提供日期、时间的显示是一个基本的功能,但是我们的设计要求并不满足于基本功能,而是要提供更多的信息,并且兼容人性化设计。在我们的设计中,除了基本的走时 ( 包括时间、日期、星期 ) 、板载按键校时功能外,还提供闹钟、温度测量、红外遥控校时这几项实用功能,所以称之为多功能。
如果一个产品只是所需功能的杂乱堆积,而不考虑怎样让人用起来更舒服、更愉悦,那么这就非常的不人性化,也绝对不是一个优秀的设计或者说产品。比如电子钟把日期和时间都显示到液晶上,这样看起来主次就不是很分明,显得杂乱。人性化设计考虑的是大多数人的行为习惯,当然最终的产品依靠了设计人员的经验和审美等因素。比如我们 KST-51 开发板的器件布局,右上方向是显示器件,右下是按键输入,有一些外围器件比如上下拉电阻,三极管等我们可以隐藏到液晶底下,这就是大多数人的习惯。而在我们的多功能电子钟项目中,如何去体现人性化设计呢?
我们先来观察一下各种显示器件,数字显示如果采用 LED 点阵或者数码管就会比较醒目,但是点阵无法同时显示这么多数字,于是我们就把最常用的时间用数码管来显示,日期、闹钟设置、温度等辅助信息我们显示到液晶上。那么点阵呢?我们可以用它来显示星期,这对于盼望着周末的人们来说是不是很醒目很人性化呢?对了,还有独立的 LED ,我们就用它来给电子钟做装饰吧,用个来回跑的流水灯增加点活泼气氛。最后再来个遥控器功能,如果电子钟挂的太高了或者放在不方便触碰的位置,我们就可以使用遥控器来校时。大家再来想想看,整个过程是不是挺人性化的。
当然了,我们所用的是 KST-51 单片机开发板来作为我们的硬件平台,如果这个是个实际项目,就不需要那么多外围器件了,首先做好单片机最小系统,而后配备我们多功能电子钟所需要的硬件外设就可以了。也就是说,我们在进行项目开发时,设计的硬件电路是根据我们的实际项目需求来设计的。

19.4.2 程序结构组织
项目需求和硬件规划已经确定了,我们就得研究如何实现它们,程序结构如何组织。一个项目,如果需要的部件很多,同时实现的功能也很多,为了方便编写和程序维护,整个程序必须采用模块化编程,也就是每个模块对应一个 c文件来实现,这种用法实际上在前面的章节已经开始使用了。一方面,如果所有的代码堆到一起会显得杂乱无章,更重要的是容易造成意外错误,程序一旦有逻辑上的问题或者更新需求,这种维护将变成一种灾难。此外,当一个项目程序量很大的时候,可以由多个程序员共同参与编程,多模块的方式也可以让每个程序员之间的代码最终很方便的融合到一起。
模块的划分并没有什么教条可以遵循,而是根据具体需要灵活处理。那么我们就以这个多功能电子钟项目为例,来给大家介绍说明如何合理的划分模块。我们要实现的功能有:走时、校时、闹钟、温度、遥控这几个功能。要想实现这几个功能,其中走时所需要的就是时钟芯片,即 DS1302 ;时间需要显示给人看,就需要显示器件,我们用到了点阵、数码管、独立 LED 、液晶;再来看校时,校时需要输入器件,本例中我们可以用板载按键和遥控器,他们各自的驱动代码不同,但是实现的功能是一样的,都是校时;还有闹钟设置,在校时的输入器件的支持下,闹钟也就不需要额外的硬件输入了,只需要用程序代码让蜂鸣器响就行了。
功能上大概列举出来了,那么我们就可以把程序源代码划分为这样几个模块: DS1302 作为走时的核心自成一个模块;点阵、数码管、独立 LED 都属于 LED 的范畴,控制方式都类似,也都需要动态扫描,所以把他们整体作为一个模块;液晶是另一个显示模块;按键和遥控器的驱动各自成为一个模块。
模块划分到这里,大家就要特别注意,随着我们程序量变大,功能变强,对程序的划分要分层了。前边我们划分的这些模块,都属于是底层驱动范畴的,他们要共同为上层应用服务,那么上层应用是什么呢?就是根据最终需要显示的效果来调度各种显示驱动函数,决定把时间的哪一部分显示到哪个器件上,然后还要根据按键或者遥控器的输入来具体实现时间的调整,还要不停的对比当前时间和设定的闹钟时间来完成闹钟功能,那么这些功能函数自然就成为一个应用层模块了 ( 当然你也可以把它们都放在 main.c 文件内实现,但我们不推荐这样做,如果程序还有其他应用层代码模块的话, main.c 仍然会变得复杂而不易维护 ) 。这个应用层模块在本例中我们取名为 Time.c ,即完成时间相关的应用层功能。最后,还有一个温度功能,除了要加入温度传感器的 DS18B20 底层驱动模块外,它的上层显示功能非常简单,不值得再单独占一个 c 文件,所以我们直接把它放到 main.c 中实现。
模块划分完毕,我们就要进行整体程序流程的规划。我们刚刚对程序进行了分层,一层是硬件底层驱动,再就是上层应用功能。底层驱动这些是不需要什么流程图的,流程图的主要结构都是上层应用程序的流程。当我们把上层应用程序的流程划分出来之后,每个上层应用功能都会有对底层硬件操作的需求,比如按键实现的功能,必然要写按键的底层驱动程序,这些在前边的学习过程中我们都会写了。根据我们所需要的上层应用功能,我们画出了我们的流程图,如图 19-2 所示。
 
19-2  多功能电子钟流程图
[size=14.0000pt]19.4.3 [size=14.0000pt]程序代码编写
在实际项目开发中,我们不仅仅希望我们的源程序、头文件等文件结构规范、代码编写规范,更希望我们的工程文件规整规范,方便维护。因此我们首先新建一个 lesson19_1 的文件夹,用来存放我们本章的工程文件。而后我们新建工程保存的时候,在 lesson19-1 文件夹内再建立一个文件夹,取名为 project ,专门用于存放工程文件的,如图 19-3 所示。
 
图19-3  工程文件夹
然后我们新建文件,保存的时候,在 lesson19_1 目录再建立一个文件夹,取名为 source 文件夹,专门用来存放我们的源代码,如图 19-4 所示。
 
图19-4  文件文件夹
最后,随便看一个之前的例子都能看到,工程编译后会生成很多额外的文件,这些文件可以统称为编译输出文件,输出文件的路径配置,进入 Options for Target->Output ,点击 Select Folder for Objects ,在 lesson19_1 建立一个文件夹,取名为 output ,专门用来存放这些输出文件,如图 19-5 所示。
 
图19-5  输出文件夹
进行了这样三个步骤,当今后我们要对这个工程进行整理编写的时候,文件就不再凌乱了,而是非常规整的排列在我们的文件夹内。尤其是今后大家还可能学到编写程序的另外的方式,就是编译的时候使用 Keil 软件,而编写代码的时候在其他更好的编辑器中进行,那么编辑器的工程文件也可以放到 project 下,而不会对其它部分产生任何影响。总之,这是一套规范而又实用的工程文件组织方案。
工程建立完毕,文件夹也整理妥当,下面就开始正式编写代码。当我们要进行一个实际产品或者项目开发的时候,首先电路原理图是确定的,所使用的单片机的引脚也是明确的,还有一些比如类型说明,一些特殊的全局参数及宏声明,我们会放到一个专门的头文件中,在这里我们命名为 config.h 文件。
/***********************config.h 文件程序源代码 *************************/
#ifndef _CONFIG_H
#define _CONFIG_H

/*  通用头文件  */
#include <reg52.h>
#include <intrins.h>

/*  数据类型定义  */
typedef  signed    char    int8;    // 8 位有符号整型数
typedef  signed    int     int16;   //16 位有符号整型数
typedef  signed    long    int32;   //32 位有符号整型数
typedef  unsigned  char    uint8;   // 8 位无符号整型数
typedef  unsigned  int     uint16;  //16 位无符号整型数
typedef  unsigned  long    uint32;  //32 位无符号整型数

/*  全局运行参数定义  */
#define SYS_MCLK   (11059200/12)  // 系统主时钟频率,即振荡器频率÷ 12

/* IO 引脚分配定义  */
sbit KEY_IN_1  = P2^4;  // 矩阵按键的扫描输入引脚 1
sbit KEY_IN_2  = P2^5;  // 矩阵按键的扫描输入引脚 2
sbit KEY_IN_3  = P2^6;  // 矩阵按键的扫描输入引脚 3
sbit KEY_IN_4  = P2^7;  // 矩阵按键的扫描输入引脚 4
sbit KEY_OUT_1 = P2^3;  // 矩阵按键的扫描输出引脚 1
sbit KEY_OUT_2 = P2^2;  // 矩阵按键的扫描输出引脚 2
sbit KEY_OUT_3 = P2^1;  // 矩阵按键的扫描输出引脚 3
sbit KEY_OUT_4 = P2^0;  // 矩阵按键的扫描输出引脚 4

sbit ADDR0 = P1^0;  //LED 位选译码地址引脚 0
sbit ADDR1 = P1^1;  //LED 位选译码地址引脚 1
sbit ADDR2 = P1^2;  //LED 位选译码地址引脚 2
sbit ADDR3 = P1^3;  //LED 位选译码地址引脚 3
sbit ENLED = P1^4;  //LED 显示部件的总使能引脚

#define LCD1602_DB  P0   //1602 液晶数据端口
sbit LCD1602_RS = P1^0;  //1602 液晶指令 / 数据选择引脚
sbit LCD1602_RW = P1^1;  //1602 液晶读写引脚
sbit LCD1602_E  = P1^5;  //1602 液晶使能引脚

sbit DS1302_CE = P1^7;  //DS1302 片选引脚
sbit DS1302_CK = P3^5;  //DS1302 通信时钟引脚
sbit DS1302_IO = P3^4;  //DS1302 通信数据引脚

sbit I2C_SCL = P3^7;  //I2C 总线时钟引脚
sbit I2C_SDA = P3^6;  //I2C 总线数据引脚

sbit BUZZER = P1^6;  // 蜂鸣器控制引脚

sbit IO_18B20 = P3^2;  //DS18B20 通信引脚

sbit IR_INPUT = P3^3;  // 红外接收引脚

#endif
这个 config.h 声明包含了系统所共同使用的类型说明以及宏声明,方便使用。下边的编程步骤,就是从 main.c 文件开始,以流程图作为主线来进行代码编写。
作为研发工程师来讲,调试这样一个程序,也得几个小时的时间,不可能写出来就好用,所以我这里是无法全部把整个过程给大家还原出来。但是主要的编写代码的过程我会尽可能的给大家介绍一下。
我们程序的流程虽然是从 main.c 开始的,但是那是整体程序框架,而编写代码,往往用流程图来做主线,却不是严格按照流程图的顺序来。比如我们这个程序,首先我们要进行功能性调试验证。
习惯上,我首先要调试显示程序,因为显示程序可以直观的看到,而且调试好显示后,如果要调试其他的模块,可以用显示模块来验证其他模块运行结果正确与否。显示设备就是 1602 液晶和 LED ,由于蜂鸣器比较简单,所以我们将蜂鸣器和 LED 放到一起。调试的时候,可以在 main.c 文件中,添加临时的调试函数,比如给 1602 液晶发送数据,让 1602 液晶显示个字符串,保证 1602 液晶的底层程序是没问题的;调用相应的函数让 LED 进行显示以及刷新,保证 LED 部分的程序也是没问题的。通过这种方式,如果发现哪部分还有问题就继续调整,如果发现显示部分 OK 了,那就可以继续往下编写了。
1602 液晶的底层驱动我们之前都已经写过了,直接拿过来用就行了。而对于 LED 的动态刷新问题,在讲红外的时候已经阐述过,用于 LED 刷新的定时器应该采用高优先级以避免红外接收中断动辄上百 ms 的执行时间影响视觉效果,我们选择 T1 用来作为红外接收的计时,按理说再用 T0 设置成高优先级来处理 LED 刷新即可,但是,本例中我们还启用了矩阵按键,而矩阵按键的扫描也采用 T0 而对红外中断实现嵌套的话,由于按键扫描的时间会达到几百 us ,这几百 us 的延时则足以使红外对码位的解析产生误判了。怎么办呢?是不是会很自然的想到:再增加一个定时器用来做 LED 扫描并实现对红外中断的嵌套,而按键扫描和红外处于相同的低优先级而不能彼此嵌套,按键迟后上百 ms 再响应不会感觉到问题,同样几百 us 的延时对红外起始引导码的 9ms 来说也完全可以容忍。那么还有没有定时器可用了呢,好在我们的 STC89C52 还有一个定时器 T2 (标准的 8051 是没有 T2 的,它是 8052 的扩充外设,现在绝大多数的 51 系列单片机都是有这个 T2 的),于是问题解决。此外还有一个问题,就是由于操作液晶的时候要对 P1.0 P1.1 进行操作,而刷新 LED 是中断,优先级是高于液晶的。如果我们当前正在操作液晶,对 P1.0 P1.1 操作了,数码管刷新的中断又来了,也要对 P1.0 P1.1 进行操作,就会导致逻辑错误。虽然这种错误出现机率极小,但是逻辑必须要严谨,必需避免它。那么当我们进行液晶操作的时候,如果数码管的定时中断来了,我们在本次中断中就放弃对数码管的刷新,不对那几个口线进行操作,因为液晶的读写操作都很快,所以对实际显示效果并没有太大的影响。
这部分代码除了定时器 2 的寄存器配置外,其他的内容我们之前几乎都用到过,大家可以通过分析程序学明白。而定时器 2 的寄存器配置,相信学到这里的同学也可以通过查阅数据手册自己看明白,这里要求同学们自学一下。我直接把代码贴出来,大家研究一下。
/***********************Lcd1602.h 文件程序源代码 *************************/
#ifndef _LCD1602_H
#define _LCD1602_H


#ifndef _LCD1602_C

#endif

void InitLcd1602();
void LcdClearScreen();
void LcdOpenCursor();
void LcdCloseCursor();
void LcdSetCursor(uint8 x, uint8 y);
void LcdShowStr(uint8 x, uint8 y, uint8 *str);
void LcdShowChar(uint8 x, uint8 y, uint8 chr);

#endif
/***********************Lcd1602.c 文件程序源代码 *************************/
#define  _LCD1602_C
#include "config.h"
#include "Lcd1602.h"

bit tmpADDR0;  // 暂存 LED 位选译码地址 0 的值
bit tmpADDR1;  // 暂存 LED 位选译码地址 1 的值

/*  暂停 LED 动态扫描,暂存相关引脚的值  */
void LedScanPause()
{
    ENLED = 1;
    tmpADDR0 = ADDR0;
    tmpADDR1 = ADDR1;
}
/*  恢复 LED 动态扫描,恢复相关引脚的值  */
void LedScanContinue()
{
    ADDR0 = tmpADDR0;
    ADDR1 = tmpADDR1;
    ENLED = 0;
}
/*  等待液晶准备好  */
void LcdWaitReady()
{
    uint8 sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do {
        LCD1602_E = 1;
        sta = LCD1602_DB; // 读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}
/*  LCD1602 液晶写入一字节命令, cmd- 待写入命令值  */
void LcdWriteCmd(uint8 cmd)
{
    LedScanPause();
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
    LedScanContinue();
}
/*  LCD1602 液晶写入一字节数据, dat- 待写入数据值  */
void LcdWriteDat(uint8 dat)
{
    LedScanPause();
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
    LedScanContinue();
}
/*  清屏  */
void LcdClearScreen()
{
    LcdWriteCmd(0x01);
}
/*  打开光标的闪烁效果  */
void LcdOpenCursor()
{
    LcdWriteCmd(0x0F);
}
/*  关闭光标显示  */
void LcdCloseCursor()
{
    LcdWriteCmd(0x0C);
}
/*  设置显示 RAM 起始地址,亦即光标位置, (x,y)- 对应屏幕上的字符坐标  */
void LcdSetCursor(uint8 x, uint8 y)
{
    uint8 addr;

    if (y == 0)  // 由输入的屏幕坐标计算显示 RAM 的地址
        addr = 0x00 + x;  // 第一行字符地址从 0x00 起始
    else
        addr = 0x40 + x;  // 第二行字符地址从 0x40 起始
    LcdWriteCmd(addr | 0x80);  // 设置 RAM 地址
}
/*  在液晶上显示字符串, (x,y)- 对应屏幕上的起始坐标, str- 字符串指针  */
void LcdShowStr(uint8 x, uint8 y, uint8 *str)
{
    LcdSetCursor(x, y);   // 设置起始地址
    while (*str != '\0')  // 连续写入字符串数据,直到检测到结束符
    {
        LcdWriteDat(*str++);
    }
}
/*  在液晶上显示一个字符, (x,y)- 对应屏幕上的起始坐标, chr- 字符 ASCII 码  */
void LcdShowChar(uint8 x, uint8 y, uint8 chr)
{
    LcdSetCursor(x, y);  // 设置起始地址
    LcdWriteDat(chr);    // 写入 ASCII 字符
}
/*  初始化 1602 液晶  */
void InitLcd1602()
{
    LcdWriteCmd(0x38);  //16*2 显示, 5*7 点阵, 8 位数据接口
    LcdWriteCmd(0x0C);  // 显示器开,光标关闭
    LcdWriteCmd(0x06);  // 文字不动,地址自动 +1
    LcdWriteCmd(0x01);  // 清屏
}
/***********************LedBuzzer.h 文件程序源代码 *************************/
#ifndef _LED_BUZZER_H
#define _LED_BUZZER_H

struct sLedBuff {  //LED 显示缓冲区结构
    uint8 array[8];   // 点阵缓冲区
    uint8 number[6];  // 数码管缓冲区
    uint8 alone;      // 独立 LED 缓冲区
};

#ifndef _LED_BUZZER_C
extern bit staBuzzer;
extern struct sLedBuff ledBuff;
#endif

void InitLed();
void FlowingLight();
void ShowLedNumber(uint8 index, uint8 num, uint8 point);
void ShowLedArray(uint8 *ptr);

#endif
/***********************LedBuzzer.c 文件程序源代码 *************************/
#define  _LED_BUZZER_C
#include "config.h"
#include "LedBuzzer.h"

uint8 code LedChar[] = {  // 数码管显示字符转换表
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};

bit staBuzzer = 0; // 蜂鸣器状态控制位, 1- 鸣叫、 0- 关闭
struct sLedBuff ledBuff; //LED 显示缓冲区,默认初值全 0 ,正好达到上电全亮的效果

/* LED 初始化函数,初始化 IO 、配置定时器  */
void InitLed()
{
    // 初始化 IO
    P0 = 0xFF;
    ENLED = 0;
    // 配置 T2 作为动态扫描定时
    T2CON = 0x00;  // 配置 T2 工作在 16 位自动重载定时器模式
    RCAP2H = ((65536-SYS_MCLK/1500)>>8);  // 配置重载值,每秒产生 1500 次中断,
    RCAP2L = (65536-SYS_MCLK/1500);       // 以使刷新率达到 100Hz 无闪烁的效果
    TH2 = RCAP2H;  // 设置初值等于重载值
    TL2 = RCAP2L;
    ET2 = 1;       // 使能 T2 中断
    PT2 = 1;       // 设置 T2 中断为高优先级
    TR2 = 1;       // 启动 T2
}
/*  流水灯实现函数,间隔调用实现流动效果  */
void FlowingLight()
{
    static uint8 i = 0;
    const uint8 code tab[] = {  // 流动表
        0x7F, 0x3F, 0x1F, 0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF
    };

    ledBuff.alone = tab[ i];   // 表中对应值送到独立 LED 的显示缓冲区
    if (i < (sizeof(tab)-1))  // 索引递增循环,遍历整个流动表
        i++;
    else
        i = 0;
}
/*  数码管上显示一位数字, index- 数码管位索引 ( 从右到左对应 0 5)
**     num- 待显示的数字, point- 代表是否显示此位上的小数点  */
void ShowLedNumber(uint8 index, uint8 num, uint8 point)
{
    ledBuff.number[ index] = LedChar[num];  // 输入数字转换为数码管字符 0 F
    if (point != 0)
    {
        ledBuff.number[ index] &= 0x7F;  //point 不为 0 时点亮当前位的小数点
    }
}
/*  点阵上显示一帧图片, ptr- 待显示图片指针  */
void ShowLedArray(uint8 *ptr)
{
    uint8 i;

    for (i=0; i<sizeof(ledBuff.array); i++)
    {
        ledBuff.array[ i] = *ptr++;
    }
}
/* T2 中断服务函数, LED 动态扫描、蜂鸣器控制  */
void InterruptTimer2() interrupt 5
{
    static uint8 i = 0;  //LED 位选索引

    TF2 = 0;  // 清零 T2 中断标志
    // 全部 LED 动态扫描显示
    if (ENLED == 0)  //LED 使能时才进行动态扫描
    {
        P0 = 0xFF;                       // 关闭所有段选位,显示消隐
        P1 = (P1 & 0xF0) | i;            // 位选索引值赋值到 P1 口低 4
        P0 = *((uint8 data*)&ledBuff+i); // 缓冲区中索引位置的数据送到 P0
        if (i < (sizeof(ledBuff)-1))     // 索引递增循环,遍历整个缓冲区
            i++;
        else
            i = 0;
    }
    // 由蜂鸣器状态位控制蜂鸣器
    if (staBuzzer == 1)
        BUZZER = ~BUZZER;  // 蜂鸣器鸣叫
    else
        BUZZER = 1;        // 蜂鸣器静音
}
第二个部分,我们就要调试时钟 DS1302 的程序代码了,这部分代码,我们首先可以把前边在 1602 液晶上显示时间的代码拿过来当作调试手段,当可以成功显示到 1602 液晶上后,我们就可以写进去一个初始时间,再读出来,把星期显示在 LED 点阵上,时间显示到数码管上,日期显示到液晶上,并且让流水灯流动起来。这块功能调试好以后,就是一个简单的电子钟了。
/***********************DS1302.h 文件程序源代码 *************************/
#ifndef _DS1302_H
#define _DS1302_H

struct sTime {  // 日期时间结构
    uint16 year; //
    uint8 mon;   //
    uint8 day;   //
    uint8 hour;  //
    uint8 min;   //
    uint8 sec;   //
    uint8 week;  // 星期
};

#ifndef _DS1302_C

#endif

void InitDS1302();
void GetRealTime(struct sTime *time);
void SetRealTime(struct sTime *time);

#endif
/***********************Ds1302.c 文件程序源代码 *************************/
#define  _DS1302_C
#include "config.h"
#include "DS1302.h"

/*  发送一个字节到 DS1302 通信总线上  */
void DS1302ByteWrite(uint8 dat)
{
    uint8 mask;

    for (mask=0x01; mask!=0; mask<<=1)  // 低位在前,逐位移出
    {
        if ((mask&dat) != 0) // 首先输出该位数据
            DS1302_IO = 1;
        else
            DS1302_IO = 0;
        DS1302_CK = 1;       // 然后拉高时钟
        DS1302_CK = 0;       // 再拉低时钟,完成一个位的操作
    }
    DS1302_IO = 1;           // 最后确保释放 IO 引脚
}
/*  DS1302 通信总线上读取一个字节  */
uint8 DS1302ByteRead()
{
    uint8 mask;
    uint8 dat = 0;

    for (mask=0x01; mask!=0; mask<<=1)  // 低位在前,逐位读取
    {
        if (DS1302_IO != 0)  // 首先读取此时的 IO 引脚,并设置 dat 中的对应位
        {
            dat |= mask;
        }
        DS1302_CK = 1;       // 然后拉高时钟
        DS1302_CK = 0;       // 再拉低时钟,完成一个位的操作
    }
    return dat;              // 最后返回读到的字节数据
}
/*  用单次写操作向某一寄存器写入一个字节, reg- 寄存器地址, dat- 待写入字节  */
void DS1302SingleWrite(uint8 reg, uint8 dat)
{
    DS1302_CE = 1;                   // 使能片选信号
    DS1302ByteWrite((reg<<1)|0x80);  // 发送写寄存器指令
    DS1302ByteWrite(dat);            // 写入字节数据
    DS1302_CE = 0;                   // 除能片选信号
}
/*  用单次读操作从某一寄存器读取一个字节, reg- 寄存器地址,返回值 - 读到的字节  */
uint8 DS1302SingleRead(uint8 reg)
{
    uint8 dat;

    DS1302_CE = 1;                   // 使能片选信号
    DS1302ByteWrite((reg<<1)|0x81);  // 发送读寄存器指令
    dat = DS1302ByteRead();          // 读取字节数据
    DS1302_CE = 0;                   // 除能片选信号

    return dat;
}
/*  用突发模式连续写入 8 个寄存器数据, dat- 待写入数据指针  */
void DS1302BurstWrite(uint8 *dat)
{
    uint8 i;

    DS1302_CE = 1;
    DS1302ByteWrite(0xBE);  // 发送突发写寄存器指令
    for (i=0; i<8; i++)     // 连续写入 8 字节数据
    {
        DS1302ByteWrite(dat[ i]);
    }
    DS1302_CE = 0;
}
/*  用突发模式连续读取 8 个寄存器的数据, dat- 读取数据的接收指针  */
void DS1302BurstRead(uint8 *dat)
{
    uint8 i;

    DS1302_CE = 1;
    DS1302ByteWrite(0xBF);  // 发送突发读寄存器指令
    for (i=0; i<8; i++)     // 连续读取 8 个字节
    {
        dat[ i] = DS1302ByteRead();
    }
    DS1302_CE = 0;
}
/*  获取实时时间,即读取 DS1302 当前时间并转换为时间结构体格式  */
void GetRealTime(struct sTime *time)
{
    uint8 buf[8];

    DS1302BurstRead(buf);
    time->year = buf[6] + 0x2000;
    time->mon  = buf[4];
    time->day  = buf[3];
    time->hour = buf[2];
    time->min  = buf[1];
    time->sec  = buf[0];
    time->week = buf[5];
}
/*  设定实时时间,时间结构体格式的设定时间转换为数组并写入 DS1302 */
void SetRealTime(struct sTime *time)
{
    uint8 buf[8];

    buf[7] = 0;
    buf[6] = time->year;
    buf[5] = time->week;
    buf[4] = time->mon;
    buf[3] = time->day;
    buf[2] = time->hour;
    buf[1] = time->min;
    buf[0] = time->sec;
    DS1302BurstWrite(buf);
}
/* DS1302 初始化,如发生掉电则重新设置初始时间  */
void InitDS1302()
{
    uint8 dat;
    struct sTime code InitTime[] = {  // 默认初始值: 2014-01-01 12:30:00  星期 3
        0x2014,0x01,0x01, 0x12,0x30,0x00, 0x03
    };

    DS1302_CE = 0;  // 初始化 DS1302 通信引脚
    DS1302_CK = 0;
    dat = DS1302SingleRead(0);  // 读取秒寄存器
    if ((dat & 0x80) != 0)      // 由秒寄存器最高位 CH 的值判断 DS1302 是否已停止
    {
        DS1302SingleWrite(7, 0x00);  // 撤销写保护以允许写入数据
        SetRealTime(&InitTime);      // 设置 DS1302 为默认的初始时间
    }
}
时钟显示调试完毕后,下一步就可以开始编写按键代码,使用按键可以调整时钟,调整闹钟的时间。当然,我们在调试按键底层驱动的时候,不一定要把所有想要的功能都罗列出来,可以先进行按键底层功能程序的调试,按下按键让蜂鸣器响一下,或者闪烁个小灯等都可以用来检验按键底层代码工作的正确性。随着我们程序量的加大,有些功能也可以进行综合了,可以在 Time.c 文件中和 main.c 文件中添加程序了,一边添加一边调试,而不是把所有的程序代码都写完后,像无头苍蝇一样到处找漏洞。

/***********************keyboard.h文件程序源代码*************************/
#ifndef _KEY_BOARD_H
#define _KEY_BOARD_H


#ifndef _KEY_BOARD_C

#endif

void KeyScan();
void KeyDriver();
void KeyAction(uint8 keycode);

#endif
/***********************keyboard.c文件程序源代码*************************/
#define  _KEY_BOARD_C
#include "config.h"
#include "keyboard.h"
#include "Time.h"
#include "main.h"

const uint8 code KeyCodeMap[4][4] = {  //矩阵按键到PC标准键码的映射表
    { '1',  '2',  '3', 0x26 },  //数字键1、数字键2、数字键3、向上键
    { '4',  '5',  '6', 0x25 },  //数字键4、数字键5、数字键6、向左键
    { '7',  '8',  '9', 0x28 },  //数字键7、数字键8、数字键9、向下键
    { '0', 0x1B, 0x0D, 0x27 }   //数字键0、ESC键、  回车键、 向右键
};
uint8 pdata KeySta[4][4] = {  //全部矩阵按键的当前状态
    {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1}
};

/* 按键动作函数,根据键码执行相应的操作 */
void KeyAction(uint8 keycode)
{
    if  ((keycode>='0') && (keycode<='9'))  //数字键输入当前位设定值
    {
        InputSetNumber(keycode);
    }
    else if (keycode == 0x25)  //向左键,向左切换设置位
    {
        SetLeftShift();
    }
    else if (keycode == 0x27)  //向右键,向右切换设置位
    {
        SetRightShift();
    }
    else if (keycode == 0x0D)  //回车键,切换运行状态/保存设置
    {
        SwitchSystemSta();
    }
    else if (keycode == 0x1B)  //Esc键,静音/取消当前设置
    {
        if (staSystem == E_NORMAL) //处于正常运行状态时闹铃静音
        {
            staMute = 1;
        }
        else                       //处于设置状态时退出设置
        {
            CancelCurSet();
        }
    }
}
/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void KeyDriver()
{
    uint8 i, j;
    static uint8 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++)  //循环扫描4*4的矩阵按键
    {
        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];     //刷新前一次的备份值
            }
        }
    }
}
/* 按键扫描函数,需在定时中断中调用,推荐调用间隔1ms */
void KeyScan()
{
    uint8 i;
    static uint8 keyout = 0;   //矩阵按键扫描输出索引
    static uint8 keybuf[4][4] = {  //矩阵按键扫描缓冲区
        {0xFF, 0xFF, 0xFF, 0xFF},  {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF},  {0xFF, 0xFF, 0xFF, 0xFF}
    };

    //将一行的4个按键值移入缓冲区
    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++)  //每行4个按键,所以循环4次
    {
        if ((keybuf[keyout][ i] & 0x0F) == 0x00)
        {   //连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下
            KeySta[keyout][ i] = 0;
        }
        else if ((keybuf[keyout][ i] & 0x0F) == 0x0F)
        {   //连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起
            KeySta[keyout][ i] = 1;
        }
    }
    //执行下一次的扫描输出
    keyout++;        //输出索引递增
    keyout &= 0x03;  //索引值加到4即归零
    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;
    }
}
   按键程序调试完毕后,下一步毫无疑问就是红外的代码了。红外所要实现的功能是和按键完全一样的,但是如果说我们把红外按键的代码解析出来后,再去做相应的操作显得有点多余了。我们的处理方式是,把红外的按键代码解析出来,和我们板载按键进行映射对应关系,不同的红外按键映射为板子上的不同的板载按键值就可以了,这样只需要写一套按键驱动程序,红外的代码只做解析和映射功能即可。
/***********************Infrared.h文件程序源代码*************************/
#ifndef _INFRARED_H
#define _INFRARED_H


#ifndef _INFRARED_C

#endif

void InitInfrared();
void InfraredDriver();

#endif
/***********************Infrared.c文件程序源代码*************************/
#define  _INFRARED_C
#include "config.h"
#include "Infrared.h"
#include "keyboard.h"

const uint8 code IrCodeMap[][2] = {  //红外键码到标准PC键码的映射表
    {0x45, 0x00}, {0x46, 0x00}, {0x47, 0x1B}, //开关->无  Mode->无   静音->ESC
    {0x44, 0x00}, {0x40, 0x25}, {0x43, 0x27}, //播放->无  后退->向左 前进->向右
    {0x07, 0x00}, {0x15, 0x28}, {0x09, 0x26}, // EQ->无   减号->向下 加号->向上
    {0x16, 0x30}, {0x19, 0x1B}, {0x0D, 0x0D}, //'0'->'0'  箭头->ESC  U/SD->回车
    {0x0C, 0x31}, {0x18, 0x32}, {0x5E, 0x33}, //'1'->'1'  '2'->'2'   '3'->'3'
    {0x08, 0x34}, {0x1C, 0x35}, {0x5A, 0x36}, //'4'->'4'  '5'->'5'   '6'->'6'
  {0x42, 0x37}, {0x52, 0x38}, {0x4A, 0x39}, //'7'->'7'  '6'->'8'   '9'->'9'
};

bit irflag = 0;  //红外接收标志,收到一帧正确数据后置1
uint8 ircode[4];  //红外代码接收缓冲区

/* 红外接收驱动,检测接收到的键码,调度相应动作函数 */
void InfraredDriver()
{
    uint8 i;
   
    if (irflag)
    {
        irflag = 0;
        for (i=0; i<sizeof(IrCodeMap)/sizeof(IrCodeMap[0]); i++) //遍历映射表
        {
            if (ircode[2] == IrCodeMap[ i][0])  //在表中找到当前接收的键码后,
            {                                  //用对应的映射码执行函数调度,
                KeyAction(IrCodeMap[ i][1]);    //直接调用按键动作函数即可。
                break;
            }
        }
    }
}
/* 初始化红外接收功能 */
void InitInfrared()
{
    IR_INPUT = 1;  //确保红外接收引脚被释放
    TMOD &= 0x0F;  //清零T1的控制位
    TMOD |= 0x10;  //配置T1为模式1
    TR1 = 0;       //停止T1计数
            ET1 = 0;       //禁止T1中断
    IT1 = 1;       //设置INT1为负边沿触发
    EX1 = 1;       //使能INT1中断
}
/* 获取当前高电平的持续时间 */
uint16 GetHighTime()
{
    TH1 = 0;  //清零T1计数初值
    TL1 = 0;
    TR1 = 1;  //启动T1计数
    while (IR_INPUT)  //红外输入引脚为1时循环检测等待,变为0时则结束本循环
    {
        if (TH1 >= 0x40)
        {            //当T1计数值大于0x4000,即高电平持续时间超过约18ms时,
            break;   //强制退出循环,是为了避免信号异常时,程序假死在这里。
        }
    }
    TR1 = 0;  //停止T1计数

    return (TH1*256 + TL1);  //T1计数值合成为16bit整型数,并返回该数
}
/* 获取当前低电平的持续时间 */
uint16 GetLowTime()
{
    TH1 = 0;  //清零T1计数初值
    TL1 = 0;
    TR1 = 1;  //启动T1计数
    while (!IR_INPUT)  //红外输入引脚为0时循环检测等待,变为1时则结束本循环
    {
        if (TH1 >= 0x40)
        {            //当T1计数值大于0x4000,即低电平持续时间超过约18ms时,
            break;   //强制退出循环,是为了避免信号异常时,程序假死在这里。
        }
    }
    TR1 = 0;  //停止T1计数

    return (TH1*256 + TL1);  //T1计数值合成为16bit整型数,并返回该数
}
/* INT1中断服务函数,执行红外接收及解码 */
void EXINT1_ISR() interrupt 2
{
    uint8 i, j;
    uint8 byt;
    uint16 time;
   
    //接收并判定引导码的9ms低电平
    time = GetLowTime();
    if ((time<7833) || (time>8755))  //时间判定范围为8.5~9.5ms,
    {                                //超过此范围则说明为误码,直接退出
        IE1 = 0;   //退出前清零INT1中断标志
        return;
    }
    //接收并判定引导码的4.5ms高电平
    time = GetHighTime();
    if ((time<3686) || (time>4608))  //时间判定范围为4.0~5.0ms,
    {                                //超过此范围则说明为误码,直接退出
        IE1 = 0;
        return;
    }
    //接收并判定后续的4字节数据
    for (i=0; i<4; i++)  //循环接收4个字节
    {
        for (j=0; j<8; j++)  //循环接收判定每字节的8个bit
        {
            //接收判定每bit的560us低电平
            time = GetLowTime();
            if ((time<313) || (time>718)) //时间判定范围为340~780us,
            {                             //超过此范围则说明为误码,直接退出
                IE1 = 0;
                return;
            }
            //接收每bit高电平时间,判定该bit的值
            time = GetHighTime();
            if ((time>313) && (time<718)) //时间判定范围为340~780us,
            {                             //在此范围内说明该bit值为0
                byt >>= 1;   //因低位在先,所以数据右移,高位为0
            }
            else if ((time>1345) && (time<1751)) //时间范围1460~1900us,
            {                                    //在此范围内说明该bit值为1
                byt >>= 1;   //因低位在先,所以数据右移,
                byt |= 0x80; //高位置1
            }
            else  //不在上述范围内则说明为误码,直接退出
            {
                IE1 = 0;
                return;
            }
        }
        ircode[ i] = byt;  //接收完一个字节后保存到缓冲区
    }
    irflag = 1;  //接收完毕后设置标志
    IE1 = 0;     //退出前清零INT1中断标志
}
   这一切底层的驱动完成之后,我们就可以整理调试main.c和Time.c内的功能代码了。一边添加功能一边调试,把最终的功能代码调试出来,在KST-51开发板上做验证。这一切都完事之后,我们可以添加一项新功能,就是DS18B20温度传感器显示,这个是个独立功能,直接写好代码,添加进去就可以了。
/***********************DS18B20.h文件程序源代码*************************/
#ifndef _DS18B20_H
#define _DS18B20_H


#ifndef _DS18B20_C

#endif

bit Start18B20();
bit Get18B20Temp(int16 *temp);

#endif
/***********************DS18B20.c文件程序源代码*************************/
#define  _DS18B20_C
#include "config.h"
#include "DS18B20.h"

/* 软件延时函数,延时时间(t*10)us */
void DelayX10us(uint8 t)
{
    do {
        _nop_();
        _nop_();
        _nop_();
        _nop_();
        _nop_();
        _nop_();
        _nop_();
        _nop_();
    } while (--t);
}
/* 复位总线,获取存在脉冲,以启动一次读写操作 */
bit Get18B20Ack()
{
    bit ack;
   
    EA = 0;   //禁止总中断
    IO_18B20 = 0;     //产生500us复位脉冲
    DelayX10us(50);
    IO_18B20 = 1;
    DelayX10us(6);    //延时60us
    ack = IO_18B20;   //读取存在脉冲
    while(!IO_18B20); //等待存在脉冲结束
    EA = 1;   //重新使能总中断
   
    return ack;
}
/* 向DS18B20写入一个字节,dat-待写入字节 */
void Write18B20(uint8 dat)
{
    uint8 mask;
   
    EA = 0;   //禁止总中断
            for (mask=0x01; mask!=0; mask<<=1)  //低位在先,依次移出8个bit
    {
                        IO_18B20 = 0;         //产生2us低电平脉冲
                        _nop_();
                        _nop_();
        if ((mask&dat) == 0)  //输出该bit值
            IO_18B20 = 0;
        else
            IO_18B20 = 1;
                        DelayX10us(6);        //延时60us
                        IO_18B20 = 1;         //拉高通信引脚
            }
    EA = 1;   //重新使能总中断
}
/* 从DS18B20读取一个字节,返回值-读到的字节 */
uint8 Read18B20()
{
    uint8 dat;
    uint8 mask;
   
    EA = 0;   //禁止总中断
            for (mask=0x01; mask!=0; mask<<=1)  //低位在先,依次采集8个bit
    {
                        IO_18B20 = 0;         //产生2us低电平脉冲
                        _nop_();
                        _nop_();
                        IO_18B20 = 1;         //结束低电平脉冲,等待18B20输出数据
                        _nop_();              //延时2us
                        _nop_();
                        if (!IO_18B20)        //读取通信引脚上的值
                                dat &= ~mask;
        else
            dat |= mask;
                        DelayX10us(6);        //再延时60us
            }
    EA = 1;   //重新使能总中断

    return dat;
}
/* 启动一次18B20温度转换,返回值-表示是否启动成功 */
bit Start18B20()
{
    bit ack;
   
    ack = Get18B20Ack();   //执行总线复位,并获取18B20应答
    if (ack == 0)          //如18B20正确应答,则启动一次转换
    {
            Write18B20(0xCC);  //跳过ROM操作
            Write18B20(0x44);  //启动一次温度转换
    }
    return ~ack;   //ack==0表示操作成功,所以返回值对其取反
}
/* 读取DS18B20转换的温度值,返回值-表示是否读取成功 */
bit Get18B20Temp(int16 *temp)
{
    bit ack;
    uint8 LSB, MSB; //16bit温度值的低字节和高字节
   
    ack = Get18B20Ack();    //执行总线复位,并获取18B20应答
    if (ack == 0)           //如18B20正确应答,则读取温度值
    {
                Write18B20(0xCC);   //跳过ROM操作
                Write18B20(0xBE);   //发送读命令
                LSB = Read18B20();  //读温度值的低字节
                MSB = Read18B20();  //读温度值的高字节
        *temp = ((int16)MSB << 8) + LSB;  //合成为16bit整型数
    }
            return ~ack;  //ack==0表示操作应答,所以返回值为其取反值
}
/***********************Time.h文件程序源代码*************************/
#ifndef _TIME_H
#define _TIME_H


#ifndef _TIME_C
extern bit staMute;
#endif

void RefreshTime();
void RefreshDate(uint8 ops);
void RefreshAlarm();
void AlarmMonitor();
void SwitchSystemSta();
void CancelCurSet();
void SetRightShift();
void SetLeftShift();
void InputSetNumber(uint8 ascii);

#endif
/***********************Time.c文件程序源代码*************************/
#define  _TIME_C
#include "config.h"
#include "DS1302.h"
#include "LedBuzzer.h"
#include "Lcd1602.h"
#include "Time.h"
#include "main.h"

uint8 code WeekMod[] = {  //星期X字符图片表
    0xFF, 0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7,  //星期日(红心)
    0xEF, 0xE7, 0xE3, 0xE7, 0xE7, 0xE7, 0xE7, 0xC3,  //星期1
    0xC3, 0x81, 0x9D, 0x87, 0xC3, 0xF9, 0xC1, 0x81,  //星期2
    0xC3, 0x81, 0x9D, 0xC7, 0xC7, 0x9D, 0x81, 0xC3,  //星期3
    0xCF, 0xC7, 0xC3, 0xC9, 0xC9, 0x81, 0xCF, 0xCF,  //星期4
    0x81, 0xC1, 0xF9, 0xC3, 0x87, 0x9D, 0x81, 0xC3,  //星期5
    0xC3, 0x81, 0xF9, 0xC3, 0x81, 0x99, 0x81, 0xC3,  //星期6
};

bit staMute = 0;  //静音标志位
uint8 AlarmHour = 0x07;  //闹钟时间的小时数
uint8 AlarmMin  = 0x30;  //闹钟时间的分钟数
struct sTime CurTime;    //当前日期时间

uint8 SetIndex = 0;  //设置位索引
uint8 pdata SetAlarmHour;    //闹钟小时数设置缓冲
uint8 pdata SetAlarmMin;     //闹钟分钟数设置缓冲
struct sTime pdata SetTime;  //日期时间设置缓冲区

/* 获取当前日期时间,并刷新时间和星期的显示 */
void RefreshTime()
{
    GetRealTime(&CurTime);                  //获取当前日期时间
    ShowLedNumber(5, CurTime.hour>>4, 0);   //时
    ShowLedNumber(4, CurTime.hour&0xF,1);
    ShowLedNumber(3, CurTime.min>>4,  0);   //分
    ShowLedNumber(2, CurTime.min&0xF, 1);
    ShowLedNumber(1, CurTime.sec>>4,  0);   //秒
    ShowLedNumber(0, CurTime.sec&0xF, 0);
    ShowLedArray(WeekMod + CurTime.week*8); //星期
}
/* 日期刷新函数,ops-刷新选项:为0时只当日期变化才刷新,非0则立即刷新 */
void RefreshDate(uint8 ops)
{
    uint8 pdata str[12];
    static uint8 backup = 0;
   
    if ((backup!=CurTime.day) || (ops!=0))
    {
        str[0] = ((CurTime.year>>12) & 0xF) + '0';  //4位数年份
        str[1] = ((CurTime.year>>8) & 0xF) + '0';
        str[2] = ((CurTime.year>>4) & 0xF) + '0';
        str[3] = (CurTime.year & 0xF) + '0';
        str[4] = '-';                        //分隔符
        str[5] = (CurTime.mon >> 4) + '0';   //月份
        str[6] = (CurTime.mon & 0xF) + '0';
        str[7] = '-';                        //分隔符
        str[8] = (CurTime.day >> 4) + '0';   //日期
        str[9] = (CurTime.day & 0xF) + '0';
        str[10] = '\0';         //字符串结束符
        LcdShowStr(0, 0, str);  //显示到液晶上
        backup = CurTime.day;   //刷新上次日期值
    }
}
/* 刷新闹钟时间的显示 */
void RefreshAlarm()
{
    uint8 pdata str[8];
   
    LcdShowStr(0, 1, "Alarm at ");     //显示提示标题
    str[0] = (AlarmHour >> 4) + '0';   //闹钟小时数
    str[1] = (AlarmHour & 0xF) + '0';
    str[2] = ':';                      //分隔符
    str[3] = (AlarmMin >> 4) + '0';    //闹钟分钟数
    str[4] = (AlarmMin & 0xF) + '0';
    str[5] = '\0';                     //字符串结束符
    LcdShowStr(9, 1, str);             //显示到液晶上
}
/* 闹钟监控函数,抵达设定的闹钟时间时执行闹铃 */
void AlarmMonitor()
{
    if ((CurTime.hour==AlarmHour) && (CurTime.min==AlarmMin)) //检查时间匹配
    {
        if (!staMute)  //检查是否静音
            staBuzzer = ~staBuzzer;  //实现蜂鸣器断续鸣叫
        else
            staBuzzer = 0;
    }
    else
    {
        staMute = 0;
        staBuzzer = 0;
    }
}
/* 将设置时间及标题提示显示到液晶上 */
void ShowSetTime()
{
    uint8 pdata str[18];
   
    str[0]  = ((SetTime.year>>4) & 0xF) + '0';  //2位数年份
    str[1]  = (SetTime.year & 0xF) + '0';
    str[2]  = '-';
    str[3]  = (SetTime.mon >> 4) + '0';   //月份
    str[4]  = (SetTime.mon & 0xF) + '0';
    str[5]  = '-';
    str[6]  = (SetTime.day >> 4) + '0';   //日期
    str[7]  = (SetTime.day & 0xF) + '0';
    str[8]  = '-';
    str[9]  = (SetTime.week & 0xF) + '0'; //星期
    str[10] = ' ';
    str[11] = (SetTime.hour >> 4) + '0';  //小时
    str[12] = (SetTime.hour & 0xF) + '0';
    str[13] = ':';
    str[14] = (SetTime.min >> 4) + '0';   //分钟
    str[15] = (SetTime.min & 0xF) + '0';
    str[16] = '\0';
    LcdShowStr(0, 0, "Set Date Time");  //显示提示标题
    LcdShowStr(0, 1, str);              //显示设置时间值
}
/* 将设置闹钟及标题提示显示到液晶上 */
void ShowSetAlarm()
{
    uint8 pdata str[8];
   
    str[0] = (SetAlarmHour >> 4) + '0';   //小时
    str[1] = (SetAlarmHour & 0xF) + '0';
    str[2] = ':';
    str[3] = (SetAlarmMin >> 4) + '0';    //分钟
    str[4] = (SetAlarmMin & 0xF) + '0';
    str[5] = '\0';
    LcdShowStr(0, 0, "Set Alarm");  //显示提示标题
    LcdShowStr(0, 1, str);          //显示设定闹钟值
}
/* 切换系统运行状态 */
void SwitchSystemSta()
{
    if (staSystem == E_NORMAL)  //正常运行切换到时间设置
    {
        staSystem = E_SET_TIME;
        SetTime.year = CurTime.year;  //当前时间拷贝到时间设置缓冲区中
        SetTime.mon  = CurTime.mon;
        SetTime.day  = CurTime.day;
        SetTime.hour = CurTime.hour;
        SetTime.min  = CurTime.min;
        SetTime.sec  = CurTime.sec;
        SetTime.week = CurTime.week;
        LcdClearScreen();  //液晶清屏
        ShowSetTime();     //显示设置时间
        SetIndex = 255;    //与接下来的右移一起将光标设在最左边的位置上
        SetRightShift();
        LcdOpenCursor();   //开启光标
    }
    else if (staSystem == E_SET_TIME)  //时间设置切换到闹钟设置
    {
        staSystem = E_SET_ALARM;
        SetTime.sec = 0;          //秒清零,即当设置时间后从0秒开始走时
        SetRealTime(&SetTime);    //设定时间写入实时时钟
        SetAlarmHour = AlarmHour; //当前闹钟值拷贝到设置缓冲区
        SetAlarmMin  = AlarmMin;
        LcdClearScreen();  //液晶清屏
        ShowSetAlarm();    //显示设置闹钟
        SetIndex = 255;    //与接下来的右移一起将光标设在最左边的位置上
        SetRightShift();
    }
    else  //闹钟设置切换会正常运行
    {
        staSystem = E_NORMAL;
        AlarmHour = SetAlarmHour;  //设定的闹钟值写入闹钟时间
        AlarmMin  = SetAlarmMin;
        LcdCloseCursor();  //关闭光标
        LcdClearScreen();  //液晶清屏
        RefreshTime();   //刷新当前时间
        RefreshDate(1);  //立即刷新日期显示
        RefreshTemp(1);  //立即刷新温度显示
        RefreshAlarm();  //闹钟设定值显示
    }
}
/* 取消当前设置,返回正常运行状态 */
void CancelCurSet()
{
    staSystem = E_NORMAL;
    LcdCloseCursor();  //关闭光标
    LcdClearScreen();  //液晶清屏
    RefreshTime();   //刷新当前时间
    RefreshDate(1);  //立即刷新日期显示
    RefreshTemp(1);  //立即刷新温度显示
    RefreshAlarm();  //闹钟设定值显示
}
/* 时间或闹钟设置时,设置位右移一位,到头后折回 */
void SetRightShift()
{
    if (staSystem == E_SET_TIME)
    {
        switch (SetIndex)
        {
            case 0: SetIndex=1;  LcdSetCursor(1, 1); break;
            case 1: SetIndex=2;  LcdSetCursor(3, 1); break;
            case 2: SetIndex=3;  LcdSetCursor(4, 1); break;
            case 3: SetIndex=4;  LcdSetCursor(6, 1); break;
            case 4: SetIndex=5;  LcdSetCursor(7, 1); break;
            case 5: SetIndex=6;  LcdSetCursor(9, 1); break;
            case 6: SetIndex=7;  LcdSetCursor(11,1); break;
            case 7: SetIndex=8;  LcdSetCursor(12,1); break;
            case 8: SetIndex=9;  LcdSetCursor(14,1); break;
            case 9: SetIndex=10; LcdSetCursor(15,1); break;
            default: SetIndex=0; LcdSetCursor(0, 1); break;
        }
    }
    else if (staSystem == E_SET_ALARM)
    {
        switch (SetIndex)
        {
            case 0: SetIndex=1;  LcdSetCursor(1,1); break;
            case 1: SetIndex=2;  LcdSetCursor(3,1); break;
            case 2: SetIndex=3;  LcdSetCursor(4,1); break;
            default: SetIndex=0; LcdSetCursor(0,1); break;
        }
    }
}
/* 时间或闹钟设置时,设置位左移一位,到头后折回 */
void SetLeftShift()
{
    if (staSystem == E_SET_TIME)
    {
        switch (SetIndex)
        {
            case 0: SetIndex=10; LcdSetCursor(15,1); break;
            case 1: SetIndex=0;  LcdSetCursor(0, 1); break;
            case 2: SetIndex=1;  LcdSetCursor(1, 1); break;
            case 3: SetIndex=2;  LcdSetCursor(3, 1); break;
            case 4: SetIndex=3;  LcdSetCursor(4, 1); break;
            case 5: SetIndex=4;  LcdSetCursor(6, 1); break;
            case 6: SetIndex=5;  LcdSetCursor(7, 1); break;
            case 7: SetIndex=6;  LcdSetCursor(9, 1); break;
            case 8: SetIndex=7;  LcdSetCursor(11,1); break;
            case 9: SetIndex=8;  LcdSetCursor(12,1); break;
            default: SetIndex=9; LcdSetCursor(14,1); break;
        }
    }
    else if (staSystem == E_SET_ALARM)
    {
        switch (SetIndex)
        {
            case 0: SetIndex=3;  LcdSetCursor(4,1); break;
            case 1: SetIndex=0;  LcdSetCursor(0,1); break;
            case 2: SetIndex=1;  LcdSetCursor(1,1); break;
            default: SetIndex=2; LcdSetCursor(3,1); break;
        }
    }
}
/* 输入设置数字,修改对应的设置位,并显示该数字,ascii-输入数字的ASCII码 */
void InputSetNumber(uint8 ascii)
{
    uint8 num;
   
    num = ascii - '0';
    if (num <= 9)  //只响应0~9的数字
    {
        if (staSystem == E_SET_TIME)
        {
            switch (SetIndex)
            {
                case 0: SetTime.year = (SetTime.year&0xFF0F)|(num<<4);
                        LcdShowChar(0, 1, ascii);  break;      //年份高位数字
                case 1: SetTime.year = (SetTime.year&0xFFF0)|(num);
                        LcdShowChar(1, 1, ascii);  break;      //年份低位数字
                case 2: SetTime.mon = (SetTime.mon&0x0F)|(num<<4);
                        LcdShowChar(3, 1, ascii);  break;      //月份高位数字
                case 3: SetTime.mon = (SetTime.mon&0xF0)|(num);
                        LcdShowChar(4, 1, ascii);  break;      //月份低位数字
                case 4: SetTime.day = (SetTime.day&0x0F)|(num<<4);
                        LcdShowChar(6, 1, ascii);  break;      //日期高位数字
                case 5: SetTime.day = (SetTime.day&0xF0)|(num);
                        LcdShowChar(7, 1, ascii);  break;      //日期低位数字
                case 6: SetTime.week = (SetTime.week&0xF0)|(num);
                        LcdShowChar(9, 1, ascii);  break;      //星期数字
                case 7: SetTime.hour = (SetTime.hour&0x0F)|(num<<4);
                        LcdShowChar(11,1, ascii);  break;      //小时高位数字
                case 8: SetTime.hour = (SetTime.hour&0xF0)|(num);
                        LcdShowChar(12,1, ascii);  break;      //小时低位数字
                case 9: SetTime.min = (SetTime.min&0x0F)|(num<<4);
                        LcdShowChar(14,1, ascii);  break;      //分钟高位数字
                default:SetTime.min = (SetTime.min&0xF0)|(num);
                        LcdShowChar(15,1, ascii);  break;      //分钟低位数字
            }
            SetRightShift();  //完成该位设置后自动右移
        }
        else if (staSystem == E_SET_ALARM)
        {
            switch (SetIndex)
            {
                case 0: SetAlarmHour = (SetAlarmHour&0x0F) | (num<<4);
                        LcdShowChar(0,1, ascii); break;      //小时高位数字
                case 1: SetAlarmHour = (SetAlarmHour&0xF0) | (num);
                        LcdShowChar(1,1, ascii); break;      //小时低位数字
                case 2: SetAlarmMin = (SetAlarmMin&0x0F) | (num<<4);
                        LcdShowChar(3,1, ascii); break;      //分钟高位数字
                default:SetAlarmMin = (SetAlarmMin&0xF0) | (num);
                        LcdShowChar(4,1, ascii); break;      //分钟低位数字
            }
            SetRightShift();  //完成该位设置后自动右移
        }
    }
}
/***********************main.h文件程序源代码*************************/
#ifndef _MAIN_H
#define _MAIN_H

enum eStaSystem {  //系统运行状态枚举
    E_NORMAL, E_SET_TIME, E_SET_ALARM
};

#ifndef _MAIN_C
extern enum eStaSystem staSystem;
#endif

void RefreshTemp(uint8 ops);
void ConfigTimer0(uint16 ms);

#endif
/***********************main.c文件程序源代码*************************/
#define  _MAIN_C
#include "config.h"
#include "Lcd1602.h"
#include "LedBuzzer.h"
#include "keyboard.h"
#include "DS1302.h"
#include "DS18B20.h"
#include "Infrared.h"
#include "Time.h"
#include "main.h"

bit flag2s = 0;    //2s定时标志位
bit flag200ms = 0; //200ms定时标志
uint8 T0RH = 0;    //T0重载值的高字节
uint8 T0RL = 0;    //T0重载值的低字节
enum eStaSystem staSystem = E_NORMAL;  //系统运行状态

void main()
{
    EA = 1;           //开总中断
    ConfigTimer0(1);  //配置T0定时1ms
    InitLed();        //初始化LED模块
    InitDS1302();     //初始化实时时钟模块
    InitInfrared();   //初始化红外接收模块
    InitLcd1602();    //初始化液晶模块
    Start18B20();     //启动首次温度转换
   
    while (!flag2s);  //上电后延时2秒
    flag2s = 0;
    RefreshTime();    //刷新当前时间
    RefreshDate(1);   //立即刷新日期显示
    RefreshTemp(1);   //立即刷新温度显示
    RefreshAlarm();   //闹钟设定值显示
   
    while (1)  //进入主循环
    {
        KeyDriver();      //执行按键驱动
        InfraredDriver(); //执行红外接收驱动
        if (flag200ms)    //每隔200ms执行以下分支
        {
            flag200ms = 0;
            FlowingLight();  //流水灯效果实现
            RefreshTime();   //刷新当前时间
            AlarmMonitor();  //监控闹钟
            if (staSystem == E_NORMAL)  //正常运行时刷新日期显示
            {
                RefreshDate(0);
            }
        }
        if (flag2s)  //每隔2s执行以下分支
        {
            flag2s = 0;
            if (staSystem == E_NORMAL)  //正常运行时刷新温度显示
            {
                RefreshTemp(0);
            }
        }
    }
}
/* 温度刷新函数,读取当前温度并根据需要刷新液晶显示,
** ops-刷新选项:为0时只当温度变化才刷新,非0则立即刷新 */
void RefreshTemp(uint8 ops)
{
    int16 temp;
    uint8 pdata str[8];
    static int16 backup = 0;
   
    Get18B20Temp(&temp); //获取当前温度值
    Start18B20();        //启动下一次转换
    temp >>= 4;          //舍弃4bit小数位
    if ((backup!=temp) || (ops!=0)) //按需要刷新液晶显示
    {
        str[0] = (temp/10) + '0';  //十位转为ASCII码
        str[1] = (temp%10) + '0';  //个位转为ASCII码
        str[2] = '\'';             //用'C代替℃
        str[3] = 'C';
        str[4] = '\0';             //字符串结束符
        LcdShowStr(12, 0, str);    //显示到液晶上
        backup = temp;             //刷新上次温度值
    }
}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(uint16 ms)
{
    uint32 tmp;
   
    tmp = (SYS_MCLK*ms)/1000; //计算所需的计数值
    tmp = 65536 - tmp;        //计算定时器重载值
    tmp = tmp + 34;           //补偿中断响应延时造成的误差   
    T0RH = (uint8)(tmp>>8);   //定时器重载值拆分为高低字节
    T0RL = (uint8)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}
/* T0中断服务函数,实现系统定时和按键扫描 */
void InterruptTimer0() interrupt 1
{
    static uint8 tmr2s = 0;
    static uint8 tmr200ms = 0;
   
    TH0 = T0RH;  //定时器重新加载重载值
    TL0 = T0RL;
    tmr200ms++;  //定时200ms
    if (tmr200ms >= 200)
    {
        tmr200ms = 0;
        flag200ms = 1;
        tmr2s++;  //定时2s
        if (tmr2s >= 10)
        {
            tmr2s = 0;
            flag2s = 1;
        }
    }
    KeyScan();   //执行按键扫描
}
   程序代码已经完成了,但是大家的学习还得继续,把思路学差不多之后,要自己能够不看源代码,独立把这个程序编写出来,那么我就可以很高兴的告诉你,你的单片机已经合格了,你可以动手开发一些小产品,进入下一个层次的历练了。
   当然了,同学们不要指望这样的代码一下子写出来就好用,包括我们研发工程师,调试这种代码也是一步步来的,在调试的过程中,可能还要穿插修改很多之前写好的代码,协调功能工作等等。同学们如果独立写这种代码,3天到一个周调试出来还是比较正常的。学到这里,相信同学们对于做技术的基本耐性已经具备了。做技术,耐心、细心、恒心,缺一不可。不要像初学那样遇到一个问题动不动就浮躁了,慢慢来,最终把这个功能实现出来,完成你单片机之路的第一个项目。
  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Scratch多功能电子闹钟是一款功能强大的电子闹钟,它采用Scratch编程语言来实现各种复杂的功能。 首先,它具有精确的时间显示功能。它可以显示当前时间,并且能够根据设定的时间进行自动调整。这意味着无论你在哪里,你都能知道准确的时间。 其次,Scratch多功能电子闹钟拥有多种闹钟设置。你可以根据自己的需求设置多个闹钟,比如平日闹钟、周末闹钟、临时闹钟等等。而且,你可以设置不同的闹铃声音和闹钟模式,如震动、铃声、渐响等,以适应不同的场合和个人需求。 另外,Scratch多功能电子闹钟还具有闹钟提醒功能。它可以在提醒时间到达时自动发出声音或其他提醒方式,确保你不会错过任何重要的事情。 此外,该闹钟还配备了室内温度显示功能。这个功能可以告诉你当前的室内温度,让你了解到室内环境的变化,方便你做出相应的调整。 最后,Scratch多功能电子闹钟还支持显示日历和定时器功能。它可以显示当前日期,并且能够设置倒计时功能,方便你在做饭、运动或其他需要计时的活动中使用。 综上所述,Scratch多功能电子闹钟不仅具有显示时间、设置闹钟和提醒功能,还能显示室内温度、日历和定时器功能,极大地方便了我们的生活和工作。 ### 回答2: Scrach多功能电子闹钟是一款实用且功能丰富的电子设备。它不仅可以作为闹钟,还可以充当温湿度计、日历以及多个时区的世界时钟。 首先,Scrach电子闹钟具有明亮的彩色液晶显示屏,可以清晰地显示时间、日期和温湿度。它采用了创新的触摸屏设计,使用者只需要轻轻触摸屏幕,就可以设置闹钟以及调整其他功能。而且,液晶屏背光可以手动或自动调节,确保在任何环境下都能读取显示信息。 其次,Scrach电子闹钟内置了多个实用功能。例如,它可以设置多个闹钟,用户可以根据自己的需求设置不同的闹铃并选择是否重复响铃。此外,它还可以监测室内温度和湿度,并提供相应的指示。用户可以根据这些信息来调整空调和加湿器,使室内环境更加舒适。 除此之外,Scrach电子闹钟还具备日历和世界时钟功能。在液晶屏上,用户可以轻松浏览日历,查看具体日期和星期几。同时,Scrach闹钟还可以显示多个时区的时间,方便人们了解不同国家或地区的时间,特别适用于商务人士和旅行者。 总之,Scrach多功能电子闹钟是一款多用途的电子设备,具备闹钟、温湿度计、日历和世界时钟等多项功能。它的智能设计和简单操作使用户可以轻松掌握和使用这些功能。无论是在家庭、办公室还是旅行中,Scrach电子闹钟都能提供准确的时间和相关信息,使生活更加便捷和高效。 ### 回答3: Scratch多功能电子闹钟是一款集闹钟、日历、温度计和音乐播放等功能于一身的创新产品。它使用Scratch编程软件来进行操作和设置,给用户带来了全新的使用体验。 首先,Scratch多功能电子闹钟具有准确可靠的闹钟功能。用户可以通过设置闹铃时间来提醒自己每天的起床时间或者重要的活动时间。在设计上,闹钟的铃声可以任意选择,用户可以从预设的音乐中挑选喜欢的作为闹铃声音。 此外,Scratch闹钟还具备日历功能,能够实时显示当前日期和星期,让用户清楚了解每天的具体时间和日期。特别是在工作和学习中,了解当前日期对于人们的常规行动计划非常重要。 Scratch多功能电子闹钟还设计有温度计功能。它可以实时测量室内的温度,提醒用户当前周围的温度情况。尤其是在变换季节和室内温度波动大的时候,温度计功能可以帮助我们更好地调整室内温度和舒适度。 最后,这款闹钟还具备音乐播放功能。通过连接手机或其他音乐设备,用户可以播放自己喜欢的音乐、收听广播或者解压放松。音乐是生活中必不可少的一部分,Scratch多功能电子闹钟的音乐播放功能让我们在起床或者放松的时候都能享受到美妙的音乐。 总而言之,Scratch多功能电子闹钟将闹钟、日历、温度计和音乐播放等功能集合于一体,为用户带来全方位的便利和享受。无论是在起床、提醒、查看日期,还是了解温度和放松娱乐,这款闹钟都能满足我们的需求,让我们过上更加有规律、精确和愉快的生活。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值