嵌入式C语言模块编写
C 语言源文件 *.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?
很多朋友似乎了习惯程序中利用如下语句来对数据类型进行定义
#define uint unsigned int
#define uchar unsigned char
然后在定义变量的时候 直接这样使用 :uint g_nTimeCounter = 0 ;
不可否认,这样确实很方便,而且对于移植起来也有一定的方便性。但是考虑下面这种情况你还会 这么认为吗?
#define pINT unsigned int * // 定义unsigned int 指针类型
A pINT2 a,b; 的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。
typedef (int*) pINT;
B pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。
typedef为了给变量起一个别名:
typedef unsigned int uint16 ; //给指向无符号整形变量起一个别名 uint16
typedef unsigned int * puint16 ; //给指向无符号整形变量指针起一个别名 puint16
51单片机的C 语言编程的时候,整形变量的范围是16位,而在基于32的微处理下的整形变量是32位。倘若我们在8位单片机下编写的一些代码想要移植到32位的处理器上,那么很可能我们就需要在源文件中到处修改变量的类型定义。这就是一个庞大的任务了,因此,在一开始,我们就应该养成良好的习惯,用变量的别名进行定义。
如在8位单片机的平台下,有如下一个变量定义
uint16 g_nTimeCounter = 0 ;
如果移植32单片机的平台下,想要其的范围依旧为16位。可以直接修改 uint16 的定义,即
typedef unsigned short int uint16 ;
这样就可以了,而不需要到源文件处处寻找并修改。
1 将常用的数据类型全部采用此种方法定义,形成一个头文件,便于我们以后编程直接调用。 2 文件名 MacroAndConst.h 3 其内容如下: 4 #ifndef _MACRO_AND_CONST_H_ 5 #define _MACRO_AND_CONST_H_ 6 typedef unsigned int uint16; 7 typedef unsigned int UINT; 8 typedef unsigned int uint; 9 typedef unsigned int UINT16; 10 typedef unsigned int WORD; 11 typedef unsigned int word; 12 typedef int int16; 13 typedef int INT16; 14 typedef unsigned long uint32; 15 typedef unsigned long UINT32; 16 typedef unsigned long DWORD; 17 typedef unsigned long dword; 18 typedef long int32; 19 typedef long INT32; 20 typedef signed char int8; 21 typedef signed char INT8; 22 typedef unsigned char byte; 23 typedef unsigned char BYTE; 24 typedef unsigned char uchar; 25 typedef unsigned char UINT8; 26 typedef unsigned char uint8; 27 typedef unsigned char BOOL; 28 #endif
我们编写的LED 闪烁函数进行模块划分并重新组织进行编译:主要完成的功能是P0口所驱动的LED 以1Hz 的频率闪烁。其中用到了定时器,以及LED 驱动模
块。因而我们可以简单的将整个工程分成三个模块,定时器模块,LED 模块,以及主函数对应的文件关系如下:
main.c ; Timer.c --?Timer.h ;Led.c --?Led.h
首先编写Ti mer.c 这个文件主要内容就是定时器初始化,以及定时器中断服务函数。其内容如下:
1 #include <reg52.h> 2 bit g_bSystemTime1Ms = 0 ; // 1MS 系统时标 3 void Timer0Init(void) 4 { 5 TMOD &= 0xf0 ; 6 TMOD |= 0x01 ; // 定时器0工作方式1 7 TH0 = 0xfc ; //定时器初始值 8 TL0 = 0x66 ; 9 TR0 = 1 ; 10 ET0 = 1 ; 11 } 12 void Time0Isr(void) interrupt 1 13 { 14 TH0 = 0xfc ; //定时器重新赋初值 15 TL0 = 0x66 ; 16 g_bSystemTime1Ms = 1 ; //1MS 时标标志位置位 17 }
由于在Led.c 文件中需要调用我们的 g_bSystemTime1Ms变量。同时主函数需要调用 Timer0Init()初始化函数,所以应该对这个变量和函数在头文件里作外部声明。以方便其它函数调用。
1 Ti mer.h 内容如下。 2 #ifndef _TIMER_H_ 3 #define _TIMER_H_ 4 extern void Timer0Init(void) ; 5 extern bit g_bSystemTime1Ms ; 6 #endif
完成了定时器模块后,我们开始编写LED 驱动模块。
1 Led.c 内容如下: 2 #include <reg52.h> 3 #include "MacroAndConst.h" 4 #include "Led.h" 5 #include "Timer.h" 6 static uint16 g_u16LedTimeCount = 0 ; //LED 计数器 7 static uint8 g_u8LedState = 0 ; //LED 状态标志, 0表示亮,1表示熄灭 8 #define LED P0 // 定义 LED 接口 9 #define LED_ON() LED = 0x00 ; // 所有LED 亮 10 #define LED_OFF() LED = 0xff ; //所有LED 熄灭 11 void LedProcess(void) 12 { 13 if(0 == g_u8LedState) //如果LED 的状态为亮,则点亮LED 14 { 15 LED_ON() ; 16 } 17 else //否则熄灭 LED 18 { 19 LED_OFF() ; 20 } 21 } 22 23 void LedStateChange(void) 24 { 25 if(g_bSystemTime1Ms) // 系统1MS时标到 26 { 27 g_bSystemTime1Ms = 0 ; 28 g_u16LedTimeCount++ ; //LED 计数器加一 29 if(g_u16LedTimeCount >= 500) // 计数达到500, 即500MS到了, 改变 LED 的状态。 30 { 31 g_u16LedTimeCount = 0 ; 32 g_u8LedState = ! g_u8LedState ; 33 } 34 } 35 }
这个模块对外的借口只有两个函数,因此在相应的Led.h 中需要作相应的声明。
1 Led.h 内容: 2 #ifndef _LED_H_ 3 #define _LED_H_ 4 extern void LedProcess(void) ; 5 extern void LedStateChange(void) ; 6 #endif
这两个模块完成后,我们将其C 文件添加到工程中。然后开始编写主函数里的代码:
1 #include <reg52.h> 2 #include "MacroAndConst.h" 3 #include "Timer.h" 4 #include "Led.h" 5 sbit LED_SEG = P1^4; //数码管段选 6 sbit LED_DIG = P1^5; //数码管位选 7 sbit LED_CS11 = P1^6; //led 控制位 8 void main(void) 9 { 10 LED_CS11 = 1 ; //74HC595输出允许 11 LED_SEG = 0 ; //数码管段选和位选禁止(因为它们和LED 共用P0口) 12 LED_DIG = 0 ; 13 Timer0Init() ; 14 EA = 1 ; 15 while(1) 16 { 17 LedProcess() ; 18 LedStateChange() ; 19 } 20 }