好的开端是成功的一半
经过上一章的学习,我想你现已把握了如安在程序中开释CPU了。期望可以持续坚持下去。一个杰出的开端是成功的一半。咱们今日所做的一切都是为了在单片机编程上做的更好。
在议论今日的主题之前,先说下我曾经的一些阅历。在刚开端接触到C言语程序的时分,由于学习内容所限,写的程序都不是很大,一般也就几百行而矣。所以一切的程序都完结在一个源文件里边。记住那时分大一参与校园里的一个电子设计大赛,调试了一个多星期,一切程序加起来大约将近1000行,长长的一个文件,从上阅读下来都要好半天。出了过错简略的语法过错还好定位,其它一些过错,往往找半天才找的到。那个时分开端知道了模块化编程这个东西,也尝试着开端把程序分模块编写。最开端是把相同功用的一些函数(比如1602液晶的驱动)悉数写在一个头文件(.h)文件里边,然后需求调用的当地包括进去,可是很快发现这种办法有其局限性,很简略犯重复包括的过错。
并且调用起来也很不便利。很快暑假的电子设计大赛来临了,校园对咱们的单片机软件编程进行了一些培训。由于校园历年来参与国赛和省赛,因此积累了必定数量的驱动模块,那些日子,教师每天都会安置必定量的使命,让咱们用这些模块组合起来,完结必定功用。而正是那些日子模块化编程的培训,使我关于模块化编程有了更进一步的知道。并且程序标准也开端渐渐留意起来。尔后的日子,不论程序的巨细,均选用模块化编程的办法去编写。很长一段时间以来,一向有单片机爱好者在QQ上和我一同沟通。有时分,他们会发过来一些有问题的程序源文件,让我帮助修正一下。同样是长长的一个文件,并且命名极不标准,从头看下来,着实是苦楚,说实话,还真不如我从头给他们写一个更快一些,此话到不假,由于手头积累了必定量的模块,在完结一个新的体系时分,只需求根据上层功用需求,在底层模块的支持下,可以很快便利的完结。而不需求自始至终再一砖一瓦的从头编写。藉此,也可以看出模块化编程的一个优点,便是可重复运用率高。下面让咱们揭开模块化奥秘面纱,一窥其真面目。
C言语源文件 *.c
说到C言语源文件,咱们都不会生疏。由于咱们往常写的程序代码简直都在这个XX.C文件里边。编译器也是以此文件来进行编译并生成相应的方针文件。作为模块化编程的组成根底,咱们所要完结的一切功用的源代码均在这个文件里。抱负的模块化应该可以看成是一个黑盒子。即咱们只关怀模块供给的功用,而不论模块内部的完结细节。比如咱们买了一部手机,咱们只需求会用手机供给的功用即可,不需求知晓它是怎么把短信发出去的,怎么呼应咱们按键的输入,这些进程对咱们用户而言,便是是一个黑盒子。
在大规模程序开发中,一个程序由许多个模块组成,很可能,这些模块的编写使命被分配到不同的人。而你在编写这个模块的时分很可能就需求运用到他人写好的模块的托言,这个时分咱们关怀的是,它的模块完结了什么样的接口,我该怎么去调用,至于模块内部是怎么安排的,关于我而言,无需过多重视。而寻求接口的单一性,把不需求的细节尽可能对外部屏蔽起来,正是咱们所需求留意的当地。
C言语头文件 *.h
谈及到模块化编程,必然会涉及到多文件编译,也便是工程编译。在这样的一个体系中,往往会有多个C文件,并且每个C文件的效果不尽相同。在咱们的C文件中,由于需求对外供给接口,因此有必要有一些函数或许是变量供给给外部其它文件进行调用。
假定咱们有一个LCD.C文件,其供给最基本的LCD的驱动函数
LcdPutChar(char cNewValue) ; //在当时方位输出一个字符
而在咱们的别的一个文件中需求调用此函数,那么咱们该怎么做呢?
头文件的效果正是在此。可以称其为一份接口描绘文件。其文件内部不应该包括任何实质性的函数代码。咱们可以把这个头文件了解成为一份阐明书,阐明的内容便是咱们的模块对外供给的接口函数或许是接口变量。一起该文件也包括了一些很重要的宏界说以及一些结构体的信息,离开了这些信息,很可能就无法正常运用接口函数或许是接口变量。可是总的原则是:不应让外界知道的信息就不应该出现在头文件里,而外界调用模块内接口函数或许是接口变量一切必要的信息就必定要出现在头文件里,不然,外界就无法正确的调用咱们供给的接口功用。因此为了让外部函数或许文件调用咱们供给的接口功用,就有必要包括咱们供给的这个接口描绘文件—-即头文件。一起,咱们自身模块也需求包括这份模块头文件(由于其包括了模块源文件中所需求的宏界说或许是结构体),比如咱们往常所用的文件都是一式三份相同,模块自身也需求包括这个头文件。
下面咱们来界说这个头文件,一般来说,头文件的姓名应该与源文件的姓名保持一致,这样咱们便可以明晰的知道哪个头文件是哪个源文件的描绘。
所以便得到了LCD.C的头文件LCD.h 其内容如下。
#ifndef _LCD_H_
#define _LCD_H_
extern LcdPutChar(char cNewValue) ;
#endif
这与咱们在源文件中界说函数时有点相似。不同的是,在其前面添加了extern 修饰符标明其是一个外部函数,可以被外部其它模块进行调用。
#ifndef _LCD_H_
#define _LCD_H_
#endif
这个几条条件编译和宏界说是为了避免重复包括。假如有两个不同源文件需求调用LcdPutChar(char cNewValue)这个函数,他们别离都经过#include “Lcd.h”把这个头文件包括了进去。在第一个源文件进行编译时分,由于没有界说过 _LCD_H_ 因此 #ifndef _LCD_H_ 条件树立,所以界说_LCD_H_ 并将下面的声明包括进去。在第二个文件编译时分,由于第一个文件包括时分,现已将_LCD_H_界说过了。因此#ifndef _LCD_H_ 不树立,整个头文件内容就没有被包括。假定没有这样的条件编译句子,那么两个文件都包括了extern LcdPutChar(char cNewValue) ; 就会引起重复包括的过错。
不得不说的typedef
许多朋友好像了习气程序中运用如下句子来对数据类型进行界说
#define uint unsigned int
#define uchar unsigned char
然后在界说变量的时分 直接这样运用
uint g_nTimeCounter = 0 ;
不可否认,这样的确很便利,并且关于移植起来也有必定的便利性。可是考虑下面这种状况你还会 这么以为吗?
#define PINT unsigned int * //界说unsigned int 指针类型
PINT g_npTimeCounter, g_npTimeState ;
那么你到底是界说了两个unsigned int 型的指针变量,仍是一个指针变量,一个整形变量呢?而你的初衷又是什么呢,想界说两个unsigned int 型的指针变量吗?假如是这样,那么估量过不久就会处处抓狂找过错了。
幸亏的是C言语现已为咱们考虑到了这一点。typedef 正是为此而生。为了给变量起一个别号咱们可以用如下的句子
typedef unsigned int uint16 ; //给指向无符号整形变量起一个别号 uint16
typedef unsigned int * puint16 ; //给指向无符号整形变量指针起一个别号 puint16
在咱们界说变量时分便可以这样界说了:
uint16 g_nTimeCounter = 0 ; //界说一个无符号的整形变量
puint16 g_npTimeCounter ; //界说一个无符号的整形变量的指针
在咱们运用51单片机的C言语编程的时分,整形变量的规模是16位,而在根据32的微处理下的整形变量是32位。假使咱们在8位单片机下编写的一些代码想要移植到32位的处理器上,那么很可能咱们就需求在源文件中处处修正变量的类型界说。这是一件巨大的作业,为了考虑程序的可移植性,在一开端,咱们就应该养成杰出的习气,用变量的别号进行界说。
如在8位单片机的平台下,有如下一个变量界说
uint16 g_nTimeCounter = 0 ;
假如移植32单片机的平台下,想要其的规模仍旧为16位。
可以直接修正uint16 的界说,即
typedef unsigned short int uint16 ;
这样就可以了,而不需求到源文件处处寻觅并修正。
将常用的数据类型悉数选用此种办法界说,构成一个头文件,便于咱们今后编程直接调用。
文件名 MacroAndConst.h
其内容如下:
#ifndef _MACRO_AND_CONST_H_
#define _MACRO_AND_CONST_H_
typedef unsigned int uint16;
typedef unsigned int UINT;
typedef unsigned int uint;
typedef unsigned int UINT16;
typedef unsigned int WORD;
typedef unsigned int word;
typedef int int16;
typedef int INT16;
typedef unsigned long uint32;
typedef unsigned long UINT32;
typedef unsigned long DWORD;
typedef unsigned long dword;
typedef long int32;
typedef long INT32;
typedef signed char int8;
typedef signed char INT8;
typedef unsigned char byte;
typedef unsigned char BYTE;
typedef unsigned char uchar;
typedef unsigned char UINT8;
typedef unsigned char uint8;
typedef unsigned char BOOL;
#endif
至此,好像咱们关于源文件和头文件的分工以及模块化编程有那么一点概念了。那么让咱们抓住时机,将上一章的咱们编写的LED闪耀函数进行模块区分并从头安排进行编译。
在上一章中咱们首要完结的功用是P0口所驱动的LED以1Hz的频率闪耀。其间用到了定时器,以及LED驱动模块。因此咱们可以简略的将整个工程分红三个模块,定时器模块,LED模块,以及主函数
对应的文件联系如下
main.c
Timer.c –?Timer.h
Led.c –?Led.h
在开端从头编写咱们的程序之前,先给咱们讲一下如安在KEIL中树立工程模板吧,这个模板是我一向沿用至今。期望可以给咱们一点启示。
下面的内容就首要以图片为主了。一起辅以少数文字阐明。
咱们以芯片AT89S52为例。
OK ,到此一个简略的工程模板就树立起来了,今后咱们再新建源文件和头文件的时分,就可以直接保存到src文件目录下面了。
下面咱们开端编写各个模块文件。
首要编写Timer.c 这个文件首要内容便是定时器初始化,以及定时器中止服务函数。其内容如下。
#include
bit g_bSystemTime1Ms = 0 ; // 1MS体系时标
void Timer0Init(void)
{
TMOD &= 0xf0 ;
TMOD |= 0x01 ; //定时器0作业办法1
TH0 = 0xfc ; //定时器初始值
TL0 = 0x66 ;
TR0 = 1 ;
ET0 = 1 ;
}
void Time0Isr(void) interrupt 1
{
TH0 = 0xfc ; //定时器从头赋初值
TL0 = 0x66 ;
g_bSystemTime1Ms = 1 ; //1MS时标标志方位位
}
由于在Led.c文件中需求调用咱们的g_bSystemTime1Ms变量。一起主函数需求调用Timer0Init()初始化函数,所以应该对这个变量和函数在头文件里作外部声明。以便利其它函数调用。
Timer.h 内容如下。
#ifndef _TIMER_H_
#define _TIMER_H_
extern void Timer0Init(void) ;
extern bit g_bSystemTime1Ms ;
#endif
完结了定时器模块后,咱们开端编写LED驱动模块。
Led.c 内容如下:
#include
#include “MacroAndConst.h”
#include “Led.h”
#include “Timer.h”
static uint16 g_u16LedTimeCount = 0 ; //LED计数器
static uint8 g_u8LedState = 0 ; //LED状况标志, 0表明亮,1表明平息
#define LED P0 //界说LED接口
#define LED_ON() LED = 0x00 ; //一切LED亮
#define LED_OFF() LED = 0xff ; //一切LED平息
void LedProcess(void)
{
if(0 == g_u8LedState) //假如LED的状况为亮,则点亮LED
{
LED_ON() ;
}
else //不然平息LED
{
LED_OFF() ;
}
}
void LedStateChange(void)
{
if(g_bSystemTime1Ms) //体系1MS时标到
{
g_bSystemTime1Ms = 0 ;
g_u16LedTimeCount++ ; //LED计数器加一
if(g_u16LedTimeCount >= 500) //计数到达500,即500MS到了,改动LED的状况。
{
g_u16LedTimeCount = 0 ;
g_u8LedState = ! g_u8LedState ;
}
}
}
这个模块对外的托言只要两个函数,因此在相应的Led.h 中需求作相应的声明。
Led.h 内容:
#ifndef _LED_H_
#define _LED_H_
extern void LedProcess(void) ;
extern void LedStateChange(void) ;
#endif
这两个模块完结后,咱们将其C文件添加到工程中。然后开端编写主函数里的代码。
如下所示:
#include
#include “MacroAndConst.h”
#include “Timer.h”
#include “Led.h”
sbit LED_SEG = P1^4; //数码管段选
sbit LED_DIG = P1^5; //数码管位选
sbit LED_CS11 = P1^6; //led操控位
void main(void)
{
LED_CS11 = 1 ; //74HC595输出答应
LED_SEG = 0 ; //数码管段选和位选制止(由于它们和LED共用P0口)
LED_DIG = 0 ;
Timer0Init() ;
EA = 1 ;
while(1)
{
LedProcess() ;
LedStateChange() ;
}
}
整个工程截图如下