[51单片机] 从单片机基础到程序框架(连载)

[复制链接]
355609|720
 楼主| jianhong_wu 发表于 2017-9-4 19:36 | 显示全部楼层
第八十五节: 定时中断的寄存器配置。

【85.1   寄存器配置的本质。】

        单片机内部可供我们选择的资源非常丰富,有定时器,有串口,有外部中断,等等。这些丰富的资源,就像你进入一家超市,你只需选择你所需要的东西就可以了,所以配置寄存器的关键在于选择,所谓选择就是往寄存器里面做填空题,单片机系统内部再根据你的“选择清单”,去启动对应的资源。那么我们怎么知道某个型号的单片机内部有哪些资源呢?看该型号“单片机的说明书”呀,“单片机的说明书”就是我们通常所说的“芯片的datasheet”,或者说是“芯片的数据手册”,这些资料单片机厂家会提供的。
        跟单片机打交道,其实跟人打交道没什么区别,你要让单片机按照你的“意愿”走,你首先要把你的“意愿”表达清楚,这个“意愿”就是信息,信息要具备准确性和唯一性,不能模凌两可。比如,现在要让单片机“每1ms产生一次中断”,你想想你可能需要给单片机提供哪些信息?
      (1)51单片机有2个定时器,一个是0号定时器,一个是1号定时器,我们要面临“二选一”的选择,本例子中用的是“0号定时器”。
      (2)0号定时器内部又有4种工作方式:方式0,方式1,方式2,方式3,本例子中用的是“方式1”。
      (3)定时器到底多长时间中断一次,这就涉及到填充与中断时间有关的寄存器的数值,该数值是跟时间成比例关系,本例子中配置的是1ms中断,就要填充对应的数值。
      (4)默认状态下,定时器是不会被开启的,如果要开启,这里就是涉及到定时器的“开关”,本例子要开启此开关。
      (5)定时器时间到了就要产生中断,中断也有“总开关”和“定时器的局部开关”,这两个开关都必须同时打开,中断才会有效。
        要配置定时器“每1ms产生一次中断”,大概就上述这些信息,根据这些信息提示,下面开始讲解一下寄存器的具体内容。

【85.2  定时器/计数器的模式控制寄存器TMOD。】

        寄存器TMOD是一个8位的特殊变量,里面每一位都代表了不同的功能选择。根据芯片的说明书,TMOD的8位从左到右依次对应从D7到D0(左高位,右低位),定义如下:

        GATE    C/T    M1     M0    GATE    C/T    M1    M0

        仔细观察,发现左4位与右4位是对称的,分别都是“GATE,C/T , M1 , M0”,左4位控制的是“定时器1”,右4位控制的是“定时器0”,因为本例子用的是“定时器0”,因此“定时器1”的左4位都设置为0的默认数值,我们只需重点关注右4位的“定时器0”即可。
        GATE:定时器是否受“其它外部开关”的影响的标志位。定时器的开启或者停止,受到两个开关的影响,第一个开关是“自身原配开关”,第二个开关是“其它外部开关”。GATE取1代表定时器受“其它外部开关”的影响,取0代表定时器不受“其它外部开关”的影响。本例子中,定时器只受到“自身原配开关”的影响,而不受到“其它外部开关”的影响,因此,GATE取0。
        C/T:定时器有两种模式,该位取1代表“计数器模式”,取0代表“定时器模式”。本例子是“定时器模式”,因此,C/T取0。
        M1与M0:工作方式的选择。M1与M0这两位的01搭配,可以有4种组合(00,01,10,11),每一种组合就代表一种工作方式。本例子选用“方式1”,因此M1与M0取“01”的组合。
        综上所述,TMOD的配置代码是:TMOD=0x01;

【85.3  决定时间长度的寄存器TH0与TL0。】

        TH0与TL0,T代表定时器英文单词TIME的T,H代表高位,L代表低位,0代表定时器0。
        TH0是一个8位宽度的寄存器,TL0也是一个8位宽度的寄存器,两者合并起来成为一个整体,实际上就是一个16位宽度的寄存器,TH0是高8位,TL0是低8位,它们合并后的数值范围是:0到65535。该16位寄存器取值越大,定时中断一次的时间反倒越小,为什么?TH0与TL0的初始值,就像一个水桶里装的水。如果这个桶是空桶(取值为0),“雨水”想把这个桶“滴满溢出”所需要的时间就很大。如果里面已经装了大半的水(取值为大于32767),“雨水”想把这个桶“滴满溢出”所需要的时间就很小。这里的关键词“滴满溢出”的“滴”与“满溢出”,“滴”的速度是由单片机晶振决定的,而“满溢出”一次就代表产生一次中断,执行完中断函数在即将返回主函数之前,我们重新装入特定容量的水(重装初值),为下一次的“滴满溢出”做准备,依次循环,从而连续不断地产生间歇的定时中断。
         配置中断时间的大小是需要经验的,因为,每次定时中断的时间太长,就意味着时间的可分度太粗,而如果每次定时中断的时间太短,则会产生很频繁的中断,势必会影响主函数main()的执行效率,而且累记中断次数的时间误差也会增大。因此,配置中断时间是需要经验的,根据经验,定时中断取1ms一次,是几乎所有单片机项目的最佳选择,按我的理解,“1ms定时中断一次”已经是单片机界公认的一种“标配”。
         要配置1ms定时中断,TH0与TL0如何取值?刚才提到一个形象的例子“桶,滴,满溢出”。TH0与TL0的最大取值范围是65535,可以理解成为最大65535“滴”,如果超过65535“滴”(比如加1“滴”后变成65536“滴”)就会“满溢出”,从而产生一次中断(65536是中断发生的临界值)。而“滴一次的时间”就刚好是单片机执行“一次单指令的时间”,“一次单指令的时间”等于12个晶振周期,比如12MHz的晶振,晶振周期是(1/12000000)秒,而“一次单指令的时间”就等于12乘以(1/12000000)秒,等于0.000001秒,也就是1us。1us“滴”一次,要产生1ms的时间就需要“滴”1000次。“满溢出”的前提条件是“桶里”一共需要装入65536滴才溢出,因此,在12MHz的晶振下要产生1ms的定时中断,TH0与TL0的初值应该是64536(65536减去1000等于64536),而64536变成十六进制0xfc17,再分解到高8位TH0为0xfc,低8位TL0为0x17。
         刚才的例子是假如晶振在12MHz的情况下所计算出来的结果,而本教程所用的晶振是11.0592MHz,根据11.0592MHz产生1ms的定时中断,TH0与TL0应该取值多少?根据刚才的计算方式:   

  1. 初值=[溢出值]-([0.001秒]/([晶振周期的12个]*([1秒]/[晶振频率])))
  2. 初值=65536-(0.001/(12*(1/11059200)))
  3. 初值=65536-922      (注:922是921.6的四舍五入)
  4. 初值=64614
  5. 初值=64614
  6. 初值=0xfc66
  7. 初值TH0=0xfc
  8. 初值TL0=0x66



【85.4  中断的总开关EA与局部开关ET0。】

         EA:中断的总开关。宽度是1位的位变量。此开关如果取0,就会强行屏蔽所有的中断,因此,只要用到中断,此开关必须取1。
         ET0:专门针对定时器0中断的局部开关。宽度是1位的位变量。此开关如果取0,则会屏蔽定时器0的中断,如果取1则允许定时器0中断。如果要定时器0能产生中断,那么总开关EA与ET0必须同时都打开(都取1),两者缺一不可。

【85.5  定时器0的“自身原配开关”TR0。】

         TR0:定时器的“自身原配开关”。宽度是1位的位变量。很多初学者会把EA,ET0,TR0三者搞不清。定时器可以工作在“查询标志位”和“中断”这两种状态,也就是说在没有中断的情况下定时器也可以单独使用的。TR0是定时器0自身的发动引擎,要不要把这个发动引擎所产生的能量传输到中断的渠道,则取决于中断开关EA和ET0。TR0是源头开关,EA是中断总渠道开关,ET0是中断分支渠道的定时器0开关。TR0取1表示启动定时器0,取0表示关闭定时器0。

【85.6  定时器0的中断函数的书写格式。】

  1. void 函数名() interrupt 1
  2. {
  3.        ...中断程序内容;
  4.        ...此处省去若干代码
  5.        ...中断程序内容;
  6.        ...最后面的代码,要记得重装TH0与TL0的初值;
  7. }

         函数名可以随便取,只要不是编译器已经征用的关键字。这里的1是定时器0的中断号。不同的中断号代表不同类型的中断,至于哪类中断对应哪个中断号,大家可以查找相关书籍和资料。本节用的定时器0处于工作方式1的情况下,在即将退出中断之前,需要重装TH0与TL0的初始值。

【85.7  寄存器的名字来源。】

         前面讲的寄存器都有固定的名字,而且这些名字都是唯一的,拼写的时候少一个字母或者多一个字母,C编译器都会报错不让你通过,因此问题来了,初学者刚接触一款单片机的时候,如何知道某个寄存器它特定的唯一的名字?有两个来源。
         第一个来源,可以打开C编译器的某个头文件(.h格式)查看这些寄存器的名字。比如51单片机可以查看REG52.H这个头文件。如何打开REG52.H这个文件?在keil源代码编辑器界面下,选中上面REG52.H这几个字符,在右键弹出的菜单下点击Open ducument“REG52.H”即可。
         第二个来源是直接参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。

【85.8  如何快速配置寄存器。】

         建议一边阅读芯片的数据手册,一边参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。

【85.9  练习例程。】

         现在编写一个定时中断程序,让两个LED灯闪烁,一个是在主函数里用累计主循环次数的方式实现(P0.0控制),另一个是在定时中断函数里用累计定时中断次数的方式实现(P0.1控制)。这两个闪烁的LED灯,一个在main函数,一个是在中断函数,两路任务互不干涉独立运行,并行处理的“雏形”略显出来。

         
         图85.9.1  灌入式驱动8个LED

  1. #include "REG52.H"  

  2. #define  CYCLE_SUM   5000   //主循环的次数

  3. #define  INTERRUPT_SUM   500   //中断的次数

  4. sbit P0_0=P0^0;  //在主循环里的LED灯
  5. sbit P0_1=P0^1;  //在定时中断里的LED灯

  6. unsigned char Gu8CycleStep=0;
  7. unsigned long Gu32CycleCnt=0;      //累计主循环的计数器

  8. unsigned char Gu8InterruptStep=0;
  9. unsigned long Gu32InterruptCnt=0;  //累计定时中断次数的计数器

  10. void main()
  11. {
  12. TMOD=0x01;  //设置定时器0为工作方式1
  13. TH0=0xfc;   //产生1ms中断的TH0初始值
  14. TL0=0x66;   //产生1ms中断的TL0初始值
  15. EA=1;       //开总中断
  16. ET0=1;      //允许定时0的中断
  17. TR0=1;      //启动定时0的中断

  18.     while(1)  //主循环
  19.     {  
  20.        switch(Gu8CycleStep)
  21.        {
  22.            case 0:
  23.                Gu32CycleCnt++;   
  24.                if(Gu32CycleCnt>=CYCLE_SUM)
  25. {
  26. Gu32CycleCnt=0;     
  27.                     P0_0=0;  //主循环的LED灯亮。
  28. Gu8CycleStep=1;  
  29. }
  30.                break;

  31.            case 1:
  32.                Gu32CycleCnt++;   
  33.                if(Gu32CycleCnt>=CYCLE_SUM)
  34. {
  35. Gu32CycleCnt=0;     
  36.                     P0_0=1;   //主循环的LED灯灭。
  37. Gu8CycleStep=0;
  38. }
  39.                break;
  40. }
  41.    }
  42. }


  43. void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
  44. {
  45.        switch(Gu8InterruptStep)
  46.        {
  47.            case 0:
  48.                Gu32InterruptCnt++;   //累计中断次数的次数
  49.                if(Gu32InterruptCnt>=INTERRUPT_SUM) //次数达到设定值就跳到下一步骤
  50. {
  51. Gu32InterruptCnt=0;     //及时清零计数器,为下一步骤的新一轮计数准备
  52.                     P0_1=0;  //定时中断的LED灯亮。
  53. Gu8InterruptStep=1;  //跳到下一步骤
  54. }
  55.                break;

  56.            case 1:
  57.                Gu32InterruptCnt++;   //累计中断次数的次数
  58.                if(Gu32InterruptCnt>=INTERRUPT_SUM)  //次数达到设定值就返回上一步骤
  59. {
  60. Gu32InterruptCnt=0;     //及时清零计数器,为返回上一步骤的新一轮计数准备
  61.                     P0_1=1;  //定时中断的LED灯灭。
  62. Gu8InterruptStep=0;  //返回到上一个步骤
  63. }
  64.                break;
  65. }

  66. TH0=0xfc;   //重装初值,不能忘。
  67. TL0=0x66;   //重装初值,不能忘。
  68. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
554867362 发表于 2017-9-5 17:40 | 显示全部楼层
顶楼主,真是豪言壮语,真性人也。希望楼主**自己的梦想,**自己的豪言壮语,支持你!
Linkxia 发表于 2017-9-6 14:24 | 显示全部楼层
大赞楼主!
刺猬D优雅 发表于 2017-9-6 17:04 | 显示全部楼层
享受福利
 楼主| jianhong_wu 发表于 2017-9-10 10:55 | 显示全部楼层
第八十六节: 定时中断的“非阻塞”延时控制LED闪烁。

【86.1   定时中断应用的四大关键词。】

       本节主要内容有四大个关键词:1ms,互斥量,volatile,switch。
      (1)1ms。把定时中断设置为1ms中断一次,几乎是单片机界公认的“标配”。这个1 ms是系统时间的节拍来源,有了1ms“标配”意识,你的程序在不同单片机平台上移植的时候会得心应手运用自如。
      (2)互斥量。“主函数”与“定时中断函数”,本质上是两个独立进程在不断切换并行运行,两个进程之间不断切换,就会涉及到数据的安全保护,数据的安全保护主要是针对多字节的变量,比如int类型(2个字节),long类型(4个字节)。但是单字节的char变量不用额外保护,因为“字节”是变量中的最小单位(在不考虑“位”的情况下),这里的“最小单位不可分”就像“原子是最小单位不可分”一样,因此也有很多前辈把“互斥量”称为“原子锁”。为什么要用互斥量?因为,在多个线程同时访问同一个全局变量的时候,如果双方都是“读操作”,则不会出现问题,但是,如果双方都是“既有写操作也有读操作”的情况下,比如,我在主函数里正在修改(写操作)一个unsigned int类型的变量,unsigned int类型的变量占用2个字节,在更改数据的时候至少需要2条指令,当我刚执行完第1条指令还没来得及执行第2指令的时候,突然来了一个定时中断,并且在定时中断函数里也对这个变量进行了修改(写操作)并且还进行了读取判断操作,这个瞬间就可能给程序带来了隐患。话说回来,互斥量到底有没有必要,其实还是有点争议的,我曾经为这个问题纠结过很久,毕竟,如果不用互斥量,这么微观的隐患到底存不存在,目前很难做一个“让故障重现”的实验去证明,最后,我是本着“宁可信其有不可信其无”的态度,把互斥量应用在了我的工作中。
      (3)volatile。volatile是一个前缀的修饰关键词,也是用来保护主函数与中断函数共用的全局变量的,只不过,volatile是针对C编译器的,预防“C编译器在优化代码的时候误伤一些重要的共享数据”,就像预防杀毒软软件用力过猛把一些合法软件当作病毒而误杀。加了volatile修饰的全局变量,就能提醒C编译器不要对这类特殊变量擅作主张去优化。
      (4)switch。switch是“非阻塞程序框架”的核心语句,在以switch为核心的框架下,进行不同步骤之间的程序跳转,是做大型裸机程序的常态。

【86.2  主函数与定时中断函数的程序框架。】

        主函数与定时中断函数之间相互配合,主函数负责做什么,中断函数负责做什么,对于初学者来说可能是一头雾水,但是对于像我这种在单片机界深耕多年即将修炼成精的工程师来说,我心中是有很清晰的模板和套路的,这种模板和套路是经过多年沉淀下来的经验。比如,定时中断函数尽量放一些精简的计时器代码,一般不调用函数,但是“输入IO口的消抖动”(按键扫描)以及“蜂鸣器鸣叫”这两类特殊函数我是喜欢破例放在定时中断函数里调用的。定时中断如何产生时间,这个时间如何跟主函数关联起来,请看下面的框架代码:


  1. volatile unsigned char vGu8TimeFlag=0;  //互斥量变量标志
  2. volatile unsigned int vGu16TimeCnt=0;  //计时器变量

  3. void main()
  4. {
  5. vGu8TimeFlag=0;  //在“写操作”vGu16TimeCnt全局变量之前,互斥量vGu8TimeFlag的“加锁”
  6. vGu16TimeCnt=1000;  //全局变量的赋值,就是“写操作”
  7. vGu8TimeFlag=1; //互斥量vGu8TimeFlag的“解锁”。同时也起到“启动计时器”的开关作用

  8.     while(1)  //主循环
  9. {  
  10.      if(0==vGu16TimeCnt)  //时间变量为0则表示时间到了
  11.      {
  12.           ...在这里执行具体的功能代码
  13. }
  14.    }
  15. }


  16. void T0_time() interrupt 1    //每1ms中断一次的定时中断函数
  17. {
  18. if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断。
  19. {
  20. vGu16TimeCnt--;  //“自减一”的操作
  21. }

  22. }


        分析:上述代码中,vGu8TimeFlag是一箭双雕,既起到互斥量的作用,也起到了计数器vGu16TimeCnt开始计时的启动开关作用。

【86.3  练习例程。】

        现在根据上述程序框架,编写一个LED灯闪烁的程序。

     
     图86.3.1  灌入式驱动8个LED

  1. #include "REG52.H"  

  2. #define  BLINK_TIME   500   //时间是500ms

  3. sbit P0_0=P0^0;

  4. volatile unsigned char vGu8TimeFlag=0;  //互斥量变量标志
  5. volatile unsigned int vGu16TimeCnt=0;  //计时器变量

  6. unsigned char Gu8Step=0;  //switch的切换步骤
  7. void main()
  8. {
  9. TMOD=0x01;  //设置定时器0为工作方式1
  10. TH0=0xfc;   //产生1ms中断的TH0初始值
  11. TL0=0x66;   //产生1ms中断的TL0初始值
  12. EA=1;       //开总中断
  13. ET0=1;      //允许定时0的中断
  14. TR0=1;      //启动定时0的中断


  15.     while(1)  //主循环
  16.     {  
  17.        switch(Gu8Step)
  18.        {
  19.            case 0:
  20.                if(0==vGu16TimeCnt)  //时间到
  21.                {
  22.                     P0_0=0;   //LED灯亮
  23. vGu8TimeFlag=0;  //互斥量“加锁”
  24. vGu16TimeCnt=BLINK_TIME;  //计时器的写操作。设定计时的长度
  25. vGu8TimeFlag=1;  //互斥量“解锁”,同时蕴含了计时器“启动”的动作

  26. Gu8Step=1;  //切换到case 1这个步骤
  27. }
  28.                break;

  29.            case 1:

  30.               if(0==vGu16TimeCnt)  //时间到
  31.                {
  32.                     P0_0=1;   //LED灯灭。
  33. vGu8TimeFlag=0;  //互斥量“加锁”
  34. vGu16TimeCnt=BLINK_TIME;  //计时器的写操作。设定计时的长度
  35. vGu8TimeFlag=1;  //互斥量“解锁”,同时蕴含了计时器“启动”的动作

  36. Gu8Step=0;  //切换到case 0这个步骤,依次循环
  37. }
  38.                break;
  39. }
  40.    }
  41. }


  42. void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
  43. {
  44. if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断
  45. {
  46. vGu16TimeCnt--;  //“自减一”的操作
  47. }

  48. TH0=0xfc;   //重装初值,不能忘
  49. TL0=0x66;   //重装初值,不能忘
  50. }



【86.4  解决闪烁出现不规则“非对称感”现象的方法。】

       上述例子,实验现象应该是LED闪烁很有规则的每1s闪烁一次,但是也有一部分初学者可能会遇到闪烁出现不规则“非对称感”的现象,这个问题的解决办法如下:在keil2的project下拉菜单下,选择Options for Target选项,弹出的窗口中,切换到Target选项,在Memory Model选项中选择small:variables in Data。




                   图86.4.1  设置窗口

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

评论

楼主厉害呀,如此细致的讲解让人很受用  发表于 2021-10-28 16:13
xmgh1208 发表于 2017-9-12 10:56 | 显示全部楼层
感谢楼主,学习了~
wangxueq 发表于 2017-9-17 20:07 | 显示全部楼层
楼主写的挺好
szsfy 发表于 2017-9-17 22:37 来自手机 | 显示全部楼层
chenyongand 发表于 2017-9-18 08:59 | 显示全部楼层
看了目录不错,但是要等到2020年,这个太久了
llggll1234 发表于 2017-9-18 13:00 | 显示全部楼层
感谢楼主
 楼主| jianhong_wu 发表于 2017-9-19 10:07 | 显示全部楼层
第八十七节: 一个定时中断产生N个软件定时器。

【87.1   信手拈来的软件定时器。】

        初学者会疑惑,51单片机只有2个定时器T0和T1,是不是太少了一点?2个定时器怎能满足实际项目的需要,很多项目涉及到的定时器往往十几个,怎么办?这个问题的奥秘就在本节的内容。
        51单片机内置的2个定时器T0和T1,是属于硬件定时器,硬件定时器是一个母体,它可以孕育出N个软件定时器,实际项目中,我们需要多少个定时器只需要从同一个硬件定时器中断里构造出对应数量的软件定时器即可。构造N个软件定时器的框架如下:

  1. //“软件定时器1”的相关变量
  2. volatile unsigned char vGu8TimeFlag_1=0;  
  3. volatile unsigned int vGu16TimeCnt_1=0;   

  4. //“软件定时器2”的相关变量
  5. volatile unsigned char vGu8TimeFlag_2=0;  
  6. volatile unsigned int vGu16TimeCnt_2=0;  

  7. //“软件定时器3”的相关变量
  8. volatile unsigned char vGu8TimeFlag_3=0;  
  9. volatile unsigned int vGu16TimeCnt_3=0;  

  10. void main()
  11. {
  12. vGu8TimeFlag_1=0;  
  13. vGu16TimeCnt_1=1000;  //“软件定时器1”的定时时间是1000ms
  14. vGu8TimeFlag_1=1;

  15. vGu8TimeFlag_2=0;  
  16. vGu16TimeCnt_2=500;  //“软件定时器2”的定时时间是500ms
  17. vGu8TimeFlag_2=1;

  18. vGu8TimeFlag_3=0;  
  19. vGu16TimeCnt_3=250;  //“软件定时器3”的定时时间是250ms
  20. vGu8TimeFlag_3=1;

  21.     while(1)  //主循环
  22. {  
  23.      if(0==vGu16TimeCnt_1)  //“软件定时器1”的时间到了
  24.      {
  25.           ...在这里执行具体的功能代码
  26. }

  27.      if(0==vGu16TimeCnt_2)  //“软件定时器2”的时间到了
  28.      {
  29.           ...在这里执行具体的功能代码
  30. }

  31.      if(0==vGu16TimeCnt_3  //“软件定时器3”的时间到了
  32.      {
  33.           ...在这里执行具体的功能代码
  34. }

  35.    }
  36. }

  37. void T0_time() interrupt 1    //每1ms中断一次的定时中断函数
  38. {
  39. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //在定时中断里衍生出“软件定时器1”
  40. {
  41. vGu16TimeCnt_1--;  
  42. }

  43. if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //在定时中断里衍生出“软件定时器2”
  44. {
  45. vGu16TimeCnt_2--;  
  46. }

  47. if(1==vGu8TimeFlag_3&&vGu16TimeCnt_3>0) //在定时中断里衍生出“软件定时器3”
  48. {
  49. vGu16TimeCnt_3--;  
  50. }

  51. //按上面的套路继续写,可以衍生出N个“软件定时器”,只要不超过单片机的RAM和ROM。
  52. }



【87.2  练习例程。】

       现在根据上述程序框架,编写3个LED灯闪烁的程序。第1个LED灯的一闪一灭的周期是2秒,第2个LED灯的一闪一灭的周期是1秒,第3个LED灯一闪一灭的周期是0.5秒。这3个灯的闪烁频率是不一样的,因此需要3个软件定时器。该例子其实也是一个多任务并行处理的典型例子,这3个LED灯就代表3个不同的任务,它们之间是通过switch这个关键语句进行多任务并行处理的。switch的精髓在于根据某个特定条件切换到对应的步骤(或称“跳转到对应的步骤”)。


       图87.2.1  灌入式驱动8个LED

  1. #include "REG52.H"  

  2. #define  BLINK_TIME_1   1000   //时间是1000ms
  3. #define  BLINK_TIME_2   500    //时间是500ms
  4. #define  BLINK_TIME_3   250    //时间是250ms

  5. sbit P0_0=P0^0;
  6. sbit P0_1=P0^1;
  7. sbit P0_2=P0^2;

  8. //“软件定时器1”的相关变量
  9. volatile unsigned char vGu8TimeFlag_1=0;  
  10. volatile unsigned int vGu16TimeCnt_1=0;   

  11. //“软件定时器2”的相关变量
  12. volatile unsigned char vGu8TimeFlag_2=0;  
  13. volatile unsigned int vGu16TimeCnt_2=0;  

  14. //“软件定时器3”的相关变量
  15. volatile unsigned char vGu8TimeFlag_3=0;  
  16. volatile unsigned int vGu16TimeCnt_3=0;  

  17. unsigned char Gu8Step_1=0;  //软件定时器1的switch切换步骤
  18. unsigned char Gu8Step_2=0;  //软件定时器2的switch切换步骤
  19. unsigned char Gu8Step_3=0;  //软件定时器3的switch切换步骤

  20. void main()
  21. {
  22. TMOD=0x01;  //设置定时器0为工作方式1
  23. TH0=0xfc;   //产生1ms中断的TH0初始值
  24. TL0=0x66;   //产生1ms中断的TL0初始值
  25. EA=1;       //开总中断
  26. ET0=1;      //允许定时0的中断
  27. TR0=1;      //启动定时0的中断


  28.     while(1)  //主循环
  29. {  

  30.   //软件定时器1控制的LED灯闪烁
  31.        switch(Gu8Step_1)
  32.        {
  33.            case 0:
  34.                if(0==vGu16TimeCnt_1)  
  35.                {
  36.                     P0_0=0;   
  37. vGu8TimeFlag_1=0;  
  38. vGu16TimeCnt_1=BLINK_TIME_1;  
  39. vGu8TimeFlag_1=1;  

  40. Gu8Step_1=1;  
  41. }
  42.                break;

  43.            case 1:

  44.               if(0==vGu16TimeCnt_1)  
  45.                {
  46.                     P0_0=1;   
  47. vGu8TimeFlag_1=0;  
  48. vGu16TimeCnt_1=BLINK_TIME_1;  
  49. vGu8TimeFlag_1=1;  

  50. Gu8Step_1=0;  
  51. }
  52.                break;
  53. }

  54.   //软件定时器2控制的LED灯闪烁
  55.        switch(Gu8Step_2)
  56.        {
  57.            case 0:
  58.                if(0==vGu16TimeCnt_2)  
  59.                {
  60.                     P0_1=0;   
  61. vGu8TimeFlag_2=0;  
  62. vGu16TimeCnt_2=BLINK_TIME_2;  
  63. vGu8TimeFlag_2=1;  

  64. Gu8Step_2=1;  
  65. }
  66.                break;

  67.            case 1:

  68.               if(0==vGu16TimeCnt_2)  
  69.                {
  70.                     P0_1=1;   
  71. vGu8TimeFlag_2=0;  
  72. vGu16TimeCnt_2=BLINK_TIME_2;  
  73. vGu8TimeFlag_2=1;  

  74. Gu8Step_2=0;  
  75. }
  76.                break;
  77. }

  78.   //软件定时器3控制的LED灯闪烁
  79.        switch(Gu8Step_3)
  80.        {
  81.            case 0:
  82.                if(0==vGu16TimeCnt_3)  
  83.                {
  84.                     P0_2=0;   
  85. vGu8TimeFlag_3=0;  
  86. vGu16TimeCnt_3=BLINK_TIME_3;  
  87. vGu8TimeFlag_3=1;  

  88. Gu8Step_3=1;  
  89. }
  90.                break;

  91.            case 1:

  92.               if(0==vGu16TimeCnt_3)  
  93.                {
  94.                     P0_2=1;   
  95. vGu8TimeFlag_3=0;  
  96. vGu16TimeCnt_3=BLINK_TIME_3;  
  97. vGu8TimeFlag_3=1;  

  98. Gu8Step_3=0;  
  99. }
  100.                break;
  101. }

  102.    }
  103. }


  104. void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
  105. {
  106. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //在定时中断里衍生出“软件定时器1”
  107. {
  108. vGu16TimeCnt_1--;  
  109. }

  110. if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //在定时中断里衍生出“软件定时器2”
  111. {
  112. vGu16TimeCnt_2--;  
  113. }

  114. if(1==vGu8TimeFlag_3&&vGu16TimeCnt_3>0) //在定时中断里衍生出“软件定时器3”
  115. {
  116. vGu16TimeCnt_3--;  
  117. }


  118. TH0=0xfc;   //重装初值,不能忘
  119. TL0=0x66;   //重装初值,不能忘
  120. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| jianhong_wu 发表于 2017-9-24 10:26 | 显示全部楼层
第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)。

【88.1   四区一线。】

       提出“四区一线”理论,主要方便初学者理解单片机程序大概的“空间分区”。
       “四区”代表四大主流函数,分别是:系统初始化函数,外设初始化函数,主程序的任务函数,定时中断函数。
       “一线”是指“系统初始化函数”与“外设初始化函数”的“分割线”,这个“分割线”是一个delay的延时函数。
       “四区一线”的布局如下:

  1. void main()
  2. {
  3. SystemInitial();            //“四区一线”的“第一区”
  4. Delay(10000);               //“四区一线”的“一线”
  5. PeripheralInitial();        //“四区一线”的“第二区”
  6.     while(1)  //主循环
  7. {  
  8. LedService();           //“四区一线”的“第三区”
  9. KeyService();           //“四区一线”的“第三区”
  10. UsartService();         //“四区一线”的“第三区”
  11. ...                     //凡是在主循环里的函数都是属于“第三区”
  12.    }
  13. }

  14. void T0_time() interrupt 1      //“四区一线”的“第四区”
  15. {

  16. }


       “第一区”的函数SystemInitial(),是一个系统的初始化函数,专门用来初始化单片机自己的寄存器以及个别外围要求响应速度快的输出设备,防止刚上电之后,由于输出IO口电平状态不确定而导致外围设备误动作,比如驱动继电器的误动作等等。
       “一线”的函数Delay(10000),是一个延时函数,为什么这里要插入一个延时函数?主要目的是为接下来的PeripheralInitial()做准备的。上电后先延时一段时间,再执行PeripheralInitial()函数,因为PeripheralInitial()函数专门用来初始化不要求上电立即处理的外设芯片和模块。比如液晶模块,AT24C02存储芯片,DS1302时钟芯片,等等。这些芯片在上电的瞬间,内部自身的复位需要一点时间,以及外部电压稳定也需要一点时间,只有过了这一点时间,这些芯片才处于正常的工作状态,这个时候单片机才能跟它正常通信,所以“一线”函数Delay(10000)的意义就在这里。
       “第二区”的函数PeripheralInitial(),是一个外设的初始化函数。专门用来初始化不要求上电立即处理的外设芯片和模块。
       “第三区”的函数LedService(),KeyService(),UsartService(),等等,是一些在主循环里不断扫描的任务函数。
       “第四区”的函数void T0_time() interrupt 1,是一个定时中断函数,一个系统必须标配一个定时中断函数才算完美齐全,这个中断函数提供系统的节拍时间,以及处理扫描一些跟IO口消抖动相关的函数,以及跟蜂鸣器驱动相关的函数。

【88.2   switch外加定时中断。】

        提出“switch外加定时中断”理论,主要方便初学者理解单片机程序大概的“逻辑框架”。
        switch是一个万能语句,它外加while与for循环就可以做任何复杂的算法,比如,搜索算法,运动算法,提取关键词算法,等等。它外加定时中断,就可以搭建一个系统的基本框架。比如,做通信的程序框架,人机界面的程序框架,按键服务的程序框架,等等。switch的精髓在于“根据条件进行步骤的灵活切换”。具体内容请看本节的练习程序。

【88.3  练习例程。】

        根据上述的两大核心框架理论,编写1个LED灯闪烁的程序。


     图88.3.1  灌入式驱动8个LED

  1. #include "REG52.H"  

  2. void T0_time();
  3. void SystemInitial(void) ;
  4. void Delay(unsigned long u32DelayTime) ;
  5. void PeripheralInitial(void) ;
  6. void LedService(void);

  7. #define  BLINK_TIME_1   1000  

  8. sbit P0_0=P0^0;

  9. volatile unsigned char vGu8TimeFlag_1=0;  
  10. volatile unsigned int vGu16TimeCnt_1=0;   

  11. void main()
  12. {
  13. SystemInitial();            //“四区一线”的“第一区”
  14. Delay(10000);               //“四区一线”的“一线”
  15. PeripheralInitial();        //“四区一线”的“第二区”
  16.     while(1)  //主循环
  17. {  
  18. LedService();           //“四区一线”的“第三区”
  19.    }
  20. }

  21. void T0_time() interrupt 1     //“四区一线”的“第四区”
  22. {
  23. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0)
  24. {
  25. vGu16TimeCnt_1--;  
  26. }

  27. TH0=0xfc;   
  28. TL0=0x66;   
  29. }


  30. void SystemInitial(void)
  31. {
  32. TMOD=0x01;  
  33. TH0=0xfc;   
  34. TL0=0x66;   
  35. EA=1;      
  36. ET0=1;      
  37. TR0=1;      
  38. }

  39. void Delay(unsigned long u32DelayTime)
  40. {
  41.     for(;u32DelayTime>0;u32DelayTime--);
  42. }

  43. void PeripheralInitial(void)
  44. {

  45. }

  46. void LedService(void)
  47. {
  48. static unsigned char Su8Step=0; //加static修饰的局部变量,每次进来都会保留上一次值。

  49.        switch(Su8Step)
  50.        {
  51.            case 0:
  52.                if(0==vGu16TimeCnt_1)  //时间到
  53.                {
  54.                     P0_0=0;   
  55. vGu8TimeFlag_1=0;  
  56. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  57. vGu8TimeFlag_1=1;  

  58. Su8Step=1;  //切换到下一个步骤,精髓语句!
  59. }
  60.                break;

  61.            case 1:

  62.               if(0==vGu16TimeCnt_1)     //时间到
  63.                {
  64.                     P0_0=1;   
  65. vGu8TimeFlag_1=0;  
  66. vGu16TimeCnt_1=BLINK_TIME_1;  //重装定时的时间
  67. vGu8TimeFlag_1=1;  

  68. Su8Step=0;   //返回到上一个步骤,精髓语句!
  69. }
  70.                break;
  71. }

  72. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
chen472015439 发表于 2017-9-24 17:33 | 显示全部楼层
真诚感谢吴老师,现在社会像您这样的奉献精神真的是太少了
 楼主| jianhong_wu 发表于 2017-10-2 11:54 | 显示全部楼层
第八十九节: 跑马灯的三种境界。

【89.1   跑马灯的三种境界。】

        跑马灯也称为流水灯,排列的几个LED依次循环的点亮和熄灭,给人“跑动起来”的感觉,故称为“跑马灯”。实现跑马灯的效果,编程上有三种思路,分别代表了跑马灯的三种境界,分别是:移位阻塞,移位非阻塞,状态切换非阻塞。


       图89.1.1  灌入式驱动8个LED

       本节用的是8个LED灯依次挨个熄灭点亮,如上图所示。

【89.2   移位阻塞。】

       移位阻塞,“移位”用的是C语言的左移或者右移语句,“阻塞”用的是delay延时。代码如下:

  1. #include "REG52.H"  

  2. void T0_time();
  3. void SystemInitial(void) ;
  4. void Delay(unsigned long u32DelayTime) ;
  5. void PeripheralInitial(void) ;
  6. void LedTask(void);

  7. void main()
  8. {
  9. SystemInitial();            
  10. Delay(10000);               
  11. PeripheralInitial();        
  12.     while(1)  
  13. {  
  14. LedTask();   
  15.     }
  16. }

  17. void T0_time() interrupt 1     
  18. {

  19. TH0=0xfc;   
  20. TL0=0x66;   
  21. }


  22. void SystemInitial(void)
  23. {
  24. TMOD=0x01;  
  25. TH0=0xfc;   
  26. TL0=0x66;   
  27. EA=1;      
  28. ET0=1;      
  29. TR0=1;      
  30. }

  31. void Delay(unsigned long u32DelayTime)
  32. {
  33.     for(;u32DelayTime>0;u32DelayTime--);
  34. }

  35. void PeripheralInitial(void)
  36. {

  37. }

  38. //跑马灯的任务程序   
  39. void LedTask(void)
  40. {
  41. static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
  42. static unsigned char Su8Cnt=0;     //加static修饰的局部变量,每次进来都会保留上一次值。

  43. P0=Su8Data;   //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
  44. Delay(10000) ; //阻塞延时
  45. Su8Data=Su8Data<<1;  //左移一位
  46. Su8Cnt++; //计数器累加1
  47. if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
  48. {
  49. Su8Cnt=0;  
  50. Su8Data=0x01;  //重新赋初值,继续下一次循环移动
  51. }   
  52. }


        分析总结:这是第1种境界的跑马灯,这种思路虽然实现了跑马灯的效果,但是因为“阻塞延时”,整个程序显得僵硬机械,缺乏多任务并行的框架。

【89.3   移位非阻塞。】

        移位非阻塞,“移位”用的是C语言的左移或者右移语句,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:

  1. #include "REG52.H"  

  2. void T0_time();
  3. void SystemInitial(void) ;
  4. void Delay(unsigned long u32DelayTime) ;
  5. void PeripheralInitial(void) ;
  6. void LedTask(void);

  7. #define  BLINK_TIME_1   1000  

  8. volatile unsigned char vGu8TimeFlag_1=0;  
  9. volatile unsigned int vGu16TimeCnt_1=0;   

  10. void main()
  11. {
  12. SystemInitial();            
  13. Delay(10000);               
  14. PeripheralInitial();        
  15.     while(1)  
  16. {  
  17. LedTask();   
  18.     }
  19. }

  20. void T0_time() interrupt 1     
  21. {
  22. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器
  23. {
  24. vGu16TimeCnt_1--;  
  25. }

  26. TH0=0xfc;   
  27. TL0=0x66;   
  28. }


  29. void SystemInitial(void)
  30. {
  31. TMOD=0x01;  
  32. TH0=0xfc;   
  33. TL0=0x66;   
  34. EA=1;      
  35. ET0=1;      
  36. TR0=1;      
  37. }

  38. void Delay(unsigned long u32DelayTime)
  39. {
  40.     for(;u32DelayTime>0;u32DelayTime--);
  41. }

  42. void PeripheralInitial(void)
  43. {

  44. }

  45. //跑马灯的任务程序   
  46. void LedTask(void)
  47. {
  48. static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
  49. static unsigned char Su8Cnt=0;     //加static修饰的局部变量,每次进来都会保留上一次值。

  50.       if(0==vGu16TimeCnt_1)     //时间到
  51.       {
  52. vGu8TimeFlag_1=0;  
  53. vGu16TimeCnt_1=BLINK_TIME_1;  //重装定时的时间
  54. vGu8TimeFlag_1=1;  

  55. P0=Su8Data;   //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
  56. Su8Data=Su8Data<<1;  //左移一位
  57. Su8Cnt++; //计数器累加1
  58. if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
  59. {
  60. Su8Cnt=0;  
  61. Su8Data=0x01;  //重新赋初值,继续下一次循环移动
  62. }   
  63. }
  64. }


       分析总结:这是第2种境界的跑马灯,这种思路虽然实现了跑马灯的效果,也用到了多任务并行处理的基本元素“软件定时器”,但是因为还停留在“移位”语句的阶段,此时的程序并没有超越跑马灯本身,跑马灯还是跑马灯,处于“看山还是山”的境界。

【89.4   状态切换非阻塞。】

       状态切换非阻塞,“状态切换”用的是switch语句中根据特定条件进行步骤切换,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:

  1. #include "REG52.H"  

  2. void T0_time();
  3. void SystemInitial(void) ;
  4. void Delay(unsigned long u32DelayTime) ;
  5. void PeripheralInitial(void) ;
  6. void LedTask(void);

  7. #define  BLINK_TIME_1   1000  

  8. sbit P0_0=P0^0;
  9. sbit P0_1=P0^1;
  10. sbit P0_2=P0^2;
  11. sbit P0_3=P0^3;
  12. sbit P0_4=P0^4;
  13. sbit P0_5=P0^5;
  14. sbit P0_6=P0^6;
  15. sbit P0_7=P0^7;

  16. volatile unsigned char vGu8TimeFlag_1=0;  
  17. volatile unsigned int vGu16TimeCnt_1=0;   

  18. void main()
  19. {
  20. SystemInitial();            
  21. Delay(10000);               
  22. PeripheralInitial();        
  23.     while(1)  
  24. {  
  25. LedTask();   
  26.     }
  27. }

  28. void T0_time() interrupt 1     
  29. {
  30. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器
  31. {
  32. vGu16TimeCnt_1--;  
  33. }

  34. TH0=0xfc;   
  35. TL0=0x66;   
  36. }


  37. void SystemInitial(void)
  38. {
  39. TMOD=0x01;  
  40. TH0=0xfc;   
  41. TL0=0x66;   
  42. EA=1;      
  43. ET0=1;      
  44. TR0=1;      
  45. }

  46. void Delay(unsigned long u32DelayTime)
  47. {
  48.     for(;u32DelayTime>0;u32DelayTime--);
  49. }

  50. void PeripheralInitial(void)
  51. {

  52. }

  53. //跑马灯的任务程序   
  54. void LedTask(void)
  55. {
  56. static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

  57.        switch(Su8Step)
  58.        {
  59.            case 0:
  60.                if(0==vGu16TimeCnt_1)  //时间到
  61.                {

  62. vGu8TimeFlag_1=0;  
  63. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  64. vGu8TimeFlag_1=1;  

  65.                     P0_0=1;   //第0个灯熄灭
  66.                     P0_1=0;   
  67.                     P0_2=0;   
  68.                     P0_3=0;   
  69.                     P0_4=0;   
  70.                     P0_5=0;   
  71.                     P0_6=0;   
  72.                     P0_7=0;   

  73. Su8Step=1;  //切换到下一个步骤,精髓语句!
  74. }
  75.                break;

  76.            case 1:
  77.                if(0==vGu16TimeCnt_1)  //时间到
  78.                {

  79. vGu8TimeFlag_1=0;  
  80. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  81. vGu8TimeFlag_1=1;  

  82.                     P0_0=0;   
  83.                     P0_1=1;   //第1个灯熄灭
  84.                     P0_2=0;   
  85.                     P0_3=0;   
  86.                     P0_4=0;   
  87.                     P0_5=0;   
  88.                     P0_6=0;   
  89.                     P0_7=0;   

  90. Su8Step=2;  //切换到下一个步骤,精髓语句!
  91. }
  92.                break;

  93.            case 2:
  94.                if(0==vGu16TimeCnt_1)  //时间到
  95.                {

  96. vGu8TimeFlag_1=0;  
  97. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  98. vGu8TimeFlag_1=1;  

  99.                     P0_0=0;   
  100.                     P0_1=0;   
  101.                     P0_2=1;   //第2个灯熄灭
  102.                     P0_3=0;   
  103.                     P0_4=0;   
  104.                     P0_5=0;   
  105.                     P0_6=0;   
  106.                     P0_7=0;   

  107. Su8Step=3;  //切换到下一个步骤,精髓语句!
  108. }
  109.                break;

  110.            case 3:
  111.                if(0==vGu16TimeCnt_1)  //时间到
  112.                {

  113. vGu8TimeFlag_1=0;  
  114. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  115. vGu8TimeFlag_1=1;  

  116.                     P0_0=0;   
  117.                     P0_1=0;   
  118.                     P0_2=0;   
  119.                     P0_3=1;   //第3个灯熄灭
  120.                     P0_4=0;   
  121.                     P0_5=0;   
  122.                     P0_6=0;   
  123.                     P0_7=0;   

  124. Su8Step=4;  //切换到下一个步骤,精髓语句!
  125. }
  126.                break;

  127.            case 4:
  128.                if(0==vGu16TimeCnt_1)  //时间到
  129.                {

  130. vGu8TimeFlag_1=0;  
  131. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  132. vGu8TimeFlag_1=1;  

  133.                     P0_0=0;   
  134.                     P0_1=0;   
  135.                     P0_2=0;   
  136.                     P0_3=0;   
  137.                     P0_4=1;   //第4个灯熄灭
  138.                     P0_5=0;   
  139.                     P0_6=0;   
  140.                     P0_7=0;   

  141. Su8Step=5;  //切换到下一个步骤,精髓语句!
  142. }
  143.                break;

  144.            case 5:
  145.                if(0==vGu16TimeCnt_1)  //时间到
  146.                {

  147. vGu8TimeFlag_1=0;  
  148. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  149. vGu8TimeFlag_1=1;  

  150.                     P0_0=0;   
  151.                     P0_1=0;   
  152.                     P0_2=0;   
  153.                     P0_3=0;   
  154.                     P0_4=0;   
  155.                     P0_5=1;   //第5个灯熄灭
  156.                     P0_6=0;   
  157.                     P0_7=0;   

  158. Su8Step=6;  //切换到下一个步骤,精髓语句!
  159. }
  160.                break;

  161.            case 6:
  162.                if(0==vGu16TimeCnt_1)  //时间到
  163.                {

  164. vGu8TimeFlag_1=0;  
  165. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  166. vGu8TimeFlag_1=1;  

  167.                     P0_0=0;   
  168.                     P0_1=0;   
  169.                     P0_2=0;   
  170.                     P0_3=0;   
  171.                     P0_4=0;   
  172.                     P0_5=0;   
  173.                     P0_6=1;   //第6个灯熄灭
  174.                     P0_7=0;   

  175. Su8Step=7;  //切换到下一个步骤,精髓语句!
  176. }
  177.                break;

  178.            case 7:

  179.               if(0==vGu16TimeCnt_1)     //时间到
  180.                {
  181. vGu8TimeFlag_1=0;  
  182. vGu16TimeCnt_1=BLINK_TIME_1;  //重装定时的时间
  183. vGu8TimeFlag_1=1;  

  184.                     P0_0=0;   
  185.                     P0_1=0;   
  186.                     P0_2=0;   
  187.                     P0_3=0;   
  188.                     P0_4=0;   
  189.                     P0_5=0;   
  190.                     P0_6=0;   
  191.                     P0_7=1;   //第7个灯熄灭

  192. Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
  193. }
  194.                break;
  195. }
  196. }

       分析总结:这是第3种境界的跑马灯,很多初学者咋看此程序,表示不理解,人家一条赋值语句就解决8个LED一次性显示的问题,你非要拆分成8条按位赋值的语句,人家只用一个判断就实现了LED灯移动显示的功能,你非要整出8个步骤的切换,况且,整个程序的代码量明显增加了很多,这个程序好在哪?其实,我这么做是用心良苦呀。这个程序的代码量虽然增多了,但是仔细一看,并没有影响运行的效率。之所以把8个LED灯拆分成一个一个的LED灯单独赋值显示,是因为,在我眼里,这个8个LED灯代表的不仅仅是LED灯,而是8个输出信号!这8个输出信号未来驱动的可能是不同的继电器,气缸,电机,大**,导弹,以及它们的各种千变万化的组合逻辑,拆分之后程序框架就有了无限可能的扩展性。之所以整出8个步骤的切换,也是同样的道理,为了增加程序框架无限可能的扩展性。这个程序虽然表面看起来繁琐,但是仔细一看它是“多而不乱”,非常富有“队形感”。因此可以这么说,这个看似繁琐的跑马灯程序,其实背后蕴藏了编程界的大智慧,它已经突破了“看山还是山”的境界。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
qin552011373 发表于 2017-10-4 14:46 | 显示全部楼层
后排站位关注一下
 楼主| jianhong_wu 发表于 2017-10-9 10:02 | 显示全部楼层
第九十节: 多任务并行处理两路跑马灯。

【90.1   多任务并行处理。】

        两路速度不同的跑马灯,代表了两路独立运行的任务,单片机如何“并行”处理这两路任务,就涉及到“多任务并行处理的编程思路”。

                 
                  上图90.1.1  灌入式驱动8个LED   第1路跑马灯


                  
                 上图90.1.2  灌入式驱动4个LED  新增加的第2路跑马灯

       如上图,本节特别值得一提的是,新增加的第2路跑马灯用的是4个LED,这4个LED的驱动IO口是“散装的”,因为,前面3个是P1口的(P1.4,P1.5,P1.6),最后1个是P3口的(P3.3),这种情况下,肯定用不了“移位”的处理思路,只能用跑马灯第3种境界里所介绍的“状态切换非阻塞”思路,可见,“IO口拆分”和“switch状态切换”又一次充分体现了它们“程序框架万能扩展”的优越性。代码如下:

  1. #include "REG52.H"  

  2. void T0_time();
  3. void SystemInitial(void) ;
  4. void Delay(unsigned long u32DelayTime) ;
  5. void PeripheralInitial(void) ;
  6. void Led_1_Task(void);
  7. void Led_2_Task(void);

  8. #define  BLINK_TIME_1   1000  //控制第1路跑马灯的速度,数值越大“跑动”越慢。
  9. #define  BLINK_TIME_2   200   //控制第2路跑马灯的速度,数值越大“跑动”越慢。

  10. sbit P0_0=P0^0;
  11. sbit P0_1=P0^1;
  12. sbit P0_2=P0^2;
  13. sbit P0_3=P0^3;
  14. sbit P0_4=P0^4;
  15. sbit P0_5=P0^5;
  16. sbit P0_6=P0^6;
  17. sbit P0_7=P0^7;

  18. sbit P1_4=P1^4;
  19. sbit P1_5=P1^5;
  20. sbit P1_6=P1^6;
  21. sbit P3_3=P3^3;

  22. volatile unsigned char vGu8TimeFlag_1=0;  
  23. volatile unsigned int vGu16TimeCnt_1=0;   

  24. volatile unsigned char vGu8TimeFlag_2=0;  
  25. volatile unsigned int vGu16TimeCnt_2=0;  

  26. void main()
  27. {
  28. SystemInitial();            
  29. Delay(10000);               
  30. PeripheralInitial();        
  31.     while(1)  
  32. {  
  33. Led_1_Task();   //第1路跑马灯
  34. Led_2_Task();   //第2路跑马灯
  35.     }
  36. }

  37. void T0_time() interrupt 1     
  38. {
  39. if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器1
  40. {
  41. vGu16TimeCnt_1--;  
  42. }

  43. if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //软件定时器2
  44. {
  45. vGu16TimeCnt_2--;  
  46. }


  47. TH0=0xfc;   
  48. TL0=0x66;   
  49. }


  50. void SystemInitial(void)
  51. {
  52. TMOD=0x01;  
  53. TH0=0xfc;   
  54. TL0=0x66;   
  55. EA=1;      
  56. ET0=1;      
  57. TR0=1;      
  58. }

  59. void Delay(unsigned long u32DelayTime)
  60. {
  61.     for(;u32DelayTime>0;u32DelayTime--);
  62. }

  63. void PeripheralInitial(void)
  64. {

  65. }

  66. //第1路跑马灯  
  67. void Led_1_Task(void)
  68. {
  69. static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

  70.        switch(Su8Step)
  71.        {
  72.            case 0:
  73.                if(0==vGu16TimeCnt_1)  //时间到
  74.                {

  75. vGu8TimeFlag_1=0;  
  76. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  77. vGu8TimeFlag_1=1;  

  78.                     P0_0=1;   //第0个灯熄灭
  79.                     P0_1=0;   
  80.                     P0_2=0;   
  81.                     P0_3=0;   
  82.                     P0_4=0;   
  83.                     P0_5=0;   
  84.                     P0_6=0;   
  85.                     P0_7=0;   

  86. Su8Step=1;  //切换到下一个步骤,精髓语句!
  87. }
  88.                break;

  89.            case 1:
  90.                if(0==vGu16TimeCnt_1)  //时间到
  91.                {

  92. vGu8TimeFlag_1=0;  
  93. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  94. vGu8TimeFlag_1=1;  

  95.                     P0_0=0;   
  96.                     P0_1=1;   //第1个灯熄灭
  97.                     P0_2=0;   
  98.                     P0_3=0;   
  99.                     P0_4=0;   
  100.                     P0_5=0;   
  101.                     P0_6=0;   
  102.                     P0_7=0;   

  103. Su8Step=2;  //切换到下一个步骤,精髓语句!
  104. }
  105.                break;

  106.            case 2:
  107.                if(0==vGu16TimeCnt_1)  //时间到
  108.                {

  109. vGu8TimeFlag_1=0;  
  110. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  111. vGu8TimeFlag_1=1;  

  112.                     P0_0=0;   
  113.                     P0_1=0;   
  114.                     P0_2=1;   //第2个灯熄灭
  115.                     P0_3=0;   
  116.                     P0_4=0;   
  117.                     P0_5=0;   
  118.                     P0_6=0;   
  119.                     P0_7=0;   

  120. Su8Step=3;  //切换到下一个步骤,精髓语句!
  121. }
  122.                break;

  123.            case 3:
  124.                if(0==vGu16TimeCnt_1)  //时间到
  125.                {

  126. vGu8TimeFlag_1=0;  
  127. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  128. vGu8TimeFlag_1=1;  

  129.                     P0_0=0;   
  130.                     P0_1=0;   
  131.                     P0_2=0;   
  132.                     P0_3=1;   //第3个灯熄灭
  133.                     P0_4=0;   
  134.                     P0_5=0;   
  135.                     P0_6=0;   
  136.                     P0_7=0;   

  137. Su8Step=4;  //切换到下一个步骤,精髓语句!
  138. }
  139.                break;

  140.            case 4:
  141.                if(0==vGu16TimeCnt_1)  //时间到
  142.                {

  143. vGu8TimeFlag_1=0;  
  144. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  145. vGu8TimeFlag_1=1;  

  146.                     P0_0=0;   
  147.                     P0_1=0;   
  148.                     P0_2=0;   
  149.                     P0_3=0;   
  150.                     P0_4=1;   //第4个灯熄灭
  151.                     P0_5=0;   
  152.                     P0_6=0;   
  153.                     P0_7=0;   

  154. Su8Step=5;  //切换到下一个步骤,精髓语句!
  155. }
  156.                break;

  157.            case 5:
  158.                if(0==vGu16TimeCnt_1)  //时间到
  159.                {

  160. vGu8TimeFlag_1=0;  
  161. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  162. vGu8TimeFlag_1=1;  

  163.                     P0_0=0;   
  164.                     P0_1=0;   
  165.                     P0_2=0;   
  166.                     P0_3=0;   
  167.                     P0_4=0;   
  168.                     P0_5=1;   //第5个灯熄灭
  169.                     P0_6=0;   
  170.                     P0_7=0;   

  171. Su8Step=6;  //切换到下一个步骤,精髓语句!
  172. }
  173.                break;

  174.            case 6:
  175.                if(0==vGu16TimeCnt_1)  //时间到
  176.                {

  177. vGu8TimeFlag_1=0;  
  178. vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
  179. vGu8TimeFlag_1=1;  

  180.                     P0_0=0;   
  181.                     P0_1=0;   
  182.                     P0_2=0;   
  183.                     P0_3=0;   
  184.                     P0_4=0;   
  185.                     P0_5=0;   
  186.                     P0_6=1;   //第6个灯熄灭
  187.                     P0_7=0;   

  188. Su8Step=7;  //切换到下一个步骤,精髓语句!
  189. }
  190.                break;

  191.            case 7:

  192.               if(0==vGu16TimeCnt_1)     //时间到
  193.                {
  194. vGu8TimeFlag_1=0;  
  195. vGu16TimeCnt_1=BLINK_TIME_1;  //重装定时的时间
  196. vGu8TimeFlag_1=1;  

  197.                     P0_0=0;   
  198.                     P0_1=0;   
  199.                     P0_2=0;   
  200.                     P0_3=0;   
  201.                     P0_4=0;   
  202.                     P0_5=0;   
  203.                     P0_6=0;   
  204.                     P0_7=1;   //第7个灯熄灭

  205. Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
  206. }
  207.                break;
  208. }
  209. }

  210. //第2路跑马灯  
  211. void Led_2_Task(void)
  212. {
  213. /*
  214. 疑点讲解(1):
  215.     这里第2路跑马灯的“Su8Step”与第1路跑马灯的“Su8Step”虽然同名,但是,因为它们是静态的局部变量,在两个不同的函数内部,是两个不同的变量,这两个变量所分配的RAM内存地址是不一样的,因此,它们虽然同名,但是不矛盾不冲突。
  216. */
  217. static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

  218.        switch(Su8Step)
  219.        {
  220.            case 0:
  221.                if(0==vGu16TimeCnt_2)  //时间到
  222.                {

  223. vGu8TimeFlag_2=0;  
  224. vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
  225. vGu8TimeFlag_2=1;  

  226.                     P1_4=1; //第0个灯熄灭
  227.                     P1_5=0;   
  228.                     P1_6=0;   
  229.                     P3_3=0;   

  230. Su8Step=1;  //切换到下一个步骤,精髓语句!
  231. }
  232.                break;

  233.            case 1:
  234.                if(0==vGu16TimeCnt_2)  //时间到
  235.                {

  236. vGu8TimeFlag_2=0;  
  237. vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
  238. vGu8TimeFlag_2=1;  

  239.                     P1_4=0;     
  240.                     P1_5=1;   //第1个灯熄灭
  241.                     P1_6=0;   
  242.                     P3_3=0;   

  243. Su8Step=2;  //切换到下一个步骤,精髓语句!
  244. }
  245.                break;

  246.            case 2:
  247.                if(0==vGu16TimeCnt_2)  //时间到
  248.                {

  249. vGu8TimeFlag_2=0;  
  250. vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
  251. vGu8TimeFlag_2=1;  

  252.                     P1_4=0;
  253.                     P1_5=0;   
  254.                     P1_6=1;   //第2个灯熄灭
  255.                     P3_3=0;   

  256. Su8Step=3;  //切换到下一个步骤,精髓语句!
  257. }
  258.                break;

  259.            case 3:
  260.                if(0==vGu16TimeCnt_2)  //时间到
  261.                {

  262. vGu8TimeFlag_2=0;  
  263. vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
  264. vGu8TimeFlag_2=1;  

  265.                     P1_4=0;
  266.                     P1_5=0;   
  267.                     P1_6=0;   
  268.                     P3_3=1;   //第3个灯熄灭

  269. Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
  270. }
  271.                break;

  272. }
  273. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

评论

vGu8TimeFlag_2, vGu8TimeFlag_1有什么作用,感觉这里有点多余,楼主解释下精妙之处  发表于 2019-3-26 14:59
 楼主| jianhong_wu 发表于 2017-10-17 18:01 | 显示全部楼层
第九十一节: 蜂鸣器的“非阻塞”驱动。

【91.1   蜂鸣器的硬件电路简介。】


       上图91.1.1  PNP三极管驱动有源蜂鸣器

       蜂鸣器有两种,一种是有源蜂鸣器,一种是无源蜂鸣器。有源蜂鸣器的驱动最简单,只要通电就一直响,断电就停,跟驱动LED灯一样。无源蜂鸣器则不一样,无源蜂鸣器一直断电不响,奇怪的是一直通电也不响,只有“通,关,通,关...”反复通电关电的状态,才会持续发生稳定的声音,此方式称为脉冲驱动方式,或者PWM驱动方式。本教程用的是有源蜂鸣器。
       蜂鸣器的驱动电路也有两种常用的方式,一种是NPN三极管驱动,一种是PNP三极管驱动。NPN三极管驱动电路,单片机输出“1”(高电平)蜂鸣器导通,输出“0”(低电平)蜂鸣器关闭。而PNP三极管驱动电路恰恰相反,单片机输出“0”(低电平)蜂鸣器导通,输出“1”(高电平)蜂鸣器关闭。本教程所用的是PNP三极管驱动电路,如上图。

【91.2   “非阻塞”驱动程序。】

       “驱动层”是相对“应用层”而言。“应用层”发号施令,“驱动层”负责执行。一个好的“驱动层”必须给“应用层”提供快捷便利的调用接口,此接口可以是函数或者全局变量。本节驱动蜂鸣器所用的是全局变量vGu16BeepTimerCnt。“应用层”只需给vGu16BeepTimerCnt赋值,就可以控制蜂鸣器发声,赋值越大,发声越长,500代表发声500ms,1000代表发声1000ms,具体细节实现,则由“驱动层”的驱动函数负责执行,驱动函数放在定时中断函数里定时扫描。为什么不把驱动函数放到main函数的循环里去?因为放在定时中断里,能保证蜂鸣器的声音长度是一致的,如果放在main循环里,声音的长度有可能在某些项目中受到某些必须一气呵成的任务干扰,得不到及时响应,影响声音长度的一致性。下面代码实现的功能是,单片机只要一上电,蜂鸣器就发出一次1000ms长度的“嘀”声音。

  1. #include "REG52.H"  

  2. #define BEEP_TIME  1000   //控制蜂鸣器发声的长度,此处是1000ms

  3. void T0_time();
  4. void SystemInitial(void) ;
  5. void Delay(unsigned long u32DelayTime) ;
  6. void PeripheralInitial(void) ;

  7. void BeepOpen(void);   //蜂鸣器发声
  8. void BeepClose(void);  //蜂鸣器关闭
  9. void VoiceScan(void);  //蜂鸣器的驱动函数,放在定时中断里

  10. sbit P3_4=P3^4;  //控制蜂鸣器的IO口。0代表发声,1代表关闭。

  11. volatile unsigned char vGu8BeepTimerFlag=0;  
  12. volatile unsigned int vGu16BeepTimerCnt=0;  //控制蜂鸣器发声长度的计时器

  13. void main()
  14. {
  15. SystemInitial();            
  16. Delay(10000);               
  17. PeripheralInitial();     //此函数内部有“应用层”的赋值操作,控制上电的声音长度。  
  18.     while(1)  
  19. {  
  20.    ;
  21.     }
  22. }

  23. void T0_time() interrupt 1     
  24. {
  25. VoiceScan();  //蜂鸣器的驱动函数

  26. TH0=0xfc;   
  27. TL0=0x66;   
  28. }


  29. void SystemInitial(void)
  30. {
  31. TMOD=0x01;  
  32. TH0=0xfc;   
  33. TL0=0x66;   
  34. EA=1;      
  35. ET0=1;      
  36. TR0=1;      
  37. }

  38. void Delay(unsigned long u32DelayTime)
  39. {
  40.     for(;u32DelayTime>0;u32DelayTime--);
  41. }

  42. void PeripheralInitial(void)
  43. {
  44.     vGu8BeepTimerFlag=0;  
  45. vGu16BeepTimerCnt=BEEP_TIME;  //“应用层”只需赋值,一上电,蜂鸣器发出1000ms长度的声音。
  46.     vGu8BeepTimerFlag=1;  

  47. }


  48. //蜂鸣器发声
  49. void BeepOpen(void)
  50. {
  51. P3_4=0;  //0代表发声
  52. }

  53. //蜂鸣器关闭
  54. void BeepClose(void)
  55. {
  56. P3_4=1;  //1代表关闭
  57. }

  58. //蜂鸣器的驱动函数,放在定时中断函数里每定时1ms扫描一次。
  59. void VoiceScan(void)
  60. {
  61. //Su8Lock的作用是避免BeepOpen()被重复扫描影响效率,发声时只执行一次此函数即可。
  62. //同时,也巧妙借用else结构,实现逻辑顺序分解成“先发声,下一次再开始定时”的两个步骤。

  63.           static unsigned char Su8Lock=0;  

  64. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  65.           {
  66.                   if(0==Su8Lock)
  67.                   {
  68.                    Su8Lock=1;  //进入触发声音后就自锁起来
  69. BeepOpen(); //发声,此处封装成函数,为了今后代码的移植性。
  70.      }
  71.     else  //巧妙借用else结构,实现先发声,下一次中断再开始计时的逻辑顺序。比如,
  72. {     //如果赋值1,就能确保有1ms的计时发声。

  73.                        vGu16BeepTimerCnt--;          //定时器自减,控制蜂鸣器发声的时间长度

  74.                    if(0==vGu16BeepTimerCnt)
  75.                    {
  76.                            Su8Lock=0;     //关闭声音后,及时解锁,为下一次触发做准备
  77. BeepClose();  //关闭声音,此处封装成函数,为了今后代码的移植性。
  78.                    }

  79. }
  80.           }         
  81. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
tyronewj 发表于 2017-10-23 09:05 | 显示全部楼层
谢谢分享

评论

en  发表于 2019-4-27 21:25
en  发表于 2019-4-27 21:24
 楼主| jianhong_wu 发表于 2017-10-25 11:36 | 显示全部楼层
本帖最后由 jianhong_wu 于 2017-10-28 09:13 编辑

第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)。

【92.1   独立按键的硬件电路简介。】




                上图92.1.1  独立按键电路

       按键有两种驱动方式,一种是独立按键,一种是矩阵按键。1个独立按键要占用1个IO口,IO口不能共用。而矩阵按键的IO口是分时片选复用的,用少量的IO口就可以驱动翻倍级别的按键数量。比如,用8个IO口只能驱动8个独立按键,但是却可以驱动16个矩阵按键(4x4)。因此,按键少的时候就用独立按键,按键多的时候就用矩阵按键。这两种按键的驱动本质是一样的,都是靠识别输入信号的下降沿(或上升沿)来识别按键的触发。
       独立按键的硬件原理基础,如上图,P2.2这个IO口,在按键K1没有被按下的时候,P2.2口因为单片机内部自带上拉电阻把电平拉高,此时P2.2口是高电平的输入状态。当按键K1被按下的时候,按键K1左右像一根导线连接到电源的负极(GND),直接把原来P2.2口的电平拉低,此时P2.2口变成了低电平的输入状态。编写按键驱动程序,就是要识别这个电平从高到低的过程,这个过程也叫下降沿。多说一句,51单片机的P1,P2,P3口是内部自带上拉电阻的,而P0口是内部没有上拉电阻的,需要外接上拉电阻。除此之外,很多单片机内部其实都没有上拉电阻的,因此,建议大家在做独立按键电路的时候,养成一个习惯,凡是按键输入状态都外接上拉电阻。
       识别按键的下降沿触发有四大要素:自锁,消抖,非阻塞,清零式滤波。
       “自锁”,按键一旦进入到低电平,就要“自锁”起来,避免不断触发按键,只有当按键被松开变成高电平的时候,才及时“解锁”为下一次触发做准备。
       “消抖”,按键是一个机械触点器件,在接触的瞬间必然存在微观上的机械抖动,反馈到电平的瞬间就是“高,低,高,低...”这种不稳定的电平状态是一种干扰,但是,按键一旦按下去稳定了之后,这种状态就消失,电平就一直保持稳定的低电平。消抖的本质就是滤波,要把这种接触的瞬间抖动过滤掉,避免按键的“一按多触发”。
       “非阻塞”,在处理消抖的时候,必须用到延时,如果此时用阻塞的delay延时就会影响其它任务的运行效率,因此,用非阻塞的定时延时更加有优越性。
       “清零式滤波”,在消抖的时候,有两种境界,第一种境界是判断两次电平的状态,中间插入“固定的时间”延时,这种方法前后一共判断了两次,第一次是识别到低电平就进入延时的状态,第二次是延时后再确认一次是否继续是低电平的状态,这种方法的不足是,“固定的时间”全凭经验值,但是不同的按键它们的抖动时间长度是不同的,除此之外,前后才判断了两次,在软件的抗干扰能力上也弱了很多,“密码等级”不够高。第二种境界就是“清零式滤波”,“清零式滤波”非常巧妙,抗扰能力超强,它能自动过滤不同按键的“抖动时间”,然后再进入一个“稳定时间”的“N次识别判断”,更加巧妙的是,在“抖动时间”和“稳定时间”两者时间内,只要发现一次是高电平的干扰,就马上自动清零计时器,重新开始计时。“稳定时间”一般取20ms到30ms之间,而“抖动时间”是隐藏的,在代码上并没有直接描写出来,但是却无形地融入了代码之中,只有慢慢体会才能发现它的存在。
        具体的代码如下,实现的功能是按一次K1或者K2按键,就触发一次蜂鸣器鸣叫。
  1. #include "REG52.H"  

  2. #define KEY_VOICE_TIME   50 //按键触发后发出的声音长度  
  3. #define KEY_FILTER_TIME  25  //按键滤波的“稳定时间”25ms

  4. void T0_time();
  5. void SystemInitial(void) ;
  6. void Delay(unsigned long u32DelayTime) ;
  7. void PeripheralInitial(void) ;

  8. void BeepOpen(void);   
  9. void BeepClose(void);
  10. void VoiceScan(void);
  11. void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
  12. void KeyTask(void);    //按键任务函数,放在主函数内

  13. sbit P3_4=P3^4;  
  14. sbit KEY_INPUT1=P2^2;  //K1按键识别的输入口。
  15. sbit KEY_INPUT2=P2^1;  //K2按键识别的输入口。

  16. volatile unsigned char vGu8BeepTimerFlag=0;  
  17. volatile unsigned int vGu16BeepTimerCnt=0;  

  18. volatile unsigned char vGu8KeySec=0;  //按键的触发序号,全局变量意味着是其它函数的接口。

  19. void main()
  20. {
  21. SystemInitial();            
  22. Delay(10000);               
  23. PeripheralInitial();      
  24.     while(1)  
  25. {  
  26.    KeyTask();    //按键任务函数
  27.     }
  28. }

  29. void T0_time() interrupt 1     
  30. {
  31. VoiceScan();  
  32. KeyScan();    //按键识别的驱动函数

  33. TH0=0xfc;   
  34. TL0=0x66;   
  35. }


  36. void SystemInitial(void)
  37. {
  38. TMOD=0x01;  
  39. TH0=0xfc;   
  40. TL0=0x66;   
  41. EA=1;      
  42. ET0=1;      
  43. TR0=1;      
  44. }

  45. void Delay(unsigned long u32DelayTime)
  46. {
  47.     for(;u32DelayTime>0;u32DelayTime--);
  48. }

  49. void PeripheralInitial(void)
  50. {
  51.    
  52. }

  53. void BeepOpen(void)
  54. {
  55. P3_4=0;  
  56. }

  57. void BeepClose(void)
  58. {
  59. P3_4=1;  
  60. }

  61. void VoiceScan(void)
  62. {

  63.           static unsigned char Su8Lock=0;  

  64. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  65.           {
  66.                   if(0==Su8Lock)
  67.                   {
  68.                    Su8Lock=1;  
  69. BeepOpen();
  70.      }
  71.     else  
  72. {     

  73.                        vGu16BeepTimerCnt--;         

  74.                    if(0==vGu16BeepTimerCnt)
  75.                    {
  76.                            Su8Lock=0;     
  77. BeepClose();  
  78.                    }

  79. }
  80.           }         
  81. }

  82. /* 注释一:
  83. * 独立按键扫描的详细过程,以按键K1为例,如下:
  84. * 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
  85. * 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到
  86. *         阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
  87. *         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零了,这个过程
  88. *         非常巧妙,非常有效地去除瞬间的杂波干扰。以后凡是用到开关感应器的时候,
  89. *         都可以用类似这样的方法去干扰。
  90. * 第三步:如果按键按下的时间达到阀值KEY_FILTER_TIME时,则触发按键,把编号vGu8KeySec赋值。
  91. *         同时,马上把自锁标志Su8KeyLock1置1,防止按住按键不松手后一直触发。
  92. * 第四步:等按键松开后,自锁标志Su8KeyLock1及时清零(解锁),为下一次自锁做准备。
  93. * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。
  94. */
  95. void KeyScan(void)  //此函数放在定时中断里每1ms扫描一次
  96. {
  97.    static unsigned char Su8KeyLock1; //1号按键的自锁
  98.    static unsigned int  Su16KeyCnt1; //1号按键的计时器
  99.    static unsigned char Su8KeyLock2; //2号按键的自锁
  100.    static unsigned int  Su16KeyCnt2; //2号按键的计时器

  101.    //1号按键
  102.    if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
  103.    {
  104.       Su8KeyLock1=0; //按键解锁
  105.       Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
  106.    }
  107.    else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。这行很多初学者有疑问,请看专题分析。
  108.    {
  109.       Su16KeyCnt1++; //累加定时中断次数
  110.       if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
  111.       {
  112.          Su8KeyLock1=1;  //按键的自锁,避免一直触发
  113.          vGu8KeySec=1;    //触发1号键
  114.       }
  115.    }

  116.    //2号按键
  117.    if(0!=KEY_INPUT2)
  118.    {
  119.       Su8KeyLock2=0;
  120.       Su16KeyCnt2=0;      
  121.    }
  122.    else if(0==Su8KeyLock2)
  123.    {
  124.       Su16KeyCnt2++;
  125.       if(Su16KeyCnt2>=KEY_FILTER_TIME)
  126.       {
  127.          Su8KeyLock2=1;  
  128.          vGu8KeySec=2;    //触发2号键
  129.       }
  130.    }


  131. }

  132. void KeyTask(void)    //按键任务函数,放在主函数内
  133. {
  134. if(0==vGu8KeySec)
  135. {
  136. return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
  137. }

  138. switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
  139. {
  140.    case 1:     //1号按键

  141.         vGu8BeepTimerFlag=0;  
  142. vGu16BeepTimerCnt=KEY_VOICE_TIME;  //触发按键后,发出固定长度的声音
  143.         vGu8BeepTimerFlag=1;  
  144. vGu8KeySec=0;  //响应按键服务处理程序后,按键编号必须清零,避免一致触发
  145. break;

  146.    case 2:     //2号按键

  147.         vGu8BeepTimerFlag=0;  
  148. vGu16BeepTimerCnt=KEY_VOICE_TIME;  //触发按键后,发出固定长度的声音
  149.         vGu8BeepTimerFlag=1;  
  150. vGu8KeySec=0;  //响应按键服务处理程序后,按键编号必须清零,避免一致触发
  151. break;

  152. }
  153. }


【92.2   专题分析:else if(0==Su8KeyLock1)。】

       疑问:
  1.    if(0!=KEY_INPUT1)
  2.    {
  3.       Su8KeyLock1=0;
  4.       Su16KeyCnt1=0;      
  5.    }
  6.    else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。为什么?为什么?为什么?
  7.    {
  8.       Su16KeyCnt1++;
  9.       if(Su16KeyCnt1>KEY_FILTER_TIME)
  10.       {
  11.          Su8KeyLock1=1;  
  12.          vGu8KeySec=1;   
  13.       }
  14.    }


       解答:
       首先,我们要明白C语言的语法中,
  1. if(条件1)
  2. {

  3. }
  4. else if(条件2)
  5. {

  6. }

       以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。
       回到刚才的问题,当程序执行到(条件2) else if(0==Su8KeyLock1)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1)不成立,这个条件不成立,就意味着0==KEY_INPUT1,也就是有按键被按下,因此,这里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一个自锁标志位,一旦按键被触发后,这个标志位会变1,防止按键按住不松手的时候不断触发按键。这样,按键只能按一次触发一次,松开手后再按一次,又触发一次。

【92.3   专题分析:if(0!=KEY_INPUT1)。】

       疑问:为什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)?
       解答:其实两者在功能上是完全等效的,在这里都可以用。之所以本教程优先选用后者if(0!=KEY_INPUT1),是因为考虑到了代码在不同单片机平台上的可移植性和兼容性。很多32位的单片机提供的是库函数,库函数返回的按键状态是一个字节变量来表示,当被按下的时候是0,但是,当没有按下的时候并不一定等于1,而是一个“非0”的数值。

【92.4   专题分析:把KeyScan函数放在定时器中断里。】

       疑问:为什么把KeyScan函数放在定时器中断里?
       解答:中断函数里放的函数或者代码越少越好,但是KeyScan函数是特殊的函数,是涉及到IO口输入信号的滤波,滤波就涉及到时间的及时性与均匀性,放在定时中断函数里更加能保证时间的一致性。比如,蜂鸣器驱动,动态数码管驱动,按键扫描驱动,我个人都习惯放在定时中断函数里。

【92.5   专题分析:if(0==vGu8KeySec)return。】

       疑问:if(0==vGu8KeySec)return是不是多此一举?
       解答:在KeyTask函数这里,if(0==vGu8KeySec)return这行代码删掉,对程序功能是没有影响的,这里之所以多插入这行判断语句,是因为,当按键多达几十个的时候,避免主函数每次进入KeyTask函数,都挨个扫描判断switch的状态进行多次判断,如果增加了这行if(0==vGu8KeySec)return代码,就可以直接退出省事,在理论上感觉更加运行高效。其实,不同单片机不同的C编译器可能对switch语句的翻译不一样,因此,这里的是不是更加高效我不敢保证。但是可以保证的是,加了这行代码也没有其它副作用。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
狮子fish 发表于 2017-10-26 19:49 | 显示全部楼层
膜拜。。。。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部