Keil多任务编程Word文档下载推荐.docx
《Keil多任务编程Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《Keil多任务编程Word文档下载推荐.docx(34页珍藏版)》请在冰豆网上搜索。
谈及到模块化编程,必然会涉及到多文件编译,也就是工程编译。
在这样的一个系统中,往往会有多个C文件,而且每个C文件的作用不尽相同。
在我们的C文件中,由于需要对外提供接口,因此必须有一些函数或者是变量提供给外部其它文件进行调用。
假设我们有一个LCD.C文件,其提供最基本的LCD的驱动函数
LcdPutChar(char
cNewValue)
;
//在当前位置输出一个字符
而在我们的另外一个文件中需要调用此函数,那么我们该如何做呢?
头文件的作用正是在此。
可以称其为一份接口描述文件。
其文件内部不应该包含任何实质性的函数代码。
我们可以把这个头文件理解成为一份说明书,说明的内容就是我们的模块对外提供的接口函数或者是接口变量。
同时该文件也包含了一些很重要的宏定义以及一些结构体的信息,离开了这些信息,很可能就无法正常使用接口函数或者是接口变量。
但是总的原则是:
不该让外界知道的信息就不应该出现在头文件里,而外界调用模块内接口函数或者是接口变量所必须的信息就一定要出现在头文件里,否则,外界就无法正确的调用我们提供的接口功能。
因而为了让外部函数或者文件调用我们提供的接口功能,就必须包含我们提供的这个接口描述文件----即头文件。
同时,我们自身模块也需要包含这份模块头文件(因为其包含了模块源文件中所需要的宏定义或者是结构体),好比我们平常所用的文件都是一式三份一样,模块本身也需要包含这个头文件。
下面我们来定义这个头文件,一般来说,头文件的名字应该与源文件的名字保持一致,这样我们便可以清晰的知道哪个头文件是哪个源文件的描述。
于是便得到了LCD.C的头文件LCD.h
其内容如下。
#ifndef
_LCD_H_
#define
extern
#endif
这与我们在源文件中定义函数时有点类似。
不同的是,在其前面添加了extern
修饰符表明其是一个外部函数,可以被外部其它模块进行调用。
这个几条条件编译和宏定义是为了防止重复包含。
假如有两个不同源文件需要调用LcdPutChar(char
cNewValue)这个函数,他们分别都通过#include
“Lcd.h”把这个头文件包含了进去。
在第一个源文件进行编译时候,由于没有定义过
_LCD_H_
因此
条件成立,于是定义_LCD_H_
并将下面的声明包含进去。
在第二个文件编译时候,由于第一个文件包含时候,已经将_LCD_H_定义过了。
因此#ifndef
不成立,整个头文件内容就没有被包含。
假设没有这样的条件编译语句,那么两个文件都包含了extern
就会引起重复包含的错误。
不得不说的typedef
很多朋友似乎了习惯程序中利用如下语句来对数据类型进行定义
uint
unsigned
int
uchar
char
然后在定义变量的时候
直接这样使用
g_nTimeCounter
=
0
不可否认,这样确实很方便,而且对于移植起来也有一定的方便性。
但是考虑下面这种情况你还会
这么认为吗?
PINT
*
//定义unsigned
指针类型
g_npTimeCounter,
g_npTimeState
那么你到底是定义了两个unsigned
型的指针变量,还是一个指针变量,一个整形变量呢?
而你的初衷又是什么呢,想定义两个unsigned
型的指针变量吗?
如果是这样,那么估计过不久就会到处抓狂找错误了。
庆幸的是C语言已经为我们考虑到了这一点。
typedef
正是为此而生。
为了给变量起一个别名我们可以用如下的语句
uint16
//给指向无符号整形变量起一个别名
uint16
puint16
//给指向无符号整形变量指针起一个别名
puint16
在我们定义变量时候便可以这样定义了:
//定义一个无符号的整形变量
g_npTimeCounter
//定义一个无符号的整形变量的指针
在我们使用51单片机的C语言编程的时候,整形变量的范围是16位,而在基于32的微处理下的整形变量是32位。
倘若我们在8位单片机下编写的一些代码想要移植到32位的处理器上,那么很可能我们就需要在源文件中到处修改变量的类型定义。
这是一件庞大的工作,为了考虑程序的可移植性,在一开始,我们就应该养成良好的习惯,用变量的别名进行定义。
如在8位单片机的平台下,有如下一个变量定义
如果移植32单片机的平台下,想要其的范围依旧为16位。
可以直接修改uint16
的定义,即
short
这样就可以了,而不需要到源文件处处寻找并修改。
将常用的数据类型全部采用此种方法定义,形成一个头文件,便于我们以后编程直接调用。
文件名
MacroAndConst.h
其内容如下:
_MACRO_AND_CONST_H_
uint16;
UINT;
uint;
UINT16;
WORD;
word;
int16;
INT16;
long
uint32;
UINT32;
DWORD;
dword;
int32;
INT32;
signed
char
int8;
INT8;
byte;
BYTE;
uchar;
UINT8;
uint8;
BOOL;
至此,似乎我们对于源文件和头文件的分工以及模块化编程有那么一点概念了。
那么让我们趁热打铁,将上一章的我们编写的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
<
reg52.h>
bit
g_bSystemTime1Ms
//
1MS系统时标
void
Timer0Init(void)
{
TMOD
&
0xf0
|=
0x01
//定时器0工作方式1
TH0
0xfc
//定时器初始值
TL0
0x66
TR0
1
ET0
}
Time0Isr(void)
interrupt
1
//定时器重新赋初值
//1MS时标标志位置位
由于在Led.c文件中需要调用我们的g_bSystemTime1Ms变量。
同时主函数需要调用Timer0Init()初始化函数,所以应该对这个变量和函数在头文件里作外部声明。
以方便其它函数调用。
Timer.h
内容如下。
_TIMER_H_
Timer0Init(void)
完成了定时器模块后,我们开始编写LED驱动模块。
Led.c
内容如下:
"
MacroAndConst.h"
Led.h"
Timer.h"
static
g_u16LedTimeCount
//LED计数器
uint8
g_u8LedState
//LED状态标志,
0表示亮,1表示熄灭
LED
P0
//定义LED接口
LED_ON()
0x00
//所有LED亮
LED_OFF()
0xff
//所有LED熄灭
LedProcess(void)
if(0
==
g_u8LedState)
//如果LED的状态为亮,则点亮LED
else
//否则熄灭LED
LedStateChange(void)
if(g_bSystemTime1Ms)
//系统1MS时标到
g_u16LedTimeCount++
//LED计数器加一
if(g_u16LedTimeCount
>
500)
//计数达到500,即500MS到了,改变LED的状态。
!
这个模块对外的借口只有两个函数,因此在相应的Led.h
中需要作相应的声明。
Led.h
内容:
_LED_H_
LedProcess(void)
LedStateChange(void)
这两个模块完成后,我们将其C文件添加到工程中。
然后开始编写主函数里的代码。
如下所示:
sbit
LED_SEG
P1^4;
//数码管段选
LED_DIG
P1^5;
//数码管位选
LED_CS11
P1^6;
//led控制位
main(void)
//74HC595输出允许
//数码管段选和位选禁止(因为它们和LED共用P0口)
Timer0Init()
EA
while
(1)
LedProcess()
LedStateChange()
整个工程截图如下:
至此,第三章到此结束。
一起来总结一下我们需要注意的地方吧
[color=#FF0000]1.
C语言源文件(*.c)的作用是什么
2.
C语言头文件(*.h)的作用是什么
3.
的作用
4.
工程模板如何组织
5.
如何创建一个多模块(多文件)的工程
“从单片机初学者迈向单片机工程师”之KEY主题讨论
按键程序编写的基础
从这一章开始,我们步入按键程序设计的殿堂。
在基于单片机为核心构成的应用系统中,用户输入是必不可少的一部分。
输入可以分很多种情况,譬如有的系统支持PS2键盘的接口,有的系统输入是基于编码器,有的系统输入是基于串口或者USB或者其它输入通道等等。
在各种输入途径中,更常见的是,基于单个按键或者由单个键盘按照一定排列构成的矩阵键盘(行列键盘)。
我们这一篇章主要讨论的对象就是基于单个按键的程序设计,以及矩阵键盘的程序编写。
◎按键检测的原理
常见的独立按键的外观如下,相信大家并不陌生,各种常见的开发板学习板上随处可以看到他们的身影。
总共有四个引脚,一般情况下,处于同一边的两个引脚内部是连接在一起的,如何分辨两个引脚是否处在同一边呢?
可以将按键翻转过来,处于同一边的两个引脚,有一条突起的线将他们连接一起,以标示它们俩是相连的。
如果无法观察得到,用数字万用表的二极管挡位检测一下即可。
搞清楚这点非常重要,对于我们画PCB的时候的封装很有益。
它们和我们的单片机系统的I/O口连接一般如下:
对于单片机I/O内部有上拉电阻的微控制器而言,还可以省掉外部的那个上拉电阻。
简单分析一下按键检测的原理。
当按键没有按下的时候,单片机I/O通过上拉电阻R接到VCC,我们在程序中读取该I/O的电平的时候,其值为1(高电平);
当按键S按下的时候,该I/O被短接到GND,在程序中读取该I/O的电平的时候,其值为0(低电平)
。
这样,按键的按下与否,就和与该按键相连的I/O的电平的变化相对应起来。
结论:
我们在程序中通过检测到该I/O口电平的变化与否,即可以知道按键是否被按下,从而做出相应的响应。
一切看起来很美好,是这样的吗?
◎现实并非理想
在我们通过上面的按键检测原理得出上述的结论的时候,其实忽略了一个重要的问题,那就是现实中按键按下时候的电平变化状态。
我们的结论是基于理想的情况得出来的,就如同下面这幅按键按下时候对应电平变化的波形图一样:
而实际中,由于按键的弹片接触的时候,并不是一接触就紧紧的闭合,它还存在一定的抖动,尽管这个时间非常的短暂,但是对于我们执行时间以us为计算单位的微控制器来说,
它太漫长了。
因而,实际的波形图应该如下面这幅示意图一样。
这样便存在这样一个问题。
假设我们的系统有这样功能需求:
在检测到按键按下的时候,将某个I/O的状态取反。
由于这种抖动的存在,使得我们的微控制器误以为是多次按键的按下,从而将某个I/O的状态不断取反,这并不是我们想要的效果,假如该I/O控制着系统中某个重要的执行的部件,那结果更不是我们所期待的。
于是乎有人便提出了软件消除抖动的思想,道理很简单:
抖动的时间长度是一定的,只要我们避开这段抖动时期,检测稳定的时候的电平不久可以了吗?
听起来确实不错,而且实际应用起来效果也还可以。
于是,各种各样的书籍中,在提到按键检测的时候,总也不忘说道软件消抖。
就像下面的伪代码所描述的一样。
(假设按键按下时候,低电平有效)
If(0
io_KeyEnter)
//如果有键按下了
Delayms(20)
//先延时20ms避开抖动时期
//然后再检测,如果还是检测到有键按下
return
KeyValue