单片机 C 语言模块化编程

发布者:tetsika最新更新时间:2016-12-26 来源: eefocus关键字:单片机  语言  模块化编程 手机看文章 扫描二维码
随时随地手机看文章

  好的开始是成功的一半

  通过上一章的学习,我想你已经掌握了如何在程序中释放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 ;    //给指向无符号整形变量起一个别名 uint16typedef  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() ;
    }
}

复制代码

 

整个工程截图如下

 

 

  至此,第三章到此结束。

一起来总结一下我们需要注意的地方吧
1. C 语言源文件(*.c)的作用是什么
2. C 语言头文件(*.h)的作用是什么
3. typedef 的作用
4. 工程模板如何组织
5. 如何创建一个多模块(多文件)的工程


关键字:单片机  语言  模块化编程 引用地址:单片机 C 语言模块化编程

上一篇:Keil 程序调试窗口
下一篇:在KEIL中的模块化程序写法

推荐阅读最新更新时间:2024-03-16 15:27

LCD1602的C8051驱动
//--------------------lcd1602.c------------------------------ //********************************************** // 功能:C8051F(330) LCD1602(4线驱动) //********************************************** #include c8051f330.h #include delay.h // //1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // | | | | | |
[单片机]
单片机MSP430 - MSP430+CC1101常用自带函数
一、中断操作 __get_interrupt_state() 功能:返回当前的中断状态。通过使用此函数可以获得当前的中断状态并保存。 __disable_interrupt() 功能:关闭所有中断 __set_interrupt_state(x) 功能:恢复中断状态,可恢复 _get_interrupt_state() 函数返回的中断状态 二、初始化操作 __bis_SR_register() 功能:将 CPU 中 SR 寄存器对应位置1,例如__bis_SR_register(LPM3_bits + GIE),将 LPM3_bits 和 GIE 位置1 三、其他操作 __
[单片机]
单片机的两个外围电路:复位电路和时钟电路
一般的单片机都要具备两个外围电路:时钟电路和复位电路 时钟电路 主要由一个晶振和两个电容组成。晶振的大小决定这单片机的时钟信号 (按我的理解就是:单片机编程时,同一个延时函数,晶振决定着它们的延时时间长短) 复位电路 分为上电复位和按键复位 主要由一个电阻、一个电容,如果按键复位的话,就再加个微动开关就成。都是为了把电路初始化到一个确定的状态,一般来说,单片机复位电路作用是把一个例如状态机初始化到空状态,而在单片机内部,复位的时候单片机是把一些寄存器以及存储设备装入厂商预设的一个值。 至于怎么连接,我就直接上图(仿真软件) 连接着单片机的XTAL1和XTAL2的是时钟电路,单片机实物两个引脚是在左侧第18和19两个
[单片机]
<font color='red'>单片机</font>的两个外围电路:复位电路和时钟电路
如何使用8051单片机在7段显示器上显示数字?
在使用 单片机开发 项目的时候,经常会使用到7段显示器。7段显示器可以显示字母和数字,它内部是其实是发光 LED 灯,通过控制不同的 接口 点亮内部LED灯,从而显示出数字。目前,它在许多 电子产品 中用于显示 信息 ,比如:充电宝、直发器等。 7段显示器由8个LED构成,这些LED以顺序方式连接,以方便在打开某些LED组合时显示0到9的数字。点亮时,它一次只显示一位数字。 使用80 51单片机 在7段显示器上显示数字的原理图: 在7段显示器上显示从“0到F”的数字的程序是: #include reg51.h sbit a= P3^0; sbit x= P3^1; sbit y= P3^2; sbit z= P3
[单片机]
如何使用8051<font color='red'>单片机</font>在7段显示器上显示数字?
基于SSU7301单片机串行口红外通信的设计
0 引言 多费率电能表是我国目前节约用电和计划用电政策下不可缺少的电能计量产品,多费率电能表的通信接口一般兼有红外接口和RS485接口。红外通信具有直观、操作简便、可靠性高等优点,是电能表中使用最为普遍的一种通信方式,是电能表和掌机之间实现抄表、编程、校时、数据管理等功能的有效手段。采用新茂单片机SSU7301(51系列)、日本光电子公司的红外发射管SE303和红外接收管PIC12043,以及单片机串行口、2个定时器/计数器可以有效地实现红外通信功能。 1 红外通信原理 红外通信是利用波长为900nm~1000nm的红外波作为信息的载体,发射装置把二进制信号经过高频调制后发送出去,接收装置把接收的红外高频信号进行解调为原
[单片机]
基于SSU7301<font color='red'>单片机</font>串行口红外通信的设计
51单片机防酒后驾驶 MQ-3酒精检测系统的仿真
基于单片机的防酒后驾驶控制系统的仿真图: 课题任务的内容和要求 (1) 学习气体测量传感器的原理和使用,并完成数据采集、调理电路的设计; (2) 学习单片机系统的设计及编程,完成系统整体设计; (3) 通过不同颜色显示灯及触发措施,对应三种酒精含量范围(醉酒状态、少量饮酒状态、正常状态),; (4) 测量结果动态显示。 酒精传感器采集酒精浓度信息,将其转化为变化的电压信号。电压信号经过处理后通过A/D转换电路实现对信号的模数转换,然后送至单片机系统进行逻辑判断处理。如果驾驶员未饮酒,LED显示绿灯,汽车正常启动;如果驾驶员处于少量饮酒状态,LED显示黄灯,且会发出声光报警;如果驾驶员处于醉酒状态时,LED显示红灯,且会控
[单片机]
51<font color='red'>单片机</font>防酒后驾驶 MQ-3酒精检测系统的仿真
51单片机串口T1加看门狗程序
数码管部分的电路图 程序: #include reg52.h #define UCHAR unsigned char #define UINT unsigned int sfr WDT_CONTR = 0xe1; UCHAR table = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; UCHAR timer; UCHAR conter; void initserial(void) { timer='0'; conter=0; TMOD=0X20; SCON=0X50; TL
[单片机]
51<font color='red'>单片机</font>串口T1加看门狗程序
RAMTRON扩展VERSA 8051系列推出2KB铁电存储器MCU
非易失性数据存储/处理系统适用于存储系统状态、数据记录及各种应用中的非易失性变量 全球领先的非易失性铁电随机存取存储器 (F-RAM) 和集成半导体产品开发商及供应商Ramtron International Corporation宣布推出带2KB非易失性F-RAM 以8051为基础的微控制器 (MCU) -- VRS51L3072。Ramtron将F-RAM添加到其快速灵活的Versa 8501产品中,以进行快速可靠的非易失性数据存储与处理系统,而这是存储系统状态、数据记录及在多种应用的非易失性变量的理想选择,包括从传感器与计量仪表到工业控制、仪表与医疗设备等应用。 Ramtron战略市场拓展经理David Lee称:“我们
[新品]
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

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

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