初学单片机必看Word格式文档下载.docx
- 文档编号:22961865
- 上传时间:2023-02-06
- 格式:DOCX
- 页数:47
- 大小:903.38KB
初学单片机必看Word格式文档下载.docx
《初学单片机必看Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《初学单片机必看Word格式文档下载.docx(47页珍藏版)》请在冰豆网上搜索。
试想,整个程序除了控制LED
;
OFF;
这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。
而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了的。
因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。
宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让它停留在某个地方空转消耗CPU时间。
从上面我们可以总结出
(1)
无论什么时候我们都要以实际应用的角度去考虑程序的编写。
(2)
无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。
下面让我们从另外一个角度来考虑如何点亮一颗LED。
先看看我们的硬件结构是什么样子的。
我手上的单片机板子是电子工程师之家的开发的学习板。
就以它的实际硬件连接图来分析吧。
如下图所示
(原文件名:
led.jpg)
引用图片
一般的LED的正常发光电流为10~20MA而低电流LED的工作电流在2mA以下(亮度与普通发光管相同)。
在上图中我们可知,当Q1~Q8引脚上面的电平为低电平时,LED发光。
通过LED的电流约为(VCC
-
Vd)/
RA2
。
其中Vd为LED导通后的压降,约为1.7V左右。
这个导通压降根据LED颜色的不同,以及工作电流的大小的不同,会有一定的差别。
下面一些参数是网上有人测出来的,供大家参考。
红色的压降为1.82-1.88V,电流5-8mA,
绿色的压降为1.75-1.82V,电流3-5mA,
橙色的压降为1.7-1.8V,电流3-5mA
兰色的压降为3.1-3.3V,电流8-10mA,
白色的压降为3-3.2V,电流10-15mA,
(供电电压5V,LED直径为5mm)
74HC573真值表如下:
74hc573.jpg)
通过这个真值表我们可以看出。
当OutputEnable引脚接低电平的时候,并且LatchEnable引脚为高电平的时候,Q端电平与D端电平相同。
结合我们的LED硬件连接图可以知道LED_CS端为高电平时候,P0口电平的变化即Q端的电平的变化,进而引起LED的亮灭变化。
由于单片机的驱动能力有限,在此,74HC573的主要作用就是起一个输出驱动的作用。
需要注意的是,通过74HC573的最大电流是有限制的,否则可能会烧坏74HC573这个芯片。
上面这个图是从74HC573的DATASHEET中截取出来的,从上可以看出,每个引脚允许通过的最大电流为35mA
整个芯片允许通过的最大电流为75mA。
在我们设计相应的驱动电路时候,这些参数是相当重要的,而且是最容易被初学者所忽略的地方。
同时在设计的时候,要留出一定量的余量出来,不能说单个引脚允许通过的电流为35mA,你就设计为35mA,这个时候你应该把设计的上限值定在20mA左右才能保证能够稳定的工作。
(设计相应驱动电路时候,应该仔细阅读芯片的数据手册,了解每个引脚的驱动能力,以及整个芯片的驱动能力)
了解了相应的硬件后,我们再来编写驱动程序。
首先定义LED的接口
#define
P0
然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。
LED_ON()
0x00
//所有LED亮
LED_OFF()
0xff
//所有LED熄灭
下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。
很简单,我们为系统产生一个1MS的时标。
假定LED需要亮500MS,熄灭500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。
unsigned
int
g_u16LedTimeCount
0
//LED计数器
char
g_u8LedState
//LED状态标志,
0表示亮,1表示熄灭
LedProcess(void)
if(0
==
g_u8LedState)
//如果LED的状态为亮,则点亮LED
else
//否则熄灭LED
LedStateChange(void)
if(g_bSystemTime1Ms)
//系统1MS时标到
g_bSystemTime1Ms
g_u16LedTimeCount++
//LED计数器加一
if(g_u16LedTimeCount
>
500)
//计数达到500,即500MS到了,改变LED的状态。
!
上面有一个变量没有提到,就是g_bSystemTime1Ms
这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0)
我们的主函数就可以写成如下形式(示意代码)
while
(1)
LedProcess()
LedStateChange()
}
因为LED的亮或者灭依赖于LED状态变量(g_u8LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值(g_u16LedTimeCount
,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。
让我们来从头到尾分析一遍整个程序的流程。
程序首先执行LedProcess()
函数
因为g_u8LedState
的初始值为0
(见定义,对于全局变量,在定义的时候最好给其一个确定的值)所以LED被点亮,然后退出LedStateChange()函数,执行下一个函数LedStateChange()
在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的到来,同时对LED计数器加一,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计数,同时把LED状态变量取反,然后退出函数。
由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加一,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。
这些函数执行所花的时间都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。
对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。
系统的实时性得到了一定的保证,从宏观上看来,就是多个任务并发执行。
好了,这一章就到此为止,让我们总结一下,究竟有哪些需要注意的吧。
(3)
设计相应驱动电路时候,应该仔细阅读芯片的数据手册,了解每个引脚的驱动能力,
以及整个芯片的驱动能力
(4)
最重要的是,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。
附完整程序代码(基于电子工程师之家的单片机开发板)
#include<
reg52.h>
sbit
LED_SEG
P1^4;
//数码管段选
LED_DIG
P1^5;
//数码管位选
LED_CS11
P1^6;
//led控制位
ir=P1^7;
P0
//定义LED接口
bit
//
1MS系统时标
Timer0Init(void)
TMOD
&
0xf0
|=
0x01
//定时器0工作方式1
TH0
0xfc
//定时器初始值
TL0
0x66
TR0
1
ET0
Timer0Init()
EA
//74HC595输出允许
//数码管段选和位选禁止(因为它们和LED共用P0口)
Time0Isr(void)
interrupt
1
//定时器重新赋初值
//1MS时标标志位置位
“从单片机初学者迈向单片机工程师”
第三章----模块化编程初识
好的开始是成功的一半
通过上一章的学习,我想你已经掌握了如何在程序中释放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_
extern
#endif
这与我们在源文件中定义函数时有点类似。
不同的是,在其前面添加了extern
修饰符表明其是一个外部函数,可以被外部其它模块进行调用。
这个几条条件编译和宏定义是为了防止重复包含。
假如有两个不同源文件需要调用LcdPutChar(char
cNewValue)这个函数,他们分别都通过#include
“Lcd.h”把这个头文件包含了进去。
在第一个源文件进行编译时候,由于没有定义过
_LCD_H_
因此
条件成立,于是定义_LCD_H_
并将下面的声明包含进去。
在第二个文件编译时候,由于第一个文件包含时候,已经将_LCD_H_定义过了。
因此#ifndef
不成立,整个头文件内容就没有被包含。
假设没有这样的条件编译语句,那么两个文件都包含了extern
就会引起重复包含的错误。
不得不说的typedef
很多朋友似乎了习惯程序中利用如下语句来对数据类型进行定义
uint
uchar
char
然后在定义变量的时候
直接这样使用
g_nTimeCounter
不可否认,这样确实很方便,而且对于移植起来也有一定的方便性。
但是考虑下面这种情况你还会
这么认为吗?
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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 初学 单片机