切换到宽版
  • 40744阅读
  • 114回复

[C51]~小七又更新了【让编程更简洁高效之模块化编程】 [复制链接]

上一主题 下一主题
离线satelives
 

发帖
493
M币
12410
专家
106
粉丝
9432
只看楼主 倒序阅读 我要置顶 楼主  发表于: 2013-01-28
上一回:通俗话教你单片机之【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  这个文件的开头和结尾有这么几句

  1. #ifndef __XXX_H__  
  2. #define __XXX_H__
  3.   XXXXX 代码区  XXXXX
  4.   XXXXX 代码区  XXXXX
  5.   
  6.   #endif

  上面的代码就是为了防止这个文件被重复包含的。下面我们来学习怎么模块化编程。

  把一个程序模块化需要两种文件,一种是 头文件 XXX.h ,另一种是 代码文件 XXX.c ,我们把头文件 XXX.h 称作 合同 ,它声明了这个模块那些东西是可以被外部调用的,而代码文件 XXX.c 我们称之为 车间,它负责处理从外部传过来的订单(数据),我们举个例子:闪烁的LED灯模块化编程 (众人:怎么又是LED?! 小七:呃,这玩意儿便宜,好驱动,效果又明显....)

  我们在头文件 led.h 里面声明一个函数,说 LED闪烁模块有一些函数是可以被外部调用函数的(我们称之为接口,你也可以理解为模块对外部的接口):led(unsigned int time),其中time是闪烁的间隔时间。那么头文件 LED.H的代码可以这样写:

  1. #ifndef  __LED_H__   //防止LED.H这个文件被重复包含,写法就是把 led.h 全部改为大写
  2. #define __LED_H__  //并且把点 . 变成下划线 _ 再在两边加上两根下划线 __ 即可
  3.   
  4.   extern void led(unsigend int time); //extern 这个修饰词是声明这个函数可以被外部调用的
  5.   
  6. #endif


  合同(头文件) led.h 里面已经声明了 led(unsigend int i) 这个接口函数,那么外部传过来的订单(数据)将会交给车间 led.c 这个代码文件处理:
  1. #include <reg52.h>     //需要用到 reg52.h 里面的东西,所以要包含进来
  2. #include “led.h”        //把合同包含进来,注意是双引号
  3.   
  4.   sbit led_pin = P1^7;  //定义led所接的引脚
  5.   
  6.   void delay()  //1ms延时函数,因为是在led.c里面写的,所以只能在 led.c 里面被调用
  7.   {
  8.       unsigned char a,b;
  9.           for(b=199;b>0;b--)
  10.               for(a=1;a>0;a--);
  11.   }
  12.   
  13.   void led(unsigned int time)   //这个函数是供外部调用的
  14.   {
  15.       unsigned int i;
  16.       led_pin = 0;                    //点亮LED
  17.       for(i=time;i>0;i--)delay();        //延时 time 个毫秒
  18.       led_pin = 1;                    //熄灭LED
  19.       for(i=time;i>0;i--)delay();        //延时 time 个毫秒
  20.   }


  所以,当某个模块调用它时(比如主文件 main.c):led(500) 其中500 将会交给 led.c 处理,带来的影响就是 点亮500 ms 熄灭500ms ,如果是led(200),就是点亮200 ms 熄灭200ms

  Main.c 的代码:  
  1. #include "led.h" //因为并没有用到reg52.h 里面的内容,所以不用 #include <reg52.h>
  2. void main()
  3. {
  4.     while(1)
  5.     {
  6.         led(500);        //调用 led() 函数,并传一个500值给它处理,我们只需要结果,不需要关心它是怎么实现的。
  7.     }
  8. }


    我们把这些文件都放在一块,由于 led.c 被编成了一个模块,所以我们要在keil 中把它添加到工程里面







效果如下:



(众人:上面就那么点代码,还不如不用模块化编程呢!)下面我们来点复杂的,用1602液晶及定时器实现一个简单的时钟。思路就是,让定时器模块产生一个一秒钟的时标,通知 主文件,让 主文件 转化为时间,并让显示模块显示出来。当然,我们也可以让定时模块直接处理好时分秒,通过全局变量传给主文件,不过,能尽量不用全局变量就不要用(在适当的时候适当的使用全局变量会让更有效率,所以要根据实际情况来决定)。

先是1602显示模块:

lcd.h ,用来声明能被外部调用的函数:
  1. #ifndef __LCD_H__  
  2. #define __LCD_H__
  3.   extern void lcd_init();            //LCD初始化函数
  4.   extern void lcd_wrt(uunsigned char dat,bit rs);    //用LCD显示数据
  5. #endif

lcd.c代码  ,负责把外部传过来的数据通过1602显示出来
  1. #include <reg52.h>    //LCD模块用到了 reg52.h
  2. #include "lcd.h"    //包含 lcd.h 头文件
  3. sbit lcd_rs = P3^5;    //定义 数据/命令 写入模式控制引脚 RS
  4. sbit lcd_en = P3^4;    //定义 数据使能 控制引脚 EN
  5. void delay(unsigned char n)    //延时函数,lcd反应跟不上单片机,所以要单片机延时等待lcd
  6. {
  7.     unsigned char x,y;
  8.   for(x=n;x>0;x--)
  9.      for(y=110;y>0;y--);
  10. }
  11.     
  12. void lcd_wrt(unsigned char dat,bit rs)    //向lcd写入数据,dat是数据,rs是判断该数据是命令还是用来显示的
  13. {
  14.   if(rs==1)            //若 key 的值为1,则
  15.       lcd_rs = 1;        //rs = 1 表示将要传给它的数据是用来显示的
  16.   else                //若 key 的值不为1,则
  17.       lcd_rs = 0;        //rs = 0 表示这个数据是用来控制液晶屏的
  18.         
  19.   P0 = dat;            //假设我们lcd的8根数据线接在单片机的 P0 口,则把传过来的 dat 的值送到P0口,即P0口的状态等于dat变量的值(16进制)
  20.     
  21.   delay(5);            //由于lcd的反应速度比较慢,跟不上单片机,所以延时一小会儿,让单片机等它把数据写完
  22.   lcd_en = 1;            //当lcd的 en 引脚从高电平变成低电平时,lcd会接收从P1口传过来的数据
  23.   lcd_en = 0;
  24. }
  25. void lcd_init()              //液晶初始化函数
  26. {
  27.   lcd_wrt(0x38,0);    //向lcd_rs写入 0x38 命令,0x38 命令代表设定lcd为两行显示模式,0代表写入的数据是控制命令
  28.   lcd_wrt(0x0c,0);    //设定lcd不显示光标
  29.   lcd_wrt(0x06,0);    //设定lcd自动把显示地址递增,比如第1次在第1行第1列显示了字符 A,那么下次写入字符 B ,lcd会自动把它显示在第1行第2列
  30.   lcd_wrt(0x01,0);    //清空屏幕,把黑板擦干净
  31. }


    还有定时器模块,负责定时,产生1秒钟的时标

    Timer.h  用来声明能被外部调用的函数:
  1. #ifndef __TIMER_H__  
  2. #define __TIMER_H__
  3.   extern void timer_init();    //声明定时器初始化函数是可以被外部调用的
  4.     extern bit time_out();        //声明时间处理函数,1秒到了返回1,否则返回0
  5. #endif


    timer.c 用来初始化定时器,处理定时器中断等

  1. #include <reg52.h>    //定时器模块用到了 reg52.h
  2. #include "timer.h"    //包含 timer.h 头文件
  3. static char i=0;    //static 说明这个全局变量只能在本文件使用,与extern相反
  4. void timer_init()                //定时器初始化函数
  5. {
  6.     TMOD=0x01;                    //设定定时器0的工作方式(方式1)
  7.     TH0 = (65536-50000)/256;    //设定初始值,50000us(50ms) 中断一次
  8.     TL0 = (65536-50000)%256;
  9.     ET0=1;                        //允许定时器0产生中断
  10.     EA=1;                        //总中断开关
  11.     TR0=1;                        //启动定时器,开始计时
  12. }
  13. void time_svr() interrupt 1        //定时器中断服务 50ms中断一次
  14. {
  15.     i++;                        //变量 i 用来计算产生了多少次中断
  16.     TH0 = (65536-50000)/256;    //重新设定初始值,50000us(50ms) 中断一次
  17.     TL0 = (65536-50000)%256;    
  18. }
  19. bit time_out()        //调用后会返回1或0
  20. {
  21.     if(i==20)
  22.     {
  23.        i=0;        //当i=20次时即1秒到了,i清零
  24.        return(1);    //返回 1,说明1秒时间到了
  25.     }        
  26.     else
  27.        return(0);        //否则返回 0
  28. }


模块都写好了,我们现在来写主文件,把它们都调用在一起

Main.c
  1. /*********************************************
  2. 注意哦!main.c 并没有用到 reg52.h 里面的东西,所以不用把 reg52.h 包含进来,因此不用写了。
  3. *********************************************/
  4. #include "timer.h"    //把定时器模块包含进来
  5. #include "lcd.h"    //把液晶模块包含进来
  6. /***********************************************
  7. 定义字符串数组,用于给液晶传显示数据,因为液晶只认识字符类型的数据,比如'A'、'B'、'1'、'2'等,而
  8. 我们处理出来的是数值类型的 1、2、3不能直接显示,所以要做个转换,把数值的 1 转成能识别的字符 '1'
  9. ***********************************************/
  10. unsigned char code num[]="0123456789"; //定义字符串
  11. char hh=0,mm=0,ss=0;    //定义小时、分钟、秒钟的变量
  12. /***********************************************
  13. 定义一个 time_show 函数,获取定时器产生的1秒时标并处理和刷新时间。lcd_wrt(num[hh/10%10],1)的意思
  14. 是:把小时 hh 变量分离出十位,比如12分离出1,这样就变成了lcd_wrt(num[1],1),即把数组num的1号元素
  15. (字符'1')传给液晶显示,如果分离的是2,就是'2',那么液晶就会在相应的位置显示出 “2”来
  16. ***********************************************/
  17. void time_show()
  18. {
  19.   if(time_out())                //判断 time_out() 是不是返回1
  20.   {
  21.     ss++;                    //若返回1,说明1秒钟到了,秒数 ss+1
  22.     if(ss==60){ss=0;mm++;}    //如果60秒到了,秒数清零,分钟 mm+1
  23.     if(mm==60){mm=0;hh++;}    //如果60分到了,分钟清零,小时 hh+1
  24.     if(hh==24){hh=0;}
  25. /*****设定第1个字符显示在第1行第4列(为了居中),后面递增****/
  26.     lcd_wrt(0x80+4,0);
  27.     lcd_wrt(num[hh/10%10],1);    //分离小时的十位,并用液晶显示出来
  28.     lcd_wrt(num[hh%10],1);        //分离小时的个位,并用液晶显示出来
  29.     lcd_wrt('-',1);                //显示时间分隔符号“ —— ”
  30.     lcd_wrt(num[mm/10%10],1);     //分钟的十位
  31.     lcd_wrt(num[mm%10],1);         //分钟的个位
  32.     lcd_wrt('-',1);                //显示时间分隔符号“ —— ”
  33.     lcd_wrt(num[ss/10%10],1);    //秒钟的十位
  34.     lcd_wrt(num[ss%10],1);        //秒钟的个位
  35.   }
  36. }
  37. void main()
  38. {
  39.   lcd_init();            //液晶初始化
  40. timer_init();         //定时器初始化
  41.     while(1)            //死循环,不断执行
  42.   {
  43.     time_show();    //调用时间显示函数
  44.   }
  45. }


运行的效果如下:



  我们可以发现,运用模块化编程后,思路清晰,井井有条了,假使我们发现时间不对了,我们就去找 定时器模块 ,假使是显示有问题,我们就去找 显示模块,更更更可怕的是,把他们写成模块后,假使以后别的程序要用显示模块的话,我直接调用 lcd.h ,往那个 lcd_wrt() 接口传数据就行了,不用再写一次显示代码,是不是很爽歪歪?虽然目前还体现不出很明显的优势,但当你做一些很庞大的项目时,相信你会从中受益的!~

  PS:为了方便大家,小七把源代码文件传上来了,有需要的就下载吧。虽然程序看上去很长很可怕,其实只要你有思路了,这些代码写出来就如什么什么行云流水一样,噼里啪啦的就写出来了。。。另外,没有人能够保证写完代码后一次编译就通过的,编程避免不了出错,当发现错误时,希望大家能积极的去寻找错误,不要被困难击倒,定时器时钟小七也调试了半个小时才弄好,不是分号忘记写了就是变量没声明就用了(众人:羞羞,这种低级错误也犯。。。),所以,当你排除所有的错误,点击编译后出现下面这种结果,你会觉得特有成就感!







十分感谢 linuxyouxia 网友的指正,由于小七的粗心大意,在发帖时没有细心的检查,出现了1L所说的错误。现在问题已经更正,再次感谢这位热心网友的指正,同时也鞭策着小七要严谨做事,不能马虎大意!





本文内容包含图片或附件,获取更多资讯,请 登录 后查看;或者 注册 成为会员获得更多权限
本帖最近打赏记录:共32条打赏 ,M币+188 ,专家+2
离线linuxyouxia
发帖
59
M币
139
专家
1
粉丝
6
只看该作者 1楼 发表于: 2013-01-28
请登录后查看
本帖最近打赏记录:共2条打赏 ,M币+11
离线q562379863

发帖
4364
M币
8195
专家
37
粉丝
212
只看该作者 2楼 发表于: 2013-01-29
请登录后查看
离线医生

发帖
601
M币
1765
专家
1
粉丝
35
只看该作者 3楼 发表于: 2013-01-29
请登录后查看
离线baiwenlongba

发帖
2126
M币
6458
专家
9
粉丝
85
只看该作者 4楼 发表于: 2013-01-29
请登录后查看
离线土豆哥

发帖
701
M币
2227
专家
7
粉丝
48
只看该作者 5楼 发表于: 2013-01-29
请登录后查看
离线finallyan
发帖
216
M币
142
专家
0
粉丝
13
只看该作者 6楼 发表于: 2013-01-29
请登录后查看
离线ntwxd

发帖
218
M币
1014
专家
2
粉丝
18
只看该作者 7楼 发表于: 2013-01-29
请登录后查看
离线xou822

发帖
477
M币
3770
专家
2
粉丝
15
只看该作者 8楼 发表于: 2013-01-29
请登录后查看
离线djlemm

发帖
214
M币
331
专家
1
粉丝
17
只看该作者 9楼 发表于: 2013-01-30
请登录后查看
快速回复
限80 字节
“新手上路”发帖需审核后才能显示(请认真发帖),达到数码9级后取消此限制
 
上一个 下一个