上一回:通俗话教你单片机之【LCD 1602】篇
传送门:
http://bbs.mydigit.cn/read.php?tid=535041
回顾过去,从当初点亮一个LED只要一句代码 led = 0 到后来驱动1602液晶需要的一堆函数,我们发现我们的代码越来越复杂了。我们不难想象,当我们继续学习下去,需要写越来越多的代码,倘若所有的代码都写在了一起,不仅看起来让人心烦,而且调试起来更加痛苦!比如,让你从成千上万的代码堆中,找到一个变量,这将会耗掉你一上午的时间,可不可怕?更可怕的是,老板还催着你,让你2小时内搞定,这不比坑爹还坑爷爷么?!所以,我们有必要学习一种让编程更简洁高效的方法——
模块化编程。
什么是模块化编程?单从字面上理解,就是
把一个完整的程序分成几个模块,再组合起来。让我们先举一个简单的例子:
大家都知道,水果手机是由LG生产的屏幕,索尼生产的摄像头,三星生产的处理器及闪存,还有其他厂家生产的零部件,最后送往中国的富士康组装而成的。
我们把一部完整的水果手机当做一个程序,那么这个程序就是由
屏幕模块、摄像头模块、处理器模块等各种各样的模块 构成的,每个模块完成一个特定的功能,并且相互独立。比如LG生产的屏幕跟你三星生产的处理器一点屁关系都没有,同样,索尼生产的摄像头跟你LG的屏幕没有任何的联系。
我们可以想象,假若所有的零部件都由苹果生产,由苹果组装,不仅仅效率低下,而且生产周期长(你要生产处理器还要开个晶圆厂呢。。。),照这样算,就算到下个世纪我们都不见得能用上苹果的手机。对一个大型的程序(比如一个MP3)来说也一样,我们把整个程序分成几个模块(显示模块,按键模块,解码模块),每个模块相互独立(显示模块跟按键模块没有任何关系),每个人负责一个模块,最后组装起来,倘若显示有问题,直接去找显示模块,而不用从头找到尾,这样的效率是不是变得很高?这个便是模块化编程。
由上面的例子我们可以知道,在进行模块化编程的时候,应注意以下几点:
1、尽可能的保持模块的独立化。比如说,在显示模块里面,尽量不要再调用外部的资源,就算没有其他模块的配合,这个显示模块也能独立地完成显示任务。
2、留给外部调用的接口尽可能的少且简单。就像三星生产处理器一样,我们只需给它一个订单(把要显示的数据传给显示模块)就行了,至于它是怎么生产(怎么把数据显示出来),一句话:关我鸟事?
3、尽量少用全局变量。比如说,在显示模块中定义了一个全局变量 temp ,而在按键模块中也定义了一个全局变量 temp ,那么这两个变量就会冲突,当显示模块更改这个 temp 变量的值时,按键模块的 temp 值也随之改变,这将导致程序产生严重的错误。所以尽量要避免这种情况产生。 所有的零部件都在富士康组装,所以富士康很关键,同样我们的程序也需要在一个地方组装,我们把它叫做
主文件 ,当然了,你爱怎么叫就怎么叫。手机在富士康是用工人组装的,那么在程序中是怎样组装的?还记得我们每次写程序第一句都要写的
#include <reg52.h> 么?这句话的意思是把 reg52.h 这个文件里面的所有代码都包含到这个文件里面(相当于把reg52.h这个文件的所有代码都复制并粘贴到任何写有 #include <reg52.h> 的这个文件里面)不信你打开 reg52.h 这个文件试试?
在 #include <reg52.h> 这个语句这里右键
打开 reg52.h 这个文件,看看,我们平时写程序用到的
P0,P1,定时器的 TMOD、TH串口的 SBUF 等关键字都在reg52.h这个文件里定义过了,所以我们在开头用一句#include <reg52.h> 就把这些定义全部组装到我们的程序里面,直接使用而不用再次定义这些关键字了。如果你不写 #include <reg52.h> ,编译器就不知道你写的 SBUF = seven 这个
SBUF 到底是什么东西。
另外,按键模块 需要 #include <reg52.h> ,而显示模块、解码模块也需要 #include <reg52.h> ,
那么在组装的时候 reg52.h 这个文件就被重复包含了三次,然而我们只需要包含一次就可以了,所以我们要防止它被重复包含进来,我们仔细看看 reg52.h 这个文件的开头和结尾有这么几句
- #ifndef __XXX_H__
- #define __XXX_H__
- XXXXX 代码区 XXXXX
- XXXXX 代码区 XXXXX
-
- #endif
上面的代码就是为了防止这个文件被重复包含的。下面我们来学习怎么模块化编程。
把一个程序模块化需要两种文件,一种是
头文件 XXX.h ,另一种是
代码文件 XXX.c ,我们把头文件 XXX.h 称作
合同 ,
它声明了这个模块那些东西是可以被外部调用的,而代码文件 XXX.c 我们称之为
车间,
它负责处理从外部传过来的订单(数据),我们举个例子:闪烁的LED灯模块化编程 (众人:怎么又是LED?! 小七:呃,这玩意儿便宜,好驱动,效果又明显....)
我们在头文件 led.h 里面声明一个函数,说
LED闪烁模块有一些函数是可以被外部调用函数的(我们称之为
接口,你也可以理解为模块对外部的接口):led(unsigned int time),其中time是闪烁的间隔时间。那么头文件 LED.H的代码可以这样写:
- #ifndef __LED_H__ //防止LED.H这个文件被重复包含,写法就是把 led.h 全部改为大写
- #define __LED_H__ //并且把点 . 变成下划线 _ 再在两边加上两根下划线 __ 即可
-
- extern void led(unsigend int time); //extern 这个修饰词是声明这个函数可以被外部调用的
-
- #endif
合同(头文件) led.h 里面已经声明了 led(unsigend int i) 这个接口函数,那么外部传过来的订单(数据)将会交给车间 led.c 这个代码文件处理:
- #include <reg52.h> //需要用到 reg52.h 里面的东西,所以要包含进来
- #include “led.h” //把合同包含进来,注意是双引号
-
- sbit led_pin = P1^7; //定义led所接的引脚
-
- void delay() //1ms延时函数,因为是在led.c里面写的,所以只能在 led.c 里面被调用
- {
- unsigned char a,b;
- for(b=199;b>0;b--)
- for(a=1;a>0;a--);
- }
-
- void led(unsigned int time) //这个函数是供外部调用的
- {
- unsigned int i;
- led_pin = 0; //点亮LED
- for(i=time;i>0;i--)delay(); //延时 time 个毫秒
- led_pin = 1; //熄灭LED
- for(i=time;i>0;i--)delay(); //延时 time 个毫秒
- }
所以,当某个模块调用它时(比如主文件 main.c):led(500) 其中500 将会交给 led.c 处理,带来的影响就是 点亮500 ms 熄灭500ms ,如果是led(200),就是点亮200 ms 熄灭200ms
Main.c 的代码:
- #include "led.h" //因为并没有用到reg52.h 里面的内容,所以不用 #include <reg52.h>
- void main()
- {
- while(1)
- {
- led(500); //调用 led() 函数,并传一个500值给它处理,我们只需要结果,不需要关心它是怎么实现的。
- }
- }
我们把这些文件都放在一块,由于 led.c 被编成了一个模块,所以我们要在keil 中把它添加到工程里面
效果如下:
(众人:上面就那么点代码,还不如不用模块化编程呢!)下面我们来点复杂的,用1602液晶及定时器实现一个简单的时钟。思路就是,让定时器模块产生一个一秒钟的时标,通知 主文件,让 主文件 转化为时间,并让显示模块显示出来。当然,我们也可以让定时模块直接处理好时分秒,通过全局变量传给主文件,不过,能尽量不用全局变量就不要用(在适当的时候适当的使用全局变量会让更有效率,所以要根据实际情况来决定)。
先是1602显示模块:
lcd.h ,用来声明能被外部调用的函数:
- #ifndef __LCD_H__
- #define __LCD_H__
- extern void lcd_init(); //LCD初始化函数
- extern void lcd_wrt(uunsigned char dat,bit rs); //用LCD显示数据
- #endif
lcd.c代码 ,负责把外部传过来的数据通过1602显示出来
- #include <reg52.h> //LCD模块用到了 reg52.h
- #include "lcd.h" //包含 lcd.h 头文件
- sbit lcd_rs = P3^5; //定义 数据/命令 写入模式控制引脚 RS
- sbit lcd_en = P3^4; //定义 数据使能 控制引脚 EN
- void delay(unsigned char n) //延时函数,lcd反应跟不上单片机,所以要单片机延时等待lcd
- {
- unsigned char x,y;
- for(x=n;x>0;x--)
- for(y=110;y>0;y--);
- }
-
- void lcd_wrt(unsigned char dat,bit rs) //向lcd写入数据,dat是数据,rs是判断该数据是命令还是用来显示的
- {
- if(rs==1) //若 key 的值为1,则
- lcd_rs = 1; //rs = 1 表示将要传给它的数据是用来显示的
- else //若 key 的值不为1,则
- lcd_rs = 0; //rs = 0 表示这个数据是用来控制液晶屏的
-
- P0 = dat; //假设我们lcd的8根数据线接在单片机的 P0 口,则把传过来的 dat 的值送到P0口,即P0口的状态等于dat变量的值(16进制)
-
- delay(5); //由于lcd的反应速度比较慢,跟不上单片机,所以延时一小会儿,让单片机等它把数据写完
- lcd_en = 1; //当lcd的 en 引脚从高电平变成低电平时,lcd会接收从P1口传过来的数据
- lcd_en = 0;
- }
- void lcd_init() //液晶初始化函数
- {
- lcd_wrt(0x38,0); //向lcd_rs写入 0x38 命令,0x38 命令代表设定lcd为两行显示模式,0代表写入的数据是控制命令
- lcd_wrt(0x0c,0); //设定lcd不显示光标
- lcd_wrt(0x06,0); //设定lcd自动把显示地址递增,比如第1次在第1行第1列显示了字符 A,那么下次写入字符 B ,lcd会自动把它显示在第1行第2列
- lcd_wrt(0x01,0); //清空屏幕,把黑板擦干净
- }
还有定时器模块,负责定时,产生1秒钟的时标
Timer.h 用来声明能被外部调用的函数:
- #ifndef __TIMER_H__
- #define __TIMER_H__
- extern void timer_init(); //声明定时器初始化函数是可以被外部调用的
- extern bit time_out(); //声明时间处理函数,1秒到了返回1,否则返回0
- #endif
timer.c 用来初始化定时器,处理定时器中断等
- #include <reg52.h> //定时器模块用到了 reg52.h
- #include "timer.h" //包含 timer.h 头文件
- static char i=0; //static 说明这个全局变量只能在本文件使用,与extern相反
- void timer_init() //定时器初始化函数
- {
- TMOD=0x01; //设定定时器0的工作方式(方式1)
- TH0 = (65536-50000)/256; //设定初始值,50000us(50ms) 中断一次
- TL0 = (65536-50000)%256;
- ET0=1; //允许定时器0产生中断
- EA=1; //总中断开关
- TR0=1; //启动定时器,开始计时
- }
- void time_svr() interrupt 1 //定时器中断服务 50ms中断一次
- {
- i++; //变量 i 用来计算产生了多少次中断
- TH0 = (65536-50000)/256; //重新设定初始值,50000us(50ms) 中断一次
- TL0 = (65536-50000)%256;
- }
- bit time_out() //调用后会返回1或0
- {
- if(i==20)
- {
- i=0; //当i=20次时即1秒到了,i清零
- return(1); //返回 1,说明1秒时间到了
- }
- else
- return(0); //否则返回 0
- }
模块都写好了,我们现在来写主文件,把它们都调用在一起
Main.c
- /*********************************************
- 注意哦!main.c 并没有用到 reg52.h 里面的东西,所以不用把 reg52.h 包含进来,因此不用写了。
- *********************************************/
- #include "timer.h" //把定时器模块包含进来
- #include "lcd.h" //把液晶模块包含进来
- /***********************************************
- 定义字符串数组,用于给液晶传显示数据,因为液晶只认识字符类型的数据,比如'A'、'B'、'1'、'2'等,而
- 我们处理出来的是数值类型的 1、2、3不能直接显示,所以要做个转换,把数值的 1 转成能识别的字符 '1'
- ***********************************************/
- unsigned char code num[]="0123456789"; //定义字符串
- char hh=0,mm=0,ss=0; //定义小时、分钟、秒钟的变量
- /***********************************************
- 定义一个 time_show 函数,获取定时器产生的1秒时标并处理和刷新时间。lcd_wrt(num[hh/10%10],1)的意思
- 是:把小时 hh 变量分离出十位,比如12分离出1,这样就变成了lcd_wrt(num[1],1),即把数组num的1号元素
- (字符'1')传给液晶显示,如果分离的是2,就是'2',那么液晶就会在相应的位置显示出 “2”来
- ***********************************************/
- void time_show()
- {
- if(time_out()) //判断 time_out() 是不是返回1
- {
- ss++; //若返回1,说明1秒钟到了,秒数 ss+1
- if(ss==60){ss=0;mm++;} //如果60秒到了,秒数清零,分钟 mm+1
- if(mm==60){mm=0;hh++;} //如果60分到了,分钟清零,小时 hh+1
- if(hh==24){hh=0;}
- /*****设定第1个字符显示在第1行第4列(为了居中),后面递增****/
- lcd_wrt(0x80+4,0);
- lcd_wrt(num[hh/10%10],1); //分离小时的十位,并用液晶显示出来
- lcd_wrt(num[hh%10],1); //分离小时的个位,并用液晶显示出来
- lcd_wrt('-',1); //显示时间分隔符号“ —— ”
- lcd_wrt(num[mm/10%10],1); //分钟的十位
- lcd_wrt(num[mm%10],1); //分钟的个位
- lcd_wrt('-',1); //显示时间分隔符号“ —— ”
- lcd_wrt(num[ss/10%10],1); //秒钟的十位
- lcd_wrt(num[ss%10],1); //秒钟的个位
- }
- }
- void main()
- {
- lcd_init(); //液晶初始化
- timer_init(); //定时器初始化
- while(1) //死循环,不断执行
- {
- time_show(); //调用时间显示函数
- }
- }
运行的效果如下:
我们可以发现,运用模块化编程后,思路清晰,井井有条了,假使我们发现时间不对了,我们就去找 定时器模块 ,假使是显示有问题,我们就去找 显示模块,
更更更可怕的是,把他们写成模块后,假使以后别的程序要用显示模块的话,我直接调用 lcd.h ,往那个 lcd_wrt() 接口传数据就行了,不用再写一次显示代码,是不是很爽歪歪?虽然目前还体现不出很明显的优势,但当你做一些很庞大的项目时,相信你会从中受益的!~
PS:为了方便大家,小七把源代码文件传上来了,有需要的就下载吧。虽然程序看上去很长很可怕,其实只要你有思路了,这些代码写出来就如什么什么行云流水一样,噼里啪啦的就写出来了。。。另外,没有人能够保证写完代码后一次编译就通过的,编程避免不了出错,当发现错误时,希望大家能积极的去寻找错误,不要被困难击倒,定时器时钟小七也调试了半个小时才弄好,不是分号忘记写了就是变量没声明就用了(众人:羞羞,这种低级错误也犯。。。),所以,当你排除所有的错误,点击编译后出现下面这种结果,你会觉得特有成就感!
十分感谢
linuxyouxia 网友的指正,由于小七的粗心大意,在发帖时没有细心的检查,出现了1L所说的错误。现在问题已经更正,再次感谢这位热心网友的指正,同时也鞭策着小七要严谨做事,不能马虎大意!