第19章 实践项目开发指导--多功能电子钟

发布者:太和清音最新更新时间:2020-07-23 来源: 51hei关键字:实践项目  开发指导  多功能电子钟 手机看文章 扫描二维码
随时随地手机看文章

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 ,这个文件所在的位置是keil软件安装目录的C51INC这个路径内,大家可以去看看,在这个文件夹内,有很多系统自带的头文件,当然也包含了这个头文件。当我们一旦写了#include 这条指令后,那么相当于在我们当前的.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 这条指令,我们就可以随便使用P0、TCON、TMOD这些寄存器和TR0、TR1、TI、RI等这些寄存器的位,都是因为它们已经在这个头文件中定义或声明过了。


前边我们讲过,要调用某个函数,必须提前进行声明。而Keil自己做了很多函数,生成了库文件,我们如果要使用这些函数的时候,不需要写这些函数的代码,而直接调用这些函数即可,调用之前首先要进行声明一下,而这些声明也放在头文件当中。比如我们所用的_nop_();函数,就是在这个头文件中。


在我们前边应用的实例中,很多文件中的所要用到的函数,都是在其他文件中定义的,我们在当前文件要调用它的时候,提前声明一下即可。为了让我们程序的易维护性和可移植性提高,我们自己就可以编写我们所需要的头文件。我们自己编写的头文件中不仅仅可以进行函数的声明,变量的外部声明,一些宏定义也可以放在其中。


举个例子,比如我们在main.c这个文件中,配套写了一个main.h文件。新建头文件的方式也很简单,和.c是类似的,首先点击新建文件的那个图标,或者点击菜单File->New,然后点击保存文件,保存的时候命名为main.h即可。为了方便我们编写、修改维护,我们在Keil编程环境中新建一个头文件组,把所有的源文件放在一个组内,把所有的头文件放在一个组内,如图19-1所示。

psb(36).jpeg

图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 多功能电子钟
本章的重头戏就是我们要做的这个项目实践开发——多功能电子钟。当接到一个具体项目开发任务后,要根据项目做出框架规划,整理出逻辑思路,并且写出规范的程序,调试代码最终完成功能。


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所示。

psb(37).jpeg

图19-2 多功能电子钟流程图


19.4.3程序代码编写

在实际项目开发中,我们不仅仅希望我们的源程序、头文件等文件结构规范、代码编写规范,更希望我们的工程文件规整规范,方便维护。因此我们首先新建一个lesson19_1的文件夹,用来存放我们本章的工程文件。而后我们新建工程保存的时候,在lesson19-1文件夹内再建立一个文件夹,取名为project,专门用于存放工程文件的,如图19-3所示。

psb(38).jpeg

图19-3 工程文件夹


然后我们新建文件,保存的时候,在lesson19_1目录再建立一个文件夹,取名为source文件夹,专门用来存放我们的源代码,如图19-4所示。

psb(39).jpeg

图19-4 文件文件夹


最后,随便看一个之前的例子都能看到,工程编译后会生成很多额外的文件,这些文件可以统称为编译输出文件,输出文件的路径配置,进入Options for Target->Output,点击Select Folder for Objects,在lesson19_1建立一个文件夹,取名为output,专门用来存放这些输出文件,如图19-5所示。

psb(40).jpeg

图19-5 输出文件夹


进行了这样三个步骤,当今后我们要对这个工程进行整理编写的时候,文件就不再凌乱了,而是非常规整的排列在我们的文件夹内。尤其是今后大家还可能学到编写程序的另外的方式,就是编译的时候使用Keil软件,而编写代码的时候在其他更好的编辑器中进行,那么编辑器的工程文件也可以放到project下,而不会对其它部分产生任何影响。总之,这是一套规范而又实用的工程文件组织方案。

[1] [2] [3] [4] [5] [6] [7] [8]
关键字:实践项目  开发指导  多功能电子钟 引用地址:第19章 实践项目开发指导--多功能电子钟

上一篇:第20章 单片机开发常用工具的使用
下一篇:第18章 RS485通信和Modbus协议

小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
  • 学习ARM开发(16)
    ARM有很多东西要学习,那么中断,就肯定是需要学习的东西。自从CPU引入中断以来,才真正地进入多任务系统工作,并且大大提高了工作效率。采 ...
  • 学习ARM开发(17)
    因为嵌入式系统里全部要使用中断的,那么我的S3C44B0怎么样中断流程呢?那我就需要了解整个流程了。要深入了解,最好的方法,就是去写程序 ...
  • 学习ARM开发(18)
    上一次已经了解ARM的中断处理过程,并且可以设置中断函数,那么它这样就可以工作了吗?答案是否定的。因为S3C44B0还有好几个寄存器是控制中 ...
  • 嵌入式系统调试仿真工具
    嵌入式硬件系统设计出来后就要进行调试,不管是硬件调试还是软件调试或者程序固化,都需要用到调试仿真工具。 随着处理器新品种、新 ...
  • 最近困扰在心中的一个小疑问终于解惑了~~
    最近在驱动方面一直在概念上不能很好的理解 有时候结合别人写的一点usb的例子能有点感觉,但是因为arm体系里面没有像单片机那样直接讲解引脚 ...
  • 学习ARM开发(1)
  • 学习ARM开发(2)
  • 学习ARM开发(4)
  • 学习ARM开发(6)
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved