Keil多任务编程.docx

上传人:b****3 文档编号:3664441 上传时间:2022-11-24 格式:DOCX 页数:34 大小:860.95KB
下载 相关 举报
Keil多任务编程.docx_第1页
第1页 / 共34页
Keil多任务编程.docx_第2页
第2页 / 共34页
Keil多任务编程.docx_第3页
第3页 / 共34页
Keil多任务编程.docx_第4页
第4页 / 共34页
Keil多任务编程.docx_第5页
第5页 / 共34页
点击查看更多>>
下载资源
资源描述

Keil多任务编程.docx

《Keil多任务编程.docx》由会员分享,可在线阅读,更多相关《Keil多任务编程.docx(34页珍藏版)》请在冰豆网上搜索。

Keil多任务编程.docx

Keil多任务编程

在谈论今天的主题之前,先说下我以前的一些经历。

在刚开始接触到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.hTimer.c  --

Led.hLed.c      --

在开始重新编写我们的程序之前,先给大家讲一下如何在KEIL中建立工程模板吧,这个模板是我一直沿用至今。

希望能够给大家一点启发。

下面的内容就主要以图片为主了。

同时辅以少量文字说明。

我们以芯片AT89S52为例。

 (原文件名:

1.jpg) 

引用图片

 (原文件名:

2.jpg) 

引用图片

 (原文件名:

3.jpg) 

引用图片

 (原文件名:

4.jpg) 

引用图片

 (原文件名:

5.jpg) 

引用图片

 (原文件名:

6.jpg) 

引用图片

 (原文件名:

7.jpg) 

引用图片

 (原文件名:

8.jpg) 

引用图片

 (原文件名:

9.jpg) 

引用图片

 (原文件名:

10.jpg) 

引用图片

 (原文件名:

11.jpg) 

引用图片

 (原文件名:

12.jpg) 

引用图片

 (原文件名:

13.jpg) 

引用图片

 (原文件名:

14.jpg) 

引用图片

 (原文件名:

15.jpg) 

引用图片

 (原文件名:

16.jpg) 

引用图片

 (原文件名:

17.jpg) 

引用图片

 (原文件名:

18.jpg) 

引用图片

 (原文件名:

19.jpg) 

引用图片

 (原文件名:

20.jpg) 

引用图片

 (原文件名:

21.jpg) 

引用图片

 (原文件名:

22.jpg) 

引用图片

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() ;

      }

}

整个工程截图如下:

 

至此,第三章到此结束。

一起来总结一下我们需要注意的地方吧

[color=#FF0000]1.    C语言源文件(*.c)的作用是什么

2.    C语言头文件(*.h)的作用是什么

3.    typedef 的作用

4.    工程模板如何组织

5.    如何创建一个多模块(多文件)的工程

“从单片机初学者迈向单片机工程师”之KEY主题讨论

                                               按键程序编写的基础

  从这一章开始,我们步入按键程序设计的殿堂。

在基于单片机为核心构成的应用系统中,用户输入是必不可少的一部分。

输入可以分很多种情况,譬如有的系统支持PS2键盘的接口,有的系统输入是基于编码器,有的系统输入是基于串口或者USB或者其它输入通道等等。

在各种输入途径中,更常见的是,基于单个按键或者由单个键盘按照一定排列构成的矩阵键盘(行列键盘)。

我们这一篇章主要讨论的对象就是基于单个按键的程序设计,以及矩阵键盘的程序编写。

◎按键检测的原理

常见的独立按键的外观如下,相信大家并不陌生,各种常见的开发板学习板上随处可以看到他们的身影。

 (原文件名:

1.jpg) 

引用图片

  

    总共有四个引脚,一般情况下,处于同一边的两个引脚内部是连接在一起的,如何分辨两个引脚是否处在同一边呢?

可以将按键翻转过来,处于同一边的两个引脚,有一条突起的线将他们连接一起,以标示它们俩是相连的。

如果无法观察得到,用数字万用表的二极管挡位检测一下即可。

搞清楚这点非常重要,对于我们画PCB的时候的封装很有益。

它们和我们的单片机系统的I/O口连接一般如下:

 (原文件名:

2.jpg) 

引用图片

 

    对于单片机I/O内部有上拉电阻的微控制器而言,还可以省掉外部的那个上拉电阻。

简单分析一下按键检测的原理。

当按键没有按下的时候,单片机I/O通过上拉电阻R接到VCC,我们在程序中读取该I/O的电平的时候,其值为1(高电平); 当按键S按下的时候,该I/O被短接到GND,在程序中读取该I/O的电平的时候,其值为0(低电平) 。

这样,按键的按下与否,就和与该按键相连的I/O的电平的变化相对应起来。

结论:

我们在程序中通过检测到该I/O口电平的变化与否,即可以知道按键是否被按下,从而做出相应的响应。

一切看起来很美好,是这样的吗?

◎现实并非理想

在我们通过上面的按键检测原理得出上述的结论的时候,其实忽略了一个重要的问题,那就是现实中按键按下时候的电平变化状态。

我们的结论是基于理想的情况得出来的,就如同下面这幅按键按下时候对应电平变化的波形图一样:

 (原文件名:

3.jpg) 

引用图片

 

    而实际中,由于按键的弹片接触的时候,并不是一接触就紧紧的闭合,它还存在一定的抖动,尽管这个时间非常的短暂,但是对于我们执行时间以us为计算单位的微控制器来说,

它太漫长了。

因而,实际的波形图应该如下面这幅示意图一样。

 (原文件名:

4.jpg) 

引用图片

 

这样便存在这样一个问题。

假设我们的系统有这样功能需求:

在检测到按键按下的时候,将某个I/O的状态取反。

由于这种抖动的存在,使得我们的微控制器误以为是多次按键的按下,从而将某个I/O的状态不断取反,这并不是我们想要的效果,假如该I/O控制着系统中某个重要的执行的部件,那结果更不是我们所期待的。

于是乎有人便提出了软件消除抖动的思想,道理很简单:

抖动的时间长度是一定的,只要我们避开这段抖动时期,检测稳定的时候的电平不久可以了吗?

听起来确实不错,而且实际应用起来效果也还可以。

于是,各种各样的书籍中,在提到按键检测的时候,总也不忘说道软件消抖。

就像下面的伪代码所描述的一样。

(假设按键按下时候,低电平有效)

If(0 == io_KeyEnter)            //如果有键按下了

{

    Delayms(20) ;            //先延时20ms避开抖动时期

    If(0 == io_KeyEnter)        //然后再检测,如果还是检测到有键按下

    {

        return KeyValue ; 

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 能源化工

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1