jianhong_wu 发表于 2018-5-12 10:29:16

本帖最后由 jianhong_wu 于 2018-5-12 10:45 编辑

第一百一十七节: 按键切换数码管窗口来设置参数。

【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)
{

}




jianhong_wu 发表于 2018-5-29 09:51:28

本帖最后由 jianhong_wu 于 2018-5-29 10:08 编辑

第一百一十八节: 按键让某位数码管闪烁跳动来设置参数。

【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)
{

}


jianhong_wu 发表于 2018-6-3 10:28:23

本帖最后由 jianhong_wu 于 2018-6-3 10:41 编辑

第一百一十九节: 一个完整的人机界面的程序框架的脉络。

【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;

}
}


jianhong_wu 发表于 2018-6-10 12:06:42

本帖最后由 jianhong_wu 于 2018-6-10 12:17 编辑

第一百二十节: 按键切换窗口切换局部来设置参数。

【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)
{

}




jianhong_wu 发表于 2018-6-21 14:29:57

本帖最后由 jianhong_wu 于 2018-6-21 14:53 编辑

第一百二十一节: 可调参数的数码管倒计时。

【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)
{

}




jianhong_wu 发表于 2018-7-1 12:29:33

本帖最后由 jianhong_wu 于 2018-7-1 12:48 编辑

第一百二十二节: 利用定时中断做的“时分秒”数显时钟。

【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-2 15:10:04

RE: 从单片机基础到程序框架(连载)

jianhong_wu 发表于 2017-10-2 11:45
第八十九节: 跑马灯的三种境界。

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


思考的对象不一样,如果控制是LED,肯定这代码比较累赘,如果换做一些复杂的系统那就不一样,代码看中的是思想:lol

jianhong_wu 发表于 2018-7-9 09:59:44

本帖最后由 jianhong_wu 于 2018-7-9 10:16 编辑

第一百二十三节: 一种能省去一个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也不影响代码本该有的自锁功能”。通过此例子,给初学者一个建议,在代码的“队形感,易读性,扩展性,分类清晰”和“巧妙,节省代码”两者之间,建议大家优先考虑“队形感,易读性,扩展性,分类清晰”,追求一种原则上的“工整,不出奇兵,扎硬寨,打呆仗,步步为营”,这样阵脚不易乱,能走得更远,驾驭更多千军万马的代码。


jianhong_wu 发表于 2018-7-16 10:22:31

本帖最后由 jianhong_wu 于 2018-7-16 10:42 编辑

第一百二十四节: 数显仪表盘显示“速度、方向、计数器”的跑马灯。

【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)
{

}



jianhong_wu 发表于 2018-7-24 11:10:22

本帖最后由 jianhong_wu 于 2018-8-5 09:19 编辑

第一百二十五节: “双线”的肢体接触通信。

【125.1   “双线”的肢体接触通信。】

      芯片之间通信,都离不开“数据信号”和“时钟信号”,缺一不可。“数据信号”和“时钟信号”是什么关系,它们是怎样相互配合来实现通信的功能?其实原理也很简单。打个比喻,甲乙两个人,规定只能靠一只“手”和一只“脚”进行肢体接触的通信,他们之间如何传输数据?“手”可以产生“两种”状态“握紧”和“松开”,“脚”可以产生“一种”状态“踢一脚”。他们之间约定,甲发送数据给乙,乙每被甲“踢一脚”就去记录一次手的状态是“握紧”还是“松开”,“握紧”代表二进制的0,“松开”代表二进制的1,这样,如果他们之间想传输一个字节的十六进制数据0x59,只需把十六进制的数据0x59展开成二进制01011001,从右到左(从低位到高位)以“位”为单位挨个发送,过程如下:
       第一次“踢一脚”:手的状态是“松开”,记录1。
       第二次“踢一脚”:手的状态是“握紧”,记录0。
       第三次“踢一脚”:手的状态是“握紧”,记录0。
       第四次“踢一脚”:手的状态是“松开”,记录1。
       第五次“踢一脚”:手的状态是“松开”,记录1。
       第六次“踢一脚”:手的状态是“握紧”,记录0。
       第七次“踢一脚”:手的状态是“松开”,记录1。
       第八次“踢一脚”:手的状态是“握紧”,记录0。
       上述肢体接触的通信过程,其实一只“手”就代表了一根“数据线”,可以产生高电平“1”和低电平“0”这两种状态,而一只“脚”代表了一根“时钟线”,但是“踢一脚”代表了“时钟线”上的一种什么状态呢?注意,“踢一脚”既不是高电平“1”也不是低电平“0”,而是瞬间只产生一次的“上升沿”或者“下降沿”。何谓“上升沿”何谓“下降沿”?“上升沿”是代表“时钟线从低电平跳变到高电平的瞬间”,“下升沿”是代表“时钟线从高电平跳变到低电平的瞬间”。“踢一脚”、“上升沿”、“下降沿”此三者都可以统一理解成“节拍”。
       芯片之间通信,“时钟信号”只需1个足矣,而“数据信号”却可以不止1个。1个“数据信号”往往叫“串行”通信,一个节拍只能传输1位数据。8个以上并列的“数据信号”往往叫“并行”通信,一个节拍能传输8位以上的数据。可见,并行的“数据信号”越多,传输的速率越快。
       常见的系统中,串口,IIC,SPI,USB,CAN这类都是“串行”通信。而32位单片机与外部的nandflash,norflash,sdram,sram这些芯片通信往往是“并行”通信,并行的数据信号多达8个16个甚至32个。
       本节标题之所以强调“双线”,是因为“手”代表数据线,“脚”代表时钟线,一共两条线因此为“双线”。现在把上述的肢体通信过程翻译成C语言代码,如下:
sbit Hand_DATA=P2^6;//手的数据线
sbit Foot_CLK=P2^7;   //脚的时钟线

void SendByte(unsiged char u8Data) //肢体接触通信发送一个字节的数据的发送函数
{
    static unsigned char i;
    for(i=0;i<8;i++) //一个字节包含8个位数据,需要循环8次
    {
       if(0==(u8Data&0x01))//根据数据的每一位状态,发送对应的位数据。
       {
            Hand_DATA=0;//0代表“握紧”
       }
       else
       {
            Hand_DATA=1;//1代表“松开”
       }

       Foot_CLK=1;
       Delay(); //为产生均匀的脉冲节拍,时钟线的高电平先延时一会
       Foot_CLK=0;//从高电平跳变到低电平,产生瞬间的“下降沿”,代表“踢一脚”
       Delay(); //为产生均匀的脉冲节拍,时钟线的低电平先延时一会

       u8Data=u8Data>>1; //右移一位,为即将发送下一位做准备
    }
}


jianhong_wu 发表于 2018-8-5 12:35:56

本帖最后由 jianhong_wu 于 2018-8-5 12:47 编辑

第一百二十六节: “单线”的肢体接触通信。

【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();//甲的心跳间隔时间
}




jianhong_wu 发表于 2018-8-26 13:07:17

本帖最后由 jianhong_wu 于 2018-8-26 13:37 编辑

第一百二十七节: 单片机串口接收数据的机制。

【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就是一个压差信号放大的通信专用芯片。


jianhong_wu 发表于 2018-9-9 17:00:38

本帖最后由 jianhong_wu 于 2018-9-9 17:22 编辑

第一百二十八节: 接收“固定协议”的串口程序框架。

【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();
                   }

}
          }         
}





jianhong_wu 发表于 2018-9-18 10:50:47

本帖最后由 jianhong_wu 于 2018-9-18 11:05 编辑

第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架。

【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();
                   }

}
          }       
}





jianhong_wu 发表于 2018-10-4 10:58:48

本帖最后由 jianhong_wu 于 2018-10-4 11:19 编辑

第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架。

【130.1   “异或”的校验。】

      通信的校验常用有两种,一种是“累加和”,另一种是“异或”。“异或”算法的详细介绍请看前面章节的第32节。
      上一节讲的“累加和”,放在数据串的最后一个字节,是前面所有字节的累加之和(不包括自己本身的字节),累加的结果高于一个字节的那部分自动溢出丢掉,只保留低8位的一个字节的数据。本节讲的“异或”,也是放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。本节在上一节的基础上,只更改以下这段校验算法的代码即可。

      上一节的“累加和”算法如下:


Gu8ReceZZ=Gu8ReceBuffer;//提取“累加和”

Su8RecZZ=0;
for(i=0;i<(Gu32ReceDataLength-1);i++)
{
Su8RecZZ=Su8RecZZ+Gu8ReceBuffer;   //计算“累加和”
}

if(Su8RecZZ==Gu8ReceZZ) //验证“累加和”,“计算的”与“接收的”是否一致
{
    //此处省去若干代码
}

      本节的“异或”算法如下:
Gu8ReceZZ=Gu8ReceBuffer;//提取接收到的“异或”

Su8RecZZ=Gu8ReceBuffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
{
Su8RecZZ=Su8RecZZ^Gu8ReceBuffer;   //计算“异或”
}

if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
{
    //此处省去若干代码
}

【130.2   通信协议。】

       数据头(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,依次循环。这是数据校验的一种方式。
       异或(0B)。“异或”放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 0B。其中最后一个字节0B就是“异或”字节,前面所有字节相“异或”等于十六进制的0B。验证“异或”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”,该计算器软件的异或运算按键是“Xor”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“异或”,把自己计算得到的“异或”与接收到的最后一个字节的“异或”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【130.3   程序例程。】


      
       上图130.3.1有源蜂鸣器电路





       上图130.3.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 0B
       让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 01 00 00 00 0B 00 64 00 02 87

#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 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;//计算的“异或”



    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=Gu8ReceBuffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
{
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();
                   }

}
          }       
}





jianhong_wu 发表于 2018-10-14 10:01:47

本帖最后由 jianhong_wu 于 2018-12-10 10:13 编辑

第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架。

【131.1   切换各种不同大小“接收内存”。】

      很多32位的单片机,只要外挂SRAM或者SDRAM这类内存芯片,就可以轻松的把一个全局变量的数组开辟到几百K甚至几兆的容量。开辟这么大的数组,往往是用来处理一些文件类的大数据,比如串口接收一张480x272点阵大小的.BMP格式的图片文件,就需要开辟一个几百K的全局变量大数组。串口通信中,从接收内存的容量来划分,常用有两种数据类型,一种是常规控制类(容量小),一种是文件类(容量大),要能做到在这两种“接收内存”中灵活切换,关键是用到“指针的中转切换”技术。
      “常规控制类内存”负责两块事务,一块是接收“前部分的”[数据头,数据类型,数据长度],另一块是“后部分的”[常规控制类的专用数据]。
      “文件类内存”只负责“后部分的”[文件类的专用数据],而“前部分的”[数据头,数据类型,数据长度]是需要借助“常规控制类内存”来实现的。
      本节破题的关键在于,根据不同的数据类型,利用“指针的中转切换”实现不同接收内存的灵活切换。关键代码是串口中断函数这部分的处理,片段代码的讲解如下:
unsigned char Gu8ReceBuffer; //常规控制类的小内存
unsigned char Gu8FileBuffer; //文件类的大内存
unsigned char *pGu8ReceBuffer;//用来切换接收内存的“中转指针”
unsigned long Gu32ReceCntMax=20;//最大缓存(初始值20或者40都没关系,因为后续会动态改变)

void usart(void) interrupt 4         
{      
   if(1==RI)
   {
      RI = 0;

           if(0==Gu8FinishFlag)
           {
                  Gu8ReceFeedDog=1;
                  switch(Gu8ReceStep)
                  {
                          case 0:   //“前部分的”数据头。接头暗号的步骤
                                   Gu8ReceBuffer=SBUF;
                                   if(0xeb==Gu8ReceBuffer)
                                   {
                                          Gu32ReceCnt=1;
                                          Gu8ReceStep=1;
                                   }
                                   break;               
                                       
                          case 1:   //“前部分的”数据类型和长度
                                   Gu8ReceBuffer=SBUF;
                                   Gu32ReceCnt++;
                                   if(Gu32ReceCnt>=6)//前6个数据。接收完了“数据类型”和“数据长度”。
                                   {
                                          Gu8ReceType=Gu8ReceBuffer;//提取“数据类型”
                                                pu32Data=(unsigned long *)&Gu8ReceBuffer;
                                                Gu32ReceDataLength=*pu32Data; //提取“数据长度”
                                          if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
                                                {
                                                        Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                                        Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                                }
                                                else   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
                                                {
                           //以下几行代码是本节的破题关键!!!
                           if(0x02==Gu8ReceType) //如果是文件类,把指针关联到Gu8FileBuffer
                              {
                                 pGu8ReceBuffer=(unsigned char *)&Gu8FileBuffer;//下标0
Gu32ReceCntMax=40+6;//最大缓存
}
else //如果是常规类,继续把指针关联到Gu8ReceBuffer本身的数组
                              {
                                 pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer;//下标6
Gu32ReceCntMax=20;//最大缓存
}

                                                        Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                       
                                   }
                                   break;               
                          case 2:   //“后部分的”数据
                                   pGu8ReceBuffer=SBUF; //这里的指针就是各种不同内存的化身!!!
                                   Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                   if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
{
                                          Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                          Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                   }
                                   break;       
                  }
       }
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}



【131.2   通信协议。】

      数据头(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,依次循环。这是数据校验的一种方式。
       异或(0B)。“异或”放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 0B。其中最后一个字节0B就是“异或”字节,前面所有字节相“异或”等于十六进制的0B。验证“异或”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”,该计算器软件的异或运算按键是“Xor”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“异或”,把自己计算得到的“异或”与接收到的最后一个字节的“异或”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【131.3   程序例程。】


   
       上图131.3.1有源蜂鸣器电路





       上图131.3.2232串口电路

程序功能如下:
      (1)单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。数据类型为01时,把“后部分的”数据发送给“常规控制类内存”;数据类型为02时,把“后部分的”数据发送给“文件类内存”。
      (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 代表前面所有字节的异或结果。
比如:
      数据类型01,“后部分的”数据发给“常规控制类内存”,让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 0B
      数据类型02,“后部分的”数据发给“文件类内存”,让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 02 00 00 00 0B 00 64 00 02 84






#include "REG52.H"

#define RECE_TIME_OUT    2000//通信过程中字节之间的超时时间2000ms
#define REC_BUFFER_SIZE20    //常规控制类数组的长度
#define FILE_BUFFER_SIZE40   //文件类数组的长度


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 char Gu8FileBuffer; //文件类的大内存
unsigned char *pGu8ReceBuffer;//用来切换接收内存的“中转指针”

unsigned long Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存
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 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   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
                                                {
                           //以下几行代码是本节的破题关键!!!
                           if(0x02==Gu8ReceType) //如果是文件类,把指针关联到Gu8FileBuffer
                              {
                                 pGu8ReceBuffer=(unsigned char *)&Gu8FileBuffer;//下标0
Gu32ReceCntMax=FILE_BUFFER_SIZE+6;//最大缓存
}
else //如果是常规类,继续把指针关联到Gu8ReceBuffer本身的数组
                              {
                                 pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer;//下标6
Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存
}

                                                        Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                       
                                   }
                                   break;               
                          case 2:   //“后部分的”数据
                                   pGu8ReceBuffer=SBUF; //这里的指针就是各种不同内存的化身!!!
                                   Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                   if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
{
                                          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;//计算的“异或”



    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=Gu8ReceBuffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
{
Su8RecZZ=Su8RecZZ^Gu8ReceBuffer;   //计算“异或”
}

if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
{
                              pSu16Data=(unsigned int *)&Gu8ReceBuffer; //数据转换。
                                     Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
vGu8BeepTimerFlag=1;
}
       
                  break;

                        case 0x02:   //文件类的大内存。驱动蜂鸣器。
//以下的数据转换,在第62节讲解过的指针法

                               pSu16Data=(unsigned int *)&Gu8ReceBuffer; //数据转换
Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

//注意,请留意以下代码文件类内存数组Gu8FileBuffer的下标位置
Gu8ReceZZ=Gu8FileBuffer;//提取接收到的“异或”

//前面6个字节是“前部分的”[数据头,数据类型,数据长度]
Su8RecZZ=Gu8ReceBuffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<6;i++) //注意,这里是从第“i=1”个数据开始
{
Su8RecZZ=Su8RecZZ^Gu8ReceBuffer;   //计算“前部分的”“异或”
}

//6个字节之后是“后部分的”“文件类专用的数据”
for(i=0;i<(Gu32ReceDataLength-1-6);i++)
{
Su8RecZZ=Su8RecZZ^Gu8FileBuffer;   //计算“后部分的”“异或”
}

if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
{
                              pSu16Data=(unsigned int *)&Gu8FileBuffer; //数据转换。此处下标0!
                                     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();
                   }

}
          }       
}






jianhong_wu 发表于 2018-10-30 11:24:12

本帖最后由 jianhong_wu 于 2018-10-30 13:06 编辑

第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架。

【132.1   字节间隔时间、双缓存切换、指针切换关联。】

       在一些通讯模块的项目中,常常涉及数据的转发,透传,提取关键字的处理,单片机接收到的数据不许随意丢失,必须全部暂存,然后提取关键字,再把整包数据原封不动地转发或者透传给“下家”。这类项目的特点是,通讯协议不是固定唯一的,因此,前面章节那种接头暗号(数据头)的程序框架不适合这里,本节跟大家分享另外一种程序框架。
       第一个要突破的技术难点是,既然通讯协议不是固定唯一的,那么,如何识别一串数据已经接收完毕?答案是靠接收每个字节之间的间隔时间来识别。当一串数据正在接收时,每个字节之间的间隔时间是“短暂的相对均匀的”。当一串数据已经接收完毕时,每个字节之间的间隔时间是“突然变长的”。代码的具体实现,是靠一个软件定时器,模拟单片机“看门狗”的“喂狗”原理。
       第二个要突破的技术难点是,既然通讯协议不是固定唯一的,数据内容带有随机性,甚至字节之间的间隔时间的长短也带有随机性和不确定性,那么,如何预防正在处理数据时突然“接收中断”又接收到的新数据覆盖了尚未来得及处理的旧数据,或者,如何预防正在处理旧数据时丢失了突然又新过来的本应该接收的新数据?答案是用双缓存轮流切换的机制。双缓存,一个用在处理刚刚接收到的旧数据,另一个用在时刻准备着接收新数据,轮流切换,两不误。
       第三个要突破的技术难点是,既然是用双缓存轮流切换的机制,那么,在主程序里如何统一便捷地处理两个缓存的数组?这里的“统一”是关键,要把两个数组“统一”成(看成是)一个数组,方法是,只需用“指针切换关联”的技术就可以了。

【132.2   程序例程。】


      
       上图132.2.1有源蜂鸣器电路





       上图132.2.2232串口电路

      程序功能如下:单片机接收任意长度(最大一次不超过30字节)的一串数据。如果发现连续有三个字节是0x02 0x03 0x04,蜂鸣器则“短叫”100ms提示;如果发现连续有四个字节是0x06 0x07 0x08 0x09,蜂鸣器则“长叫”2000ms提示。
       比如测试“短叫”100ms,发送十六进制的数据串:05 02 00 00 02 03 04 09
       比如测试“长叫”2000ms,发送十六进制的数据串:02 02 06 07 08 09 01 08 03 00 05
       代码如下:
#include "REG52.H"

#define DOG_TIME_OUT20//理论上,9600波特率的字节间隔时间大概0.8ms左右,因此取20ms足够
#define RECE_BUFFER_SIZE30   //接收缓存的数组大小

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;

unsigned char Gu8CurrentReceBuffer_Sec=0; //当前接收缓存的选择标志。0代表缓存A,1代表缓存B

unsigned char Gu8ReceBuffer_A; //双缓存其中之一的缓存A
unsigned long Gu32ReceCnt_A=0;    //缓存A的数组下标与计数器,必须初始化为0,做好接收准备

unsigned char Gu8ReceBuffer_B; //双缓存其中之一的缓存B
unsigned long Gu32ReceCnt_B=0;    //缓存B的数组下标与计数器,必须初始化为0,做好接收准备

unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8FinishFlag=0; //接收完成标志。0代表还没有完成,1代表已经完成了一次接收

volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

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

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   UsartTask();   //串口接收的任务函数
    }
}

void usart(void) interrupt 4         
{      
   if(1==RI)
   {
      RI = 0;

Gu8FinishFlag=0; //此处也清零,意味深长,当主函数正在处理数据时,可以兼容多次接收完成
          Gu8ReceFeedDog=1; //看门狗的“喂狗”操作,给软件定时器继续“输血”
if(0==Gu8CurrentReceBuffer_Sec)   //0代表选择缓存A
{
      if(Gu32ReceCnt_A<RECE_BUFFER_SIZE)
{
Gu8ReceBuffer_A=SBUF;
Gu32ReceCnt_A++; //记录当前缓存A的接收字节数
}
}
else   //1代表选择缓存B
{
      if(Gu32ReceCnt_B<RECE_BUFFER_SIZE)
{
Gu8ReceBuffer_B=SBUF;
Gu32ReceCnt_B++;//记录当前缓存B的接收字节数
}

}
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
      //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}

void UsartTask(void)    //串口接收的任务函数,放在主函数内
{
static unsigned char *pSu8ReceBuffer;//“指针切换关联”中的指针,切换内存
static unsigned char Su8Lock=0;//用来避免一直更新的临时变量
static unsigned long i;//用在数据处理中的循环变量
static unsigned long Su32ReceSize=0; //接收到的数据大小的临时变量


    if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
        {
                Gu8ReceFeedDog=0;

Su8Lock=0; //解锁。用来避免一直更新的临时变量
               
      //以下三行代码是看门狗中的“喂狗”操作。继续给软件定时器“输血”               
                vGu8ReceTimeOutFlag=0;
      vGu16ReceTimeOutCnt=DOG_TIME_OUT;//正在通信时,两个字节间隔的最大时间,本节选用20ms
                vGu8ReceTimeOutFlag=1;
        }
        else if(0==Su8Lock&&0==vGu16ReceTimeOutCnt) //超时,代表一串数据已经接收完成
        {
          Su8Lock=1;//避免一直进来更新
      Gu8FinishFlag=1; //两个字节之间的时间超时,因此代表了一串数据已经接收完成
    }

       
        if(1==Gu8FinishFlag)//1代表已经接收完毕一串新的数据,需要马上去处理
        {
if(0==Gu8CurrentReceBuffer_Sec)
{
Gu8CurrentReceBuffer_Sec=1; //以最快的速度先切换接收内存,避免丢失新发过来的数据
//Gu32ReceCnt_B=0;//这里不能清零缓存B的计数器,意味深长,避免此处临界点发生中断
            Gu8FinishFlag=0;//尽可能以最快的速度清零本次完成的标志,为下一次新数据做准备
pSu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer_A; //关联刚刚接收的数据缓存
Su32ReceSize=Gu32ReceCnt_A; //记录当前缓存的有效字节数
Gu32ReceCnt_A=0; //及时把当前缓存计数清零,为一次切换接收缓存做准备。意味深长。
}
else
{
Gu8CurrentReceBuffer_Sec=0; //以最快的速度先切换接收内存,避免丢失新发过来的数据
//Gu32ReceCnt_A=0;//这里不能清零缓存A的计数器,意味深长,避免此处临界点发生中断
            Gu8FinishFlag=0;//尽可能以最快的速度清零本次完成的标志,为下一次新数据做准备
pSu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer_B; //关联刚刚接收的数据缓存
Su32ReceSize=Gu32ReceCnt_B; //记录当前缓存的有效字节数
Gu32ReceCnt_B=0; //及时把当前缓存计数清零,为一次切换接收缓存做准备。意味深长。
}

      //Gu8FinishFlag=0; //之所以不选择在这里清零,是因为在上面清零更及时快速。意味深长。

      //开始处理刚刚接收到的一串新数据,直接“统一”处理pSu8ReceBuffer指针为代表的数据即可
      for(i=0;i<Su32ReceSize;i++)
{
             if(0x02==pSu8ReceBuffer&&
0x03==pSu8ReceBuffer&&
0x04==pSu8ReceBuffer) //连续三个数是0x02 0x03 0x04
{
vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=100;   //让蜂鸣器“短叫”100ms
vGu8BeepTimerFlag=1;
    return; //直接退出当前函数
}

             if(0x06==pSu8ReceBuffer&&
0x07==pSu8ReceBuffer&&
0x08==pSu8ReceBuffer&&
0x09==pSu8ReceBuffer) //连续四个数是0x06 0x07 0x08 0x09
{
vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=2000;   //让蜂鸣器“长叫”2000ms
vGu8BeepTimerFlag=1;
    return; //直接退出当前函数
}

}

    }
}

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();
                   }

}
          }       
}



jianhong_wu 发表于 2018-11-14 11:59:40

本帖最后由 jianhong_wu 于 2018-11-14 12:34 编辑

第一百三十三节:常用的三种串口发送函数。

【133.1   发送单字节的底层驱动函数。】

       单片机内置的“独立硬件串口模块”能直接实现“发送一个字节数据”的基础功能,因此,发送单字节的函数是应用层与硬件层的最小单位的接口函数,也称为底层驱动函数。应用层再复杂的发送函数都基于此最小单位的接口函数来实现。单片机应用层与“独立硬件串口模块”之间的接口通信是靠寄存器SBUF作为中间载体的,要实现发送单字节的最小接口函数,有如下三个关键点。
       第一个,单片机应用层如何知道“硬件模块”已经发送完了一个字节,靠什么来识别?答:在初始化函数里,可以把“硬件模块”配置成,每发送完一个字节后都产生一次发送中断,在发送中断函数里让一个全局变量从0变成1,依此全局变量作为识别是否已经发送完一个字节的标志。
       第二个,发送一个字节数据的时候,如果“硬件模块”通讯异常,没有按预期产生发送中断,单片机就会一直处于死循环等待“完成标志”的状态,怎么办?答:在等待“完成标志”的时候,加入超时处理的机制。
       第三个,在连续发送一堆数据时,如果接收方(或者上位机)发现有丢失数据的时候,如何调节此发送函数?答:可以根据实际调试的结果,如果接收方发现丢失数据,可以尝试在每发送一个字节之后插入一个Delay延时,延时的时间长度根据实际调试为准。我个人的经验中,感觉stm32这类M3核或者M4核的单片机在发送一个字节的时候只需判断是否发送完成的标志位即可,不需要插入Delay延时。但是在其它某些个别厂家单片机的串口发送数据中,是需要插入Delay延时作为调节,否则在连续发送一堆数据时会丢失数据,这个,应该以实际调试项目为准。
       片段的讲解代码如下:

unsigned char Gu8ReceData;
unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志
void usart(void) interrupt 4   //串口的中断函数      
{      
      if(1==RI)
      {
            RI = 0;
Gu8ReceData=SBUF;
}
      else//发送数据引起的中断
      {
         TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
       Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
      }                                                      
}

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;//超时处理的延时计时器

    Gu8SendByteFinish=0;//在发送一个字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;//超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)//超时处理
{
    if(1==Gu8SendByteFinish)
{
    break;//如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;//超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}


【133.2   发送任意起始位置任意长度的函数。】

       要连续发送一堆数据,必须先把这堆数据封装成一个数组,然后编写一个发送数组的函数。该函数内部是基于“发送单字节的最小接口函数”来实现的。该函数对外通常需要两个接口,一个是数组的任意起始位置,一个发送的数据长度。数组的任意起始位置只需靠指针即可实现。片段的讲解代码如下:

//任意数组
unsigned char Gu8SendBuffer={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize)
{
    static unsigned long i;
    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendBuffer);//基于“发送单字节的最小接口函数”来实现的
    }
}

void main()
{
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer,5);//从第0位置发送5个数据
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer,5);//从第6位置发送5个数据
    while(1)
{

    }
}


【133.3   发送带协议的函数。】

       前面章节中,我们讲过接收“带固定协议”的程序框架,这类“带固定协议”的数据串里本身就自带了“数据的长度”,因此,要编程一个发送带协议的函数,关键在于,在函数内部根据协议先提取整串数据的有效长度。该函数对外通常也需要两个接口,一个是数组的起始位置,一个发送数据的最大限制长度。最大限制长度的作用是用来防止数组越界,增强程序的安全性。片段的讲解代码如下:

//“固定协议”十六进制的数据格式:EB 01 00 00 00 0B 03 E8 00 01 0B 。其中:
    // EB是数据头。
// 01是代表数据类型。
// 00 00 00 0B代表数据长度是11个(十进制)。
// 03 E8 00 01 0B代表其它数据

//“带固定协议”的数组
unsigned char Gu8SendMessage={0xEB,0x01,0x00,0x00,0x00,0x0B,0x03,0xE8,0x00,0x01,0x0B};

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage;
u32SendSize=*pSu32;//从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;//数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage); //基于“发送单字节的最小接口函数”来实现的
    }
}

void main()
{
    UsartSendMessage((const unsigned char *)&Gu8SendMessage,100); //必须从第0位置发送
    while(1)
{

    }
}




【133.4   程序例程。】


       上图133.4.1232串口电路

       程序功能如下:
       单片机上电瞬间,直接发送三串数据。
       第一串是十六进制的任意数据:00 01 02 03 04
       第二串是十六进制的任意数据:06 07 08 09 0A
       第三串是十六进制的“带协议”数据:EB 01 00 00 00 0B 03 E8 00 01 0B
       波特率9600,校验位NONE(无),数据位8,停止位1。在电脑的串口助手软件里,设置接收显示的为“十六进制”(HEX模式),即可观察到发送的三串数据。
       代码如下:

#include "REG52.H"

void UsartSendByteData(unsigned char u8SendData); //发送一个字节的底层驱动函数

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize);

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize);

void usart(void);//串口接收的中断函数
void SystemInitial(void);
void Delay(unsigned long u32DelayTime);
void PeripheralInitial(void);

unsigned char Gu8ReceData;
unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志

//任意数组
unsigned char Gu8SendBuffer={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};

//“固定协议”十六进制的数据格式:EB 01 00 00 00 0B 03 E8 00 01 0B 。其中:
// EB是数据头。
// 01是代表数据类型。
// 00 00 00 0B代表数据长度是11个(十进制)。
// 03 E8 00 01 0B代表其它数据
//“带固定协议”的数组
unsigned char Gu8SendMessage={0xEB,0x01,0x00,0x00,0x00,0x0B,0x03,0xE8,0x00,0x01,0x0B};

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();    //在此函数内部调用了发送的三串数据
    while(1)
{
    }
}

void usart(void) interrupt 4   //串口的中断函数      
{      
      if(1==RI)
      {
            RI = 0;
Gu8ReceData=SBUF;
}
      else//发送数据引起的中断
      {
         TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
       Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
      }                                                      
}

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;//超时处理的延时计时器

    Gu8SendByteFinish=0;//在发送以字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;//超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)//超时处理
{
    if(1==Gu8SendByteFinish)
{
    break;//如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;//超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize)
{
    static unsigned long i;
    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendBuffer);//基于“发送单字节的最小接口函数”来实现的
    }
}

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage;
u32SendSize=*pSu32;//从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;//数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage); //基于“发送单字节的最小接口函数”来实现的
    }
}

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)
{
    //发送任意数组
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer,5);//从第0位置发送5个数据
UsartSendBuffer((const unsigned char *)&Gu8SendBuffer,5);//从第6位置发送5个数据

//发送带协议的数组
    UsartSendMessage((const unsigned char *)&Gu8SendMessage,100); //必须从第0位置发送
}






jianhong_wu 发表于 2019-2-2 12:29:45

本帖最后由 jianhong_wu 于 2019-2-2 13:06 编辑

第一百三十四节:“应用层半双工”双机串口通讯的程序框架。

【134.1   应用层的“半双工”和“全双工”。】

       应用层的“半双工”。主机与从机在程序应用层采用“一问一答”的查询模式,主机是主动方,从机是被动方,主机问一句从机答一句,“聊天对话“的氛围很无趣很呆板。从机没有发言权,当从机想主动给主机发送一些数据时就“憋得慌”。半双工适用于大多数单向通讯的场合。
       应用层的“全双工”。主机与从机在程序应用层可以实现任意双向的通讯,这时从机也可变为主机,主机也可变为从机,就像两个人平时聊天,无所谓谁是从机谁是主机,也无所谓非要对方对我每句话都要应答附和(只要对方能听得清我讲什么就可以),“聊天对话”的氛围很生动很活泼。全双工适用于通讯更复杂的场合。
       本节从“半双工“开始讲,让初学者先熟悉双机通讯的基本程序框架,下一节再讲“全双工“。

【134.2   双机通讯的三类核心函数。】

       双机通讯在程序框架层面有三类核心的涵数,它们分别是:通讯过程的控制涵数,发送的队列驱动涵数,接收数据后的处理涵数。
       “通讯过程的控制涵数”的数量可以不止1个,每一个通讯事件都对应一个独立的“通讯过程的控制涵数”,根据通讯事件的数量,一个系统往往有N个“通讯过程的控制涵数”。顾名思义,它负责过程的控制,无论什么项目,凡是过程控制我都首选switch语句。此函数是属于上层应用的函数,它的基础底层是“发送的队列驱动涵数”和“接收数据后的处理涵数”这两个函数。
       “发送的队列驱动涵数”在系统中只有1个“发送的队列驱动涵数”,负责“通讯管道的占用”的分配,负责数据的具体发送。当同时存在很多“待发送”的请求指令时,此函数会根据“if ,else if...”的优先级,像队列一样安排各指令发送的先后顺序,确保各指令不会发生冲突。此函数属于底层的驱动函数。
       “接收数据后的处理涵数”在系统中只有1个,负责处理当前接收到的数据,它既属于“底层函数”也属于“应用层函数”,二者成分皆有。
       我们一旦深刻地领悟了这三类函数各自的分工与关联方式,将来应付再复杂的通讯系统都会脉络清析,游刃有余。

【134.3   例程的功能需求。】

       上位机与下位机都有一个一模一样的57个字节的大数组。在上位机端按下独立按键K1后,上位机开始与下位机建立通讯,上位机的目的是读取下位机的那57个字节的大数组,分批读取,每批读取10个字节,最后一批读取的是余下的7个字节。读取完毕后,上位机把读取到的大数组与自己的大数组进行对比:如果相等,表示通讯正确,蜂鸣器“长鸣”一声;如果不相等,表示通讯错误,蜂鸣器“短鸣”一声。在通讯过程中,如果出现通信异常(比如因为接收超时或者接收某批次数据错误而导致重发的次数超过最大限制的次数)也表示通讯错误,蜂鸣器也会发出“短鸣”一声的提示。

【134.4   例程的电路图。】

      两个单片机进行232串口通讯,一共需要3根线:1根作为共地线,其它2根是交叉的收发数据线(上位机的“接收线”连接下位机的“发送线”,上位机的“发送线”连接下位机的“接收线”),如下图所示:

       上图134.4.1双机通讯的232串口接线图



       上图134.4.2上位机的独立按键



       上图134.4.3 上位机的有源蜂鸣器

【134.5   例程的通讯协议。】

(一)通讯参数。波特率9600,校验位NONE(无),数据位8,停止位1。

(二)上位机读取下位机的数组容量的大小的指令。
      (1)上位机发送十六进制的数据:EB 01 00 00 00 07 ED。
         EB是数据头。
         01是指令类型,01代表请求下位机返回大数组的容量大小。
         00 00 00 07代表整个指令的数据长度。
         ED是前面所有字节数据的异或结果,用来作为校验数据。

       (2)下位机返回十六进制的数据:EB 01 00 00 00 0C XX XX XX XX ZZ。
         EB是数据头。
         01是指令类型,01代表返回大数组的容量大小。
         00 00 00 0B代表整个指令的数据长度
         XX XX XX XX代表大数组的容量大小
         ZZ是前面所有字节数据的异或结果,用来作为校验数据。

(三)上位机读取下位机的大数组的分段数据的指令。
       (1)上位机发送十六进制的数据:EB 02 00 00 00 0F RR RR RR RR YY YY YY YY ZZ
         EB是数据头
         02是指令类型,02代表请求下位机返回当前分段的数据。
         00 00 00 0F代表整个指令的数据长度
         RR RR RR RR代表请求下位机返回的数据的“请求起始地址”
         YY YY YY YY代表请求下位机从“请求起始地址”一次返回的数据长度
         ZZ是前面所有字节数据的异或结果,用来作为校验数据。

      (2)下位机返回十六进制的数据:EB 02 TT TT TT TT RR RR RR RR YY YY YY YY HH ...HH ZZ
      EB是数据头
      02是指令类型,02代表返回大数组当前分段的数据
      TT TT TT TT 代表整个指令的数据长度
      RR RR RR RR代表下位机返回数据时的“请求起始地址”
      YY YY YY YY代表下位机从“请求起始地址”一次返回的数据长度
      HH ...HH代表中间有效的数据内容
      ZZ是前面所有字节数据的异或结果,用来作为校验数据。

【134.6   解决本节例程编译不过去的方法。】

      因为本节用到的全局变量比较多,如果有初学者在编译的时候出现“error C249: 'DATA': SEGMENT TOO LARGE”的提示,请按下图的窗口提示来设置一下编译的环境。



       上图134.5.1 设置编译的环境

【134.7   例程的上位机程序。】
#include "REG52.H"

#define RECE_TIME_OUT    2000//通讯过程中字节之间的超时时间2000ms
#define REC_BUFFER_SIZE30    //常规控制类数组的长度
#define KEY_FILTER_TIME25    //按键滤波的“稳定时间”

void usart(void);//串口接收的中断函数
void T0_time();    //定时器的中断函数

void BigBufferUsart(void);//读取下位机大数组的“通讯过程的控制涵数”。三大核心函数之一
void QueueSend(void);       //发送的队列驱动涵数。三大核心函数之一
void ReceDataHandle(void);//接收数据后的处理涵数。三大核心函数之一

void UsartTask(void);    //串口收发的任务函数,放在主函数内

unsigned char CalculateXor(const unsigned char *pCu8Buffer, //异或的算法函数
unsigned long u32BufferSize);

//比较两个数组的是否相等。返回1代表相等,返回0代表不相等
//u32BufferSize是参与对比的数组的大小
unsigned char CmpTwoBufferIsSame(const unsigned char *pCu8Buffer_1,
const unsigned char *pCu8Buffer_2,
unsigned long u32BufferSize);

void UsartSendByteData(unsigned char u8SendData); //发送一个字节的底层驱动函数

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize);

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 KEY_INPUT1=P2^2;//K1按键识别的输入口。

//下面表格数组的数据与下位机的表格数据一模一样,目的用来检测接收到的数据是否正确
code unsigned char Cu8TestTable[]=
{
0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,
0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,
0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,
0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,
0x51,0x52,0x53,0x54,0x55,0x56,0x57
};

unsigned char Gu8ReceTable; //从下位机接收到的表格数据的数组

//把一些针对某个特定事件的全局变量放在一个结构体内,可以让全局变量的分类更加清晰
struct StructBigBufferUsart   //控制读取大数组的通讯过程的结构体
{
unsigned char u8Status; //通讯过程的状态 0为初始状态 1为通讯成功 2为通讯失败
unsigned char u8ReSendCnt; //重发计数器
unsigned char u8Step;   //通讯过程的步骤
unsigned char u8Start;//通讯过程的启动
unsigned long u32NeedSendSize;    //一共需要发送的全部数据量
unsigned long u32AlreadySendSize; //实际已经发送的数据量
unsigned long u32CurrentAddr; //当前批次需要发送的起始地址
unsigned long u32CurrentSize; //当前批次从起始地址开始发送的数据量
unsigned char u8QueueSendTrig;//队列驱动函数的发送的启动
unsigned char u8QueueSendBuffer; //队列驱动函数的发送指令的数组
unsigned char u8QueueStatus; //队列驱动函数的通讯状态 0为初始状态 1为通讯成功 2为通讯失败
};

unsigned char Gu8QueueReceUpdate=0;//1代表“队列发送数据后,收到了新的数据”

struct StructBigBufferUsartGtBigBufferUsart;//此结构体变量专门用来控制读取大数组的通讯事件

volatile unsigned char vGu8BigBufferUsartTimerFlag=0;//过程控制的超时定时器
volatile unsigned int vGu16BigBufferUsartTimerCnt=0;

volatile unsigned char vGu8QueueSendTimerFlag=0;//队列发送的超时定时器
volatile unsigned int vGu16QueueSendTimerCnt=0;

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

volatile unsigned char vGu8KeySec=0;

unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志

unsigned char Gu8ReceBuffer; //常规控制类的小内存
unsigned char *pGu8ReceBuffer;//用来切换接收内存的“中转指针”

unsigned long Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存
unsigned long Gu32ReceCnt=0;//接收缓存数组的下标
unsigned char Gu8ReceStep=0;//接收中断函数里的步骤变量
unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8ReceType=0; //接收的数据类型
unsigned char Gu8Rece_Xor=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();   //串口收发的任务函数
KeyTask();   
    }
}

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

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键。K1的独立按键
//GtBigBufferUsart.u8Start在开机初始化函数里必须初始化为0!这一步很关键!
            if(0==GtBigBufferUsart.u8Start) //只有在还没有启动的情况下,才能启动
{
GtBigBufferUsart.u8Status=0; //通讯过程的状态 0为初始状态
GtBigBufferUsart.u8Step=0;   //通讯过程的步骤 0为从当前开始的步骤
GtBigBufferUsart.u8Start=1;//通讯过程的启动
}
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}
}

/* 注释一:
*每一个通讯事件都对应的一个独立的“通讯过程的控制涵数”,一个系统中有多少个通讯事件,就存在
*多少个“通讯过程的控制涵数”。该函数负责某个通讯事件从开始到结束的整个过程。比如本节项目,
*在通讯过程中,如果发现接收到的数据错误,则继续启动重发的机制。当发现接收到的累加字节数等于
*预期想要接收的数量时,则结束这个通讯的事件。
*/

void BigBufferUsart(void)//读取下位机大数组的“通讯过程的控制涵数”
{
    static const unsigned char SCu8ReSendCntMax=3; //重发的次数
static unsigned long *pSu32Data; //用于数据与数组转换的指针

switch(GtBigBufferUsart.u8Step)//过程控制,我首选switch语句!
{
    case 0:
         if(1==GtBigBufferUsart.u8Start) //通讯过程的启动
         {
               //根据实际项目需要,在此第0步骤里可以添加一些初始化相关的数据
                  GtBigBufferUsart.u8ReSendCnt=0; //重发计数器清零

                  GtBigBufferUsart.u8Step=1;   //切换到下一步
}
         break;

//-----------先发送“读取下位机的数组容量的大小的指令”---------------------
//-----------EB 01 00 00 00 07 ED                      ---------------------
    case 1:
         GtBigBufferUsart.u8QueueSendBuffer=0xeb; //数据头
         GtBigBufferUsart.u8QueueSendBuffer=0x01; //数据类型 读取数组容量大小
         pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
         *pSu32Data=7; //数据长度本条指令的数据总长是7个字节
//异或算法的函数
GtBigBufferUsart.u8QueueSendBuffer=CalculateXor(GtBigBufferUsart.u8QueueSendBuffer,
6);//最后一个字节不纳入计算

             //队列驱动函数的状态 0为初始状态 1为通讯成功 2为通讯失败
             GtBigBufferUsart.u8QueueStatus=0; //队列驱动函数的通讯状态
             GtBigBufferUsart.u8QueueSendTrig=1;//队列驱动函数的发送的启动


             vGu8BigBufferUsartTimerFlag=0;
vGu16BigBufferUsartTimerCnt=2000;   
             vGu8BigBufferUsartTimerFlag=1;   //过程控制的超时定时器的启动

             GtBigBufferUsart.u8Step=2;   //切换到下一步
         break;

    case 2://发送之后,等待下位机返回的数据的状态
         if(1==GtBigBufferUsart.u8QueueStatus) //当前批次的接收到的数据成功
         {
                  GtBigBufferUsart.u8ReSendCnt=0; //重发计数器清零
                  GtBigBufferUsart.u32AlreadySendSize=0; //实际已经发送的数据量清零
                  GtBigBufferUsart.u32CurrentAddr=0;//当前批次需要发送的起始地址
                  GtBigBufferUsart.u32CurrentSize=10; //从当前批次起始地址开始发送的数据量
                  GtBigBufferUsart.u8Step=3;   //切换到下一步
         }
         else if(2==GtBigBufferUsart.u8QueueStatus) //当前批次的接收到的数据失败
         {
            GtBigBufferUsart.u8ReSendCnt++;
            if(GtBigBufferUsart.u8ReSendCnt>=SCu8ReSendCntMax) //大于最大的重发次数
            {
GtBigBufferUsart.u8Step=0;
GtBigBufferUsart.u8Start=0; //结束当前的过程通讯
GtBigBufferUsart.u8Status=2; //对外宣布“通讯失败”
                      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=30;   //让蜂鸣器“短鸣”一声
vGu8BeepTimerFlag=1;
}
else
{
                      GtBigBufferUsart.u8Step=1;   //返回上一步,重发当前段的数据
}
         }
         else if(0==vGu16BigBufferUsartTimerCnt) //当前批次在等待接收返回数据时,超时
         {
            GtBigBufferUsart.u8ReSendCnt++;
            if(GtBigBufferUsart.u8ReSendCnt>=SCu8ReSendCntMax) //大于最大的重发次数
            {
GtBigBufferUsart.u8Step=0;
GtBigBufferUsart.u8Start=0; //结束当前的过程通讯
GtBigBufferUsart.u8Status=2; //对外宣布“通讯失败”

                      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=30;   //让蜂鸣器“短鸣”一声
vGu8BeepTimerFlag=1;
}
else
{
                      GtBigBufferUsart.u8Step=1;   //返回上一步,重发当前段的数据
}
         }
         break;

//-----------接着发送“读取下位机的大数组的分段数据的指令”---------------------
//-----------EB 02 00 00 00 0F RR RR RR RR YY YY YY YY ZZ---------------------
    case 3:
         GtBigBufferUsart.u8QueueSendBuffer=0xeb; //数据头
         GtBigBufferUsart.u8QueueSendBuffer=0x02; //数据类型 读取分段数据
         pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
         *pSu32Data=15; //数据长度本条指令的数据总长是15个字节

         pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
         *pSu32Data=GtBigBufferUsart.u32CurrentAddr; //当前批次需要发送的起始地址

         pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
         *pSu32Data=GtBigBufferUsart.u32CurrentSize; //从当前批次起始地址发送的数据量

//异或算法的函数
GtBigBufferUsart.u8QueueSendBuffer=CalculateXor(GtBigBufferUsart.u8QueueSendBuffer,
14);//最后一个字节不纳入计算

             //队列驱动函数的状态 0为初始状态 1为通讯成功 2为通讯失败
             GtBigBufferUsart.u8QueueStatus=0; //队列驱动函数的通讯状态
             GtBigBufferUsart.u8QueueSendTrig=1;//队列驱动函数的发送的启动

             vGu8BigBufferUsartTimerFlag=0;
vGu16BigBufferUsartTimerCnt=2000;   
             vGu8BigBufferUsartTimerFlag=1;   //过程控制的超时定时器的启动

             GtBigBufferUsart.u8Step=4;   //切换到下一步
         break;

    case 4: //发送之后,等待下位机返回的数据的状态
         if(1==GtBigBufferUsart.u8QueueStatus) //当前批次的接收到的数据成功
         {
            //更新累加当前实际已经发送的字节数
GtBigBufferUsart.u32AlreadySendSize=GtBigBufferUsart.u32AlreadySendSize+
GtBigBufferUsart.u32CurrentSize;

            //更新下一步起始的发送地址
GtBigBufferUsart.u32CurrentAddr=GtBigBufferUsart.u32CurrentAddr+
GtBigBufferUsart.u32CurrentSize;

            //更新下一步从起始地址开始发送的字节数
if((GtBigBufferUsart.u32CurrentAddr+GtBigBufferUsart.u32CurrentSize)>
GtBigBufferUsart.u32NeedSendSize) //最后一段数据的临界点的判断
{
GtBigBufferUsart.u32CurrentSize=GtBigBufferUsart.u32NeedSendSize-
GtBigBufferUsart.u32CurrentAddr;
}
else
{
GtBigBufferUsart.u32CurrentSize=10;
}

                  //判断是否已经把整个大数组的57个字节都已经接收完毕。如果已经接收完毕,则
                  //结束当前通信;如果还没结束,则继续请求下位机发送下一段新数据。
if(GtBigBufferUsart.u32AlreadySendSize>=GtBigBufferUsart.u32NeedSendSize)
{
GtBigBufferUsart.u8Step=0;
GtBigBufferUsart.u8Start=0; //结束当前的过程通讯

if(1==CmpTwoBufferIsSame(Cu8TestTable,    //如果接收的数据与存储的相等
Gu8ReceTable,
                                                57))
                      {
                        vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=1000;   //让蜂鸣器“长鸣”一声
vGu8BeepTimerFlag=1;
GtBigBufferUsart.u8Status=1; //对外宣布“通讯成功”
}
else
{
                        vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=30;   //让蜂鸣器“短鸣”一声
vGu8BeepTimerFlag=1;
GtBigBufferUsart.u8Status=2; //对外宣布“通讯失败”
}

}
else
{
                      GtBigBufferUsart.u8ReSendCnt=0; //重发计数器清零
                      GtBigBufferUsart.u8Step=3;   //返回上一步,继续发下一段的新数据
}

         }
         else if(2==GtBigBufferUsart.u8QueueStatus) //当前批次的接收到的数据失败
         {
            GtBigBufferUsart.u8ReSendCnt++;
            if(GtBigBufferUsart.u8ReSendCnt>=SCu8ReSendCntMax) //大于最大的重发次数
            {
GtBigBufferUsart.u8Step=0;
GtBigBufferUsart.u8Start=0; //结束当前的过程通讯
GtBigBufferUsart.u8Status=2; //对外宣布“通讯失败”

                      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=30;   //让蜂鸣器“短鸣”一声
vGu8BeepTimerFlag=1;
}
else
{
                      GtBigBufferUsart.u8Step=3;   //返回上一步,重发当前段的数据
}
         }
         else if(0==vGu16BigBufferUsartTimerCnt) //当前批次在等待接收返回数据时,超时
         {
            GtBigBufferUsart.u8ReSendCnt++;
            if(GtBigBufferUsart.u8ReSendCnt>=SCu8ReSendCntMax) //大于最大的重发次数
            {
GtBigBufferUsart.u8Step=0;
GtBigBufferUsart.u8Start=0; //结束当前的过程通讯
GtBigBufferUsart.u8Status=2; //对外宣布“通讯失败”

                      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=30;   //让蜂鸣器“短鸣”一声
vGu8BeepTimerFlag=1;
}
else
{
                      GtBigBufferUsart.u8Step=3;   //返回上一步,重发当前段的数据
}
         }
         break;
}

}

/* 注释二:
*整个项目中只有一个“发送的队列驱动涵数”,负责“通讯管道的占用”的分配,负责数据的具体发
*送。当同时存在很多“待发送”的请求指令时,此函数会根据“if ,else if...”的优先级,像队列一
*样安排各指令发送的先后顺序,确保各指令不会发生冲突。
*/

void QueueSend(void)//发送的队列驱动涵数
{
static unsigned char Su8Step=0;

switch(Su8Step)
{
   case 0://分派即将要发送的任务
      if(1==GtBigBufferUsart.u8QueueSendTrig)
      {
            GtBigBufferUsart.u8QueueSendTrig=0;//及时清零。驱动层,不管结果,只发一次。

Gu8QueueReceUpdate=0; //接收应答数据的状态恢复初始值

            //发送带指令的数据
UsartSendMessage((const unsigned char *)&GtBigBufferUsart.u8QueueSendBuffer,
30);

vGu8QueueSendTimerFlag=0;
vGu16QueueSendTimerCnt=2000;
vGu8QueueSendTimerFlag=1;//队列发送的超时定时器
Su8Step=1;
}
//      else if(...)//当有其它发送的指令时,可以在此处继续添加判断,越往下优先级越低
//      else if(...)//当有其它发送的指令时,可以在此处继续添加判断,越往下优先级越低

      break;

   case 1: //发送之后,等待下位机的应答。驱动层,只管有没有应答,不管应答对不对。
      if(1==Gu8QueueReceUpdate) //如果“接收数据后的处理涵数”接收到应答数据
      {
Su8Step=0;//返回上一步继续处理其它“待发送的指令”
}

if(0==vGu16QueueSendTimerCnt) //发送指令之后,等待应答超时
      {
Su8Step=0;//返回上一步继续处理其它“待发送的指令”
}

      break;
}
}


/* 注释三:
*整个项目中只有一个“接收数据后的处理涵数”,负责即时处理当前接收到的数据。
*/

void ReceDataHandle(void)//接收数据后的处理涵数
{
static unsigned long *pSu32Data; //数据转换的指针
static unsigned long i;
static unsigned char Su8Rece_Xor=0;//计算的“异或”

static unsigned long Su32CurrentAddr; //读取的起始地址
static unsigned long Su32CurrentSize; //读取的发送的数据量

    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:   //读取下位机的数组容量的大小
Gu8QueueReceUpdate=1;//告诉“队列驱动函数”收到了新的应答数据

Gu8Rece_Xor=Gu8ReceBuffer;//提取接收到的“异或”
Su8Rece_Xor=CalculateXor(Gu8ReceBuffer,Gu32ReceDataLength-1); //计算“异或”

if(Gu32ReceDataLength>=11&& //接收到的数据长度必须大于或者等于11个字节
Su8Rece_Xor==Gu8Rece_Xor) //验证“异或”,“计算的”与“接收的”是否一致
{
                           pSu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换。
                                     GtBigBufferUsart.u32NeedSendSize=*pSu32Data;//提取将要接收数组的大小
GtBigBufferUsart.u8QueueStatus=1; //告诉“过程控制函数”,当前通讯成功
}
else
{
                      GtBigBufferUsart.u8QueueStatus=2; //告诉“过程控制函数”,当前通讯失败
}
       
                  break;

                        case 0x02:   //读取下位机的分段数据
Gu8QueueReceUpdate=1;//告诉“队列驱动函数”收到了新的应答数据

Gu8Rece_Xor=Gu8ReceBuffer;//提取接收到的“异或”
Su8Rece_Xor=CalculateXor(Gu8ReceBuffer,Gu32ReceDataLength-1); //计算“异或”

                        pSu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换。
                                  Su32CurrentAddr=*pSu32Data;//读取的起始地址

                        pSu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换。
                                  Su32CurrentSize=*pSu32Data;//读取的发送的数据量

if(Gu32ReceDataLength>=11&& //接收到的数据长度必须大于或者等于11个字节
Su8Rece_Xor==Gu8Rece_Xor&& //验证“异或”,“计算的”与“接收的”是否一致
Su32CurrentAddr==GtBigBufferUsart.u32CurrentAddr&& //验证“地址”,相当于验证“动态密匙”
Su32CurrentSize==GtBigBufferUsart.u32CurrentSize) //验证“地址”,相当于验证“动态密匙”
{
                      for(i=0;i<Su32CurrentSize;i++)
                      {
//及时把接收到的数据存储到Gu8ReceTable数组
                           Gu8ReceTable=Gu8ReceBuffer;
}

GtBigBufferUsart.u8QueueStatus=1; //告诉“过程控制函数”,当前通讯成功
}
else
{
                      GtBigBufferUsart.u8QueueStatus=2; //告诉“过程控制函数”,当前通讯失败
}
       
                  break;
      }
      
      Gu8FinishFlag=0;//上面处理完数据再清零标志,为下一次接收新的数据做准备
    }
}

void UsartTask(void)    //串口收发的任务函数,放在主函数内
{
BigBufferUsart();//读取下位机大数组的“通讯过程的控制涵数”
QueueSend();       //发送的队列驱动涵数
ReceDataHandle();//接收数据后的处理涵数
}

void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。

           if(0==Gu8FinishFlag)//1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
           {
                  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   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
                                                {
                           //本节只用到一个接收数组,把指针关联到Gu8ReceBuffer本身的数组
                              pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer;
Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存

                                                       Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                       
                                   }
                                   break;               
                          case 2:   //“后部分的”数据
                                   pGu8ReceBuffer=SBUF; //这里的指针就是各种不同内存的化身!!!
                                   Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                   if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
{
                                          Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                          Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                   }
                                   break;       
                  }
       }
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
    Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
   }                                                      
}

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;//超时处理的延时计时器

    Gu8SendByteFinish=0;//在发送以字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;//超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)//超时处理
{
    if(1==Gu8SendByteFinish)
{
    break;//如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;//超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage;
u32SendSize=*pSu32;//从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;//数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage); //基于“发送单字节的最小接口函数”来实现的
    }
}

unsigned char CalculateXor(const unsigned char *pCu8Buffer, //此处加const代表数组“只读”
unsigned long u32BufferSize)//参与计算的数组的大小
{
unsigned long i;
unsigned char Su8Rece_Xor;
Su8Rece_Xor=pCu8Buffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<u32BufferSize;i++) //注意,这里是从第“i=1”个数据开始
{
Su8Rece_Xor=Su8Rece_Xor^pCu8Buffer;   //计算“异或”
}
    return Su8Rece_Xor;//返回运算后的异或的计算结果
}

//比较两个数组的是否相等。返回1代表相等,返回0代表不相等
unsigned char CmpTwoBufferIsSame(const unsigned char *pCu8Buffer_1,
const unsigned char *pCu8Buffer_2,
unsigned long u32BufferSize)//参与对比的数组的大小
{
unsigned long i;
for(i=0;i<u32BufferSize;i++)
{
   if(pCu8Buffer_1!=pCu8Buffer_2)
   {
          return 0; //只要有一个不相等,则返回0并且退出当前函数
}
}
    return 1; //相等
}


void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1; //1号按键的自锁
   static unsigned intSu16KeyCnt1; //1号按键的计时器

   //1号按键
   if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。
   {
      Su16KeyCnt1++; //累加定时中断次数
      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
      {
         Su8KeyLock1=1;//按键的自锁,避免一直触发
         vGu8KeySec=1;    //触发1号键
      }
   }
}

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

if(1==vGu8BigBufferUsartTimerFlag&&vGu16BigBufferUsartTimerCnt>0) //过程控制的超时定时器
        {
               vGu16BigBufferUsartTimerCnt--;       
}   

if(1==vGu8QueueSendTimerFlag&&vGu16QueueSendTimerCnt>0) //队列发送的超时定时器
        {
               vGu16QueueSendTimerCnt--;       
}   

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)
{
GtBigBufferUsart.u8Start=0;//通讯过程的启动变量必须初始化为0!这一步很关键!
}

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();
                   }

}
          }       
}
【134.8   例程的下位机程序。】

      下位机作为从机应答上位机的指令,程序相对简化了很多。不需要“通讯过程的控制涵数”,直接在“接收数据后的处理涵数”里启动“发送的队列驱动涵数”来发送应答的数据即可。发送应答数据后,也不用等待上位机的应答数据。
#include "REG52.H"

#define RECE_TIME_OUT    2000//通讯过程中字节之间的超时时间2000ms
#define REC_BUFFER_SIZE30    //常规控制类数组的长度

void usart(void);//串口接收的中断函数
void T0_time();    //定时器的中断函数

void QueueSend(void);       //发送的队列驱动涵数
void ReceDataHandle(void);//接收数据后的处理涵数

void UsartTask(void);    //串口收发的任务函数,放在主函数内

unsigned char CalculateXor(const unsigned char *pCu8Buffer, //异或的算法的函数
unsigned long u32BufferSize);

void UsartSendByteData(unsigned char u8SendData); //发送一个字节的底层驱动函数

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize);

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

//下面表格数组的数据与上位机的表格数据一模一样,目的用来让上位机检测接收到的数据是否正确
code unsigned char Cu8TestTable[]=
{
0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,
0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,
0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,
0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,
0x51,0x52,0x53,0x54,0x55,0x56,0x57
};

//把一些针对某个特定事件的全局变量放在一个结构体内,可以让全局变量的分类更加清晰
struct StructBigBufferUsart   //应答读取大数组的通讯过程的结构体
{
unsigned char u8QueueSendTrig;//队列驱动函数的发送的启动
unsigned char u8QueueSendBuffer; //队列驱动函数的发送指令的数组
unsigned char u8QueueStatus; //队列驱动函数的通讯状态 0为初始状态 1为通讯成功 2为通讯失败
};

unsigned char Gu8QueueReceUpdate=0;//1代表“队列发送数据后,收到了新的数据”

struct StructBigBufferUsartGtBigBufferUsart;//此结构体变量专门用来应答读取大数组的通讯事件

volatile unsigned char vGu8QueueSendTimerFlag=0;//队列发送的超时定时器
volatile unsigned int vGu16QueueSendTimerCnt=0;

unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志

unsigned char Gu8ReceBuffer; //常规控制类的小内存
unsigned char *pGu8ReceBuffer;//用来切换接收内存的“中转指针”

unsigned long Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存
unsigned long Gu32ReceCnt=0;//接收缓存数组的下标
unsigned char Gu8ReceStep=0;//接收中断函数里的步骤变量
unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8ReceType=0; //接收的数据类型
unsigned char Gu8Rece_Xor=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();   //串口收发的任务函数
    }
}

/* 注释一:
*整个项目中只有一个“发送的队列驱动涵数”,负责“通讯管道的占用”的分配,负责数据的具体发
*送。当同时存在很多“待发送”的请求指令时,此函数会根据“if ,else if...”的优先级,像队列一
*样安排各指令发送的先后顺序,确保各指令不会发生冲突。
*/

void QueueSend(void)//发送的队列驱动涵数
{
static unsigned char Su8Step=0;

switch(Su8Step)
{
   case 0://分派即将要发送的任务
      if(1==GtBigBufferUsart.u8QueueSendTrig)
      {
            GtBigBufferUsart.u8QueueSendTrig=0;//及时清零。驱动层,不管结果,只发一次。

Gu8QueueReceUpdate=0; //接收应答数据的状态恢复初始值

            //发送带指令的数据
UsartSendMessage((const unsigned char *)&GtBigBufferUsart.u8QueueSendBuffer,
30);
                //注意,这里是从机应答主机的数据,不需要等待返回的数据,因此不需要切换Su8Step
}
//      else if(...)//当有其它发送的指令时,可以在此处继续添加判断,越往下优先级越低
//      else if(...)//当有其它发送的指令时,可以在此处继续添加判断,越往下优先级越低

      break;

   case 1: //发送之后,等待下位机的应答。驱动层,只管有没有应答,不管应答对不对。
      if(1==Gu8QueueReceUpdate) //如果“接收数据后的处理涵数”接收到应答数据
      {
Su8Step=0;//返回上一步继续处理其它“待发送的指令”
}

if(0==vGu16QueueSendTimerCnt) //发送指令之后,等待应答超时
      {
Su8Step=0;//返回上一步继续处理其它“待发送的指令”
}

      break;
}
}


/* 注释二:
*整个项目中只有一个“接收数据后的处理涵数”,负责即时处理当前接收到的数据。
*/

void ReceDataHandle(void)//接收数据后的处理涵数
{
static unsigned long *pSu32Data; //数据转换的指针
static unsigned long i;
static unsigned char Su8Rece_Xor=0;//计算的“异或”

static unsigned long Su32CurrentAddr; //读取的起始地址
static unsigned long Su32CurrentSize; //读取的发送的数据量

    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:   //返回下位机的数组容量的大小

Gu8Rece_Xor=Gu8ReceBuffer;//提取接收到的“异或”
Su8Rece_Xor=CalculateXor(Gu8ReceBuffer,Gu32ReceDataLength-1); //计算“异或”

if(Su8Rece_Xor!=Gu8Rece_Xor) //验证“异或”,如果不相等,退出当前switch
{
    break;    //退出当前switch
}

            GtBigBufferUsart.u8QueueSendBuffer=0xeb; //数据头
            GtBigBufferUsart.u8QueueSendBuffer=0x01; //数据类型 返回数组容量的大小
            pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
            *pSu32Data=11; //数据长度本条指令的数据总长是11个字节

            //提取数组容量的大小
            pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
            *pSu32Data=sizeof(Cu8TestTable);//相当于*pSu32Data=57;sizeof请参考第69节

//异或算法的函数
GtBigBufferUsart.u8QueueSendBuffer=CalculateXor(GtBigBufferUsart.u8QueueSendBuffer,
10);//最后一个字节不纳入计算

                  //队列驱动函数的状态 0为初始状态 1为通讯成功 2为通讯失败
                  GtBigBufferUsart.u8QueueStatus=0; //队列驱动函数的通讯状态
                  GtBigBufferUsart.u8QueueSendTrig=1;//队列驱动函数的发送的启动

Gu8QueueReceUpdate=1;//告诉“队列驱动函数”此发送指令无需等待上位机的应答
                  break;

                        case 0x02:   //返回下位机的分段数据

Gu8Rece_Xor=Gu8ReceBuffer;//提取接收到的“异或”
Su8Rece_Xor=CalculateXor(Gu8ReceBuffer,Gu32ReceDataLength-1); //计算“异或”

if(Su8Rece_Xor!=Gu8Rece_Xor) //验证“异或”,如果不相等,退出当前switch
{
    break;    //退出当前switch
}

                        pSu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换。
                                  Su32CurrentAddr=*pSu32Data;//读取的起始地址

                        pSu32Data=(unsigned long *)&Gu8ReceBuffer; //数据转换。
                                  Su32CurrentSize=*pSu32Data;//读取的发送的数据量

            GtBigBufferUsart.u8QueueSendBuffer=0xeb; //数据头
            GtBigBufferUsart.u8QueueSendBuffer=0x02; //数据类型 返回分段数据

            pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
            *pSu32Data=6+4+4+Su32CurrentSize+1; //数据总长度

            pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
            *pSu32Data=Su32CurrentAddr; //返回接收到的起始地址

            pSu32Data=(unsigned long *)&GtBigBufferUsart.u8QueueSendBuffer;
            *pSu32Data=Su32CurrentSize; //返回接收到的当前批次的数据量

                  for(i=0;i<Su32CurrentSize;i++)
                  {
//装载即将要发送的分段数据
               GtBigBufferUsart.u8QueueSendBuffer=Cu8TestTable;
}

//异或算法的函数
GtBigBufferUsart.u8QueueSendBuffer=
CalculateXor(GtBigBufferUsart.u8QueueSendBuffer, 6+4+4+Su32CurrentSize);

                  //队列驱动函数的状态 0为初始状态 1为通讯成功 2为通讯失败
                  GtBigBufferUsart.u8QueueStatus=0; //队列驱动函数的通讯状态
                  GtBigBufferUsart.u8QueueSendTrig=1;//队列驱动函数的发送的启动

Gu8QueueReceUpdate=1;//告诉“队列驱动函数”此发送指令无需等待上位机的应答
                  break;
      }
      
      Gu8FinishFlag=0;//上面处理完数据再清零标志,为下一次接收新的数据做准备
    }
}

void UsartTask(void)    //串口收发的任务函数,放在主函数内
{
QueueSend();       //发送的队列驱动涵数
ReceDataHandle();//接收数据后的处理涵数
}

void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
{      
   if(1==RI)//接收完一个字节后引起的中断
   {
      RI = 0; //及时清零,避免一直无缘无故的进入中断。

           if(0==Gu8FinishFlag)//1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
           {
                  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   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
                                                {
                           //本节只用到一个接收数组,把指针关联到Gu8ReceBuffer本身的数组
                              pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer;
Gu32ReceCntMax=REC_BUFFER_SIZE;//最大缓存

                                                       Gu8ReceStep=2;   //切换到下一个步骤
                                                }                                                       
                                   }
                                   break;               
                          case 2:   //“后部分的”数据
                                   pGu8ReceBuffer=SBUF; //这里的指针就是各种不同内存的化身!!!
                                   Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

//靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
                                   if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
{
                                          Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
                                          Gu8ReceStep=0;   //及时切换回接头暗号的步骤
                                   }
                                   break;       
                  }
       }
   }
   else//发送数据引起的中断
   {
      TI = 0;//及时清除发送中断的标志,避免一直无缘无故的进入中断。
    Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
   }                                                      
}

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;//超时处理的延时计时器

    Gu8SendByteFinish=0;//在发送以字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;//超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)//超时处理
{
    if(1==Gu8SendByteFinish)
{
    break;//如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;//超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage;
u32SendSize=*pSu32;//从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;//数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage); //基于“发送单字节的最小接口函数”来实现的
    }
}

unsigned char CalculateXor(const unsigned char *pCu8Buffer, //此处加const代表数组“只读”
unsigned long u32BufferSize)//参与计算的数组的大小
{
unsigned long i;
unsigned char Su8Rece_Xor;
Su8Rece_Xor=pCu8Buffer; //提取数据串第“i=0”个数据作为异或的原始数据
for(i=1;i<u32BufferSize;i++) //注意,这里是从第“i=1”个数据开始
{
Su8Rece_Xor=Su8Rece_Xor^pCu8Buffer;   //计算“异或”
}
    return Su8Rece_Xor;//返回运算后的异或的计算结果
}

void T0_time() interrupt 1   
{

if(1==vGu8QueueSendTimerFlag&&vGu16QueueSendTimerCnt>0) //队列发送的超时定时器
        {
               vGu16QueueSendTimerCnt--;       
}   

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)
{

}

页: 1 2 3 4 5 6 [7]
查看完整版本: 从单片机基础到程序框架(连载)