我们课程到了这里,基本知识介绍完毕。如果同学们能够认真把前边的“降龙十八章”领悟透彻,那剩下的主要工作就是不断反复练习巩固了。本章我们首先介绍实际项目开发中的一些技巧和规范性的东西,然后带领大家一起来做一个真正的项目,把项目开发的整个流程都走一遍。
19.1 类型说明
C 语言不仅提供了丰富的数据类型给我们使用,而且还允许用户自己定义类型说明符,也就是说为了方便,给已经存在的数据类型起个“代号”,比如“ 9527 就是你的终身代号”,就用 9527 来代表某个人。在 C 语言中,使用 typedef 即可完成这项功能,定义格式如下:
在前边的章节中,我们多次使用过文件包含命令 #include ,这条指令的功能是将指定的被包含文件的全部内容插到该命令行的位置处,从而把指定文件和当前的源程序文件连成一个源文件参与编译,通常的写法如下:
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 条件编译
条件编译属于预处理程序,包括我们之前讲的宏,都是程序在编译之前的一些必要的处理过程,这些都不是实际程序功能代码,而仅仅是告诉编译器需要进行的特定操作等。
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 硬件布局规划
19.4 多功能电子钟
本章的重头戏就是我们要做的这个项目实践开发——多功能电子钟。当接到一个具体项目开发任务后,要根据项目做出框架规划,整理出逻辑思路,并且写出规范的程序,调试代码最终完成功能。[size=14.0000pt]19.4.1 硬件布局规划
作为电子钟,或者说万年历,提供日期、时间的显示是一个基本的功能,但是我们的设计要求并不满足于基本功能,而是要提供更多的信息,并且兼容人性化设计。在我们的设计中,除了基本的走时
(
包括时间、日期、星期
)
、板载按键校时功能外,还提供闹钟、温度测量、红外遥控校时这几项实用功能,所以称之为多功能。
如果一个产品只是所需功能的杂乱堆积,而不考虑怎样让人用起来更舒服、更愉悦,那么这就非常的不人性化,也绝对不是一个优秀的设计或者说产品。比如电子钟把日期和时间都显示到液晶上,这样看起来主次就不是很分明,显得杂乱。人性化设计考虑的是大多数人的行为习惯,当然最终的产品依靠了设计人员的经验和审美等因素。比如我们
KST-51
开发板的器件布局,右上方向是显示器件,右下是按键输入,有一些外围器件比如上下拉电阻,三极管等我们可以隐藏到液晶底下,这就是大多数人的习惯。而在我们的多功能电子钟项目中,如何去体现人性化设计呢?
我们先来观察一下各种显示器件,数字显示如果采用
LED
点阵或者数码管就会比较醒目,但是点阵无法同时显示这么多数字,于是我们就把最常用的时间用数码管来显示,日期、闹钟设置、温度等辅助信息我们显示到液晶上。那么点阵呢?我们可以用它来显示星期,这对于盼望着周末的人们来说是不是很醒目很人性化呢?对了,还有独立的
LED
,我们就用它来给电子钟做装饰吧,用个来回跑的流水灯增加点活泼气氛。最后再来个遥控器功能,如果电子钟挂的太高了或者放在不方便触碰的位置,我们就可以使用遥控器来校时。大家再来想想看,整个过程是不是挺人性化的。
当然了,我们所用的是
KST-51
单片机开发板来作为我们的硬件平台,如果这个是个实际项目,就不需要那么多外围器件了,首先做好单片机最小系统,而后配备我们多功能电子钟所需要的硬件外设就可以了。也就是说,我们在进行项目开发时,设计的硬件电路是根据我们的实际项目需求来设计的。
19.4.2 程序结构组织
项目需求和硬件规划已经确定了,我们就得研究如何实现它们,程序结构如何组织。一个项目,如果需要的部件很多,同时实现的功能也很多,为了方便编写和程序维护,整个程序必须采用模块化编程,也就是每个模块对应一个 c文件来实现,这种用法实际上在前面的章节已经开始使用了。一方面,如果所有的代码堆到一起会显得杂乱无章,更重要的是容易造成意外错误,程序一旦有逻辑上的问题或者更新需求,这种维护将变成一种灾难。此外,当一个项目程序量很大的时候,可以由多个程序员共同参与编程,多模块的方式也可以让每个程序员之间的代码最终很方便的融合到一起。
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天到一个周调试出来还是比较正常的。学到这里,相信同学们对于做技术的基本耐性已经具备了。做技术,耐心、细心、恒心,缺一不可。不要像初学那样遇到一个问题动不动就浮躁了,慢慢来,最终把这个功能实现出来,完成你单片机之路的第一个项目。