单片机学习笔记.docx
《单片机学习笔记.docx》由会员分享,可在线阅读,更多相关《单片机学习笔记.docx(45页珍藏版)》请在冰豆网上搜索。
单片机学习笔记
关于LCD1602显示的应用:
LCD1602是51单片机中很重要的模块之一,为了方便新手学习1602,笔者把自己学习1602的一些心得体会拿出来和菜鸟分享,因笔者水平有限,错误在所难免,望大家指正。
本文程序基于慧净HJ-1G51开发板。
很多程序为本人自己编写,可以直接拿去用。
一、关于LCD1602:
在编写LCD1602程序前,我们必须了解其手册上一些非常重要的信息,如果这些信息不能理解透彻,编程可能会遇到或多或少的问题,在此先大致归纳几点。
1.管脚:
1602共16个管脚,但是编程用到的主要管脚不过三个,分别为:
RS(数据命令选择端),R/W(读写选择端),E(使能信号);以后编程便主要围绕这三个管脚展开进行初始化,写命令,写数据。
以下具体阐述这三个管脚:
RS为寄存器选择,高电平选择数据寄存器,低电平选择指令寄存器。
R/W为读写选择,高电平进行读操作,低电平进行写操作。
E端为使能端,后面和时序联系在一起。
除此外,D0~D7分别为8位双向数据线。
2.操作时序:
RS
R/W
操作说明
0
写入指令码D0~D7
1
读取输出的D0~D7状态字
写入数据D0~D7
从D0~D7读取数据
注:
关于E=H脉冲——开始时初始化E为0,然后置E为1,再清0.
读取状态字时,注意D7位,D7=1,禁止读写操作;D7=0,允许读写操作;
所以对控制器每次进行读写操作前,必须进行读写检测。
(即后面的读忙子程序)
3.指令集:
LCD_1602初始化指令小结:
0x38设置16*2显示,5*7点阵,8位数据接口
0x01清屏
0x0F开显示,显示光标,光标闪烁
0x08只开显示
0x0e开显示,显示光标,光标不闪烁
0x0c开显示,不显示光标
0x06地址加1,当写入数据的时候光标右移
0x02地址计数器AC=0;(此时地址为0x80)光标归原点,但是DDRAM中断内容不变
0x18光标和显示一起向左移动
4.显示地址:
LCD1602内部RAM显示缓冲区地址的映射图,00~0F、40~4F分别对应LCD1602的上下两行的每一个字符,只要往对应的RAM地址写入要显示字符的ASCII代码,就可以显示出来。
5.读写时序:
时序图1602手册中有,这里不引用了。
时序图很重要,编程就是根据时序图设置寄存器,让LCD工作。
二、LCD1602程序编写流程:
LCD1602在了解完以上信息后便可以编写,这里我们把程序分为以下几步:
1.定义LCD1602管脚,包括RS,R/W,E。
这里定义是指这些管脚分别接在单片机哪些I/O口上。
现举例如下:
sbitEN=P3^4;
sbitRS=P3^5;
sbitRW=P3^6;
2.显示初始化,在这一步进行初始化及设置显示模式等操作,包括以下步骤:
A.设置显示方式
B.延时
C.清理显示缓存
D.设置显示模式
通常推荐的初始化过程如下:
延时15ms
写指令38H
延时5ms
以上写38H指令可以看情况省略1~2步
(以上都不检测忙信号)
(以下都要检测忙信号)
写指令08H关闭显示
写指令01H显示清屏
写指令06H光标移动设置
写指令0cH显示开及光标设置
3.设置显示地址(写显示字符的位置)。
4.写显示字符的数据。
三、LCD1602各子程序模块及主程序编写:
现在按照上面编写程序的流程,给出各子程序模块及主程序的例子。
1.头文件,宏定义,定义管脚等:
#include
#defineucharunsignedchar
#defineuintunsignedint
ucharcodetable0[]={"QQ:
545699636"
};//此条语句为显示字符串时定义的字符串数组
2.LCD1602基本初始化子程序:
voidLCD1602()
{
EN=0;
RS=1;
RW=1;
P0=0xff;//这里P0为与LCDD0~D7相连的I/O口
}
3.读忙子程序:
voidread_busy()
P0=0xff;
RS=0;
EN=1;
while(P0&0x80);//P0和10000000相与,D7位若不为0,停在此处
EN=0;//若为0跳出进入下一步;这条语句的作用就是检测
}//D7位,若忙在此等待,不忙跳出读忙子程序执行读写指令
4.写指令写数据子程序:
voidwrite(uchari,bitj)
read_busy();
P0=i;//其中i=0,写指令;i=1,写数据;
RS=j;
RW=0;
5.延时子程序:
voiddelay(uintc)//功能为提供初始化等其他子程序中的延时1xcMS
uinta,b;
for(a=0;afor(b=0;b<120;b++);}6.LCD1602初始化子程序:voidinit()//完全按照要求初始化流程来,中间省略了一步写指令38H{delay(15);write(0x38,0);delay(5);write(0x38,0);write(0x08,0);write(0x01,0);write(0x06,0);write(0x0c,0);}7.显示单个字符子程序:voiddisplay_lcd_byte(uchary,ucharx,ucharz)//Y=0,1(起始行),{//X=0~15(起始列),Z=想写入字符的ASCII码if(y)//是否显示在第二行(若在第一行Y=0,不进入IF语句,若在第{//二行,进入IF语句x+=0x40;//第二行起始地址加上列数为字符显示地址}x+=0x80;//设置数据指针位置write(x,0);write(z,1);//写入数据}8.显示字符串子程序:voiddisplay_lcd_text(uchary,ucharx,uchartable[]){//Y(起始行),X(起始列)同字符显示,table[]字符串数组ucharz=0;uchart;t=strlen(table)+x;//求得字符串长度加上起始列位置while(x{//没有16个字符,从而不够位产生乱码;display_lcd_byte(y,x,table[z]);//逐位显示数组内字符x++;z++;}}9.主程序:主程序里除了放入初始化程序外就是加入自己编写的显示子程序,根据你所要的不用功能可以编写各种类型的显示子程序,这里不做详细介绍,以下举例为显示一个字符和显示字符串的显示子程序。voidmain(){LCD1602();init();display_lcd_byte(0,0,'A');//显示一个字符display_lcd_text(1,3,table);//显示字符串while(1);}到此,让LCD1602显示的操作流程和编程思想基本可以告一段落了,但是1602的功能实现远不止这些。利用1602你可以做出动态效果的显示,并且除了显示一般字符外,1602还支持自定义字符等等其他一些功能,下面在最后简单介绍下显示动态效果和自定义字符。一、显示动态效果:显示动态效果包括让一个字符或字符串原位置闪烁,或者前后移动等等。其实动态效果原理很简单,就是简单的利用延时。例如让字符原位置闪烁,可以认为是先让1602显示字符,延时一段时间后,可以显示空格或者直接清屏操作都可以达到让字符消失不见的效果,再延时一段时间后再让1602显示这个字符。同理,让字符前后移动也是这样,例如让字符在第一个位置显示,延时一段时间后让其在后面第二个位置显示,只要显示地址加1,然后显示即可。字符串也是同样的道理。在这里补充一点就是如何让字符串从1602第16个地址外进入,动态向前移动。其实可以通过显示地址表我们知道起始位置开始后1602一行只能显示16个字符,但是一行的地址却远远不止16个。大家可以看到第一行显示地址是从00~27,然而能显示在1602可见范围的只有00~0F,后面的位置其实就是起到一个缓冲的作用,你完全可以让字符数据存在在10地址后的RAM中,只不过,我们无法看到就是了,如果存在10前的地址我们就能看到显示。因此,我们可以先把显示起始地址设在10地址后的某一个位置,然后让字符显示地址每次加1,当加到0F时,我们就可以看到字符串第一个字符出现在1602的最末一位,然后继续向前移动。下面给出一段字符串移动显示的例子:voiddisplay_lcd_byte(uchary,ucharx,ucharz){if(y){x+=0x40;}x+=0x80;write(x,0);write(z,1);}voiddisplay_lcd_text(uchary,ucharx,uchartable[]){ucharz=0;uchart;t=strlen(table)+x;while(x{display_lcd_byte(y,x,table[z]);x++;z++;}display_lcd_byte(y,x,'');}//前两个子程序是显示子程序voidmain(){uchari;LCD1602();init();for(i=16;i>=0;i--)//这里的循环就是为了字符串从后往前显示{display_lcd_text(0,i,table0);//i减一次,首个字符就往前去一位delay(200);}while(1);}二、显示自定义字符:要想显示自定义字符,首先就得取得想要的图形或者字符的字模数组,可以通过手动提取的方法,取得相应的字模。如下图所示,对应一个字符显示区域。每8个字节,组成一个点阵数组。要想让某一格子显示就让那一位为1,每行自定义5位,全白为0x00;全黑为0x1f。一共8行,每行一位数据。将生成的点阵数组保存到CGRAM存储器中,生成自定义字符。1602内部CGRAM用于自定义的字符点阵的存储,总共64字节。由上一步点阵提取可知,每一个字符由8个字节数据组成。所以64字节CGRAM存储器,能够存储8组自定义字符的点阵数组。按照CGRAM地址划分为0~7为第一组,8~15为第二组,依次类推56~63为第8组数据。CHARACTERCODE是数据的显示地址,0-7的范围,能存储8位自定义的字符。(能存八个自定义,每个字符存放的)CGRAMADDRESS是存储数据的地址,从0-63共64个字节.存储64个数据。我们写入的数据是0x40~0x7F,共128位。(把字符数组内的8个数送进这8个地址,每存完一个字符的8位,下次地址直接转到0x48)CGRAMDATA字模每一行5位数据存内部常用字符显示时,显示编码是从0x20开始的。0x00~0x0F是专门留给自定义字符显示的。0x00~0x07和0x08~0x0F内容是一样的。例如:调用0x01位置和0x09位置,显示的内容是一样的。LCD1602自定义显示字符的方式共四步,如下面所示:1.设置向CGRAM中存入这个数据.初始地址是0x40。然后存一位向后加8,总共能存8位自定义的字符。2.然后可以把自定义的数据送入到LCD的CGRAM中。3.向LCD写指令,送入需要显示数据的地址。4.向LCD写指令,把显示的数据指向LCD的CGRAM存储的位置,显示出自定义字符。举例如下:1.建立一个字符数组;ucharLCD_Data1[]={0x01,0x03,0x1D,0x11,0x1D,0x03,0x01,0x00};2.设置CGRAM地址,写指令;Write_LCD(0x40,0);3.把数据送入CGRAM地址内;for(i=0;i<8;i++){Write_LCD(LCD_Data1[i],1);}4.写需要显示的位置指令;Write_LCD(0x80,0);5.把CGRAM的0位的数据送向LCD1602,显示数据存储的数据;Write_LCD(0x00,1); 从最简单的LED灯闪烁想到的:前段时间在某论坛看到一篇文章,写的很好,里面很多思路都是我们刚进入单片机领域的菜鸟很少想到很少用的。因此在原作者想法基础上归纳总结一下。在市面上众多的单片机学习资料中,最基础的实验无疑于点亮LED了,即控制单片机的I/O的电平的变化。就是这简单的不得了的程序能引起我们什么思考呢?如同如下实例代码一般:void main(){While(1){ LED = ON ;DelayMs(500) ;LED = OFF ;DelayMs(500) ;}}程序很简单,从它的结构可以看出,LED先点亮500MS,然后熄灭500MS,如此循环下去,形成的效果就是LED以1HZ的频率进行闪烁。下面让我们分析上面的程序有没有什么问题。新手看来,简单到刚入门都会的LED点亮程序好像很正常的啊,能有什么问题呢?这个时候我们应该换一个思路去想了。试想,整个程序除了控制LED = ON ; LED = OFF; 这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了的。因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。这里就是重点宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让它停留在某个地方空转消耗CPU时间。这往往就是新手不在乎并且忽略的问题。从上面我们可以总结出:(1)无论什么时候我们都要以实际应用的角度去考虑程序的编写。(2)无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。下面开始来编写程序。首先,肯定是头文件和一些必要的声明定义;#include#defineucharunsignedchar#defineuintunsignedint然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。 #define LED_ON() LED = 0x00 //所有LED亮 #define LED_OFF() LED = 0xff //所有LED熄灭下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。很简单,我们为系统产生一个1MS的时标。假定LED需要亮500MS,熄灭500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。uintLedTimeCount = 0 ;//LED计数器uchar LedState = 0 ;//LED状态标志, 0表示亮,1表示熄灭void LedProcess(void){if(LedState==0) //如果LED的状态为亮,则点亮LED{LED_ON() ;}else //否则熄灭LED{ LED_OFF() ;}}void LedStateChange(void){ if(SystemTime1Ms) //系统1MS时标到{ SystemTime1Ms = 0 ;LedTimeCount++ ; //LED计数器加1if(LedTimeCount >= 500) //计数达到500,即500MS到了,改变LED的状态。{LedTimeCount = 0 ; LedState = ! g_u8LedState ;} }}上面有一个变量没有提到,就是SystemTime1Ms 。这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0) 。我们的主函数就可以写成如下形式(示意代码)void main(void){while(1){LedProcess() ;LedStateChange() ;}}因为LED的亮或者灭依赖于LED状态变量(LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值(LedTimeCount ,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。让我们来从头到尾分析一遍整个程序的流程。程序首先执行LedProcess()函数,因为LedState的初始值为0(见定义)所以LED被点亮,然后退出 LedProcess()函数,执行下一个函数LedStateChange()。在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的到来,同时对LED计数器加1,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计数,同时把LED状态变量取反,然后退出函数。由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加1,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。这些函数执行所花的时间都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。系统的实时性得到了一定的保证,从宏观上看来,就是多个任务并发执行。综上所述,我们在编写程序时应该以实际应用的角度去考虑程序的编写,无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。附完整程序代码:#include#defineucharunsignedchar#defineuintunsignedint#defineLED_ON()LED=0x00//所有LED亮#defineLED_OFF()LED=0xff//所有LED熄灭#defineLEDP1//定义LED接口bitSystemTime1Ms=0;//1MS系统时标uintLedTimeCount=0;//LED计数器ucharLedState=0;//LED状态标志,0表示亮,1表示熄灭voidLedProcess(){if(LedState==0)//如果LED的状态为亮,则点亮LED{LED_ON();}else//否则熄灭LED{LED_OFF();}}voidLedStateChange(){if(SystemTime1Ms)//系统1MS时标到{SystemTime1Ms=0;//时标清0;LedTimeCount++;//LED计数器加1if(LedTimeCount>=500)//计数达到500,即500MS到了,改变LED的状态。{LedTimeCount=0;//计数器到500清0;LedState=!LedState;}}}voidTimer0Init()//定时器0初始化{TMOD&=0xf0;TMOD|=0x01;//定时器0工作方式1TH0=0xfc;//定时器初始值TL0=0x66;TR0=1;ET0=1;}voidTimer0()interrupt1{TH0=0xfc;//定时器重新赋初值TL0=0x66;SystemTime1Ms=1;//1MS时标标志位置位}voidmain(){Timer0Init();EA=1;while(1){LedProcess();LedStateChange();}} 关于中断部分的小结(1):中断是单片机和其他各种微处理器中必不可少的一部分之一,在此对51单片机中断有用的部分加以小结。小结(1)主要是对中断部分,对中断编程常用的特殊功能寄存器加以罗列小结,这一部分是中断思想的核心,以后的编程全部围绕这些寄存器进行。以后对于中断的编程及其寄存器设置可参考这一部分内容。一、关于51中断:对于51单片机中断子程序的编写我们必须知道大多51单片机芯片有5个中断源,有2个中断优先级,每个中断源的优先级可以编程控制。5个中断源分别是:外部中断0,由INT0/P3.2输入外部中断1,由INT1/P3.3输入定时器/计数器0,溢出中断请求;定时器/计数器1,溢出中断请求;串行口发送/接收,中断请求;这里给出一个非常经典的中断源图,根据这个图我们就能大概知道中断的具体过程,以及在中断编程中哪些地方需要设置。很明显的,5个中断源可以分为三类,外部中断,定时器中断和串口中断;并且我们需要对TCON,IE,IP等进行设置。二、几个重要的特殊功能寄存器:1.中断允许控制寄存器IECPU对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器IE控制的。EX0(IE.0),外部中断0允许位;ET0(IE.1),定时/计数器T0中断允许位;EX1(IE.2),外部中断1允许位;ET1(IE.3),定时/计数器T1中断允许位;ES(IE.4),串行口中断允许位;EA(IE.7),CPU中断允许(总允许)位。2.控制寄存器TCONTCON的低4位用于控制外部中断,TCON的高4位用于控制定时/计数器的启动和中断申请。其格式如下:TF1
for(b=0;b<120;b++);
6.LCD1602初始化子程序:
voidinit()//完全按照要求初始化流程来,中间省略了一步写指令38H
delay(15);
write(0x38,0);
delay(5);
write(0x08,0);
write(0x01,0);
write(0x06,0);
write(0x0c,0);
7.显示单个字符子程序:
voiddisplay_lcd_byte(uchary,ucharx,ucharz)//Y=0,1(起始行),
{//X=0~15(起始列),Z=想写入字符的ASCII码
if(y)//是否显示在第二行(若在第一行Y=0,不进入IF语句,若在第
{//二行,进入IF语句
x+=0x40;//第二行起始地址加上列数为字符显示地址
x+=0x80;//设置数据指针位置
write(x,0);
write(z,1);//写入数据
8.显示字符串子程序:
voiddisplay_lcd_text(uchary,ucharx,uchartable[])
{//Y(起始行),X(起始列)同字符显示,table[]字符串数组
ucharz=0;
uchart;
t=strlen(table)+x;//求得字符串长度加上起始列位置
while(x{//没有16个字符,从而不够位产生乱码;display_lcd_byte(y,x,table[z]);//逐位显示数组内字符x++;z++;}}9.主程序:主程序里除了放入初始化程序外就是加入自己编写的显示子程序,根据你所要的不用功能可以编写各种类型的显示子程序,这里不做详细介绍,以下举例为显示一个字符和显示字符串的显示子程序。voidmain(){LCD1602();init();display_lcd_byte(0,0,'A');//显示一个字符display_lcd_text(1,3,table);//显示字符串while(1);}到此,让LCD1602显示的操作流程和编程思想基本可以告一段落了,但是1602的功能实现远不止这些。利用1602你可以做出动态效果的显示,并且除了显示一般字符外,1602还支持自定义字符等等其他一些功能,下面在最后简单介绍下显示动态效果和自定义字符。一、显示动态效果:显示动态效果包括让一个字符或字符串原位置闪烁,或者前后移动等等。其实动态效果原理很简单,就是简单的利用延时。例如让字符原位置闪烁,可以认为是先让1602显示字符,延时一段时间后,可以显示空格或者直接清屏操作都可以达到让字符消失不见的效果,再延时一段时间后再让1602显示这个字符。同理,让字符前后移动也是这样,例如让字符在第一个位置显示,延时一段时间后让其在后面第二个位置显示,只要显示地址加1,然后显示即可。字符串也是同样的道理。在这里补充一点就是如何让字符串从1602第16个地址外进入,动态向前移动。其实可以通过显示地址表我们知道起始位置开始后1602一行只能显示16个字符,但是一行的地址却远远不止16个。大家可以看到第一行显示地址是从00~27,然而能显示在1602可见范围的只有00~0F,后面的位置其实就是起到一个缓冲的作用,你完全可以让字符数据存在在10地址后的RAM中,只不过,我们无法看到就是了,如果存在10前的地址我们就能看到显示。因此,我们可以先把显示起始地址设在10地址后的某一个位置,然后让字符显示地址每次加1,当加到0F时,我们就可以看到字符串第一个字符出现在1602的最末一位,然后继续向前移动。下面给出一段字符串移动显示的例子:voiddisplay_lcd_byte(uchary,ucharx,ucharz){if(y){x+=0x40;}x+=0x80;write(x,0);write(z,1);}voiddisplay_lcd_text(uchary,ucharx,uchartable[]){ucharz=0;uchart;t=strlen(table)+x;while(x{display_lcd_byte(y,x,table[z]);x++;z++;}display_lcd_byte(y,x,'');}//前两个子程序是显示子程序voidmain(){uchari;LCD1602();init();for(i=16;i>=0;i--)//这里的循环就是为了字符串从后往前显示{display_lcd_text(0,i,table0);//i减一次,首个字符就往前去一位delay(200);}while(1);}二、显示自定义字符:要想显示自定义字符,首先就得取得想要的图形或者字符的字模数组,可以通过手动提取的方法,取得相应的字模。如下图所示,对应一个字符显示区域。每8个字节,组成一个点阵数组。要想让某一格子显示就让那一位为1,每行自定义5位,全白为0x00;全黑为0x1f。一共8行,每行一位数据。将生成的点阵数组保存到CGRAM存储器中,生成自定义字符。1602内部CGRAM用于自定义的字符点阵的存储,总共64字节。由上一步点阵提取可知,每一个字符由8个字节数据组成。所以64字节CGRAM存储器,能够存储8组自定义字符的点阵数组。按照CGRAM地址划分为0~7为第一组,8~15为第二组,依次类推56~63为第8组数据。CHARACTERCODE是数据的显示地址,0-7的范围,能存储8位自定义的字符。(能存八个自定义,每个字符存放的)CGRAMADDRESS是存储数据的地址,从0-63共64个字节.存储64个数据。我们写入的数据是0x40~0x7F,共128位。(把字符数组内的8个数送进这8个地址,每存完一个字符的8位,下次地址直接转到0x48)CGRAMDATA字模每一行5位数据存内部常用字符显示时,显示编码是从0x20开始的。0x00~0x0F是专门留给自定义字符显示的。0x00~0x07和0x08~0x0F内容是一样的。例如:调用0x01位置和0x09位置,显示的内容是一样的。LCD1602自定义显示字符的方式共四步,如下面所示:1.设置向CGRAM中存入这个数据.初始地址是0x40。然后存一位向后加8,总共能存8位自定义的字符。2.然后可以把自定义的数据送入到LCD的CGRAM中。3.向LCD写指令,送入需要显示数据的地址。4.向LCD写指令,把显示的数据指向LCD的CGRAM存储的位置,显示出自定义字符。举例如下:1.建立一个字符数组;ucharLCD_Data1[]={0x01,0x03,0x1D,0x11,0x1D,0x03,0x01,0x00};2.设置CGRAM地址,写指令;Write_LCD(0x40,0);3.把数据送入CGRAM地址内;for(i=0;i<8;i++){Write_LCD(LCD_Data1[i],1);}4.写需要显示的位置指令;Write_LCD(0x80,0);5.把CGRAM的0位的数据送向LCD1602,显示数据存储的数据;Write_LCD(0x00,1); 从最简单的LED灯闪烁想到的:前段时间在某论坛看到一篇文章,写的很好,里面很多思路都是我们刚进入单片机领域的菜鸟很少想到很少用的。因此在原作者想法基础上归纳总结一下。在市面上众多的单片机学习资料中,最基础的实验无疑于点亮LED了,即控制单片机的I/O的电平的变化。就是这简单的不得了的程序能引起我们什么思考呢?如同如下实例代码一般:void main(){While(1){ LED = ON ;DelayMs(500) ;LED = OFF ;DelayMs(500) ;}}程序很简单,从它的结构可以看出,LED先点亮500MS,然后熄灭500MS,如此循环下去,形成的效果就是LED以1HZ的频率进行闪烁。下面让我们分析上面的程序有没有什么问题。新手看来,简单到刚入门都会的LED点亮程序好像很正常的啊,能有什么问题呢?这个时候我们应该换一个思路去想了。试想,整个程序除了控制LED = ON ; LED = OFF; 这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了的。因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。这里就是重点宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让它停留在某个地方空转消耗CPU时间。这往往就是新手不在乎并且忽略的问题。从上面我们可以总结出:(1)无论什么时候我们都要以实际应用的角度去考虑程序的编写。(2)无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。下面开始来编写程序。首先,肯定是头文件和一些必要的声明定义;#include#defineucharunsignedchar#defineuintunsignedint然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。 #define LED_ON() LED = 0x00 //所有LED亮 #define LED_OFF() LED = 0xff //所有LED熄灭下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。很简单,我们为系统产生一个1MS的时标。假定LED需要亮500MS,熄灭500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。uintLedTimeCount = 0 ;//LED计数器uchar LedState = 0 ;//LED状态标志, 0表示亮,1表示熄灭void LedProcess(void){if(LedState==0) //如果LED的状态为亮,则点亮LED{LED_ON() ;}else //否则熄灭LED{ LED_OFF() ;}}void LedStateChange(void){ if(SystemTime1Ms) //系统1MS时标到{ SystemTime1Ms = 0 ;LedTimeCount++ ; //LED计数器加1if(LedTimeCount >= 500) //计数达到500,即500MS到了,改变LED的状态。{LedTimeCount = 0 ; LedState = ! g_u8LedState ;} }}上面有一个变量没有提到,就是SystemTime1Ms 。这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0) 。我们的主函数就可以写成如下形式(示意代码)void main(void){while(1){LedProcess() ;LedStateChange() ;}}因为LED的亮或者灭依赖于LED状态变量(LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值(LedTimeCount ,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。让我们来从头到尾分析一遍整个程序的流程。程序首先执行LedProcess()函数,因为LedState的初始值为0(见定义)所以LED被点亮,然后退出 LedProcess()函数,执行下一个函数LedStateChange()。在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的到来,同时对LED计数器加1,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计数,同时把LED状态变量取反,然后退出函数。由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加1,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。这些函数执行所花的时间都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。系统的实时性得到了一定的保证,从宏观上看来,就是多个任务并发执行。综上所述,我们在编写程序时应该以实际应用的角度去考虑程序的编写,无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。附完整程序代码:#include#defineucharunsignedchar#defineuintunsignedint#defineLED_ON()LED=0x00//所有LED亮#defineLED_OFF()LED=0xff//所有LED熄灭#defineLEDP1//定义LED接口bitSystemTime1Ms=0;//1MS系统时标uintLedTimeCount=0;//LED计数器ucharLedState=0;//LED状态标志,0表示亮,1表示熄灭voidLedProcess(){if(LedState==0)//如果LED的状态为亮,则点亮LED{LED_ON();}else//否则熄灭LED{LED_OFF();}}voidLedStateChange(){if(SystemTime1Ms)//系统1MS时标到{SystemTime1Ms=0;//时标清0;LedTimeCount++;//LED计数器加1if(LedTimeCount>=500)//计数达到500,即500MS到了,改变LED的状态。{LedTimeCount=0;//计数器到500清0;LedState=!LedState;}}}voidTimer0Init()//定时器0初始化{TMOD&=0xf0;TMOD|=0x01;//定时器0工作方式1TH0=0xfc;//定时器初始值TL0=0x66;TR0=1;ET0=1;}voidTimer0()interrupt1{TH0=0xfc;//定时器重新赋初值TL0=0x66;SystemTime1Ms=1;//1MS时标标志位置位}voidmain(){Timer0Init();EA=1;while(1){LedProcess();LedStateChange();}} 关于中断部分的小结(1):中断是单片机和其他各种微处理器中必不可少的一部分之一,在此对51单片机中断有用的部分加以小结。小结(1)主要是对中断部分,对中断编程常用的特殊功能寄存器加以罗列小结,这一部分是中断思想的核心,以后的编程全部围绕这些寄存器进行。以后对于中断的编程及其寄存器设置可参考这一部分内容。一、关于51中断:对于51单片机中断子程序的编写我们必须知道大多51单片机芯片有5个中断源,有2个中断优先级,每个中断源的优先级可以编程控制。5个中断源分别是:外部中断0,由INT0/P3.2输入外部中断1,由INT1/P3.3输入定时器/计数器0,溢出中断请求;定时器/计数器1,溢出中断请求;串行口发送/接收,中断请求;这里给出一个非常经典的中断源图,根据这个图我们就能大概知道中断的具体过程,以及在中断编程中哪些地方需要设置。很明显的,5个中断源可以分为三类,外部中断,定时器中断和串口中断;并且我们需要对TCON,IE,IP等进行设置。二、几个重要的特殊功能寄存器:1.中断允许控制寄存器IECPU对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器IE控制的。EX0(IE.0),外部中断0允许位;ET0(IE.1),定时/计数器T0中断允许位;EX1(IE.2),外部中断1允许位;ET1(IE.3),定时/计数器T1中断允许位;ES(IE.4),串行口中断允许位;EA(IE.7),CPU中断允许(总允许)位。2.控制寄存器TCONTCON的低4位用于控制外部中断,TCON的高4位用于控制定时/计数器的启动和中断申请。其格式如下:TF1
{//没有16个字符,从而不够位产生乱码;
display_lcd_byte(y,x,table[z]);//逐位显示数组内字符
x++;
z++;
9.主程序:
主程序里除了放入初始化程序外就是加入自己编写的显示子程序,根据你所要的不用功能可以编写各种类型的显示子程序,这里不做详细介绍,以下举例为显示一个字符和显示字符串的显示子程序。
voidmain()
LCD1602();
init();
display_lcd_byte(0,0,'A');//显示一个字符
display_lcd_text(1,3,table);//显示字符串
while
(1);
到此,让LCD1602显示的操作流程和编程思想基本可以告一段落了,但是1602的功能实现远不止这些。
利用1602你可以做出动态效果的显示,并且除了显示一般字符外,1602还支持自定义字符等等其他一些功能,下面在最后简单介绍下显示动态效果和自定义字符。
一、显示动态效果:
显示动态效果包括让一个字符或字符串原位置闪烁,或者前后移动等等。
其实动态效果原理很简单,就是简单的利用延时。
例如让字符原位置闪烁,可以认为是先让1602显示字符,延时一段时间后,可以显示空格或者直接清屏操作都可以达到让字符消失不见的效果,再延时一段时间后再让1602显示这个字符。
同理,让字符前后移动也是这样,例如让字符在第一个位置显示,延时一段时间后让其在后面第二个位置显示,只要显示地址加1,然后显示即可。
字符串也是同样的道理。
在这里补充一点就是如何让字符串从1602第16个地址外进入,动态向前移动。
其实可以通过显示地址表我们知道起始位置开始后1602一行只能显示16个字符,但是一行的地址却远远不止16个。
大家可以看到第一行显示地址是从00~27,然而能显示在1602可见范围的只有00~0F,后面的位置其实就是起到一个缓冲的作用,你完全可以让字符数据存在在10地址后的RAM中,只不过,我们无法看到就是了,如果存在10前的地址我们就能看到显示。
因此,我们可以先把显示起始地址设在10地址后的某一个位置,然后让字符显示地址每次加1,当加到0F时,我们就可以看到字符串第一个字符出现在1602的最末一位,然后继续向前移动。
下面给出一段字符串移动显示的例子:
voiddisplay_lcd_byte(uchary,ucharx,ucharz)
if(y)
x+=0x40;
x+=0x80;
write(z,1);
t=strlen(table)+x;
while(x{display_lcd_byte(y,x,table[z]);x++;z++;}display_lcd_byte(y,x,'');}//前两个子程序是显示子程序voidmain(){uchari;LCD1602();init();for(i=16;i>=0;i--)//这里的循环就是为了字符串从后往前显示{display_lcd_text(0,i,table0);//i减一次,首个字符就往前去一位delay(200);}while(1);}二、显示自定义字符:要想显示自定义字符,首先就得取得想要的图形或者字符的字模数组,可以通过手动提取的方法,取得相应的字模。如下图所示,对应一个字符显示区域。每8个字节,组成一个点阵数组。要想让某一格子显示就让那一位为1,每行自定义5位,全白为0x00;全黑为0x1f。一共8行,每行一位数据。将生成的点阵数组保存到CGRAM存储器中,生成自定义字符。1602内部CGRAM用于自定义的字符点阵的存储,总共64字节。由上一步点阵提取可知,每一个字符由8个字节数据组成。所以64字节CGRAM存储器,能够存储8组自定义字符的点阵数组。按照CGRAM地址划分为0~7为第一组,8~15为第二组,依次类推56~63为第8组数据。CHARACTERCODE是数据的显示地址,0-7的范围,能存储8位自定义的字符。(能存八个自定义,每个字符存放的)CGRAMADDRESS是存储数据的地址,从0-63共64个字节.存储64个数据。我们写入的数据是0x40~0x7F,共128位。(把字符数组内的8个数送进这8个地址,每存完一个字符的8位,下次地址直接转到0x48)CGRAMDATA字模每一行5位数据存内部常用字符显示时,显示编码是从0x20开始的。0x00~0x0F是专门留给自定义字符显示的。0x00~0x07和0x08~0x0F内容是一样的。例如:调用0x01位置和0x09位置,显示的内容是一样的。LCD1602自定义显示字符的方式共四步,如下面所示:1.设置向CGRAM中存入这个数据.初始地址是0x40。然后存一位向后加8,总共能存8位自定义的字符。2.然后可以把自定义的数据送入到LCD的CGRAM中。3.向LCD写指令,送入需要显示数据的地址。4.向LCD写指令,把显示的数据指向LCD的CGRAM存储的位置,显示出自定义字符。举例如下:1.建立一个字符数组;ucharLCD_Data1[]={0x01,0x03,0x1D,0x11,0x1D,0x03,0x01,0x00};2.设置CGRAM地址,写指令;Write_LCD(0x40,0);3.把数据送入CGRAM地址内;for(i=0;i<8;i++){Write_LCD(LCD_Data1[i],1);}4.写需要显示的位置指令;Write_LCD(0x80,0);5.把CGRAM的0位的数据送向LCD1602,显示数据存储的数据;Write_LCD(0x00,1); 从最简单的LED灯闪烁想到的:前段时间在某论坛看到一篇文章,写的很好,里面很多思路都是我们刚进入单片机领域的菜鸟很少想到很少用的。因此在原作者想法基础上归纳总结一下。在市面上众多的单片机学习资料中,最基础的实验无疑于点亮LED了,即控制单片机的I/O的电平的变化。就是这简单的不得了的程序能引起我们什么思考呢?如同如下实例代码一般:void main(){While(1){ LED = ON ;DelayMs(500) ;LED = OFF ;DelayMs(500) ;}}程序很简单,从它的结构可以看出,LED先点亮500MS,然后熄灭500MS,如此循环下去,形成的效果就是LED以1HZ的频率进行闪烁。下面让我们分析上面的程序有没有什么问题。新手看来,简单到刚入门都会的LED点亮程序好像很正常的啊,能有什么问题呢?这个时候我们应该换一个思路去想了。试想,整个程序除了控制LED = ON ; LED = OFF; 这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了的。因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。这里就是重点宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让它停留在某个地方空转消耗CPU时间。这往往就是新手不在乎并且忽略的问题。从上面我们可以总结出:(1)无论什么时候我们都要以实际应用的角度去考虑程序的编写。(2)无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。下面开始来编写程序。首先,肯定是头文件和一些必要的声明定义;#include#defineucharunsignedchar#defineuintunsignedint然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。 #define LED_ON() LED = 0x00 //所有LED亮 #define LED_OFF() LED = 0xff //所有LED熄灭下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。很简单,我们为系统产生一个1MS的时标。假定LED需要亮500MS,熄灭500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。uintLedTimeCount = 0 ;//LED计数器uchar LedState = 0 ;//LED状态标志, 0表示亮,1表示熄灭void LedProcess(void){if(LedState==0) //如果LED的状态为亮,则点亮LED{LED_ON() ;}else //否则熄灭LED{ LED_OFF() ;}}void LedStateChange(void){ if(SystemTime1Ms) //系统1MS时标到{ SystemTime1Ms = 0 ;LedTimeCount++ ; //LED计数器加1if(LedTimeCount >= 500) //计数达到500,即500MS到了,改变LED的状态。{LedTimeCount = 0 ; LedState = ! g_u8LedState ;} }}上面有一个变量没有提到,就是SystemTime1Ms 。这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0) 。我们的主函数就可以写成如下形式(示意代码)void main(void){while(1){LedProcess() ;LedStateChange() ;}}因为LED的亮或者灭依赖于LED状态变量(LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值(LedTimeCount ,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。让我们来从头到尾分析一遍整个程序的流程。程序首先执行LedProcess()函数,因为LedState的初始值为0(见定义)所以LED被点亮,然后退出 LedProcess()函数,执行下一个函数LedStateChange()。在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的到来,同时对LED计数器加1,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计数,同时把LED状态变量取反,然后退出函数。由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加1,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。这些函数执行所花的时间都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。系统的实时性得到了一定的保证,从宏观上看来,就是多个任务并发执行。综上所述,我们在编写程序时应该以实际应用的角度去考虑程序的编写,无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。附完整程序代码:#include#defineucharunsignedchar#defineuintunsignedint#defineLED_ON()LED=0x00//所有LED亮#defineLED_OFF()LED=0xff//所有LED熄灭#defineLEDP1//定义LED接口bitSystemTime1Ms=0;//1MS系统时标uintLedTimeCount=0;//LED计数器ucharLedState=0;//LED状态标志,0表示亮,1表示熄灭voidLedProcess(){if(LedState==0)//如果LED的状态为亮,则点亮LED{LED_ON();}else//否则熄灭LED{LED_OFF();}}voidLedStateChange(){if(SystemTime1Ms)//系统1MS时标到{SystemTime1Ms=0;//时标清0;LedTimeCount++;//LED计数器加1if(LedTimeCount>=500)//计数达到500,即500MS到了,改变LED的状态。{LedTimeCount=0;//计数器到500清0;LedState=!LedState;}}}voidTimer0Init()//定时器0初始化{TMOD&=0xf0;TMOD|=0x01;//定时器0工作方式1TH0=0xfc;//定时器初始值TL0=0x66;TR0=1;ET0=1;}voidTimer0()interrupt1{TH0=0xfc;//定时器重新赋初值TL0=0x66;SystemTime1Ms=1;//1MS时标标志位置位}voidmain(){Timer0Init();EA=1;while(1){LedProcess();LedStateChange();}} 关于中断部分的小结(1):中断是单片机和其他各种微处理器中必不可少的一部分之一,在此对51单片机中断有用的部分加以小结。小结(1)主要是对中断部分,对中断编程常用的特殊功能寄存器加以罗列小结,这一部分是中断思想的核心,以后的编程全部围绕这些寄存器进行。以后对于中断的编程及其寄存器设置可参考这一部分内容。一、关于51中断:对于51单片机中断子程序的编写我们必须知道大多51单片机芯片有5个中断源,有2个中断优先级,每个中断源的优先级可以编程控制。5个中断源分别是:外部中断0,由INT0/P3.2输入外部中断1,由INT1/P3.3输入定时器/计数器0,溢出中断请求;定时器/计数器1,溢出中断请求;串行口发送/接收,中断请求;这里给出一个非常经典的中断源图,根据这个图我们就能大概知道中断的具体过程,以及在中断编程中哪些地方需要设置。很明显的,5个中断源可以分为三类,外部中断,定时器中断和串口中断;并且我们需要对TCON,IE,IP等进行设置。二、几个重要的特殊功能寄存器:1.中断允许控制寄存器IECPU对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器IE控制的。EX0(IE.0),外部中断0允许位;ET0(IE.1),定时/计数器T0中断允许位;EX1(IE.2),外部中断1允许位;ET1(IE.3),定时/计数器T1中断允许位;ES(IE.4),串行口中断允许位;EA(IE.7),CPU中断允许(总允许)位。2.控制寄存器TCONTCON的低4位用于控制外部中断,TCON的高4位用于控制定时/计数器的启动和中断申请。其格式如下:TF1
display_lcd_byte(y,x,table[z]);
display_lcd_byte(y,x,'');
//前两个子程序是显示子程序
uchari;
for(i=16;i>=0;i--)//这里的循环就是为了字符串从后往前显示
display_lcd_text(0,i,table0);//i减一次,首个字符就往前去一位
delay(200);
二、显示自定义字符:
要想显示自定义字符,首先就得取得想要的图形或者字符的字模数组,可以通过手动提取的方法,取得相应的字模。
如下图所示,对应一个字符显示区域。
每8个字节,组成一个点阵数组。
要想让某一格子显示就让那一位为1,每行自定义5位,全白为0x00;全黑为0x1f。
一共8行,每行一位数据。
将生成的点阵数组保存到CGRAM存储器中,生成自定义字符。
1602内部CGRAM用于自定义的字符点阵的存储,总共64字节。
由上一步点阵提取可知,每一个字符由8个字节数据组成。
所以64字节CGRAM存储器,能够存储8组自定义字符的点阵数组。
按照CGRAM地址划分为0~7为第一组,8~15为第二组,依次类推56~63为第8组数据。
CHARACTERCODE是数据的显示地址,0-7的范围,能存储8位自定义的字符。
(能存八个自定义,每个字符存放的)
CGRAMADDRESS是存储数据的地址,从0-63共64个字节.存储64个数据。
我们写入的数据是0x40~0x7F,共128位。
(把字符数组内的8个数送进这8个地址,每存完一个字符的8位,下次地址直接转到0x48)
CGRAMDATA字模每一行5位数据存
内部常用字符显示时,显示编码是从0x20开始的。
0x00~0x0F是专门留给自定义字符显示的。
0x00~0x07和0x08~0x0F内容是一样的。
例如:
调用0x01位置和0x09位置,显示的内容是一样的。
LCD1602自定义显示字符的方式共四步,如下面所示:
1.设置向CGRAM中存入这个数据.初始地址是0x40。
然后存一位向后加8,
总共能存8位自定义的字符。
2.然后可以把自定义的数据送入到LCD的CGRAM中。
3.向LCD写指令,送入需要显示数据的地址。
4.向LCD写指令,把显示的数据指向LCD的CGRAM存储的位置,显示出自定义字符。
举例如下:
1.建立一个字符数组;
ucharLCD_Data1[]={0x01,0x03,0x1D,0x11,0x1D,0x03,0x01,0x00};
2.设置CGRAM地址,写指令;
Write_LCD(0x40,0);
3.把数据送入CGRAM地址内;
for(i=0;i<8;i++)
Write_LCD(LCD_Data1[i],1);
4.写需要显示的位置指令;
Write_LCD(0x80,0);
5.把CGRAM的0位的数据送向LCD1602,显示数据存储的数据;
Write_LCD(0x00,1);
从最简单的LED灯闪烁想到的:
前段时间在某论坛看到一篇文章,写的很好,里面很多思路都是我们刚进入单片机领域的菜鸟很少想到很少用的。
因此在原作者想法基础上归纳总结一下。
在市面上众多的单片机学习资料中,最基础的实验无疑于点亮LED了,即控制单片机的I/O的电平的变化。
就是这简单的不得了的程序能引起我们什么思考呢?
如同如下实例代码一般:
void main()
While
(1)
LED = ON ;
DelayMs(500) ;
LED = OFF ;
程序很简单,从它的结构可以看出,LED先点亮500MS,然后熄灭500MS,如此循环下去,形成的效果就是LED以1HZ的频率进行闪烁。
下面让我们分析上面的程序有没有什么问题。
新手看来,简单到刚入门都会的LED点亮程序好像很正常的啊,能有什么问题呢?
这个时候我们应该换一个思路去想了。
试想,整个程序除了控制LED = ON ; LED = OFF; 这两条语句外,其余的时间,全消耗在了DelayMs(500)这两个函数上。
而在实际应用系统中是没有哪个系统只闪烁一只LED就其它什么事情都不做了的。
因此,在这里我们要想办法,把CPU解放出来,让它不要白白浪费500MS的延时等待时间。
这里就是重点宁可让它一遍又一遍的扫描看有哪些任务需要执行,也不要让它停留在某个地方空转消耗CPU时间。
这往往就是新手不在乎并且忽略的问题。
从上面我们可以总结出:
(1)无论什么时候我们都要以实际应用的角度去考虑程序的编写。
(2)无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方。
下面开始来编写程序。
首先,肯定是头文件和一些必要的声明定义;
然后为亮灭常数定义一个宏,由硬件连接图可以,当P0输出为低电平时候LED亮,P0输出为高电平时,LED熄灭。
#define LED_ON() LED = 0x00 //所有LED亮
#define LED_OFF() LED = 0xff //所有LED熄灭
下面到了重点了,究竟该如何释放CPU,避免其做延时空等待这样的事情呢。
很简单,我们为系统产生一个1MS的时标。
假定LED需要亮500MS,熄灭500MS,那么我们可以对这个1MS的时标进行计数,当这个计数值达到500时候,清零该计数值,同时把LED的状态改变。
uintLedTimeCount = 0 ;//LED计数器
uchar LedState = 0 ;//LED状态标志, 0表示亮,1表示熄灭
void LedProcess(void)
if(LedState==0) //如果LED的状态为亮,则点亮LED
LED_ON() ;
else //否则熄灭LED
LED_OFF() ;
void LedStateChange(void)
if(SystemTime1Ms) //系统1MS时标到
SystemTime1Ms = 0 ;
LedTimeCount++ ; //LED计数器加1
if(LedTimeCount >= 500) //计数达到500,即500MS到了,改变LED的状态。
LedTimeCount = 0 ;
LedState = !
g_u8LedState ;
上面有一个变量没有提到,就是SystemTime1Ms 。
这个变量可以定义为位变量或者是其它变量,在我们的定时器中断函数中对其置位,其它函数使用该变量后,应该对其复位(清0) 。
我们的主函数就可以写成如下形式(示意代码)
void main(void)
LedProcess() ;
LedStateChange() ;
因为LED的亮或者灭依赖于LED状态变量(LedState)的改变,而状态变量的改变,又依赖于LED计数器的计数值(LedTimeCount ,只有计数值达到一定后,状态变量才改变)所以,两个函数都没有堵塞CPU的地方。
让我们来从头到尾分析一遍整个程序的流程。
程序首先执行LedProcess()函数,因为LedState的初始值为0(见定义)所以LED被点亮,然后退出 LedProcess()函数,执行下一个函数LedStateChange()。
在函数LedStateChange()内部首先判断1MS的系统时标是否到了,如果没有到就直接退出函数,如果到了,就把时标清0以便下一个时标消息的到来,同时对LED计数器加1,然后再判断LED计数器是否到达我们预先想要的值500,如果没有,则退出函数,如果有,对计数器清0,以便下次重新计数,同时把LED状态变量取反,然后退出函数。
由上面整个流程可以知道,CPU所做的事情,就是对一些计数器加1,然后根据条件改变状态,再根据这个状态来决定是否点亮LED。
这些函数执行所花的时间都是相当短的,如果主程序中还有其它函数,则CPU会顺次往下执行下去。
对于其它的函数(如果有的话)也要采取同样的措施,保证其不堵塞CPU,如果全部基于这种方法设计,那么对于不是非常庞大的系统,我们的系统依旧可以保证多个任务(多个函数)同时执行。
系统的实时性得到了一定的保证,从宏观上看来,就是多个任务并发执行。
综上所述,我们在编写程序时应该以实际应用的角度去考虑程序的编写,无论什么时候都不要让CPU白白浪费等待,尤其是延时(超过1MS)这样的地方,如何去释放CPU(参考本章的例子),这是写出合格程序的基础。
附完整程序代码:
#defineLED_ON()LED=0x00//所有LED亮
#defineLED_OFF()LED=0xff//所有LED熄灭
#defineLEDP1//定义LED接口
bitSystemTime1Ms=0;//1MS系统时标
uintLedTimeCount=0;//LED计数器
ucharLedState=0;//LED状态标志,0表示亮,1表示熄灭
voidLedProcess()
if(LedState==0)//如果LED的状态为亮,则点亮LED
LED_ON();
else//否则熄灭LED
LED_OFF();
voidLedStateChange()
if(SystemTime1Ms)//系统1MS时标到
SystemTime1Ms=0;//时标清0;
LedTimeCount++;//LED计数器加1
if(LedTimeCount>=500)//计数达到500,即500MS到了,改变LED的状态。
LedTimeCount=0;//计数器到500清0;
LedState=!
LedState;
voidTimer0Init()//定时器0初始化
TMOD&=0xf0;
TMOD|=0x01;//定时器0工作方式1
TH0=0xfc;//定时器初始值
TL0=0x66;
TR0=1;
ET0=1;
voidTimer0()interrupt1
TH0=0xfc;//定时器重新赋初值
SystemTime1Ms=1;//1MS时标标志位置位
Timer0Init();
EA=1;
LedProcess();
LedStateChange();
关于中断部分的小结
(1):
中断是单片机和其他各种微处理器中必不可少的一部分之一,在此对51单片机中断有用的部分加以小结。
小结
(1)主要是对中断部分,对中断编程常用的特殊功能寄存器加以罗列小结,这一部分是中断思想的核心,以后的编程全部围绕这些寄存器进行。
以后对于中断的编程及其寄存器设置可参考这一部分内容。
一、关于51中断:
对于51单片机中断子程序的编写我们必须知道大多51单片机芯片有5个中断源,有2个中断优先级,每个中断源的优先级可以编程控制。
5个中断源分别是:
外部中断0,由INT0/P3.2输入
外部中断1,由INT1/P3.3输入
定时器/计数器0,溢出中断请求;
定时器/计数器1,溢出中断请求;
串行口发送/接收,中断请求;
这里给出一个非常经典的中断源图,根据这个图我们就能大概知道中断的具体过程,以及在中断编程中哪些地方需要设置。
很明显的,5个中断源可以分为三类,外部中断,定时器中断和串口中断;并且我们需要对TCON,IE,IP等进行设置。
二、几个重要的特殊功能寄存器:
1.中断允许控制寄存器IE
CPU对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器IE控制的。
EX0(IE.0),外部中断0允许位;
ET0(IE.1),定时/计数器T0中断允许位;
EX1(IE.2),外部中断1允许位;
ET1(IE.3),定时/计数器T1中断允许位;
ES(IE.4),串行口中断允许位;
EA(IE.7),CPU中断允许(总允许)位。
2.控制寄存器TCON
TCON的低4位用于控制外部中断,TCON的高4位用于控制定时/计数器的启动和中断申请。
其格式如下:
TF1
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1