ghhuang 发表于 2017-10-31 10:44:30

支持楼主,很受启发。请楼主继续

tt98 发表于 2017-10-31 21:29:21

距100节不远了{:titter:}

guolun 发表于 2017-11-1 19:49:30

为楼主的毅力,言出必行的负责任的精神点赞。

吴坚鸿 发表于 2017-11-5 11:28:46

第九十四节: 两个独立按键构成的组合按键。

【94.1   组合按键。】



            
                上图94.1.1独立按键电路


            
                上图94.1.2LED电路

            
                上图94.1.3有源蜂鸣器电路

      组合按键的触发,是指两个按键同时按下时的“非单击”触发。一次组合按键的产生,必然包含了三类按键的触发。比如,K1与K2两个独立按键,当它们产生一次组合按键的操作时,就包含了三类触发:K1单击触发,K2单击触发,K1与K2的组合触发。这三类触发可以看作是底层的按键驱动程序,在按键应用层的任务函数SingleKeyTask和CombinationKeyTask中,可以根据项目的实际需要进行响应。本节程序例程要实现的功能是:(1)K1单击让LED变成“亮”的状态。(2)K2单击让LED变成“灭”的状态。(3)K1与K2的组合按键触发让蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   //组合按键触发后发出的声音长度
#define KEY_FILTER_TIME25   //按键滤波的“稳定时间”25ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void CombinationKeyTask(void);   //组合按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8CombinationKeySec=0;//组合按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   CombinationKeyTask();//组合按键任务函数
   SingleKeyTask();    //单击按键任务函数
    }
}

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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
LedClose();//初始化关闭LED
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}


/* 注释一:
* 组合按键扫描的详细过程:
* 第一步:平时只要K1与K2两个按键中有一个没有被按下时,按键的自锁标志,去抖动延时计数器
* 一直被清零。
* 第二步:一旦两个按键都处于被按下的状态,去抖动延时计数器开始在定时中断函数里累加,在还没
*         累加到阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使其中一个
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16CombinationKeyTimeCnt
*         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果两个按键按下的时间超过了阀值KEY_FILTER_TIME,马上把自锁标志Su8CombinationKeyLock
*         置1,防止按住两个按键不松手后一直触发。并把按键编号vGu8CombinationKeySec赋值,
*         触发一次组合按键。
* 第四步:等其中一个按键松开后,自锁标志Su8CombinationKeyLock及时清零,为下一次自锁做准备。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;         
   static unsigned char Su8KeyLock2;         
   static unsigned intSu16KeyCnt2;         

   static unsigned char Su8CombinationKeyLock;//组合按键的自锁
   static unsigned intSu16CombinationKeyCnt;//组合按键的计时器


   //K1按键与K2按键的组合触发
   if(0!=KEY_INPUT1||0!=KEY_INPUT2)//两个按键只要有一个按键没有按下,处于“非组合按键”的状态。
   {
      Su8CombinationKeyLock=0; //组合按键解锁
      Su16CombinationKeyCnt=0;//组合按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8CombinationKeyLock)//两个按键被同时按下,且是第一次被按下。此行请看专题分析。
   {
      Su16CombinationKeyCnt++; //累加定时中断次数
      if(Su16CombinationKeyCnt>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME。
      {
         Su8CombinationKeyLock=1;//组合按键的自锁,避免一直触发
         vGu8CombinationKeySec=1;   //触发K1与K2的组合键操作
      }
   }

   //K1按键的单击
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;      
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8SingleKeySec=1;    //触发K1的单击键
      }
   }

   //K2按键的单击
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8SingleKeySec=2;    //触发K2的单击键
      }
   }

}

void CombinationKeyTask(void)    //组合按键任务函数,放在主函数内
{
if(0==vGu8CombinationKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8CombinationKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1与K2的组合按键任务

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次组合按键后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8CombinationKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1单击任务
      LedOpen();    //LED亮

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K2单击任务
      LedClose();   //LED灭

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}

}


【94.2   专题分析:else if(0==Su8CombinationKeyLock)。】

      疑问:
   if(0!=KEY_INPUT1||0!=KEY_INPUT2)
   {
      Su8CombinationKeyLock=0;
      Su16CombinationKeyCnt=0;      
   }
   else if(0==Su8CombinationKeyLock)//两个按键被同时按下,且是第一次被按下。为什么?
   {
      Su16CombinationKeyCnt++;
      if(Su16CombinationKeyCnt>=KEY_FILTER_TIME)
      {
         Su8CombinationKeyLock=1;
         vGu8CombinationKeySec=1;   
      }
   }

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

}
else if(条件2)
{

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

kkfy888 发表于 2017-11-9 07:53:25

等了一段时间了,以为不更新了,支持鸿哥继续更新!

吴坚鸿 发表于 2017-11-12 15:35:23

第九十五节: 两个独立按键的“电脑键盘式”组合按键。

【95.1   “电脑键盘式”组合按键。】



            
                上图95.1.1独立按键电路


            
                上图95.1.2LED电路

            
                上图95.1.3有源蜂鸣器电路

      上一节也讲了由K1和K2构成的组合按键,但是这种组合按键是普通的组合按键,因为它们的K1和K2是不分先后顺序的,你先按住K1然后再按K2,或者你先按住K2然后再按K1,效果都一样。本节讲的组合按键是分先后顺序的,比如,像电脑的复制快捷键(Ctrl+C),你必须先按住Ctrl再按住C此时“复制快捷键”才有效,如果你先按住C再按住Ctrl此时“复制快捷键”无效。本节讲的例程就是要实现这个功能,用K1代表C这类“字符数字键”,用K2代表Ctrl这类“辅助按键”,因此,要触发组合键(K2+K1),必须先按住K2再按K1才有效。本节讲的例程功能如下:(1)K1每单击一次,LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)如果先按住K2再按K1,就认为构造了“电脑键盘式”组合键,蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   //组合按键触发后发出的声音长度 50ms
#define KEY_FILTER_TIME25   //按键滤波的“稳定时间”25ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void CombinationKeyTask(void);   //组合按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8CombinationKeySec=0;//组合按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   CombinationKeyTask();//组合按键任务函数
   SingleKeyTask();       //单击按键任务函数
    }
}

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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus)
{
LedClose();
}
else
{
LedOpen();
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}


/* 注释一:
* “电脑键盘式”组合按键扫描的详细过程:
* 第一步:K2与K1构成的组合按键触发是融合在K1单击按键程序里的,只需稍微更改一下K1单击的程序
*         ,就可以兼容到K2与K1构成的“电脑键盘式”组合按键。平时只要K1没有被按下时,按
*         键的自锁标志Su8KeyLock1和去抖动延时计数器Su16KeyCnt1一直被清零。
* 第二步:一旦K1按键被按下,去抖动延时计数器Su16KeyCnt1开始在定时中断函数里累加,在还没
*         累加到阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零了,
*         这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果K1按键按下的时间超过了阀值KEY_FILTER_TIME,马上把自锁标志Su8KeyLock1置1,
*         防止按住按键不松手后一直触发,此时才开始判断一次K2按键的电平状态,如果K2为低电
*         平就认为是组合按键,并给按键编号vGu8CombinationKeySec赋值,否则,就认为是K1的单击
*         按键,并给按键编号vGu8SingleKeySec赋值。
* 第四步:等K1按键松开后,自锁标志Su8KeyLock1及时清零,为下一次自锁做准备。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;         


   //K1的单击,或者K2与K1构成的“电脑键盘式组合按键”。
   if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数
      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME。
      {
         if(0==KEY_INPUT2)//此时才开始判断一次K2的电平状态,为低电平则是组合按键。
         {
            Su8KeyLock1=1;
            vGu8CombinationKeySec=1;//组合按键的触发
}
else   
         {
            Su8KeyLock1=1;
            vGu8SingleKeySec=1;    //K1单击按键的触发
}
      }
   }
}

void CombinationKeyTask(void)    //组合按键任务函数,放在主函数内
{
if(0==vGu8CombinationKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8CombinationKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1与K2的组合按键任务

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次组合按键后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8CombinationKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1单击任务
if(0==Gu8LedStatus)
{
Gu8LedStatus=1;
LedOpen();    //LED亮   
}
else
{
Gu8LedStatus=0;
LedClose();    //LED灭   
}

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

tt98 发表于 2017-11-12 16:35:14

谢谢鸿哥坚持!!{:lol:}

PEcontrol 发表于 2017-11-12 18:23:28

给楼主点赞!

asong14437546 发表于 2017-11-12 20:20:39

为楼主的坚持点赞!

吴坚鸿 发表于 2017-11-19 11:28:10

第九十六节: 独立按键“一键两用”的短按与长按。

【96.1   “一键两用”的短按与长按。】


                上图96.1.1独立按键电路


                上图96.1.2LED电路

            
                上图96.1.3有源蜂鸣器电路

      某些项目,当外部按键的资源比较少的时候,一个按键也可以“一键多用”。“一键多用”有很多种玩法,比如,谍战片的无线电通信,依赖一个按键的“不同敲击频率”就可以发送内容丰富的情报。本节“一键两用”也是属于“一键多用”的众多玩法之一。“短按与长按”的原理是依赖“按键按下的时间长度”来区分识别。“短按”是指从按下的“下降沿”到松手的“上升沿”时间,“长按”是指从按下的“下降沿”到一直按住不松手的“低电平持续时间”。本节的例程功能如下:(1)K1每“短按”一次(25ms),LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)K1每“长按”一次(500ms),蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   //按键“长按”触发后发出的声音长度 50ms

#define KEY_SHORT_TIME25      //按键的“短按”兼“滤波”的“稳定时间”25ms
#define KEY_LONG_TIME500      //按键的“长按”兼“滤波”的“稳定时间”500ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   SingleKeyTask();       //单击按键任务函数
    }
}

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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus)
{
LedClose();
}
else
{
LedOpen();
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

/* 注释一:
* “长按”与“短按”的识别过程:
* 第一步:平时只要K1没有被按下,按键的自锁标志Su8KeyLock1和去抖动延时计数器Su16KeyCnt1
*         一直被清零。此时属于按键“松手时间”,因此同时检测“短按”标志Su8KeyShortFlag
*         是否有效,如果有效就触发一次“短按”。
* 第二步:一旦K1按键被按下,去抖动延时计数器Su16KeyCnt1开始在定时中断函数里累加,在还没
*         累加到阀值KEY_SHORT_TIME和KEY_LONG_TIME时,如果在这期间由于受外界干扰或者
*         按键抖动,而使IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零,
*         这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果K1按键按下的时间超过了“短按”阀值KEY_SHORT_TIME,马上把“短按”标志
*         Su8KeyShortFlag置1,如果此时还没有松手,直到发现按下的时间超过“长按”阀值
*         KEY_LONG_TIME时,先把“短按”标志ucShortTouchFlag1清零,然后触发“长按”,同时,为
*         了防止按住按键不松手后一直触发,要及时把Su8KeyLock1置1“自锁”。
* 第四步:等K1按键松手后,自锁标志Su8KeyLock1及时清零,为下一次自锁做准备,同时,也检测
*         “短按”标志Su8KeyShortFlag是否有效,如果有效就触发一次“短按”。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyShortFlag=0;//按键“短按”触发的标志      

    if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。
      if(1==Su8KeyShortFlag)//松手的时候,如果“短按”标志有效就触发一次“短按”
      {
Su8KeyShortFlag=0;   //先清零“短按”标志避免一直触发。
vGu8SingleKeySec=1;    //触发K1的“短按”
}
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数

      if(Su16KeyCnt1>=KEY_SHORT_TIME) //“短按”兼“滤波”的“稳定时间”KEY_SHORT_TIME
      {
            //注意,这里不能“自锁”。后面“长按”触发的时候才“自锁”。
            Su8KeyShortFlag=1;    //K1的“短按”标志有效,待松手时触发。
      }


      if(Su16KeyCnt1>=KEY_LONG_TIME) //“长按”兼“滤波”的“稳定时间”KEY_LONG_TIME
      {
            Su8KeyLock1=1;      //此时“长按”触发才“自锁”
Su8KeyShortFlag=0;//既然此时“长按”有效,那么就要废除潜在的“短按”。
            vGu8SingleKeySec=2; //触发K1的“长按”
      }
   }
}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1“短按”触发的任务
if(0==Gu8LedStatus)
{
Gu8LedStatus=1;
LedOpen();    //LED亮   
}
else
{
Gu8LedStatus=0;
LedClose();    //LED灭   
}
vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K1“长按”触发的任务
      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次“长按”后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}
}

kkfy888 发表于 2017-11-22 22:33:50


为楼主的毅力点赞

rqiang 发表于 2017-11-22 23:43:37

为楼主的毅力点赞!!!!!!!!!!!!!!!

吴坚鸿 发表于 2017-11-26 11:44:28

第九十七节: 独立按键按住不松手的连续均匀触发。

【97.1   按住不松手的连续均匀触发。】




                上图97.1.1独立按键电路


         
                上图97.1.2灌入式驱动8个LED

         
                上图97.1.3有源蜂鸣器电路

      在电脑上删除某个文件某行文字的时候,单击一次“退格按键”,就删除一个文字,如果按住“退格按键”不松手,就会“连续均匀”的触发“删除”的功能,自动逐个把整行文字删除清空,这就是“按住不松手的连续均匀触发”应用案例之一。除此之外,在很多需要人机交互的项目中都有这样的功能,为了快速加减某个数值,按住某个按键不松手,某个数值有节奏地快速往上加或者快速往下减。这种“按住不松手连续均匀触发”的按键识别,在程序上有“3个时间”需要留意,第1个是按键单击的“滤波”时间,第2个是按键“从单击进入连击”的间隔时间(此时间是“单击”与“连击”的分界线),第3个是按键“连击”的间隔时间,
      本节例程实现的功能如下:(1)8个受按键控制的跑马灯在某一时刻只有1个LED亮,每触发一次K1按键,“亮的LED”就“往左边跑一步”;相反,每触发一次K2按键,“亮的LED”就“往右边跑一步”。如果按住K1或者K2不松手就连续触发,“亮的LED”就“连续跑”,一直跑到左边或者右边的尽头。(2)按键每“单击”一次蜂鸣器就鸣叫一次,但是,当按键“从单击进入连击”后,蜂鸣器就不鸣叫。

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME25      //按键单击的“滤波”时间25ms
#define KEY_ENTER_CONTINUITY_TIME    300//按键“从单击进入连击”的间隔时间300ms
#define KEY_CONTINUITY_TIME    80//按键“连击”的间隔时间80ms

#define BUS_P0    P0   //8个LED灯一一对应单片机的P0口总线

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void DisplayTask(void);   //显示的任务函数(LED显示状态)

sbit P3_4=P3^4;       //蜂鸣器

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //LED灯的状态
unsigned char Gu8DisplayUpdate=1; //显示的刷新标志

volatile unsigned char vGu8KeySec=0;//按键的触发序号
volatile unsigned char vGu8ShieldVoiceFlag=0;//屏蔽声音的标志

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();       //按键的任务函数
DisplayTask();   //显示的任务函数(LED显示状态)
    }
}


/* 注释一:
* Gu8DisplayUpdate这类“显示刷新变量”在“显示框架”里是很常见的,而且屡用屡爽。
* 目的是,既能及时刷新显示,又能避免主函数“不断去执行显示代码”而影响程序效率。
*/

void DisplayTask(void)   //显示的任务函数(LED显示状态)
{
if(1==Gu8DisplayUpdate)//需要刷新一次显示
{
Gu8DisplayUpdate=0;//及时清零,避免主函数“不断去执行显示代码”而影响程序效率

//Gu8LedStatus是左移的位数,范围(0至7),决定了跑马灯的显示状态。
BUS_P0=~(1<<Gu8LedStatus);//“左移<<”之后的“取反~”,因为LED电路是灌入式驱动方式。
}
}

/* 注释二:
* 按键“连续均匀触发”的识别过程:
* 第一步:平时只要K1没有被按下,按键的自锁标志Su8KeyLock1、去抖动延时计数器Su16KeyCnt1、
*         连击计数器Su16KeyContinuityCnt1,一直被清零。
* 第二步:一旦K1按键被按下,去抖动延时计数器Su16KeyCnt1开始在定时中断函数里累加,在还没
*         累加到阀值KEY_SHORT_TIME时,如果在这期间由于受外界干扰或者按键抖动,
*         而使IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零,
*         这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果K1按键按下的时间超过了阀值KEY_SHORT_TIME,则触发一次“单击”, 同时,马上把自锁
*         标志Su8KeyLock1置1防止按住按键不松手后一直触发,并且把计数器Su16KeyCnt1清零为了下
*         一步用来累加“从单击进入连击的间隔时间1000ms”。如果此时还没有松手,直到发现按下的时
*         间超过“从单击进入连击的间隔时间”阀值KEY_ENTER_CONTINUITY_TIME时,从此进入“连击”
*         的模式,连击计数器Su16KeyContinuityCnt1开始累加,每到达一次阀值
*         KEY_CONTINUITY_TIME就触发1次按键,为了屏蔽按键声音及时把vGu8ShieldVoiceFlag也置1,
*         同时,Su16KeyContinuityCnt1马上清零为继续连击作准备。
* 第四步:等K1按键松手后,自锁标志Su8KeyLock1、去抖动延时计数器Su16KeyCnt1、
*         连击计数器Su16KeyContinuityCnt1,及时清零,为下一次按键触发做准备。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;
   static unsigned intSu16KeyContinuityCnt1;//连击计数器

   static unsigned char Su8KeyLock2;      
   static unsigned intSu16KeyCnt2;
   static unsigned intSu16KeyContinuityCnt2;//连击计数器

   //K1按键
   if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。
Su16KeyContinuityCnt1=0;//连击计数器
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数,每一次累加额度是1ms
      if(Su16KeyCnt1>=KEY_SHORT_TIME) //按键的“滤波”时间25ms
      {
            Su8KeyLock1=1;      //“自锁”
            vGu8KeySec=1;       //触发一次K1按键      
            Su16KeyCnt1=0;      //清零,为了下一步用来累加“从单击进入连击的间隔时间300ms”
      }
   }
   else if(Su16KeyCnt1<=KEY_ENTER_CONTINUITY_TIME)//按住不松手累加到300ms
   {
      Su16KeyCnt1++; //累加定时中断次数,每一次累加额度是1ms
   }
   else//按住累加到300ms后仍然不放手,这个时候进入有节奏的连续触发
   {
       Su16KeyContinuityCnt1++; //连击计数器开始累加,每一次累加额度是1ms
       if(Su16KeyContinuityCnt1>=KEY_CONTINUITY_TIME)//按住没松手,每0.08秒就触发一次
       {
            Su16KeyContinuityCnt1=0; //清零,为了继续连击。
            vGu8KeySec=1;       //触发一次K1按键   
vGu8ShieldVoiceFlag=1;//把当前按键触发的声音屏蔽掉
       }

}

   //K2按键
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;
Su16KeyContinuityCnt2=0;   
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_SHORT_TIME)
      {
            Su8KeyLock2=1;   
            vGu8KeySec=2;       //触发一次K2按键      
            Su16KeyCnt2=0;      
      }
   }
   else if(Su16KeyCnt2<=KEY_ENTER_CONTINUITY_TIME)
   {
      Su16KeyCnt2++;
   }
   else
   {
       Su16KeyContinuityCnt2++;
       if(Su16KeyContinuityCnt2>=KEY_CONTINUITY_TIME)
       {
            Su16KeyContinuityCnt2=0;
            vGu8KeySec=2;       //触发一次K2按键   
vGu8ShieldVoiceFlag=1;//把当前按键触发的声音屏蔽掉
       }

}
}

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1触发的任务
      if(Gu8LedStatus>0)
{
Gu8LedStatus--;//控制LED“往左边跑”
Gu8DisplayUpdate=1;//刷新显示
}

      if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
                vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次“长按”后,发出“嘀”一声
                vGu8BeepTimerFlag=1;
            }

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K2触发的任务
      if(Gu8LedStatus<7)
{
Gu8LedStatus++;//控制LED“往右边跑”
Gu8DisplayUpdate=1;//刷新显示
}

      if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
                vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次“长按”后,发出“嘀”一声
                vGu8BeepTimerFlag=1;
            }

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}
}



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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

tt98 发表于 2017-11-27 21:59:16

鸿哥坚持哈{:titter:}

吴坚鸿 发表于 2017-12-7 11:48:36

第九十八节: 独立按键按住不松手的“先加速后匀速”的触发。

【98.1   “先加速后匀速”的触发。】


            

                上图98.1.1独立按键电路


            
                上图98.1.2灌入式驱动8个LED

            
                上图98.1.3有源蜂鸣器电路

      当“连续加”或者“连续减”的数据范围很大的时候,就需要按键的加速与匀速相结合的触发方式。“加速”是指按住按键不松手,按键刚开始触发是从慢到快的渐进过程,当“加速”到某个特别快的速度的时候,就“不再加速”,而是以该“恒定高速”进行“连续匀速”触发。这种触发方式,“加速”和“匀速”是相辅相成缺一不可的,为什么?假如没有“加速”只有“匀速”,那么刚按下按键就直接以最高速的“匀速”进行,就会跑过头,缺乏微调功能;而假如没有“匀速”只有“加速”,那么按下按键不松手后,速度就会一直不断飙升,最后失控过冲。
      本节例程实现的功能如下:
       (1)要更改一个“设置参数”(一个全局变量),参数的范围是0到800。
       (2)8个受“设置参数”控制的跑马灯在某一时刻只有1个LED亮,每触发一次K1按键,该“设置参数”就自减1,最小值为0;相反,每触发一次K2按键,该“设置参数”就自加1,最大值为800。
       (3)LED灯实时显示“设置参数”的范围状态:
                只有第0个LED灯亮:0<=“设置参数”<100。
                只有第1个LED灯亮:100<=“设置参数”<200。
                只有第2个LED灯亮:200<=“设置参数”<300。
                只有第3个LED灯亮:300<=“设置参数”<400。
                只有第4个LED灯亮:400<=“设置参数”<500。
                只有第5个LED灯亮:500<=“设置参数”<600。
                只有第6个LED灯亮:600<=“设置参数”<700。
                只有第7个LED灯亮:700<=“设置参数”<=800。
       (4)按键每“单击”一次蜂鸣器就鸣叫一次,但是,当按键“从单击进入连击”后,蜂鸣器就不鸣叫。

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME25      //按键单击的“滤波”时间
#define KEY_ENTER_CONTINUITY_TIME    300//按键“从单击进入连击”的间隔时间
#define KEY_CONTINUITY_INITIAL_TIME    80//按键“连击”起始的预设间隔时间
#define KEY_SUB_DT_TIME   8      //按键在“加速”时每次减小的时间。
#define KEY_CONTINUITY_MIN_TIME   10//按键时间减小到最后的“匀速”间隔时间。


#define BUS_P0    P0   //8个LED灯一一对应单片机的P0口总线

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void DisplayTask(void);   //显示的任务函数(LED显示状态)

sbit P3_4=P3^4;       //蜂鸣器

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned int Gu16SetData=0; //“设置参数”。范围从0到800。LED灯反映该当前值的范围状态
unsigned char Gu8DisplayUpdate=1; //显示的刷新标志

volatile unsigned char vGu8KeySec=0;//按键的触发序号
volatile unsigned char vGu8ShieldVoiceFlag=0;//屏蔽声音的标志

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();       //按键的任务函数
DisplayTask();   //显示的任务函数(LED显示状态)
    }
}


/* 注释一:
* Gu8DisplayUpdate这类“显示刷新变量”在“显示框架”里是很常见的,而且屡用屡爽。
* 目的是,既能及时刷新显示,又能避免主函数“不断去执行显示代码”而影响程序效率。
*/
void DisplayTask(void)   //显示的任务函数(LED显示状态)
{
if(1==Gu8DisplayUpdate)//需要刷新一次显示
{
Gu8DisplayUpdate=0;//及时清零,避免主函数“不断去执行显示代码”而影响程序效率

      if(Gu16SetData<100)
{
BUS_P0=~(1<<0);//第0个灯亮
}
      else if(Gu16SetData<200)
{
BUS_P0=~(1<<1);//第1个灯亮
}
      else if(Gu16SetData<300)
{
BUS_P0=~(1<<2);//第2个灯亮
}
      else if(Gu16SetData<400)
{
BUS_P0=~(1<<3);//第3个灯亮
}
      else if(Gu16SetData<500)
{
BUS_P0=~(1<<4);//第4个灯亮
}
      else if(Gu16SetData<600)
{
BUS_P0=~(1<<5);//第5个灯亮
}
      else if(Gu16SetData<700)
{
BUS_P0=~(1<<6);//第6个灯亮
}
      else
{
BUS_P0=~(1<<7);//第7个灯亮
}
}
}

/* 注释二:
* 按键“先加速后匀速”的识别过程:
* 第一步:每次按一次就触发一次“单击”,如果按下去到松手的时间不超过1秒,则不会进入
*      “连击”模式。
* 第二步:如果按下去不松手的时间超过1秒,则进入“连击”模式。按键触发的节奏
*         不断加快,直至到达某个极限值,然后以此极限值间隔匀速触发。这就是“先加速后匀速”。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;
   static unsigned intSu16KeyContinuityCnt1;//连击计数器
   static unsigned intSu16KeyContinuityTime1=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值

   static unsigned char Su8KeyLock2;      
   static unsigned intSu16KeyCnt2;
   static unsigned intSu16KeyContinuityCnt2;//连击计数器
   static unsigned intSu16KeyContinuityTime2=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值

   //K1按键
   if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。
Su16KeyContinuityCnt1=0;//连击计数器
Su16KeyContinuityTime1=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数,每一次累加额度是1ms
      if(Su16KeyCnt1>=KEY_SHORT_TIME) //按键的“滤波”时间25ms
      {
            Su8KeyLock1=1;      //“自锁”
            vGu8KeySec=1;       //触发一次K1按键      
            Su16KeyCnt1=0;      //清零,为了下一步用来累加“从单击进入连击的间隔时间300ms”
      }
   }
   else if(Su16KeyCnt1<=KEY_ENTER_CONTINUITY_TIME)//按住不松手累加到300ms
   {
      Su16KeyCnt1++; //累加定时中断次数,每一次累加额度是1ms
   }
   else//按住累加到300ms后仍然不放手,这个时候进入有节奏的连续触发
   {
       Su16KeyContinuityCnt1++; //连击计数器开始累加,每一次累加额度是1ms
       if(Su16KeyContinuityCnt1>=Su16KeyContinuityTime1)//按住没松手,每隔一会就触发一次
       {
            Su16KeyContinuityCnt1=0; //清零,为了继续连击。
            vGu8KeySec=1;       //触发一次K1按键   
vGu8ShieldVoiceFlag=1;//把当前按键触发的声音屏蔽掉
if(Su16KeyContinuityTime1>=KEY_SUB_DT_TIME)
{
    //此数值不断被减小,按键的触发速度就不断变快
Su16KeyContinuityTime1=Su16KeyContinuityTime1-KEY_SUB_DT_TIME;//变快节奏
}

if(Su16KeyContinuityTime1<KEY_CONTINUITY_MIN_TIME) //最小间隔时间就是“高速匀速”
{
Su16KeyContinuityTime1=KEY_CONTINUITY_MIN_TIME; //最后以此最高速进行“匀速”
}

       }

}

   //K2按键
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;
Su16KeyContinuityCnt2=0;
Su16KeyContinuityTime2=KEY_CONTINUITY_INITIAL_TIME;
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_SHORT_TIME)
      {
            Su8KeyLock2=1;      
            vGu8KeySec=2;       //触发一次K2按键      
            Su16KeyCnt2=0;         
      }
   }
   else if(Su16KeyCnt2<=KEY_ENTER_CONTINUITY_TIME)
   {
      Su16KeyCnt2++;
   }
   else
   {
       Su16KeyContinuityCnt2++;
       if(Su16KeyContinuityCnt2>=Su16KeyContinuityTime2)
       {
            Su16KeyContinuityCnt2=0;
            vGu8KeySec=2;       //触发一次K2按键   
vGu8ShieldVoiceFlag=1;
if(Su16KeyContinuityTime2>=KEY_SUB_DT_TIME)
{
Su16KeyContinuityTime2=Su16KeyContinuityTime2-KEY_SUB_DT_TIME;
}

if(Su16KeyContinuityTime2<KEY_CONTINUITY_MIN_TIME)
{
Su16KeyContinuityTime2=KEY_CONTINUITY_MIN_TIME;
}
       }
}
}

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1触发的任务
      if(Gu16SetData>0)
{
Gu16SetData--;   //“设置参数”
Gu8DisplayUpdate=1;//刷新显示
}

      if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
                vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
                vGu8BeepTimerFlag=1;
            }

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K2触发的任务
      if(Gu16SetData<800)
{
Gu16SetData++;       //“设置参数”
Gu8DisplayUpdate=1;//刷新显示
}

      if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
                vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
                vGu8BeepTimerFlag=1;
            }

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}
}



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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

吴坚鸿 发表于 2017-12-17 11:45:22

第九十九节: “行列扫描式”矩阵按键的单个触发(原始版)。

【99.1   “行列扫描式”矩阵按键。】

            
                上图99.1.1有源蜂鸣器电路

      
      
                上图99.1.23*3矩阵按键的电路

       上图是3*3的矩阵按键电路,其它4*4或者8*8的矩阵电路原理是一样的,编程思路也是一样的。相对独立按键,矩阵按键因为采用动态行列扫描的方式,能更加节省IO口,比如3*3的3行3列,1行占用1根IO口,1列占用1根IO口,因此3*3矩阵按键占用6个IO口(3+3=6),但是能识别9个按键(3*3=9)。同理,8*8矩阵按键占用16个IO口(8+8=16),但是能识别64个按键(8*8=64)。
      矩阵按键的编程原理。如上图3*3矩阵按键的电路,行IO口(P2.2,P2.1,P2.0)定为输入,列IO口(P2.5,P2.4,P2.3)定为输出。同一时刻,列输出的3个IO口只能有1根是输出L(低电平),其它2根必须全是H(高电平),然后依次轮番切换输出状态,列输出每切换一次,就分别读取一次行输入的3个IO口,这样一次就能识别到3个按键的状态,如果列连续切换3次就可以读取全部9个按键的状态。列的3种输出状态分别是:(P2.5为L,P2.4为H,P2.3为H),(P2.5为H,P2.4为L,P2.3为H),(P2.5为H,P2.4为H,P2.3为L)。为什么列输出每切换一次就能识别到3个按键的状态?因为,首先要明白一个前提,在没有任何按键“被按下”的时候,行输入的3个IO口因为内部上拉电阻的作用,默认状态都是H电平。并且,H与H相互短接输出为H,H与L相互短接输出L,也就是,L(低电平)的优先级最大,任何H(高电平)碰到L(低电平)输出的结果都是L(低电平)。L(低电平)就像数学乘法运算里的数字0,任何数跟0相乘必然等于0。多说一句,这个“L最高优先级”法则是有前提的,就是H(高电平)的产生必须是纯粹靠上拉电阻拉高的H(高电平)才行,比如刚好本教程所用的51单片机内部IO口输出的H(高电平)是依靠内部的上拉电阻产生,如果是其它“非上拉电阻产生的高电平”与“低电平”短接就有“短路烧坏芯片”的风险,这时就需要额外增加“三极管开漏式输出”电路或者外挂“开漏式输出集成芯片”电路。继续回到正题,为什么列输出每切换一次就能识别到3个按键的状态?举个例子,比如当列输出状态处于(P2.5为L,P2.4为H,P2.3为H)下,我们读取行输入的P2.2口,行输入的P2.2与列输出P2.5,P2.4,P2.3的“交叉处”有3个按键S1,S2,S3,此时,如果P2.2口是L(低电平),那么必然是S1“被按下”,因为想让P2.2口是L,只有S1有这个能力,而如果S1没有“被按下”,另外两个S2,S3即使“被按下”,P2.2口也是H而绝对不会为L,因为S2,S3的列输出P2.4为H,P2.3为H,H与H相互短接输出的结果必然为H。
      本节例程实现的功能:9个矩阵按键,每按下1个按键都触发一次蜂鸣器鸣叫。

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME20   //按键去抖动的“滤波”时间

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   

sbit P3_4=P3^4;       //蜂鸣器

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;


volatile unsigned char vGu8KeySec=0;//按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();       //按键的任务函数
    }
}

/* 注释一:
*矩阵按键扫描的详细过程:
*先输出某1列低电平,其它2列输出高电平,延时等待2ms后(等此3列输出同步稳定),
*再分别判断3行的输入IO口, 如果发现哪一行是低电平,就说明对应的某个按键被触发。
*依次循环切换输出的3种状态,并且分别判断输入的3行,就可以检测完9个按键。矩阵按键的
*去抖动处理方法跟我前面讲的独立按键去抖动方法是一样的,不再重复多讲。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   switch(Su8KeyStep)
   {
   case 1:   //按键扫描输出第一列低电平
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;   

          Su16KeyCnt=0;//延时计数器清零
          Su8KeyStep++;//切换到下一个运行步骤
          break;

   case 2:   //延时等待2ms后(等此3列输出同步稳定)。不是按键的去抖动延时。
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   //切换到下一个运行步骤
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep++;//如果没有按键按下,切换到下一个运行步骤
             Su8KeyLock=0;//按键自锁标志清零
             Su16KeyCnt=0; //按键去抖动延时计数器清零,此行非常巧妙      
          }
          else if(0==Su8KeyLock)//有按键按下,且是第一次触发
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=1;//触发1号键 对应S1键
                  }
                        
            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=2;//触发2号键 对应S2键
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=3;//触发3号键 对应S3键
                  }   
            }
   
          }
          break;
   case 4:   //按键扫描输出第二列低电平
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;   

          Su16KeyCnt=0;//延时计数器清零
          Su8KeyStep++;//切换到下一个运行步骤
          break;

   case 5:   //延时等待2ms后(等此3列输出同步稳定)。不是按键的去抖动延时。
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   //切换到下一个运行步骤
          }
          break;

   case 6:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep++;//如果没有按键按下,切换到下一个运行步骤
             Su8KeyLock=0;//按键自锁标志清零
             Su16KeyCnt=0; //按键去抖动延时计数器清零,此行非常巧妙      
          }
          else if(0==Su8KeyLock)//有按键按下,且是第一次触发
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=4;//触发4号键 对应S4键
                  }
                        
            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=5;//触发5号键 对应S5键
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=6;//触发6号键 对应S6键
                  }   
            }
   
          }
          break;
   case 7:   //按键扫描输出第三列低电平
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;   

          Su16KeyCnt=0;//延时计数器清零
          Su8KeyStep++;//切换到下一个运行步骤
          break;

   case 8:   //延时等待2ms后(等此3列输出同步稳定)。不是按键的去抖动延时。
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   //切换到下一个运行步骤
          }
          break;

   case 9:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;//如果没有按键按下,返回到第一步,重新开始扫描!!!!!!
             Su8KeyLock=0;//按键自锁标志清零
             Su16KeyCnt=0; //按键去抖动延时计数器清零,此行非常巧妙      
          }
          else if(0==Su8KeyLock)//有按键按下,且是第一次触发
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=7;//触发7号键 对应S7键
                  }
                        
            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=8;//触发8号键 对应S8键
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      vGu8KeySec=9;//触发9号键 对应S9键
                  }   
            }
   
          }
          break;
   }

}

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //S1触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 2:   //S2触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 3:   //S3触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 4:   //S4触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 5:   //S5触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 6:   //S6触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 7:   //S7触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 8:   //S8触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 9:   //S9触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;
   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

}
}



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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;       

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }       
}

吴坚鸿 发表于 2017-12-17 11:47:31

第一百节: “行列扫描式”矩阵按键的单个触发(优化版)。

【100.1   “行列扫描式”矩阵按键。】

            
                上图100.1.1有源蜂鸣器电路


            
                上图100.1.23*3矩阵按键的电路

       写程序,凡是出现“重复性、相似性”的代码,都可以加入“循环,判断,数组”这类语句对代码进行压缩优化。上一节讲的矩阵按键,代码是记流水账式的,出现很多“重复性、相似性”的代码,是没有经过优化的“原始版”,本节的目的是对上一节的代码进行优化,让大家从中发现一些技巧。
       多说一句,我一直认为,只要单片机容量够,代码多一点少一点并不重要,只要不影响运行效率就行。而且有时候,代码写多一点,可读性非常强,修改起来也非常方便。如果一味的追求压缩代码,就会刻意用很多“循环,判断,数组”等元素,代码虽然紧凑了,但是可分离性,可更改性,可阅读性就没那么强。因此,做项目的时候,某些代码要不要进行压缩,是没有绝对标准的,能因敌而取胜者谓之神。
       本节例程实现的功能:9个矩阵按键,每按下1个按键都触发一次蜂鸣器鸣叫。

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME20   //按键去抖动的“滤波”时间

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   

sbit P3_4=P3^4;       //蜂鸣器

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;


volatile unsigned char vGu8KeySec=0;//按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();       //按键的任务函数
    }
}

/* 注释一:
*矩阵按键扫描的详细过程:
*先输出某1列低电平,其它2列输出高电平,延时等待2ms后(等此3列输出同步稳定),
*再分别判断3行的输入IO口, 如果发现哪一行是低电平,就说明对应的某个按键被触发。
*依次循环切换输出的3种状态,并且分别判断输入的3行,就可以检测完9个按键。矩阵按键的
*去抖动处理方法跟我前面讲的独立按键去抖动方法是一样的,不再重复多讲。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;//本节多增加此变量用来切换当前列的输出

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)//按键扫描输出第一列低电平
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)//按键扫描输出第二列低电平
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   //按键扫描输出第三列低电平
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;//延时计数器清零
          Su8KeyStep++;//切换到下一个运行步骤
          break;

   case 2:   //延时等待2ms后(等此3列输出同步稳定)。不是按键的去抖动延时。
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   //切换到下一个运行步骤
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;//如果没有按键按下,返回到第一个运行步骤重新开始扫描!!!!!!
             Su8KeyLock=0;//按键自锁标志清零
             Su16KeyCnt=0;//按键去抖动延时计数器清零,此行非常巧妙   
             Su8ColumnRecord++;//输出下一列
             if(Su8ColumnRecord>=3)
             {
                Su8ColumnRecord=0; //依次输出完第3列之后,继续从第1列开始输出低电平
             }   
          }
          else if(0==Su8KeyLock)//有按键按下,且是第一次触发
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零

                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           vGu8KeySec=1;//触发1号键 对应S1键
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8KeySec=2;//触发2号键 对应S2键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8KeySec=3;//触发3号键 对应S3键
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零

                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           vGu8KeySec=4;//触发4号键 对应S4键
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8KeySec=5;//触发5号键 对应S5键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8KeySec=6;//触发6号键 对应S6键
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su16KeyCnt=0;
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           vGu8KeySec=7;//触发7号键 对应S7键
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8KeySec=8;//触发8号键 对应S8键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8KeySec=9;//触发9号键 对应S9键
                      }
                  }   
            }

          }
          break;
   }

}

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //S1触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 2:   //S2触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 3:   //S3触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 4:   //S4触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 5:   //S5触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 6:   //S6触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 7:   //S7触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 8:   //S8触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   case 9:   //S9触发的任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

}
}



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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

tt98 发表于 2017-12-18 12:00:56

100节!!{:lol:}

kkfy888 发表于 2017-12-19 21:52:36

默默关注到2023

吴坚鸿 发表于 2017-12-31 16:48:09

第一百零一节: 矩阵按键鼠标式的单击与双击。

【101.1   矩阵按键鼠标式的单击与双击。】

            
                上图101.1.1有源蜂鸣器电路

            
                上图101.1.2LED电路


            
                上图101.1.33*3矩阵按键的电路

      矩阵按键与前面章节独立按键的单击与双击的处理思路是一样的,本节讲矩阵按键的单击与双击,也算是重温之前章节讲的内容。
      鼠标的左键,可以触发单击,也可以触发双击。双击的规则是这样的,两次单击,如果第1次单击与第2次单击的时间比较“短”的时候,则这两次单击就构成双击。编写这个程序的最大亮点是如何控制好第1次单击与第2次单击的时间间隔。程序例程要实现的功能是:以S1按键为例,(1)单击改变LED灯的显示状态。单击一次LED从原来“灭”的状态变成“亮”的状态,或者从原来“亮”的状态变成“灭”的状态,依次循环切换。(2)双击则蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   
#define KEY_SHORT_TIME20   //按键去抖动的“滤波”时间
#define KEY_INTERVAL_TIME80//连续两次单击之间的最大有效时间。因为是矩阵,80不一定是80ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);   
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void DoubleKeyTask(void);   //双击按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮
volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8DoubleKeySec=0;//双击按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      SingleKeyTask();    //单击按键任务函数
      DoubleKeyTask();    //双击按键任务函数
    }
}

/* 注释一:
*矩阵按键扫描的详细过程:
*先输出某1列低电平,其它2列输出高电平,延时等待2ms后(等此3列输出同步稳定),
*再分别判断3行的输入IO口, 如果发现哪一行是低电平,就说明对应的某个按键被触发。
*依次循环切换输出的3种状态,并且分别判断输入的3行,就可以检测完9个按键。矩阵按键的
*去抖动处理方法跟我前面讲的独立按键去抖动方法是一样的。
*/

/* 注释二:
* 双击按键扫描的详细过程:
* 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
*         如果之前已经有按键触发过1次单击,那么启动时间间隔计数器Su16KeyIntervalCnt1,
*         在KEY_INTERVAL_TIME这个允许的时间差范围内,如果一直没有第2次单击触发,
*         则把累加按键触发的次数Su8KeyTouchCnt1也清零,上一次累计的单击数被清零,
*         就意味着下一次新的双击必须重新开始累加两次单击数。
* 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到
*         阀值KEY_SHORT_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt
*         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰,以后凡是用到开关感应器的时候,
*         都可以用类似这样的方法去干扰。
* 第三步:如果按键按下的时间超过了阀值KEY_SHORT_TIME,马上把自锁标志Su8KeyLock置1,
*         防止按住按键不松手后一直触发。与此同时,累加1次按键次数,如果按键次数累加有2次,
*         则认为触发双击按键,并把编号vGu8DoubleKeySec赋值。
* 第四步:等按键松开后,自锁标志Su8KeyLock及时清零解锁,为下一次自锁做准备。并且累加间隔时间,
*         防止两次按键的间隔时间太长。如果连续2次单击的间隔时间太长达到了KEY_INTERVAL_TIME
*         的长度,立即清零当前按键次数的计数器,这样意味着上一次的累加单击数无效,下一次双击
*         必须重新累加新的单击数。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;//用来切换当前列的输出

   static unsigned char Su8KeyTouchCnt1;       //S1按键的次数记录
   static unsigned intSu16KeyIntervalCnt1;   //S1按键的间隔时间计数器

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)//按键扫描输出第一列低电平
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)//按键扫描输出第二列低电平
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   //按键扫描输出第三列低电平
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;//延时计数器清零
          Su8KeyStep++;//切换到下一个运行步骤
          break;

   case 2:   //延时等待2ms后(等此3列输出同步稳定)。不是按键的去抖动延时。
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   //切换到下一个运行步骤
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;//如果没有按键按下,返回到第一个运行步骤重新开始扫描!!!!!!
             Su8KeyLock=0;//按键自锁标志清零
             Su16KeyCnt=0;//按键去抖动延时计数器清零,此行非常巧妙

if(Su8KeyTouchCnt1>=1) //之前已经有按键触发过一次,启动间隔时间的计数器
{
Su16KeyIntervalCnt1++; //按键间隔的时间计数器累加
if(Su16KeyIntervalCnt1>=KEY_INTERVAL_TIME) //达到最大允许的间隔时间,溢出无效
{
Su16KeyIntervalCnt1=0; //时间计数器清零
Su8KeyTouchCnt1=0;   //清零按键的按下的次数,因为间隔时间溢出无效
}
}

             Su8ColumnRecord++;//输出下一列
             if(Su8ColumnRecord>=3)
             {
                Su8ColumnRecord=0; //依次输出完第3列之后,继续从第1列开始输出低电平
             }   
          }
          else if(0==Su8KeyLock)//有按键按下,且是第一次触发
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零

                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           Su16KeyIntervalCnt1=0;   //按键有效间隔的时间计数器清零
                           Su8KeyTouchCnt1++;       //记录当前单击的次数
                           if(1==Su8KeyTouchCnt1)   //只按了1次
                           {
                               vGu8SingleKeySec=1;   //单击任务,触发1号键 对应S1键
                           }
                           else if(Su8KeyTouchCnt1>=2)//连续按了两次以上
                           {
                              Su8KeyTouchCnt1=0;    //统计按键次数清零
                              vGu8SingleKeySec=1;   //单击任务,触发1号键 对应S1键
vGu8DoubleKeySec=1;   //双击任务,触发1号键 对应S1键
                           }
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8SingleKeySec=2;//触发2号键 对应S2键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8SingleKeySec=3;//触发3号键 对应S3键
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零

                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           vGu8SingleKeySec=4;//触发4号键 对应S4键
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8SingleKeySec=5;//触发5号键 对应S5键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8SingleKeySec=6;//触发6号键 对应S6键
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;//去抖动延时计数器
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;//自锁置1,避免一直触发,只有松开按键,此标志位才会被清零
                      if(0==Su8ColumnRecord)//第1列输出低电平
                      {
                           vGu8SingleKeySec=7;//触发7号键 对应S7键
                      }
                      else if(1==Su8ColumnRecord)//第2列输出低电平
                      {
                           vGu8SingleKeySec=8;//触发8号键 对应S8键
                      }
                      else if(2==Su8ColumnRecord)//第3列输出低电平
                      {
                           vGu8SingleKeySec=9;//触发9号键 对应S9键
                      }
                  }   
            }

          }
          break;
   }

}

void SingleKeyTask(void)    //按键单击的任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //S1按键的单击任务

            //通过Gu8LedStatus的状态切换,来反复切换LED的“灭”与“亮”的状态
            if(0==Gu8LedStatus)
            {
                Gu8LedStatus=1; //标识并且更改当前LED灯的状态。0就变成1。
                LedOpen();   //点亮LED
}
            else
            {
                Gu8LedStatus=0; //标识并且更改当前LED灯的状态。1就变成0。
                LedClose();//关闭LED
}

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

   default://其它按键触发的单击

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

}
}

void DoubleKeyTask(void)    //双击按键任务函数,放在主函数内
{
if(0==vGu8DoubleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8DoubleKeySec) //根据不同的按键触发序号执行对应的代码
{
      case 1:   //S1按键的双击任务

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发双击后,发出“嘀”一声
            vGu8BeepTimerFlag=1;

vGu8DoubleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

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

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
/* 注释三:
* 把LED的初始化放在PeripheralInitial而不是放在SystemInitial,是因为LED显示内容对上电
* 瞬间的要求不高。但是,如果是控制继电器,则应该把继电器的输出初始化放在SystemInitial。
*/

    //根据Gu8LedStatus的值来初始化LED当前的显示状态,0代表灭,1代表亮
if(0==Gu8LedStatus)
{
    LedClose();//关闭LED
}
else
{
    LedOpen();   //点亮LED
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

Ilove51andAVR 发表于 2017-12-31 19:19:28

支持一下楼主,希望你能坚持写完,造福坛友,这样的精神很不错!

吴坚鸿 发表于 2018-1-7 12:38:11

第一百零二节: 两个“任意行输入”矩阵按键的“有序”组合触发。
         
【102.1   “异行输入”“同行输入”“有序”。】

         
                上图102.1.1有源蜂鸣器电路

         
                上图102.1.2LED电路


         
                上图102.1.33*3矩阵按键的电路

       “任意行输入”是指能兼容“异行输入”与“同行输入”这两种按键状态。
      何谓“异行输入”何谓“同行输入”?如上图矩阵按键,P2.2,P2.1,P2.0是输入行,P2.5,P2.4,P2.3是输出列。以S1按键为例,很明显,S2和S3都是属于S1的“同行输入”,都是属于P2.2的输入行。除了S2和S3以外,其它所有的按键都是S1的“异行输入”,比如S5按键就是S1的“异行输入”,因为S1是属于P2.2的输入行,而S5是属于P2.1的输入行。
何谓“有序”组合触发?就是两个按键的触发必须遵守“先后顺序”才能构成“组合触发”。比如,像电脑的复制快捷键(Ctrl+C),你必须先按住Ctrl再按住C此时“复制快捷键”才有效,如果你先按住C再按住Ctrl此时“复制快捷键”无效。
       “异行输入”与“同行输入”,相比之下,“同行输入”更难更有代表性,如果把“同行输入”的程序写出来了,那么完全按“同行输入”的思路,就可以把“异行输入”的程序写出来。因此,只要把“同行输入”的程序写出来了,也就意味着“任意行输入”的程序也就实现了。本节以S1和S2的“同行输入”按键为例,S1是主键,类似复制快捷键的Ctrl键;S2是从键,类似复制快捷键的C键。要触发组合键(S1+S2),必须先按住S1再按S2才有效。功能如下:(1)S1每单击一次,LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)如果先按住S1再按S2,就认为构造了“有序”组合键,蜂鸣器发出“嘀”的一声。

#include "REG52.H"

#define KEY_VOICE_TIME   50   
#define KEY_SHORT_TIME20   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);   
void SingleKeyTask(void);   
void DoubleKeyTask(void);   

sbit P3_4=P3^4;      
sbit P1_4=P1^4;      

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0;
volatile unsigned char vGu8SingleKeySec=0;
volatile unsigned char vGu8DoubleKeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      SingleKeyTask();   
      DoubleKeyTask();   
    }
}

/* 注释一:
*两个“任意行输入”矩阵按键“有序”触发的两个最关键地方:
*(1)当S1按键被按下单击触发之后, “马上更新输出列的信号状态”,然后切换到后面的步骤。
*(2)在后面的步骤里,进入到S1和S2两个按键的轮番循环监控之中,如果发现S1按键率先
*   被松开了,就把步骤切换到开始的第一步,重新开始新一轮的按键扫描。
*(3)按照这个模板,只需“更改不同的列输出,判断不同的行输入”,就可以实现“任意行输入”
*   矩阵按键的“有序”组合触发。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;
          Su8KeyStep++;
          break;

   case 2:      //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;
             Su8KeyLock=0;
             Su16KeyCnt=0;

             Su8ColumnRecord++;
             if(Su8ColumnRecord>=3)
             {
                Su8ColumnRecord=0;
             }   
          }
          else if(0==Su8KeyLock)
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=1;   //单击任务,触发1号键 对应S1键

                           //“马上更新输出列的信号状态”
                     COLUMN_OUTPUT1=1;   
                     COLUMN_OUTPUT2=0;   //列2也输出0,非常关键的代码!
                     COLUMN_OUTPUT3=1;

                     Su16KeyCnt=0;//去抖动延时清零,为下一步计时做准备
                           Su8KeyStep++;   //切换到下一步步骤
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=2;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=3;
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=4;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=5;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=6;
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;
                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=7;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=8;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=9;
                      }
                  }   
            }

          }
          break;
   case 4:             //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 5:    //判断S2按键
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S2按键没有被按下
          {
             Su8KeyLock=0;
             Su16KeyCnt=0;

             //“马上更新输出列的信号状态”
         COLUMN_OUTPUT1=0;   //列1输出0,非常关键的代码!
         COLUMN_OUTPUT2=1;
         COLUMN_OUTPUT3=1;

             Su8KeyStep++;    //切换到下一个步骤,监控S1是否率先已经松开
          }
else if(0==Su8KeyLock)
{   
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S2按键被按下
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                        Su8KeyLock=1;//组合按键的自锁
vGu8DoubleKeySec=1;   //触发组合按键(S1+S2)
                  }
            }



}
          break;

   case 6:       //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 7:    //监控S1按键是否率先已经松开
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
{
            Su16KeyCnt=0;
Su8KeyLock=0;   
            Su8KeyStep=1;   //如果S1按键已经松开,返回到第一个运行步骤重新开始扫描

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
                  Su8ColumnRecord=0;
            }
}
else
{
            //“马上更新输出列的信号状态”
          COLUMN_OUTPUT1=1;   
          COLUMN_OUTPUT2=0;   //列2输出0,非常关键的代码!
          COLUMN_OUTPUT3=1;
            Su8KeyStep=4;   //如果S1按键没有松开,继续返回判断S2是否已按下
}
          break;
   }

}

void SingleKeyTask(void)   
{
if(0==vGu8SingleKeySec)
{
return;
}

switch(vGu8SingleKeySec)
{
   case 1:   //S1按键的单击任务,更改LED灯的显示状态

            if(0==Gu8LedStatus)
            {
                Gu8LedStatus=1;
                LedOpen();   
}
            else
            {
                Gu8LedStatus=0;
                LedClose();
}

vGu8SingleKeySec=0;
break;

   default:

vGu8SingleKeySec=0;
break;

}
}

void DoubleKeyTask(void)   
{
if(0==vGu8DoubleKeySec)
{
return;
}

switch(vGu8DoubleKeySec)
{
      case 1:   //S1与S2的组合按键触发,发出“嘀”一声

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;
            vGu8BeepTimerFlag=1;

vGu8DoubleKeySec=0;
break;
}

}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus)
{
    LedClose();
}
else
{
    LedOpen();   
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

tt98 发表于 2018-1-7 14:30:23

力顶鸿哥!!{:victory:}

ju2320601 发表于 2018-1-7 23:40:16

支持一下

吴坚鸿 发表于 2018-1-14 10:45:08

第一百零三节: 两个“任意行输入”矩阵按键的“无序”组合触发。

【103.1   “无序”组合触发。】

            
                上图103.1.1有源蜂鸣器电路

            
                上图103.1.2LED电路


            
                上图103.1.33*3矩阵按键的电路

       “无序”是指两个组合按键不分先后顺序,都能构成组合触发。比如,要触发组合键(S1+S2),先按S1再按S2,或者先按S2再按S1,功能都是一样的。
       本节程序功能如下:(1)S1每单击一次,P1.4所在的LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)S2每单击一次,P1.5所在的LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(3)如果先按住S1再按S2,或者先按住S2再按S1,都认为构造了“无序”组合键,蜂鸣器发出“嘀”的一声。

#include "REG52.H"

#define KEY_VOICE_TIME   50   
#define KEY_SHORT_TIME20   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen_P1_4(void);   
void LedClose_P1_4(void);
void LedOpen_P1_5(void);   
void LedClose_P1_5(void);

void VoiceScan(void);
void KeyScan(void);   
void SingleKeyTask(void);   
void DoubleKeyTask(void);   

sbit P3_4=P3^4;      
sbit P1_4=P1^4;    //P1.4所在的LED
sbit P1_5=P1^5;    //P1.5所在的LED

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus_P1_4=0;//P1.4所在的LED的状态
unsigned char Gu8LedStatus_P1_5=0;//P1.5所在的LED的状态
volatile unsigned char vGu8SingleKeySec=0;
volatile unsigned char vGu8DoubleKeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      SingleKeyTask();   
      DoubleKeyTask();   
    }
}

/* 注释一:
*矩阵按键“无序”触发的两个最关键地方:
*(1)如果是S1按键先被按下并且单击触发之后,“马上更新输出列的信号状态”,然后切换到
*   “S1后面所在的步骤里”,进入到S1和S2两个按键的轮番循环监控之中,如果发现S1按键率先
*   被松开了,就把步骤切换到开始的第一步,重新开始新一轮的按键扫描。
*(2)如果是S2按键先被按下并且单击触发之后,“马上更新输出列的信号状态”,然后切换到
*   “S2后面所在的步骤里”,进入到S1和S2两个按键的轮番循环监控之中,如果发现S2按键率先
*   被松开了,就把步骤切换到开始的第一步,重新开始新一轮的按键扫描。
*(3)上面两个描述中的两种步骤,“S1后面所在的步骤里”和“S2后面所在的步骤里”是分开的,
*   不共用的,这是本节破题的关键。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;
          Su8KeyStep++;
          break;

   case 2:      //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;
             Su8KeyLock=0;
             Su16KeyCnt=0;

             Su8ColumnRecord++;
             if(Su8ColumnRecord>=3)
             {
                Su8ColumnRecord=0;
             }   
          }
          else if(0==Su8KeyLock)
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=1;   //单击任务,触发1号键 对应S1键

                           //“马上更新输出列的信号状态”
                     COLUMN_OUTPUT1=1;   
                     COLUMN_OUTPUT2=0;   //列2也输出0,下一步监控S2,非常关键的代码!
                     COLUMN_OUTPUT3=1;

                     Su16KeyCnt=0;//去抖动延时清零,为下一步计时做准备
                           Su8KeyStep=4;   //切换到“S1后面所在的步骤里”,破题的关键!!!
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=2;    //单击任务,触发2号键 对应S2键

                           //“马上更新输出列的信号状态”
                     COLUMN_OUTPUT1=0;//列1也输出0,下一步监控S1,非常关键的代码!
                     COLUMN_OUTPUT2=1;
                     COLUMN_OUTPUT3=1;

                     Su16KeyCnt=0;//去抖动延时清零,为下一步计时做准备
                           Su8KeyStep=8;   //切换到“S2后面所在的步骤里”,破题的关键!!!
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=3;
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=4;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=5;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=6;
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;
                      if(0==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=7;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=8;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8SingleKeySec=9;
                      }
                  }   
            }

          }
          break;

/*--------------“S1后面所在的步骤里”------------------*/
   case 4:             //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 5:    //判断S2按键
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S2按键没有被按下
          {
             Su8KeyLock=0;
             Su16KeyCnt=0;

             //“马上更新输出列的信号状态”
         COLUMN_OUTPUT1=0;   //列1输出0,下一步监控S1,非常关键的代码!
         COLUMN_OUTPUT2=1;
         COLUMN_OUTPUT3=1;

             Su8KeyStep++;    //切换到下一个步骤,监控S1是否率先已经松开
          }
else if(0==Su8KeyLock)
{   
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S2按键被按下
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                        Su8KeyLock=1;//组合按键的自锁
vGu8DoubleKeySec=1;   //触发组合按键(S1+S2)
                  }
            }



}
          break;

   case 6:       //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 7:    //监控S1按键是否率先已经松开
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
{
            Su16KeyCnt=0;
Su8KeyLock=0;   
            Su8KeyStep=1;   //如果S1按键已经松开,返回到第一个运行步骤重新开始扫描

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
                  Su8ColumnRecord=0;
            }
}
else
{
            //“马上更新输出列的信号状态”
          COLUMN_OUTPUT1=1;   
          COLUMN_OUTPUT2=0;   //列2输出0,下一步监控S2,非常关键的代码!
          COLUMN_OUTPUT3=1;
            Su8KeyStep=4;   //如果S1按键没有松开,继续返回判断S2是否已按下
}
          break;

/*--------------“S2后面所在的步骤里”------------------*/
   case 8:             //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 9:    //判断S1按键
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S1按键没有被按下
          {
             Su8KeyLock=0;
             Su16KeyCnt=0;

             //“马上更新输出列的信号状态”
         COLUMN_OUTPUT1=1;   
         COLUMN_OUTPUT2=0;//列2输出0,下一步监控S2,非常关键的代码!
         COLUMN_OUTPUT3=1;

             Su8KeyStep++;    //切换到下一个步骤,监控S2是否率先已经松开
          }
else if(0==Su8KeyLock)
{   
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //S1按键被按下
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                        Su8KeyLock=1;//组合按键的自锁
vGu8DoubleKeySec=1;   //触发组合按键(S1+S2)
                  }
            }



}
          break;

   case 10:       //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)   
          {
            Su16KeyCnt=0;
Su8KeyLock=0;   //关键语句!自锁清零,为下一步自锁组合按键做准备
            Su8KeyStep++;   
          }
          break;

   case 11:    //监控S2按键是否率先已经松开
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
{
            Su16KeyCnt=0;
Su8KeyLock=0;   
            Su8KeyStep=1;   //如果S2按键已经松开,返回到第一个运行步骤重新开始扫描

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
                  Su8ColumnRecord=0;
            }
}
else
{
            //“马上更新输出列的信号状态”
          COLUMN_OUTPUT1=0;    //列1输出0,下一步监控S1,非常关键的代码!
          COLUMN_OUTPUT2=1;   
          COLUMN_OUTPUT3=1;
            Su8KeyStep=8;   //如果S2按键没有松开,继续返回判断S1是否已按下
}
          break;


   }

}

void SingleKeyTask(void)   
{
if(0==vGu8SingleKeySec)
{
return;
}

switch(vGu8SingleKeySec)
{
   case 1:   //S1按键的单击任务,更改P1.4所在的LED灯的显示状态

            if(0==Gu8LedStatus_P1_4)
            {
                Gu8LedStatus_P1_4=1;
                LedOpen_P1_4();   
}
            else
            {
                Gu8LedStatus_P1_4=0;
                LedClose_P1_4();
}

vGu8SingleKeySec=0;
break;

   case 2:   //S2按键的单击任务,更改P1.5所在的LED灯的显示状态

            if(0==Gu8LedStatus_P1_5)
            {
                Gu8LedStatus_P1_5=1;
                LedOpen_P1_5();   
}
            else
            {
                Gu8LedStatus_P1_5=0;
                LedClose_P1_5();
}

vGu8SingleKeySec=0;
break;

   default:

vGu8SingleKeySec=0;
break;

}
}

void DoubleKeyTask(void)   
{
if(0==vGu8DoubleKeySec)
{
return;
}

switch(vGu8DoubleKeySec)
{
      case 1:   //S1与S2的组合按键触发,发出“嘀”一声

            vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;
            vGu8BeepTimerFlag=1;

vGu8DoubleKeySec=0;
break;
}

}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus_P1_4)
{
    LedClose_P1_4();
}
else
{
    LedOpen_P1_4();   
}

if(0==Gu8LedStatus_P1_5)
{
    LedClose_P1_5();
}
else
{
    LedOpen_P1_5();   
}

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void LedOpen_P1_4(void)
{
P1_4=0;
}

void LedClose_P1_4(void)
{
P1_4=1;
}

void LedOpen_P1_5(void)
{
P1_5=0;
}

void LedClose_P1_5(void)
{
P1_5=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

吴坚鸿 发表于 2018-1-22 15:29:54


第一百零四节: 矩阵按键“一键两用”的短按与长按。

【104.1   “一键两用”的短按与长按。】

            
                上图104.1.1有源蜂鸣器电路

            
                上图104.1.2LED电路


            
                上图104.1.33*3矩阵按键的电路

      矩阵按键与前面章节独立按键的“短按与长按”的处理思路是一样的,本节讲矩阵按键的“短按与长按”,也算是重温之前章节讲的内容。“短按与长按”的原理是依赖“按键按下的时间长度”来区分识别。“短按”是指从按下的“下降沿”到松手的“上升沿”时间,“长按”是指从按下的“下降沿”到一直按住不松手的“低电平持续时间”。本节的例程功能如下:(1)S1每“短按”一次,LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)S1每“长按”一次,蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME20    //按键的“短按”兼“滤波”的“稳定时间”
#define KEY_LONG_TIME400    //按键的“长按”兼“滤波”的“稳定时间”

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen_P1_4(void);   
void LedClose_P1_4(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   

sbit P3_4=P3^4;      
sbit P1_4=P1^4;   

sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus_P1_4=0;
volatile unsigned char vGu8KeySec=0;//短按与长按共用一个全局变量vGu8KeySec来传递按键信息

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      KeyTask();   
    }
}

/* 注释一:
*本节破题的关键:
*矩阵按键涉及的按键数量很多,但是实际项目上一般只需要少数个别按键具备这种
*“短按”与“长按”的特殊技能,因此,在代码上,必须把这类“特殊技能按键”与
*“大众按键”区分开来,才能相互清晰互不干扰。本节的“特殊技能按键”是S1。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;

   static unsigned char Su8KeyShortFlag_S1=0;//S1按键专属的“短按”触发标志

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;
          Su8KeyStep++;
          break;

   case 2:      //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
             Su8KeyStep=1;
             Su8KeyLock=0;
             Su16KeyCnt=0;

             if(1==Su8KeyShortFlag_S1)//松手的时候,如果“短按”标志有效就触发一次“短按”
             {
Su8KeyShortFlag_S1=0;   //先清零“短按”标志避免一直触发。
vGu8KeySec=1;    //触发S1的“短按”
}

             Su8ColumnRecord++;
             if(Su8ColumnRecord>=3)
             {
                Su8ColumnRecord=0;
             }   
          }
          else if(0==Su8KeyLock)
          {
            //以下第1行,直接把S1按键单独扣出来,用“&&0==Su8ColumnRecord”作为筛选条件
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3&&0==Su8ColumnRecord)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME) //“短按”兼“滤波”的“稳定时间”      
{
                      //注意,这里不能“自锁”。后面“长按”触发的时候才“自锁”。
                      Su8KeyShortFlag_S1=1;    //S1的“短按”标志有效,待松手时触发。
               }

               if(Su16KeyCnt>=KEY_LONG_TIME) //“长按”兼“滤波”的“稳定时间”
               {
                      Su8KeyLock=1;      //此时“长按”触发才“自锁”
Su8KeyShortFlag_S1=0;//既然此时“长按”有效,那么就要废除潜在的“短按”。
                      vGu8KeySec=21; //触发S1的“长按”
               }

            }
            else if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

//既然S1按键已经被上面几行代码单独扣出来,这里就直接从S2按键开始判断
                      if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=2;   
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8KeySec=3;
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=4;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=5;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8KeySec=6;
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;
                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=7;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=8;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8KeySec=9;
                      }
                  }   
            }

          }
          break;

   }

}

void KeyTask(void)   
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //S1按键的“短按”任务,更改P1.4所在的LED灯的显示状态

            if(0==Gu8LedStatus_P1_4)
            {
                Gu8LedStatus_P1_4=1;
                LedOpen_P1_4();   
}
            else
            {
                Gu8LedStatus_P1_4=0;
                LedClose_P1_4();
}

vGu8KeySec=0;
break;

//以下S1按键的“长按”直接选择case 21的“21”,是为了不占用前排其它按键的编号。
   case 21:   //S1按键的“长按”任务,蜂鸣器发出“嘀”一声
vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//蜂鸣器发出“嘀”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   default:

vGu8KeySec=0;
break;

}
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus_P1_4)
{
    LedClose_P1_4();
}
else
{
    LedOpen_P1_4();   
}

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void LedOpen_P1_4(void)
{
P1_4=0;
}

void LedClose_P1_4(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

吴坚鸿 发表于 2018-1-28 13:05:20

第一百零五节: 矩阵按键按住不松手的连续均匀触发。

【105.1   按住不松手的连续均匀触发。】

            
                上图105.1.1有源蜂鸣器电路

            
                上图105.1.2LED电路


            
                上图105.1.33*3矩阵按键的电路

       矩阵按键与前面章节独立按键的“按住不松手的连续均匀触发”的处理思路是一样的。在电脑上删除某个文件某行文字的时候,单击一次“退格按键”,就删除一个文字,如果按住“退格按键”不松手,就会“连续均匀”的触发“删除”的功能,自动逐个把整行文字删除清空,这就是“按住不松手的连续均匀触发”应用案例之一。除此之外,在很多需要人机交互的项目中都有这样的功能,为了快速加减某个数值,按住某个按键不松手,某个数值有节奏地快速往上加或者快速往下减。这种“按住不松手连续均匀触发”的按键识别,在程序上有“3个时间”需要留意,第1个是按键单击的“滤波”时间,第2个是按键“从单击进入连击”的间隔时间(此时间是“单击”与“连击”的分界线),第3个是按键“连击”的间隔时间,
       本节例程实现的功能如下:(1)8个受按键控制的跑马灯在某一时刻只有1个LED亮,每触发一次S1按键,“亮的LED”就“往左边跑一步”;相反,每触发一次S9按键,“亮的LED”就“往右边跑一步”。如果按住S1或者S9不松手就连续触发,“亮的LED”就“连续跑”,一直跑到左边或者右边的尽头。(2)按键每“单击”一次S1或者S9蜂鸣器就鸣叫一次,但是,当按键“从单击进入连击”后,蜂鸣器就不鸣叫。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME20    //按键单击的“滤波”时间

#define KEY_ENTER_CONTINUITY_TIME    240//按键“从单击进入连击”的间隔时间
#define KEY_CONTINUITY_TIME    64   //按键“连击”的间隔时间

#define BUS_P0    P0   //8个LED灯一一对应单片机的P0口总线

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void DisplayTask(void);   //显示的任务函数(LED显示状态)

sbit P3_4=P3^4;      


sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //LED灯的状态
unsigned char Gu8DisplayUpdate=1; //显示的刷新标志

volatile unsigned char vGu8KeySec=0;//按键的触发序号
volatile unsigned char vGu8ShieldVoiceFlag=0;//屏蔽声音的标志

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      KeyTask();   
DisplayTask();   //显示的任务函数(LED显示状态)
    }
}

/* 注释一:
* Gu8DisplayUpdate这类“显示刷新变量”在“显示框架”里是很常见的,而且屡用屡爽。
* 目的是,既能及时刷新显示,又能避免主函数“不断去执行显示代码”而影响程序效率。
*/

void DisplayTask(void)   //显示的任务函数(LED显示状态)
{
if(1==Gu8DisplayUpdate)//需要刷新一次显示
{
Gu8DisplayUpdate=0;//及时清零,避免主函数“不断去执行显示代码”而影响程序效率

//Gu8LedStatus是左移的位数,范围(0至7),决定了跑马灯的显示状态。
BUS_P0=~(1<<Gu8LedStatus);//“左移<<”之后的“取反~”,因为LED电路是灌入式驱动方式。
}
}

/* 注释二:
*本节破题的关键:
*矩阵按键涉及的按键数量很多,但是实际项目上一般只需要少数个别按键具备这种
*“单击”与“连续均匀触发”的特殊技能,因此,在代码上,必须把这类“特殊技能按键”与
*“大众按键”区分开来,才能相互清晰互不干扰。本节的“特殊技能按键”是S1和S9。
*如果觉得本节的讲解不够详细具体,请先阅读一下前面章节“独立按键按住不松手的连续均匀触发”。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;

   static unsigned char Su8ColumnRecord=0;

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;
          Su8KeyStep++;
          break;

   case 2:      //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          else if(0==Su8KeyLock)
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                            vGu8KeySec=1;    //触发一次单击
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                            Su8KeyStep=4;    //跳到S1按键的专属区,脱离大众按键
                      }
                      else if(1==Su8ColumnRecord)
                      {
                            vGu8KeySec=2;   
                      }
                      else if(2==Su8ColumnRecord)
                      {
                            vGu8KeySec=3;
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=4;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=5;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8KeySec=6;
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;
                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=7;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=8;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                            vGu8KeySec=9;    //触发一次单击
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                            Su8KeyStep=6;    //跳到S9按键的专属区,脱离大众按键
                      }
                  }   
            }

          }
          break;

/*---------S1按键的专属区----------------*/
   case 4:
          if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //仅判断S1按键,避免交叉影响
          {
Su16KeyCnt++;
if(Su16KeyCnt>=KEY_ENTER_CONTINUITY_TIME)//该时间是“单击”与“连击”的分界线
            {
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                  Su8KeyStep=5;    //S1按键进入有节奏的连续触发      
}
}
          else //如果期间检查到S1按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }

          break;
   case 5://S1按键进入有节奏的连续触发
          if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //仅判断S1按键,避免交叉影响
          {
Su16KeyCnt++;
            if(Su16KeyCnt>=KEY_CONTINUITY_TIME)//该时间是“连击”的时间
            {
                  Su16KeyCnt=0;         //清零,为了继续连击。
                  vGu8KeySec=1;         //触发一次S1按键   
vGu8ShieldVoiceFlag=1;//因为连击,把当前按键触发的声音屏蔽掉   
}
}
          else //如果期间检查到S1按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          break;

/*---------S9按键的专属区----------------*/
   case 6:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3) //仅判断S9按键,避免交叉影响
          {
Su16KeyCnt++;
if(Su16KeyCnt>=KEY_ENTER_CONTINUITY_TIME)//该时间是“单击”与“连击”的分界线
            {
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                  Su8KeyStep=7;    //S9按键进入有节奏的连续触发      
}
}
          else //如果期间检查到S9按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }

          break;
   case 7://S9按键进入有节奏的连续触发
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3) //仅判断S9按键,避免交叉影响
          {
Su16KeyCnt++;
            if(Su16KeyCnt>=KEY_CONTINUITY_TIME)//该时间是“连击”的时间
            {
                  Su16KeyCnt=0;         //清零,为了继续连击。
                  vGu8KeySec=9;         //触发一次S9按键   
vGu8ShieldVoiceFlag=1;//因为连击,把当前按键触发的声音屏蔽掉   
}
}
          else //如果期间检查到S9按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          break;


   }

}

void KeyTask(void)   
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //S1按键的任务
if(Gu8LedStatus>0)
{
Gu8LedStatus--;//控制LED“往左边跑”
Gu8DisplayUpdate=1;//刷新显示
}

if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
    vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
    vGu8BeepTimerFlag=1;
}

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 9:   //S9按键的任务
if(Gu8LedStatus<7)
{
Gu8LedStatus++;//控制LED“往右边跑”
Gu8DisplayUpdate=1;//刷新显示
}

if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
    vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
    vGu8BeepTimerFlag=1;
}

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   default:

vGu8KeySec=0;
break;

}
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

zcp17521 发表于 2018-1-28 22:34:25

LZ,辛苦了。

lylm123 发表于 2018-1-29 21:23:43

2018继续,用过楼主的矩阵,确实不错,顶一下。。。。。

anyanggcmy 发表于 2018-1-30 15:51:55

期待楼主更新

tt98 发表于 2018-1-30 22:09:22

期待楼主再次更新

吴坚鸿 发表于 2018-2-4 12:29:13

第一百零六节: 矩阵按键按住不松手的“先加速后匀速”触发。

【106.1   按住不松手的先加速后匀速触发。】

            
                上图106.1.1有源蜂鸣器电路

            
                上图106.1.2LED电路


            
                上图106.1.33*3矩阵按键的电路

      矩阵按键与前面章节“独立按键按住不松手的先加速后匀速的触发”的处理思路是一样的。 当“连续加”或者“连续减”的数据范围很大的时候,就需要按键的加速与匀速相结合的触发方式。“加速”是指按住按键不松手,按键刚开始触发是从慢到快的渐进过程,当“加速”到某个特别快的速度的时候,就“不再加速”,而是以该“恒定高速”进行“连续匀速”触发。这种触发方式,“加速”和“匀速”是相辅相成缺一不可的,为什么?假如没有“加速”只有“匀速”,那么刚按下按键就直接以最高速的“匀速”进行,就会跑过头,缺乏微调功能;而假如没有“匀速”只有“加速”,那么按下按键不松手后,速度就会一直不断飙升,最后失控过冲。
       本节例程实现的功能如下:
      (1)要更改一个“设置参数”(一个全局变量),参数的范围是0到800。
      (2)8个受“设置参数”控制的跑马灯在某一时刻只有1个LED亮,每触发一次S1按键,该“设置参数”就自减1,最小值为0;相反,每触发一次S9按键,该“设置参数”就自加1,最大值为800。
      (3)LED灯实时显示“设置参数”的范围状态:
            只有第0个LED灯亮:0<=“设置参数”<100。
            只有第1个LED灯亮:100<=“设置参数”<200。
            只有第2个LED灯亮:200<=“设置参数”<300。
            只有第3个LED灯亮:300<=“设置参数”<400。
            只有第4个LED灯亮:400<=“设置参数”<500。
            只有第5个LED灯亮:500<=“设置参数”<600。
            只有第6个LED灯亮:600<=“设置参数”<700。
            只有第7个LED灯亮:700<=“设置参数”<=800。
      (4)按键每“单击”一次蜂鸣器就鸣叫一次,但是,当按键“从单击进入连击”后,蜂鸣器就不鸣叫。

#include "REG52.H"

#define KEY_VOICE_TIME   50   

#define KEY_SHORT_TIME20    //按键单击的“滤波”时间

#define KEY_ENTER_CONTINUITY_TIME    240//按键“从单击进入连击”的间隔时间
#define KEY_CONTINUITY_INITIAL_TIME    64//按键“连击”起始的预设间隔时间
#define KEY_SUB_DT_TIME   6      //按键在“加速”时每次减小的时间。
#define KEY_CONTINUITY_MIN_TIME   8//按键时间减小到最后的“匀速”间隔时间。

#define BUS_P0    P0   //8个LED灯一一对应单片机的P0口总线

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void DisplayTask(void);   //显示的任务函数(LED显示状态)

sbit P3_4=P3^4;      


sbit ROW_INPUT1=P2^2;//第1行输入口。
sbit ROW_INPUT2=P2^1;//第2行输入口。
sbit ROW_INPUT3=P2^0;//第3行输入口。

sbit COLUMN_OUTPUT1=P2^5;//第1列输出口。
sbit COLUMN_OUTPUT2=P2^4;//第2列输出口。
sbit COLUMN_OUTPUT3=P2^3;//第3列输出口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned int Gu16SetData=0; //“设置参数”。范围从0到800。LED灯反映该当前值的范围状态
unsigned char Gu8DisplayUpdate=1; //显示的刷新标志

volatile unsigned char vGu8KeySec=0;//按键的触发序号
volatile unsigned char vGu8ShieldVoiceFlag=0;//屏蔽声音的标志

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
      KeyTask();   
DisplayTask();   //显示的任务函数(LED显示状态)
    }
}

/* 注释一:
* Gu8DisplayUpdate这类“显示刷新变量”在“显示框架”里是很常见的,而且屡用屡爽。
* 目的是,既能及时刷新显示,又能避免主函数“不断去执行显示代码”而影响程序效率。
*/

void DisplayTask(void)   //显示的任务函数(LED显示状态)
{
if(1==Gu8DisplayUpdate)//需要刷新一次显示
{
Gu8DisplayUpdate=0;//及时清零,避免主函数“不断去执行显示代码”而影响程序效率

      if(Gu16SetData<100)
{
BUS_P0=~(1<<0);//第0个灯亮
}
      else if(Gu16SetData<200)
{
BUS_P0=~(1<<1);//第1个灯亮
}
      else if(Gu16SetData<300)
{
BUS_P0=~(1<<2);//第2个灯亮
}
      else if(Gu16SetData<400)
{
BUS_P0=~(1<<3);//第3个灯亮
}
      else if(Gu16SetData<500)
{
BUS_P0=~(1<<4);//第4个灯亮
}
      else if(Gu16SetData<600)
{
BUS_P0=~(1<<5);//第5个灯亮
}
      else if(Gu16SetData<700)
{
BUS_P0=~(1<<6);//第6个灯亮
}
      else
{
BUS_P0=~(1<<7);//第7个灯亮
}
}
}

/* 注释二:
*本节破题的关键:
*矩阵按键涉及的按键数量很多,但是实际项目上一般只需要少数个别按键具备这种
*“单击”与“先加速后均匀触发”的特殊技能,因此,在代码上,必须把这类“特殊技能按键”与
*“大众按键”区分开来,才能相互清晰互不干扰。本节的“特殊技能按键”是S1和S9。
*如果觉得本节的讲解不够详细具体,请先阅读一下前面章节“独立按键按住不松手的先加速后匀速触发”。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock=0;      
   static unsigned intSu16KeyCnt=0;
   static unsigned char Su8KeyStep=1;
   static unsigned intSu16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值

   static unsigned char Su8ColumnRecord=0;

   switch(Su8KeyStep)
   {
   case 1:   
          if(0==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=0;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=1;
}
          else if(1==Su8ColumnRecord)
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=0;
          COLUMN_OUTPUT3=1;
}
          else   
{
          COLUMN_OUTPUT1=1;      
          COLUMN_OUTPUT2=1;
          COLUMN_OUTPUT3=0;
}
          Su16KeyCnt=0;
          Su8KeyStep++;
          break;

   case 2:      //等待列输出稳定,但不是去抖动延时
          Su16KeyCnt++;
          if(Su16KeyCnt>=2)
          {
             Su16KeyCnt=0;
             Su8KeyStep++;   
          }
          break;

   case 3:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;
Su16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          else if(0==Su8KeyLock)
          {
            if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                            vGu8KeySec=1;    //触发一次单击
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                            Su8KeyStep=4;    //跳到S1按键的专属区,脱离大众按键
                      }
                      else if(1==Su8ColumnRecord)
                      {
                            vGu8KeySec=2;   
                      }
                      else if(2==Su8ColumnRecord)
                      {
                            vGu8KeySec=3;
                      }
                  }

            }
            else if(1==ROW_INPUT1&&0==ROW_INPUT2&&1==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;

                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=4;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=5;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                           vGu8KeySec=6;
                      }
                  }   
            }
            else if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3)
            {
                  Su16KeyCnt++;
                  if(Su16KeyCnt>=KEY_SHORT_TIME)
                  {
                      Su8KeyLock=1;
                      if(0==Su8ColumnRecord)
                      {
                           vGu8KeySec=7;
                      }
                      else if(1==Su8ColumnRecord)
                      {
                           vGu8KeySec=8;
                      }
                      else if(2==Su8ColumnRecord)
                      {
                            vGu8KeySec=9;    //触发一次单击
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                            Su8KeyStep=6;    //跳到S9按键的专属区,脱离大众按键
                      }
                  }   
            }

          }
          break;

/*---------S1按键的专属区----------------*/
   case 4:
          if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //仅判断S1按键,避免交叉影响
          {
Su16KeyCnt++;
if(Su16KeyCnt>=KEY_ENTER_CONTINUITY_TIME)//该时间是“单击”与“连击”的分界线
            {
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                  Su8KeyStep=5;    //S1按键进入有节奏的连续触发      
}
}
          else //如果期间检查到S1按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;
Su16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }

          break;
   case 5://S1按键进入有节奏的连续触发
          if(0==ROW_INPUT1&&1==ROW_INPUT2&&1==ROW_INPUT3) //仅判断S1按键,避免交叉影响
          {
Su16KeyCnt++;
            if(Su16KeyCnt>=Su16KeyContinuityTime)//该时间是“刚开始不断减小,最后不变”
            {
                  Su16KeyCnt=0;         //清零,为了继续连击。
                  vGu8KeySec=1;         //触发一次S1按键   
vGu8ShieldVoiceFlag=1;//因为连击,把当前按键触发的声音屏蔽掉   
if(Su16KeyContinuityTime>=KEY_SUB_DT_TIME)
{
          //Su16KeyContinuityTime数值不断被减小,按键的触发速度就不断变快
Su16KeyContinuityTime=Su16KeyContinuityTime-KEY_SUB_DT_TIME;//变快节奏
}

//最小间隔时间KEY_CONTINUITY_MIN_TIME就是“高速匀速”
if(Su16KeyContinuityTime<KEY_CONTINUITY_MIN_TIME)
{
//最后以KEY_CONTINUITY_MIN_TIME时间为最高速进行“匀速”
Su16KeyContinuityTime=KEY_CONTINUITY_MIN_TIME;
}

}
}
          else //如果期间检查到S1按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;
Su16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。


            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          break;

/*---------S9按键的专属区----------------*/
   case 6:
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3) //仅判断S9按键,避免交叉影响
          {
Su16KeyCnt++;
if(Su16KeyCnt>=KEY_ENTER_CONTINUITY_TIME)//该时间是“单击”与“连击”的分界线
            {
Su16KeyCnt=0;    //计时器清零,为即将来临的计时做准备
                  Su8KeyStep=7;    //S9按键进入有节奏的连续触发      
}
}
          else //如果期间检查到S9按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;
Su16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }

          break;
   case 7://S9按键进入有节奏的连续触发
          if(1==ROW_INPUT1&&1==ROW_INPUT2&&0==ROW_INPUT3) //仅判断S9按键,避免交叉影响
          {
Su16KeyCnt++;
            if(Su16KeyCnt>=Su16KeyContinuityTime)//该时间是“刚开始不断减小,最后不变”
            {
                  Su16KeyCnt=0;         //清零,为了继续连击。
                  vGu8KeySec=9;         //触发一次S9按键   
vGu8ShieldVoiceFlag=1;//因为连击,把当前按键触发的声音屏蔽掉   
if(Su16KeyContinuityTime>=KEY_SUB_DT_TIME)
{
          //Su16KeyContinuityTime数值不断被减小,按键的触发速度就不断变快
Su16KeyContinuityTime=Su16KeyContinuityTime-KEY_SUB_DT_TIME;//变快节奏
}

//最小间隔时间KEY_CONTINUITY_MIN_TIME就是“高速匀速”
if(Su16KeyContinuityTime<KEY_CONTINUITY_MIN_TIME)
{
//最后以KEY_CONTINUITY_MIN_TIME时间为最高速进行“匀速”
Su16KeyContinuityTime=KEY_CONTINUITY_MIN_TIME;
}
}
}
          else //如果期间检查到S9按键已经松手
          {
            Su8KeyStep=1;    //返回步骤1继续扫描
            Su8KeyLock=0;
            Su16KeyCnt=0;
Su16KeyContinuityTime=KEY_CONTINUITY_INITIAL_TIME;//动态时间阀值。重装初始值。

            Su8ColumnRecord++;
            if(Su8ColumnRecord>=3)
            {
               Su8ColumnRecord=0;
            }   
          }
          break;


   }
}

void KeyTask(void)   
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //S1按键的任务
      if(Gu16SetData>0)
{
Gu16SetData--;   //“设置参数”
Gu8DisplayUpdate=1;//刷新显示
}

if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
    vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
    vGu8BeepTimerFlag=1;
}

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 9:   //S9按键的任务

      if(Gu16SetData<800)
{
Gu16SetData++;       //“设置参数”
Gu8DisplayUpdate=1;//刷新显示
}

if(0==vGu8ShieldVoiceFlag) //声音没有被屏蔽
{
    vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//发出“嘀”一声
    vGu8BeepTimerFlag=1;
}

vGu8ShieldVoiceFlag=0;//及时把屏蔽标志清零,避免平时正常的单击声音也被淹没。
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   default:

vGu8KeySec=0;
break;

}
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

kkfy888 发表于 2018-2-10 17:30:36

2018继续,继续为你点赞!

吴坚鸿 发表于 2018-2-11 11:14:37

第一百零七节: 开关感应器的识别与软件滤波。

【107.1   开关感应器的识别与软件滤波。】


               
                上图107.1.1独立按键模拟开关感应器

               
                上图107.1.2LED电路

       什么叫开关感应器?凡是只能输出0和1这两种状态的感应器都可以统称为开关感应器。前面花了大量的章节讲按键,按键的识别主要是识别电平变化状态的“下降沿”,程序代码中有1个特别的变量标志叫“自锁标志”,还有1个用来消除抖动的“计时器”。本节讲的开关感应器跟按键很相似,差别在于,开关感应器是识别电平变化状态的“电平”,程序代码中没有“自锁标志”,但是多增加了1个用来消除抖动的“计时器”,也就是一共有两个用来消除抖动的“计时器”,这两个“计时器”相互“清零”相互“抗衡”,从而实现了开关感应器的“消抖”处理,专业术语也叫“软件滤波”。消抖的时间跟按键差不多,我的经验值是20ms到30ms之间,我平时在项目中喜欢用20ms。
       在显示框架方面,除了之前讲过Gu8DisplayUpdate这类“显示刷新变量”,本节介绍另外一种常用的显示框架,原理是“某数值跟上一次对比,如果发生了变化(两数值不一样),则自动刷新显示,并及时记录当前值”。
       本节例程实现的功能如下:用K1独立按键模拟开关感应器,K1独立按键“没有被按下”时是高电平,单片机识别到这种“高电平”,就让P1.4所在的LED灯发亮;K1独立按键“被按下”时是低电平,单片机识别到这种“低电平”,就让P1.4所在的LED灯熄灭。

#include "REG52.H"

#define SENSOR_TIME20    //开关感应器的“滤波”时间

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void VoiceScan(void);
void SensorScan(void);   
void DisplayTask(void);   //显示的任务函数(LED显示状态)

sbit P1_4=P1^4;      
sbit Sensor_K1_sr=P2^2;   //开关感应器K1所在的引脚

volatile unsigned char vGu8Sensor_K1=0;//K1开关感应器的当前电平状态。

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{   
DisplayTask();   //显示的任务函数(LED显示状态)
    }
}

/* 注释一:
* 后缀为_Last这类“对比上一次数值发生变化而自动刷新显示”在“显示框架”里是很常见的,
* 目的是,既能及时刷新显示,又能避免主函数“不断去执行显示代码”而影响程序效率。
*/

void DisplayTask(void)   //显示的任务函数(LED显示状态)
{
   // Su8Sensor_K1_Last初始化取值255,只要不为0或者1就行,目的是让上电就发生第一次刷新。
static unsigned char Su8Sensor_K1_Last=255;//记录K1开关感应器上一次的电平状态。

if(Su8Sensor_K1_Last!=vGu8Sensor_K1)//如果当前值与上一次值不一样,就自动刷新
{
Su8Sensor_K1_Last=vGu8Sensor_K1;//及时记录最新值,避免主函数“不断去执行显示代码”

      if(0==vGu8Sensor_K1) //如果当前电平状态为“低电平”,LED熄灭
{
P1_4=1;//LED熄灭
}
      else//如果当前电平状态为“高电平”,LED发亮
{
P1_4=0;//LED发亮
}
}
}

/* 注释二:
*本节破题的关键:
*两个“计时器”相互“清零”相互“抗衡”,从而实现了开关感应器的“消抖”处理,
*专业术语也叫“软件滤波”。这种滤波方式,不管是从“高转成低”,还是“低转成高”,
*如果在某个瞬间出现干扰抖动,某个计数器都会及时被“清零”,从而起到非常高效的消抖滤波作用。
*/

void SensorScan(void)//此函数放在定时中断里每1ms扫描一次,用来识别和滤波开关感应器
{
      static unsigned int Su16Sensor_K1_H_Cnt=0;//判断高电平的计时器
      static unsigned int Su16Sensor_K1_L_Cnt=0;//判断低电平的计时器

      if(0==Sensor_K1_sr)
          {
                  Su16Sensor_K1_H_Cnt=0;//在判断低电平的时候,高电平的计时器被清零,巧妙极了!
                  Su16Sensor_K1_L_Cnt++;
                  if(Su16Sensor_K1_L_Cnt>=SENSOR_TIME)
                  {
                      Su16Sensor_K1_L_Cnt=0;
                        vGu8Sensor_K1=0;   //此全局变量反馈当前电平的状态
                  }
         
          }
          else
          {
                  Su16Sensor_K1_L_Cnt=0;   //在判断高电平的时候,低电平的计时器被清零,巧妙极了!
                  Su16Sensor_K1_H_Cnt++;
                  if(Su16Sensor_K1_H_Cnt>=SENSOR_TIME)
                  {
                      Su16Sensor_K1_H_Cnt=0;
                        vGu8Sensor_K1=1;//此全局变量反馈当前电平的状态
                  }
          }
}

void T0_time() interrupt 1   
{
SensorScan();//开关感应器的识别与软件滤波处理

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

kkfy888 发表于 2018-2-11 16:29:28

支持鸿哥无私精神

XTXB 发表于 2018-2-22 09:34:08

本帖最后由 XTXB 于 2018-2-22 10:36 编辑

鸿哥,新年好!都是实用型代码,大赞!回头再看某些教科书上的代码,像是玩具。

lylm123 发表于 2018-2-22 12:57:23

新年好,支持一下,顶一下。{:handshake:}

吴坚鸿 发表于 2018-2-23 14:42:10

第一百零八节: 按键控制跑马灯的启动和暂停和停止。

【108.1   按键控制跑马灯的启动和暂停和停止。】


         
                上图108.1.1独立按键

         
                上图108.1.2LED电路

         
                上图108.1.3有源蜂鸣器的电路

       在我眼里,按键不仅仅是按键,跑马灯不仅仅是跑马灯。按键是输入设备,跑马灯是应用程序。本节表面上讲按键控制跑马灯的简单项目,实际上作者用心良苦立意深远,试图通过按键与跑马灯,来分享一种输入设备如何关联应用程序的程序框架。
       本节例程实现的功能如下:
      (1)【启动暂停】按键K1。按下【启动暂停】按键K1启动之后,跑马灯处于“启动”状态,4个LED灯从左到右依次循环的变亮,给人“跑”起来的感觉。此时如果再按一次【启动暂停】按键K1,则跑马灯处于“暂停”状态,如果再按一次【启动暂停】按键K1,跑马灯又变回“启动”状态。因此,【启动暂停】按键K1是专门用来切换“启动”和“暂停”这两种状态。
      (2)【停止】按键K2。当跑马灯处于“启动”或者“暂停”或者“停止”的状态时,只要按下【停止】按键K2,当前的运动状态就终止,强制变回初始的“停止”状态,类似“复位”按键的作用。当跑马灯处于“停止”状态时,此时再按下【启动暂停】按键K1之后,跑马灯又处于“启动”状态。

#include "REG52.H"

#define KEY_VOICE_TIME   50
#define KEY_FILTER_TIME25
#define RUN_TIME200   //跑马灯的跑动速度的时间参数

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void RunTask(void);   //跑马灯的任务函数

//4个跑马灯的输出口
sbit P1_4=P1^4;
sbit P1_5=P1^5;
sbit P1_6=P1^6;
sbit P3_3=P3^3;

//蜂鸣器的输出口
sbit P3_4=P3^4;

sbit KEY_INPUT1=P2^2;//【启动暂停】按键K1的输入口。
sbit KEY_INPUT2=P2^1;//【停止】按键K2的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8RunStart=0;   //控制跑马灯启动的总开关
unsigned char Gu8RunStatus=0;//标识跑马灯当前的状态。0代表停止,1代表启动,2代表暂停。

volatile unsigned char vGu8RunTimerFlag=0;   //用于控制跑马灯跑动速度的定时器
volatile unsigned int vGu16RunTimerCnt=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();    //按键的任务函数
RunTask();    //跑马灯的任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

if(1==vGu8RunTimerFlag&&vGu16RunTimerCnt>0)//用于控制跑马灯跑动速度的定时器
{
vGu16RunTimerCnt--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;

   //【启动暂停】按键K1的扫描识别
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;    //触发1号键
      }
   }

   //【停止】按键K2的扫描识别
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;    //触发2号键
      }
   }


}

/* 注释一:
*本节破题的关键:
*在KeyTask和RunTask两个任务函数之间,主要是靠Gu8RunStart和Gu8RunStatus这两个
*全局变量来传递信息。
*/

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键。【启动暂停】按键K1
      if(0==Gu8RunStatus) //当跑马灯处于“停止”状态时
      {
Gu8RunStart=1;   //总开关“打开”。
Gu8RunStatus=1;//状态切换到“启动”状态
}
      else if(1==Gu8RunStatus) //当跑马灯处于“启动”状态时
      {
Gu8RunStatus=2;//状态切换到“暂停”状态
}
      else//当跑马灯处于“暂停”状态时
      {
Gu8RunStatus=1;//状态切换到“启动”状态
}


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

   case 2:   //2号按键。【停止】按键K2

Gu8RunStart=0;   //总开关“关闭”。
Gu8RunStatus=0;//状态切换到“停止”状态

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

}
}

void RunTask(void)    //跑马灯的任务函数,放在主函数内
{
static unsigned char Su8RunStep=0; //运行的步骤


//当总开关处于“停止”并且“步骤不为0”时,强制把步骤归零,跑马灯初始化。
if(0!=Su8RunStep&&0==Gu8RunStart)
{
Su8RunStep=0; //步骤归零

//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

}

switch(Su8RunStep) //屡见屡爱的switch又来了
{
   case 0:
       if(1==Gu8RunStart) //总开关“打开”
{
vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=0;//定时器清零
            Su8RunStep=1;//切换到下一步,启动
}
       break;
   case 1:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器
            Su8RunStep=2;//切换到下一步
}

       break;
   case 2:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=0;   //第2个灯亮
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器
            Su8RunStep=3;//切换到下一步
}

       break;
   case 3:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=0;   //第3个灯亮
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器
            Su8RunStep=4;//切换到下一步
}

       break;
   case 4:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=0;   //第4个灯亮

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器
            Su8RunStep=1;//返回到第1步,重新开始下一轮的循环!!!
}

       break;

}

}

tt98 发表于 2018-2-26 16:52:19

谢谢坚持,鸿哥{:lol:}

吴坚鸿 发表于 2018-3-1 09:25:56

第一百零九节: 按键控制跑马灯的方向。

【109.1   按键控制跑马灯的方向。】


         
                上图109.1.1独立按键

         
                上图109.1.2LED电路

         
                上图109.1.3有源蜂鸣器的电路

      之前108节讲到跑马灯的启动、暂停、停止,本节在此基础上,增加一个“方向”的控制,除了加深理解输入设备如何关联应用程序的程序框架之外,还有一个知识点值得一提,就是如何通过灵活切换switch的“步骤变量”来达到随心所欲的过程控制,本节的“方向”的控制就用到这个方法。
      本节例程的功能如下:
   (1)【启动暂停】按键K1。按下【启动暂停】按键K1启动之后,跑马灯处于“启动”状态,4个LED灯挨个依次循环的变亮,给人“跑”起来的感觉。此时如果再按一次【启动暂停】按键K1,则跑马灯处于“暂停”状态,如果再按一次【启动暂停】按键K1,跑马灯又变回“启动”状态。因此,【启动暂停】按键K1是专门用来切换“启动”和“暂停”这两种状态。
   (2)【停止】按键K2。当跑马灯处于“启动”或者“暂停”或者“停止”的状态时,只要按下【停止】按键K2,当前的运动状态就终止,强制变回初始的“停止”状态,类似“复位”按键的作用。当跑马灯处于“停止”状态时,此时再按下【启动暂停】按键K1之后,跑马灯又处于“启动”状态。
   (3)【方向】按键K3。跑马灯上电后默认处于“往右跑”的方向。每按一次【方向】按键K3,跑马灯就在“往右跑”与“往左跑”两个方向之间切换。
#include "REG52.H"

#define KEY_VOICE_TIME   50
#define KEY_FILTER_TIME25
#define RUN_TIME200   //跑马灯的跑动速度的时间参数

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void RunTask(void);   //跑马灯的任务函数

//4个跑马灯的输出口
sbit P1_4=P1^4;
sbit P1_5=P1^5;
sbit P1_6=P1^6;
sbit P3_3=P3^3;

//蜂鸣器的输出口
sbit P3_4=P3^4;

sbit KEY_INPUT1=P2^2;//【启动暂停】按键K1的输入口。
sbit KEY_INPUT2=P2^1;//【停止】按键K2的输入口。
sbit KEY_INPUT3=P2^0;//【方向】按键K3的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8RunStart=0;      //控制跑马灯启动的总开关
unsigned char Gu8RunStatus=0;   //标识跑马灯当前的状态。0代表停止,1代表启动,2代表暂停。
unsigned char Gu8RunDirection=0;//标识跑马灯当前的方向。0代表往右跑,1代表往左跑。

volatile unsigned char vGu8RunTimerFlag=0;   //用于控制跑马灯跑动速度的定时器
volatile unsigned int vGu16RunTimerCnt=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();    //按键的任务函数
RunTask();    //跑马灯的任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

if(1==vGu8RunTimerFlag&&vGu16RunTimerCnt>0)//用于控制跑马灯跑动速度的定时器
{
vGu16RunTimerCnt--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;


   //【启动暂停】按键K1的扫描识别
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;    //触发1号键
      }
   }

   //【停止】按键K2的扫描识别
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;    //触发2号键
      }
   }

   //【方向】按键K3的扫描识别
   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;    //触发3号键
      }
   }
}

/* 注释一:
*本节破题的关键:
*在KeyTask和RunTask两个任务函数之间,主要是靠Gu8RunStart、Gu8RunStatus、Gu8RunDirection
*这三个全局变量来传递信息。
*/

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键。【启动暂停】按键K1
      if(0==Gu8RunStatus) //当跑马灯处于“停止”状态时
      {
Gu8RunStart=1;   //总开关“打开”。
Gu8RunStatus=1;//状态切换到“启动”状态
}
      else if(1==Gu8RunStatus) //当跑马灯处于“启动”状态时
      {
Gu8RunStatus=2;//状态切换到“暂停”状态
}
      else//当跑马灯处于“暂停”状态时
      {
Gu8RunStatus=1;//状态切换到“启动”状态
}


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

   case 2:   //2号按键。【停止】按键K2

Gu8RunStart=0;   //总开关“关闭”。
Gu8RunStatus=0;//状态切换到“停止”状态

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

   case 3:   //3号按键。【方向】按键K3
      //每按一次K3按键,Gu8RunDirection就在0和1之间切换,从而控制方向
      if(0==Gu8RunDirection)
{
Gu8RunDirection=1;
}
else
{
Gu8RunDirection=0;
}

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

}
}

/* 注释二:
* “方向”的控制,是通过Gu8RunDirection的判断,来灵活切换switch的“步骤变量”来达到
*随心所欲的过程控制。
*/

void RunTask(void)    //跑马灯的任务函数,放在主函数内
{
static unsigned char Su8RunStep=0; //运行的步骤


//当总开关处于“停止”并且“步骤不为0”时,强制把步骤归零,跑马灯初始化。
if(0!=Su8RunStep&&0==Gu8RunStart)
{
Su8RunStep=0; //步骤归零

//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

}

switch(Su8RunStep) //屡见屡爱的switch又来了
{
   case 0:
       if(1==Gu8RunStart) //总开关“打开”
{
vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=0;//定时器清零
            Su8RunStep=1;//切换到下一步,启动
}
       break;
   case 1:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=2;
}
else//往左跑
{
               Su8RunStep=4;
}

}

       break;
   case 2:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=0;   //第2个灯亮
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=3;
}
else//往左跑
{
               Su8RunStep=1;
}
}

       break;
   case 3:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=0;   //第3个灯亮
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=4;
}
else//往左跑
{
               Su8RunStep=2;
}
}

       break;
   case 4:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=0;   //第4个灯亮

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=RUN_TIME;   //用于控制跑马灯跑动速度的定时器
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=1;
}
else//往左跑
{
               Su8RunStep=3;
}
}

       break;

}

}

tt98 发表于 2018-3-1 15:52:46

继续跟进{:lol:}

吴坚鸿 发表于 2018-3-4 10:27:31

第一百一十节: 按键控制跑马灯的速度。

【110.1   按键控制跑马灯的速度。】


      
                上图110.1.1独立按键

      
                上图110.1.2LED电路

      
                上图110.1.3有源蜂鸣器的电路

      之前109节讲到跑马灯的启动、暂停、停止、方向,本节在此基础上,把原来的“停止”更改为“速度”,
加深理解输入设备如何关联应用程序的程序框架。
      本节例程的功能如下:
   (1)【启动暂停】按键K1。上电后,按下【启动暂停】按键K1启动之后,跑马灯处于“启动”状态,4个LED灯挨个依次循环的变亮,给人“跑”起来的感觉。此时如果再按一次【启动暂停】按键K1,则跑马灯处于“暂停”状态,如果再按一次【启动暂停】按键K1,跑马灯又变回“启动”状态。因此,【启动暂停】按键K1是专门用来切换“启动”和“暂停”这两种状态。
   (2)【速度】按键K2。每按一次【速度】按键K2,跑马灯就在“慢”、“中”、“快”三档速度之间切换。
   (3)【方向】按键K3。跑马灯上电后默认处于“往右跑”的方向。每按一次【方向】按键K3,跑马灯就在“往右跑”与“往左跑”两个方向之间切换。

#include "REG52.H"

#define KEY_VOICE_TIME   50
#define KEY_FILTER_TIME25

#define RUN_TIME_SLOW    500   //“慢”档速度的时间参数
#define RUN_TIME_MIDDLE300   //“中”档速度的时间参数
#define RUN_TIME_FAST    100   //“快”档速度的时间参数

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);
void KeyScan(void);   
void KeyTask(void);   
void RunTask(void);   //跑马灯的任务函数

//4个跑马灯的输出口
sbit P1_4=P1^4;
sbit P1_5=P1^5;
sbit P1_6=P1^6;
sbit P3_3=P3^3;

//蜂鸣器的输出口
sbit P3_4=P3^4;

sbit KEY_INPUT1=P2^2;//【启动暂停】按键K1的输入口。
sbit KEY_INPUT2=P2^1;//【速度】按键K2的输入口。
sbit KEY_INPUT3=P2^0;//【方向】按键K3的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8RunStart=0;      //控制跑马灯启动的总开关
unsigned char Gu8RunStatus=0;   //标识跑马灯当前的状态。0代表停止,1代表启动,2代表暂停。
unsigned char Gu8RunDirection=0;//标识跑马灯当前的方向。0代表往右跑,1代表往左跑。
unsigned char Gu8RunSpeed=0;      //当前的速度档位。0代表“慢”,1代表“中”,2代表“快”。
unsigned intGu16RunSpeedTimeDate=0; //承接各速度档位的时间参数的变量

volatile unsigned char vGu8RunTimerFlag=0;   //用于控制跑马灯跑动速度的定时器
volatile unsigned int vGu16RunTimerCnt=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();    //按键的任务函数
RunTask();    //跑马灯的任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   

if(1==vGu8RunTimerFlag&&vGu16RunTimerCnt>0)//用于控制跑马灯跑动速度的定时器
{
vGu16RunTimerCnt--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

//根据当前的速度档位Gu8RunSpeed,来初始化速度时间参数Gu16RunSpeedTimeDate
if(0==Gu8RunSpeed)
{
Gu16RunSpeedTimeDate=RUN_TIME_SLOW; //赋值“慢”档的时间参数
}
else if(1==Gu8RunSpeed)
{
Gu16RunSpeedTimeDate=RUN_TIME_MIDDLE; //赋值“中”档的时间参数
}
else
{
Gu16RunSpeedTimeDate=RUN_TIME_FAST; //赋值“快”档的时间参数
}


}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;


   //【启动暂停】按键K1的扫描识别
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;    //触发1号键
      }
   }

   //【速度】按键K2的扫描识别
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;    //触发2号键
      }
   }

   //【方向】按键K3的扫描识别
   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;    //触发3号键
      }
   }
}

/* 注释一:
*本节破题的关键:
*在KeyTask和RunTask两个任务函数之间,主要是靠Gu8RunStart、Gu8RunStatus、Gu8RunDirection、
*Gu16RunSpeedTimeDate这四个全局变量来传递信息。
*/

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键。【启动暂停】按键K1
      if(0==Gu8RunStatus) //当跑马灯处于“停止”状态时
      {
Gu8RunStart=1;   //总开关“打开”。
Gu8RunStatus=1;//状态切换到“启动”状态
}
      else if(1==Gu8RunStatus) //当跑马灯处于“启动”状态时
      {
Gu8RunStatus=2;//状态切换到“暂停”状态
}
      else//当跑马灯处于“暂停”状态时
      {
Gu8RunStatus=1;//状态切换到“启动”状态
}

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

   case 2:   //2号按键。【速度】按键K2

      //每按一次K2按键,Gu8RunSpeed就在0、1、2三者之间切换,并且根据Gu8RunSpeed的数值,
//对Gu16RunSpeedTimeDate赋值不同的速度时间参数,从而控制速度档位
      if(0==Gu8RunSpeed)
{
Gu8RunSpeed=1;//“中”档
Gu16RunSpeedTimeDate=RUN_TIME_MIDDLE; //赋值“中”档的时间参数
}
      else if(1==Gu8RunSpeed)
{
Gu8RunSpeed=2;   //“快”档
Gu16RunSpeedTimeDate=RUN_TIME_FAST; //赋值“快”档的时间参数
}
else
{
Gu8RunSpeed=0;   //“慢”档
Gu16RunSpeedTimeDate=RUN_TIME_SLOW; //赋值“慢”档的时间参数
}

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

   case 3:   //3号按键。【方向】按键K3
      //每按一次K3按键,Gu8RunDirection就在0和1之间切换,并且对从而控制方向
      if(0==Gu8RunDirection)
{
Gu8RunDirection=1;
}
else
{
Gu8RunDirection=0;
}

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

}
}

/* 注释二:
* “速度”是受Gu16RunSpeedTimeDate具体数值大小的影响
*/

void RunTask(void)    //跑马灯的任务函数,放在主函数内
{
static unsigned char Su8RunStep=0; //运行的步骤


//当总开关处于“停止”并且“步骤不为0”时,强制把步骤归零,跑马灯初始化。
if(0!=Su8RunStep&&0==Gu8RunStart)
{
Su8RunStep=0; //步骤归零

//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

}

switch(Su8RunStep) //屡见屡爱的switch又来了
{
   case 0:
       if(1==Gu8RunStart) //总开关“打开”
{
vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=0;//定时器清零
            Su8RunStep=1;//切换到下一步,启动
}
       break;
   case 1:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=2;
}
else//往左跑
{
               Su8RunStep=4;
}

}

       break;
   case 2:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=0;   //第2个灯亮
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=3;
}
else//往左跑
{
               Su8RunStep=1;
}
}

       break;
   case 3:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=0;   //第3个灯亮
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=4;
}
else//往左跑
{
               Su8RunStep=2;
}
}

       break;
   case 4:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=0;   //第4个灯亮

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Su8RunStep=1;
}
else//往左跑
{
               Su8RunStep=3;
}
}

       break;

}

}

吴坚鸿 发表于 2018-3-11 14:00:06

第一百一十一节: 工业自动化设备的开关信号的运动控制。

【111.1   开关信号的运动控制。】


         
                上图111.1.1独立按键

         
                上图111.1.2LED电路

         
                上图111.1.3有源蜂鸣器的电路

       本节涉及的知识点有,switch的过程控制,时间延时,开关感应器的软件滤波,工件计数器,以及整体的软件框架。
       现在有一台设备,水平方向有一个滑块,能左右移动,滑块上安装了一个能垂直伸缩的“机械手”。按下启动按键后,滑块先从左边往右边移动,移到最右边碰到“右感应器”后,滑块上的“机械手”开始往下移动2秒,移动2秒后开始原路返回,“机械手”向上移动,碰到“上感应器”后,滑块开始往左边移动,移动3秒后默认已经回到原位最左边,此时“计数器”累加1,完成一次过程,如果再按下启动按键,继续重复这个过程。
       这个设备用了2个气缸。1个“水平气缸”驱动滑块水平方向的左右移动,当控制“水平气缸”的输出信号为0时往左边跑,当控制“水平气缸”的输出信号为1时往右边跑。另1个“垂直气缸”驱动“机械手”的上下移动,当控制“垂直气缸”的输出信号为0时往上边跑,当控制“垂直气缸”的输出信号为1时往下边跑。
       这个设备用了2个开关感应器。分别是“右感应器”和“上感应器”。当感应器没有被碰到的时候信号为1,当感应器被碰到的时候信号为0。
       这个设备用了1个独立按键。控制运动的启动。
       2个气缸是输出信号,用P1.4和P1.5所控制的两个LED模拟。2个开关感应器是输入信号,用K2和K3这两个独立按键模拟。1个独立按键用K1按键。如上图。

#include "REG52.H"

#define KEY_VOICE_TIME   50
#define KEY_FILTER_TIME25
#define SENSOR_TIME   20      //开关感应器的“滤波”时间


void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);

void GoLeft(void) ; //“水平气缸”往左跑
void GoRight(void); //“水平气缸”往右跑
void GoUp(void);    //“垂直气缸”往上跑
void GoDown(void);//“垂直气缸”往下跑

void VoiceScan(void);
void SensorScan(void);//开关感应器的消抖,在定时中断里调用处理
void KeyScan(void);   
void KeyTask(void);   
void RunTask(void);    //运动控制的任务函数

sbit P1_4=P1^4;//水平气缸的输出
sbit P1_5=P1^5;//垂直气缸的输出

sbit P3_4=P3^4;//蜂鸣器的输出口

sbit KEY_INPUT1=P2^2;//【启动】按键K1的输入口。

sbit SensorRight_sr=P2^1;   //右感应器的输入口
sbit SensorUp_sr=P2^0;      //上感应器的输入口

volatile unsigned char vGu8SensorRight=0;//右感应器经过滤波后的当前电平状态。
volatile unsigned char vGu8SensorUp=0;//上感应器经过滤波后的当前电平状态。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8RunStart=0;      //启动的总开关
unsigned char Gu8RunStatus=0;   //运动的状态,0为停止,1为运行

unsigned intGu16RunCnt=0;       //计数器
unsigned intGu16ReturnLeftTime=3000;   //水平往左跑的延时变量,默认为3秒
unsigned intGu16GoDownTime=2000;       //垂直往下跑的延时变量,默认为2秒

volatile unsigned char vGu8RunTimerFlag=0;   //用于控制运动过程中的延时的定时器
volatile unsigned int vGu16RunTimerCnt=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    KeyTask();    //按键的任务函数
RunTask();    //运动控制的任务函数
    }
}

/* 注释一:
*两个“计时器”相互“清零”相互“抗衡”,从而实现了开关感应器的“消抖”处理,
*专业术语也叫“软件滤波”。这种滤波方式,不管是从“高转成低”,还是“低转成高”,
*如果在某个瞬间出现干扰抖动,某个计数器都会及时被“清零”,从而起到非常高效的消抖滤波作用。
*/

void SensorScan(void)//此函数放在定时中断里每1ms扫描一次,用来识别和滤波开关感应器
{
      static unsigned int Su16SensorRight_H_Cnt=0;//判断高电平的计时器
      static unsigned int Su16SensorRight_L_Cnt=0;//判断低电平的计时器

      static unsigned int Su16SensorUp_H_Cnt=0;//判断高电平的计时器
      static unsigned int Su16SensorUp_L_Cnt=0;//判断低电平的计时器

      //右感应器的滤波
      if(0==SensorRight_sr)
      {
         Su16SensorRight_H_Cnt=0;//在判断低电平的时候,高电平的计时器被清零,巧妙极了!
         Su16SensorRight_L_Cnt++;
         if(Su16SensorRight_L_Cnt>=SENSOR_TIME)
         {
               Su16SensorRight_L_Cnt=0;
               vGu8SensorRight=0;   //此全局变量反馈经过滤波后“右感应器”当前电平的状态
         }

       }
       else
       {
         Su16SensorRight_L_Cnt=0;   //在判断高电平的时候,低电平的计时器被清零,巧妙极了!
         Su16SensorRight_H_Cnt++;
         if(Su16SensorRight_H_Cnt>=SENSOR_TIME)
         {
               Su16SensorRight_H_Cnt=0;
               vGu8SensorRight=1;//此全局变量反馈经过滤波后“右感应器”当前电平的状态
         }
       }

      //上感应器的滤波
      if(0==SensorUp_sr)
      {
         Su16SensorUp_H_Cnt=0;
         Su16SensorUp_L_Cnt++;
         if(Su16SensorUp_L_Cnt>=SENSOR_TIME)
         {
               Su16SensorUp_L_Cnt=0;
               vGu8SensorUp=0;   //此全局变量反馈经过滤波后“上感应器”当前电平的状态
         }

       }
       else
       {
         Su16SensorUp_L_Cnt=0;   
         Su16SensorUp_H_Cnt++;
         if(Su16SensorUp_H_Cnt>=SENSOR_TIME)
         {
               Su16SensorUp_H_Cnt=0;
               vGu8SensorUp=1;//此全局变量反馈经过滤波后“上感应器”当前电平的状态
         }
       }


}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();   
SensorScan();//用来识别和滤波开关感应器

if(1==vGu8RunTimerFlag&&vGu16RunTimerCnt>0)//用于控制运动延时的定时器
{
vGu16RunTimerCnt--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
   //上电初始化气缸的开机位置
GoLeft() ;   //“水平气缸”往左跑,上电初始化时滑块处于左边
GoUp();      //“垂直气缸”往上跑,上电初始化时“机械臂”处于上方

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void GoLeft(void) //“水平气缸”往左跑
{
P1_4=0;   
}

void GoRight(void) //“水平气缸”往右跑
{
P1_4=1;   
}

void GoUp(void)//“垂直气缸”往上跑
{
P1_5=0;   
}

void GoDown(void) //“垂直气缸”往下跑
{
P1_5=1;   
}


void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;

   //【启动】按键K1的扫描识别
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;    //触发1号键
      }
   }

}

/* 注释二:
*在KeyTask中只改变Gu8RunStart的值,用于总启动开关。而运动状态Gu8RunStatus是在运动函数
*RunTask中改变,用于对外反馈实时的运动状态。
*/

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键。【启动】按键K1

      if(0==Gu8RunStatus) //根据当前运动的状态来决定“总开关”是否能受按键的控制
      {
Gu8RunStart=1;   //总开关“打开”。
}

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

   default:   
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一直触发
break;

}
}

/* 注释三:
*本节故意引入三个变量:计数器Gu16RunCnt,左延时Gu16ReturnLeftTime,下延时Gu16GoDownTime。
*在人机界面的场合,这三个变量可以用来扩展实现设置参数的功能。比如,如果有数码管,可以通过
*显示Gu16RunCnt的数值来让客户看到当前设备的计数器。如果有数码管和按键,可以通过切换到某个
*界面下,修改Gu16ReturnLeftTime和Gu16GoDownTime的数值,让客户对设备进行延时参数的设置。
*/

void RunTask(void)    //运动控制的任务函数,放在主函数内
{
static unsigned char Su8RunStep=0; //运行的步骤


//当总开关处于“停止”并且“步骤不为0”时,强制把步骤归零。
if(0!=Su8RunStep&&0==Gu8RunStart)
{
Su8RunStep=0; //步骤归零
}

switch(Su8RunStep) //屡见屡爱的switch又来了
{
   case 0:
       if(1==Gu8RunStart) //总开关“打开”
{
Gu8RunStatus=1;//及时设置Gu8RunStatus的运动状态为“运行”

GoRight() ;      //“水平气缸”往右跑。P1.4的LED灯“灭”。

            Su8RunStep=1;//切换到下一步
}
       break;
   case 1:
       if(0==vGu8SensorRight) //直到碰到了“右感应器”(按下K2),“机械臂”才往下移动。
{
GoDown();//“垂直气缸”往下跑。P1.5的LED灯“灭”。
    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16GoDownTime; //向下移动3秒的延时赋值
vGu8RunTimerFlag=1;   //启动定时器

            Su8RunStep=2;//切换到下一步
}
       break;
   case 2:
       if(0==vGu16RunTimerCnt) //当定时的3秒时间到,“机械臂”才往上移动,开始原路返回。
{
GoUp();    //“垂直气缸”往上跑。P1.5的LED灯“亮”。
            Su8RunStep=3;//切换到下一步
}
       break;
   case 3:
       if(0==vGu8SensorUp) //直到碰到了“上感应器”(按下K3),滑块才往左移动。
{
GoLeft() ; //“水平气缸”往左跑。P1.4的LED灯“亮”。
    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16ReturnLeftTime; //向左移动2秒的延时赋值
vGu8RunTimerFlag=1;   //启动定时器

            Su8RunStep=4;//切换到下一步
}

       break;
   case 4:
       if(0==vGu16RunTimerCnt) //当定时的2秒时间到,完成一次过程。
{
    Gu16RunCnt++;   //计数器加1,统计设备运行的次数
Gu8RunStatus=0;//及时设置Gu8RunStatus的运动状态为“停止”
Gu8RunStart=0;//总开关“关闭”,为下一次启动作准备
            Su8RunStep=0;   //步骤变量清零,为下一次启动作准备
}
       break;
}
}

kkfy888 发表于 2018-3-12 08:43:58

很好,继续关注,期待下一节!

吴坚鸿 发表于 2018-3-30 10:21:37

第一百一十二节: 数码管显示的基础知识。

【112.1   数码管显示的基础知识。】
         
                上图112.1.1数码管

         
                上图112.1.2等效图

       如上图112.1.1,一个数码管内部有8个段码,每个段码内部对应一颗能发光的LED灯,把相关位置的段码点亮或熄灭就可以显示出不同的数字或者小数点。比如,要显示一个数字“1”,只需要点亮b和c这两个段码LED即可,其它6个a,d,e,f,g,h段码LED熄灭,就可以显示一个数字“1”。再进一步深入分析数码管内部的等效图(上图112.1.2),com4是右起第1位数码管内部8个段码LED的公共端,要点亮任何一个段码LED的前提必须是公共端com4为低电平(P1.0输出0信号)。如果公共端com4为高电平(P1.0输出1信号),则不管段码端P0口的8个IO口输出什么信号,8个段码LED都是熄灭的(无正压差,则无电流无回路)。因此,公共端(比如com4,com3,com2,com1)就是某个数码管的“总开关”。比如,右起第1位数码管要显示数字“1”,要点亮b和c,则P0.1和P0.2必须输出“1”高电平,其它P0.0,P0.3,P0.4,P0.5,P0.6,P0.7必须输出“0”低电平,把这8个IO口二进制的信号转换成十六进制,则整个P0口总线只需输出一个十六进制的0x06,最后,“总开关”打开,公共端com4输出“0”,即可显示一个数字“1”。如果需要显示其它的不同数字,只需要改变段码端P0口的十六进制输出数值即可,如果提前把要显示的数字放在一个数组里,这个数组就是编码转换表,类似于一个字库表。现在编写一个程序例子,右起第1个和第3个数码管循环显示从0到9的数字,另外右起第2个和第4个数码管则关闭不显示,程序代码如下:

#include "REG52.H"

#define CHANGE_TIME1000    //数码管切换显示数字的时间

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void DisplayTask(void);    //数码管显示的任务函数

sbit P1_0=P1^0;//右起第1位数码管的公共端com4
sbit P1_1=P1^1;//右起第2位数码管的公共端com3
sbit P1_2=P1^2;//右起第3位数码管的公共端com2
sbit P1_3=P1^3;//右起第4位数码管的公共端com1

//根据原理图得出的共阴数码管编码转换表,类似于一个字库表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};
volatile unsigned char vGu8ChangeTimerFlag=0;//控制切换数字的时间的定时器
volatile unsigned int vGu16ChangeTimerCnt=0;

unsigned char Gu8Number=0; //从0到9依次循环显示的数字

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
DisplayTask();    //数码管显示的任务函数
    }
}

void DisplayTask(void)    //数码管显示的任务函数
{
static unsigned char Su8GetCode;//从编码转换表中提取出来的编码。

if(0==vGu16ChangeTimerCnt)//定时的时间到,更新显示下一个数字,依次循环显示
{
Su8GetCode=Cu8DigTable; //从编码转换表中提取出来的编码。

    P0=Su8GetCode; //段码端输出需要显示的编码
P1_0=0;//右起第1位数码管的公共端com4,“总开关”打开,输出低电平0
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=0;//右起第3位数码管的公共端com2,“总开关”打开,输出低电平0
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1

Gu8Number++;//显示的数字不断从0到9累加
if(Gu8Number>9)
{
Gu8Number=0;
}

vGu8ChangeTimerFlag=0;
vGu16ChangeTimerCnt=CHANGE_TIME;
vGu8ChangeTimerFlag=1;//启动新一轮的定时器
}
}

void T0_time() interrupt 1   
{
if(1==vGu8ChangeTimerFlag&&vGu16ChangeTimerCnt>0)//数码管显示切换时间的定时器
{
vGu16ChangeTimerCnt--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
    //初始化上电瞬间数码管的状态
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出低电平1
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出低电平1
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

tt98 发表于 2018-4-6 09:12:35

期待鸿哥再次更新!{:lol:}

三国小兵 发表于 2018-4-6 11:39:55

关注下...................

yyinfo263 发表于 2018-4-7 20:38:57

这样的精神一定要大力支持 ,谢谢楼主的好文给与的帮助!!{:smile:}

吴坚鸿 发表于 2018-4-12 09:36:02

本帖最后由 吴坚鸿 于 2018-4-12 19:38 编辑

第一百一十三节: 动态扫描的数码管显示数字。

【113.1   动态扫描的数码管。】
         
                上图113.1.1数码管

       上一节,看到打开显示的数码管右起第1个(com4)和第3个(com2)在任意时刻显示的数字是一样的,为什么?因为四个数码管的8个段码a,b,c,d,e,f,g,h所连接的单片机IO口是共用的,如果把四个数码管全部打开(com1,com2,com3,com4全部输出低电平),会发现四个数码管在任意时刻显示的四个数字也是一样的!实际应用中,要四个数码管能各自独立显示不同的数字,就需要用到“分时动态扫描”的方式。所谓分时,就是在任意时刻只能显示其中一个数码管(某个com输出低电平),其它三个数码管关闭(其它三个com输出高电平),每个数码管显示停留的时间固定一致并且非常短暂,四个数码管依次循环的切换显示,只要切换画面的速度足够快,人的视觉就分辨不出来,感觉八个数码管是同时亮的(实际不是同时亮),跟动画片“1秒钟动态切换显示多少幅画面”的原理一样。现在编写一个程序例子,四个数码管要显示四个不同的数字“1234”,程序代码如下:

#include "REG52.H"

/* 注释一:
*SCAN_TIME是每个数码管停留显示的短暂时间。这里称为“扫描时间”。这个时间既不能太长也不能
*太短,要调试到恰到好处。太长,则影响其它数码管的显示,会让人觉得画面不连贯不是同时亮;
*太短,又会影响显示的亮度。具体的时间应该根据实际项目不断调试修正而得到最佳显示的数值。
*/

#define SCAN_TIME1   


void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void DisplayScan(void);    //数码管的动态扫描函数,放在定时中断里。

sbit P1_0=P1^0;//右起第1位数码管的公共端com4
sbit P1_1=P1^1;//右起第2位数码管的公共端com3
sbit P1_2=P1^2;//右起第3位数码管的公共端com2
sbit P1_3=P1^3;//右起第4位数码管的公共端com1

//根据原理图得出的共阴数码管编码转换表,类似于一个字库表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};

volatile unsigned char vGu8ScanTimerFlag=0;//动态扫描的定时器
volatile unsigned int vGu16ScanTimerCnt=0;

/* 注释二:
*vGu8Display_Righ_4,vGu8Display_Righ_3,vGu8Display_Righ_2,vGu8Display_Righ_1,这四个
*全局变量用来传递每位数码管需要显示的数字,作为对上面应用层调用的接口变量。
*/

volatile unsigned char vGu8Display_Righ_4=1;//右起第4位数码管显示的变量。这里显示“1”
volatile unsigned char vGu8Display_Righ_3=2;//右起第3位数码管显示的变量。这里显示“2”
volatile unsigned char vGu8Display_Righ_2=3;//右起第2位数码管显示的变量。这里显示“3”
volatile unsigned char vGu8Display_Righ_1=4;//右起第1位数码管显示的变量。这里显示“4”

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    }
}

/* 注释三:
*DisplayScan数码管的动态扫描函数,之所以放在定时中断里,是因为动态扫描数码管对时间均匀度
*要求很高,如果放在main主函数中,期间稍微出现一些延时滞后或者超前执行的情况,都会导致
*数码管出现“闪烁”或者“忽暗忽亮”的显示效果。
*/

void DisplayScan(void)   
{
static unsigned char Su8GetCode;//从编码转换表中提取出来的编码。
static unsigned char Su8ScanStep=1;//扫描步骤

if(0==vGu16ScanTimerCnt)//定时的时间到,切换显示下一个数码管,依次动态快速循环切换显示
{

/* 注释四:
*在即将切换显示到下一个新的数码管之前,应该先关闭显示所有的数码管,避免因关闭不彻底而导致
*数码管某些段位出现“漏光”,也就是数码管因程序处理不善而出现常见的“鬼影”显示情况。
*/

    P0=0x00; //输出显示先清零,先关闭显示所有的数码管
    //先关闭所有的com口,先关闭显示所有的数码管
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出低电平1
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出低电平1
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1

    switch(Su8ScanStep)
{
       case 1: //显示右起第1个数码管
Su8GetCode=Cu8DigTable; //从编码转换表中提取出来的编码。
    P0=Su8GetCode; //段码端输出需要显示的编码
P1_0=0;//右起第1位数码管的公共端com4,“总开关”打开,输出低电平0
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出高电平1
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1            
break;

       case 2: //显示右起第2个数码管
Su8GetCode=Cu8DigTable; //从编码转换表中提取出来的编码。
    P0=Su8GetCode; //段码端输出需要显示的编码
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出高电平1
P1_1=0;//右起第2位数码管的公共端com3,“总开关”打开,输出低电平0
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出高电平1
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1            
break;

       case 3: //显示右起第3个数码管
Su8GetCode=Cu8DigTable; //从编码转换表中提取出来的编码。
    P0=Su8GetCode; //段码端输出需要显示的编码
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出高电平1
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=0;//右起第3位数码管的公共端com2,“总开关”打开,输出低电平0
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1            
break;

       case 4: //显示右起第4个数码管
Su8GetCode=Cu8DigTable; //从编码转换表中提取出来的编码。
    P0=Su8GetCode; //段码端输出需要显示的编码
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出高电平1
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出高电平1
P1_3=0;//右起第4位数码管的公共端com1,“总开关”打开,输出低电平0            
break;

}

Su8ScanStep++;
if(Su8ScanStep>4) //如果扫描步骤大于4,继续从第1步开始扫描
{
Su8ScanStep=1;

}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;//启动新一轮的定时器
}
}

void T0_time() interrupt 1   
{
DisplayScan(); //数码管的动态扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)//数码管显示切换时间的定时器
{
vGu16ScanTimerCnt--;
}

   
TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
//初始化上电瞬间数码管的状态,关闭显示所有的数码管
P0=0x00;
P1_0=1;//右起第1位数码管的公共端com4,“总开关”关闭,输出低电平1
P1_1=1;//右起第2位数码管的公共端com3,“总开关”关闭,输出高电平1
P1_2=1;//右起第3位数码管的公共端com2,“总开关”关闭,输出低电平1
P1_3=1;//右起第4位数码管的公共端com1,“总开关”关闭,输出高电平1
   
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}


anyanggcmy 发表于 2018-4-12 10:07:12

期待楼主更新谢谢

吴坚鸿 发表于 2018-4-16 12:57:01

第一百一十四节: 动态扫描的数码管显示小数点。
      
【114.1   动态扫描的数码管显示小数点。】
      
                上图114.1.1数码管

       如上图,小数点的段码是h,对应单片机的P0.7口。数码管编码转换表(类似字库)的11个以字节为单位的数据,把它们从十六进制转换成二进制后,可以发现第7位(对应P0.7口)都是0。因此,从转换表里取数据后,得到的数据默认是让数码管的小数点不显示的。如果想显示这个小数点,就需要用到“或(|)”语句操作。比如,本节程序需要显示“1.234”这个带小数点的数值,代码如下:

#include "REG52.H"

#define SCAN_TIME1   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void DisplayScan(void);   

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

//转换表,里面的11个数据,转换成二进制后,第7位数据都是0默认不显示小数点
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;


volatile unsigned char vGu8Display_Righ_4=1;//右起第4位数码管显示的变量。这里显示“1”
volatile unsigned char vGu8Display_Righ_3=2;//右起第3位数码管显示的变量。这里显示“2”
volatile unsigned char vGu8Display_Righ_2=3;//右起第2位数码管显示的变量。这里显示“3”
volatile unsigned char vGu8Display_Righ_1=4;//右起第1位数码管显示的变量。这里显示“4”

/* 注释一:
*vGu8Display_Righ_Dot_4,vGu8Display_Righ_Dot_3,vGu8Display_Righ_Dot_2,
*vGu8Display_Righ_Dot_1,这四个全局变量用来传递每位数码管是否需要显示它的小数点,如果是1
*代表需要显示其小数点,如果是0则不显示小数点。这四个变量作为对上面应用层调用的接口变量。
*/

volatile unsigned char vGu8Display_Righ_Dot_4=1;//右起第4位数码管的小数点。1代表打开显示。
volatile unsigned char vGu8Display_Righ_Dot_3=0;//右起第3位数码管的小数点。0代表关闭显示。
volatile unsigned char vGu8Display_Righ_Dot_2=0;//右起第2位数码管的小数点。0代表关闭显示。
volatile unsigned char vGu8Display_Righ_Dot_1=0;//右起第1位数码管的小数点。0代表关闭显示。

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
    }
}


void DisplayScan(void)   
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

/* 注释二:
*这里是本节的关键。通过判断全局的接口变量的数值,来决定是否打开显示小数点。
*从转换表取出字模数据后再跟0x80进行“或”运算即可把第7位数据改为1。
*/

if(1==vGu8Display_Righ_Dot_1) //如果打开了需要显示第1个数码管的小数点
{
Su8GetCode=Su8GetCode|0x80;//把第7位数据改为1,显示小数点
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2) //如果打开了需要显示第2个数码管的小数点
{
Su8GetCode=Su8GetCode|0x80;//把第7位数据改为1,显示小数点
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3) //如果打开了需要显示第3个数码管的小数点
{
Su8GetCode=Su8GetCode|0x80;//把第7位数据改为1,显示小数点
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4) //如果打开了需要显示第4个数码管的小数点
{
Su8GetCode=Su8GetCode|0x80;//把第7位数据改为1,显示小数点
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}

void T0_time() interrupt 1   
{
DisplayScan();

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;
}


TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

tt98 发表于 2018-4-17 14:43:31

再顶鸿哥!谢谢坚持更新!

kkfy888 发表于 2018-4-20 11:06:15

既然选择了,就坚持到底{:victory:}

mikeliujia 发表于 2018-4-20 13:40:47

最难的是把小事坚持做好

yanzhiwei 发表于 2018-4-20 14:45:28

能坚持持续更新,难得。谢谢楼主

吴坚鸿 发表于 2018-4-22 12:11:40

第一百一十五节: 按键控制数码管的秒表。

【115.1   按键控制数码管的秒表。】
         
                上图115.1.1数码管



         
                上图115.1.2独立按键

       本节通过一个秒表的小项目,让大家学会以下4个知识点:
      (1)上层的界面显示框架几乎都要用到更新变量,更新变量包括整屏更新和局部更新,本节只用到整屏更新。更新变量是用全局变量在函数之间传递信息。作用是,当有某个需要显示的数据发生改变的时候,就要给更新变量置1,让显示函数重新更新一次显示,确保最新的数据能及时显示出来,平时没有数据更新改变的时候不用频繁更新显示避免占用CPU过多的时间。
      (2)凡是需要显示数字的地方,都必须涉及如何把一个数据按“个十百千万...”的位逐个提取出来的算法。这个算法比较简单,主要用“求余”和“求商”这两个运算语句就可以随心所欲的把数据位提取出来。除此之外,还要学会如何用if语句判断数据的范围,来把高位尚未用到的某个数码管屏蔽,让该位数码管只显示一个“不显示”的数据(避免直接显示一个0)。
      (3)我常把单片机程序简化成4个代表:按键(人机输入),数码管(人机界面),跑马灯(应用程序),串口(通信)。本节的“应用程序”不是跑马灯,而是秒表。不管是跑马灯,还是秒表,都要用到一个总启动Gu8RunStart和一个总运行步骤Gu8RunStep。建议大家,总启动Gu8RunStart和总运行步骤Gu8RunStep应该成双成对的出现(这样关断响应更及时,并且结构更紧凑,漏洞更少),比如,凡是总启动Gu8RunStart发生改变的时候,总运行步骤Gu8RunStep都复位归零一下。
      (4)一个硬件的定时器中断,可以衍生出N个软件定时器,之前跟大家介绍的是“递减式”的软件定时器,而且实际应用中,“递减式”的软件定时器也是用得最多。本节因为项目的需要,需要用到的是“累加式”的软件定时器。不管是哪种软件定时器,大家都要注意定时器变量在定义时所用到的数据类型,这个数据类型决定了定时时间的长度,比如在51单片机中,unsigned int的范围是0到65535,最大一次性定时65.535秒。而unsigned long的范围是0到4294967295,最大一次性定时4294967.295秒。本节秒表的时间超过65.535秒,因此需要用到unsigned long类型的定时器变量。
本节秒表程序的功能:K1按键是复位按键,每按一次,秒表都停止并且重新归零。K2按键是启动和暂停按键,当秒表处于复位后停止的状态时按一次则开始启动,当秒表处于正在工作的状态时按一次则处于暂停状态,当秒表处于暂停的状态时按一次则继续处于工作的状态。本节4位数码管,显示的时间是带2位小数点的,能显示的时间范围是:0.00秒到99.99秒。代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25

#define SCAN_TIME1   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void DisplayScan(void);//底层显示的驱动函数
void DisplayTask(void);//上层显示的任务函数

void RunTask(void);//秒表的应用程序

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};

//数码管底层驱动扫描的软件定时器
volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

//秒表的软件定时器,注意,这里是unsigned long类型,范围是0到4294967295毫秒
volatile unsigned char vGu8StopWatchTimerFlag=0;
volatile unsigned long vGu32StopWatchTimerCnt=0;

//数码管上层每10ms就定时刷新一次显示的软件定时器。用于及时更新显示秒表当前的实时数值
volatile unsigned char vGu8UpdateTimerFlag=0;
volatile unsigned int vGu16UpdateTimerCnt=0;


unsigned char Gu8RunStart=0;//应用程序的总启动
unsigned char Gu8RunStep=0;//应用程序的总运行步骤。建议跟vGu8RunStart成双成对出现
unsigned char Gu8RunStatus=0; //当前秒表的状态。0代表停止,1代表正在工作中,2代表暂停

unsigned char Gu8WdUpdate=1;//开机默认整屏更新一次显示。此变量在显示框架中是非常重要的变量

volatile unsigned char vGu8Display_Righ_4=10;//开机默认最高位数码管显示一个“不显示”数据
volatile unsigned char vGu8Display_Righ_3=0;
volatile unsigned char vGu8Display_Righ_2=0;
volatile unsigned char vGu8Display_Righ_1=0;

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=1;    //开机默认保留显示2个小数点
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
RunTask();      //秒表的应用程序
    }
}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //复位按键

Gu8RunStatus=0; //秒表返回停止的状态

Gu8RunStart=0;//秒表停止
            Gu8RunStep=0;//总运行步骤归零。建议跟vGu8RunStart成双成对出现

vGu8StopWatchTimerFlag=0;
vGu32StopWatchTimerCnt=0;   //秒表的软件定时器清零

            Gu8WdUpdate=1;//整屏更新一次显示

vGu8KeySec=0;
break;

   case 2:   //启动与暂停的按键
      if(0==Gu8RunStatus) //在停止状态下
      {
            Gu8RunStatus=1;//秒表处于工作状态

vGu8StopWatchTimerFlag=0;
vGu32StopWatchTimerCnt=0;
vGu8StopWatchTimerFlag=1;//启动秒表的软件定时器


Gu8RunStart=1;   //秒表总开关启动
                Gu8RunStep=0;    //总运行步骤归零。建议跟vGu8RunStart成双成对出现
}
      else if(1==Gu8RunStatus) //在工作状态下
      {
            Gu8RunStatus=2;//秒表处于暂停状态
}
      else//在暂停状态下
      {
            Gu8RunStatus=1;//秒表处于工作状态
}

      Gu8WdUpdate=1;//整屏更新一次显示,确保在暂停的时候能显示到最新的数据

vGu8KeySec=0;
break;

}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
//需要借用的中间变量,用来拆分数据位
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

/* 注释一:
*此处为什么要多加4个中间过渡变量Su8Temp_X?是因为vGu32StopWatchTimerCnt分解数据的时候
*需要进行除法和求余数的运算,就会用到好多条指令,就会耗掉一点时间,类似延时了一会。我们
*的定时器每隔一段时间都会产生中断,然后在中断里驱动数码管显示,当vGu32StopWatchTimerCnt
*还没完全分解出4位有效数据时,这个时候来的定时中断,就有可能导致显示的数据瞬间产生不完整,
*影响显示效果。因此,为了把需要显示的数据过渡最快,所以采取了先分解,再过渡显示的方法。
*/

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          //先分解数据

          //Su8Temp_4提取“十秒”位。
Su8Temp_4=vGu32StopWatchTimerCnt/10000; //实际精度是0.0001秒,但显示精度是0.01秒
Su8Temp_4=Su8Temp_4%10;

         //Su8Temp_3提取“个秒”位。
Su8Temp_3=vGu32StopWatchTimerCnt/1000; //实际精度是0.0001秒,但显示精度是0.01秒
Su8Temp_3=Su8Temp_3%10;

         //Su8Temp_2提取“百毫秒”位。
Su8Temp_2=vGu32StopWatchTimerCnt/100; //实际精度是0.0001秒,但显示精度是0.01秒
Su8Temp_2=Su8Temp_2%10;

          //Su8Temp_1提取“十毫秒”位。
Su8Temp_1=vGu32StopWatchTimerCnt/10; //实际精度是0.0001秒,但显示精度是0.01秒
Su8Temp_1=Su8Temp_1%10;

          //判断数据范围,来决定最高位数码管是否需要显示。
          if(vGu32StopWatchTimerCnt<10000) //10.000秒。实际4位数码管最大只能显示99.99秒
          {
             Su8Temp_4=10;//在数码管转换表里,10代表一个“不显示”的数据
}

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=1;   //保留显示2位小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

}
}

void RunTask(void)//秒表的应用程序
{
if(0==Gu8RunStart)
{
   return;// 如果秒表处于停止状态,则直接退出当前函数,不执行该函数以下的其它代码
}

switch(Gu8RunStep)
{
    case 0://在这个步骤里,主要用来初始化一些参数

vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//每10ms更新显示一次当前秒表的时间
vGu8UpdateTimerFlag=1;

Gu8RunStep=1; //跳转到每10ms更新显示一次的步骤里
break;

    case 1://每10ms更新一次显示,确保实时显示秒表当前的时间
       if(0==vGu16UpdateTimerCnt) //每10ms更新显示一次当前秒表的时间
       {
    vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//重置定时器,为下一个10ms更新做准备
vGu8UpdateTimerFlag=1;

            Gu8WdUpdate=1;//整屏更新一次显示当前秒表的时间
}
break;

}

}


void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;

   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;   
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }
}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}

void T0_time() interrupt 1   
{
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

//每10ms就定时更新一次显示的软件定时器
if(1==vGu8UpdateTimerFlag&&vGu16UpdateTimerCnt>0)
{
vGu16UpdateTimerCnt--;//递减式的软件定时器
}

//秒表实际走的时间的软件定时器,注意,这里是“累加式”的软件定时器
if(1==vGu8StopWatchTimerFlag&&1==Gu8RunStatus) //秒表处于工作的状态
{
vGu32StopWatchTimerCnt++;//累加式的软件定时器
}


TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

bigk2000 发表于 2018-4-22 13:14:18

mark,谢谢

kkfy888 发表于 2018-4-29 11:07:58

再忙也要抽时间来学习下!

吴坚鸿 发表于 2018-4-30 11:58:08

第一百一十六节: 按键控制数码管的倒计时。

【116.1   按键控制数码管的倒计时。】

                上图116.1.1数码管




                上图116.1.2独立按键

      
                上图116.1.3有源蜂鸣器

       上一节讲“累加式”的秒表,这一节讲“递减式”的倒计时。通过此小项目,加深理解在显示框架中常用的更新变量(整屏更新和局部更新),以及应用程序与按键是如何关联的框架。同时,在拆分“个十百千万...”的时候,有一处地方必须注意,“先整除后求余”必须用一行代码一气呵成,不能拆分成两行代码“先整除;后求余”,否则会有隐患会有bug。除非,把四个临时变都改成unsigned long类型。上一节在此处残留了一个bug我发帖后还来不及修改过来,敬请大家注意。比如:

      以下这样是对的:
static unsigned char Su8Temp_1;
Su8Temp_1=vGu32CountdownTimerCnt/10%10; //一气呵成,没毛病。

      以下这样是有隐患有bug的(除非把Su8Temp_1改成unsigned long类型):
static unsigned char Su8Temp_1;
Su8Temp_1=vGu32CountdownTimerCnt/10;
Su8Temp_1=Su8Temp_1%10; //拆分成两行代码后,有隐患有bug。数据溢出的原因引起。

      本节倒计时程序的功能:K1按键是复位按键,每按一次,倒计时停止并且重新恢复初始值10.00秒。K2按键是启动按键,当秒表处于复位后停止的状态时按一次则开始启动倒计时,当倒计时变为0.00秒的时候,蜂鸣器发出一次“滴”的提示声。此时,如果想启动新一轮的倒计时,只需按一次K1复位键,然后再按K2启动按键,就可以启动新一轮10.00秒的倒计时。代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25

#define SCAN_TIME1   

#define VOICE_TIME   50   //蜂鸣器一次“滴”的声音长度 50ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);//蜂鸣器的驱动函数
void DisplayScan(void);//底层显示的驱动函数
void DisplayTask(void);//上层显示的任务函数

void RunTask(void);//倒计时的应用程序

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};

//数码管底层驱动扫描的软件定时器
volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

//倒计时的软件定时器,注意,这里是unsigned long类型,范围是0到4294967295毫秒
volatile unsigned char vGu8CountdownTimerFlag=0;
volatile unsigned long vGu32CountdownTimerCnt=10000;//开机默认显示初始值10.000秒

//数码管上层每10ms就定时刷新一次显示的软件定时器。用于及时更新显示秒表当前的实时数值
volatile unsigned char vGu8UpdateTimerFlag=0;
volatile unsigned int vGu16UpdateTimerCnt=0;

//蜂鸣器的软件定时器
volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8RunStart=0;//应用程序的总启动
unsigned char Gu8RunStep=0;//应用程序的总运行步骤。建议跟vGu8RunStart成双成对出现
unsigned char Gu8RunStatus=0; //当前倒计时的状态。0代表停止,1代表正在工作中

unsigned char Gu8WdUpdate=1;//开机默认整屏更新一次显示。此变量在显示框架中是非常重要的变量

volatile unsigned char vGu8Display_Righ_4=1;//显示“1”,跟vGu32CountdownTimerCnt高位一致
volatile unsigned char vGu8Display_Righ_3=0;
volatile unsigned char vGu8Display_Righ_2=0;
volatile unsigned char vGu8Display_Righ_1=0;

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=1;    //开机默认保留显示2个小数点
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
RunTask();      //倒计时的应用程序
    }
}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //复位按键

Gu8RunStatus=0; //倒计时返回停止的状态

Gu8RunStart=0;//倒计时的运行步骤的停止
            Gu8RunStep=0;//总运行步骤归零。建议跟vGu8RunStart成双成对出现

vGu8CountdownTimerFlag=0;    //禁止倒计时开始(而Gu8RunStart是启动开关,不矛盾)
vGu32CountdownTimerCnt=10000;   //倒计时的软件定时器恢复初始值10.000秒

            Gu8WdUpdate=1;//整屏更新一次显示

vGu8KeySec=0;
break;

   case 2:   //启动的按键
      if(0==Gu8RunStatus) //在停止状态下
      {

vGu8CountdownTimerFlag=0;
vGu32CountdownTimerCnt=10000;//初始值是10.000秒
vGu8CountdownTimerFlag=1;      //允许倒计时的软件定时器的启动

            Gu8RunStatus=1;//倒计时处于工作状态(并且,这一瞬间才正式启动倒计时)

Gu8RunStart=1;   //倒计时的运行步骤的总开关开启
                Gu8RunStep=0;    //总运行步骤归零。建议跟vGu8RunStart成双成对出现

            Gu8WdUpdate=1;//整屏更新一次显示,确保在启动的时候能显示到最新的数据
}


vGu8KeySec=0;
break;

}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          //先分解数据,注意,这里分解的时候,“先整除后求余”必须用一行代码一气呵成,不能拆
          //分成两行代码,否则会有隐患会有bug。除非,把四个临时变都改成unsigned long类型。

          //Su8Temp_4提取“十秒”位。
Su8Temp_4=vGu32CountdownTimerCnt/10000%10; //实际精度是0.001秒,但显示精度是0.01秒

         //Su8Temp_3提取“个秒”位。
Su8Temp_3=vGu32CountdownTimerCnt/1000%10; //实际精度是0.001秒,但显示精度是0.01秒

         //Su8Temp_2提取“百毫秒”位。
Su8Temp_2=vGu32CountdownTimerCnt/100%10; //实际精度是0.001秒,但显示精度是0.01秒

          //Su8Temp_1提取“十毫秒”位。
Su8Temp_1=vGu32CountdownTimerCnt/10%10; //实际精度是0.001秒,但显示精度是0.01秒

          //判断数据范围,来决定最高位数码管是否需要显示。
          if(vGu32CountdownTimerCnt<10000) //10.000秒。实际4位数码管最大只能显示99.99秒
          {
             Su8Temp_4=10;//在数码管转换表里,10代表一个“不显示”的数据
}

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=1;   //保留显示2位小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

}
}

void RunTask(void)//倒计时的应用程序
{
if(0==Gu8RunStart)
{
   return;// 如果总开关处于停止状态,则直接退出当前函数,不执行该函数以下的其它代码
}

switch(Gu8RunStep)
{
    case 0://在这个步骤里,主要用来初始化一些参数

vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//每10ms更新显示一次当前倒计时的时间
vGu8UpdateTimerFlag=1;

Gu8RunStep=1; //跳转到每10ms更新显示一次的步骤里
break;

    case 1://每10ms更新一次显示,确保实时显示当前倒计时的时间
       if(0==vGu16UpdateTimerCnt) //每10ms更新显示一次当前倒计时的时间
       {


    vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//重置定时器,为下一个10ms更新做准备
vGu8UpdateTimerFlag=1;

            Gu8WdUpdate=1;//整屏更新一次显示当前倒计时的时间

            if(0==vGu32CountdownTimerCnt) //如果倒计时的时间到,则跳转到结束的步骤
            {
Gu8RunStep=2; //跳转到倒计时结束的步骤
}

}
break;

case 2://倒计时结束的步骤
//Gu8RunStatus=0; //这行代码注释掉,让每次新启动之前都必须按一次K1复位按键才有效

Gu8RunStart=0;//倒计时的运行步骤的停止
            Gu8RunStep=0;   //总运行步骤归零。建议跟vGu8RunStart成双成对出现

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

      Gu8WdUpdate=1;//整屏更新一次显示当前倒计时的时间

break;

}

}


void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;

   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;   
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }
}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}


void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

//每10ms就定时更新一次显示的软件定时器
if(1==vGu8UpdateTimerFlag&&vGu16UpdateTimerCnt>0)
{
vGu16UpdateTimerCnt--;//递减式的软件定时器
}

//倒计时实际走的时间的软件定时器,注意,这里还附加了启动状态的条件“&&1==Gu8RunStatus”
if(1==vGu8CountdownTimerFlag&&vGu32CountdownTimerCnt>0&&1==Gu8RunStatus)
{
vGu32CountdownTimerCnt--;//递减式的软件定时器
}


TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

chenghuiwan 发表于 2018-4-30 14:38:25

楼主定时更新一节的精神值得赞赏,谢谢分亨!

吴坚鸿 发表于 2018-5-12 10:43:55

第一百一十七节: 按键切换数码管窗口来设置参数。

【117.1   按键切换数码管窗口来设置参数。】
         
                上图117.1.1数码管



         
                上图117.1.2独立按键

         
                上图117.1.3有源蜂鸣器

      单片机是“数据”驱动型的。按什么逻辑跑,以什么方式跑,都是“数据”决定的。人机交互的核心就是“人”以什么渠道去更改“机”内部的某些“数据”。在程序框架层面,按键更改或者编辑某些数据,我的核心思路都是“在某个窗口下去更改某个特定的数据”,如果某个窗口的数据很多,就需要在此窗口下进一步细分,细分为“某个窗口下的某个局部(子菜单、光标选择)”。可见,“窗口”是支点,“局部”是支点中再细分出来的支点。窗口对应一个名叫 “窗口选择”的全局变量Gu8Wd,局部(子菜单、光标选择)对应一个名叫“局部选择”的全局变量Gu8Part。数据发生变化的时候,才需要更新显示到数码管上,平时不用一直更新显示,因此,与“窗口选择”Gu8Wd还对应一个名叫“整屏更新”的全局变量Gu8WdUpdate,与“局部选择”Gu8Part还对应一个名叫“局部更新”的全局变量Gu8PartUpdate。本节的小项目程序只用到“窗口”,没有用到“局部”。
      本节小项目的程序功能,利用按键与数码管的人机交互,可以对单片机内部三个参数Gu8SetData_1,Gu8SetData_2,Gu8SetData_3进行编辑。这三个参数分别在三个窗口下进行编辑,这三个窗口是数码管显示“1-XX”,“2-YY”,“3-ZZ”。其中,XX代表Gu8SetData_1数据,YY代表Gu8SetData_2数据,ZZ代表Gu8SetData_3数据,这三个数据的范围是从0到99。K1是窗口切换按键,每按一次,窗口会在“1-XX”,“2-YY”,“3-ZZ”三个窗口之间进行切换。K2是数字累加按键,每按一次,显示的数字会累加1。K3是数字递减按键,每按一次,显示的数字会递减1。代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25
#define SCAN_TIME1   
#define VOICE_TIME   50   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);//上层显示的任务函数
void Wd1(void);   //窗口1显示函数
void Wd2(void);   //窗口2显示函数
void Wd3(void);   //窗口3显示函数

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8SetData_1=0; //单片机内部第1个可编辑的参数
unsigned char Gu8SetData_2=0; //单片机内部第2个可编辑的参数
unsigned char Gu8SetData_3=0; //单片机内部第3个可编辑的参数

unsigned char Gu8Wd=1;   //窗口选择变量。人机交互程序框架的支点。初始化开机后显示第1个窗口。
unsigned char Gu8WdUpdate=1;//整屏更新变量。初始化为1开机后整屏更新一次显示。

volatile unsigned char vGu8Display_Righ_4=1;//显示窗口“1”
volatile unsigned char vGu8Display_Righ_3=11; //显示横杠“-”
volatile unsigned char vGu8Display_Righ_2=0;//显示十位数值“0”
volatile unsigned char vGu8Display_Righ_1=0;//显示个位数值“0”

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=0;   
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
    }
}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //窗口切换的按键
Gu8Wd++;//窗口切换到下一个窗口
if(Gu8Wd>3)//一共3个窗口。切换第3个窗口之后,继续返回到第1个窗口
{
Gu8Wd=1;   //返回到第1个窗口
}
            Gu8WdUpdate=1;//整屏更新一次显示

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 2:   //累加的按键
      switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去编辑对应的数据。又一次用到switch语句
      {
             case 1:   //在第1个窗口下编辑Gu8SetData_1数据
                  Gu8SetData_1++;
                  if(Gu8SetData_1>99) //把最大范围限定在99
{
Gu8SetData_1=99;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;

             case 2:   //在第2个窗口下编辑Gu8SetData_2数据
                  Gu8SetData_2++;
                  if(Gu8SetData_2>99) //把最大范围限定在99
{
Gu8SetData_2=99;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;

             case 3:   //在第3个窗口下编辑Gu8SetData_3数据
                  Gu8SetData_3++;
                  if(Gu8SetData_3>99) //把最大范围限定在99
{
Gu8SetData_3=99;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 3:   //递减的按键
      switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去编辑对应的数据。又一次用到switch语句
      {
             case 1:   //在第1个窗口下编辑Gu8SetData_1数据
                  if(Gu8SetData_1>0) //把最小范围限定在0
{
Gu8SetData_1--;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;

             case 2:   //在第2个窗口下编辑Gu8SetData_2数据
                  if(Gu8SetData_2>0) //把最小范围限定在0
{
Gu8SetData_2--;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;

             case 3:   //在第3个窗口下编辑Gu8SetData_3数据
                  if(Gu8SetData_3>0) //把最小范围限定在0
{
Gu8SetData_3--;
}
                      Gu8WdUpdate=1;//整屏更新一次显示
                  break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;
}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1显示函数
      break;
    case 2:
      Wd2();   //窗口2显示函数
      break;
    case 3:
      Wd3();   //窗口3显示函数
      break;
}

}

void Wd1(void)   //窗口1显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=1;//窗口“1”
Su8Temp_3=11; //横杠“-”
Su8Temp_2=Gu8SetData_1/10%10; //十位数值
Su8Temp_1=Gu8SetData_1/1%10;//个位数值

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

//不显示任何一个小数点
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;
}
}

void Wd2(void)   //窗口2显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=2;//窗口“2”
Su8Temp_3=11; //横杠“-”
Su8Temp_2=Gu8SetData_2/10%10; //十位数值
Su8Temp_1=Gu8SetData_2/1%10;//个位数值

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

//不显示任何一个小数点
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;
}
}

void Wd3(void)   //窗口3显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=3;//窗口“3”
Su8Temp_3=11; //横杠“-”
Su8Temp_2=Gu8SetData_3/10%10; //十位数值
Su8Temp_1=Gu8SetData_3/1%10;//个位数值

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

//不显示任何一个小数点
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;
}
}

void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
    static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;

   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;   
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}


void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

tt98 发表于 2018-5-13 18:15:55

续顶鸿哥!{:handshake:}

RFman 发表于 2018-5-17 23:30:28

佩服鸿哥的坚持和技术能力,一口从1页下载到117页,拜读中 ,期待继续更新{:tongue:}

即墨丰禾 发表于 2018-5-18 11:14:05

紧跟这个帖子,从一到三十了。

吴坚鸿 发表于 2018-5-29 10:07:00

第一百一十八节: 按键让某位数码管闪烁跳动来设置参数。

【118.1   按键让某位数码管闪烁跳动来设置参数。】
         
                上图118.1.1数码管



         
                上图118.1.2独立按键

         
                上图118.1.3有源蜂鸣器

       当一个窗口只有一个数据的时候,只需以“窗口”为支点,切换到某个窗口下去设置某个数据即可。但是,当某个窗口有几个数据时,就必须在以“窗口”为支点的前提下,再细分出一个二级的支点,这个二级支点就是“局部”(或者称为子菜单)。“窗口”对应一个“窗口选择”的全局变量Gu8Wd,“局部”对应一个“局部选择”的全局变量Gu8Part。数据需要更新显示输出到屏幕(数码管)时,有两种更新方式,一种是“整屏更新”,另一种是“局部更新”。“整屏更新”只有一个整屏的更新变量Gu8WdUpdate,而“局部更新”有N个更新变量Gu8PartUpdate_x(Gu8PartUpdate_1,Gu8PartUpdate_2,Gu8PartUpdate_3),一个窗口下有多少个数据就存在多少个局部的更新变量Gu8PartUpdate_x,这些局部的更新变量在不同的窗口下是可以共用的。当某个局部被选中的时候,可以有很多种表现方式,比如在液晶屏上,常见的有光标跳动,某行文字的底色变色(反显),本节例程用的数码管,当某个局部被选中的时候,用某位数码管闪烁跳动的方式。
       本节小项目的程序功能,在一个窗口下,对单片机内部四个参数Gu8SetData_4,Gu8SetData_3,Gu8SetData_2,Gu8SetData_1进行编辑。这四个参数的范围是从0到9,从左到右分别显示在四位数码管上,每一位数码管对应一个数据。比如左起第1位是Gu8SetData_4,左起第2位是Gu8SetData_3,左起第3位是Gu8SetData_2,左起第4位是Gu8SetData_1。K1是局部选择的切换按键,每按一次,数码管从左到右,依次闪烁跳动,表示某个数据被选中。K2是数字累加按键,每按一次,闪烁跳动的数字会累加1。K3是数字递减按键,每按一次,闪烁跳动的数字会递减1。代码如下:
#include "REG52.H"

#define KEY_FILTER_TIME25
#define SCAN_TIME1   
#define VOICE_TIME   50   
#define BLINK_TIME   250    //数码管闪烁跳动的时间的间隔


void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);//上层显示的任务函数
void Wd1(void);   //窗口1显示函数
void PartUpdate(unsigned char u8Part);//局部选择对应的某个局部变量更新显示输出

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8BlinkTimerFlag=0;   //数码管闪烁跳动的定时器
volatile unsigned int vGu16BlinkTimerCnt=0;

unsigned char Gu8SetData_4=0; //单片机内部第4个可编辑的参数
unsigned char Gu8SetData_3=0; //单片机内部第3个可编辑的参数
unsigned char Gu8SetData_2=0; //单片机内部第2个可编辑的参数
unsigned char Gu8SetData_1=0; //单片机内部第1个可编辑的参数

unsigned char Gu8Wd=1;   //窗口选择变量。人机交互程序框架的支点。初始化开机后显示第1个窗口。
unsigned char Gu8WdUpdate=1;//整屏更新变量。初始化为1开机后整屏更新一次显示。
unsigned char Gu8Part=0;   //局部选择变量。0代表当前窗口下没有数据被选中。
unsigned char Gu8PartUpdate_1=0;   //局部1的更新变量,
unsigned char Gu8PartUpdate_2=0;   //局部2的更新变量
unsigned char Gu8PartUpdate_3=0;   //局部3的更新变量
unsigned char Gu8PartUpdate_4=0;   //局部4的更新变量


volatile unsigned char vGu8Display_Righ_4=0;   //左起第1位初始化显示数值“0”
volatile unsigned char vGu8Display_Righ_3=0;   //左起第2位初始化显示数值“0”
volatile unsigned char vGu8Display_Righ_2=0;   //左起第3位初始化显示数值“0”
volatile unsigned char vGu8Display_Righ_1=0;   //左起第4位初始化显示数值“0”

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=0;   
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
    }
}

void PartUpdate(unsigned char u8Part)//局部选择对应的某个局部变量更新显示输出
{
    switch(u8Part)
{
   case 1:
Gu8PartUpdate_1=1;
break;
   case 2:
Gu8PartUpdate_2=1;
break;
   case 3:
Gu8PartUpdate_3=1;
break;
   case 4:
Gu8PartUpdate_4=1;
break;
}

}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //局部切换的按键
      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
                //以下之所以有两个PartUpdate(Gu8Part),是因为相邻的两个局部发生了变化。

                PartUpdate(Gu8Part);//切换之前的局部进行更新。
                Gu8Part++;//切换到下一个局部
                if(Gu8Part>4)
                {
Gu8Part=0;
}
                PartUpdate(Gu8Part);//切换之后的局部进行更新。
                break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 2:   //累加的按键
      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
                switch(Gu8Part)//二级支点的局部选择
                {
                  case 1://局部1被选中,代表左起第1位数据Gu8SetData_4被选中。
Gu8SetData_4++;
if(Gu8SetData_4>9)
{
Gu8SetData_4=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 2://局部2被选中,代表左起第2位数据Gu8SetData_3被选中。
Gu8SetData_3++;
if(Gu8SetData_3>9)
{
Gu8SetData_3=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 3://局部3被选中,代表左起第3位数据Gu8SetData_2被选中。
Gu8SetData_2++;
if(Gu8SetData_2>9)
{
Gu8SetData_2=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 4://局部4被选中,代表左起第4位数据Gu8SetData_1被选中。
Gu8SetData_1++;
if(Gu8SetData_1>9)
{
Gu8SetData_1=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

}
break;

         case 2:   //在窗口2下(本节只用到窗口1)
                  break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 3:   //递减的按键
      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
                switch(Gu8Part)//二级支点的局部选择
                {
                  case 1://局部1被选中,代表左起第1位数据Gu8SetData_4被选中。
if(Gu8SetData_4>0)
{
Gu8SetData_4--;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 2://局部2被选中,代表左起第2位数据Gu8SetData_3被选中。
if(Gu8SetData_3>0)
{
Gu8SetData_3--;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 3://局部3被选中,代表左起第3位数据Gu8SetData_2被选中。
if(Gu8SetData_2>0)
{
Gu8SetData_2--;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                  case 4://局部4被选中,代表左起第4位数据Gu8SetData_1被选中。
if(Gu8SetData_1>0)
{
Gu8SetData_1--;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

}
break;

         case 2:   //在窗口2下(本节只用到窗口1)
                  break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;
}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1显示函数
      break;
    case 2:      //窗口2显示选择(本节只用到窗口1)
      break;
}

}

void Wd1(void)   //窗口1显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//不显示任何一个小数点
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
Gu8PartUpdate_3=1;//局部3更新显示
Gu8PartUpdate_4=1;//局部4更新显示

}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=Gu8SetData_4;//显示左起第1个数据Gu8SetData_4

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_3=Gu8SetData_3;//显示左起第2个数据Gu8SetData_3

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_3) //局部3更新显示
{
Gu8PartUpdate_3=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8SetData_2;//显示左起第3个数据Gu8SetData_2

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_4) //局部4更新显示
{
Gu8PartUpdate_4=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_1=Gu8SetData_1;//显示左起第4个数据Gu8SetData_1

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}

if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    switch(Gu8Part)//某个局部被选中,则闪烁跳动
    {
      case 1:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_4=10;//左起第1个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_4=Gu8SetData_4;//左起第1个显示数据Gu8SetData_4
}

             break;

      case 2:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_3=10;//左起第2个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_3=Gu8SetData_3;//左起第2个显示数据Gu8SetData_3
}

             break;

      case 3:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//左起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_2=Gu8SetData_2;//左起第3个显示数据Gu8SetData_2
}

             break;

      case 4:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_1=10;//左起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_1=Gu8SetData_1;//左起第4个显示数据Gu8SetData_1
}

             break;
      default:   //都没有被选中的时候
       Su8Temp_4=Gu8SetData_4;//左起第1个显示数据Gu8SetData_4
       Su8Temp_3=Gu8SetData_3;//左起第2个显示数据Gu8SetData_3
       Su8Temp_2=Gu8SetData_2;//左起第3个显示数据Gu8SetData_2
       Su8Temp_1=Gu8SetData_1;//左起第4个显示数据Gu8SetData_1
             break;
}

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量


}

}


void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
    static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;

   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;   
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}


void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

if(1==vGu8BlinkTimerFlag&&vGu16BlinkTimerCnt>0)   //数码管闪烁跳动的定时器
{
vGu16BlinkTimerCnt--;//递减式的软件定时器
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

tt98 发表于 2018-5-30 16:54:38

鸿哥又更新了,继续期待后续!

吴坚鸿 发表于 2018-6-3 10:40:03

第一百一十九节: 一个完整的人机界面的程序框架的脉络。

【119.1   一个完整的人机界面的程序框架的脉络。】

       前面两节例子告诉我们,一个完整的人机界面的程序框架包含两个要素,分别是“支点”与“更新”。“支点”包括“窗口选择”和“局部选择”,“更新”包括“整屏更新”和“局部更新”。
       “支点”的作用是把显示函数与按键函数完美无缝的关联起来,两个函数同样的“支点”促使同样的“话语体系”,让“所见即所得”实时同步,确保按键操作的数据就是当前显示被选中的数据。
       “静态数据”与“动态数据”的概念。被窗口显示的数据通常有两种:一种是静态数据,比如装饰门面的数据,只能显示不能更改的数据,以及图片图标这类数据;另外一种是动态数据,这种数据在窗口显示上是活动的可编辑的,是需要经常修改的,往往也是系统核心的数据,需要保存或者需要跟某些关键运动密切相关的数据。比如,在前面章节中,数码管要显示三个窗口“1-XX”,“2-YY”,“3-ZZ”,其中“1-”、“2-”、“3-”是属于静态数据,它们是起“装饰”作用的。而“XX”、“YY”、“ZZ”则是动态数据,它们是可编辑的,也是单片机系统内部核心的数据。
       “整屏更新”与“局部更新”的分工。“整屏更新”主要负责在切换新窗口时,把“静态数据”一次性显示到当前窗口。而“局部更新”主要负责在当前窗口下显示“动态数据”。
       下面,我把一个完整的人机界面的程序框架的脉络勾勒出来,让大家有一个整体的观感,这种人机界面的程序框架放之四海而皆准,我已把它应用在各种数码管,单色液晶屏,彩屏,电脑上位机等项目上。假设某个项目中只有两个”窗口”只有两个“局部”,程序框架的脉络如下:

       显示部分:

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以“窗口选择”Gu8Wd为支点
{
    case 1:
      Wd1();   //窗口1显示函数
      break;
    case 2:   
      Wd2();   //窗口2显示函数
      break;
}
}


void Wd1(void)   //窗口1显示函数
{
if(1==Gu8WdUpdate) //整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          ......    //此处省略N行代码,用来显示静态的数据,比如图片图标,或者装饰的数据


//以下,“整屏更新”必然是要把所有的“局部更新”都触发一次
Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          ......    //此处省略N行代码,用来显示动态的数据。比如可编辑的数据,实时变化的数据

}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          ......    //此处省略N行代码,用来显示动态的数据。比如可编辑的数据,实时变化的数据

}

if(0==vGu16BlinkTimerCnt)//跳动的光标,或者动态闪烁的某位被选中的数据
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

      ......    //此处省略N行代码,用来制作跳动的光标或者某位被选中而闪烁的数据

}

}


void Wd2(void)   //窗口2显示函数
{
          ......    //此处省略N行代码,窗口2显示函数的代码跟窗口1类似
}


       按键部分:

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //1号按键
      switch(Gu8Wd) //以“窗口选择”Gu8Wd为支点
      {
         case 1:   //在窗口1下
                switch(Gu8Part)//以“局部选择”Gu8Part为支点
                {
                  case 1:

                        ......    //此处省略N行代码

                        break;

                  case 2://局部2被选中

                        ......    //此处省略N行代码

                        break;
}
break;

         case 2:   //在窗口2下
                switch(Gu8Part)//以“局部选择”Gu8Part为支点
                {
                  case 1:

                        ......    //此处省略N行代码

                        break;

                  case 2://局部2被选中

                        ......    //此处省略N行代码

                        break;
}
break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 2:   //2号按键

      ......    //此处省略N行代码,跟1号按键的代码类似

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

}
}

tt98 发表于 2018-6-4 21:29:58

鸿哥给力啊!{:lol:}

devcang 发表于 2018-6-5 00:56:12


觉得可以看看,谢谢

吴坚鸿 发表于 2018-6-10 12:16:14

第一百二十节: 按键切换窗口切换局部来设置参数。

【120.1   按键切换窗口切换局部来设置参数。】

         
                上图120.1.1数码管

         
                上图120.1.2独立按键

         
                上图120.1.3有源蜂鸣器

       为了更好理解上一节提出的人机界面程序框架的脉络,本节程序恰好包含了整屏更新与局部更新的应用,同时也引入了一个新的知识点:在人机界面的程序框架中,常常会遇到需要以“位”来编辑某个数据的情况,这种情况实际上是先把“待编辑数据”分解成几个“位”中间临时个体,然后显示并且编辑这些“位”中间临时个体,编辑结束后,再把这些“位”中间临时个体合并成一个完整的数据赋值给“待编辑数据”。
       本节程序功能如下:
      (1)有3个窗口1-XX,2-YY,3-ZZ,其中XX,YY,ZZ分别代表3个可编辑的数据Gu8SetDate_1,Gu8SetDate_2,Gu8SetDate_3。数据范围是从0到99。
      (2)K1按键。含“短按”与“长按”复合双功能。当数码管“没有闪烁”时,“短按”K1按键可以切换窗口,而“长按”K1按键会使数码管从“没有闪烁”进入到“闪烁模式”。当数码管处于“闪烁模式”时,“短按”K1可以使数码管在十位和个位之间切换“闪烁”的“局部位”,而“长按”K1表示更改完毕当前窗口数据并从“闪烁模式”退出到“没有闪烁”。
      (3)K2按键。当数码管处于“闪烁模式”时,每按一次K2按键就可以使当前闪烁的某位数码管“递增1”。
      (4)K3按键。当数码管处于“闪烁模式”时,每按一次K2按键就可以使当前闪烁的某位数码管“递减1”。
       上述功能,在窗口切换和退出“闪烁模式”时用到整屏更新,在闪烁的某位数码管切换“局部”时用到局部更新。代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25    //按键的“短按”兼“滤波”的“稳定时间”
#define KEY_LONG_TIME    500   //按键的“长按”兼“滤波”的“稳定时间”

#define SCAN_TIME1   
#define VOICE_TIME   50   
#define BLINK_TIME   250    //数码管闪烁跳动的时间的间隔

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);//上层显示的任务函数
void Wd1(void);   //窗口1显示函数
void Wd2(void);   //窗口2显示函数
void Wd3(void);   //窗口3显示函数

void PartUpdate(unsigned char u8Part);//局部选择对应的某个局部变量更新显示输出

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8BlinkTimerFlag=0;   //数码管闪烁跳动的定时器
volatile unsigned int vGu16BlinkTimerCnt=0;

unsigned char Gu8SetData_3=0; //单片机内部第3个可编辑的参数,在窗口3
unsigned char Gu8SetData_2=0; //单片机内部第2个可编辑的参数,在窗口2
unsigned char Gu8SetData_1=0; //单片机内部第1个可编辑的参数,在窗口1

/* 注释一:
*      在人机界面的程序框架中,常常会遇到需要以“位”来编辑某个数据的情况,这种情况
*实际上是先把“待编辑数据”分解成几个“位”临时中间个体,然后显示并且编辑这些“位”
*临时中间个体,编辑结束后,再把这些“位”临时中间个体合并成一个完整的数据赋值给
*“待编辑数据”。以下Gu8EditData_2和Gu8EditData_1就是“位”临时中间个体的中间变量。
*/

unsigned char Gu8EditData_2=0;//对应显示左起第3位数码管的“位”数据,是中间变量。
unsigned char Gu8EditData_1=0;//对应显示左起第4位数码管的“位”数据,是中间变量。

unsigned char Gu8Wd=1;   //窗口选择变量。人机交互程序框架的支点。初始化开机后显示第1个窗口。
unsigned char Gu8WdUpdate=1;//整屏更新变量。初始化为1开机后整屏更新一次显示。
unsigned char Gu8Part=0;   //局部选择变量。0代表当前窗口下没有数据被选中。
unsigned char Gu8PartUpdate_1=0;   //局部1的更新变量,
unsigned char Gu8PartUpdate_2=0;   //局部2的更新变量


volatile unsigned char vGu8Display_Righ_4=1;   //左起第1位初始化显示窗口“1”
volatile unsigned char vGu8Display_Righ_3=11;//左起第2位初始化显示横杠“-”
volatile unsigned char vGu8Display_Righ_2=0;   //左起第3位初始化显示数值“0”
volatile unsigned char vGu8Display_Righ_1=0;   //左起第4位初始化显示数值“0”

//不显示小数点
volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=0;   
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
    }
}

void PartUpdate(unsigned char u8Part)//局部选择对应的某个局部变量更新显示输出
{
    switch(u8Part)
{
   case 1:
Gu8PartUpdate_1=1;
break;
   case 2:
Gu8PartUpdate_2=1;
break;
}

}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //K1按键的“短按”,具有“切换窗口”和“切换局部”的双功能。
      if(0==Gu8Part)//处于“没有闪烁”的时候,是“切换窗口”
{
            switch(Gu8Wd) //在某个窗口下
            {
                case 1:   //在窗口1下
Gu8Wd=2;//切换到窗口2
Gu8EditData_2=Gu8SetData_2/10%10;//“待编辑数据”分解成中间个体
Gu8EditData_1=Gu8SetData_2/1%10;   //“待编辑数据”分解成中间个体
            Gu8WdUpdate=1;//整屏更新
                      break;

                case 2:   //在窗口2下
Gu8Wd=3;//切换到窗口3
Gu8EditData_2=Gu8SetData_3/10%10;//“待编辑数据”分解成中间个体
Gu8EditData_1=Gu8SetData_3/1%10;   //“待编辑数据”分解成中间个体
            Gu8WdUpdate=1;//整屏更新
                      break;

                case 3:   //在窗口3下
Gu8Wd=1;//切换到窗口1
Gu8EditData_2=Gu8SetData_1/10%10;//“待编辑数据”分解成中间个体
Gu8EditData_1=Gu8SetData_1/1%10;   //“待编辑数据”分解成中间个体
             Gu8WdUpdate=1;//整屏更新
                     break;

}
}
            else    //处于“闪烁模式”的时候,是“切换局部”
{
    PartUpdate(Gu8Part);//切换之前的局部进行更新。
    Gu8Part++;//切换局部
            if(Gu8Part>2)
            {
Gu8Part=1;
}
            PartUpdate(Gu8Part);//切换之后的局部进行更新。
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 2:   //递增按键K2
      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
         case 2:   //在窗口2下,窗口2与窗口1的代码完全一模一样,因此可以这样共享
         case 3:   //在窗口3下,窗口3与窗口1的代码完全一模一样,因此可以这样共享
                switch(Gu8Part)//二级支点的局部选择
                {
                  case 1://局部1被选中,代表左起第3位数码管被选中。
Gu8EditData_2++;//编辑“十位”个体的中间变量
if(Gu8EditData_2>9)
{
Gu8EditData_2=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                   case 2://局部2被选中,代表左起第4位数码管被选中。
Gu8EditData_1++;//编辑“个位”个体的中间变量
if(Gu8EditData_1>9)
{
Gu8EditData_1=9;
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;
}
break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 3:   //递减按键K3
      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
         case 2:   //在窗口2下,窗口2与窗口1的代码完全一模一样,因此可以这样共享
         case 3:   //在窗口3下,窗口3与窗口1的代码完全一模一样,因此可以这样共享
                switch(Gu8Part)//二级支点的局部选择
                {
                  case 1://局部1被选中,代表左起第3位数码管被选中。
if(Gu8EditData_2>0)
{
Gu8EditData_2--; //编辑“十位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;

                   case 2://局部2被选中,代表左起第4位数码管被选中。
if(Gu8EditData_1>0)
{
Gu8EditData_1--; //编辑“个位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                        break;
}
break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 4:   //K1按键的“长按”,具有进入和退出“闪烁模式”的功能。“退出”隐含“确定”

      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
                if(0==Gu8Part)//处于“没有闪烁”的时候,将进入“闪烁模式”
{
    Gu8EditData_2=Gu8SetData_1/10%10;//先把“待编辑数据”分解成中间个体
    Gu8EditData_1=Gu8SetData_1/1%10;   //先把“待编辑数据”分解成中间个体
    Gu8Part=1;//进入“闪烁模式”,从“局部1”开始闪烁
}
                  else    //处于“闪烁模式”的时候,将退出到“没有闪烁”,隐含“确定”功能
{
    Gu8SetData_1=Gu8EditData_2*10+Gu8EditData_1; //把个体合并还原成数据
    Gu8Part=0;//退出“闪烁模式”
            Gu8WdUpdate=1;//整屏更新
}
                break;

         case 2:   //在窗口2下
                if(0==Gu8Part)//处于“没有闪烁”的时候,将进入“闪烁模式”
{
    Gu8EditData_2=Gu8SetData_2/10%10;//先把“待编辑数据”分解成中间个体
    Gu8EditData_1=Gu8SetData_2/1%10;   //先把“待编辑数据”分解成中间个体
    Gu8Part=1;//进入“闪烁模式”,从“局部1”开始闪烁
}
                  else    //处于“闪烁模式”的时候,将退出到“没有闪烁”,隐含“确定”功能
{
    Gu8SetData_2=Gu8EditData_2*10+Gu8EditData_1; //把个体合并还原成数据
    Gu8Part=0;//退出“闪烁模式”
            Gu8WdUpdate=1;//整屏更新
}
                break;

         case 3:   //在窗口3下
                if(0==Gu8Part)//处于“没有闪烁”的时候,将进入“闪烁模式”
{
    Gu8EditData_2=Gu8SetData_3/10%10;//先把“待编辑数据”分解成中间个体
    Gu8EditData_1=Gu8SetData_3/1%10;   //先把“待编辑数据”分解成中间个体
    Gu8Part=1;//进入“闪烁模式”,从“局部1”开始闪烁
}
                  else    //处于“闪烁模式”的时候,将退出到“没有闪烁”,隐含“确定”功能
{
    Gu8SetData_3=Gu8EditData_2*10+Gu8EditData_1; //把个体合并还原成数据
    Gu8Part=0;//退出“闪烁模式”
            Gu8WdUpdate=1;//整屏更新
}
                break;

}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1显示函数
      break;
    case 2:
      Wd2();   //窗口2显示函数
      break;
    case 3:
      Wd3();   //窗口3显示函数
      break;
}

}

void Wd1(void)   //窗口1显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=1;   //左起第1位数码管,显示窗口“1”,属于静态数据,起“装饰”作用。
Su8Temp_3=11;//左起第2位数码管,显示横杠“-”,属于静态数据,起“装饰”作用。

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量

//不显示任何一个小数点,属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。

vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    switch(Gu8Part)//某个局部被选中,则闪烁跳动
    {
      case 1:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//左起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
}

             break;

      case 2:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_1=10;//左起第4个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。
}

             break;

      default:   //都没有被选中的时候
       Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。      
             break;
}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量

}
}

void Wd2(void)   //窗口2显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=2;   //左起第1位数码管,显示窗口“2”,属于静态数据,起“装饰”作用。
Su8Temp_3=11;//左起第2位数码管,显示横杠“-”,属于静态数据,起“装饰”作用。

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量

//不显示任何一个小数点,属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。

vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    switch(Gu8Part)//某个局部被选中,则闪烁跳动
    {
      case 1:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//左起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
}

             break;

      case 2:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_1=10;//左起第4个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。
}

             break;

      default:   //都没有被选中的时候
       Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。      
             break;
}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量

}
}

void Wd3(void)   //窗口3显示函数
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=3;   //左起第1位数码管,显示窗口“3”,属于静态数据,起“装饰”作用。
Su8Temp_3=11;//左起第2位数码管,显示横杠“-”,属于静态数据,起“装饰”作用。

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量

//不显示任何一个小数点,属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。

vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    switch(Gu8Part)//某个局部被选中,则闪烁跳动
    {
      case 1:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//左起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
}

             break;

      case 2:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_1=10;//左起第4个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。
}

             break;

      default:   //都没有被选中的时候
       Su8Temp_2=Gu8EditData_2;//显示“十位”的临时中间个体,属于动态数据。
Su8Temp_1=Gu8EditData_1;//显示“个位”的临时中间个体,属于动态数据。      
             break;
}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量

}
}


void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyShortFlag=0;//按键“短按”触发的标志   
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;

//需要详细分析以下这段“短按”与“长按”代码的朋友,请参考第96节。
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
      if(1==Su8KeyShortFlag)
      {
Su8KeyShortFlag=0;   
vGu8KeySec=1;    //触发K1的“短按”
}
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;

      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
            Su8KeyShortFlag=1;
      }

      if(Su16KeyCnt1>=KEY_LONG_TIME)
      {
            Su8KeyLock1=1;      
Su8KeyShortFlag=0;
            vGu8KeySec=4; //触发K1的“长按”
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}


void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

if(1==vGu8BlinkTimerFlag&&vGu16BlinkTimerCnt>0)   //数码管闪烁跳动的定时器
{
vGu16BlinkTimerCnt--;//递减式的软件定时器
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

lsszk 发表于 2018-6-11 09:31:46

谢谢楼主分享,顶一下

kkfy888 发表于 2018-6-13 21:27:40

说到做到,支持这精神!

zmh169 发表于 2018-6-14 13:46:31

坚持的坚,加油打call

tt98 发表于 2018-6-17 07:43:01

吴坚鸿 发表于 2018-6-10 12:16
第一百二十节: 按键切换窗口切换局部来设置参数。

【120.1   按键切换窗口切换局部来设置参数。】


谢谢鸿哥再次更新!{:lol:}

吴坚鸿 发表于 2018-6-21 14:42:56

第一百二十一节: 可调参数的数码管倒计时。

【121.1   可调参数的数码管倒计时。】
      
                上图121.1.1数码管



      
                上图121.1.2独立按键

      
                上图121.1.3有源蜂鸣器

       上节讲如何设置数据,本节讲“数据”如何关联“某种功能”,本节的“可调参数”就是“数据”,“倒计时”就是“某种功能”。程序功能如下:
      (1)倒计时范围从0.00秒到99.99秒,范围可调。开机默认是:10.00秒。
      (2)K1[设置键]。当数码管“没有闪烁”时,“长按”K1键则进入“闪烁模式”,某位数码管开始闪烁,闪烁的位代表可修改的位数据,此时再“短按”K1按键可以使数码管在位之间切换闪烁。当数码管处于“闪烁模式”时,“长按”K1按键,代表数据修改完成并停止闪烁。
      (3)K2[加键]与[复位健]。当数码管某位正在闪烁时,此时K2是[加键],按K2会使位数据“自加1”。当数码管“没有闪烁”时,此时K2是[复位键],按K2会使当前倒计时数据恢复“设置值”。
      (4)K3[减键]与[开始健]。当数码管某位正在闪烁时,此时K3是[减键],按K3会使位数据“自减1”。当数码管“没有闪烁”时,此时K3是[开始键],按K3开始倒计时。
       代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25    //按键的“短按”兼“滤波”的“稳定时间”
#define KEY_LONG_TIME    500   //按键的“长按”兼“滤波”的“稳定时间”

#define SCAN_TIME1   
#define VOICE_TIME   50   
#define BLINK_TIME   250    //数码管闪烁跳动的时间的间隔

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);//上层显示的任务函数
void RunTask(void);      //倒计时的应用程序

void Wd1(void);   //窗口1显示函数。用来设置参数。
void Wd2(void);   //窗口2显示函数。倒计时的运行显示窗口

void PartUpdate(unsigned char u8Part);//局部选择对应的某个局部变量更新显示输出

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8BlinkTimerFlag=0;   //数码管闪烁跳动的定时器
volatile unsigned int vGu16BlinkTimerCnt=0;

//倒计时的软件定时器,注意,这里是unsigned long类型,范围是0到4294967295毫秒
volatile unsigned char vGu8CountdownTimerFlag=0;
volatile unsigned long vGu32CountdownTimerCnt=10000;//当前倒计时的计时值
unsigned long Gu32SetData_Countdown=10000;//倒计时的设置值


//数码管上层每10ms就定时刷新一次显示的软件定时器。用于及时更新显示秒表当前的实时数值
volatile unsigned char vGu8UpdateTimerFlag=0;
volatile unsigned int vGu16UpdateTimerCnt=0;

unsigned char Gu8RunStart=0;//应用程序的总启动
unsigned char Gu8RunStep=0;//应用程序的总运行步骤。建议跟vGu8RunStart成双成对出现
unsigned char Gu8RunStatus=0; //当前倒计时的状态。0代表停止,1代表正在工作中

unsigned char Gu8EditData_4=0;//对应显示右起第4位数码管的“位”数据,是中间变量。
unsigned char Gu8EditData_3=0;//对应显示右起第3位数码管的“位”数据,是中间变量。
unsigned char Gu8EditData_2=0;//对应显示右起第2位数码管的“位”数据,是中间变量。
unsigned char Gu8EditData_1=0;//对应显示右起第1位数码管的“位”数据,是中间变量。

unsigned char Gu8Wd=1;   //窗口选择变量。人机交互程序框架的支点。初始化开机后显示第1个窗口。
unsigned char Gu8WdUpdate=1;//整屏更新变量。初始化为1开机后整屏更新一次显示。
unsigned char Gu8Part=0;   //局部选择变量。0代表当前窗口下没有数据被选中。
unsigned char Gu8PartUpdate_1=0;   //局部1的更新变量,
unsigned char Gu8PartUpdate_2=0;   //局部2的更新变量
unsigned char Gu8PartUpdate_3=0;   //局部3的更新变量,
unsigned char Gu8PartUpdate_4=0;   //局部4的更新变量

volatile unsigned char vGu8Display_Righ_4=1;//显示“1”,跟vGu32CountdownTimerCnt高位一致
volatile unsigned char vGu8Display_Righ_3=0;
volatile unsigned char vGu8Display_Righ_2=0;
volatile unsigned char vGu8Display_Righ_1=0;

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=1;    //开机默认保留显示2个小数点
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
RunTask();      //倒计时的应用程序
    }
}

void PartUpdate(unsigned char u8Part)//局部选择对应的某个局部变量更新显示输出
{
    switch(u8Part)
{
   case 1:
Gu8PartUpdate_1=1;
break;
   case 2:
Gu8PartUpdate_2=1;
break;
   case 3:
Gu8PartUpdate_3=1;
break;
   case 4:
Gu8PartUpdate_4=1;
break;
}

}

void RunTask(void)//倒计时的应用程序
{
if(0==Gu8RunStart)
{
   return;// 如果总开关处于停止状态,则直接退出当前函数,不执行该函数以下的其它代码
}

switch(Gu8RunStep)
{
    case 0://在这个步骤里,主要用来初始化一些参数

vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//每10ms更新显示一次当前倒计时的时间
vGu8UpdateTimerFlag=1;

Gu8RunStep=1; //跳转到每10ms更新显示一次的步骤里
break;

    case 1://每10ms更新一次显示,确保实时显示当前倒计时的时间
       if(0==vGu16UpdateTimerCnt) //每10ms更新显示一次当前倒计时的时间
       {


    vGu8UpdateTimerFlag=0;
vGu16UpdateTimerCnt=10;//重置定时器,为下一个10ms更新做准备
vGu8UpdateTimerFlag=1;

            Gu8WdUpdate=1;//整屏更新一次显示当前倒计时的时间

            if(0==vGu32CountdownTimerCnt) //如果倒计时的时间到,则跳转到结束的步骤
            {
Gu8RunStep=2; //跳转到倒计时结束的步骤
}

}
break;

case 2://倒计时结束的步骤
//Gu8RunStatus=0; //这行代码注释掉,让每次新启动之前都必须按一次K1复位按键才有效

Gu8RunStart=0;//倒计时的运行步骤的停止
            Gu8RunStep=0;   //总运行步骤归零。建议跟vGu8RunStart成双成对出现

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

      Gu8WdUpdate=1;//整屏更新一次显示当前倒计时的时间

break;

}

}


void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

if(0!=Gu8RunStatus) //在“非停止”状态下,用return来拦截一些“不该响应”的按键
{
    if(2==vGu8KeySec) //在“非停止”状态下,只响应[复位]这个按键
    {
       ;   //这里没有return语句,表示可以继续往下扫描本函数余下的代码,没有被拦截。
}
else
{
   return;   //其余的按键则拦截退出
}
}


switch(vGu8KeySec)
{
   case 1:   //按键K1的“短按”。切换数码管闪烁的位。
      switch(Gu8Wd) //在某个窗口下
      {
            case 1:   //在窗口1下

               if(0!=Gu8Part)//处于“闪烁模式”的时候,是“切换局部”
{
            PartUpdate(Gu8Part);//切换之前的局部进行更新。
            Gu8Part++;//切换局部
                      if(Gu8Part>4)
                      {
Gu8Part=1;
}
                      PartUpdate(Gu8Part);//切换之后的局部进行更新。

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;
}
                      break;
            }

vGu8KeySec=0;
break;

   case 2:   //按键K2[加键]与[复位健]
      if(0!=Gu8Part)//处于“闪烁模式”的时候,是[加键]
{
            switch(Gu8Wd) //在某个窗口下
            {
                case 1:   //在窗口1下
                     switch(Gu8Part)//二级支点的局部选择
                     {
                        case 1://局部1被选中,代表右起第4位数码管被选中。
if(Gu8EditData_4<9)
{
Gu8EditData_4++;//编辑“千位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 2://局部2被选中,代表右起第3位数码管被选中。
if(Gu8EditData_3<9)
{
Gu8EditData_3++;//编辑“百位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 3://局部3被选中,代表右起第2位数码管被选中。
if(Gu8EditData_2<9)
{
Gu8EditData_2++;//编辑“十位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 4://局部4被选中,代表右起第1位数码管被选中。
if(Gu8EditData_1<9)
{
Gu8EditData_1++;//编辑“个位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;
}
break;
}
            }
            else   //处于“没有闪烁”的时候,是[复位健]
{
Gu8EditData_4=Gu32SetData_Countdown/10000%10; //分解成“十秒”个体
Gu8EditData_3=Gu32SetData_Countdown/1000%10;//分解成“个秒”个体
Gu8EditData_2=Gu32SetData_Countdown/100%10; //分解成“百毫秒”个体
Gu8EditData_1=Gu32SetData_Countdown/10%10;//分解成“十毫秒”个体

Gu8RunStatus=0; //倒计时返回停止的状态

Gu8RunStart=0;//倒计时的运行步骤的停止
                Gu8RunStep=0;//总运行步骤归零。建议跟vGu8RunStart成双成对出现

                Gu8Wd=1; //返回设置数据的窗口
                Gu8WdUpdate=1;//整屏更新一次显示
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 3:   //按键K3[减键]与[开始健]
      if(0!=Gu8Part)//处于“闪烁模式”的时候,是[减键]
{
            switch(Gu8Wd) //在某个窗口下
            {
                case 1:   //在窗口1下
                     switch(Gu8Part)//二级支点的局部选择
                     {
                        case 1://局部1被选中,代表右起第4位数码管被选中。
if(Gu8EditData_4>0)
{
Gu8EditData_4--;//编辑“十秒”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 2://局部2被选中,代表右起第3位数码管被选中。
if(Gu8EditData_3>0)
{
Gu8EditData_3--;//编辑“个秒”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 3://局部3被选中,代表右起第2位数码管被选中。
if(Gu8EditData_2>0)
{
Gu8EditData_2--;//编辑“百毫秒”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;

                        case 4://局部4被选中,代表右起第1位数码管被选中。
if(Gu8EditData_1>0)
{
Gu8EditData_1--;//编辑“十毫位”个体的中间变量
}
PartUpdate(Gu8Part); //当前局部更新显示输出到数码管
                              break;
}
break;
}
            }
            else   //处于“没有闪烁”的时候,是[开始健]
{
            if(0==Gu8RunStatus) //在停止状态下
            {

vGu8CountdownTimerFlag=0;
vGu32CountdownTimerCnt=Gu32SetData_Countdown;   //从“设置值”开始倒计时
vGu8CountdownTimerFlag=1;      //允许倒计时的软件定时器的启动

                Gu8RunStatus=1;//倒计时处于工作状态(并且,这一瞬间才正式启动倒计时)

Gu8RunStart=1;   //倒计时的运行步骤的总开关开启
                  Gu8RunStep=0;    //总运行步骤归零。建议跟vGu8RunStart成双成对出现

                  Gu8Wd=2; //进入倒计时运行的窗口
                Gu8WdUpdate=1;//整屏更新一次显示,确保在启动的时候能显示到最新的数据
}
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 4:   //K1按键的“长按”,具有进入和退出“闪烁模式”的功能。“退出”隐含“确定”

      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //在窗口1下
                if(0==Gu8Part)//处于“没有闪烁”的时候,将进入“闪烁模式”
{
    Gu8EditData_4=Gu32SetData_Countdown/10000%10; //分解成“十秒”个体
    Gu8EditData_3=Gu32SetData_Countdown/1000%10;//分解成“个秒”个体
    Gu8EditData_2=Gu32SetData_Countdown/100%10; //分解成“百毫秒”个体
    Gu8EditData_1=Gu32SetData_Countdown/10%10;//分解成“十毫秒”个体
    Gu8Part=1;//进入“闪烁模式”,从“局部1”开始闪烁
}
                  else    //处于“闪烁模式”的时候,将退出到“没有闪烁”,隐含“确定”功能
{
//把个体合并还原成数据
Gu32SetData_Countdown=Gu8EditData_4*10000+Gu8EditData_3*1000;
Gu32SetData_Countdown=Gu32SetData_Countdown+Gu8EditData_2*100;
Gu32SetData_Countdown=Gu32SetData_Countdown+Gu8EditData_1*10;

    Gu8Part=0;//退出“闪烁模式”
            Gu8WdUpdate=1;//整屏更新
}

                break;

}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1显示函数。用来设置参数。
      break;
    case 2:
      Wd2();   //窗口2显示函数。倒计时的运行显示窗口。
      break;
}

}

void Wd1(void)   //窗口1显示函数。用来设置参数。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=1;   //保留显示2位小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
Gu8PartUpdate_3=1;//局部3更新显示
Gu8PartUpdate_4=1;//局部4更新显示

}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

      if(Gu32SetData_Countdown<10000)
      {
    Su8Temp_4=10;//显示“无”
}
else
{
    Su8Temp_4=Gu8EditData_4;//显示“十秒”的临时中间个体,属于动态数据。
}

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_3=Gu8EditData_3;//显示“个秒”的临时中间个体,属于动态数据。

vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_3) //局部3更新显示
{
Gu8PartUpdate_3=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_2;//显示“百毫秒”的临时中间个体,属于动态数据。

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_4) //局部4更新显示
{
Gu8PartUpdate_4=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_1=Gu8EditData_1;//显示“十毫秒”的临时中间个体,属于动态数据。

vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}

if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    switch(Gu8Part)//某个局部被选中,则闪烁跳动
    {
      case 1:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_4=10;//右起第4个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_4=Gu8EditData_4;//显示“十秒”的临时中间个体,属于动态数据。
}

             break;

      case 2:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_3=10;//右起第3个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_3=Gu8EditData_3;//显示“个秒”的临时中间个体,属于动态数据。
}

             break;

      case 3:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//右起第2个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_2=Gu8EditData_2;//显示“百毫秒”的临时中间个体,属于动态数据。
}

             break;

      case 4:
             if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_1=10;//右起第1个显示“不显示”(10代表不显示)
}
else
{
Su8BlinkFlag=0;
            Su8Temp_1=Gu8EditData_1;//显示“十毫秒”的临时中间个体,属于动态数据。
}

             break;

      default:   //都没有被选中的时候
            if(Gu32SetData_Countdown<10000)
            {
         Su8Temp_4=10;//显示“无”
}
else
{
            Su8Temp_4=Gu8EditData_4;//显示“十秒”的临时中间个体,属于动态数据。
}
Su8Temp_3=Gu8EditData_3;//显示“个秒”的临时中间个体,属于动态数据。   
       Su8Temp_2=Gu8EditData_2;//显示“百毫秒”的临时中间个体,属于动态数据。
Su8Temp_1=Gu8EditData_1;//显示“十毫秒”的临时中间个体,属于动态数据。      
             break;
}

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量

}
}

void Wd2(void)   //窗口2显示函数。倒计时的运行显示窗口。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

          //先分解数据,注意,这里分解的时候,“先整除后求余”必须用一行代码一气呵成,不能拆
          //分成两行代码,否则会有隐患会有bug。除非,把四个临时变都改成unsigned long类型。

          //Su8Temp_4提取“十秒”位。
Su8Temp_4=vGu32CountdownTimerCnt/10000%10; //实际精度是0.001秒,但显示精度是0.01秒

         //Su8Temp_3提取“个秒”位。
Su8Temp_3=vGu32CountdownTimerCnt/1000%10; //实际精度是0.001秒,但显示精度是0.01秒

         //Su8Temp_2提取“百毫秒”位。
Su8Temp_2=vGu32CountdownTimerCnt/100%10; //实际精度是0.001秒,但显示精度是0.01秒

          //Su8Temp_1提取“十毫秒”位。
Su8Temp_1=vGu32CountdownTimerCnt/10%10; //实际精度是0.001秒,但显示精度是0.01秒

          //判断数据范围,来决定最高位数码管是否需要显示。
          if(vGu32CountdownTimerCnt<10000) //10.000秒。实际4位数码管最大只能显示99.99秒
          {
             Su8Temp_4=10;//在数码管转换表里,10代表一个“不显示”的数据
}

//上面先分解数据之后,再过渡需要显示的数据到底层驱动变量里,让过渡的时间越短越好
vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;
vGu8Display_Righ_2=Su8Temp_2;
vGu8Display_Righ_1=Su8Temp_1;

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=1;   //保留显示2位小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

}
}

void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyShortFlag=0;//按键“短按”触发的标志   
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;

//需要详细分析以下这段“短按”与“长按”代码的朋友,请参考第96节。
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
      if(1==Su8KeyShortFlag)
      {
Su8KeyShortFlag=0;   
vGu8KeySec=1;    //触发K1的“短按”
}
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;

      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
            Su8KeyShortFlag=1;
      }

      if(Su16KeyCnt1>=KEY_LONG_TIME)
      {
            Su8KeyLock1=1;      
Su8KeyShortFlag=0;
            vGu8KeySec=4; //触发K1的“长按”
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}

void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

if(1==vGu8BlinkTimerFlag&&vGu16BlinkTimerCnt>0)   //数码管闪烁跳动的定时器
{
vGu16BlinkTimerCnt--;//递减式的软件定时器
}

//每10ms就定时更新一次显示的软件定时器
if(1==vGu8UpdateTimerFlag&&vGu16UpdateTimerCnt>0)
{
vGu16UpdateTimerCnt--;//递减式的软件定时器
}

//倒计时实际走的时间的软件定时器,注意,这里还附加了启动状态的条件“&&1==Gu8RunStatus”
if(1==vGu8CountdownTimerFlag&&vGu32CountdownTimerCnt>0&&1==Gu8RunStatus)
{
vGu32CountdownTimerCnt--;//递减式的软件定时器
}

TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
}

void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
EA=1;      
ET0=1;      
TR0=1;      

//上电初始化开机显示的窗口
Gu8EditData_4=Gu32SetData_Countdown/10000%10; //分解成“十秒”个体
Gu8EditData_3=Gu32SetData_Countdown/1000%10;//分解成“个秒”个体
Gu8EditData_2=Gu32SetData_Countdown/100%10; //分解成“百毫秒”个体
Gu8EditData_1=Gu32SetData_Countdown/10%10;//分解成“十毫秒”个体
    Gu8Wd=1; //返回设置数据的窗口
    Gu8WdUpdate=1;//整屏更新一次显示

}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

tt98 发表于 2018-6-21 20:56:29

再次说声谢谢鸿哥!{:titter:}

261854681 发表于 2018-6-24 22:14:20

多谢分享,这么多年坚持,难得喔。

Billion 发表于 2018-6-25 14:21:47

多谢分享,这么多年坚持,什么时候老师出书,买一根支持一下

吴坚鸿 发表于 2018-7-1 12:45:09

第一百二十二节: 利用定时中断做的“时分秒”数显时钟。

【122.1   利用定时中断做的“时分秒”数显时钟。】
      
                上图122.1.1数码管



      
                上图122.1.2独立按键

      
                上图122.1.3有源蜂鸣器

      本节的数显时钟小项目,意在人机界面程序框架的综合训练。程序功能如下:
   (1)只有“时分秒”,没有“年月日”。
   (2)平时时钟正常工作的时候,四位数码管的显示格式是这样的“HH.MM”,其中HH代表“时”,MM代表“分”,而中间的小数点“.”每隔一秒闪烁一次。
   (3)K1[设置键]与[切换窗口键]。当数码管“没有闪烁”时(处于正常工作模式),“长按”K1键则进入“闪烁模式”(修改时钟模式),“闪烁模式”一共有3个窗口,分别是“1-HH”,“2-MM”,“3-SS”。其中“HH”“MM”“SS”分别代表可修改的“时”“分”“秒”,它们处于“闪烁”的状态,代表可编辑。此时,“短按”K1按键代表[切换窗口键],可以使数码管在“1-HH”,“2-MM”,“3-SS”三个窗口之间依次切换。修改完毕后,只需“长按”K1键代表确定完成并且退出当前“闪烁模式”返回到时钟的“正常工作模式”。
   (4)K2[加键]。当数码管某位正在闪烁时,此时K2是[加键],按K2会使数据“自加1”。
   (5)K3[减键]。当数码管某位正在闪烁时,此时K3是[减键],按K3会使数据“自减1”。
   (6)处于“闪烁模式”时的3个窗口的数据范围。处于修改“时”的“1-HH”窗口时,HH的范围是:0到23;处于修改“分”的“2-MM”窗口时,MM的范围是:0到59;处于修改“秒”的“3-SS”窗口时,SS的范围是:0到59。
      代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25   
#define KEY_LONG_TIME    500   

#define SCAN_TIME1   
#define VOICE_TIME   50   
#define BLINK_TIME   250   

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);
void Wd1(void);   //窗口1。时钟正常工作的窗口“HH.MM”。小数点在闪烁跳动。
void Wd2(void);   //窗口2。闪烁模式,修改“时”的“1-HH”的窗口。
void Wd3(void);   //窗口3。闪烁模式,修改“分”的“2-MM”的窗口。
void Wd4(void);   //窗口4。闪烁模式,修改“秒”的“3-SS”的窗口。

void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8BlinkTimerFlag=0;   
volatile unsigned int vGu16BlinkTimerCnt=0;

//时钟的软件定时器,注意,这里是unsigned long类型,范围是0到4294967295毫秒
volatile unsigned char vGu8ClockTimerFlag=0;
volatile unsigned long vGu32ClockTimerCnt=0;

//时钟正常工作的时候,每500ms更新显示一次
volatile unsigned char vGu8UpdateTimerFlag=0;
volatile unsigned int vGu16UpdateTimerCnt=0;

unsigned char Gu8EditData_1=0;//是中间变量,用于编辑窗口“1-HH”下的HH数据。
unsigned char Gu8EditData_2=0;//是中间变量,用于编辑窗口“2-MM”下的MM数据。
unsigned char Gu8EditData_3=0;//是中间变量,用于编辑窗口“3-SS”下的SS数据。

unsigned char Gu8Wd=0;   //窗口选择变量。人机交互程序框架的支点。
unsigned char Gu8WdUpdate=0;//整屏更新变量。

unsigned char Gu8PartUpdate_1=0;   //局部1的更新变量,
unsigned char Gu8PartUpdate_2=0;   //局部2的更新变量
unsigned char Gu8PartUpdate_3=0;   //局部3的更新变量,

volatile unsigned char vGu8Display_Righ_4=0;
volatile unsigned char vGu8Display_Righ_3=0;
volatile unsigned char vGu8Display_Righ_2=0;
volatile unsigned char vGu8Display_Righ_1=0;

volatile unsigned char vGu8Display_Righ_Dot_4=0;
volatile unsigned char vGu8Display_Righ_Dot_3=1;    //开机默认保留显示2个小数点
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
    }
}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //按键K1的“短按”。在“闪烁模式”下切换数码管的窗口。
      switch(Gu8Wd) //在某个窗口下
      {
            case 2:   //窗口2。修改“时”的“1-HH”窗口。
Gu8Wd=3; //切换到窗口3的“2-MM”窗口
Gu8WdUpdate=1; //整屏更新
                      break;

            case 3:   //窗口3。修改“分”的“2-MM”窗口。
Gu8Wd=4; //切换到窗口4的“3-SS”窗口
Gu8WdUpdate=1; //整屏更新
                      break;

            case 4:   //窗口4。修改“秒”的“3-SS”窗口。
Gu8Wd=2; //切换到窗口2的“1-HH”窗口
Gu8WdUpdate=1; //整屏更新
                      break;
            }

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 2:   //按键K2[加键]

      switch(Gu8Wd) //在某个窗口下
      {
            case 2:   //窗口2。修改“时”的“1-HH”窗口。
                  if(Gu8EditData_1<23) //“时”的范围是0到23
{
Gu8EditData_1++;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;

            case 3:   //窗口3。修改“分”的“2-MM”窗口。
                  if(Gu8EditData_2<59) //“分”的范围是0到59
{
Gu8EditData_2++;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;

            case 4:   //窗口4。修改“秒”的“3-SS”窗口。
                  if(Gu8EditData_3<59) //“秒”的范围是0到59
{
Gu8EditData_3++;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;
            }

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 3:   //按键K3[减键]与[开始健]
      switch(Gu8Wd) //在某个窗口下
      {
            case 2:   //窗口2。修改“时”的“1-HH”窗口。
                  if(Gu8EditData_1>0)
{
Gu8EditData_1--;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;

            case 3:   //窗口3。修改“分”的“2-MM”窗口。
                  if(Gu8EditData_2>0)
{
Gu8EditData_2--;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;

            case 4:   //窗口4。修改“秒”的“3-SS”窗口。
                  if(Gu8EditData_3>0)
{
Gu8EditData_3--;
}
Gu8PartUpdate_1=1;   //局部1更新显示
                      break;
            }

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

   case 4:   //K1按键的“长按”,具有进入和退出“闪烁模式”的功能。“退出”隐含“确定”

      switch(Gu8Wd) //在某个窗口下
      {
         case 1:   //窗口1。时钟正常工作的窗口。   
                vGu8ClockTimerFlag=0;//停止时钟的定时器

Gu8EditData_1=vGu32ClockTimerCnt/3600000;//分解成“时”个体
Gu8EditData_2=vGu32ClockTimerCnt%3600000/60000;//分解成“分”个体
Gu8EditData_3=vGu32ClockTimerCnt%3600000%60000/1000;//分解成“秒”个体

Gu8Wd=2; //切换到窗口2的“1-HH”的闪烁窗口
Gu8WdUpdate=1; //整屏更新
break;

         case 2:   //窗口2。修改时钟时间的“1-HH”的闪烁窗口
         case 3:   //窗口3。修改时钟时间的“2-MM”的闪烁窗口
         case 4:   //窗口4。修改时钟时间的“3-SS”的闪烁窗口
//把个体合并还原成当前时钟时间的数据
vGu32ClockTimerCnt=Gu8EditData_1*3600000+Gu8EditData_2*60000+Gu8EditData_3*1000;
vGu8ClockTimerFlag=1;//启动时钟的定时器

Gu8Wd=1; //切换到窗口1的正常工作的窗口
Gu8WdUpdate=1; //整屏更新
break;
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;

vGu8KeySec=0;
break;

}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1。时钟正常运行的窗口
      break;
    case 2:
      Wd2();   //窗口2。修改“时”的“1-HH”窗口
      break;
    case 3:
      Wd3();   //窗口3。修改“分”的“2-MM”窗口
      break;
    case 4:
      Wd4();   //窗口4。修改“秒”的“3-SS”窗口
      break;
}
}

void Wd1(void)   //窗口1。时钟正常运行的窗口
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=1;   //保留显示2位小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示,更新显示一次数据和闪烁的的小数点
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=vGu32ClockTimerCnt/3600000/10; //时的十位
Su8Temp_3=vGu32ClockTimerCnt/3600000%10; //时的个位
Su8Temp_2=vGu32ClockTimerCnt%3600000/60000/10; //分的十位
Su8Temp_1=vGu32ClockTimerCnt%3600000/60000%10; //秒的个位

          //小数点的闪烁
          if(0==Su8BlinkFlag)
{
Su8BlinkFlag=1;
vGu8Display_Righ_Dot_3=1;   //显示第2位小数点。

}
          else
{
Su8BlinkFlag=0;
vGu8Display_Righ_Dot_3=0;   //不显示第2位小数点
}

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}

if(0==vGu16UpdateTimerCnt)//每隔500ms就更新显示一次数据和闪烁的的小数点
{
vGu8UpdateTimerFlag=0;
    vGu16UpdateTimerCnt=500;//重设定时器的定时时间
vGu8UpdateTimerFlag=1;

Gu8PartUpdate_1=1;//局部1更新显示
}

}

void Wd2(void)   //窗口2。修改“时”的“1-HH”窗口。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量


if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_4=1;   //显示数字“1”
vGu8Display_Righ_3=11;//显示横杠“-”

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;   
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_1/10;//显示“时”的十位
Su8Temp_1=Gu8EditData_1%10;//显示“时”的个位

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//10代表不显示
Su8Temp_1=10;//10代表不显示
}
else
{
Su8BlinkFlag=0;
Su8Temp_2=Gu8EditData_1/10;//显示“时”的十位
Su8Temp_1=Gu8EditData_1%10;//显示“时”的个位

}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}
}

void Wd3(void)   //窗口3。修改“分”的“2-MM”窗口。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_4=2;   //显示数字“2”
vGu8Display_Righ_3=11;//显示横杠“-”

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;   
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_2/10;//显示“分”的十位
Su8Temp_1=Gu8EditData_2%10;//显示“分”的个位

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//10代表不显示
Su8Temp_1=10;//10代表不显示
}
else
{
Su8BlinkFlag=0;
Su8Temp_2=Gu8EditData_2/10;//显示“分”的十位
Su8Temp_1=Gu8EditData_2%10;//显示“分”的个位
}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}
}

void Wd4(void)   //窗口4。修改“秒”的“3-SS”窗口。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_2,Su8Temp_1; //需要借用的中间变量
static unsigned char Su8BlinkFlag=0;//两种状态的切换判断的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_4=3;   //显示数字“3”
vGu8Display_Righ_3=11;//显示横杠“-”

vGu8Display_Righ_Dot_4=0;
vGu8Display_Righ_Dot_3=0;   
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8EditData_3/10;//显示“秒”的十位
Su8Temp_1=Gu8EditData_3%10;//显示“秒”的个位

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}


if(0==vGu16BlinkTimerCnt)//某位被选中的数码管跳动闪烁的定时器
{
vGu8BlinkTimerFlag=0;
    vGu16BlinkTimerCnt=BLINK_TIME;//重设定时器的定时时间
vGu8BlinkTimerFlag=1;

    if(0==Su8BlinkFlag)//两种状态的切换判断
{
Su8BlinkFlag=1;
Su8Temp_2=10;//10代表不显示
Su8Temp_1=10;//10代表不显示
}
else
{
Su8BlinkFlag=0;
Su8Temp_2=Gu8EditData_3/10;//显示“秒”的十位
Su8Temp_1=Gu8EditData_3%10;//显示“秒”的个位
}

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}
}

void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyShortFlag=0;//按键“短按”触发的标志   
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;

//需要详细分析以下这段“短按”与“长按”代码的朋友,请参考第96节。
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
      if(1==Su8KeyShortFlag)
      {
Su8KeyShortFlag=0;   
vGu8KeySec=1;    //触发K1的“短按”
}
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;

      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
            Su8KeyShortFlag=1;
      }

      if(Su16KeyCnt1>=KEY_LONG_TIME)
      {
            Su8KeyLock1=1;      
Su8KeyShortFlag=0;
            vGu8KeySec=4; //触发K1的“长按”
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}

void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;//递减式的软件定时器
}

if(1==vGu8BlinkTimerFlag&&vGu16BlinkTimerCnt>0)   //数码管闪烁跳动的定时器
{
vGu16BlinkTimerCnt--;//递减式的软件定时器
}

//在正常工作的窗口下,每500ms就定时更新一次显示的软件定时器
if(1==vGu8UpdateTimerFlag&&vGu16UpdateTimerCnt>0)
{
vGu16UpdateTimerCnt--;//递减式的软件定时器
}

//时钟实际走的时间的软件定时器,注意,这里是递增式的软件定时器
if(1==vGu8ClockTimerFlag)
{
vGu32ClockTimerCnt++;//递增式的软件定时器
if(vGu32ClockTimerCnt>=86400000) //86400000毫秒代表24时
{
vGu32ClockTimerCnt=0;
}
}

TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
}

void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
EA=1;      
ET0=1;      
TR0=1;      

//上电初始化一些关键的数据

Gu8Wd=1;   //窗口1。开机默认处于正常工作的窗口
Gu8WdUpdate=1;//整屏更新变量

vGu8ClockTimerFlag=0;
vGu32ClockTimerCnt=43200000;//43200000毫秒开机默认12:00点。12时就是43200000毫秒
vGu8ClockTimerFlag=1;   //启动时钟的定时器

//时钟正常工作的时候,每500ms更新显示一次
    vGu16UpdateTimerCnt=500;
vGu8UpdateTimerFlag=1; //启动小数点闪烁的定时器

}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

吴坚鸿 发表于 2018-7-9 10:15:29

第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序。

【123.1   一种能省去一个lock自锁变量的按键驱动程序。】

       一位群友给我提到了一个按键的改进建议,能巧妙的省去一个lock自锁变量。这个建议引起了我对“变量的分工要专一,一个变量尽量只用在一类事物上,尽量不取巧兼容”的思考。

       第一种:带lock自锁变量,也是我一直在用的代码。
if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
    Su8KeyLock1=0; //按键解锁
    Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
}
else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。这行如果有疑问,请看92节的专题分析。
{
    Su16KeyCnt1++; //累加定时中断次数
    if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
    {
         Su8KeyLock1=1;//按键的自锁,避免一直触发
         vGu8KeySec=1;    //触发1号键
    }
}


       第二种:省略掉一个lock自锁变量,群友提出的改进建议。
if(0!=KEY_INPUT1)
{
    Su16KeyCnt1=0;
}
else if(Su16KeyCnt1<KEY_FILTER_TIME) //巧妙的利用了Su16KeyCnt1等于滤波时间时,只执行一次
{
    Su16KeyCnt1++;
    if(KEY_FILTER_TIME==Su16KeyCnt1) //巧妙的利用了Su16KeyCnt1等于滤波时间时,只执行一次
    {
         vGu8KeySec=1;   
    }
}

分析:
       不得不佩服群友的智慧,第二种改进后看起来非常巧妙,犹如蜻蜓点水般轻盈洒脱。但是,为此代码狂欢片刻后,我又有了新的思考和看法。“计时器Su16KeyCnt1”和“自锁变量Su8KeyLock1”是两个不同的事物,是两个不同的范畴,就应该用两个不同的变量进行区分。如果逞一时之巧,把两种不同范畴的事物巧妙合并成一个变量,势必会导致程序的“易读性”和“后续维护的可扩展性”大打折扣。“自锁变量Su8KeyLock1”真的是可有可无吗?假设,如果“计时器Su16KeyCnt1”的消抖时间KEY_FILTER_TIME要求等于0,那么第二种改进后的代码立刻暴露出了问题,行不通。而第一种代码,因为有“自锁变量Su8KeyLock1”的存在,即使消抖时间KEY_FILTER_TIME等于0,也不影响代码功能的完整性,因为第一种代码的理念是“自锁与计时器是两种不同的功能范畴,用两个不同的变量进行分开隔离,各自管理两种不同的事物,计时器即使为0也不影响代码本该有的自锁功能”。通过此例子,给初学者一个建议,在代码的“队形感,易读性,扩展性,分类清晰”和“巧妙,节省代码”两者之间,建议大家优先考虑“队形感,易读性,扩展性,分类清晰”,追求一种原则上的“工整,不出奇兵,扎硬寨,打呆仗,步步为营”,这样阵脚不易乱,能走得更远,驾驭更多千军万马的代码。

kkfy888 发表于 2018-7-12 21:48:36

吴坚鸿 发表于 2018-7-9 10:15
第一百二十三节: 一种能省去一个lock自锁变量的按键驱动程序。

【123.1   一种能省去一个lock自锁变量的 ...

不错,又学了一个新技巧{:victory:}

changshs 发表于 2018-7-16 10:23:51

有楼主这样搞普及教育的人,整个国家的搞单片机的人才会多起来,整体的人员素质才会有本质上的提升!为楼主点赞!

吴坚鸿 发表于 2018-7-16 10:40:00

第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯。

【124.1   数显仪表盘显示“速度、方向、计数器”的跑马灯。】
      
                上图124.1.1数码管



      
                上图124.1.2独立按键

      
                上图124.1.3有源蜂鸣器


      
                上图124.1.4LED电路

       本节小项目,意在“人机界面”与“过程控制”如何关联的练习。
       程序功能如下:
      (1)数码管显示的格式是“S.D.CC”。其中S是代表3档速度,能显示的数字范围是“1、2、3”,分别代表“慢、中、快”3档速度。D代表方向,往右跑显示符号“r”(right的首字母),往左跑显示符号“L”(Left的首字母)。CC代表计数器,跑马灯每跑完一次,计数器自动加1,范围是0到99。
      (2)【速度】按键K1。每按一次【速度】按键K1,速度档位显示的数字在“1、2、3”之间切换。
      (3)【方向】按键K2。跑马灯上电后默认处于“往右跑”的方向,默认显示字符“r”。每按一次【方向】按键K2,跑马灯就在“往右跑”与“往左跑”两个方向之间切换,显示的字符在“r、L”之间切换。
      (4)【启动暂停】按键K3。上电后,按下【启动暂停】按键K3启动之后,跑马灯处于“启动”状态,4个LED灯挨个依次循环的变亮,给人“跑”起来的感觉,此时再按一次【启动暂停】按键K3,则跑马灯处于“暂停”状态,接着又按一次【启动暂停】按键K3,跑马灯又变回“启动”状态。因此,【启动暂停】按键K3是专门用来切换“启动”和“暂停”这两种状态。
       代码如下:

#include "REG52.H"

#define KEY_FILTER_TIME25   

#define SCAN_TIME1   
#define VOICE_TIME   50   

#define RUN_TIME_SLOW    500   //“慢”档速度的时间参数
#define RUN_TIME_MIDDLE300   //“中”档速度的时间参数
#define RUN_TIME_FAST    100   //“快”档速度的时间参数

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void KeyScan(void);
void KeyTask(void);
void RunTask(void);   //跑马灯的任务函数

void VoiceScan(void);
void DisplayScan(void);
void DisplayTask(void);
void Wd1(void);   //窗口1。
void BeepOpen(void);   
void BeepClose(void);

sbit KEY_INPUT1=P2^2;
sbit KEY_INPUT2=P2^1;
sbit KEY_INPUT3=P2^0;

sbit P1_0=P1^0;
sbit P1_1=P1^1;
sbit P1_2=P1^2;
sbit P1_3=P1^3;

sbit P3_4=P3^4;

//4个跑马灯的输出口
sbit P1_4=P1^4;
sbit P1_5=P1^5;
sbit P1_6=P1^6;
sbit P3_3=P3^3;


//数码管转换表
code unsigned char Cu8DigTable[]=
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
0x40,//横杠-   序号11
0x38,//字符L   序号12
0x70,//字符r   序号13
};

volatile unsigned char vGu8ScanTimerFlag=0;
volatile unsigned int vGu16ScanTimerCnt=0;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8Wd=0;   //窗口选择变量。人机交互程序框架的支点。
unsigned char Gu8WdUpdate=0;//整屏更新变量。

unsigned char Gu8PartUpdate_1=0;   //局部1的更新变量,
unsigned char Gu8PartUpdate_2=0;   //局部2的更新变量
unsigned char Gu8PartUpdate_3=0;   //局部3的更新变量,

volatile unsigned char vGu8Display_Righ_4=0;
volatile unsigned char vGu8Display_Righ_3=0;
volatile unsigned char vGu8Display_Righ_2=0;
volatile unsigned char vGu8Display_Righ_1=0;

volatile unsigned char vGu8Display_Righ_Dot_4=1;//需要显示的小数点
volatile unsigned char vGu8Display_Righ_Dot_3=1;//需要显示的小数点
volatile unsigned char vGu8Display_Righ_Dot_2=0;
volatile unsigned char vGu8Display_Righ_Dot_1=0;

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8RunCounter=0;    //计数器,范围是0到99

unsigned char Gu8RunStep=0; //运行的步骤
unsigned char Gu8RunStart=0;      //控制跑马灯启动的总开关

unsigned char Gu8RunStatus=0;   //标识跑马灯当前的状态。0代表停止,1代表启动,2代表暂停。
unsigned char Gu8RunDirection=0;//标识跑马灯当前的方向。0代表往右跑,1代表往左跑。
unsigned char Gu8RunSpeed=1;      //当前的速度档位。1代表“慢”,2代表“中”,3代表“快”。
unsigned intGu16RunSpeedTimeDate=0; //承接各速度档位的时间参数的变量

volatile unsigned char vGu8RunTimerFlag=0;   //用于控制跑马灯跑动速度的定时器
volatile unsigned int vGu16RunTimerCnt=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
KeyTask();      //按键的任务函数
DisplayTask();//数码管显示的上层任务函数
RunTask();    //跑马灯的任务函数
    }
}

void RunTask(void)    //跑马灯的任务函数,放在主函数内
{
if(0==Gu8RunStart) //如果是停止的状态
{
return;//如果是停止的状态,退出当前函数,不扫描余下代码。
}

switch(Gu8RunStep) //屡见屡爱的switch又来了
{
   case 0:
vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=0;//定时器清零
      Gu8RunStep=1;//切换到下一步,启动

       break;
   case 1:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Gu8RunStep=2;
}
else//往左跑
{
    if(Gu8RunCounter<99)
{
Gu8RunCounter++; //往左边跑完一次,运行的计数器自加1
}
    Gu8PartUpdate_3=1;   //局部3的更新变量,更新显示计数器

                Gu8RunStep=4;
}

}

       break;
   case 2:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=0;   //第2个灯亮
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Gu8RunStep=3;
}
else//往左跑
{
               Gu8RunStep=1;
}
}

       break;
   case 3:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=0;   //第3个灯亮
P3_3=1;   //第4个灯灭

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
               Gu8RunStep=4;
}
else//往左跑
{
               Gu8RunStep=2;
}
}

       break;
   case 4:
       if(1==Gu8RunStatus&&0==vGu16RunTimerCnt) //当前处于“启动”状态,并且定时器等于0
{
P1_4=1;   //第1个灯灭
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=0;   //第4个灯亮

    vGu8RunTimerFlag=0;   
vGu16RunTimerCnt=Gu16RunSpeedTimeDate; //速度时间参数变量的大小,决定了速度
vGu8RunTimerFlag=1;   //启动定时器

//灵活切换“步骤变量”
if(0==Gu8RunDirection) //往右跑
{
    if(Gu8RunCounter<99)
{
Gu8RunCounter++; //往右边跑完一次,运行的计数器自加1
}
    Gu8PartUpdate_3=1;   //局部3的更新变量,更新显示计数器

               Gu8RunStep=1;
}
else//往左跑
{
               Gu8RunStep=3;
}
}

       break;

}

}

void KeyTask(void)    //按键的任务函数
{
if(0==vGu8KeySec)
{
return;
}

switch(vGu8KeySec)
{
   case 1:   //【速度】按键K1
      switch(Gu8Wd) //在某个窗口下
      {
            case 1:   //窗口1。
                  //每按一次K1按键,Gu8RunSpeed就在1、2、3三者之间切换,
//并且根据Gu8RunSpeed的数值,对Gu16RunSpeedTimeDate赋值
//不同的速度时间参数,从而控制速度档位。

                  if(1==Gu8RunSpeed)
{
Gu8RunSpeed=2;//“中”档
Gu16RunSpeedTimeDate=RUN_TIME_MIDDLE; //赋值“中”档的时间参数
}
                  else if(2==Gu8RunSpeed)
{
Gu8RunSpeed=3;   //“快”档
Gu16RunSpeedTimeDate=RUN_TIME_FAST; //赋值“快”档的时间参数
}
else
{
Gu8RunSpeed=1;   //“慢”档
Gu16RunSpeedTimeDate=RUN_TIME_SLOW; //赋值“慢”档的时间参数
}

Gu8PartUpdate_1=1;   //局部1的更新变量,更新显示“速度”

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;
                      break;
            }

vGu8KeySec=0;
break;

   case 2:   //【方向】按键K2

      switch(Gu8Wd) //在某个窗口下
      {
            case 1:   //窗口1。
                  //每按一次K2按键,Gu8RunDirection就在0和1之间切换,从而控制方向
                  if(0==Gu8RunDirection)
{
Gu8RunDirection=1;
}
else
{
Gu8RunDirection=0;
}

Gu8PartUpdate_2=1;   //局部2更新显示,更新显示“方向”

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;
                      break;
            }
vGu8KeySec=0;
break;

   case 3:   //【启动暂停】按键K3
      switch(Gu8Wd) //在某个窗口下
      {
            case 1:   //窗口1。
                  if(0==Gu8RunStatus) //当跑马灯处于“停止”状态时
                  {
                      Gu8RunStep=0;    //运行步骤从0开始
Gu8RunStart=1;   //总开关“打开”。
Gu8RunStatus=1;//状态切换到“启动”状态
}
                  else if(1==Gu8RunStatus) //当跑马灯处于“启动”状态时
                  {
Gu8RunStatus=2;//状态切换到“暂停”状态
}
                  else//当跑马灯处于“暂停”状态时
                  {
Gu8RunStatus=1;//状态切换到“启动”状态
}

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=VOICE_TIME;//蜂鸣器发出“滴”一声
vGu8BeepTimerFlag=1;
                      break;
            }

vGu8KeySec=0;
break;
}
}

void DisplayTask(void) //数码管显示的上层任务函数
{
switch(Gu8Wd)//以窗口选择Gu8Wd为支点,去执行对应的窗口显示函数。又一次用到switch语句
{
    case 1:
      Wd1();   //窗口1。
      break;
}
}

void Wd1(void)   //窗口1。
{
//需要借用的中间变量,用来拆分数据位。
static unsigned char Su8Temp_4,Su8Temp_3,Su8Temp_2,Su8Temp_1; //需要借用的中间变量

if(1==Gu8WdUpdate) //如果需要整屏更新
{
Gu8WdUpdate=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

//属于静态数据,起“装饰”作用,切换窗口后只扫描一次的代码。
vGu8Display_Righ_Dot_4=1;//显示小数点
vGu8Display_Righ_Dot_3=1;//显示小数点
vGu8Display_Righ_Dot_2=0;
vGu8Display_Righ_Dot_1=0;

Gu8PartUpdate_1=1;//局部1更新显示
Gu8PartUpdate_2=1;//局部2更新显示
Gu8PartUpdate_3=1;//局部3更新显示
}

if(1==Gu8PartUpdate_1) //局部1更新显示,速度
{
Gu8PartUpdate_1=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_4=Gu8RunSpeed;

vGu8Display_Righ_4=Su8Temp_4;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_2) //局部2更新显示,方向
{
Gu8PartUpdate_2=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

if(0==Gu8RunDirection) //往右跑
{
Su8Temp_3=13; //数码管的字模转换表序号13代表显示字符“r”
}
else
{
Su8Temp_3=12; //数码管的字模转换表序号12代表显示字符“L”
}
vGu8Display_Righ_3=Su8Temp_3;//过渡需要显示的数据到底层驱动变量
}

if(1==Gu8PartUpdate_3) //局部3更新显示,计数器
{
Gu8PartUpdate_3=0; //及时清零,只更新一次显示即可,避免一直进来更新显示

Su8Temp_2=Gu8RunCounter%100/10; //提取十位
Su8Temp_1=Gu8RunCounter%10/1;   //提取个位

vGu8Display_Righ_2=Su8Temp_2;//过渡需要显示的数据到底层驱动变量
vGu8Display_Righ_1=Su8Temp_1;//过渡需要显示的数据到底层驱动变量
}

}


void KeyScan(void)//按键底层的驱动扫描函数,放在定时中断函数里
{
   static unsigned char Su8KeyLock1;
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyLock2;
   static unsigned intSu16KeyCnt2;
   static unsigned char Su8KeyLock3;
   static unsigned intSu16KeyCnt3;


   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;   
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
            Su8KeyLock1=1;      
            vGu8KeySec=1;
      }
   }

   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;   
      }
   }

   if(0!=KEY_INPUT3)
   {
      Su8KeyLock3=0;
      Su16KeyCnt3=0;      
   }
   else if(0==Su8KeyLock3)
   {
      Su16KeyCnt3++;
      if(Su16KeyCnt3>=KEY_FILTER_TIME)
      {
         Su8KeyLock3=1;
         vGu8KeySec=3;   
      }
   }

}

void DisplayScan(void)    //数码管底层的驱动扫描函数,放在定时中断函数里
{
static unsigned char Su8GetCode;
static unsigned char Su8ScanStep=1;

if(0==vGu16ScanTimerCnt)
{


    P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

    switch(Su8ScanStep)
{
       case 1:
Su8GetCode=Cu8DigTable;

if(1==vGu8Display_Righ_Dot_1)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=0;
P1_1=1;
P1_2=1;
P1_3=1;            
break;

       case 2:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_2)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=0;
P1_2=1;
P1_3=1;         
break;

       case 3:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_3)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=0;
P1_3=1;         
break;

       case 4:
Su8GetCode=Cu8DigTable;
if(1==vGu8Display_Righ_Dot_4)
{
Su8GetCode=Su8GetCode|0x80;
}
    P0=Su8GetCode;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=0;         
break;

}

Su8ScanStep++;
if(Su8ScanStep>4)
{
Su8ScanStep=1;
}

vGu8ScanTimerFlag=0;
vGu16ScanTimerCnt=SCAN_TIME;
vGu8ScanTimerFlag=1;
}
}

void VoiceScan(void) //蜂鸣器的驱动函数
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void T0_time() interrupt 1   
{
VoiceScan();    //蜂鸣器的驱动函数
KeyScan();      //按键底层的驱动扫描函数
DisplayScan();//数码管底层的驱动扫描函数

if(1==vGu8ScanTimerFlag&&vGu16ScanTimerCnt>0)
{
vGu16ScanTimerCnt--;
}

if(1==vGu8RunTimerFlag&&vGu16RunTimerCnt>0)//用于控制跑马灯跑动速度的定时器
{
vGu16RunTimerCnt--;
}

TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
}

void SystemInitial(void)
{
P0=0x00;
P1_0=1;
P1_1=1;
P1_2=1;
P1_3=1;

TMOD=0x01;
TH0=0xfd;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
TL0=0x40;   //此参数可根据具体的时间来修改,尽量确保每定时中断一次接近1ms
EA=1;      
ET0=1;      
TR0=1;      

//上电初始化一些关键的数据

Gu8Wd=1;   //窗口1。开机默认处于正常工作的窗口
Gu8WdUpdate=1;//整屏更新变量
//跑马灯处于初始化的状态
P1_4=0;   //第1个灯亮
P1_5=1;   //第2个灯灭
P1_6=1;   //第3个灯灭
P3_3=1;   //第4个灯灭

//根据当前的速度档位Gu8RunSpeed,来初始化速度时间参数Gu16RunSpeedTimeDate
if(1==Gu8RunSpeed)
{
Gu16RunSpeedTimeDate=RUN_TIME_SLOW; //赋值“慢”档的时间参数
}
else if(2==Gu8RunSpeed)
{
Gu16RunSpeedTimeDate=RUN_TIME_MIDDLE; //赋值“中”档的时间参数
}
else
{
Gu16RunSpeedTimeDate=RUN_TIME_FAST; //赋值“快”档的时间参数
}

}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

吴坚鸿 发表于 2018-7-24 11:20:40

第一百二十五节: 串口开篇之“通信的底层原理”。

【125.1   通信的底层原理。】

       芯片之间通信,都离不开“数据线”和“时钟线”,缺一不可。“数据线”和“时钟线”是什么关系,它们是怎样相互配合来实现通信的功能?其实原理也很简单。打个比喻,甲乙两个人,规定只能靠一只“手”和一只“脚”进行肢体接触的通信,他们之间如何传输数据?“手”可以产生“两种”状态“握紧”和“松开”,“脚”可以产生“一种”状态“踢一脚”。他们之间约定,甲发送数据给乙,乙每被甲“踢一脚”就去记录一次手的状态是“握紧”还是“松开”,“握紧”代表二进制的1,“松开”代表二进制的0,这样,如果他们之间想传输一个字节的十六进制数据0x59,只需把十六进制的数据0x59展开成二进制01011001,从左到右(从高位到低位)以“位”为单位挨个发送,过程如下:
       第一次“踢一脚”:手的状态是“松开”,记录0。
       第二次“踢一脚”:手的状态是“握紧”,记录1。
       第三次“踢一脚”:手的状态是“松开”,记录0。
       第四次“踢一脚”:手的状态是“握紧”,记录1。
       第五次“踢一脚”:手的状态是“握紧”,记录1。
       第六次“踢一脚”:手的状态是“松开”,记录0。
       第七次“踢一脚”:手的状态是“松开”,记录0。
       第八次“踢一脚”:手的状态是“握紧”,记录1。
      上述肢体接触的通信过程,其实一只“手”就代表了一根“数据线”,可以产生高电平“1”和低电平“0”这两种状态,而一只“脚”代表了一根“时钟线”,但是“踢一脚”代表了“时钟线”上的一种什么状态呢?注意,“踢一脚”既不是高电平“1”也不是低电平“0”,而是瞬间只产生一次的“上升沿”或者“下降沿”。何谓“上升沿”何谓“下降沿”?“上升沿”是代表“时钟线从低电平跳变到高电平的瞬间”,“下升沿”是代表“时钟线从高电平跳变到低电平的瞬间”。“踢一脚”、“上升沿”、“下降沿”此三者都可以统一理解成“节拍”。
       芯片之间通信,“时钟线”只需1根足矣,而“数据线”却可以不止1根。1根“数据线”往往叫“串行”通信,一个节拍只能传输一位数据。8根以上的“数据线”往往叫“并行”通信,一个节拍能传输8位以上的数据。可见,“数据线”越多,传输的速率越快。
       常见的系统中,串口,IIC,SPI,USB,CAN这类都是“串行”通信。而32位单片机与外部的nandflash,norflash,sdram,sram这些芯片通信往往是“并行”通信,数据线多达8根16根甚至32根。

dadian 发表于 2018-7-24 11:23:28

支持一下,可以出一本书了

tt98 发表于 2018-7-24 20:23:23

谢谢鸿哥更新!{:lol:}

kkfy888 发表于 2018-7-30 01:36:47

通俗易懂,支持鸿哥!

吴坚鸿 发表于 2018-8-5 12:43:53

第一百二十六节: “单线”的肢体接触通信。

【126.1   同步通信与异步通信。】

       既然芯片之间通信离不开“数据”和“时钟”这两种信号,那么是不是说,通信必须至少两根线(双线)以上?不是。单线也可以通信,继续拿甲乙两人的肢体通信做比喻,这一次只允许用一只“手”不许用“脚”,“手”继续做数据信号,那么时钟信号在哪?时钟信号在甲乙两人各自的“心跳”。用两个人的“心跳”作为时钟信号就有两个时钟节拍,初学者可能在这里会有疑惑,这两人的“心跳”频率可能不一致,时钟节拍可能不同步,怎么能进行通信呢?说到这里,恰好通讯界有两个专业的概念,一个是“同步通信”另一个是“异步通信”。像上一节讲那种用脚的动作“踢一脚”作为时钟信号,这个时钟信号只有一个,对于通讯的甲乙双方是实时“同步的”时钟信号,因此这种通信叫做“同步通信”。而本节提到的用两个人各自的“心跳”做时钟信号,有两个时钟源,时钟信号是“不同步的”,这种通信叫做“异步通信”。

【126.2   异步通信的原理。】

       既然两人各自的“心跳”不同步(异步),而且“心跳”是从甲乙两人出生开始就一直持续存在不停跳动的,那么发送一个字节的数据是从什么时候开始到什么时候结束就必须事先有一个约定。他们是这样约定的:
      (一)平时的待命状态。甲是发送方,乙是接收方,平时待命没有发送数据的时候,甲手的状态一直是“松开”的(电平1)。
      (二)1个开始位与8个数据位。当甲要发送数据给乙的时候,第1个心跳甲先“握紧”(电平0)代表“开始位”,“开始位”用来通知乙方请做好接收数据的准备,然后第2个到第9个心跳甲依次靠手的状态发送8个位的字节数据(数据位),乙方因为“甲的开始位”的存在已经做好了接收第2个心跳数据的准备,因此乙方能完全接收第2个心跳至第9个心跳的数据位的数据。
      (三)1个停止位。甲发送了第9个心跳的数据后,必须马上恢复到待命的状态“松开”(电平1),以便为下一次发送数据时能正确发送“开始位”,但是这个待命的状态“松开”至少应该持续多长的时间呢?至少持续1个“心跳”的时间以上。这样,虽然两个人的“心跳”不同步并且频率也不一样,但是只要8个“心跳”的累加误差不超过1个“心跳”的停止位时间,数据就肯定不会错位。这个至少持续1个“心跳”的待命状态就起到消除累加误差的作用。

【126.3   异步的肢体通信的例子。】

       “手”可以产生“两种”状态“握紧”和“松开”,甲发送数据给乙,乙每“心跳”一次就去判断一次手的状态,“握紧”代表二进制的0,“松开”代表二进制的1,这样,如果他们之间想传输一个字节的十六进制数据0x59,只需把十六进制的数据0x59展开成二进制01011001,从右到左(从低位到高位)以“位”为单位挨个发送,过程如下:
       平时手的状态一直处于“松开”的待命状态,直到手第一次出现“握紧”的状态......
       第一次“心跳”:手的状态是“握紧”,开始位,通知乙作好接收即将过来的8个“心跳”数据位。
       第二次“心跳”:手的状态是“松开”,数据位bit0,记录1。
       第三次“心跳”:手的状态是“握紧”,数据位bit1,记录0。
       第四次“心跳”:手的状态是“握紧”,数据位bit2,记录0。
       第五次“心跳”:手的状态是“松开”,数据位bit3,记录1。
       第六次“心跳”:手的状态是“松开”,数据位bit4,记录1。
       第七次“心跳”:手的状态是“握紧”,数据位bit5,记录0。
       第八次“心跳”:手的状态是“松开”,数据位bit6,记录1。
       第九次“心跳”:手的状态是“握紧”,数据位bit7,记录0。
       第十次“心跳”:手的状态是“松开”,停止位,至少持续1个“心跳”的待命状态。
       现在把上述的“单线”(异步)的肢体通信过程翻译成C语言代码,甲发送数据的代码如下:


sbit Hand_DATA=P2^6;//手的数据线

void SendByte(unsiged char u8Data) //甲发送数据的发送函数
{
    static unsigned char i;

    Hand_DATA=0;//开始位。0代表“握紧”
Delay();//甲的心跳间隔时间
    for(i=0;i<8;i++) //发送8个数据位
    {
      if(0==(u8Data&0x01))//根据数据的每一位状态,发送对应的位数据。
      {
            Hand_DATA=0;//0代表“握紧”
      }
      else
      {
            Hand_DATA=1;//1代表“松开”
      }

Delay();//甲的心跳间隔时间

      u8Data=u8Data>>1; //右移一位,为即将发送下一位做准备
    }
    Hand_DATA=1;//停止位。1代表“松开”
Delay();//甲的心跳间隔时间
}

tt98 发表于 2018-8-5 20:34:00

再次收藏,谢谢鸿哥!{:lol:}

XTXB 发表于 2018-8-14 08:56:07

每一个知识点都讲得狠透,多谢鸿哥!

吴坚鸿 发表于 2018-8-26 13:33:02

第一百二十七节: 单片机串口接收数据的机制。

【127.1   单片机串口接收数据的底层时序。】

       上一节“单线的肢体接触通信”其实是为本节打基础的,通信线只用了一根“数据”线,没有用到“时钟”线,属于异步通信方式,还分析时序中的“1个开始位,8个数据位,1个停止位”等细节内容,这些时序其实就是本节单片机串口通信的底层时序,一模一样。继续上一节的内容(很有必要重新温习一次上一节的异步通信原理),继续沿用甲乙双方靠各自“心跳”的节拍来异步通信的例子,本节单片机串口接收数据是代表乙方,我把乙方串口接收数据的过程翻译成C语言,代码如下:

sbit USART_RX=P3^0;//用来接收串口数据的数据线
unsigned char Gu8ReceiveData=0;//串口接收到的8位数据
unsigned char i; //连续接收8位数据的循环变量
void main()
{
    Gu8ReceiveData=0;
    while(1)
    {
      USART_RX=1;   //51单片机的规则,每次读取数据前都执行一条“置1”指令
    Delay();      //乙的心跳间隔时间,待机时,每一个节拍监控一次数据线的状态
      if(0==USART_RX) //如果监控到甲发送的“开始位0”,从下一个节拍开始连续接收8位数据
{
            for(i=0;i<8;i++) //连续循环接收8个“数据位”
{
                  USART_RX=1;   //51单片机的规则,每次读取数据前都执行一条“置1”指令
                  Delay();      //乙的心跳间隔时间,每个节拍判断读取一位数据
                  if(1==USART_RX)//判断读取数据线上的状态
{
       Gu8ReceiveData=Gu8ReceiveData | 0x80;
}
else
{
       Gu8ReceiveData=Gu8ReceiveData & 0x7F;
}
Gu8ReceiveData=Gu8ReceiveData>>1; //右移一位,为即将接收下一位做准备
                  }
            Delay();    //乙的心跳间隔时间,这里额外增加一个节拍,作为“停止位”的开销。
}
    }
}


【127.2   单片机内置的“硬件串口模块”。】

       很显然,上面【127.1】分享的时序代码会占用单片机大量的时间,单片机每接收一个字节的数据都会被束缚一次手脚,耽误了其它大事,怎么办?为了把单片机从底层繁琐的时序中解放出来,单片机内置了很多“硬货”,俗称“硬件资源”,“硬件串口模块”便是其中之一。何谓“硬件”,单片机内置的“硬件”可以看作是另外一个独立运行的“核”,这个“核”可以看作是另外一个CPU,可以独立工作,相当于单片机主人在某个领域的一个专用助手。单片机只需要跟这个“核”通信发指令就可以,具体的执行过程由这个“核”独立去完成,这个“核”完成工作之后再把处理结果反馈给单片机。那么,单片机是如何跟这些内置“硬件资源”通信呢?其实它们的通信接口是“寄存器”,不管是单片机给“硬件资源”发送指令,还是单片机从“硬件资源”里读取所需要的结果数据,都是通过“寄存器”来完成。

【127.3   单片机与硬件串口通信的接口“寄存器”。】

       硬件串口的寄存器主要涉及:串口的方式选择,波特率,允许串口接收数据,中断的优先级,中断的允许,等等。比如,51单片机的串口是兼容很多种方式的,可以同步通信,也可以异步通信,异步通信还可以兼容10位(1开始位、8数据位、1停止),11位(1开始位、8数据位、1校验位、1停止),等等,这些就是多选题,我们要在某个特定的寄存器里面做出选择。波特率,是用来衡量通信的速度,比如波特率是9600,就意味着1秒钟能收发9600个二进制的位数据,也就是1秒钟能产生9600个时钟节拍,波特率越高通信的速度越快,这些也需要我们往相关的寄存器填入相应的数据,来告知“硬件串口”以哪种波特率进行通信。
       那么,对于初学者,寄存器如何配置呢?主要有这些思路:查看芯片手册(datasheet),产看C编译器的手册,查看芯片相关的C语言的头文件(比如51单片机的REG.H),在网上参考别人已经配置好的代码,或者购买相关芯片的学习板时所配套的程序例程。
       本节用到的串口,是10位数据长度的异步通信,波特率9600,相关配置的代码如下:

unsigned char u8_TMOD_Temp=0;

//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; /此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;//往高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);//波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);//L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;//开启定时器1

SM0=0;
SM1=1;//SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变
REN=1;//允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数,实现嵌套的功能,
IP =0x10;//把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断
EA=1;         //允许总中断

【127.4   硬件串口的中断函数。】

       硬件串口接收完一个字节的数据之后,会及时产生一个串口中断去通知单片机接收数据。单片机在串口中断函数里直接读取“串口专用缓存寄存器”SBUF的数据,就可以直接获得一个完整的8位宽度的数据,省去了繁琐的驱动时序底层。
       串口的中断函数跟定时器的中断函数很类似,只不过中断号不一样而已,比如我们前面章节用的定时器0的中断号是“1”,而本节串口的中断号是“4”。这些其实是C编译器定的游戏规则,我们只要根据它提供的数据手册遵守它的游戏规则就好了。串口中断函数里还有一个地方要注意,硬件串口“接收完一个字节”的数据后产生一次中断,而硬件串口“发送完一个字节”的数据后也产生一次中断,这两个一“收”一“发”的中断都是共用中断号为“4”的中断函数,因此,我们必须在中断函数里通过判断寄存器的RI和TI的标志位来判断到底是“收”的中断,还是“发”的中断,并且软件上要及时把RI或者TI及时清零,避免不断进入中断的情况。参考代码如下:

unsigned char Gu8ReceiveData=0;//接收到一个字节的数据

void usart(void) interrupt 4   //串口接发的中断,中断号为4      
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。
      Gu8ReceiveData=SBUF;//直接读取“串口专用缓存寄存器”SBUF的8位数据。
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。

   }                                                      
}

【127.5   上位机与单片机的串口通信例程。】


       上图127.5.1灌入式驱动8个LED

       程序功能如下:
       波特率9600,校验位NONE(无),数据位8,停止位1。在上位机的串口助手里,发送一个十六进制(HEX模式)的01,让单片机的一颗LED“亮”。发送一个十六进制(HEX模式)的00,让单片机的一颗LED“灭”。上位机的串口助手的使用,请参考前面第11节的相关内容,或者自己在网上查找串口助手软件的使用方法,串口助手软件网上很多,我们的使用要求不高,随便选用一家都可以。代码如下:

#include "REG52.H"

void usart(void);

sbit P0_0=P0^0;//一颗LED灯

unsigned char Gu8ReceiveData=0;//接收到一个字节的数据

void main()
{
unsigned char u8_TMOD_Temp=0;

P0_0=1;//LED灭。1代表LED灭, 0代表亮

//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;//把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);//波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);//L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;//开启定时器1

SM0=0;
SM1=1;//SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变
REN=1;//允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10;//把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断
EA=1;         //允许总中断
    while(1)//主循环
    {
    }
}

void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。
      Gu8ReceiveData=SBUF;//直接读取“串口专用缓存寄存器”SBUF的8位数据。
      if(0x01==Gu8ReceiveData)
{
P0_0=0;//LED亮。1代表LED灭, 0代表亮
}
else
{
P0_0=1;//LED灭。1代表LED灭, 0代表亮
}
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}


【127.6   单片机串口电路的简易分析。】




       上图127.6.1232串口电路

      单片机串口对外的引脚是与IO口的“P3.1、P3.0”共用的。P3.1是串口的TX引脚,即对外发送数据的引脚。P3.0是串口的RX引脚,即接收外部数据的引脚。一旦项目中用了串口,那么这两个引脚就必须“专脚专用”,只给串口单独使用,不再做IO口。平时通信的时候,跟其它单片机或者系统进行串口通信,除了接TX和RX这两根数据线之外,必须一定把双方的负极GND也“共地”接上,否则双方建立不了同样的电压参考点,通信毕然失败。因此,串口通信最低标配是3根线:RX,TX,GND。
      如果两个甲乙单片机都布在一块板子上,距离不超过半米,他们两个要进行串口通信,怎么接线?把他们的GND连起来,然后RX与TX“交叉”对接,甲的RX接到乙的TX,甲的TX接到乙的RX。这种在短距离通信的时候,不用增加任何外部辅助压差信号放大芯片,这种方式叫做“串口的TTL”接线方式。
      如果两个系统串口通信的距离比较远,比如在不同的板子上,1米以上10米以下的距离,这时就不能采用原始的“串口的TTL”接线方式,因为线缆越长电阻越大,本身就要消耗一些压降,而3.3V的压降很容易就会被消耗完,通信的可靠度和抗扰能力就会降低。为了解决这个问题,可以引用232标准的接线方式,外部需接一个压差放大的芯片,把从原来3.3V的压差放大到一两倍左右,通信的距离就大大提高。具体232的细节,大家可以网上搜搜“RS232”。注意,采用232协议通信,也要注意“共地”和数据线“交叉”的两个问题,232通信的最低标配也是3根线:R,T,GND。上图SP232E就是一个压差信号放大的通信专用芯片。

wahaha 发表于 2018-8-27 00:23:41

讲的很详细,学习了

kkfy888 发表于 2018-8-28 23:12:03

支持鸿哥,以做几片板子回来了,以后可以做多机通讯实验用,期待以后液晶屏的教程,方便实验.

qq335702318 发表于 2018-8-29 08:57:44

支持楼主

吴坚鸿 发表于 2018-9-9 17:18:57

第一百二十八节: 接收“固定协议”的串口程序框架。

【128.1   固定协议。】

       实际项目中,串口一个回合的收发数据量远远不止1个字节,而是往往携带了某种“固定协议”的一串数据(专业术语称“一帧数据”)。一串数据的“固定协议”因为起到类似“校验”和“密码确认”的功能,因此在安全可靠性方面大大增强。但是上一节也提到,单片机利用最底层硬件的串口接口,一次收发的最小单位是“1个字节”,那么,怎么样在此基础上搭建一个能快速收发并且能快速解析数据的程序框架就显得尤为重要。本节我跟大家分享我常用的串口程序框架,此框架主要包含“数据头,数据类型,数据长度,其它数据”这四部分。比如,为了通过串口去控制单片机的蜂鸣器发出不同长度的声音,我专门制定了一串十六进制的数据:EB 01 00 00 00 08 03 E8 ,下面以此串数据来跟大家详细分析。
       数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。只有“接头暗号”吻合,单片机才会进入到接收其它有效数据的步骤,否则一直被“数据头”挡在门外视为无效数据。注意,数据头不能用十六进制的00或者FF,因为00和FF的密码等级太弱,很多单片机一上电的瞬间因为硬件的某种不确定的原因,会直接误发送00或者FF这类干扰数据。
       数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途,比如,01代表用来控制蜂鸣器的,02代表控制LED的,03代表机器启动,等等功能,都可以用这个字节的数据进行分类定义。本例子用01代表控制蜂鸣器发出不同时间长度的声音。
       数据长度(00 00 00 08):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。本例子中,数据长度占用了4个字节,就意味着最大数据长度是一个unsigned long类型的数据范围,从0到4294967295。比如,本例子中一串数据的长度是8个字节(EB 01 00 00 00 08 03 E8 ),因此这“数据长度”四个字节分别是00 00 00 08,十六进制的08代表十进制的8字节。注意,51单片机的内存是属于大端模式,因此十进制的8在四字节unsigned long的内存排列顺序是00 00 00 08,也就是低位放在数组的高下标。如果是stm32的单片机,stm32单片机的内存是属于小端模式,十进制的8在四字节unsigned long的内存排列顺序是08 00 00 00,低位放在数组的低下标。为什么强调这个?因为主要方便我们用指针的方法实现数据的拆分和整合,这个知识点的内容我在前面第62节详细讲解过。
       其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。本例子十六进制的03 E8,代表一个unsigned int的十进制数据1000。此数据的大小用来控制蜂鸣器发声的长度,1000代表长叫1000ms。如果想让蜂鸣器短叫100ms,只需把这两个字节改为:00 64。

【128.2   程序框架的四个要点分析。】

       第一点:先接收后处理,开辟一块专用的内存数组。要处理一串数据,必须先征用一块内存数组专门用来缓存接收到的数据,等接收完此串数据再处理。
       第二点:接头暗号。本节例子的数据头EB就是接头暗号。一旦接头暗号吻合,才会进入到下一步接收其它有效数据的步骤上。
       第三点:如何识别接收一串数据的完毕。本节例子中,是靠“固定协议”提供的“数据长度”来判别是否已经接收完一串数据。中断函数接收完一串数据后,应该用一个全局变量来给外部main函数一个通知,让main函数里面的相关函数来处理此串数据。
       第四点:接收数据中相邻字节之间通信超时的异常处理。如果接头暗号吻合之后,马上切换到“接受其它有效数据”的步骤,但是,如果在此步骤的通信过程中一旦发现通信不连贯,就应该及时退出当下“接受其它有效数据”的步骤,继续返回到刚开始的“接头暗号”的步骤,为下一次接收新的一串数据做准备。那么,如何识别通信不连贯?靠判断接收数据中相邻字节之间的时间是否超时来决定,详细内容请看下面的程序例程。

【128.3   程序例程。】


         
            上图128.3.1有源蜂鸣器电路




         
            上图128.3.2232串口电路

程序功能如下:
      (1)在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
      (2)波特率9600,校验位NONE(无),数据位8,停止位1。
      (3)十六进制的数据格式如下:
       EB 01 00 00 00 08 XX XX
       其中EB是数据头,01是代表数据类型,00 00 00 08代表数据长度是8个(十进制)。XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。比如:
       让蜂鸣器鸣叫1000ms的时间,发送十六进制的: EB 01 00 00 00 08 03 E8
       让蜂鸣器鸣叫100ms的时间,发送十六进制的: EB 01 00 00 00 08 00 64

#include "REG52.H"

#define RECE_TIME_OUT    2000//通信过程中字节之间的超时时间2000ms
#define REC_BUFFER_SIZE20    //接收数据的缓存数组的长度


void usart(void);//串口接收的中断函数
void T0_time();    //定时器的中断函数

void UsartTask(void);    //串口接收的任务函数,放在主函数内

void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);

sbit P3_4=P3^4;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8ReceBuffer; //开辟一片接收数据的缓存
unsigned long Gu32ReceCnt=0;//接收缓存数组的下标
unsigned char Gu8ReceStep=0;//接收中断函数里的步骤变量
unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8ReceType=0; //接收的数据类型
unsigned long Gu32ReceDataLength=0;//接收的数据长度
unsigned char Gu8FinishFlag=0;//是否已接收完成一串数据的标志
unsigned long *pu32Data; //用于数据转换的指针
volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   UsartTask();   //串口接收的任务函数
    }
}

void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。

/* 注释一:
* 以下Gu8FinishFlag变量的用途。
* 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
* UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
* 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
*/
         if(0==Gu8FinishFlag)//1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
         {

/* 注释二:
* 以下Gu8ReceFeedDog变量的用途。
* 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
* 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
* 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
* 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
* 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
* 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
*/
                  Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
                  switch(Gu8ReceStep)
                  {
                        case 0:   //接头暗号的步骤。判断数据头的步骤。
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 if(0xeb==Gu8ReceBuffer)//等于数据头0xeb,接头暗号吻合。
                                 {
                                          Gu32ReceCnt=1; //接收缓存的下标
                                          Gu8ReceStep=1;//切换到下一个步骤,接收其它有效的数据
                                 }
                                 break;               
                                       
                        case 1:   //数据类型和长度
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
                                 if(Gu32ReceCnt>=6)//前6个数据。接收完了“数据类型”和“数据长度”。
                                 {
                                          Gu8ReceType=Gu8ReceBuffer;//提取“数据类型”
//以下的数据转换,在第62节讲解过的指针法
                                                pu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换
                                                Gu32ReceDataLength=*pu32Data; //提取“数据长度”
                                          if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
                                                {
                                                      Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                                      Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                                }
                                                else   //如果还没结束,继续切换到下一个步骤,接收“其它数据”
                                                {
                                                      Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                      
                                 }
                                 break;               
                        case 2:   //其它数据
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                 if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC_BUFFER_SIZE)
{
                                          Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                          Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                 }
                                 break;      
                  }
       }
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}


void UsartTask(void)    //串口接收的任务函数,放在主函数内
{
static unsigned int *pSu16Data; //数据转换的指针
static unsigned int Su16Data;//转换后的数据

    if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
      {
                Gu8ReceFeedDog=0;
                              
                vGu8ReceTimeOutFlag=0;
      vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
                vGu8ReceTimeOutFlag=1;
      }
      else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
      {
            Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
    }

      
      if(1==Gu8FinishFlag)//1代表已经接收完毕一串新的数据,需要马上去处理
      {
                switch(Gu8ReceType)//接收到的数据类型
                {
                        case 0x01:   //驱动蜂鸣器
//以下的数据转换,在第62节讲解过的指针法
                                 pSu16Data=(unsigned int *)&Gu8ReceBuffer; //数据转换。
                                 Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
vGu8BeepTimerFlag=1;
                  break;
      }

      Gu8FinishFlag=0;//上面处理完数据再清零标志,为下一次接收新的数据做准备
    }
}



void T0_time() interrupt 1   
{
VoiceScan();

if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
      {
               vGu16ReceTimeOutCnt--;      
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
unsigned char u8_TMOD_Temp=0;

//以下是定时器0的中断的配置
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;   

//以下是串口接收中断的配置
//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;//把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);//波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);//L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;//开启定时器1

SM0=0;
SM1=1;//SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变
REN=1;//允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10;//把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断
EA=1;         //允许总中断
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

tt98 发表于 2018-9-9 21:47:01

谢谢鸿哥{:lol:}

soos 发表于 2018-9-9 21:55:22

虽然没仔细看连,但这份坚持就值得支持一把了

liu615 发表于 2018-9-10 09:02:37

非常感谢,都非常实用。

吴坚鸿 发表于 2018-9-18 11:01:16

第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架。

【129.1   “累加和”与“动态密匙”。】

      上一节讲了串口基本的程序框架,但是没有讲到校验。校验在很多通信项目中是必不可少的。比如,在事关金融或者生命安全的项目,是不允许有任何的数据丢失或错误的;在容易受干扰的工业环境,或者在无线通信的项目中,这些项目往往容易丢失数据;还有一种常见的人为过失是,在编写程序的层面,因为超时重发的时间与从机不匹配,导致反馈的信息延时而造成数据丢失,如果这种情况也加上校验,通信会稳定可靠很多。
      上一节讲到“数据头,数据类型,数据长度,其它数据”这四个元素,本节在此基础上,增加两个校验的元素,分别是“动态密匙”与“累加和”。“动态密匙”占用2个字节,“累加和”占用1个字节,因此,这两个元素一共占用最后面的3个字节。分析如下:
       数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。
       数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途。
       数据长度(00 00 00 0B):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。
       其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。
       动态密匙(00 01):这两个字节代表一个unsigned int类型的数据,数据范围是从0到65535,但是考虑到数据更加安全可靠,一般丢弃了首尾的0(十六进制的00 00)与65535(十六进制的FF FF),只保留从1到65534的变化。大部分的通信模型都是主机对从机的“一问一应答”模式,也就是,主机每发送一条指令给从机,从机才返回一条消息作为应答。如果主机发送了信息后,在规定的时间内,没有收到从机的应答指令,主机就继续发送信息给从机,但是此时,从机本来应该应答主机当前指令的,可能因为某种情况导致反馈的信息发生了延时,导致此时应答的数据是主机的上一条指令,从而造成“一问一应答”的数据帧发送了错位,这种情况加上“动态密匙”就能使问题得到有效的解决。主机每发送一条信息,信息里都携带了2个字节的“动态密匙”,从机每收到主机的一条信息,在应答此信息时都把收到的“动态密匙”原封不动的反馈给主机,主机再查看发送的“动态密匙”与接收到的“动态密匙”是否一致,以此来判断应答数据是否有效。“动态密匙”像流水号一样,每发送一次指令后都累加1,不断发生变化,从1到65534,依次循环。这就是数据校验的一种方式。
       累加和(E3)。“累加和”放在数据串的最后一个字节,是前面所有字节的累加之和(不包括自己本身的字节),累加的结果高于一个字节的那部分自动溢出丢掉,只保留低8位的一个字节的数据。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 E3。其中最后一个字节E3就是“累加和”,前面所有字节相加等于十六进制的0x1E3,只保留低8位的一个字节的数据,因此为十六进制的0xE3。验证“累加和”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“累加和”,把自己计算得到的“累加和”与接收到的最后一个字节的“累加和”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【129.2   程序例程。】


   
       上图129.2.1有源蜂鸣器电路




   
       上图129.2.2232串口电路

程序功能如下:
      (1)单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
      (2)本节因为还没有讲到数据发送的内容,因此应答那部分的代码暂时不写,只写“累加和”那部分的代码。
      (3)波特率9600,校验位NONE(无),数据位8,停止位1。
      (4)十六进制的数据格式:EB 01 00 00 00 0B XX XX YY YY ZZ 。其中:
       EB是数据头。
       01是代表数据类型。
       00 00 00 0B代表数据长度是11个(十进制)。
       XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。
       YY YY代表一个unsigned int的动态密匙,每收发一条指令,此数据累加一次1,范围从1到65534。
       ZZ 代表前面所有字节的累加和。
       比如:
       让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 E3
       让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 01 00 00 00 0B 00 64 00 02 5D

#include "REG52.H"

#define RECE_TIME_OUT    2000//通信过程中字节之间的超时时间2000ms
#define REC_BUFFER_SIZE20    //接收数据的缓存数组的长度


void usart(void);//串口接收的中断函数
void T0_time();    //定时器的中断函数

void UsartTask(void);    //串口接收的任务函数,放在主函数内

void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);

sbit P3_4=P3^4;

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8ReceBuffer; //开辟一片接收数据的缓存
unsigned long Gu32ReceCnt=0;//接收缓存数组的下标
unsigned char Gu8ReceStep=0;//接收中断函数里的步骤变量
unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8ReceType=0; //接收的数据类型
unsigned int Gu16ReceYY=0; //接收的动态密匙
unsigned char Gu8ReceZZ=0; //接收的累加和,必须是unsigned char的数据类型
unsigned long Gu32ReceDataLength=0;//接收的数据长度
unsigned char Gu8FinishFlag=0;//是否已接收完成一串数据的标志
unsigned long *pu32Data; //用于数据转换的指针
volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   UsartTask();   //串口接收的任务函数
    }
}

void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。

/* 注释一:
* 以下Gu8FinishFlag变量的用途。
* 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
* UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
* 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
*/
         if(0==Gu8FinishFlag)//1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
         {

/* 注释二:
* 以下Gu8ReceFeedDog变量的用途。
* 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
* 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
* 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
* 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
* 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
* 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
*/
                  Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
                  switch(Gu8ReceStep)
                  {
                        case 0:   //接头暗号的步骤。判断数据头的步骤。
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 if(0xeb==Gu8ReceBuffer)//等于数据头0xeb,接头暗号吻合。
                                 {
                                          Gu32ReceCnt=1; //接收缓存的下标
                                          Gu8ReceStep=1;//切换到下一个步骤,接收其它有效的数据
                                 }
                                 break;               
                                       
                        case 1:   //数据类型和长度
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
                                 if(Gu32ReceCnt>=6)//前6个数据。接收完了“数据类型”和“数据长度”。
                                 {
                                          Gu8ReceType=Gu8ReceBuffer;//提取“数据类型”
//以下的数据转换,在第62节讲解过的指针法
                                                pu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换
                                                Gu32ReceDataLength=*pu32Data; //提取“数据长度”
                                          if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
                                                {
                                                      Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                                      Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                                }
                                                else   //如果还没结束,继续切换到下一个步骤,接收“其它数据”
                                                {
                                                      Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                      
                                 }
                                 break;               
                        case 2:   //其它数据
                                 Gu8ReceBuffer=SBUF; //直接读取刚接收完的一个字节的数据。
                                 Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                 if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC_BUFFER_SIZE)
{
                                          Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                          Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                 }
                                 break;      
                  }
       }
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}


void UsartTask(void)    //串口接收的任务函数,放在主函数内
{
static unsigned int *pSu16Data; //数据转换的指针
static unsigned int Su16Data;//转换后的数据
static unsigned int i;
static unsigned char Su8RecZZ=0;//计算的“累加和”,必须是unsigned char的数据类型



    if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
      {
                Gu8ReceFeedDog=0;
                              
                vGu8ReceTimeOutFlag=0;
      vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
                vGu8ReceTimeOutFlag=1;
      }
      else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
      {
            Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
    }

      
      if(1==Gu8FinishFlag)//1代表已经接收完毕一串新的数据,需要马上去处理
      {
                switch(Gu8ReceType)//接收到的数据类型
                {
                        case 0x01:   //驱动蜂鸣器
//以下的数据转换,在第62节讲解过的指针法

                                 pSu16Data=(unsigned int *)&Gu8ReceBuffer; //数据转换
Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

Gu8ReceZZ=Gu8ReceBuffer;//提取“累加和”

Su8RecZZ=0;
for(i=0;i<(Gu32ReceDataLength-1);i++)
{
Su8RecZZ=Su8RecZZ+Gu8ReceBuffer;   //计算“累加和”
}

if(Su8RecZZ==Gu8ReceZZ) //验证“累加和”,“计算的”与“接收的”是否一致
{
                              pSu16Data=(unsigned int *)&Gu8ReceBuffer; //数据转换。
                                     Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
vGu8BeepTimerFlag=1;
}

               
                  break;
      }

      Gu8FinishFlag=0;//上面处理完数据再清零标志,为下一次接收新的数据做准备
    }
}

void T0_time() interrupt 1   
{
VoiceScan();

if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
      {
               vGu16ReceTimeOutCnt--;      
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
unsigned char u8_TMOD_Temp=0;

//以下是定时器0的中断的配置
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;   

//以下是串口接收中断的配置
//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;//把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);//波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);//L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;//开启定时器1

SM0=0;
SM1=1;//SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变
REN=1;//允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10;//把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断
EA=1;         //允许总中断
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}



tt98 发表于 2018-9-18 11:50:39

谢谢鸿哥!{:handshake:}
页: 1 2 3 [4] 5
查看完整版本: 从单片机基础到程序框架(连载)