jianhong_wu 发表于 2014-2-28 17:07:27

第三十一节:数码管通过一二级菜单来设置数据的综合程序。

开场白:
   上一节讲了二级菜单,这一节要教会大家两个知识点:
第一个:数码管通过一二级菜单来设置数据的综合程序框架。
第二个:继续加深熟悉鸿哥首次提出的“一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。


具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标闪烁”按键对应S9键,切换窗口按键对应S13键。

(2)实现功能:
   通过按键设置4个不同的参数。
    有2个窗口。每个窗口显示2个参数。
   第8,7,6,5位数码管显示”P-1 ”代表第1个窗口,显示”P-2 ”代表第2个窗口。第4,3位数码管显示该窗口下其中一个参数,第2,1位数码管显示该窗口下其中另外一个参数。每个参数的范围是从0到99。
有四个按键。
一个是切换窗口按键,依次按下此按键,会依次切换窗口显示。一个是“光标闪烁”按键,依次按下此按键,每两位数码管会依次处于闪烁的状态,哪两位数码管处于闪烁状态时,此时按加键或者减键就可以设置当前选中的参数。依次按下“光标闪烁”按键,数码管会在以下3种状态中循环:只有第4,3位数码管闪烁---只有第2,1位数码管闪烁---所有的数码管都不闪烁。

(3)源代码讲解如下:

#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_dpy_time_half200//数码管闪烁时间的半值
#define const_dpy_time_all   400//数码管闪烁时间的全值 一定要比const_dpy_time_half 大

void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);

//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(); //显示数码管字模的驱动函数

void display_service(); //显示的窗口菜单服务程序

//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time();//定时中断函数

void key_service(); //按键服务的应用程序
void key_scan();//按键扫描函数 放在定时中断里


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为中途暂停指示灯 亮的时候表示中途暂停


sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器


unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容


unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志

unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucPart=0;//本程序的核心变量,局部显示变量。类似于二级菜单的变量。代表显示不同的局部。

unsigned char ucWd1Part1Update=0;//在窗口1中,局部1的更新显示标志
unsigned char ucWd1Part2Update=0; //在窗口1中,局部2的更新显示标志
unsigned char ucWd2Part1Update=0; //在窗口2中,局部1的更新显示标志
unsigned char ucWd2Part2Update=0; //在窗口2中,局部2的更新显示标志

unsigned intuiSetData1=0;//本程序中需要被设置的参数1
unsigned intuiSetData2=0;//本程序中需要被设置的参数2
unsigned intuiSetData3=0;//本程序中需要被设置的参数3
unsigned intuiSetData4=0;//本程序中需要被设置的参数4


unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量
unsigned char ucTemp5=0;//中间过渡变量
unsigned char ucTemp6=0;//中间过渡变量
unsigned char ucTemp7=0;//中间过渡变量
unsigned char ucTemp8=0;//中间过渡变量

unsigned intuiDpyTimeCnt=0;//数码管的闪烁计时器,放在定时中断里不断累加

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
   key_service(); //按键服务的应用程序
         display_service(); //显示的窗口菜单服务程序
   }

}

/* 注释一:
*鸿哥首次提出的"一二级菜单显示理论":
*凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,
*每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。
*局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,
*表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。
*/


void display_service() //显示的窗口菜单服务程序
{


   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示窗口1的数据

                        if(ucWd1Part1Update==1)//仅仅参数1局部更新
                        {
                           ucWd1Part1Update=0;   //及时清零标志,避免一直进来扫描

               ucTemp4=uiSetData1/10;//第1个参数
               ucTemp3=uiSetData1%10;
                           if(uiSetData1<10)
                           {
                              ucDigShow4=10;
                           }
                           else
                           {
                              ucDigShow4=ucTemp4;
                           }
                           ucDigShow3=ucTemp3;
                        }

                        if(ucWd1Part2Update==1)//仅仅参数2局部更新
                        {
                           ucWd1Part2Update=0;//及时清零标志,避免一直进来扫描

               ucTemp2=uiSetData2/10;//第2个参数
               ucTemp1=uiSetData2%10;
                           if(uiSetData2<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                        }

       

/* 注释二:
* 必须注意局部更新和全部更新的编写顺序,局部更新应该写在全部更新之前,
* 当局部更新和全部更新同时发生时,这样就能保证到全部更新的优先响应。
*/

            if(ucWd1Update==1)//窗口1要全部更新显示
                        {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描

               ucTemp8=12;//显示P
               ucTemp7=11;//显示-
               ucTemp6=1;//显示1
               ucTemp5=10;//显示空

               ucTemp4=uiSetData1/10;//第1个参数
               ucTemp3=uiSetData1%10;

               ucTemp2=uiSetData2/10;//第2个参数
               ucTemp1=uiSetData2%10;


               ucDigShow8=ucTemp8;
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;

                           if(uiSetData1<10)
                           {
                              ucDigShow4=10;
                           }
                           else
                           {
                              ucDigShow4=ucTemp4;
                           }
                           ucDigShow3=ucTemp3;


                           if(uiSetData2<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                       

            }


                        //数码管闪烁
            switch(ucPart)//根据局部变量的值,使对应的参数产生闪烁的动态效果。
                        {
                           case 0://2个参数都不闪烁

                                break;
                           case 1://第1个参数闪烁
                                if(uiDpyTimeCnt==const_dpy_time_half)
                                        {
                                   if(uiSetData1<10)      //数码管显示内容
                                   {
                                      ucDigShow4=10;
                                   }
                                   else
                                   {
                                      ucDigShow4=ucTemp4;
                                   }
                                   ucDigShow3=ucTemp3;
                                        }
                                else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                                        {
                                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零

                                   ucDigShow4=10;   //数码管显示空,什么都不显示
                                   ucDigShow3=10;

                                        }
                                break;
                           case 2:   //第2个参数闪烁
                                if(uiDpyTimeCnt==const_dpy_time_half)
                                        {
                                   if(uiSetData2<10)      //数码管显示内容
                                   {
                                      ucDigShow2=10;
                                   }
                                   else
                                   {
                                      ucDigShow2=ucTemp2;
                                   }
                                   ucDigShow1=ucTemp1;
                                        }
                                else if(uiDpyTimeCnt>const_dpy_time_all)//const_dpy_time_all一定要比const_dpy_time_half 大
                                        {
                                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零

                                   ucDigShow2=10;   //数码管显示空,什么都不显示
                                   ucDigShow1=10;

                                        }
                                break;
       
                        }

            break;
       case 2:   //显示窗口2的数据

                        if(ucWd2Part1Update==1)//在窗口2中,仅仅参数1局部更新
                        {
                           ucWd2Part1Update=0;   //及时清零标志,避免一直进来扫描

               ucTemp4=uiSetData3/10;//第3个参数
               ucTemp3=uiSetData3%10;
                           if(uiSetData3<10)
                           {
                              ucDigShow4=10;
                           }
                           else
                           {
                              ucDigShow4=ucTemp4;
                           }
                           ucDigShow3=ucTemp3;
                        }

                        if(ucWd2Part2Update==1)//在窗口2中,仅仅参数2局部更新
                        {
                           ucWd2Part2Update=0;//及时清零标志,避免一直进来扫描

               ucTemp2=uiSetData4/10;//第4个参数
               ucTemp1=uiSetData4%10;
                           if(uiSetData4<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                        }

            if(ucWd2Update==1)//窗口2要全部更新显示
                        {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描

               ucTemp8=12;//显示P
               ucTemp7=11;//显示-
               ucTemp6=2;//显示2
               ucTemp5=10;//显示空

               ucTemp4=uiSetData3/10;//第3个参数
               ucTemp3=uiSetData3%10;

               ucTemp2=uiSetData4/10;//第4个参数
               ucTemp1=uiSetData4%10;


               ucDigShow8=ucTemp8;
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;

                           if(uiSetData3<10)
                           {
                              ucDigShow4=10;
                           }
                           else
                           {
                              ucDigShow4=ucTemp4;
                           }
                           ucDigShow3=ucTemp3;


                           if(uiSetData4<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                       

            }


                        //数码管闪烁
            switch(ucPart)//根据局部变量的值,使对应的参数产生闪烁的动态效果。
                        {
                           case 0://2个参数都不闪烁

                                break;
                           case 1://第3个参数闪烁
                                if(uiDpyTimeCnt==const_dpy_time_half)
                                        {
                                   if(uiSetData3<10)      //数码管显示内容
                                   {
                                      ucDigShow4=10;
                                   }
                                   else
                                   {
                                      ucDigShow4=ucTemp4;
                                   }
                                   ucDigShow3=ucTemp3;
                                        }
                                else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                                        {
                                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零

                                   ucDigShow4=10;   //数码管显示空,什么都不显示
                                   ucDigShow3=10;

                                        }
                                break;
                           case 2:   //第4个参数闪烁
                                if(uiDpyTimeCnt==const_dpy_time_half)
                                        {
                                   if(uiSetData4<10)      //数码管显示内容
                                   {
                                      ucDigShow2=10;
                                   }
                                   else
                                   {
                                      ucDigShow2=ucTemp2;
                                   }
                                   ucDigShow1=ucTemp1;
                                        }
                                else if(uiDpyTimeCnt>const_dpy_time_all)//const_dpy_time_all一定要比const_dpy_time_half 大
                                        {
                                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零

                                   ucDigShow2=10;   //数码管显示空,什么都不显示
                                   ucDigShow1=10;

                                        }
                                break;
       
                        }

            break;   
   }
   


}


void key_scan()//按键扫描函数 放在定时中断里
{

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}

}


void key_service() //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部闪烁位置来设置不同的参数
                                   {
                                      case 0:
                                             break;
                                      case 1:
                           uiSetData1++;   
                           if(uiSetData1>99) //最大值是99
                           {
                               uiSetData1=99;
                           }
                                                   ucWd1Part1Update=1; //局部更新显示参数1
                                             break;
                                      case 2:
                           uiSetData2++;   
                           if(uiSetData2>99) //最大值是99
                           {
                               uiSetData2=99;
                           }
                                                   ucWd1Part2Update=1; //局部更新显示参数2
                                             break;
                                   }
                   break;
            case 2:
                   switch(ucPart)//在窗口2下,根据不同的局部闪烁位置来设置不同的参数
                                   {
                                      case 0:
                                             break;
                                      case 1:
                           uiSetData3++;   
                           if(uiSetData3>99) //最大值是99
                           {
                               uiSetData3=99;
                           }
                                                   ucWd2Part1Update=1; //局部更新显示参数1
                                             break;
                                      case 2:
                           uiSetData4++;   
                           if(uiSetData4>99) //最大值是99
                           {
                               uiSetData4=99;
                           }
                                                   ucWd2Part2Update=1; //局部更新显示参数2
                                             break;
                                   }
                   break;
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部闪烁位置来设置不同的参数
                                   {
                                      case 0:
                                             break;
                                      case 1:
                           uiSetData1--;   
                           if(uiSetData1>99) //0减去1溢出肯定大于99
                           {
                               uiSetData1=0;
                           }
                                                   ucWd1Part1Update=1; //局部更新显示参数1
                                             break;
                                      case 2:
                           uiSetData2--;   
                           if(uiSetData2>99) //0减去1溢出肯定大于99
                           {
                               uiSetData2=0;
                           }
                                                   ucWd1Part2Update=1; //局部更新显示参数2
                                             break;
                                   }
                   break;
            case 2:
                   switch(ucPart)//在窗口2下,根据不同的局部闪烁位置来设置不同的参数
                                   {
                                      case 0:
                                             break;
                                      case 1:
                           uiSetData3--;   
                           if(uiSetData3>99) //0减去1溢出肯定大于99
                           {
                               uiSetData3=0;
                           }
                                                   ucWd2Part1Update=1; //局部更新显示参数1
                                             break;
                                      case 2:
                           uiSetData4--;   
                           if(uiSetData4>99) //0减去1溢出肯定大于99
                           {
                               uiSetData4=0;
                           }
                                                   ucWd2Part2Update=1; //局部更新显示参数2
                                             break;
                                   }
                   break;
          }
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 切换"光标闪烁"按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://在窗口1下,切换"光标闪烁"
                   ucPart++;
                                   if(ucPart>2)
                                   {
                                     ucPart=0;
                                   }
                                   ucWd1Update=1;//窗口1全部更新显示
                   break;
            case 2://在窗口2下,切换"光标闪烁"
                   ucPart++;
                                   if(ucPart>2)
                                   {
                                     ucPart=0;
                                   }
                                   ucWd2Update=1;//窗口2全部更新显示
                   break;
          }
      
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
   
    case 4:// 切换窗口按键 对应朱兆祺学习板的S13键
              ucWd++;
                  if(ucWd>2)
                  {
                     ucWd=1;
                  }

                  ucPart=0; //强行把局部变量复位,让新切换的窗口不闪烁

          switch(ucWd)//在不同的窗口下,更新显示不同的窗口
          {
            case 1://在窗口1下
                                   ucWd1Update=1;//窗口1全部更新显示
                   break;
            case 2://在窗口2下
                                   ucWd2Update=1;//窗口2全部更新显示
                   break;
          }
      
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         

}               
}


void display_drive()
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }



}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数

uiDpyTimeCnt++;//数码管的闪烁计时器

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
//   beep_dr=1;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
//   beep_dr=0;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//数码管字模的驱动函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

/* 注释三:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0;//关闭独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯

TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}

void initial_peripheral() //第二区 初始化外围
{


   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}


总结陈词:
这节讲了数码管通过一二级菜单来设置数据的综合程序,鸿哥的人机界面程序框架基本上都涉及到了,为了继续加深熟悉鸿哥的“一二级菜单显示理论”,下一节会继续讲一个常用的数码管项目小程序,这个项目小程序鸿哥是怎么写的?欲知详情,请听下回分解-----数码管中的倒计时程序。
(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-3-9 20:21:23

第三十二节:数码管中的倒计时程序。

开场白:
   上一节讲了一二级菜单的综合程序,这一节要教会大家三个知识点:
第一个:通过本程序,继续加深理解按键与数码管的关联方法。
第二个:复习一下我在第五节教给大家的时间校正法。
第三个:继续加深熟悉鸿哥首次提出的“一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。


具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。启动和暂停键对应S1键,复位键对应S5键。

(2)实现功能:按下启动暂停按键时,倒计时开始工作,再按一次启动暂停按键时,则暂停倒计时。在任何时候,按下复位按键,倒计时将暂停工作,并且恢复倒计时当前默认值99。
   
(3)源代码讲解如下:

#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_voice_long   200    //蜂鸣器长叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间


#define const_dpy_time_half200//数码管闪烁时间的半值
#define const_dpy_time_all   400//数码管闪烁时间的全值 一定要比const_dpy_time_half 大

/* 注释一:
* 如何知道1秒钟需要多少个定时中断?
* 这个需要编写一段小程序测试,得到测试的结果后再按比例修正。
* 步骤:
* 第一步:在程序代码上先写入1秒钟大概需要200个定时中断。
* 第二步:把程序烧录进单片机后,上电开始测试,手上同步打开手机里的秒表。
*         如果单片机倒计时跑完了99秒,而手机上的秒表才走了45秒。
* 第三步:那么最终得出1秒钟需要的定时中断次数是:const_1s=(200*99)/45=440
*/


#define const_1s440   //大概一秒钟所需要的定时中断次数

void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);

//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(); //显示数码管字模的驱动函数
void display_service(); //显示的窗口菜单服务程序

//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time();//定时中断函数
void key_service(); //按键服务的应用程序
void key_scan();//按键扫描函数 放在定时中断里


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为中途暂停指示灯 亮的时候表示中途暂停


sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器


unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容


unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志

unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned char ucWd1Update=1; //窗口1更新显示标志


unsigned char ucCountDown=99;//倒计时的当前值
unsigned char ucStartFlag=0;//暂停与启动的标志位
unsigned intuiTimeCnt=0;//倒计时的时间计时器

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量
unsigned char ucTemp5=0;//中间过渡变量
unsigned char ucTemp6=0;//中间过渡变量
unsigned char ucTemp7=0;//中间过渡变量
unsigned char ucTemp8=0;//中间过渡变量


//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       key_service(); //按键服务的应用程序
       display_service(); //显示的窗口菜单服务程序
   }

}


/* 注释二:
*鸿哥首次提出的"一二级菜单显示理论":
*凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,
*每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。
*局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,
*表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。
*/


void display_service() //显示的窗口菜单服务程序
{



//由于本程序只有一个窗口,读者在做实际项目的时候,可以省略switch(ucWd)
   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示窗口1的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
                        {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描

               ucTemp8=10;//显示空
               ucTemp7=10;//显示空
               ucTemp6=10;//显示空
               ucTemp5=10;//显示空
               ucTemp4=10;//显示空
               ucTemp3=10;//显示空

               ucTemp2=ucCountDown/10;//倒计时的当前值
               ucTemp1=ucCountDown%10;


               ucDigShow8=ucTemp8;
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;
               ucDigShow4=ucTemp4;
               ucDigShow3=ucTemp3;


                           if(ucCountDown<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                       

            }
            break;
   
   }
   


}


void key_scan()//按键扫描函数 放在定时中断里
{

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

}


void key_service() //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 启动和暂停按键 对应朱兆祺学习板的S1键

       
         //由于本程序只有一个窗口,读者在做实际项目的时候,可以省略switch(ucWd)
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   if(ucStartFlag==0)//如果原来处于暂停的状态,则启动
                                   {
                      ucStartFlag=1; //启动
                                   }
                                   else   //如果原来处于启动的状态,则暂停
                                   {
                                      ucStartFlag=0;//暂停
                                   }
                   break;
         
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 复位按键 对应朱兆祺学习板的S5键

         //由于本程序只有一个窗口,读者在做实际项目的时候,可以省略switch(ucWd)
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                                   ucStartFlag=0;//暂停
                   ucCountDown=99;//恢复倒计时的默认值99
                   uiTimeCnt=0;//倒计时的时间计时器清零
                                   ucWd1Update=1; //窗口1更新显示标志只要ucCountDown变化了,就要更新显示一次
                   break;
         
          }
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

}               
}


void display_drive()
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }



}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数


if(ucStartFlag==1)//启动倒计时的计时器
{
   uiTimeCnt++;
   if(uiTimeCnt>=const_1s)    //1秒钟的时间到
   {
          if(ucCountDown!=0) //加这个判断,就是避免在0的情况下减1
          {
             ucCountDown--;//倒计时当前显示值减1
          }

      if(ucCountDown==0)//倒计时结束
          {
             ucStartFlag=0;//暂停
         uiVoiceCnt=const_voice_long; //蜂鸣器触发提醒,滴一声就停。
          }

      ucWd1Update=1; //窗口1更新显示标志
      uiTimeCnt=0;   //计时器清零,准备从新开始计时
   }
}



if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
//   beep_dr=1;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
//   beep_dr=0;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//数码管字模的驱动函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

/* 注释三:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0;//关闭独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯

TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}

void initial_peripheral() //第二区 初始化外围
{


   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}


总结陈词:
这节讲了数码管中的倒计时程序。如果要在此程序上多增加两个按键,用来控制数码管倒计时的速度档位,并且需要在数码管中闪烁显示被设置的速度档位,该怎么编写这个程序?欲知详情,请听下回分解-----能设置速度档位的数码管倒计时程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-3-9 20:22:53

第三十三节:能设置速度档位的数码管倒计时程序。

开场白:
   上一节讲了数码管中的倒计时程序。这节要在此程序上多增加两个按键,用来控制数码管倒计时的速度档位,并且需要在数码管中闪烁显示被设置的速度档位。这一节要教会大家三个知识点:
第一个:把一个按键的短按与长按复合应用在项目中的程序结构。
第二个:通过本程序,继续加深理解按键与数码管的关联方法。
第三个:继续加深熟悉鸿哥首次提出的“一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。

具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。启动和暂停键对应S1键,复位键对应S5键。加键对应S9键,减键对应S13键。

(2)实现功能:按下启动暂停按键时,倒计时开始工作,再按一次启动暂停按键时,则暂停倒计时。在任何时候,按下复位按键,倒计时将暂停工作,并且恢复倒计时当前默认值99。如果长按复位按键,在数码管会切换到第2个闪烁窗口,用来设置速度档位,修改完速度档位后,再一次按下复位按键,或者直接按启动暂停按键,会切换回窗口1显示倒计时的当前数据。
   
(3)源代码讲解如下:

#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_voice_long   200    //蜂鸣器长叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_key_long_time 200//长按复位按键的时间


#define const_dpy_time_half200//数码管闪烁时间的半值
#define const_dpy_time_all   400//数码管闪烁时间的全值 一定要比const_dpy_time_half 大


void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);

//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(); //显示数码管字模的驱动函数

void display_service(); //显示的窗口菜单服务程序

//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time();//定时中断函数
void key_service(); //按键服务的应用程序
void key_scan();//按键扫描函数 放在定时中断里


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键


sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为中途暂停指示灯 亮的时候表示中途暂停


sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志




unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器


unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容


unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志

unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=1; //窗口2更新显示标志

unsigned char ucCountDown=99;//倒计时的当前值
unsigned char ucStartFlag=0;//暂停与启动的标志位
unsigned intuiTimeCnt=0;//倒计时的时间计时器

unsigned intuiDpyTimeCnt=0;//数码管的闪烁计时器,放在定时中断里不断累加

unsigned intuiSetData1=50;//速度档位
unsigned intuiSpeedCnt=400;//影响速度变量,它跟速度档位uiSetData1有关联

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量
unsigned char ucTemp5=0;//中间过渡变量
unsigned char ucTemp6=0;//中间过渡变量
unsigned char ucTemp7=0;//中间过渡变量
unsigned char ucTemp8=0;//中间过渡变量


//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       key_service(); //按键服务的应用程序
       display_service(); //显示的窗口菜单服务程序
   }

}



/* 注释一:
*鸿哥首次提出的"一二级菜单显示理论":
*凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,
*每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。
*局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,
*表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。
*/


void display_service() //显示的窗口菜单服务程序
{


   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示窗口1的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
                        {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描

               ucTemp8=10;//显示空
               ucTemp7=10;//显示空
               ucTemp6=10;//显示空
               ucTemp5=10;//显示空
               ucTemp4=10;//显示空
               ucTemp3=10;//显示空

               ucTemp2=ucCountDown/10;//倒计时的当前值
               ucTemp1=ucCountDown%10;


               ucDigShow8=ucTemp8;
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;
               ucDigShow4=ucTemp4;
               ucDigShow3=ucTemp3;


                           if(ucCountDown<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                       

            }
            break;
       case 2:   //显示窗口2的数据
            if(ucWd2Update==1)//窗口2要全部更新显示
            {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描

               ucTemp8=10;//显示空
               ucTemp7=10;//显示空
               ucTemp6=10;//显示空
               ucTemp5=10;//显示空
               ucTemp4=10;//显示空
               ucTemp3=10;//显示空

               ucTemp2=uiSetData1/10;//倒计时的速度档位
               ucTemp1=uiSetData1%10;


               ucDigShow8=ucTemp8;
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;
               ucDigShow4=ucTemp4;
               ucDigShow3=ucTemp3;


                           if(uiSetData1<10)
                           {
                              ucDigShow2=10;
                           }
                           else
                           {
                              ucDigShow2=ucTemp2;
                           }
                           ucDigShow1=ucTemp1;

                        

            }

            //数码管闪烁
            if(uiDpyTimeCnt==const_dpy_time_half)
            {
               if(uiSetData1<10)      //数码管显示内容
               {
                  ucDigShow2=10;
               }
               else
               {
                  ucDigShow2=ucTemp2;
               }
               ucDigShow1=ucTemp1;
            }
            else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
            {                       
               uiDpyTimeCnt=0;   //及时把闪烁记时器清零                          

               ucDigShow2=10;   //数码管显示空,什么都不显示
               ucDigShow1=10;

            }

            break;

   
   }
   


}


void key_scan()//按键扫描函数 放在定时中断里
{

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

/* 注释二:
* 请注意以下长按复位按键与短按复位按键的写法。在本程序中,每次长按复位按键必然
* 触发一次短按复位按键。
*/

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}
else if(uiKeyTimeCnt2<const_key_long_time)   //长按复位按键
{
      uiKeyTimeCnt2++;
          if(uiKeyTimeCnt2==const_key_long_time)
          {
             ucKeySec=17;    //触发17号长按复位键
          }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}




}


void key_service() //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 启动和暂停按键 对应朱兆祺学习板的S1键

          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   if(ucStartFlag==0)//如果原来处于暂停的状态,则启动
                                   {
                      ucStartFlag=1; //启动
                                   }
                                   else   //如果原来处于启动的状态,则暂停
                                   {
                                      ucStartFlag=0;//暂停
                                   }
                   break;


         
          }   

          ucWd=1;//不管在哪个窗口,强行切换回窗口1
                  ucWd1Update=1; //窗口1更新显示标志

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 复位按键 对应朱兆祺学习板的S5键

          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:    //在窗口1中
                                   ucStartFlag=0;//暂停
                   ucCountDown=99;//恢复倒计时的默认值99
                   uiTimeCnt=0;//倒计时的时间计时器清零
                                   ucWd1Update=1; //窗口1更新显示标志只要ucCountDown变化了,就要更新显示一次
                   break;
            case 2:    //在窗口2中
                   ucWd=1;//切换回窗口1
                                   ucWd1Update=1; //窗口1更新显示标志
                   break;
          }
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 加按键 对应朱兆祺学习板的S9键


          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 2:   //在窗口2中
                   uiSetData1++;       //速度档位累加,档位越大,速度越快.
                                   if(uiSetData1>99)
                                   {
                                      uiSetData1=99;
                                   }
                   uiSpeedCnt=440-(uiSetData1*2);//速度档位越大,累计中断数uiSpeedCnt越小,从而倒计时的时间越快

                                   ucWd2Update=1; //窗口2更新显示
                   break;
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 4:// 减按键 对应朱兆祺学习板的S13键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 2:   //在窗口2中
                             if(uiSetData1>0)//加此条件判断,避免0减1
                                   {
                      uiSetData1--;       //速度档位累减,档位越小,速度越慢.
                                   }

                   uiSpeedCnt=440-(uiSetData1*2);//速度档位越小,累计中断数uiSpeedCnt越大,从而倒计时的时间越慢

                                   ucWd2Update=1; //窗口2更新显示
                   break;
          }
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 17:// 长按复位按键 对应朱兆祺学习板的S5键

          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://窗口1下
                   ucWd=2;//切换到闪烁窗口2进行设置速度档位显示
                                   ucWd2Update=1; //窗口2更新显示标志
                   break;
         
          }
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

}               
}


void display_drive()
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }



}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数


if(ucStartFlag==1)//启动倒计时的计时器
{
   uiTimeCnt++;
   if(uiTimeCnt>=uiSpeedCnt)    //时间到
   {
          if(ucCountDown!=0) //加这个判断,就是避免在0的情况下减1
          {
             ucCountDown--;//倒计时当前显示值减1
          }

      if(ucCountDown==0)//倒计时结束
          {
             ucStartFlag=0;//暂停
         uiVoiceCnt=const_voice_long; //蜂鸣器触发提醒,滴一声就停。
          }

      ucWd1Update=1; //窗口1更新显示标志
      uiTimeCnt=0;   //计时器清零,准备从新开始计时
   }
}


uiDpyTimeCnt++;//数码管的闪烁计时器


if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
//   beep_dr=1;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
//   beep_dr=0;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//数码管字模的驱动函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

/* 注释三:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0;//关闭独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯

TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}

void initial_peripheral() //第二区 初始化外围
{


   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   uiSpeedCnt=440-(uiSetData1*2);//速度档位越大,累计中断数uiSpeedCnt越小,从而倒计时的时间越快

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}


总结陈词:
这节讲了能设置速度档位的数码管倒计时程序。现在很多人用iphone4S的手机,这个手机每次开机显示的时候,都要通过4个密码开锁,如果我们要用4位数码管来实现这个密码锁功能,该怎么编写这个程序?欲知详情,请听下回分解-----在数码管中实现iphone4S开机密码锁的程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-3-15 10:11:52

第三十四节:在数码管中实现iphone4S开机密码锁的程序。
开场白:
    这一节要教会大家四个知识点:第一个:类似手机上10秒钟内无按键操作将自动进入锁屏的程序。第二个:如何用一个数组来接收按键的一串数字输入。第三个:矩阵键盘中,数字按键的输入,由于这部分按键的代码相似度非常高,因此把它封装在一个函数里可以非常简洁方便。第四个:继续加深熟悉鸿哥首次提出的“一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。

具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。数字1键对应S1键,数字2键对应S2键,数字3键对应S3键…. 数字9键对应S9键,数字0键对应S10键。其他的按键不用。

(2)实现功能:本程序有3个窗口。开机显示第1个密码登录框窗口“----”,在这个窗口下输入密码,如果密码等于”9922”表示密码正确,将会切换到第2个显示按键值的窗口。在窗口2下,按不同的按键会显示不同的按键值,如果10秒内没有按键操作,将会自动切换到第1个密码登录窗口,类似手机上的自动锁屏操作。在密码登录窗口1下,如果密码不正确,会自动清除密码的数字,继续在窗口1下显示”----”。窗口3是用来停留0.5秒显示全部密码的信息,然后根据密码的正确与否自动切换到对应的窗口。
(3)源代码讲解如下:
#include "REG52.H"


#define const_no_key_push 4400   //大概10秒内无按键按下的时间
#define const_0_1s220            //大概0.5秒的时间

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time20    //按键去抖动延时的时间


void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(); //显示数码管字模的驱动函数
void display_service(); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);
void T0_time();//定时中断函数

void number_key_input(unsigned char ucWhichKey);//由于数字按键的代码相似度高,因此封装在这个函数里
void key_service(); //按键服务的应用程序
void key_scan();//按键扫描函数 放在定时中断里

sbit key_sr1=P0^0; //第一行输入
sbit key_sr2=P0^1; //第二行输入
sbit key_sr3=P0^2; //第三行输入
sbit key_sr4=P0^3; //第四行输入

sbit key_dr1=P0^4; //第一列输出
sbit key_dr2=P0^5; //第二列输出
sbit key_dr3=P0^6; //第三列输出
sbit key_dr4=P0^7; //第四列输出


sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为中途暂停指示灯 亮的时候表示中途暂停

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucKeyStep=1;//按键扫描步骤变量

unsigned intuiKeyTimeCnt=0; //按键去抖动延时计数器
unsigned char ucKeyLock=0; //按键触发后自锁的变量标志

unsigned char ucRowRecord=1; //记录当前扫描到第几列了


unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucWd3Update=0; //窗口3更新显示标志
unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。


unsigned char ucInputPassword;//在第1个窗口下,显示输入的4个密码
unsigned char ucPasswordCnt=0; //记录当前已经输入到哪一位密码了
unsigned char ucKeyNumber=1;//在第2个窗口下,显示当前被按下的按键

unsigned intuiNoKeyPushTimer=const_no_key_push;//10秒内无按键按下的计时器
unsigned intuiPasswordTimer=const_0_1s;//显示0.5秒钟全部密码的计时器,让窗口3停留显示0.5秒钟之后自动消失

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
   key_service(); //按键服务的应用程序
         display_service(); //显示的窗口菜单服务程序
   }
}
/* 注释一:
*鸿哥首次提出的"一二级菜单显示理论":
*凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,
*每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。
*局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,
*表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。
*/

void display_service() //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示输入密码的登录框
            if(ucWd1Update==1)//窗口1要全部更新显示
            {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描

               ucDigShow8=10;//第8位数码管显示无
               ucDigShow7=10;//第7位数码管显示无
               ucDigShow6=10;//第6位数码管显示无
               ucDigShow5=10;//第5位数码管显示无

               ucDigShow4=ucInputPassword;//第4位数码管显示输入的密码
               ucDigShow3=ucInputPassword;//第3位数码管显示输入的密码
               ucDigShow2=ucInputPassword;//第2位数码管显示输入的密码
               ucDigShow1=ucInputPassword;//第1位数码管显示输入的密码

            }
            break;
      case 2://显示被按下的键值
            if(ucWd2Update==1)//窗口2要全部更新显示
            {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描

               ucDigShow8=10;//第8位数码管显示无
               ucDigShow7=10;//第7位数码管显示无
               ucDigShow6=10;//第6位数码管显示无
               ucDigShow5=10;//第5位数码管显示无

               ucDigShow4=10;//第4位数码管显示无
               ucDigShow3=10;//第3位数码管显示无
               ucDigShow2=10;//第2位数码管显示无
               ucDigShow1=ucKeyNumber; //第1位数码管显示被按下的键值
            }
            break;
       case 3:   //当输入完4个密码后,显示1秒钟的密码登录框,
            if(ucWd3Update==1)//窗口3要全部更新显示
            {
               ucWd3Update=0;//及时清零标志,避免一直进来扫描

               ucDigShow8=10;//第8位数码管显示无
               ucDigShow7=10;//第7位数码管显示无
               ucDigShow6=10;//第6位数码管显示无
               ucDigShow5=10;//第5位数码管显示无

               ucDigShow4=ucInputPassword;//第4位数码管显示输入的密码
               ucDigShow3=ucInputPassword;//第3位数码管显示输入的密码
               ucDigShow2=ucInputPassword;//第2位数码管显示输入的密码
               ucDigShow1=ucInputPassword;//第1位数码管显示输入的密码

            }
            break;   
    }
   

}
void key_scan()//按键扫描函数 放在定时中断里
{


switch(ucKeyStep)
{
   case 1:   //按键扫描输出第ucRowRecord列低电平
            if(ucRowRecord==1)//第一列输出低电平
                  {
             key_dr1=0;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==2)//第二列输出低电平
                  {
             key_dr1=1;      
             key_dr2=0;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==3)//第三列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=0;   
             key_dr4=1;
                  }
            else   //第四列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=0;
                  }

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

   case 2:   //此处的小延时用来等待刚才列输出信号稳定,再判断输入信号。不是去抖动延时。
          uiKeyTimeCnt++;
                  if(uiKeyTimeCnt>1)
                  {
                     uiKeyTimeCnt=0;
             ucKeyStep++;   //切换到下一个运行步骤
                  }
            break;

   case 3:
          if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==1)
          {
             ucKeyStep=1;//如果没有按键按下,返回到第一个运行步骤重新开始扫描
             ucKeyLock=0;//按键自锁标志清零
             uiKeyTimeCnt=0; //按键去抖动延时计数器清零,此行非常巧妙   
   
                         ucRowRecord++;//输出下一列
                         if(ucRowRecord>4)
                         {
                            ucRowRecord=1; //依次输出完四列之后,继续从第一列开始输出低电平
                         }

          }
                  else if(ucKeyLock==0)//有按键按下,且是第一次触发
                  {
                     if(key_sr1==0&&key_sr2==1&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零

                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=1;//触发1号键 对应朱兆祺学习板的S1键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=2;//触发2号键 对应朱兆祺学习板的S2键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=3;//触发3号键 对应朱兆祺学习板的S3键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=4;//触发4号键 对应朱兆祺学习板的S4键
                           }

                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==0&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=5;//触发5号键 对应朱兆祺学习板的S5键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=6;//触发6号键 对应朱兆祺学习板的S6键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=7;//触发7号键 对应朱兆祺学习板的S7键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=8;//触发8号键 对应朱兆祺学习板的S8键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==0&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=9;//触发9号键 对应朱兆祺学习板的S9键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=10;//触发10号键 对应朱兆祺学习板的S10键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=11;//触发11号键 对应朱兆祺学习板的S11键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=12;//触发12号键 对应朱兆祺学习板的S12键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==0)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=13;//触发13号键 对应朱兆祺学习板的S13键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=14;//触发14号键 对应朱兆祺学习板的S14键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=15;//触发15号键 对应朱兆祺学习板的S15键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=16;//触发16号键 对应朱兆祺学习板的S16键
                           }
                              }
                        
                         }
                  
                  }
            break;

}


}


void key_service() //第三区 按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 1号键 对应朱兆祺学习板的S1键
          number_key_input(1);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
    case 2:// 2号键 对应朱兆祺学习板的S2键
          number_key_input(2);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 3:// 3号键 对应朱兆祺学习板的S3键
          number_key_input(3);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         
    case 4:// 4号键 对应朱兆祺学习板的S4键
          number_key_input(4);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 5:// 5号键 对应朱兆祺学习板的S5键
          number_key_input(5);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 6:// 6号键 对应朱兆祺学习板的S6键
          number_key_input(6);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 7:// 7号键 对应朱兆祺学习板的S7键
          number_key_input(7);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 8:// 8号键 对应朱兆祺学习板的S8键
          number_key_input(8);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 9:// 9号键 对应朱兆祺学习板的S9键
          number_key_input(9);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 10:// 把这个按键专门用来输入数字0    对应朱兆祺学习板的S10键
          number_key_input(0);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 11:// 11号键 对应朱兆祺学习板的S11键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 12:// 12号键 对应朱兆祺学习板的S12键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 13:// 13号键 对应朱兆祺学习板的S13键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 14:// 14号键 对应朱兆祺学习板的S14键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 15:// 15号键 对应朱兆祺学习板的S15键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 16:// 16号键 对应朱兆祺学习板的S16键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
}               
}

void number_key_input(unsigned char ucWhichKey)//由于数字按键的代码相似度高,因此封装在这个函数里
{


    switch(ucWd)
           {
           case 1:   //在显示密码登录框的窗口下
            ucInputPassword=ucWhichKey;   //输入的密码值显示
            ucPasswordCnt++;
                        if(ucPasswordCnt>=4)
                        {
                          ucPasswordCnt=0;
                                ucWd=3;//切换到第3个的窗口,停留显示1秒钟全部密码
                          ucWd3Update=1;//更新显示窗口3
                uiPasswordTimer=const_0_1s;//显示0.5秒钟全部密码的计时器,让窗口3停留显示0.5秒钟之后自动消失
                        }

                        ucWd1Update=1; //更新显示窗口1

                        uiNoKeyPushTimer=const_no_key_push;//10秒内无按键按下的计时器赋新值
                        break;
           case 2:   //在显示按键值的窗口下
                ucKeyNumber=ucWhichKey; //输入的按键数值显示
                  ucWd2Update=1;//更新显示窗口2

                        uiNoKeyPushTimer=const_no_key_push;//10秒内无按键按下的计时器赋新值
                  break;
    }

}


void display_drive()
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}

void T0_time() interrupt 1
{
unsigned int i;
TF0=0;//清除中断标志
TR0=0; //关中断

if(ucWd==3)//在窗口3下
{
   if(uiPasswordTimer>0)   
       {
          uiPasswordTimer--;
       }
   if(uiPasswordTimer==0)
       {
                if(ucInputPassword==9&&ucInputPassword==9&&ucInputPassword==2&&ucInputPassword==2)
          {   //如果密码等于9922,则正确
                      ucWd=2;//切换到第2个显示按键的窗口
                      ucWd2Update=1;//更新显示窗口2
          }
                else //如果密码不正确,则继续显示----
          {
            for(i=0;i<4;i++)
            {
               ucInputPassword=11;//开机默认密码全部显示"----"
            }
                          ucWd=1;
                ucWd1Update=1; //更新显示窗口1
                }

       }
}


if(ucWd==2)//在窗口2下
{
   if(uiNoKeyPushTimer>0)   
       {
          uiNoKeyPushTimer--;
       }
   if(uiNoKeyPushTimer==0)//如果10秒内无按键按下,则自动切换到显示密码登录框的界面
       {
       for(i=0;i<4;i++)
       {
          ucInputPassword=11;//开机默认密码全部显示"----"
       }
           ucWd=1;
           ucWd1Update=1; //更新显示窗口1
       }
}


key_scan(); //按键扫描函数
if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
//   beep_dr=1;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
//   beep_dr=0;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void initial_myself()//第一区 初始化单片机
{

led_dr=0;//关闭独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
}
void initial_peripheral() //第二区 初始化外围
{
   unsigned int i; //个人的变量命名习惯,i,j,k等单个字母的变量名只用在for循环里
   for(i=0;i<4;i++)
   {
   ucInputPassword=11;//开机默认密码全部显示"----"
   }

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;
   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断
}


总结陈词:这节讲了iphone4S开机密码锁的程序。2014年春节的时候,一帮朋友举行小规模的象棋比赛,有一些朋友下棋的速度实在是太慢了,为了限制比赛时间,我专门用朱兆祺的51学习板做了一个棋类比赛专用计时器给他们用,这个程序该怎么编写?欲知详情,请听下回分解-----带数码管显示的象棋比赛专用计时器。
(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-3-21 01:34:48

第三十五节:带数码管显示的象棋比赛专用计时器。
开场白:2014年春节的时候,一帮朋友举行小规模的象棋比赛,有一些朋友下棋的速度实在是太慢了,为了限制比赛时间,我专门用朱兆祺的51学习板做了一个棋类比赛专用计时器给他们用。这一节要教会大家两个知识点:第一个:按键服务程序操作的精髓在于根据当前系统处于什么窗口状态下就执行什么操作。紧紧围绕着不同的窗口ucWd来执行不同的操作。第二个:继续加深熟悉鸿哥首次提出的“一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。

具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。刚上电开机时,红棋加时键对应S1键,红棋减时键对应S2键.。刚上电开机时,黑棋加时键对应S3键,黑棋减时键对应S4键.。比赛中途暂停双方计时的暂停按键对应S6键。刚上电时,复位双方默认20分时间的复位按键对应S7按键。红棋的抢时按键对应S13键,黑棋的抢时按键对应S16按键。

(2)实现功能:棋类计时器有点像抢答器,本质上有两个计时器。比赛的时候对弈的两个棋友各用一个不同的按键抢时间,红棋走一步棋后,就按一下自己的抢时按键,这个时候红棋的计时器停止计时,而黑棋的计时器开始计时,黑棋走了一步棋后,按一下自己的计时器,黑棋停止计时,红棋继续计时,依次循环,谁的时间最先用完谁就输,蜂鸣器也会发出长鸣的声音提示时间到。上电开机默认双方各有20分钟的时间,左边显示的是红棋的时间,右边显示的是黑棋的时间。此时可以通过S1,S2.,S3,S4的加减按键来设置各自的最大倒计时时间。此时如果按下复位按键S7,会自动把双方的时间设置为默认的20分钟。设置好最大倒计时的时间后,此时任意一方按下各自的抢时按键(S13或者S16),则自己的计时器停止计时,而对方开始倒计时。此时数码管显示的是对方的时间,而自己的时间屏蔽不显示。在开始倒计时的时候,如果中途有棋友要接听电话或者忙别的事情,需要暂时暂停一下双方的时间,这个时候可以按S6暂停按键来暂停双方的计时,忙完后再次按下暂停按键会继续倒计时。任何一方的时间走完,都会蜂鸣器长鸣提示。
(3)源代码讲解如下:
#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_voice_long   900   //蜂鸣器长叫的持续时间

#define const_key_time10    //按键去抖动延时的时间

#define const_1s   422   //产生一秒钟的时间基准

void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
void T0_time();//定时中断函数
void key_service();
void key_scan(); //按键扫描函数 放在定时中断里

void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive();//放在定时中断里的数码管驱动函数
void time_service();//放在定时中断里的时间应用程序
void display_service();


sbit key_sr1=P0^0; //第一行输入
sbit key_sr2=P0^1; //第二行输入
sbit key_sr3=P0^2; //第三行输入
sbit key_sr4=P0^3; //第四行输入

sbit key_dr1=P0^4; //第一列输出
sbit key_dr2=P0^5; //第二列输出
sbit key_dr3=P0^6; //第三列输出
sbit key_dr4=P0^7; //第四列输出

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口


sbit led_dr=P3^5;//作为中途暂停指示灯 亮的时候表示中途暂停


sbit dig_hc595_sh_dr=P2^0;   //数码管 的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;


unsigned char ucKeyStep=1;//按键扫描步骤变量

unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiKeyTimeCnt=0; //按键去抖动延时计数器
unsigned char ucKeyLock=0; //按键触发后自锁的变量标志

unsigned char ucRowRecord=1; //记录当前扫描到第几列了

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucDigShow8=0;//第8位数码管要显示的内容
unsigned char ucDigShow7=0;//第7位数码管要显示的内容
unsigned char ucDigShow6=0;//第6位数码管要显示的内容
unsigned char ucDigShow5=0;//第5位数码管要显示的内容
unsigned char ucDigShow4=0;//第4位数码管要显示的内容
unsigned char ucDigShow3=0;//第3位数码管要显示的内容
unsigned char ucDigShow2=0;//第2位数码管要显示的内容
unsigned char ucDigShow1=0;//第1位数码管要显示的内容
unsigned char ucDigDot3=1;//数码管3的小数点是否显示的标志
unsigned char ucDigDot7=1;//数码管7的小数点是否显示的标志

unsigned char ucDigShowTemp=0; //临时中间变量

unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned int uiRedTimeCnt=0;    //红棋产生秒基准的时间计时器
unsigned int uiBlackTimeCnt=0;//黑棋产生秒基准的时间计时器

unsigned int uiRedTotal=1200;    //红棋的总时间
unsigned int uiBlackTotal=1200;//黑棋的总时间

unsigned char ucRedFlag=0;//红棋是否开始计时的标志
unsigned char ucBlackFlag=0;//黑棋是否开始计时的标志

unsigned char ucDisplayUpdate=1; //更新显示标志

/* 注释一:
*ucWd变量是本程序最核心的变量,代表显示哪一个窗口和系统处于当前哪种状态
*/
unsigned char ucWd=1;

code unsigned char dig_table[]=
{
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
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       key_service();
       display_service();
   }

}


void time_service()//放在定时中断里的时间应用程序
{
if(ucRedFlag==1)//1代表红棋在运行中
{
   uiRedTimeCnt++;
       if(uiRedTimeCnt>const_1s)
       {
      uiRedTimeCnt=0;
      if(uiRedTotal>0)
                {
                   uiRedTotal--;
                }
                else//时间到
                {
                  ucRedFlag=0;    //红棋和黑棋同时停止计时
                   ucBlackFlag=0;
                   ucWd=1;//切换到第一个窗口的状态
                   uiVoiceCnt=const_voice_long; //报警声音触发
                }
               

      ucDisplayUpdate=1;//更新显示
       }
}


if(ucBlackFlag==1)//1代表黑棋在运行中
{
   uiBlackTimeCnt++;
       if(uiBlackTimeCnt>const_1s)
       {
      uiBlackTimeCnt=0;
      if(uiBlackTotal>0)
                {
                   uiBlackTotal--;
                }
                else//时间到
                {
                  ucRedFlag=0;//红棋和黑棋同时停止计时
                   ucBlackFlag=0;
                   ucWd=1;//切换到第一个窗口的状态
                   uiVoiceCnt=const_voice_long; //报警声音触发
                }
               

      ucDisplayUpdate=1;//更新显示
       }
}
}

void display_service()//放在定时中断里的显示应用程序
{
if(ucDisplayUpdate==1)//有数据更新显示
{
   ucDisplayUpdate=0;
       switch(ucWd)   //本程序最核心的变量ucWd
       {
           case 1://窗口1,代表刚上电或者复位后的状态
                    //红棋分解出分
                     ucDigShowTemp=uiRedTotal/60;
            ucDigShow8=ucDigShowTemp/10;
            ucDigShow7=ucDigShowTemp%10;

                     //红棋分解出秒
                  ucDigShowTemp=uiRedTotal%60;
            ucDigShow6=ucDigShowTemp/10;
            ucDigShow5=ucDigShowTemp%10;
                        ucDigDot7=1;//数码管7的小数点显示

                    //黑棋分解出分
                     ucDigShowTemp=uiBlackTotal/60;
            ucDigShow4=ucDigShowTemp/10;
            ucDigShow3=ucDigShowTemp%10;

                     //黑棋分解出秒
                  ucDigShowTemp=uiBlackTotal%60;
            ucDigShow2=ucDigShowTemp/10;
            ucDigShow1=ucDigShowTemp%10;
                        ucDigDot3=1;//数码管3的小数点显示

            led_dr=1;//计时器处于停止状态,LED亮

                break;
           case 2://窗口2,代表黑棋正在运行中的状态

                    //红棋全部不显示
            ucDigShow8=10;
            ucDigShow7=10;
            ucDigShow6=10;
            ucDigShow5=10;
                        ucDigDot7=0;//数码管7的小数点不显示

                    //黑棋分解出分
                     ucDigShowTemp=uiBlackTotal/60;
            ucDigShow4=ucDigShowTemp/10;
            ucDigShow3=ucDigShowTemp%10;

                     //黑棋分解出秒
                  ucDigShowTemp=uiBlackTotal%60;
            ucDigShow2=ucDigShowTemp/10;
            ucDigShow1=ucDigShowTemp%10;
                        ucDigDot3=1;//数码管3的小数点显示

            led_dr=0;//计时器处于计时状态,LED灭

                break;

           case 3://窗口3,代表黑棋在中途暂停的状态

                    //红棋全部不显示
            ucDigShow8=10;
            ucDigShow7=10;
            ucDigShow6=10;
            ucDigShow5=10;
                        ucDigDot7=0;//数码管7的小数点不显示

                    //黑棋分解出分
                     ucDigShowTemp=uiBlackTotal/60;
            ucDigShow4=ucDigShowTemp/10;
            ucDigShow3=ucDigShowTemp%10;

                     //黑棋分解出秒
                  ucDigShowTemp=uiBlackTotal%60;
            ucDigShow2=ucDigShowTemp/10;
            ucDigShow1=ucDigShowTemp%10;
                        ucDigDot3=1;//数码管3的小数点显示


            led_dr=1;//计时器处于暂停状态,LED亮

                break;
           case 4://窗口4,代表红棋正在运行中的状态
                    //红棋分解出分
                     ucDigShowTemp=uiRedTotal/60;
            ucDigShow8=ucDigShowTemp/10;
            ucDigShow7=ucDigShowTemp%10;

                     //红棋分解出秒
                  ucDigShowTemp=uiRedTotal%60;
            ucDigShow6=ucDigShowTemp/10;
            ucDigShow5=ucDigShowTemp%10;
                        ucDigDot7=1;//数码管7的小数点显示


                    //黑棋全部不显示
            ucDigShow4=10;
            ucDigShow3=10;
            ucDigShow2=10;
            ucDigShow1=10;
                        ucDigDot3=0;//数码管3的小数点不显示

            led_dr=0;//计时器处于倒计时状态,LED灭

                break;

           case 5://窗口5,代表红棋在中途暂停的状态
                    //红棋分解出分
                     ucDigShowTemp=uiRedTotal/60;
            ucDigShow8=ucDigShowTemp/10;
            ucDigShow7=ucDigShowTemp%10;

                     //红棋分解出秒
                  ucDigShowTemp=uiRedTotal%60;
            ucDigShow6=ucDigShowTemp/10;
            ucDigShow5=ucDigShowTemp%10;
                        ucDigDot7=1;//数码管7的小数点显示


                    //黑棋全部不显示
            ucDigShow4=10;
            ucDigShow3=10;
            ucDigShow2=10;
            ucDigShow1=10;
                        ucDigDot3=0;//数码管3的小数点不显示

            led_dr=1;//计时器处于暂停状态,LED亮

                break;
       }
}
}

void display_drive()//放在定时中断里的数码管驱动函数
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xfe);
             break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xfd);
             break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
             break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xf7);
             break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xef);
             break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xdf);
             break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
             break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0x7f);
             break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }
}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

/* 注释二:
*注意,此处的延时delay_short必须尽可能小,否则动态扫描数码管的速度就不够。
*/
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void key_scan()//按键扫描函数 放在定时中断里
{

switch(ucKeyStep)
{
   case 1:   //按键扫描输出第ucRowRecord列低电平
            if(ucRowRecord==1)//第一列输出低电平
                  {
             key_dr1=0;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==2)//第二列输出低电平
                  {
             key_dr1=1;      
             key_dr2=0;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==3)//第三列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=0;   
             key_dr4=1;
                  }
            else   //第四列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=0;
                  }

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

   case 2:   //此处的小延时用来等待刚才列输出信号稳定,再判断输入信号。不是去抖动延时。
          uiKeyTimeCnt++;
                  if(uiKeyTimeCnt>1)
                  {
                     uiKeyTimeCnt=0;
             ucKeyStep++;   //切换到下一个运行步骤
                  }
            break;

   case 3:
          if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==1)
          {
             ucKeyStep=1;//如果没有按键按下,返回到第一个运行步骤重新开始扫描
             ucKeyLock=0;//按键自锁标志清零
             uiKeyTimeCnt=0; //按键去抖动延时计数器清零,此行非常巧妙   
   
                         ucRowRecord++;//输出下一列
                         if(ucRowRecord>4)
                         {
                            ucRowRecord=1; //依次输出完四列之后,继续从第一列开始输出低电平
                         }

          }
                  else if(ucKeyLock==0)//有按键按下,且是第一次触发
                  {
                     if(key_sr1==0&&key_sr2==1&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零

                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=1;//触发1号键 对应朱兆祺学习板的S1键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=2;//触发2号键 对应朱兆祺学习板的S2键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=3;//触发3号键 对应朱兆祺学习板的S3键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=4;//触发4号键 对应朱兆祺学习板的S4键
                           }

                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==0&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=5;//触发5号键 对应朱兆祺学习板的S5键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=6;//触发6号键 对应朱兆祺学习板的S6键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=7;//触发7号键 对应朱兆祺学习板的S7键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=8;//触发8号键 对应朱兆祺学习板的S8键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==0&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=9;//触发9号键 对应朱兆祺学习板的S9键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=10;//触发10号键 对应朱兆祺学习板的S10键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=11;//触发11号键 对应朱兆祺学习板的S11键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=12;//触发12号键 对应朱兆祺学习板的S12键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==0)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=13;//触发13号键 对应朱兆祺学习板的S13键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=14;//触发14号键 对应朱兆祺学习板的S14键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=15;//触发15号键 对应朱兆祺学习板的S15键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=16;//触发16号键 对应朱兆祺学习板的S16键
                           }
                              }
                        
                         }
                  
                  }
            break;

}


}

/* 注释三:
*按键服务程序操作的精髓在于根据当前系统处于什么窗口下就执行什么操作。
*紧紧围绕着不同的窗口ucWd来执行不同的操作。
*/
void key_service() //第三区 放在定时中断里的按键服务应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 1号键 对应朱兆祺学习板的S1键红棋加分 按键
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                              uiRedTotal=uiRedTotal+60;//加红棋分的时间,此处60秒代表一分
                                  if(uiRedTotal>5940)
                                  {
                                     uiRedTotal=5940;
                                  }
                  uiRedTotal=uiRedTotal-(uiRedTotal%60);//去秒取整分

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态
                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态
                      break;

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

    case 2:// 2号键 对应朱兆祺学习板的S2键红棋减分 按键
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                              if(uiRedTotal>=60)
                                  {
                                 uiRedTotal=uiRedTotal-60;//减红棋分的时间,此处60秒代表一分
                                  }
                  uiRedTotal=uiRedTotal-(uiRedTotal%60);//去秒取整分

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态
                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态
                      break;

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

    case 3:// 3号键 对应朱兆祺学习板的S3键黑棋加分 按键
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                              uiBlackTotal=uiBlackTotal+60;//加黑棋分的时间,此处60秒代表一分
                                  if(uiBlackTotal>5940)
                                  {
                                     uiBlackTotal=5940;
                                  }
                  uiBlackTotal=uiBlackTotal-(uiBlackTotal%60);//去秒取整分

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态
                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态
                      break;

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

    case 4:// 4号键 对应朱兆祺学习板的S4键黑棋减分 按键
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                              if(uiBlackTotal>=60)
                                  {
                                 uiBlackTotal=uiBlackTotal-60;//减黑棋分的时间,此处60秒代表一分
                                  }
                  uiBlackTotal=uiBlackTotal-(uiBlackTotal%60);//去秒取整分

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态
                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态
                      break;

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

    case 5:// 5号键 对应朱兆祺学习板的S5键


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

    case 6:// 6号键 对应朱兆祺学习板的S6键中途暂停和启动按键
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          

                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                  ucRedFlag=0;    //暂停计时
                  ucBlackFlag=0;//暂停计时
                                  ucWd=3; //切换到黑棋中途暂停的状态

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态
                  ucRedFlag=0;   //红棋暂停计时
                  ucBlackFlag=1; //黑棋继续计时
                                  ucWd=2;       //切换到黑棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                  ucRedFlag=0;    //暂停计时
                  ucBlackFlag=0;//暂停计时
                                  ucWd=5;       //切换到红棋中途暂停的状态

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态
                  ucRedFlag=1;   //红棋继续计时
                  ucBlackFlag=0; //黑棋暂停计时
                                  ucWd=4;       //切换到红棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。

                      break;

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

    case 7:// 7号键 对应朱兆祺学习板的S7键在第一个窗口下,把计时器的值恢复为开机时的默认值20分钟
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                  uiRedTotal=1200;    //红棋的总时间
                  uiBlackTotal=1200;//黑棋的总时间

                                  ucDisplayUpdate=1;//更新显示
                                  uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       

                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态

                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          

                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态

                      break;

          }
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 8:// 8号键 对应朱兆祺学习板的S8键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 9:// 9号键 对应朱兆祺学习板的S9键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 10:// 10号键 对应朱兆祺学习板的S10键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 11:// 11号键 对应朱兆祺学习板的S11键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 12:// 12号键 对应朱兆祺学习板的S12键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 13:// 13号键 对应朱兆祺学习板的S13键红棋按下
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                  ucRedFlag=0;    //红棋暂停计时
                  ucBlackFlag=1;//黑棋继续计时
                                  ucWd=2; //切换到黑棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       

                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态

                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          
                  ucRedFlag=0;    //红棋暂停计时
                  ucBlackFlag=1;//黑棋继续计时
                                  ucWd=2; //切换到黑棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态

                      break;

          }

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 14:// 14号键 对应朱兆祺学习板的S14键

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 15:// 15号键 对应朱兆祺学习板的S15键

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

    case 16:// 16号键 对应朱兆祺学习板的S16键    黑棋按下
              switch(ucWd)//本程序最核心的变量ucWd
              {
                 case 1://窗口1,代表刚上电,完成或者复位后的状态          
                  ucRedFlag=1;    //红棋继续计时
                  ucBlackFlag=0;//黑棋暂停计时
                                  ucWd=4; //切换到红棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                      break;

                 case 2://窗口2,代表黑棋正在运行中的状态       
                  ucRedFlag=1;    //红棋继续计时
                  ucBlackFlag=0;//黑棋暂停计时
                                  ucWd=4; //切换到红棋正在运行中的状态

                                  ucDisplayUpdate=1;//更新显示
                      break;

                 case 3://窗口3,代表黑棋在中途暂停的状态

                      break;

                 case 4://窗口4,代表红棋正在运行中的状态          

                      break;

                 case 5://窗口5,代表红棋在中途暂停的状态

                      break;

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



void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断
key_scan(); //放在定时中断里的按键扫描函数
time_service();//放在定时中断里的时间应用程序

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
         beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
         beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//放在定时中断里的数码管驱动函数

/* 注释四:
*注意,此处的重装初始值不能太大,否则动态扫描数码管的速度就不够。我把原来常用的2000改成了500。
*/
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

led_dr=1;
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

hc595_drive(0x00,0x00);
TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
}
void initial_peripheral() //第二区 初始化外围
{


EA=1;   //开总中断
ET0=1;    //允许定时中断
TR0=1;    //启动定时中断



}

总结陈词:这节讲了象棋比赛专用计时器的项目程序。为了继续加深读者理解按键和显示是如何有规律关联起来的,下节会继续讲一个相关的小项目程序。欲知详情,请听下回分解-----带数码管显示的加法简易计算器。
(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-3-27 12:54:10

第三十六节:带数码管显示的加法简易计算器。

开场白:
    这一节要做一个简单的计算器。这个计算器不带小数点,只能进行不超过8位数据的加法运算,它麻雀虽小但是五脏俱全,它能清晰地勾勒出商业计算器的程序框架和思路。读者只要看懂本节程序框架的规律,以后自己想做一个复杂一点的计算器应该是没问题的。复杂的计算器在算法上要用数组进行特殊处理,不能简单地直接用C语言的+,-,*,/运算符,这方面的内容我会在以后的章节中跟大家分享。
这一节要教会大家两个知识点:
第一个:数字按键的输入和十进制数值的移位方法。
第二个:继续加深理解按键与数码管的关联程序框架。

具体内容,请看源代码讲解。

(1)硬件平台:
基于坚鸿51单片机学习板。数字1键对应S1键,数字2键对应S2键,数字3键对应S3键…. 数字9键对应S9键, 数字0键对应S10键。加号键对应S13,等于号键对应S14,清除复位按键对应S16。其它按键不用。

(2)实现功能:
常用的加法计算器功能。有连加功能。
本程序有2个窗口。
第1个窗口:原始数据和运算结果窗口。比如加法运算中的被加数
第2个窗口:第二个参与运行的数据窗口。比如加法运算中的加数

(3)源代码讲解如下:
#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_voice_long   900   //蜂鸣器长叫的持续时间

#define const_key_time10    //按键去抖动延时的时间

#define const_1s   422   //产生一秒钟的时间基准

void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
void T0_time();//定时中断函数
void key_service();
void key_scan(); //按键扫描函数 放在定时中断里

void number_key_input(unsigned long ucWhichKey);//由于数字按键的代码相似度高,因此封装在这个函数里

void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive();//放在定时中断里的数码管驱动函数
void display_service();


sbit key_sr1=P0^0; //第一行输入
sbit key_sr2=P0^1; //第二行输入
sbit key_sr3=P0^2; //第三行输入
sbit key_sr4=P0^3; //第四行输入

sbit key_dr1=P0^4; //第一列输出
sbit key_dr2=P0^5; //第二列输出
sbit key_dr3=P0^6; //第三列输出
sbit key_dr4=P0^7; //第四列输出

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口


sbit led_dr=P3^5; //LED指示灯


sbit dig_hc595_sh_dr=P2^0;   //数码管 的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;


unsigned char ucKeyStep=1;//按键扫描步骤变量

unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiKeyTimeCnt=0; //按键去抖动延时计数器
unsigned char ucKeyLock=0; //按键触发后自锁的变量标志

unsigned char ucRowRecord=1; //记录当前扫描到第几列了

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucDigShow8=0;//第8位数码管要显示的内容
unsigned char ucDigShow7=0;//第7位数码管要显示的内容
unsigned char ucDigShow6=0;//第6位数码管要显示的内容
unsigned char ucDigShow5=0;//第5位数码管要显示的内容
unsigned char ucDigShow4=0;//第4位数码管要显示的内容
unsigned char ucDigShow3=0;//第3位数码管要显示的内容
unsigned char ucDigShow2=0;//第2位数码管要显示的内容
unsigned char ucDigShow1=0;//第1位数码管要显示的内容


unsigned char ucDigShowTemp=0; //临时中间变量

unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量


unsigned char ucDisplayUpdate=1; //更新显示标志

unsigned long ulSource=0;//原始数据    比如在加法运算中的被加数
unsigned long ulOther=0; //另外一个参与运算的数据比如在加法运算中的加数
unsigned long ulResult=0; //运算结果
unsigned char ucOperator=0; //运行符号。0代表当前没有选择运行符号。1代表当前的运算符是加法。

/* 注释一:
*ucWd变量是本程序最核心的变量,代表数码管显示哪一个窗口
*本程序只有两个窗口,他们分别是:
*第一个窗口:原始数据和运算结果窗口。比如加法运算中的被加数
*第二个窗口:第二个参与运行的数据窗口。比如加法运算中的加数
*/
unsigned char ucWd=1;

code unsigned char dig_table[]=
{
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
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       key_service();
       display_service();
   }

}



void display_service()//放在定时中断里的显示应用程序
{
if(ucDisplayUpdate==1)//有数据更新显示
{
   ucDisplayUpdate=0;
         switch(ucWd)   //本程序最核心的变量ucWd
         {
         case 1://窗口1原始数据和运算结果窗口
                if(ulSource>=10000000)
                                {
                                   ucDigShow8=ulSource/10000000;
                                }
                                else
                                {
                             ucDigShow8=10;//数据显示空
                                }


                if(ulSource>=1000000)
                                {
                                   ucDigShow7=ulSource%10000000/1000000;
                                }
                                else
                                {
                             ucDigShow7=10;//数据显示空
                                }


                if(ulSource>=100000)
                                {
                                   ucDigShow6=ulSource%1000000/100000;
                                }
                                else
                                {
                             ucDigShow6=10;//数据显示空
                                }

                if(ulSource>=10000)
                                {
                                   ucDigShow5=ulSource%100000/10000;
                                }
                                else
                                {
                             ucDigShow5=10;//数据显示空
                                }

                if(ulSource>=1000)
                                {
                                   ucDigShow4=ulSource%10000/1000;
                                }
                                else
                                {
                             ucDigShow4=10;//数据显示空
                                }

                if(ulSource>=100)
                                {
                                   ucDigShow3=ulSource%1000/100;
                                }
                                else
                                {
                             ucDigShow3=10;//数据显示空
                                }

                if(ulSource>=10)
                                {
                                   ucDigShow2=ulSource%100/10;
                                }
                                else
                                {
                             ucDigShow2=10;//数据显示空
                                }

                                ucDigShow1=ulSource%10;

                break;
         case 2://窗口2第二个参与运算数据的窗口比如加法运算中的加数
                if(ulOther>=10000000)
                                {
                                   ucDigShow8=ulOther/10000000;
                                }
                                else
                                {
                             ucDigShow8=10;//数据显示空
                                }


                if(ulOther>=1000000)
                                {
                                   ucDigShow7=ulOther%10000000/1000000;
                                }
                                else
                                {
                             ucDigShow7=10;//数据显示空
                                }


                if(ulOther>=100000)
                                {
                                   ucDigShow6=ulOther%1000000/100000;
                                }
                                else
                                {
                             ucDigShow6=10;//数据显示空
                                }

                if(ulOther>=10000)
                                {
                                   ucDigShow5=ulOther%100000/10000;
                                }
                                else
                                {
                             ucDigShow5=10;//数据显示空
                                }

                if(ulOther>=1000)
                                {
                                   ucDigShow4=ulOther%10000/1000;
                                }
                                else
                                {
                             ucDigShow4=10;//数据显示空
                                }

                if(ulOther>=100)
                                {
                                   ucDigShow3=ulOther%1000/100;
                                }
                                else
                                {
                             ucDigShow3=10;//数据显示空
                                }

                if(ulOther>=10)
                                {
                                   ucDigShow2=ulOther%100/10;
                                }
                                else
                                {
                             ucDigShow2=10;//数据显示空
                                }

                                ucDigShow1=ulOther%10;

                break;
         }
}
}

void display_drive()//放在定时中断里的数码管驱动函数
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }
}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

/* 注释二:
*注意,此处的延时delay_short必须尽可能小,否则动态扫描数码管的速度就不够。
*/
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void key_scan()//按键扫描函数 放在定时中断里
{

switch(ucKeyStep)
{
   case 1:   //按键扫描输出第ucRowRecord列低电平
            if(ucRowRecord==1)//第一列输出低电平
                  {
             key_dr1=0;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==2)//第二列输出低电平
                  {
             key_dr1=1;      
             key_dr2=0;
             key_dr3=1;   
             key_dr4=1;
                  }
            else if(ucRowRecord==3)//第三列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=0;   
             key_dr4=1;
                  }
            else   //第四列输出低电平
                  {
             key_dr1=1;      
             key_dr2=1;
             key_dr3=1;   
             key_dr4=0;
                  }

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

   case 2:   //此处的小延时用来等待刚才列输出信号稳定,再判断输入信号。不是去抖动延时。
          uiKeyTimeCnt++;
                  if(uiKeyTimeCnt>1)
                  {
                     uiKeyTimeCnt=0;
             ucKeyStep++;   //切换到下一个运行步骤
                  }
            break;

   case 3:
          if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==1)
          {
             ucKeyStep=1;//如果没有按键按下,返回到第一个运行步骤重新开始扫描
             ucKeyLock=0;//按键自锁标志清零
             uiKeyTimeCnt=0; //按键去抖动延时计数器清零,此行非常巧妙   
   
                         ucRowRecord++;//输出下一列
                         if(ucRowRecord>4)
                         {
                            ucRowRecord=1; //依次输出完四列之后,继续从第一列开始输出低电平
                         }

          }
                  else if(ucKeyLock==0)//有按键按下,且是第一次触发
                  {
                     if(key_sr1==0&&key_sr2==1&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零

                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=1;//触发1号键 对应朱兆祺学习板的S1键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=2;//触发2号键 对应朱兆祺学习板的S2键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=3;//触发3号键 对应朱兆祺学习板的S3键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=4;//触发4号键 对应朱兆祺学习板的S4键
                           }

                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==0&&key_sr3==1&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=5;//触发5号键 对应朱兆祺学习板的S5键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=6;//触发6号键 对应朱兆祺学习板的S6键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=7;//触发7号键 对应朱兆祺学习板的S7键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=8;//触发8号键 对应朱兆祺学习板的S8键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==0&&key_sr4==1)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=9;//触发9号键 对应朱兆祺学习板的S9键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=10;//触发10号键 对应朱兆祺学习板的S10键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=11;//触发11号键 对应朱兆祺学习板的S11键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=12;//触发12号键 对应朱兆祺学习板的S12键
                           }
                              }
                        
                         }
                     else if(key_sr1==1&&key_sr2==1&&key_sr3==1&&key_sr4==0)
                         {
                            uiKeyTimeCnt++;//去抖动延时计数器
                              if(uiKeyTimeCnt>const_key_time)
                              {
                                 uiKeyTimeCnt=0;
                                 ucKeyLock=1;//自锁按键置位,避免一直触发,只有松开按键,此标志位才会被清零
                     if(ucRowRecord==1)//第一列输出低电平
                           {
                                    ucKeySec=13;//触发13号键 对应朱兆祺学习板的S13键
                           }
                     else if(ucRowRecord==2)//第二列输出低电平
                           {
                                    ucKeySec=14;//触发14号键 对应朱兆祺学习板的S14键
                           }
                     else if(ucRowRecord==3)//第三列输出低电平
                           {
                                    ucKeySec=15;//触发15号键 对应朱兆祺学习板的S15键
                           }
                     else   //第四列输出低电平
                           {
                                    ucKeySec=16;//触发16号键 对应朱兆祺学习板的S16键
                           }
                              }
                        
                         }
                  
                  }
            break;

}


}

/* 注释三:
*按键服务程序操作的精髓在于根据当前系统处于什么窗口下,在此窗口下的运算符处于
*什么状态,然后紧紧围绕着不同的窗口ucWd,不同的ucOperator来执行不同的操作。
*/
void key_service() //第三区 按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 1号键 对应朱兆祺学习板的S1键
          number_key_input(1);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
    case 2:// 2号键 对应朱兆祺学习板的S2键
          number_key_input(2);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 3:// 3号键 对应朱兆祺学习板的S3键
          number_key_input(3);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         
    case 4:// 4号键 对应朱兆祺学习板的S4键
          number_key_input(4);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 5:// 5号键 对应朱兆祺学习板的S5键
          number_key_input(5);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 6:// 6号键 对应朱兆祺学习板的S6键
          number_key_input(6);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 7:// 7号键 对应朱兆祺学习板的S7键
          number_key_input(7);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 8:// 8号键 对应朱兆祺学习板的S8键
          number_key_input(8);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 9:// 9号键 对应朱兆祺学习板的S9键
          number_key_input(9);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 10:// 把这个按键专门用来输入数字0    对应朱兆祺学习板的S10键
          number_key_input(0);//由于数字按键的代码相似度高,因此把具体代码封装在这个函数里
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 11:// 11号键 对应朱兆祺学习板的S11键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 12:// 12号键 对应朱兆祺学习板的S12键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 13:// 13号键 加号按键对应朱兆祺学习板的S13键
          switch(ucWd)
                 {
                case 1:   //在原始数据和运算结果的窗口下
                   ucOperator=1; //加法
                   ulOther=ulSource;//第二个运算数默认等于原始数
                   ucDisplayUpdate=1;//刷新显示窗口
                               break;
                case 2:   //在第二个参与运算数据的窗口下
                   ulResult=ulSource+ulOther;//连加
                   ulSource=ulResult; //下一次运算的原始数据默认为当前运算结果,方便连加功能
                   ucWd=1;      //切换到第一个窗口
                   ucDisplayUpdate=1;//刷新显示窗口
                           break;
          }

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 14:// 14号键 等于号按键对应朱兆祺学习板的S14键
          switch(ucWd)
                 {
                case 1:   //在原始数据和运算结果的窗口下
                           switch(ucOperator)//根据不同的运算符号进行不同的操作
                           {
                                   case 0://无运算符号

                                      break;
                                   case 1://加法
                            ulResult=ulSource+ulOther;//连加
                            ulSource=ulResult; //下一次运算的原始数据默认为当前运算结果,方便连加功能
                            ucDisplayUpdate=1;//刷新显示窗口
                                      break;
                                   case 2://减法本程序没有减法功能,如果读者想增加减法程序,可以按键这个框架添加下去

                                      break;
                       
                           }
                               break;
                case 2:   //在第二个参与运算数据的窗口下
                           switch(ucOperator)//根据不同的运算符号进行不同的操作
                           {
                                   case 1://加法
                            ulResult=ulSource+ulOther;//连加
                            ulSource=ulResult; //下一次运算的原始数据默认为当前运算结果,方便连加功能
                            ucWd=1;      //切换到第一个窗口
                            ucDisplayUpdate=1;//刷新显示窗口
                                      break;
                                   case 2://减法本程序没有减法功能,如果读者想增加减法程序,可以按键这个框架添加下去

                                      break;
                       
                           }
                           break;
          }

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 15:// 15号键 对应朱兆祺学习板的S15键

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 16:// 16号键 清除按键 相当于复位的功能。重新输入数据对应朱兆祺学习板的S16键
              ulSource=0;
                  ulOther=0;
          ulResult=0;
                  ucOperator=0;
          ucWd=1;      //切换到第一个窗口
          ucDisplayUpdate=1;//刷新显示窗口
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
}               
}


/* 注释四:
* 此处参与运算的输入数字ucWhichKey记得用最大变量类型unsigned long,可以避免数据溢出等错误
*/
void number_key_input(unsigned long ucWhichKey)//由于数字按键的代码相似度高,因此封装在这个函数里
{


    switch(ucWd)
           {
           case 1:   //在原始数据和运算结果的窗口下
            switch(ucOperator)//根据不同的运算符号进行不同的操作
                        {
                           case 0://无运算符号按键输入原始数据,比如被加输
                                if(ulSource<=9999999) //最大只能输入8位数
                                        {
                     ulSource=ulSource*10+ucWhichKey;//十进制的数值移位方法。
                                        }
                                break;
                           default://在已经按下了运算符号的情况下
                  ulOther=0;//第二个运算数先清零,再输入新的数据,然后马上切换到第2个窗口下
                  ulOther=ucWhichKey;
                  ucWd=2; //马上切换到第二个窗口下
                                break;
                       
                        }

            ucDisplayUpdate=1;//刷新显示窗口
                        break;
           case 2:   //在第二个参与运算数据的窗口下   按键输入第二个参与运算的数据
                        if(ulOther<=9999999) //最大只能输入8位数
                        {
               ulOther=ulOther*10+ucWhichKey;//十进制的数值移位方法。
                  }

            ucDisplayUpdate=1;//刷新显示窗口
                  break;
    }

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断
key_scan(); //放在定时中断里的按键扫描函数
if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
         beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
         beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//放在定时中断里的数码管驱动函数

/* 注释五:
*注意,此处的重装初始值不能太大,否则动态扫描数码管的速度就不够。我把原来常用的2000改成了500。
*/
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

led_dr=0;
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

hc595_drive(0x00,0x00);
TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
}
void initial_peripheral() //第二区 初始化外围
{


EA=1;   //开总中断
ET0=1;    //允许定时中断
TR0=1;    //启动定时中断



}

总结陈词:
这节讲了加法简易计算器的程序项目。为了让读者理解运动,按键,显示是如何有规律关联起来的,下节会继续讲一个相关的小项目程序。欲知详情,请听下回分解-----数码管作为仪表盘显示跑马灯的方向,速度和运行状态。

(未完待续,下节更精彩,不要走开哦)

东东 发表于 2014-3-28 18:59:20

不错,赞一个!

jianhong_wu 发表于 2014-4-3 01:39:15

第三十七节:数码管作为仪表盘显示跑马灯的方向,速度和运行状态。
开场白:    我在第24节中讲过按键控制跑马灯的方向,速度和运行状态的项目程序,只可惜那个程序不能直观地显示运行中的三种状态,这节我决定在24节的基础上,增加一个数码管显示作为类似汽车仪表盘的界面,实时显示跑马灯的方向,速度,和运行状态。这一节要教会大家一个知识点:继续加深理解运动,按键与数码管三者之间的关联程序框架。

具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。用S1键作为控制跑马灯的方向按键,S5键作为控制跑马灯方向的加速度按键,S9键作为控制跑马灯方向的减速度按键,S13键作为控制跑马灯方向的启动或者暂停按键。记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。

(2)实现功能:跑马灯运行:第1个至第8个LED灯一直不亮。在第9个至第16个LED灯,依次逐个亮灯并且每次只能亮一个灯。每按一次独立按键S13键,原来运行的跑马灯会暂停,原来暂停的跑马灯会运行。用S1来改变方向。用S5和S9来改变速度,每按一次按键的递增或者递减以10为单位。数码管显示:本程序只有1个窗口,这个窗口分成3个局部显示。8,7,6位数码管显示运行状态,启动时显示“on”,停止时显示“oFF”。5位数码管显示数码管方向,正向显示“n”,反向显示“U”。4,3,2,1位数码管显示速度。数值越大速度越慢,最慢的速度是550,最快的速度是50。
(3)源代码讲解如下:
#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间


#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间


void initial_myself();   
void initial_peripheral();
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);

//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(); //显示数码管字模的驱动函数
void display_service(); //显示的窗口菜单服务程序

//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);
void led_flicker_09_16(); //第9个至第16个LED的跑马灯程序,逐个亮并且每次只能亮一个.
void led_update();//LED更新函数

void T0_time();//定时中断函数
void key_service(); //按键服务的应用程序
void key_scan();//按键扫描函数 放在定时中断里


sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit led_dr=P3^5;


sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;

sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;


unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志


unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucLed_dr1=0;   //代表16个灯的亮灭状态,0代表灭,1代表亮
unsigned char ucLed_dr2=0;
unsigned char ucLed_dr3=0;
unsigned char ucLed_dr4=0;
unsigned char ucLed_dr5=0;
unsigned char ucLed_dr6=0;
unsigned char ucLed_dr7=0;
unsigned char ucLed_dr8=0;
unsigned char ucLed_dr9=0;
unsigned char ucLed_dr10=0;
unsigned char ucLed_dr11=0;
unsigned char ucLed_dr12=0;
unsigned char ucLed_dr13=0;
unsigned char ucLed_dr14=0;
unsigned char ucLed_dr15=0;
unsigned char ucLed_dr16=0;

unsigned char ucLed_update=0;//刷新变量。每次更改LED灯的状态都要更新一次。


unsigned char ucLedStep_09_16=0; //第9个至第16个LED跑马灯的步骤变量
unsigned intuiTimeCnt_09_16=0; //第9个至第16个LED跑马灯的统计定时中断次数的延时计数器

unsigned char ucLedStatus16_09=0;   //代表底层74HC595输出状态的中间变量
unsigned char ucLedStatus08_01=0;   //代表底层74HC595输出状态的中间变量

unsigned char ucLedDirFlag=0;   //方向变量,把按键与跑马灯关联起来的核心变量,0代表正方向,1代表反方向
unsigned intuiSetTimeLevel_09_16=300;//速度变量,此数值越大速度越慢,此数值越小速度越快。
unsigned char ucLedStartFlag=1;   //启动和暂停的变量,0代表暂停,1代表启动



unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容


unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志

unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Part1Update=1;//窗口1的局部1更新显示变量
unsigned char ucWd1Part2Update=1;//窗口1的局部2更新显示变量
unsigned char ucWd1Part3Update=1;//窗口1的局部3更新显示变量


//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
0x5c,//o       序号13
0x71,//F       序号14
0x3e,//U       序号15
0x37,//n       序号16
};

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
      display_service(); //显示的窗口菜单服务程序

      led_flicker_09_16(); //第9个至第16个LED的跑马灯程序,逐个亮并且每次只能亮一个.
          led_update();//LED更新函数
   }

}



/* 注释一:
* 由于本程序只有1个窗口,而这个窗口又分成3个局部,因此可以省略去窗口变量uWd,
* 只用三个局部变量ucWdxPartyUpdate就可以了。
*/

void display_service() //显示的窗口菜单服务程序
{


    if(ucWd1Part1Update==1) //更新显示当前系统是处于运行还是暂停的状态
        {
       ucWd1Part1Update=0; //及时把更新变量清零,防止一直进来更新
           if(ucLedStartFlag==1)//启动,显示on
           {
             ucDigShow8=13;//显示o
         ucDigShow7=16;//显示n
         ucDigShow6=10;//显示空
           }
           else//暂停,显示oFF
           {
                      ucDigShow8=13;//显示o
         ucDigShow7=14;//显示F
         ucDigShow6=14;//显示F
           }
        }

    if(ucWd1Part2Update==1) //更新显示当前系统是处于正方向还是反方向
        {
       ucWd1Part2Update=0; //及时把更新变量清零,防止一直进来更新
           if(ucLedDirFlag==0)//正方向,向上,显示n
           {
             ucDigShow5=16;//显示n
           }
           else//反方向,向下,显示U
           {
             ucDigShow5=15;//显示U
           }
        }

    if(ucWd1Part3Update==1) //更新显示当前系统的速度,此数值越大速度越慢,此数值越小速度越快。
        {
       ucWd1Part3Update=0; //及时把更新变量清零,防止一直进来更新

           ucDigShow4=10;//显示空这一位不用,作为空格

           if(uiSetTimeLevel_09_16>=100)
           {
          ucDigShow3=uiSetTimeLevel_09_16/100;   //显示速度的百位
           }
           else
           {
          ucDigShow3=10;   //显示空
           }

           if(uiSetTimeLevel_09_16>=10)
           {
          ucDigShow2=uiSetTimeLevel_09_16%100/10;//显示速度的十位
           }
           else
           {
          ucDigShow2=10;   //显示空
           }

       ucDigShow1=uiSetTimeLevel_09_16%10;      //显示速度的个位
        }


   


}


void key_scan()//按键扫描函数 放在定时中断里
{

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}

}


void key_service() //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 改变跑马灯方向的按键 对应朱兆祺学习板的S1键

          if(ucLedDirFlag==0) //通过中间变量改变跑马灯的方向
                  {
                     ucLedDirFlag=1;
                  }
                  else
                  {
                         ucLedDirFlag=0;
                  }

          ucWd1Part2Update=1; //及时更新显示方向

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 加速按键 对应朱兆祺学习板的S5键 uiSetTimeLevel_09_16越小速度越快
          uiSetTimeLevel_09_16=uiSetTimeLevel_09_16-10;
                  if(uiSetTimeLevel_09_16<50)//最快限定在50
                  {
                      uiSetTimeLevel_09_16=50;
                  }

          ucWd1Part3Update=1; //及时更新显示速度

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 减速按键 对应朱兆祺学习板的S9键uiSetTimeLevel_09_16越大速度越慢
          uiSetTimeLevel_09_16=uiSetTimeLevel_09_16+10;
                  if(uiSetTimeLevel_09_16>550)//最慢限定在550
                  {
                      uiSetTimeLevel_09_16=550;
                  }
          ucWd1Part3Update=1; //及时更新显示速度
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         
         
    case 4:// 启动和暂停按键 对应朱兆祺学习板的S13键ucLedStartFlag为0时代表暂停,为1时代表启动

              if(ucLedStartFlag==1)//启动和暂停两种状态循环切换
                  {
                     ucLedStartFlag=0;
                  }
                  else                   //启动和暂停两种状态循环切换
                  {
                         ucLedStartFlag=1;
                  }
          ucWd1Part1Update=1; //及时更新显示系统的运行状态,是运行还是暂停.
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
}               
}




void led_update()//LED更新函数
{

   if(ucLed_update==1)
   {
       ucLed_update=0;   //及时清零,让它产生只更新一次的效果,避免一直更新。

       if(ucLed_dr1==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x01;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xfe;
           }

       if(ucLed_dr2==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x02;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xfd;
           }

       if(ucLed_dr3==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x04;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xfb;
           }

       if(ucLed_dr4==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x08;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xf7;
           }


       if(ucLed_dr5==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x10;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xef;
           }


       if(ucLed_dr6==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x20;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xdf;
           }


       if(ucLed_dr7==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x40;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0xbf;
           }


       if(ucLed_dr8==1)
           {
              ucLedStatus08_01=ucLedStatus08_01|0x80;
           }
           else
           {
              ucLedStatus08_01=ucLedStatus08_01&0x7f;
           }

       if(ucLed_dr9==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x01;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xfe;
           }

       if(ucLed_dr10==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x02;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xfd;
           }

       if(ucLed_dr11==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x04;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xfb;
           }

       if(ucLed_dr12==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x08;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xf7;
           }


       if(ucLed_dr13==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x10;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xef;
           }


       if(ucLed_dr14==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x20;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xdf;
           }


       if(ucLed_dr15==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x40;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0xbf;
           }


       if(ucLed_dr16==1)
           {
              ucLedStatus16_09=ucLedStatus16_09|0x80;
           }
           else
           {
              ucLedStatus16_09=ucLedStatus16_09&0x7f;
           }

       hc595_drive(ucLedStatus16_09,ucLedStatus08_01);//74HC595底层驱动函数

   }
}


void display_drive()
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }

   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }



}


//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;

   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;

         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);

   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;

}


//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;

   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;

         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);

         ucTempData=ucTempData<<1;
   }

   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);

   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;

}


void led_flicker_09_16() //第9个至第16个LED的跑马灯程序,逐个亮并且每次只能亮一个.
{
if(ucLedStartFlag==1)//此变量为1时代表启动
{
   switch(ucLedStep_09_16)
   {
   case 0:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr16=0;//第16个灭
                  ucLed_dr9=1;//第9个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=1; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr15=1;//第15个亮
                  ucLed_dr16=0;//第16个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=7; //返回上一个步骤
                           }
         }
         break;
   case 1:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr9=0;//第9个灭
                  ucLed_dr10=1;//第10个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=2; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr16=1;//第16个亮
                  ucLed_dr9=0;//第9个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=0; //返回上一个步骤
                           }
         }
         break;
   case 2:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr10=0;//第10个灭
                  ucLed_dr11=1;//第11个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=3; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr9=1;//第9个亮
                  ucLed_dr10=0;//第10个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=1; //返回上一个步骤
                           }
         }
         break;
   case 3:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr11=0;//第11个灭
                  ucLed_dr12=1;//第12个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=4; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr10=1;//第10个亮
                  ucLed_dr11=0;//第11个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=2; //返回上一个步骤
                           }
         }
         break;
   case 4:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr12=0;//第12个灭
                  ucLed_dr13=1;//第13个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=5; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr11=1;//第11个亮
                  ucLed_dr12=0;//第12个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=3; //返回上一个步骤
                           }
         }
         break;
   case 5:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr13=0;//第13个灭
                  ucLed_dr14=1;//第14个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=6; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr12=1;//第12个亮
                  ucLed_dr13=0;//第13个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=4; //返回上一个步骤
                           }
         }
         break;
   case 6:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr14=0;//第14个灭
                  ucLed_dr15=1;//第15个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=7; //切换到下一个步骤
                           }
                           else//反方向
                           {
                  ucLed_dr13=1;//第13个亮
                  ucLed_dr14=0;//第14个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=5; //返回上一个步骤
                           }
         }
         break;
   case 7:
         if(uiTimeCnt_09_16>=uiSetTimeLevel_09_16) //时间到
         {
               uiTimeCnt_09_16=0; //时间计数器清零

                           if(ucLedDirFlag==0)//正方向
                           {
                  ucLed_dr15=0;//第15个灭
                  ucLed_dr16=1;//第16个亮

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=0; //返回到开始处,重新开始新的一次循环
                           }
                           else//反方向
                           {
                  ucLed_dr14=1;//第14个亮
                  ucLed_dr15=0;//第15个灭

                  ucLed_update=1;//更新显示
                  ucLedStep_09_16=6; //返回上一个步骤
                           }
         }
         break;
   
      }
   }

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(uiTimeCnt_09_16<0xffff)//设定这个条件,防止uiTimeCnt超范围。
{
      if(ucLedStartFlag==1)//此变量为1时代表启动
          {
         uiTimeCnt_09_16++;//累加定时中断的次数,
          }
}

key_scan(); //按键扫描函数






if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
//   beep_dr=1;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
//   beep_dr=0;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}

display_drive();//数码管字模的驱动函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself()//第一区 初始化单片机
{

/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0;//关闭独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

TMOD=0x01;//设置定时器0为工作方式1

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}

void initial_peripheral() //第二区 初始化外围
{


   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}



总结陈词:    前面花了大量的章节在讲数码管显示,按键,运动的关联程序框架,从下一节开始,我将会用八节内容来讲我常用的串口程序框架,内容非常精彩和震撼,思路非常简单而又实用。欲知详情,请听下回分解-----判断数据尾来接收一串数据的串口通用程序框架。
(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-5 11:19:52

本帖最后由 jianhong_wu 于 2014-4-6 22:29 编辑

第三十八节:判断数据尾来接收一串数据的串口通用程序框架。

开场白:
    在实际项目中,串口通讯不可能一次通讯只发送或接收一个字节,大部分的项目都是一次发送或者接受一串的数据。我们还要在这一串数据里解析数据协议,提取有用的数据。
这一节要教会大家三个知识点:
第一个:如何识别一串数据已经发送接收完毕。
第二个:如何在已经接收到的一串数据中解析数据尾协议并且提取有效数据。
第三个:接收一串数据的通用程序框架涉及到main循环里的串口服务程序,定时器的计时程序,串口接收中断程序的密切配合。大家要理解它们三者之间是如何关联起来的。

具体内容,请看源代码讲解。

(1)硬件平台:
基于坚鸿51单片机学习板。

(2)实现功能:
波特率是:9600 。
通讯协议:XX YYEB 00 55
         其中后三位 EB 00 55就是我所说的数据尾,它的有效数据XX YY在数据尾的前面。
      任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字前面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据尾和有效数据都是正确的。

(3)源代码讲解如下:
#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_rc_size10//接收串口中断数据的缓冲区数组大小

#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);



void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器



void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       usart_service();//串口服务程序
   }

}


void usart_service(void)//串口服务程序,在main函数里
{

      
/* 注释一:
* 识别一串数据是否已经全部接收完了的原理:
* 在规定的时间里,如果没有接收到任何一个字节数据,那么就认为一串数据被接收完了,然后就进入数据协议
* 解析和处理的阶段。这个功能的实现要配合定时中断,串口中断的程序一起阅读,要理解他们之间的关系。
*/
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据

                  //下面的代码进入数据协议解析和数据处理的阶段

                  uiRcMoveIndex=uiRcregTotal; //由于是判断数据尾,所以下标移动变量从数组的最尾端开始向0移动
            while(uiRcMoveIndex>=5)   //如果处理的数据量大于等于5(2个有效数据,3个数据头)说明还没有把缓冲区的数据处理完
            {
               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据尾eb 00 55的判断
               {
                              if(ucRcregBuf==0x01&&ucRcregBuf==0x02)//有效数据01 02的判断
                                  {
                                    uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据尾和有效数据都接收正确
                                  }
                  break;   //退出循环
               }
               uiRcMoveIndex--; //因为是判断数据尾,下标向着0的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
{
          uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
}

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

            ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
      uiSendCnt=0;//及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;
   }
                                                         
}                              


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{

beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}
总结陈词:
   这一节讲了判断数据尾的程序框架,但是在大部分的项目中,都是通过判断数据头来接收数据的,这样的程序该怎么写?欲知详情,请听下回分解-----判断数据头来接收一串数据的串口通用程序框架。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-6 12:27:36

本帖最后由 jianhong_wu 于 2014-4-7 08:58 编辑

第三十九节:判断数据头来接收一串数据的串口通用程序框架。

开场白:
上一节讲了判断数据尾的程序框架,但是在大部分的项目中,都是通过判断数据头来接收数据的,这一节要教会大家两个知识点:
第一个:如何在已经接收到的一串数据中解析数据头协议并且提取有效数据。
第二个:无论是判断数据头还是判断数据尾,无论是单片机还是上位机,最好在固定协议前多发送一个填充的无效字节0x00,因为硬件原因,第一个字节往往容易丢失。

具体内容,请看源代码讲解。

(1)硬件平台:
基于坚鸿51单片机学习板。

(2)实现功能:
波特率是:9600 。
通讯协议:EB 00 55XX YY
加无效填充字节后,上位机实际上应该发送:00EB 00 55XX YY
其中第1位00是无效填充字节,防止由于硬件原因丢失第一个字节。
其中第2,3,4位EB 00 55就是数据头
         后2位XX YY就是有效数据
任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字后面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据头和有效数据都是正确的。

(3)源代码讲解如下:
#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_rc_size10//接收串口中断数据的缓冲区数组大小

#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);



void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器



void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       usart_service();//串口服务程序
   }

}


void usart_service(void)//串口服务程序,在main函数里
{

      
/* 注释一:
* 识别一串数据是否已经全部接收完了的原理:
* 在规定的时间里,如果没有接收到任何一个字节数据,那么就认为一串数据被接收完了,然后就进入数据协议
* 解析和处理的阶段。这个功能的实现要配合定时中断,串口中断的程序一起阅读,要理解他们之间的关系。
*/
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据



                  //下面的代码进入数据协议解析和数据处理的阶段

                  uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

/* 注释二:
* 判断数据头,进入循环解析数据协议必须满足两个条件:
* 第一:最大接收缓冲数据必须大于一串数据的长度(这里是5。包括2个有效数据,3个数据头)
* 第二:游标uiRcMoveIndex必须小于等于最大接收缓冲数据减去一串数据的长度(这里是5。包括2个有效数据,3个数据头)
*/
            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
            {
               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
               {
                              if(ucRcregBuf==0x01&&ucRcregBuf==0x02)//有效数据01 02的判断
                                  {
                                    uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据头和有效数据都接收正确
                                  }
                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
{
          uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
}

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

            ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
      uiSendCnt=0;//及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;
   }
                                                         
}                              


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{

beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}
总结陈词:
   这一节讲了常用的判断数据头来接收一串数据的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,有效数据长度,有效数据,数据校验的通讯协议。这样的程序该怎么写?欲知详情,请听下回分解-----常用的自定义串口通讯协议。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-7 13:14:48

本帖最后由 jianhong_wu 于 2014-7-21 00:17 编辑

第四十节:常用的自定义串口通讯协议。
开场白:上一节讲了判断数据头的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,数据地址,有效数据长度,有效数据,数据校验的通讯协议。这一节要教会大家三个知识点:第一个:常用自定义串口通讯协议的程序框架。第二个:累加校验和的校验方法。累加和的意思是前面所有字节的数据相加,超过一个字节的溢出部分会按照固定的规则自动丢弃,不用我们管。比如以下数据:
      eb 00 55 01 00 02 0028 6b
      其中eb 00 55为数据头,01为数据类型,00 02为有效数据长度,00 28 分别为具体的有效数据,6b为前面所有字节的累加和。累加和可以用电脑系统自带的计算器来验证。打开电脑上的计算器,点击“查看”下拉的菜单,选“科学型”,然后选左边的“十六进制”,最后选右边的“字节”,然后把前面所有的字节相加,它们的和就是6b,没错吧。第三个:原子锁的使用方法,实际上是借鉴了"红金龙吸味"关于原子锁的建议,专门用来保护中断与主函数的共享数据。
具体内容,请看源代码讲解。

(1)硬件平台:基于坚鸿51单片机学习板。

(2)实现功能:波特率是:9600.通讯协议:EB 00 55GG HH HH XX XX …YYYY CY其中第1,2,3位EB 00 55就是数据头其中第4位GG就是数据类型。01代表驱动奉命,02代表驱动Led灯。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中从第7位开始,到最后一个字节Cy之前,XX..YY都是具体的有效数据。在本程序中,当数据类型是01时,有效数据代表蜂鸣器鸣叫的时间长度。当数据类型是02时,有效数据代表Led灯点亮的时间长度。最后一个字节CY是累加和,前面所有字节的累加。发送以下测试数据,将会分别控制蜂鸣器和Led灯的驱动时间长度。蜂鸣器短叫发送:eb 00 55 01 00 02 00 28 6b蜂鸣器长叫发送:eb 00 55 01 00 02 00 fa 3dLed灯短亮发送:eb 00 55 02 00 02 00 28 6cLed灯长亮发送:eb 00 55 02 00 02 00 fa3e
(3)源代码讲解如下:#include "REG52.H"

/* 注释一:
* 请评估实际项目中一串数据的最大长度是多少,并且留点余量,然后调整const_rc_size的大小。
* 本节程序把上一节的缓冲区数组大小10改成了20
*/
#define const_rc_size20//接收串口中断数据的缓冲区数组大小

#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);



void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里
void led_service(void);//Led灯的服务程序。

sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量
/* 注释二:
* 为串口计时器多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
unsigned charucSendCntLock=0; //串口计时器的原子锁

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucRcType=0;//数据类型
unsigned intuiRcSize=0;//数据长度
unsigned char ucRcCy=0;//校验累加和

unsigned intuiRcVoiceTime=0;//蜂鸣器发出声音的持续时间

unsigned intuiRcLedTime=0; //在串口服务程序中,Led灯点亮时间长度的中间变量
unsigned intuiLedTime=0;//Led灯点亮时间的长度
unsigned intuiLedCnt=0;   //Led灯点亮的计时器
unsigned char ucLedLock=0;//Led灯点亮时间的原子锁

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       usart_service();//串口服务程序
       led_service(); //Led灯的服务程序
   }

}

void led_service(void)
{
   if(uiLedCnt<uiLedTime)
   {
      led_dr=1; //开Led灯
   }
   else
   {
      led_dr=0; //关Led灯
   }
}

void usart_service(void)//串口服务程序,在main函数里
{
/* 注释三:
* 我借鉴了朱兆祺的变量命名习惯,单个字母的变量比如i,j,k,h,这些变量只用作局部变量,直接在函数内部定义。
*/
   unsigned int i;
      
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据



                  //下面的代码进入数据协议解析和数据处理的阶段

                  uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动


            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
            {
               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
               {
                         ucRcType=ucRcregBuf;   //数据类型一个字节

                                       uiRcSize=ucRcregBuf;   //数据长度两个字节
                                       uiRcSize=uiRcSize<<8;
                                       uiRcSize=uiRcSize+ucRcregBuf;
                                                               
                                       ucRcCy=ucRcregBuf;   //记录最后一个字节的校验
                     ucRcregBuf=0;//清零最后一个字节的累加和变量
/* 注释四:
* 计算校验累加和的方法:除了最后一个字节,其它前面所有的字节累加起来,
* 溢出的不用我们管,C语言编译器会按照固定的规则自动处理。
* 以下for循环里的(3+1+2+uiRcSize),其中3代表3个字节数据头,1代表1个字节数据类型,
* 2代表2个字节的数据长度变量,uiRcSize代表实际上一串数据中的有效数据个数。
*/
                                          for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                                       {
                                                 ucRcregBuf=ucRcregBuf+ucRcregBuf;
                     }      


                                       if(ucRcCy==ucRcregBuf)//如果校验正确,则进入以下数据处理
                                       {                                                
                         switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                                             {

                           case 0x01:   //驱动蜂鸣器发出声音,并且可以控制蜂鸣器持续发出声音的时间长度
      
                                  uiRcVoiceTime=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiRcVoiceTime=uiRcVoiceTime<<8;
                                  uiRcVoiceTime=uiRcVoiceTime+ucRcregBuf;

                                  ucVoiceLock=1;//共享数据的原子锁加锁
                                  uiVoiceCnt=uiRcVoiceTime; //蜂鸣器发出声音
                                  ucVoiceLock=0;//共享数据的原子锁解锁

                                                            break;      
                                                                        
                           case 0x02:   //点亮一个LED灯,并且可以控制LED灯持续亮的时间长度
                                  uiRcLedTime=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiRcLedTime=uiRcLedTime<<8;
                                  uiRcLedTime=uiRcLedTime+ucRcregBuf;

                                  ucLedLock=1;//共享数据的原子锁加锁
                                  uiLedTime=uiRcLedTime; //更改点亮Led灯的时间长度
                                                                  uiLedCnt=0;//在本程序中,清零计数器就等于自动点亮Led灯
                                  ucLedLock=0;//共享数据的原子锁解锁
                                                            break;
                                                                        
                         }

                                       }      

                     break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断

/* 注释五:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
if(ucSendCntLock==0)//原子锁判断
{
   ucSendCntLock=1; //加锁
   if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
   {
      uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
   }
   ucSendCntLock=0; //解锁
}

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

if(ucLedLock==0)//原子锁判断
{
   if(uiLedCnt<uiLedTime)
         {
            uiLedCnt++;//Led灯点亮的时间计时器
         }
}

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

            ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

      if(ucSendCntLock==0)//原子锁判断
      {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;//及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
                  ucSendCntLock=0; //解锁
                }
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;
   }
                                                         
}                              


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{
led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}


总结陈词:这一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,我会用另外一种响应速度更快的串口程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----在串口接收中断里即时解析数据头的特殊程序框架。
(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-19 11:51:19

第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。

开场白:
上一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,这样的程序框架可能会满足不了系统对速度的要求,这一节就是要介绍另外一种响应速度更加快的串口程序框架,要教会大家一个知识点:在串口接收中断里即时解析数据头的特殊程序框架。我在这种程序框架里,会尽量简化数据头和数据尾,同时也简化校验,目的都是为了提高响应速度。
具体内容,请看源代码讲解。

(1)硬件平台:
基于坚鸿51单片机学习板。

(2)实现功能:
波特率是:9600.
通讯协议:EBGG XX XX XX XX ED
其中第1位EB就是数据头.
其中第2位GG就是数据类型。01代表驱动蜂鸣器,02代表驱动Led灯。
其中第3,4,5,6位XX就是有效数据长度。高位在左,低位在右。
其中第7位ED就是数据尾,在这里也起一部分校验的作用,虽然不是累加和的方式。

在本程序中,
当数据类型是01时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么蜂鸣器就鸣叫一声表示正确。
当数据类型是02时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么LED灯就会闪烁一下表示正确。
十进制的123456789等于十六进制的75bcd15 。
发送以下测试数据,将会分别控制蜂鸣器Led灯。
控制蜂鸣器发送:eb 01 07 5b cd 15 ed
控制LED灯发送:eb 02 07 5b cd 15 ed

(3)源代码讲解如下:
#include "REG52.H"


#define const_rc_size20//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

#define const_voice_short80   //蜂鸣器短叫的持续时间
#define const_led_short80    //LED灯亮的持续时间

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);



void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void led_service(void);//Led灯的服务程序。

sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口


unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁



unsigned intuiRcVoiceTime=0;//蜂鸣器发出声音的持续时间

unsigned intuiLedCnt=0;   //Led灯点亮的计时器
unsigned char ucLedLock=0;//Led灯点亮时间的原子锁

unsigned long ulBeepData=0; //蜂鸣器的数据
unsigned long ulLedData=0; //LED的数据

unsigned char ucUsartStep=0; //串口接收字节的步骤变量

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       led_service(); //Led灯的服务程序
   }

}

void led_service(void)
{
   if(uiLedCnt<const_led_short)
   {
      led_dr=1; //开Led灯
   }
   else
   {
      led_dr=0; //关Led灯
   }
}



void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断

/* 注释一:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

if(ucLedLock==0)//原子锁判断
{
   if(uiLedCnt<const_led_short)
   {
            uiLedCnt++;//Led灯点亮的时间计时器
   }

}

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口接收数据中断      
{      
/* 注释二:
* 以下就是吴坚鸿在串口接收中断里即时解析数据头的特殊程序框架,
* 它的特点是靠数据头来启动接受有效数据,靠数据尾来识别一串数据接受完毕,
* 这里的数据尾也起到一部分的校验作用,让数据更加可靠。这种程序结构适合应用
* 在传输的数据长度不是很长,而且要求响应速度非常高的实时场合。在这种实时要求
* 非常高的场合中,我就不像之前一样做数据累加和的复杂运算校验,只用数据尾来做简单的
* 校验确认,目的是尽可能提高处理速度。
*/

   if(RI==1)
   {
      RI = 0;

      switch(ucUsartStep) //串口接收字节的步骤变量
      {
            case 0:
                             ucRcregBuf=SBUF;   
               if(ucRcregBuf==0xeb)//数据头判断
               {
                                     ucRcregBuf=0;//数据头及时清零,为下一串数据的接受判断做准备
                                     uiRcregTotal=1;//缓存数组的下标初始化
                     ucUsartStep=1;//如果数据头正确,则切换到下一步,依次把上位机来的数据存入数组缓冲区
               }
               break;
            case 1:
                             ucRcregBuf=SBUF;//依次把上位机来的数据存入数组缓冲区
                               uiRcregTotal++; //下标移动
                               if(uiRcregTotal>=7)//已经接收了7个字节
                               {
                   if(ucRcregBuf==0xed)//数据尾判断,也起到一部分校验的作用,让数据更加可靠,虽然没有用到累加和的检验方法
                                   {
                                     ucRcregBuf=0;//数据尾及时清零,为下一串数据的接受判断做准备                                     
                                     switch(ucRcregBuf) //根据不同的数据类型来做不同的数据处理
                                           {
                                             case 0x01://与蜂鸣器相关
                              ulBeepData=ucRcregBuf; //把四个字节的数据合并成一个long型的数据
                                                          ulBeepData=ulBeepData<<8;
                                                          ulBeepData=ulBeepData+ucRcregBuf;
                                                          ulBeepData=ulBeepData<<8;
                                                          ulBeepData=ulBeepData+ucRcregBuf;
                                                          ulBeepData=ulBeepData<<8;
                                                          ulBeepData=ulBeepData+ucRcregBuf;
                                                                if(ulBeepData==123456789)//如果此数据等于十进制的123456789,表示数据正确
                                                                {
                                                                  ucVoiceLock=1;//共享数据的原子锁加锁
                                    uiVoiceCnt=const_voice_short; //蜂鸣器发出声音
                                    ucVoiceLock=0;//共享数据的原子锁解锁
                                                                }

                                                        break;

                                             case 0x02://与Led灯相关
                              ulLedData=ucRcregBuf; //把四个字节的数据合并成一个long型的数据
                                                          ulLedData=ulLedData<<8;
                                                          ulLedData=ulLedData+ucRcregBuf;
                                                          ulLedData=ulLedData<<8;
                                                          ulLedData=ulLedData+ucRcregBuf;
                                                          ulLedData=ulLedData<<8;
                                                          ulLedData=ulLedData+ucRcregBuf;
                                                                if(ulLedData==123456789)//如果此数据等于十进制的123456789,表示数据正确
                                                                {
                                                                  ucLedLock=1;//共享数据的原子锁加锁
                                    uiLedCnt=0;//在本程序中,清零计数器就等于自动点亮Led灯
                                    ucLedLock=0;//共享数据的原子锁解锁
                                                                }



                                                        break;
                                           }

                                   }

                   ucUsartStep=0;   //返回上一步数据头判断,为下一次的新数据接收做准备
                               }
               break;
      }
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;
   }
                                                         
}                              


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{
led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}
总结陈词:
    前面花了4节内容仔细讲了各种串口接收数据的常用框架,从下一节开始,我开始讲串口发送数据的程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----通过串口用delay延时方式发送一串数据。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-22 10:57:04

第四十二节:通过串口用delay延时方式发送一串数据。

开场白:
   上一节讲了在串口接收中断里即时解析数据头的特殊程序框架。这节开始讲串口发送数据需要特别注意的地方和程序框架,要教会大家一个知识点:根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。

具体内容,请看源代码讲解。

(1)硬件平台:
基于坚鸿51单片机学习板。

(2)实现功能:
波特率是:9600.
按一次按键S1,单片机就往上位机发送以下一串数据:
eb 00 55 01 00 00 00 00 41

(3)源代码讲解如下:
#include "REG52.H"


#define const_send_size10//串口发送数据的缓冲区数组大小

#define const_key_time120    //按键去抖动延时的时间

#define const_voice_short40   //蜂鸣器短叫的持续时间

void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayshort);
void delay_long(unsigned int uiDelaylong);

void eusart_send(unsigned char ucSendData);//发送一个字节,内部自带每个字节之间的延时

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

void key_service(); //按键服务的应用程序
void key_scan(); //按键扫描函数 放在定时中断里

sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平



unsigned char ucSendregBuf; //接收串口中断数据的缓冲区数组


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
   }

}



void eusart_send(unsigned char ucSendData)
{

ES = 0; //关串口中断
TI = 0; //清零串口发送完成中断请求标志
SBUF =ucSendData; //发送一个字节

/* 注释一:
* 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
* 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
* 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
* 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
* 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
*/

delay_short(400);//每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

TI = 0; //清零串口发送完成中断请求标志
ES = 1; //允许串口中断

}

void key_scan()//按键扫描函数 放在定时中断里
{

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}



}


void key_service() //第三区 按键服务的应用程序
{
unsigned int i;

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 1号键 对应朱兆祺学习板的S1键
          ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
          ucSendregBuf=0x00;
          ucSendregBuf=0x55;
          ucSendregBuf=0x01;
          ucSendregBuf=0x00;
          ucSendregBuf=0x00;
          ucSendregBuf=0x00;
          ucSendregBuf=0x00;
          ucSendregBuf=0x41;

                  for(i=0;i<9;i++)
                  {
                     eusart_send(ucSendregBuf);//发送一串数据给上位机
                  }

          ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

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



void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断

/* 注释二:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
       
   }
}

key_scan();//按键扫描函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口中断      
{      

   if(RI==1)
   {
      RI = 0;   //接收中断,及时把接收中断标志位清零

      
   
   }
   else
   {
      TI = 0;    //发送中断,及时把发送中断标志位清零
   }
                                                         
}                              

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{
/* 注释三:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//串口波特率9600。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}
总结陈词:
这节在每个字节之间都添加了delay延时来等待每个字节的发送完成,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时,这种程序框架是什么样的?欲知详情,请听下回分解-----通过串口用计数延时方式发送一串数据。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-4-25 11:02:42

本帖最后由 jianhong_wu 于 2014-4-26 10:25 编辑

第四十三节:通过串口用计数延时方式发送一串数据。

开场白:
上一节讲了通过串口用delay延时方式发送一串数据,这种方式要求发送一串数据的时候一气呵成,期间不能执行其它任务,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时。本节要教会大家两个知识点:
第一个:用计数延时方式发送一串数据的程序框架。
第二个:环形消息队列的程序框架。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于坚鸿51单片机学习板。

(2)实现功能:
   波特率是:9600.
用坚鸿51单片机学习板中的S1,S5,S9,S13作为独立按键。
    按一次按键S1,发送EB 00 55 01 00 00 00 00 41
按一次按键S5,发送EB 00 55 02 00 00 00 00 42
按一次按键S9,发送EB 00 55 03 00 00 00 00 43
按一次按键S13,发送EB 00 55 04 00 00 00 00 44
(3)源代码讲解如下:
#include "REG52.H"


#define const_send_time100//累计主循环次数的计数延时 请根据项目实际情况来调整此数据大小

#define const_send_size10//串口发送数据的缓冲区数组大小

#define const_Message_size10//环形消息队列的缓冲区数组大小

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_voice_short40   //蜂鸣器短叫的持续时间

void initial_myself(void);   
void initial_peripheral(void);
//void delay_short(unsigned int uiDelayshort);
void delay_long(unsigned int uiDelaylong);

void eusart_send(unsigned char ucSendData);//发送一个字节,内部没有每个字节之间的延时
void send_service(void);//利用累计主循环次数的计数延时方式来发送一串数据

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

void key_service(void); //按键服务的应用程序
void key_scan(void); //按键扫描函数 放在定时中断里


void insert_message(unsigned char ucMessageTemp);//插入新的消息到环形消息队列里
unsigned char get_message(void);//从环形消息队列里提取消息



sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平



unsigned char ucSendregBuf; //串口发送数据的缓冲区数组

unsigned char ucMessageBuf; //环形消息队列的缓冲区数据
unsigned intuiMessageCurrent=0;//环形消息队列的取数据当前位置
unsigned intuiMessageInsert=0;//环形消息队列的插入新消息时候的位置
unsigned intuiMessageCnt=0;//统计环形消息队列的消息数量等于0时表示消息队列里没有消息

unsigned char ucMessage=0; //当前获取到的消息

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志


unsigned char ucSendStep=0;//发送一串数据的运行步骤
unsigned intuiSendTimeCnt=0; //累计主循环次数的计数延时器

unsigned int uiSendCnt=0; //发送数据时的中间变量

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
          send_service();//利用累计主循环次数的计数延时方式来发送一串数据
   }

}

/* 注释一:
* 通过判断数组下标是否超范围的条件,把一个数组的首尾连接起来,就像一个环形,
* 因此命名为环形消息队列。环形消息队列有插入消息,获取消息两个核心函数,以及一个
* 统计消息总数的uiMessageCnt核心变量,通过此变量,我们可以知道消息队列里面是否有消息需要处理.
* 我在做项目中很少用消息队列的,印象中我只在两个项目中用过消息队列这种方法。大部分的单片机
* 项目其实直接用一两个中间变量就可以起到传递消息的作用,就能满足系统的要求。以下是各变量的含义:
* #define const_Message_size10//环形消息队列的缓冲区数组大小
* unsigned char ucMessageBuf; //环形消息队列的缓冲区数据
* unsigned intuiMessageCurrent=0;//环形消息队列的取数据当前位置
* unsigned intuiMessageInsert=0;//环形消息队列的插入新消息时候的位置
* unsigned intuiMessageCnt=0;//统计环形消息队列的消息数量等于0时表示消息队列里没有消息
*/

void insert_message(unsigned char ucMessageTemp)//插入新的消息到环形消息队列里
{
   if(uiMessageCnt<const_Message_size)//消息总数小于环形消息队列的缓冲区才允许插入新消息
   {
      ucMessageBuf=ucMessageTemp;

          uiMessageInsert++;//插入新消息时候的位置
          if(uiMessageInsert>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
          {
             uiMessageInsert=0;
          }
      uiMessageCnt++; //消息数量累加等于0时表示消息队列里没有消息
   }
}

unsigned char get_message(void)//从环形消息队列里提取消息
{
   unsigned char ucMessageTemp=0;//返回的消息中间变量,默认为0

   if(uiMessageCnt>0)//只有消息数量大于0时才可以提取消息
   {
      ucMessageTemp=ucMessageBuf;
          uiMessageCurrent++;//环形消息队列的取数据当前位置
          if(uiMessageCurrent>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
          {
             uiMessageCurrent=0;
          }
      uiMessageCnt--; //每提取一次,消息数量就减一等于0时表示消息队列里没有消息
   }

   return ucMessageTemp;
}


void send_service(void)//利用累计主循环次数的计数延时方式来发送一串数据
{
switch(ucSendStep)//发送一串数据的运行步骤
{
    case 0:   //从环形消息队列里提取消息
         if(uiMessageCnt>0)//说明有消息需要处理
               {
                  ucMessage=get_message();
            switch(ucMessage)   //消息处理
                        {
                           case 1:
                  ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x55;
                  ucSendregBuf=0x01;    //01代表1号键
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x41;

                  uiSendCnt=0; //发送数据的中间变量清零
                  uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                  ucSendStep=1; //切换到下一步发送一串数据
                              break;
                           case 2:
                  ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x55;
                  ucSendregBuf=0x02;    //02代表2号键
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x42;

                  uiSendCnt=0; //发送数据的中间变量清零
                  uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                  ucSendStep=1; //切换到下一步发送一串数据
                              break;
                           case 3:
                  ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x55;
                  ucSendregBuf=0x03;    //03代表3号键
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x43;

                  uiSendCnt=0; //发送数据的中间变量清零
                  uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                  ucSendStep=1; //切换到下一步发送一串数据
                              break;
                           case 4:
                  ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x55;
                  ucSendregBuf=0x04;    //04代表4号键
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x00;
                  ucSendregBuf=0x44;

                  uiSendCnt=0; //发送数据的中间变量清零
                  uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                  ucSendStep=1; //切换到下一步发送一串数据
                              break;

               default://如果没有符合要求的消息,则不处理

                              ucSendStep=0; //维持现状,不切换
                              break;
                        }
               }
             break;

    case 1://利用累加主循环次数的计数延时方式来发送一串数据

/* 注释二:
* 这里的计数延时为什么不用累计定时中断次数的延时,而用累计主循环次数的计数延时?
* 因为本程序定时器中断一次需要500个指令时间,时间分辨率太低,不方便微调时间。因此我
* 就用累计主循环次数的计数延时方式,在做项目的时候,各位读者应该根据系统的实际情况
* 来调整const_send_time的大小。
*/
         uiSendTimeCnt++;//累计主循环次数的计数延时,为每个字节之间增加延时,
               if(uiSendTimeCnt>const_send_time)//请根据实际系统的情况,调整const_send_time的大小
               {
                  uiSendTimeCnt=0;

                        eusart_send(ucSendregBuf);//发送一串数据给上位机
            uiSendCnt++;
                        if(uiSendCnt>=9) //说明数据已经发送完毕
                        {
                           uiSendCnt=0;
               ucSendStep=0; //返回到上一步,处理其它未处理的消息
                        }
               }

             break;
}

}


void eusart_send(unsigned char ucSendData)
{

ES = 0; //关串口中断
TI = 0; //清零串口发送完成中断请求标志
SBUF =ucSendData; //发送一个字节

/* 注释三:
* 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
* 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
* 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
* 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
* 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
*/

//delay_short(400);//因为外部在每个发送字节之间用了累计主循环次数的计数延时,因此不要此行的delay延时

TI = 0; //清零串口发送完成中断请求标志
ES = 1; //允许串口中断

}


void key_scan(void)//按键扫描函数 放在定时中断里
{


if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}


}


void key_service(void) //第三区 按键服务的应用程序
{


switch(ucKeySec) //按键服务状态切换
{
    case 1:// 1号键 对应朱兆祺学习板的S1键

          insert_message(0x01);//把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 2:// 2号键 对应朱兆祺学习板的S5键

          insert_message(0x02);//把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
    case 3:// 3号键 对应朱兆祺学习板的S9键

          insert_message(0x03);//把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;
    case 4:// 4号键 对应朱兆祺学习板的S13键

          insert_message(0x04);//把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;//原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

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

}      
}



void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断

/* 注释四:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

key_scan();//按键扫描函数


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口中断      
{      

   if(RI==1)
   {
      RI = 0;   //接收中断,及时把接收中断标志位清零

      
   
   }
   else
   {
      TI = 0;    //发送中断,及时把发送中断标志位清零
   }
                                                         
}                              



//void delay_short(unsigned int uiDelayShort)
//{
//   unsigned int i;
//   for(i=0;i<uiDelayShort;i++)
//   {
//   ;   //一个分号相当于执行一条空语句
//   }
//}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{
/* 注释五:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//串口波特率9600。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}
总结陈词:
       前面几个章节中,每个章节要么独立地讲解串口收数据,要么独立地讲解发数据,实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。读者只要把我的串口收发程序结合起来,就很容易实现这样的功能,我就不再详细讲解了。从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-5-3 08:45:37

本帖最后由 jianhong_wu 于 2014-7-21 00:20 编辑

第四十四节:从机的串口收发综合程序框架

开场白:
根据上一节的预告,本来这一节内容打算讲“利用AT24C02进行掉电后的数据保存”的,但是由于网友“261854681”强烈建议我讲一个完整的串口收发程序实例,因此我决定再花两节篇幅讲讲这方面的内容。
实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。
这节先讲从机的收发端程序实例。要教会大家三个知识点:
第一个:为了保证串口中断接收的数据不丢失,在初始化时必须设置IP = 0x10,相当于把串口中断设置为最高优先级,这个时候,串口中断可以打断任何其他的中断服务函数,实现中断嵌套。
第二个:从机端的收发端程序框架。
第三个:从机的状态指示程序框架。可以指示待机,通讯中,超时出错三种状态。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于坚鸿51单片机学习板。

(2)实现功能:
显示和独立按键部分根据第29节的程序来改编,用坚鸿51单片机学习板中的S1,S5,S9,S13作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。有两种更改参数的方式:
第一种:按键更改参数:
    第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
    第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。S13是复位按键,当通讯超时蜂鸣器报警时,可以按下此键清除报警。

第二种:通过串口来更改参数:
   波特率是:9600.
通讯协议:EB 00 55GG 00 02 XX XXCY
其中第1,2,3位EB 00 55就是数据头
其中第4位GG就是数据类型。01代表更改参数1,02代表更改参数2,03代表更改参数3,04代表更改参数4,
其中第5,6位00 02就是有效数据长度。高位在左,低位在右。
其中从第7,8位XX XX是被更改的参数。高位在左,低位在右。
第9位CY是累加和,前面所有字节的累加。
一个完整的通讯必须接收完4串数据,每串数据之间的间隔时间不能超过10秒钟,否则认为通讯超时出错引发蜂鸣器报警。如果接收到得数据校验正确,
则返回校验正确应答:eb 00      55 f5 00 00 35,
否则返回校验出错应答::eb 00      55 fa 00 00 3a。
   系统处于待机状态时,LED灯一直亮,
   系统处于非待机状态时,LED灯闪烁,
   系统处于通讯超时出错状态时,LED灯闪烁,并且蜂鸣器间歇鸣叫报警。


通过电脑的串口助手,依次发送以下测试数据,将会分别更改参数1,参数2,参数3,参数4。注意,每串数据之间的时间最大不能超过10秒,否则系统认为通讯超时报警。
把参数1更改为十进制的1:   eb 00 55 01 00 02 00 01 44
把参数2更改为十进制的12:eb 00 55 02 00 02 00 0c 50
把参数3更改为十进制的123: eb 00 55 03 00 02 00 7b c0
把参数4更改为十进制的1234:eb 00 55 04 00 02 04 d2 1c

(3)源代码讲解如下:#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_led_0_5s200   //大概0.5秒的时间
#define const_led_1s    400   //大概1秒的时间

#define const_send_time_out   4000//通讯超时出错的时间 大概10秒

#define const_rc_size20//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

#define const_send_size10//串口发送数据的缓冲区数组大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里
void eusart_send(unsigned char ucSendData); //发送一个字节,内部自带每个字节之间的delay延时

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void status_service(void);//状态显示的应用程序


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为状态指示灯 亮的时候表示待机状态.闪烁表示非待机状态,处于正在发送数据或者出错的状态

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucSendregBuf; //发送的缓冲区数组

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量

unsigned charucSendCntLock=0; //串口计时器的原子锁
unsigned char ucRcType=0;//数据类型
unsigned intuiRcSize=0;//数据长度
unsigned char ucRcCy=0;//校验累加和

unsigned intuiLedCnt=0;//控制Led闪烁的延时计时器
unsigned intuiSendTimeOutCnt=0; //用来识别接收数据超时的计时器
unsigned char ucSendTimeOutLock=0; //原子锁


unsigned char ucStatus=0; //当前状态变量 0代表待机 1代表正在通讯过程 2代表发送出错

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucWd3Update=0; //窗口3更新显示标志
unsigned char ucWd4Update=0; //窗口4更新显示标志
unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned intuiSetData1=0;//本程序中需要被设置的参数1
unsigned intuiSetData2=0;//本程序中需要被设置的参数2
unsigned intuiSetData3=0;//本程序中需要被设置的参数3
unsigned intuiSetData4=0;//本程序中需要被设置的参数4

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
          usart_service();//串口服务程序
      display_service(); //显示的窗口菜单服务程序
          status_service();//状态显示的应用程序
   }
}

void status_service(void)//状态显示的应用程序
{
   if(ucStatus!=0) //处于非待机的状态,Led闪烁
   {
      if(uiLedCnt<const_led_0_5s)//大概0.5秒
          {
             led_dr=1;//前半秒亮

               if(ucStatus==2)//处于发送数据出错的状态,则蜂鸣器间歇鸣叫报警
               {
             ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
             uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
             ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
               }
          }
          else if(uiLedCnt<const_led_1s)//大概1秒
          {
             led_dr=0; //前半秒灭
          }
          else
          {
             uiLedCnt=0; //延时计时器清零,让Led灯处于闪烁的反复循环中
          }
   
   }
   else//处于待机状态,Led一直亮
   {
      led_dr=1;
   
   }



}



void usart_service(void)//串口服务程序,在main函数里
{

   unsigned int i;
      
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            //下面的代码进入数据协议解析和数据处理的阶段

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
            {

               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
               {

                   ucRcType=ucRcregBuf;   //数据类型一个字节
                   uiRcSize=ucRcregBuf;   //数据长度两个字节
                   uiRcSize=uiRcSize<<8;
                   uiRcSize=uiRcSize+ucRcregBuf;
                                                               
                   ucRcCy=ucRcregBuf;   //记录最后一个字节的校验
                   ucRcregBuf=0;//清零最后一个字节的累加和变量

                   for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                   {
                      ucRcregBuf=ucRcregBuf+ucRcregBuf;
                   }      


                   if(ucRcCy==ucRcregBuf)//如果校验正确,则进入以下数据处理
                   {                                                
                     switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                     {
                           case 0x01:   //设置参数1

                                                            ucStatus=1; //从设置参数1开始,表示当前处于正在发送数据的状态

                                  uiSetData1=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiSetData1=uiSetData1<<8;
                                  uiSetData1=uiSetData1+ucRcregBuf;
                                  ucWd1Update=1; //窗口1更新显示
                                  break;      
                                                                        
                           case 0x02:   //设置参数2

                                  uiSetData2=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiSetData2=uiSetData2<<8;
                                  uiSetData2=uiSetData2+ucRcregBuf;
                                  ucWd2Update=1; //窗口2更新显示
                                  break;   

                           case 0x03:   //设置参数3

                                  uiSetData3=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiSetData3=uiSetData3<<8;
                                  uiSetData3=uiSetData3+ucRcregBuf;
                                  ucWd3Update=1; //窗口3更新显示
                                  break;

                           case 0x04:   //设置参数4

                                                            ucStatus=0; //从设置参数4结束发送数据的状态,表示发送数据的过程成功,切换回待机状态

                                  uiSetData4=ucRcregBuf;//把两个字节合并成一个int类型的数据
                                  uiSetData4=uiSetData4<<8;
                                  uiSetData4=uiSetData4+ucRcregBuf;
                                  ucWd4Update=1; //窗口4更新显示
                                  break;

                                                                        
                        }


                                                ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0xf5;//代表校验正确
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x35;

                        for(i=0;i<7;i++)//返回校验正确的应答指令
                        {
                           eusart_send(ucSendregBuf);//发送一串数据给上位机
                        }

                     }   
                                    else
                                       {
                        ucSendTimeOutLock=1; //原子锁加锁
                                          uiSendTimeOutCnt=0;//超时计时器计时清零
                        ucSendTimeOutLock=0; //原子锁解锁

                                                ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0xfa;   //代表校验错误
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x3a;   

                        for(i=0;i<7;i++)//返回校验错误的应答指令
                        {
                           eusart_send(ucSendregBuf);//发送一串数据给上位机
                        }                                       
                                       
                                       }

                                       ucSendTimeOutLock=1; //原子锁加锁
                                       uiSendTimeOutCnt=0;//超时计时器计时清零
                     ucSendTimeOutLock=0; //原子锁解锁

                     break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void eusart_send(unsigned char ucSendData) //发送一个字节,内部自带每个字节之间的delay延时
{

ES = 0; //关串口中断
TI = 0; //清零串口发送完成中断请求标志
SBUF =ucSendData; //发送一个字节

delay_short(400);//每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

TI = 0; //清零串口发送完成中断请求标志
ES = 1; //允许串口中断

}


void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
   {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;//第5位数码管显示无

            //先分解数据
                     ucTemp4=uiSetData1/1000;   
                     ucTemp3=uiSetData1%1000/100;
                     ucTemp2=uiSetData1%100/10;
                     ucTemp1=uiSetData1%10;

                        //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
            }
            break;
      case 2://显示P--2窗口的数据
            if(ucWd2Update==1)//窗口2要全部更新显示
   {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=2;//第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData2/1000;   //分解数据
                     ucTemp3=uiSetData2%1000/100;
                     ucTemp2=uiSetData2%100/10;
                     ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
      case 3://显示P--3窗口的数据
            if(ucWd3Update==1)//窗口3要全部更新显示
   {
               ucWd3Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=3;//第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData3/1000;   //分解数据
                     ucTemp3=uiSetData3%1000/100;
                     ucTemp2=uiSetData3%100/10;
                     ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
   }
            break;
      case 4://显示P--4窗口的数据
            if(ucWd4Update==1)//窗口4要全部更新显示
   {
               ucWd4Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=4;//第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData4/1000;   //分解数据
                     ucTemp3=uiSetData4%1000/100;
                     ucTemp2=uiSetData4%100/10;
                     ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
         }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}
}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)
                                  {
                                     uiSetData1=0;//最小值是0
                                  }
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;//最小值是0
                                  }
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999)
                                  {
                                     uiSetData3=0;//最小值是0
                                  }
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999)
                                  {
                                     uiSetData4=0;//最小值是0
                                  }
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;//切换窗口
                  if(ucWd>4)
                  {
                  ucWd=1;
                  }
          switch(ucWd)//在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 4:// 复位按键 对应朱兆祺学习板的S13键
          switch(ucStatus)//在不同的状态下,进行不同的操作
          {
             case 0://处于待机状态
                  break;

             case 1://处于正在通讯的过程
                  break;

             case 2: //发送数据出错,比如中间超时没有接收到数据
                  ucStatus=0; //切换回待机的状态
                  break;
          }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

          break;   
         
}               
}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void usart_receive(void) interrupt 4   //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

         ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

      if(ucSendCntLock==0)//原子锁判断
      {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;//及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
            ucSendCntLock=0; //解锁
      }
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;//如果不是串口接收中断,那么必然是串口发送中断,及时清除发送中断的标志,否则一直发送中断
   }
                                                         
}

void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


/* 注释一:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
if(ucSendCntLock==0)//原子锁判断
{
   ucSendCntLock=1; //加锁
   if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
   {
      uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
   }
   ucSendCntLock=0; //解锁
}

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

if(ucStatus!=0) //处于非待机的状态,Led闪烁
{
   uiLedCnt++; //Led闪烁计时器不断累加
}

if(ucStatus==1) //处于正在通讯的状态,
{
   if(ucSendTimeOutLock==0)//原子锁判断
         {
      uiSendTimeOutCnt++;   //超时计时器累加
            if(uiSendTimeOutCnt>const_send_time_out)//超时出错
            {
               uiSendTimeOutCnt=0;
               ucStatus=2;//切换到出错报警状态
             }
         }
}



key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void initial_myself(void)//第一区 初始化单片机
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
led_dr=1;//点亮独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

//配置串口
SCON=0x50;
TMOD=0X21;

/* 注释三:
* 为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
* 这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
*/
IP =0x10;//把串口中断设置为最高优先级,必须的。

TH1=TL1=-(11059200L/12/32/9600);//串口波特率为9600。
TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断
}



总结陈词:
   这节详细讲了从机收发端的程序框架,而主机端的程序则用电脑的串口助手来模拟。实际上,主机端的程序也有很多内容,它包括依次发送每一串数据,根据返回的应答来决定是否需要重发数据,重发三次如果没反应则进行报错,以及超时没接收到数据等等内容。主机收发端的程序框架是什么样的?欲知详情,请听下回分解-----主机的串口收发综合程序框架

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-5-5 12:10:13

本帖最后由 jianhong_wu 于 2014-11-29 22:52 编辑

第四十五节:主机的串口收发综合程序框架

开场白:
在大部分的项目中,串口都需要“一收一应答”的握手协议,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。
   上一节已经讲了从机,这节就讲主机的收发端程序实例。要教会大家四个知识点:

第一个:为了保证串口中断接收的数据不丢失,在初始化时必须设置IP = 0x10,相当于把串口中断设置为最高优先级,这个时候,串口中断可以打断任何其他的中断服务函数,实现中断嵌套。
第二个:主机端的收发端程序框架。包括重发,超时检测等等。
第三个:主机的状态指示程序框架。可以指示待机,通讯中,超时出错三种状态。
第四个:其实上一节的LED灯闪烁的时间里,我忘了加原子锁,不加原子锁的后果是,闪烁的时间有时候会不一致,所以这节多增加一个原子锁变量ucLedLock,再次感谢“红金龙吸味”关于原子锁的建议,真的很好用。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于坚鸿51单片机学习板。

(2)实现功能:
显示和独立按键部分根据第29节的程序来改编,用坚鸿51单片机学习板中的S1,S5,S9,S13作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。串口可以把当前设置的4个数据发送给从机。从机端可以用电脑的串口助手来模拟。
    按键更改参数:
    第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
    第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。S13是启动发送数据和复位按键,当系统处于待机状态时,按下此按键会启动发送数据,注意,单片机每发送一串数据,必须在电脑串口助手端发送应答信号,否则单片机重发3次后会引发超时报警;当通讯超时蜂鸣器报警时,可以按下此键清除报警,返回到待机的状态。
通过电脑的串口助手来模拟从机,返回不同的应答
从机返回校验正确应答:eb 00 55 f5 00 00 35
从机返回校验出错应答:eb 00 55 fa 00 00 3a

   系统处于待机状态时,LED灯一直亮,
   系统处于非待机状态时,LED灯闪烁,
   系统处于出错状态时,LED灯闪烁,并且蜂鸣器间歇鸣叫报警。


(3)源代码讲解如下:#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_led_0_5s200   //大概0.5秒的时间
#define const_led_1s    400   //大概1秒的时间

#define const_send_time_out   4000//通讯超时出错的时间 大概10秒

#define const_rc_size20//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

#define const_send_size10//串口发送数据的缓冲区数组大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口接收服务程序,在main函数里
void communication_service(void); //一发一收的通讯服务程序
void eusart_send(unsigned char ucSendData); //发送一个字节,内部自带每个字节之间的delay延时

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void status_service(void);//状态显示的应用程序


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;//作为状态指示灯 亮的时候表示待机状态.闪烁表示非待机状态,处于正在发送数据或者出错的状态

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;

unsigned char ucSendregBuf; //发送的缓冲区数组

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量

unsigned charucSendCntLock=0; //串口计时器的原子锁
unsigned char ucRcType=0;//数据类型
unsigned intuiRcSize=0;//数据长度
unsigned char ucRcCy=0;//校验累加和

unsigned char ucLedLock=0; //原子锁
unsigned intuiLedCnt=0;//控制Led闪烁的延时计时器
unsigned intuiSendTimeOutCnt=0; //用来识别接收数据超时的计时器
unsigned char ucSendTimeOutLock=0; //原子锁


unsigned char ucStatus=0; //当前状态变量 0代表待机 1代表正在通讯过程 2代表发送出错
unsigned char ucSendStep=0; //发送数据的过程步骤
unsigned char ucErrorCnt=0; //累计错误总数
unsigned char ucSendTotal=0; //记录当前已经发送了多少串数据
unsigned char ucReceiveStatus=0; //返回的数据状态 0代表待机 1代表校验正确 2代表校验出错

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志


unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucWd3Update=0; //窗口3更新显示标志
unsigned char ucWd4Update=0; //窗口4更新显示标志
unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned intuiSetData1=0;//本程序中需要被设置的参数1
unsigned intuiSetData2=0;//本程序中需要被设置的参数2
unsigned intuiSetData3=0;//本程序中需要被设置的参数3
unsigned intuiSetData4=0;//本程序中需要被设置的参数4

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
      usart_service();//串口接收服务程序
      communication_service(); //一发一收的通讯服务程序
      display_service(); //显示的窗口菜单服务程序
      status_service();//状态显示的应用程序
   }
}


void communication_service(void) //一发一收的通讯服务程序
{
   unsigned int i;

   if(ucStatus==1)//处于正在通讯的过程中
   {
       switch(ucSendStep)
         {
               case 0: //通讯过程0发送一串数据
                switch(ucSendTotal)//根据当前已经发送到第几条数据来决定发送哪些参数
                              {
                                 case 0:   //发送参数1
                        ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0x01;    //代表发送参数1
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x02;    //代表发送2个字节的有效数据

                                                ucSendregBuf=uiSetData1>>8;//把int类型的参数分解成两个字节的数据
                                                ucSendregBuf=uiSetData1;
                                        break;

                                 case 1://发送参数2
                        ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0x02;    //代表发送参数2
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x02;    //代表发送2个字节的有效数据

                                                ucSendregBuf=uiSetData2>>8;//把int类型的参数分解成两个字节的数据
                                                ucSendregBuf=uiSetData2;
                                        break;

                                 case 2://发送参数3
                        ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0x03;    //代表发送参数3
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x02;    //代表发送2个字节的有效数据

                                                ucSendregBuf=uiSetData3>>8;//把int类型的参数分解成两个字节的数据
                                                ucSendregBuf=uiSetData3;
                                        break;

                                 case 3://发送参数4
                        ucSendregBuf=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x55;
                        ucSendregBuf=0x04;    //代表发送参数4
                        ucSendregBuf=0x00;
                        ucSendregBuf=0x02;    //代表发送2个字节的有效数据

                                                ucSendregBuf=uiSetData4>>8;//把int类型的参数分解成两个字节的数据
                                                ucSendregBuf=uiSetData4;
                                        break;
                              }
                              

                ucSendregBuf=0x00;
                for(i=0;i<8;i++)//最后一个字节是校验和,是前面所有字节累加,溢出部分不用我们管,系统会有规律的自动处理
                {
                  ucSendregBuf=ucSendregBuf+ucSendregBuf;
                }

                for(i=0;i<9;i++)
                {
                  eusart_send(ucSendregBuf);//把一串完整的数据发送给下位机
                }

                ucSendTimeOutLock=1; //原子锁加锁
                uiSendTimeOutCnt=0;//超时计时器计时清零
                ucSendTimeOutLock=0; //原子锁解锁

                              ucReceiveStatus=0;//返回的数据状态清零
                              ucSendStep=1;//切换到下一个步骤,等待返回的数据
                        break;
               case 1: //通讯过程1判断返回的指令
                        if(ucReceiveStatus==1)//校验正确
                              {

                                     ucErrorCnt=0; //累计校验错误总数清零

                                 ucSendTotal++;//累加当前发送了多少串数据

                                 if(ucSendTotal>=4) //已经发送完全部4串数据,结束
                                 {
                      ucStatus=0;//切换到结束时的待机状态
                                 }
                                 else//还没发送完4串数据,则继续发送下一串新数据
                                 {
                                             ucSendStep=0;//返回上一个步骤,继续发送新数据
                                 }

                              }
                        else if(ucReceiveStatus==2||uiSendTimeOutCnt>const_send_time_out)//校验出错或者超时出错
                              {

                                 ucErrorCnt++; //累计错误总数
                   if(ucErrorCnt>=3)//累加重发次数3次以上,则报错
                                 {
                      ucStatus=2;//切换到出错报警状态
                                 }
                                 else//重发还没超过3次,继续返回重发
                                 {
                                             ucSendStep=0;//返回上一个步骤,重发一次数据
                                 }
                              }
                        break;         
         
         }
   
   }

}

void status_service(void)//状态显示的应用程序
{
   if(ucStatus!=0) //处于非待机的状态,Led闪烁
   {
      if(uiLedCnt<const_led_0_5s)//大概0.5秒
          {
             led_dr=1;//前半秒亮

               if(ucStatus==2)//处于发送数据出错的状态,则蜂鸣器间歇鸣叫报警
               {
             ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
             uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
             ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
               }
          }
          else if(uiLedCnt<const_led_1s)//大概1秒
          {
             led_dr=0; //前半秒灭
          }
          else
          {
                     ucLedLock=1; //原子锁加锁
             uiLedCnt=0; //延时计时器清零,让Led灯处于闪烁的反复循环中
                         ucLedLock=0; //原子锁解锁
          }
   
   }
   else//处于待机状态,Led一直亮
   {
      led_dr=1;
   
   }



}



void usart_service(void)//串口接收服务程序,在main函数里
{

   unsigned int i;
      
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            //下面的代码进入数据协议解析和数据处理的阶段

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
            {

               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
               {

                   ucRcType=ucRcregBuf;   //数据类型一个字节
                   uiRcSize=ucRcregBuf;   //数据长度两个字节
                   uiRcSize=uiRcSize<<8;
                   uiRcSize=uiRcSize+ucRcregBuf;
                                                               
                   ucRcCy=ucRcregBuf;   //记录最后一个字节的校验
                   ucRcregBuf=0;//清零最后一个字节的累加和变量

                   for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                   {
                      ucRcregBuf=ucRcregBuf+ucRcregBuf;
                   }      


                  if(ucRcCy==ucRcregBuf)//如果一串数据校验正确,则进入以下数据指令的判断
                  {                                                
                     switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                     {
                           case 0xf5:   //返回的是正确的校验指令

                                  ucReceiveStatus=1;//代表校验正确
                                  break;      
                                                                        
                           case 0xfa:   //返回的是错误的校验指令

                                  ucReceiveStatus=2;//代表校验错误
                                  break;                                          
                        }

                     }   
                     break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void eusart_send(unsigned char ucSendData) //发送一个字节,内部自带每个字节之间的delay延时
{

ES = 0; //关串口中断
TI = 0; //清零串口发送完成中断请求标志
SBUF =ucSendData; //发送一个字节

delay_short(400);//每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

TI = 0; //清零串口发送完成中断请求标志
ES = 1; //允许串口中断

}


void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
   {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;//第5位数码管显示无

            //先分解数据
                     ucTemp4=uiSetData1/1000;   
                     ucTemp3=uiSetData1%1000/100;
                     ucTemp2=uiSetData1%100/10;
                     ucTemp1=uiSetData1%10;

                        //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
            }
            break;
      case 2://显示P--2窗口的数据
            if(ucWd2Update==1)//窗口2要全部更新显示
   {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=2;//第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData2/1000;   //分解数据
                     ucTemp3=uiSetData2%1000/100;
                     ucTemp2=uiSetData2%100/10;
                     ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
      case 3://显示P--3窗口的数据
            if(ucWd3Update==1)//窗口3要全部更新显示
   {
               ucWd3Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=3;//第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData3/1000;   //分解数据
                     ucTemp3=uiSetData3%1000/100;
                     ucTemp2=uiSetData3%100/10;
                     ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
   }
            break;
      case 4://显示P--4窗口的数据
            if(ucWd4Update==1)//窗口4要全部更新显示
   {
               ucWd4Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=4;//第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData4/1000;   //分解数据
                     ucTemp3=uiSetData4%1000/100;
                     ucTemp2=uiSetData4%100/10;
                     ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
         }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}
}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)
                                  {
                                     uiSetData1=0;//最小值是0
                                  }
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;//最小值是0
                                  }
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999)
                                  {
                                     uiSetData3=0;//最小值是0
                                  }
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999)
                                  {
                                     uiSetData4=0;//最小值是0
                                  }
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;//切换窗口
                  if(ucWd>4)
                  {
                  ucWd=1;
                  }
          switch(ucWd)//在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 4:// 启动发送数据和复位按键 对应朱兆祺学习板的S13键
          switch(ucStatus)//在不同的状态下,进行不同的操作
          {
             case 0://处于待机状态,则启动发送数据


                  ucErrorCnt=0; //累计错误总数清零
                  ucSendTotal=0; //已经发送串数据总数清零

                                  ucSendStep=0; //发送数据的过程步骤清零,返回开始的步骤待命
                  ucStatus=1; //启动发送数据,1代表正在通讯过程
                  break;

             case 1://处于正在通讯的过程
                  break;

             case 2: //发送数据出错,比如中间超时没有接收到数据
                  ucStatus=0; //切换回待机的状态
                  break;
          }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

          break;   
         
}               
}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void usart_receive(void) interrupt 4   //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

         ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

      if(ucSendCntLock==0)//原子锁判断
      {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;//及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
            ucSendCntLock=0; //解锁
      }
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;//如果不是串口接收中断,那么必然是串口发送中断,及时清除发送中断的标志,否则一直发送中断
   }
                                                         
}

void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


/* 注释一:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
if(ucSendCntLock==0)//原子锁判断
{
   ucSendCntLock=1; //加锁
   if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
   {
      uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
   }
   ucSendCntLock=0; //解锁
}

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

if(ucStatus!=0) //处于非待机的状态,Led闪烁
{
   if(ucLedLock==0)//原子锁判断
         {
      uiLedCnt++; //Led闪烁计时器不断累加
         }
}

if(ucStatus==1) //处于正在通讯的状态,
{
   if(ucSendTimeOutLock==0)//原子锁判断
   {
         uiSendTimeOutCnt++;   //超时计时器累加
   }
}



key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void initial_myself(void)//第一区 初始化单片机
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
led_dr=1;//点亮独立LED灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

//配置串口
SCON=0x50;
TMOD=0X21;

/* 注释三:
* 为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
* 这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
*/
IP =0x10;//把串口中断设置为最高优先级,必须的。

TH1=TL1=-(11059200L/12/32/9600);//串口波特率为9600。
TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断
}



总结陈词:
前面花了大量篇幅详细地讲解了串口收发数据的程序框架,从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。

(未完待续,下节更精彩,不要走开哦)


jianhong_wu 发表于 2014-5-12 13:33:02

第四十六节:利用AT24C02进行掉电后的数据保存。

开场白:
一个AT24C02可以存储256个字节,地址范围是(0至255)。利用AT24C02存储数据时,要教会大家六个知识点:
第一个:单片机操作AT24C02的通讯过程也就是IIC的通讯过程, IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此在整个通讯过程中应该先关闭总中断,完成之后再开中断。
第二个:在写入或者读取完一个字节之后,一定要加上一段延时时间。在11.0592M晶振的系统中,写入数据时经验值用delay_short(2000),读取数据时经验值用delay_short(800)。否则在连续写入或者读取一串数据时容易丢失数据。如果一旦发现丢失数据,应该适当继续把这个时间延长,尤其是在写入数据时。
第三个:如何初始化EEPROM数据的方法。系统第一次上电时,我们从EEPROM读取出来的数据有可能超出了范围,可能是ff。这个时候我们应该给它填入一个初始化的数据,这一步千万别漏了。
第四个:在时序中,发送ACK确认信号时,要记得把数据线eeprom_sda_dr_s设置为输入的状态。对于51单片机来说,只要把eeprom_sda_dr_s=1就可以。而对于PIC或者AVR单片机来说,它们都是带方向寄存器的,就不能直接eeprom_sda_dr_s=1,而要直接修改方向寄存器,把它设置为输入状态。在本驱动程序中,我没有对ACK信号进行出错判断,因为我这么多年一直都是这样用也没出现过什么问题。
第五个: 提醒各位读者在硬件上应该注意的问题,单片机跟AT24C02通讯的2根IO口都要加上一个4.7K左右的上拉电阻。凡是在IIC通讯场合,都要加上拉电阻。AT24C02的WP引脚一定要接地,否则存不进数据。
第六个:旧版的坚鸿51学习板在硬件上有一个bug,AT24C02的第8个引脚VCC悬空了!!!,读者记得把它飞线连接到5V电源处。新版的坚鸿51学习板已经改过来了。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于坚鸿51单片机学习板。

(2)实现功能:
    4个被更改后的参数断电后不丢失,数据可以保存,断电再上电后还是上一次最新被修改的数据。
    显示和独立按键部分根据第29节的程序来改编,用坚鸿51单片机学习板中的S1,S5,S9作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。
   第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
   第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。


(3)源代码讲解如下:
#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间


void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);


void start24(void);//开始位
void ack24(void);//确认位
void stop24(void);//停止位
unsigned char read24(void);//读取一个字节的时序
void write24(unsigned char dd); //发送一个字节的时序
unsigned char read_eeprom(unsigned int address);   //从一个地址读取出一个字节数据
void write_eeprom(unsigned int address,unsigned char dd); //往一个地址存入一个字节数据
unsigned int read_eeprom_int(unsigned int address);   //从一个地址读取出一个int类型的数据
void write_eeprom_int(unsigned int address,unsigned int uiWriteData); //往一个地址存入一个int类型的数据

void T0_time(void);//定时中断函数

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbit eeprom_scl_dr=P3^7;    //时钟线
sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;



unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志



unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucWd3Update=0; //窗口3更新显示标志
unsigned char ucWd4Update=0; //窗口4更新显示标志
unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned intuiSetData1=0;//本程序中需要被设置的参数1
unsigned intuiSetData2=0;//本程序中需要被设置的参数2
unsigned intuiSetData3=0;//本程序中需要被设置的参数3
unsigned intuiSetData4=0;//本程序中需要被设置的参数4

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量


//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
      display_service(); //显示的窗口菜单服务程序
   }
}



//AT24C02驱动程序
void start24(void)//开始位
{

    eeprom_sda_dr_sr=1;
    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_sda_dr_sr=0;
        delay_short(15);
    eeprom_scl_dr=0;   
}


void ack24(void)//确认位时序
{
    eeprom_sda_dr_sr=1; //51单片机在读取数据之前要先置一,表示数据输入

    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_scl_dr=0;
        delay_short(15);

//在本驱动程序中,我没有对ACK信号进行出错判断,因为我这么多年一直都是这样用也没出现过什么问题。
//有兴趣的朋友可以自己增加出错判断,不一定非要按我的方式去做。
}

void stop24(void)//停止位
{
    eeprom_sda_dr_sr=0;
    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_sda_dr_sr=1;
}



unsigned char read24(void)//读取一个字节的时序
{
      unsigned char outdata,tempdata;


      outdata=0;
                eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入
      delay_short(2);
      for(tempdata=0;tempdata<8;tempdata++)
      {
            eeprom_scl_dr=0;
            delay_short(2);
            eeprom_scl_dr=1;
            delay_short(2);
            outdata<<=1;
            if(eeprom_sda_dr_sr==1)outdata++;      
            eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入
            delay_short(2);
      }
    return(outdata);
   
}

void write24(unsigned char dd) //发送一个字节的时序
{

      unsigned char tempdata;
      for(tempdata=0;tempdata<8;tempdata++)
      {
                if(dd>=0x80)eeprom_sda_dr_sr=1;
                else eeprom_sda_dr_sr=0;
                dd<<=1;
                delay_short(2);
                eeprom_scl_dr=1;
                delay_short(4);
                eeprom_scl_dr=0;
      }


}



unsigned char read_eeprom(unsigned int address)   //从一个地址读取出一个字节数据
{

   unsigned char dd,cAddress;

   cAddress=address; //把低字节地址传递给一个字节变量。

/* 注释一:
* IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此
* 在整个通讯过程中应该先关闭总中断,完成之后再开中断。但是,这样就会引起另外一个新
* 问题,如果关闭总中断的时间太长,会导致动态数码管不能及时均匀的扫描,在操作EEPROM时,
* 数码管就会出现闪烁的现象,解决这个问题最好的办法就是在做项目中尽量不要用动态扫描数码管
* 的方案,应该用静态显示的方案。那么程序上还有没有改善的方法?有的,下一节我会讲这个问题
* 的改善方法。
*/
   EA=0; //禁止中断

   start24(); //IIC通讯开始

   write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定

   ack24(); //发送应答信号
   write24(cAddress); //发送读取的存储地址(范围是0至255)
   ack24(); //发送应答信号

   start24(); //开始
   write24(0xA1); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为读指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定
   ack24(); //发送应答信号
   dd=read24(); //读取一个字节
   ack24(); //发送应答信号
   stop24();//停止

/* 注释二:
* 在写入或者读取完一个字节之后,一定要加上一段延时时间。在11.0592M晶振的系统中,
* 写入数据时经验值用delay_short(2000),读取数据时经验值用delay_short(800)。
* 否则在连续写入或者读取一串数据时容易丢失数据。如果一旦发现丢失数据,
* 应该适当继续把这个时间延长,尤其是在写入数据时。
*/
   delay_short(800);//此处最关键,此处的延时时间一定要,而且要足够长,此处也是导致动态数码管闪烁的根本原因
   EA=1; //允许中断

   return(dd);
}

void write_eeprom(unsigned int address,unsigned char dd) //往一个地址存入一个字节数据
{
   unsigned char cAddress;   

   cAddress=address; //把低字节地址传递给一个字节变量。


   EA=0; //禁止中断

   start24(); //IIC通讯开始

   write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定
   ack24(); //发送应答信号
   write24(cAddress);   //发送写入的存储地址(范围是0至255)
   ack24(); //发送应答信号
   write24(dd);//写入存储的数据
   ack24(); //发送应答信号
   stop24();//停止
   delay_short(2000);//此处最关键,此处的延时时间一定要,而且要足够长,此处也是导致动态数码管闪烁的根本原因
   EA=1; //允许中断

}


unsigned int read_eeprom_int(unsigned int address)   //从一个地址读取出一个int类型的数据
{
   unsigned char ucReadDataH;
   unsigned char ucReadDataL;
   unsigned intuiReadDate;

   ucReadDataH=read_eeprom(address);    //读取高字节
   ucReadDataL=read_eeprom(address+1);//读取低字节

   uiReadDate=ucReadDataH;//把两个字节合并成一个int类型数据
   uiReadDate=uiReadDate<<8;
   uiReadDate=uiReadDate+ucReadDataL;

   return uiReadDate;

}

void write_eeprom_int(unsigned int address,unsigned int uiWriteData) //往一个地址存入一个int类型的数据
{
   unsigned char ucWriteDataH;
   unsigned char ucWriteDataL;

   ucWriteDataH=uiWriteData>>8;
   ucWriteDataL=uiWriteData;

   write_eeprom(address,ucWriteDataH); //存入高字节
   write_eeprom(address+1,ucWriteDataL); //存入低字节

}


void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
   {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;//第5位数码管显示无

            //先分解数据
                     ucTemp4=uiSetData1/1000;   
                     ucTemp3=uiSetData1%1000/100;
                     ucTemp2=uiSetData1%100/10;
                     ucTemp1=uiSetData1%10;

                        //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
            }
            break;
      case 2://显示P--2窗口的数据
            if(ucWd2Update==1)//窗口2要全部更新显示
   {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=2;//第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData2/1000;   //分解数据
                     ucTemp3=uiSetData2%1000/100;
                     ucTemp2=uiSetData2%100/10;
                     ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
      case 3://显示P--3窗口的数据
            if(ucWd3Update==1)//窗口3要全部更新显示
   {
               ucWd3Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=3;//第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData3/1000;   //分解数据
                     ucTemp3=uiSetData3%1000/100;
                     ucTemp2=uiSetData3%100/10;
                     ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
   }
            break;
      case 4://显示P--4窗口的数据
            if(ucWd4Update==1)//窗口4要全部更新显示
   {
               ucWd4Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=4;//第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData4/1000;   //分解数据
                     ucTemp3=uiSetData4%1000/100;
                     ucTemp2=uiSetData4%100/10;
                     ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
         }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}


}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }

                           write_eeprom_int(0,uiSetData1); //存入EEPROM 由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }


                           write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)
                                  {
                                     uiSetData1=0;//最小值是0
                                  }

                           write_eeprom_int(0,uiSetData1); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;//最小值是0
                                  }
                           write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999)
                                  {
                                     uiSetData3=0;//最小值是0
                                  }

                           write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999)
                                  {
                                     uiSetData4=0;//最小值是0
                                  }
                           write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;//切换窗口
                  if(ucWd>4)
                  {
                  ucWd=1;
                  }
          switch(ucWd)//在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

         
}               
}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


/* 注释三:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/

if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}




key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void initial_myself(void)//第一区 初始化单片机
{
/* 注释四:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 坚鸿51学习板的S1就是本程序中用到的一个独立按键。
*/
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

/* 注释五:
* 如何初始化EEPROM数据的方法。在使用EEPROM时,这一步初始化很关键!
* 第一次上电时,我们从EEPROM读取出来的数据有可能超出了范围,可能是ff。
* 这个时候我们应该给它填入一个初始化的数据,这一步千万别漏了。另外,
* 由于int类型数据占用2个字节,所以以下4个数据挨着的地址是0,2,4,6.
*/

   uiSetData1=read_eeprom_int(0);//读取uiSetData1,内部占用2个字节地址
   if(uiSetData1>9999)   //不在范围内
   {
       uiSetData1=0;   //填入一个初始化数据
       write_eeprom_int(0,uiSetData1); //存入uiSetData1,内部占用2个字节地址
   }

   uiSetData2=read_eeprom_int(2);//读取uiSetData2,内部占用2个字节地址
   if(uiSetData2>9999)//不在范围内
   {
       uiSetData2=0;//填入一个初始化数据
       write_eeprom_int(2,uiSetData2); //存入uiSetData2,内部占用2个字节地址
   }

   uiSetData3=read_eeprom_int(4);//读取uiSetData3,内部占用2个字节地址
   if(uiSetData3>9999)//不在范围内
   {
       uiSetData3=0;//填入一个初始化数据
       write_eeprom_int(4,uiSetData3); //存入uiSetData3,内部占用2个字节地址
   }

   uiSetData4=read_eeprom_int(6);//读取uiSetData4,内部占用2个字节地址
   if(uiSetData4>9999)//不在范围内
   {
       uiSetData4=0;//填入一个初始化数据
       write_eeprom_int(6,uiSetData4); //存入uiSetData4,内部占用2个字节地址
   }



}

总结陈词:
   IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此,在整个通讯过程中应该先关闭总中断,完成之后再开中断。但是,这样就会引起另外一个新问题,如果关闭总中断的时间太长,会导致动态数码管不能及时均匀的扫描,在按键更改参数,内部操作EEPROM时,数码管就会出现短暂明显的闪烁现象,解决这个问题最好的办法就是在做项目中尽量不要用动态扫描数码管的方案,应该用静态显示的方案。那么在程序上还有没有改善这种现象的方法?当然有。欲知详情,请听下回分解-----操作AT24C02时,利用“一气呵成的定时器方式”改善数码管的闪烁现象。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-5-15 12:55:05

第四十七节:操作AT24C02时,利用“一气呵成的定时器延时”改善数码管的闪烁现象。

开场白:
上一节在按键更改参数时,会出现短暂明显的数码管闪烁现象。这节通过教大家使用新型延时函数可以有效的改善闪烁现象。要教会大家三个知识点:
第一个:如何编写一气呵成的定时器延时函数。
第二个:如何编写检查EEPROM芯片是否存在短路,虚焊或者芯片坏了的监控程序。
第三个:经过网友“cjseng”的提醒,我建议大家以后在用EEPROM芯片时,如果单片机IO口足够多,WP引脚应该专门接一个IO口,并且加一个上拉电阻,需要更改EEPROM存储数据时置低,其他任何一个时刻都置高,这样可以更加有效地保护EEPROM内部数据不会被意外更改。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于坚鸿51单片机学习板。旧版的坚鸿51学习板在硬件上有一个bug,AT24C02的第8个引脚VCC悬空了!!!,读者记得把它飞线连接到5V电源处。新版的坚鸿51学习板已经改过来了。


(2)实现功能:
    4个被更改后的参数断电后不丢失,数据可以保存,断电再上电后还是上一次最新被修改的数据。如果AT24C02短路,虚焊,或者坏了,系统可以检查出来,并且蜂鸣器会间歇性鸣叫报警。按更改参数按键时,数码管比上一节大大降低了闪烁现象。
    显示和独立按键部分根据第29节的程序来改编,用坚鸿51单片机学习板中的S1,S5,S9作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。
   第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
   第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。


(3)源代码讲解如下:
#include "REG52.H"

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间


#define const_eeprom_1s    400   //大概1秒的时间

void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
void delay_timer(unsigned int uiDelayTimerTemp); //一气呵成的定时器延时方式

//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);


void start24(void);//开始位
void ack24(void);//确认位
void stop24(void);//停止位
unsigned char read24(void);//读取一个字节的时序
void write24(unsigned char dd); //发送一个字节的时序
unsigned char read_eeprom(unsigned int address);   //从一个地址读取出一个字节数据
void write_eeprom(unsigned int address,unsigned char dd); //往一个地址存入一个字节数据
unsigned int read_eeprom_int(unsigned int address);   //从一个地址读取出一个int类型的数据
void write_eeprom_int(unsigned int address,unsigned int uiWriteData); //往一个地址存入一个int类型的数据

void T0_time(void);//定时中断函数

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void eeprom_alarm_service(void); //EEPROM出错报警


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbit eeprom_scl_dr=P3^7;    //时钟线
sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;



unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志



unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量

unsigned char ucWd1Update=1; //窗口1更新显示标志
unsigned char ucWd2Update=0; //窗口2更新显示标志
unsigned char ucWd3Update=0; //窗口3更新显示标志
unsigned char ucWd4Update=0; //窗口4更新显示标志
unsigned char ucWd=1;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned intuiSetData1=0;//本程序中需要被设置的参数1
unsigned intuiSetData2=0;//本程序中需要被设置的参数2
unsigned intuiSetData3=0;//本程序中需要被设置的参数3
unsigned intuiSetData4=0;//本程序中需要被设置的参数4

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量

unsigned char ucDelayTimerLock=0; //原子锁
unsigned intuiDelayTimer=0;

unsigned char ucCheckEeprom=0;//检查EEPROM芯片是否正常
unsigned char ucEepromError=0; //EEPROM芯片是否正常的标志

unsigned char ucEepromLock=0;//原子锁
unsigned intuiEepromCnt=0; //间歇性蜂鸣器报警的计时器

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
      display_service(); //显示的窗口菜单服务程序
          eeprom_alarm_service(); //EEPROM出错报警
   }
}


void eeprom_alarm_service(void) //EEPROM出错报警
{

if(ucEepromError==1) //EEPROM出错
{
      if(uiEepromCnt<const_eeprom_1s)//大概1秒钟蜂鸣器响一次
      {
             ucEepromLock=1;//原子锁加锁
         uiEepromCnt=0; //计时器清零
             ucEepromLock=0;//原子锁解锁


         ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
         uiVoiceCnt=const_voice_short; //蜂鸣器声音触发,滴一声就停。
         ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

      }
}

}


//AT24C02驱动程序
void start24(void)//开始位
{

    eeprom_sda_dr_sr=1;
    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_sda_dr_sr=0;
        delay_short(15);
    eeprom_scl_dr=0;   
}


void ack24(void)//确认位时序
{
    eeprom_sda_dr_sr=1; //51单片机在读取数据之前要先置一,表示数据输入

    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_scl_dr=0;
        delay_short(15);

//在本驱动程序中,我没有对ACK信号进行出错判断,因为我这么多年一直都是这样用也没出现过什么问题。
//有兴趣的朋友可以自己增加出错判断,不一定非要按我的方式去做。
}

void stop24(void)//停止位
{
    eeprom_sda_dr_sr=0;
    eeprom_scl_dr=1;
        delay_short(15);
    eeprom_sda_dr_sr=1;
}



unsigned char read24(void)//读取一个字节的时序
{
      unsigned char outdata,tempdata;


      outdata=0;
                eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入
      delay_short(2);
      for(tempdata=0;tempdata<8;tempdata++)
      {
            eeprom_scl_dr=0;
            delay_short(2);
            eeprom_scl_dr=1;
            delay_short(2);
            outdata<<=1;
            if(eeprom_sda_dr_sr==1)outdata++;      
            eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入
            delay_short(2);
      }
    return(outdata);
   
}

void write24(unsigned char dd) //发送一个字节的时序
{

      unsigned char tempdata;
      for(tempdata=0;tempdata<8;tempdata++)
      {
                if(dd>=0x80)eeprom_sda_dr_sr=1;
                else eeprom_sda_dr_sr=0;
                dd<<=1;
                delay_short(2);
                eeprom_scl_dr=1;
                delay_short(4);
                eeprom_scl_dr=0;
      }


}



unsigned char read_eeprom(unsigned int address)   //从一个地址读取出一个字节数据
{

   unsigned char dd,cAddress;

   cAddress=address; //把低字节地址传递给一个字节变量。

   EA=0; //禁止中断

   start24(); //IIC通讯开始

   write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定

   ack24(); //发送应答信号
   write24(cAddress); //发送读取的存储地址(范围是0至255)
   ack24(); //发送应答信号

   start24(); //开始
   write24(0xA1); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为读指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定
   ack24(); //发送应答信号
   dd=read24(); //读取一个字节
   ack24(); //发送应答信号
   stop24();//停止
   EA=1; //允许中断
   delay_timer(2); //一气呵成的定时器延时方式,在延时的时候还可以动态扫描数码管

   return(dd);
}

void write_eeprom(unsigned int address,unsigned char dd) //往一个地址存入一个字节数据
{
   unsigned char cAddress;   

   cAddress=address; //把低字节地址传递给一个字节变量。


   EA=0; //禁止中断

   start24(); //IIC通讯开始

   write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。
                  //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定
   ack24(); //发送应答信号
   write24(cAddress);   //发送写入的存储地址(范围是0至255)
   ack24(); //发送应答信号
   write24(dd);//写入存储的数据
   ack24(); //发送应答信号
   stop24();//停止
   EA=1; //允许中断
   delay_timer(4); //一气呵成的定时器延时方式,在延时的时候还可以动态扫描数码管

}


unsigned int read_eeprom_int(unsigned int address)   //从一个地址读取出一个int类型的数据
{
   unsigned char ucReadDataH;
   unsigned char ucReadDataL;
   unsigned intuiReadDate;

   ucReadDataH=read_eeprom(address);    //读取高字节
   ucReadDataL=read_eeprom(address+1);//读取低字节

   uiReadDate=ucReadDataH;//把两个字节合并成一个int类型数据
   uiReadDate=uiReadDate<<8;
   uiReadDate=uiReadDate+ucReadDataL;

   return uiReadDate;

}

void write_eeprom_int(unsigned int address,unsigned int uiWriteData) //往一个地址存入一个int类型的数据
{
   unsigned char ucWriteDataH;
   unsigned char ucWriteDataL;

   ucWriteDataH=uiWriteData>>8;
   ucWriteDataL=uiWriteData;

   write_eeprom(address,ucWriteDataH); //存入高字节
   write_eeprom(address+1,ucWriteDataL); //存入低字节

}


void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)//窗口1要全部更新显示
   {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;//第5位数码管显示无

            //先分解数据
                     ucTemp4=uiSetData1/1000;   
                     ucTemp3=uiSetData1%1000/100;
                     ucTemp2=uiSetData1%100/10;
                     ucTemp1=uiSetData1%10;

                        //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
            }
            break;
      case 2://显示P--2窗口的数据
            if(ucWd2Update==1)//窗口2要全部更新显示
   {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=2;//第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData2/1000;   //分解数据
                     ucTemp3=uiSetData2%1000/100;
                     ucTemp2=uiSetData2%100/10;
                     ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
      case 3://显示P--3窗口的数据
            if(ucWd3Update==1)//窗口3要全部更新显示
   {
               ucWd3Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=3;//第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData3/1000;   //分解数据
                     ucTemp3=uiSetData3%1000/100;
                     ucTemp2=uiSetData3%100/10;
                     ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
   }
            break;
      case 4://显示P--4窗口的数据
            if(ucWd4Update==1)//窗口4要全部更新显示
   {
               ucWd4Update=0;//及时清零标志,避免一直进来扫描
               ucDigShow8=12;//第8位数码管显示P
               ucDigShow7=11;//第7位数码管显示-
               ucDigShow6=4;//第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                     ucTemp4=uiSetData4/1000;   //分解数据
                     ucTemp3=uiSetData4%1000/100;
                     ucTemp2=uiSetData4%100/10;
                     ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;//如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;//第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;//如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;//第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;//如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;//第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;//第1位数码管要显示的内容
    }
             break;
         }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}


}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }

                           write_eeprom_int(0,uiSetData1); //存入EEPROM 由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }


                           write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)
                                  {
                                     uiSetData1=0;//最小值是0
                                  }

                           write_eeprom_int(0,uiSetData1); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁

                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;//最小值是0
                                  }
                           write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999)
                                  {
                                     uiSetData3=0;//最小值是0
                                  }

                           write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999)
                                  {
                                     uiSetData4=0;//最小值是0
                                  }
                           write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;//切换窗口
                  if(ucWd>4)
                  {
                  ucWd=1;
                  }
          switch(ucWd)//在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;//窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;//窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;//窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;//窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

         
}               
}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}

if(ucDelayTimerLock==0) //原子锁判断
{
   if(uiDelayTimer>0)
       {
           uiDelayTimer--;   //一气呵成的定时器延时方式的计时器
       }

}


if(ucEepromError==1) //EEPROM出错
{
      if(ucEepromLock==0)//原子锁判断
          {
             uiEepromCnt++;//间歇性蜂鸣器报警的计时器
          }
}




key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void delay_timer(unsigned int uiDelayTimerTemp)
{
    ucDelayTimerLock=1; //原子锁加锁
    uiDelayTimer=uiDelayTimerTemp;
    ucDelayTimerLock=0; //原子锁解锁   

/* 注释一:
*延时等待,一直等到定时中断把它减到0为止.这种一气呵成的定时器方式,
*可以在延时的时候动态扫描数码管,改善数码管的闪烁现象
*/
    while(uiDelayTimer!=0);//一气呵成的定时器方式延时等待

}


void initial_myself(void)//第一区 初始化单片机
{

key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断


/* 注释二:
* 检查AT24C02芯片是否存在短路,虚焊,芯片坏了等不工作现象。
* 在一个特定的地址里把数据读出来,如果发现不等于0x5a,则重新写入0x5a,再读出来
* 判断是不是等于0x5a,如果不相等,则芯片有问题,出错报警提示。
*/
   ucCheckEeprom=read_eeprom(254); //判断AT24C02是否正常
   if(ucCheckEeprom!=0x5a)//如果不等于特定内容。则重新写入数据再判断一次
   {
   write_eeprom(254,0x5a);//重新写入标志数据
   ucCheckEeprom=read_eeprom(254); //判断AT24C02是否正常
       if(ucCheckEeprom!=0x5a)//如果还是不等于特定数字,则芯片不正常
       {
          ucEepromError=1;//表示AT24C02芯片出错报警
       }
   }

   uiSetData1=read_eeprom_int(0);//读取uiSetData1,内部占用2个字节地址
   if(uiSetData1>9999)   //不在范围内
   {
       uiSetData1=0;   //填入一个初始化数据
       write_eeprom_int(0,uiSetData1); //存入uiSetData1,内部占用2个字节地址
   }

   uiSetData2=read_eeprom_int(2);//读取uiSetData2,内部占用2个字节地址
   if(uiSetData2>9999)//不在范围内
   {
       uiSetData2=0;//填入一个初始化数据
       write_eeprom_int(2,uiSetData2); //存入uiSetData2,内部占用2个字节地址
   }

   uiSetData3=read_eeprom_int(4);//读取uiSetData3,内部占用2个字节地址
   if(uiSetData3>9999)//不在范围内
   {
       uiSetData3=0;//填入一个初始化数据
       write_eeprom_int(4,uiSetData3); //存入uiSetData3,内部占用2个字节地址
   }

   uiSetData4=read_eeprom_int(6);//读取uiSetData4,内部占用2个字节地址
   if(uiSetData4>9999)//不在范围内
   {
       uiSetData4=0;//填入一个初始化数据
       write_eeprom_int(6,uiSetData4); //存入uiSetData4,内部占用2个字节地址
   }



}

总结陈词:
下一节开始讲关于单片机驱动实时时钟芯片的内容,欲知详情,请听下回分解-----利用DS1302做一个实时时钟。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-5-21 11:16:30

第四十八节:利用DS1302做一个实时时钟。

开场白:
DS1302有两路独立电源输入,我们只要在其中一路电源上挂一个纽扣电池就可以实现掉电时钟继续跑的功能,纽扣电池作为备用电源必须比主电源的电压低一点。DS1302还给我们预留了一片RAM区,我们可以把一些数据存入到DS1302,只要DS1302的电池有电,那么它就相当于一个EEPROM。这个RAM区有什么用呢?因为RAM区的数据只要一掉电,所有的数据都会变成0x00或者0xff,也就是数据掉电会丢失,我们可以利用这个特点,可以在里面存入标志位数据,一旦发现这个数据改变了,就知道时钟的数据需要重新设置过,或者说明电池没电了。
       在移植DS1302驱动程序中,有一个地方最容易出错,就是DS1302芯片的数据线DIO。我们编程时要特别留意这个IO口什么时候作为数据输入,什么时候作为数据输出,以便及时更改方向寄存器。对于51单片机,IO口在读取数据之前,要先置1。
这一节要教会大家六个知识点:
第一个:DS1302做实时时钟时,修改时间和读取时间的常见程序框架。
第二个:如何编写监控备用电池电量耗尽的监控程序。
第三个:在往DS1302写入数据修改时间之前,必须注意调整一下“日”的变量,因为每个月的最大天数是不一样的,有的一个月28天,有的一个月29天,有的一个月30天,有的一个月31天。
第四个:本程序第一次出现了电平按键,跟之前讲的下降沿按键不一样,请留意我是何如用软件滤波的,以及它具体的实现代码。
第五个:本程序第一次出现了一个按键按下去后,如果不松手就会触发两次事件,第一次是短按,第二次是长按3秒。请留意我是如何在之前的按键上略做修改就实现此功能的具体代码。
第六个:继续加深了解按键与显示是如何紧密关联起来的程序框架。

具体内容,请看源代码讲解。

(1)        硬件平台.
基于坚鸿51单片机学习板。
旧版的坚鸿51学习板在硬件上有一个bug,DS1302芯片附近的R43,R42两个电阻应该去掉,并且把R41的电阻换成0欧姆的电阻,或者直接短接起来。新版的坚鸿51学习板已经改过来了。

(2)实现功能:
   本程序有2两个窗口。
   第1个窗口显示日期。显示格式“年-月-日”。注意中间有“-”分开。
   第2个窗口显示时间。显示格式“时 分 秒”。注意中间没“-”,只有空格分开。
   系统上电后,默认显示第2个窗口,实时显示动态的“时 分 秒”时间。此时按下S13按键不松手就会切换到显示日期的第1个窗口。松手后自动切换回第2个显示动态时间的窗口。
   需要更改时间的时候,长按S9按键不松手超过3秒后,系统将进入修改时间的状态,切换到第1个日期窗口,并且显示“年”的两位数码管会闪烁,此时可以按S1或者S5加减按键修改年的参数,修改完年后,继续短按S9按键,会切换到“月”的参数闪烁状态,只要依次不断按下S9按键,就会依次切换年,月,日,时,分,秒的参数闪烁状态,最后修改完秒的参数后,系统会自动把我们修改设置的日期时间一次性写入DS1302芯片内部,达到修改日期时间的目的。
S13是电平变化按键,用来切换窗口的,专门用来查看当前日期。按下S13按键时显示日期窗口,松手后返回到显示实时时间的窗口。

本程序在使用过程中的注意事项:
(a)第一次上电时,蜂鸣器会报警,只要DS1302芯片的备用电池电量充足,这个时候断电再重启一次,就不会报警了。
(b)第一次上电时,时间没有走动,需要重新设置一下日期时间才可以。长按S9按键可以进入修改日期时间的状态。

(3)源代码讲解如下:
#include "REG52.H"

#define const_dpy_time_half200//数码管闪烁时间的半值
#define const_dpy_time_all   400//数码管闪烁时间的全值 一定要比const_dpy_time_half 大

#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间

#define const_key_time171200//长按超过3秒的时间
#define const_ds1302_0_5s200   //大概0.5秒的时间

#define const_ds1302_sampling_time    360   //累计主循环次数的时间,每次刷新采样时钟芯片的时间

#define WRITE_SECOND    0x80    //DS1302内部的相关地址
#define WRITE_MINUTE    0x82
#define WRITE_HOUR      0x84
#define WRITE_DATE      0x86
#define WRITE_MONTH   0x88
#define WRITE_YEAR      0x8C

#define WRITE_CHECK   0xC2//用来检查芯片的备用电池是否用完了的地址
#define READ_CHECK      0xC3//用来检查芯片的备用电池是否用完了的地址

#define READ_SECOND   0x81
#define READ_MINUTE   0x83
#define READ_HOUR       0x85
#define READ_DATE       0x87
#define READ_MONTH      0x89
#define READ_YEAR       0x8D

#define WRITE_PROTECT   0x8E

void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);


//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time(void);//定时中断函数

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void ds1302_alarm_service(void); //ds1302出错报警
void ds1302_sampling(void); //ds1302采样程序,内部每秒钟采集更新一次
void Write1302 ( unsigned char addr, unsigned char dat );//修改时间的驱动
unsigned char Read1302 ( unsigned char addr );//读取时间的驱动

unsigned char bcd_to_number(unsigned char ucBcdTemp);//BCD转原始数值
unsigned char number_to_bcd(unsigned char ucNumberTemp); //原始数值转BCD

//日调整 每个月份的日最大取值不同,有的最大28日,有的最大29日,有的最大30,有的最大31
unsigned char date_adjust(unsigned char ucYearTemp,unsigned char ucMonthTemp,unsigned char ucDateTemp); //日调整

sbit SCLK_dr      =P1^3;
sbit DIO_dr_sr    =P1^4;
sbit DS1302_CE_dr =P1^5;

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbit eeprom_scl_dr=P3^7;    //时钟线
sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线

sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;


unsigned int uiSampingCnt=0;   //采集Ds1302的计时器,每秒钟更新采集一次

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

unsigned int uiKey4Cnt1=0;//在软件滤波中,用到的变量
unsigned int uiKey4Cnt2=0;
unsigned char ucKey4Sr=1;//实时反映按键的电平状态
unsigned char ucKey4SrRecord=0; //记录上一次按键的电平状态

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量


unsigned char ucWd=2;//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned char ucPart=0;//本程序的核心变量,局部显示变量。类似于二级菜单的变量。代表显示不同的局部。

unsigned char ucWd1Update=0; //窗口1更新显示标志
unsigned char ucWd2Update=1; //窗口2更新显示标志

unsigned char ucWd1Part1Update=0;//在窗口1中,局部1的更新显示标志
unsigned char ucWd1Part2Update=0; //在窗口1中,局部2的更新显示标志
unsigned char ucWd1Part3Update=0; //在窗口1中,局部3的更新显示标志

unsigned char ucWd2Part1Update=0;//在窗口2中,局部1的更新显示标志
unsigned char ucWd2Part2Update=0; //在窗口2中,局部2的更新显示标志
unsigned char ucWd2Part3Update=0; //在窗口2中,局部3的更新显示标志

unsigned charucYear=0;    //原始数据
unsigned charucMonth=0;
unsigned charucDate=0;
unsigned charucHour=0;
unsigned charucMinute=0;
unsigned charucSecond=0;

unsigned charucYearBCD=0;   //BCD码的数据
unsigned charucMonthBCD=0;
unsigned charucDateBCD=0;
unsigned charucHourBCD=0;
unsigned charucMinuteBCD=0;
unsigned charucSecondBCD=0;

unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量

unsigned char ucTemp4=0;//中间过渡变量
unsigned char ucTemp5=0;//中间过渡变量

unsigned char ucTemp7=0;//中间过渡变量
unsigned char ucTemp8=0;//中间过渡变量

unsigned char ucDelayTimerLock=0; //原子锁
unsigned intuiDelayTimer=0;

unsigned char ucCheckDs1302=0;//检查Ds1302芯片是否正常
unsigned char ucDs1302Error=0; //Ds1302芯片的备用电池是否用完了的报警标志

unsigned char ucDs1302Lock=0;//原子锁
unsigned intuiDs1302Cnt=0; //间歇性蜂鸣器报警的计时器

unsigned char ucDpyTimeLock=0; //原子锁
unsigned intuiDpyTimeCnt=0;//数码管的闪烁计时器,放在定时中断里不断累加

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
          ds1302_sampling(); //ds1302采样程序,内部每秒钟采集更新一次
      display_service(); //显示的窗口菜单服务程序
          ds1302_alarm_service(); //ds1302出错报警
   }
}


/* 注释一:
* 系统不用时时刻刻采集ds1302的内部数据,每隔一段时间更新采集一次就可以了。
* 这个间隔时间应该小于或者等于1秒钟的时间,否则在数码管上看不到每秒钟的时间变化。
*/
void ds1302_sampling(void) //ds1302采样程序,内部每秒钟采集更新一次
{
   if(ucPart==0)//当系统不是处于设置日期和时间的情况下
   {
      ++uiSampingCnt;//累计主循环次数的时间
      if(uiSampingCnt>const_ds1302_sampling_time)//每隔一段时间就更新采集一次Ds1302数据
          {

          uiSampingCnt=0;


          ucYearBCD=Read1302(READ_YEAR); //读取年
          ucMonthBCD=Read1302(READ_MONTH); //读取月
          ucDateBCD=Read1302(READ_DATE); //读取日
          ucHourBCD=Read1302(READ_HOUR); //读取时
          ucMinuteBCD=Read1302(READ_MINUTE); //读取分
          ucSecondBCD=Read1302(READ_SECOND); //读取秒


                  ucYear=bcd_to_number(ucYearBCD);//BCD转原始数值
                  ucMonth=bcd_to_number(ucMonthBCD);//BCD转原始数值
                  ucDate=bcd_to_number(ucDateBCD);//BCD转原始数值
                  ucHour=bcd_to_number(ucHourBCD);//BCD转原始数值
                  ucMinute=bcd_to_number(ucMinuteBCD);//BCD转原始数值
                  ucSecond=bcd_to_number(ucSecondBCD);//BCD转原始数值

          ucWd2Update=1; //窗口2更新显示时间
          }

   }
}

//修改ds1302时间的驱动 ,注意,此处写入的是BCD码,
void Write1302 ( unsigned char addr, unsigned char dat )
{
   unsigned char i,temp;         //单片机驱动DS1302属于SPI通讯方式,根据我的经验,不用关闭中断
   DS1302_CE_dr=0;                                          //CE引脚为低,数据传送中止
   delay_short(1);
   SCLK_dr=0;                                                 //清零时钟总线
   delay_short(1);
   DS1302_CE_dr = 1;                                          //CE引脚为高,逻辑控制有效
   delay_short(1);
                                                             //发送地址
   for ( i=0; i<8; i++ )                                 //循环8次移位
   {
      DIO_dr_sr = 0;
      temp = addr;
      if(temp&0x01)
      {
            DIO_dr_sr =1;
      }
      else
      {
            DIO_dr_sr =0;
      }
      delay_short(1);
      addr >>= 1;                                           //右移一位

      SCLK_dr = 1;
      delay_short(1);
      SCLK_dr = 0;
      delay_short(1);
   }

                                                            //发送数据
   for ( i=0; i<8; i++ )                                    //循环8次移位
   {
      DIO_dr_sr = 0;
      temp = dat;
      if(temp&0x01)
      {
            DIO_dr_sr =1;
      }
      else
      {
         DIO_dr_sr =0;
      }
      delay_short(1);
      dat >>= 1;                                             //右移一位

      SCLK_dr = 1;
      delay_short(1);
      SCLK_dr = 0;
      delay_short(1);
   }
   DS1302_CE_dr = 0;
   delay_short(1);
}


//读取Ds1302时间的驱动 ,注意,此处读取的是BCD码,
unsigned char Read1302 ( unsigned char addr )
{
    unsigned char i,temp,dat1;
    DS1302_CE_dr=0;      //单片机驱动DS1302属于SPI通讯方式,根据我的经验,不用关闭中断
    delay_short(1);
    SCLK_dr=0;
    delay_short(1);
    DS1302_CE_dr = 1;
    delay_short(1);

                                                               //发送地址
    for ( i=0; i<8; i++ )                                    //循环8次移位
    {
       DIO_dr_sr = 0;

       temp = addr;
       if(temp&0x01)
       {
          DIO_dr_sr =1;
       }
       else
       {
          DIO_dr_sr =0;
       }
       delay_short(1);
       addr >>= 1;                                             //右移一位

       SCLK_dr = 1;
       delay_short(1);
       SCLK_dr = 0;
       delay_short(1);
    }
                                                               
/* 注释二:
* 51单片机IO口的特点,在读取数据之前必须先输出高电平,
* 如果是PIC,AVR等单片机,这里应该把IO方向寄存器设置为输入
*/
   DIO_dr_sr =1;   //51单片机IO口的特点,在读取数据之前必须先输出高电平,
   temp=0;
   for ( i=0; i<8; i++ )
   {
      temp>>=1;

      if(DIO_dr_sr==1)
      {
         temp=temp+0x80;
      }
          DIO_dr_sr =1;//51单片机IO口的特点,在读取数据之前必须先输出高电平

      delay_short(1);
      SCLK_dr = 1;
      delay_short(1);
      SCLK_dr = 0;
      delay_short(1);
    }
    DS1302_CE_dr=0;
    delay_short(1);
    dat1=temp;

    return (dat1);
}

unsigned char bcd_to_number(unsigned char ucBcdTemp)//BCD转原始数值
{
   unsigned char ucNumberResult=0;
   unsigned char ucBcdTemp10;
   unsigned char ucBcdTemp1;

   ucBcdTemp10=ucBcdTemp;
   ucBcdTemp10=ucBcdTemp10>>4;

   ucBcdTemp1=ucBcdTemp;
   ucBcdTemp1=ucBcdTemp1&0x0f;


   ucNumberResult=ucBcdTemp10*10+ucBcdTemp1;

   return ucNumberResult;


}

unsigned char number_to_bcd(unsigned char ucNumberTemp) //原始数值转BCD
{
   unsigned char ucBcdResult=0;
   unsigned char ucNumberTemp10;
   unsigned char ucNumberTemp1;

   ucNumberTemp10=ucNumberTemp;
   ucNumberTemp10=ucNumberTemp10/10;
   ucNumberTemp10=ucNumberTemp10<<4;
   ucNumberTemp10=ucNumberTemp10&0xf0;

   ucNumberTemp1=ucNumberTemp;
   ucNumberTemp1=ucNumberTemp1%10;

   ucBcdResult=ucNumberTemp10|ucNumberTemp1;

   return ucBcdResult;

}


//日调整 每个月份的日最大取值不同,有的最大28日,有的最大29日,有的最大30,有的最大31
unsigned char date_adjust(unsigned char ucYearTemp,unsigned char ucMonthTemp,unsigned char ucDateTemp) //日调整
{


   unsigned char ucDayResult;
   unsigned int uiYearTemp;
   unsigned int uiYearYu;
   

   ucDayResult=ucDateTemp;

   switch(ucMonthTemp)//根据不同的月份来修正不同的日最大值
   {
      case 2://二月份要计算是否是闰年
         uiYearTemp=2000+ucYearTemp;
         uiYearYu=uiYearTemp%4;
         if(uiYearYu==0) //闰年
         {
               if(ucDayResult>29)
               {
                  ucDayResult=29;
               }
         }
         else
         {
               if(ucDayResult>28)
               {
                  ucDayResult=28;
               }
         }
         break;
      case 4:
      case 6:
      case 9:
      case 11:
         if(ucDayResult>30)
         {
            ucDayResult=30;
         }
         break;

   }

   return ucDayResult;

}


void ds1302_alarm_service(void) //ds1302出错报警
{
    if(ucDs1302Error==1)//备用电池的电量用完了报警提示
        {
         if(uiDs1302Cnt>const_ds1302_0_5s)//大概0.5秒钟蜂鸣器响一次
         {
                   ucDs1302Lock=1;//原子锁加锁
               uiDs1302Cnt=0; //计时器清零
                   ucDs1302Lock=0;//原子锁解锁

               ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
               uiVoiceCnt=const_voice_short; //蜂鸣器声音触发,滴一声就停。
               ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
         
         }
   }
}



void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示日期窗口的数据数据格式 NN-YY-RR 年-月-日
            if(ucWd1Update==1)//窗口1要全部更新显示
            {
               ucWd1Update=0;//及时清零标志,避免一直进来扫描

               ucDigShow6=11;//显示一杠"-"
               ucDigShow3=11;//显示一杠"-"

               ucWd1Part1Update=1;//局部年更新显示
               ucWd1Part2Update=1;//局部月更新显示
               ucWd1Part3Update=1;//局部日更新显示
            }

                        if(ucWd1Part1Update==1)//局部年更新显示
                        {
                           ucWd1Part1Update=0;
               ucTemp8=ucYear/10;//年
               ucTemp7=ucYear%10;

               ucDigShow8=ucTemp8; //数码管显示实际内容
               ucDigShow7=ucTemp7;
                        }


                        if(ucWd1Part2Update==1)//局部月更新显示
                        {
                           ucWd1Part2Update=0;
               ucTemp5=ucMonth/10;//月
               ucTemp4=ucMonth%10;

               ucDigShow5=ucTemp5; //数码管显示实际内容
               ucDigShow4=ucTemp4;
                        }


                        if(ucWd1Part3Update==1) //局部日更新显示
                        {
                           ucWd1Part3Update=0;
               ucTemp2=ucDate/10;//日
               ucTemp1=ucDate%10;
                       
               ucDigShow2=ucTemp2; //数码管显示实际内容
               ucDigShow1=ucTemp1;
                        }
            //数码管闪烁
            switch(ucPart)//相当于二级菜单,根据局部变量的值,使对应的参数产生闪烁的动态效果。
            {
                case 0://都不闪烁
                     break;
                case 1://年参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow8=ucTemp8; //数码管显示实际内容
                           ucDigShow7=ucTemp7;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow8=10;   //数码管显示空,什么都不显示
                           ucDigShow7=10;

                     }
                     break;
                case 2:   //月参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow5=ucTemp5; //数码管显示实际内容
                           ucDigShow4=ucTemp4;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow5=10;   //数码管显示空,什么都不显示
                           ucDigShow4=10;

                     }
                  break;
                case 3:   //日参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow2=ucTemp2; //数码管显示实际内容
                           ucDigShow1=ucTemp1;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow2=10;   //数码管显示空,什么都不显示
                           ucDigShow1=10;

                     }
                  break;      
            }

            break;
       case 2:   //显示时间窗口的数据数据格式 SS FF MM 时 分 秒
            if(ucWd2Update==1)//窗口2要全部更新显示
            {
               ucWd2Update=0;//及时清零标志,避免一直进来扫描

               ucDigShow6=10;//显示空
               ucDigShow3=10;//显示空

               ucWd2Part3Update=1;//局部时更新显示
               ucWd2Part2Update=1;//局部分更新显示
               ucWd2Part1Update=1;//局部秒更新显示
            }

                        if(ucWd2Part1Update==1)//局部时更新显示
                        {
                           ucWd2Part1Update=0;
               ucTemp8=ucHour/10;//时
               ucTemp7=ucHour%10;

               ucDigShow8=ucTemp8; //数码管显示实际内容
               ucDigShow7=ucTemp7;
                        }


                        if(ucWd2Part2Update==1)//局部分更新显示
                        {
                           ucWd2Part2Update=0;
               ucTemp5=ucMinute/10;//分
               ucTemp4=ucMinute%10;

               ucDigShow5=ucTemp5; //数码管显示实际内容
               ucDigShow4=ucTemp4;
                        }


                        if(ucWd2Part3Update==1) //局部秒更新显示
                        {
                           ucWd2Part3Update=0;
               ucTemp2=ucSecond/10;//秒
               ucTemp1=ucSecond%10;               
       
               ucDigShow2=ucTemp2; //数码管显示实际内容
               ucDigShow1=ucTemp1;
                        }
            //数码管闪烁
            switch(ucPart)//相当于二级菜单,根据局部变量的值,使对应的参数产生闪烁的动态效果。
            {
                case 0://都不闪烁
                     break;
                case 1://时参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow8=ucTemp8; //数码管显示实际内容
                           ucDigShow7=ucTemp7;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow8=10;   //数码管显示空,什么都不显示
                           ucDigShow7=10;

                     }
                     break;
                case 2:   //分参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow5=ucTemp5; //数码管显示实际内容
                           ucDigShow4=ucTemp4;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow5=10;   //数码管显示空,什么都不显示
                           ucDigShow4=10;

                     }
                  break;
                case 3:   //秒参数闪烁
                     if(uiDpyTimeCnt==const_dpy_time_half)
                     {
                           ucDigShow2=ucTemp2; //数码管显示实际内容
                           ucDigShow1=ucTemp1;
                      }
                     else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大
                     {
                                           ucDpyTimeLock=1; //原子锁加锁
                           uiDpyTimeCnt=0;   //及时把闪烁记时器清零
                                                   ucDpyTimeLock=0;//原子锁解锁

                           ucDigShow2=10;   //数码管显示空,什么都不显示
                           ucDigShow1=10;

                     }
                  break;      
            }


            break;
      }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}



/* 注释三:
* 注意,此处把一个按键的短按和长按的功能都实现了。
*/

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //短按触发3号键
   }
}
else if(uiKeyTimeCnt3<const_key_time17)   //长按3秒
{
   uiKeyTimeCnt3++; //累加定时中断次数
       if(uiKeyTimeCnt3==const_key_time17)//等于3秒钟,触发17号长按按键
       {
          ucKeySec=17;    //长按3秒触发17号键
       }
}


/* 注释四:
* 注意,此处是电平按键的滤波抗干扰处理
*/
   if(key_sr4==1)//对应朱兆祺学习板的S13键
   {
       uiKey4Cnt1=0; //在软件滤波中,非常关键的语句!!!类似按键去抖动程序的及时清零
       uiKey4Cnt2++; //类似独立按键去抖动的软件抗干扰处理
       if(uiKey4Cnt2>const_key_time4)
       {
         uiKey4Cnt2=0;
         ucKey4Sr=1;//实时反映按键松手时的电平状态
       }
   }
   else   
   {
       uiKey4Cnt2=0; //在软件滤波中,非常关键的语句!!!类似按键去抖动程序的及时清零
       uiKey4Cnt1++;
       if(uiKey4Cnt1>const_key_time4)
       {
          uiKey4Cnt1=0;
          ucKey4Sr=0;//实时反映按键按下时的电平状态
       }
   }


}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
               case 1:
                  switch(ucPart) //在不同的局部变量下,相当于二级菜单
                                        {
                                           case 1://年
                                                ucYear++;
                                                        if(ucYear>99)
                                                        {
                                                           ucYear=99;
                                                        }
                                      ucWd1Part1Update=1;//更新显示
                                                break;
                                           case 2: //月
                                                ucMonth++;
                                                        if(ucMonth>12)
                                                        {
                                                           ucMonth=12;
                                                        }
                                      ucWd1Part2Update=1;//更新显示                                             
                                                break;
                                           case 3: //日
                                                ucDate++;
                                                        if(ucDate>31)
                                                        {
                                                           ucDate=31;
                                                        }
                                      ucWd1Part3Update=1;//更新显示               
                                                break;                                       

                                        }


                  break;
               case 2:
                  switch(ucPart) //在不同的局部变量下,相当于二级菜单
                                        {
                                           case 1://时
                                                ucHour++;
                                                        if(ucHour>23)
                                                        {
                                                           ucHour=23;
                                                        }
                                      ucWd2Part1Update=1;//更新显示                                             
                                                break;
                                           case 2: //分
                                                ucMinute++;
                                                        if(ucMinute>59)
                                                        {
                                                           ucMinute=59;
                                                        }
                                      ucWd2Part2Update=1;//更新显示                                                     
                                                break;
                                           case 3: //秒
                                                ucSecond++;
                                                        if(ucSecond>59)
                                                        {
                                                           ucSecond=59;
                                                        }
                                      ucWd2Part3Update=1;//更新显示       
                                                break;                                       

                                        }
                  break;
         
          }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
               case 1:
                  switch(ucPart) //在不同的局部变量下,相当于二级菜单
                                        {
                                           case 1://年
                                                ucYear--;
                                                        if(ucYear>99)
                                                        {
                                                           ucYear=0;
                                                        }
                                      ucWd1Part1Update=1;//更新显示
                                                break;
                                           case 2: //月
                                                ucMonth--;
                                                        if(ucMonth<1)
                                                        {
                                                           ucMonth=1;
                                                        }
                                      ucWd1Part2Update=1;//更新显示                                             
                                                break;
                                           case 3: //日
                                                ucDate--;
                                                        if(ucDate<1)
                                                        {
                                                           ucDate=1;
                                                        }
                                      ucWd1Part3Update=1;//更新显示               
                                                break;                                       

                                        }


                  break;
               case 2:
                  switch(ucPart) //在不同的局部变量下,相当于二级菜单
                                        {
                                           case 1://时
                                                ucHour--;
                                                        if(ucHour>23)
                                                        {
                                                           ucHour=0;
                                                        }
                                      ucWd2Part1Update=1;//更新显示                                             
                                                break;
                                           case 2: //分
                                                ucMinute--;
                                                        if(ucMinute>59)
                                                        {
                                                           ucMinute=0;
                                                        }
                                      ucWd2Part2Update=1;//更新显示                                                     
                                                break;
                                           case 3: //秒
                                                ucSecond--;
                                                        if(ucSecond>59)
                                                        {
                                                           ucSecond=0;
                                                        }
                                      ucWd2Part3Update=1;//更新显示       
                                                break;                                       

                                        }
                  break;
         
          }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3://短按设置按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
               case 1:
                  ucPart++;
                                        if(ucPart>3)
                                        {
                                           ucPart=1;
                                           ucWd=2; //切换到第二个窗口,设置时分秒
                                           ucWd2Update=1;//窗口2更新显示
                                        }
                                  ucWd1Update=1;//窗口1更新显示
                  break;
               case 2:
                                if(ucPart>0) //在窗口2的时候,要第一次激活设置时间,必须是长按3秒才可以,这里短按激活不了第一次
                                        {
                     ucPart++;
                                              if(ucPart>3)//设置时间结束
                                           {
                                             ucPart=0;



/* 注释五:
* 每个月份的天数最大值是不一样的,在写入ds1302时钟芯片内部数据前,应该做一次调整。
* 有的月份最大28天,有的月份最大29天,有的月份最大30天,有的月份最大31天,
*/                                                  
                           ucDate=date_adjust(ucYear,ucMonth,ucDate); //日调整 避免日的数值在某个月份超范围

                                 ucYearBCD=number_to_bcd(ucYear);//原始数值转BCD
                                 ucMonthBCD=number_to_bcd(ucMonth); //原始数值转BCD
                                   ucDateBCD=number_to_bcd(ucDate);//原始数值转BCD
                                 ucHourBCD=number_to_bcd(ucHour);//原始数值转BCD
                                 ucMinuteBCD=number_to_bcd(ucMinute);//原始数值转BCD
                                 ucSecondBCD=number_to_bcd(ucSecond);//原始数值转BCD

                                                   Write1302 (WRITE_PROTECT,0X00);          //禁止写保护
                           Write1302 (WRITE_YEAR,ucYearBCD);      //年修改
                           Write1302 (WRITE_MONTH,ucMonthBCD);      //月修改
                           Write1302 (WRITE_DATE,ucDateBCD);      //日修改
                           Write1302 (WRITE_HOUR,ucHourBCD);      //小时修改
                           Write1302 (WRITE_MINUTE,ucMinuteBCD);    //分钟修改
                           Write1302 (WRITE_SECOND,ucSecondBCD);    //秒位修改
                           Write1302 (WRITE_PROTECT,0x80);          //允许写保护
                                             }
                                              ucWd2Update=1;//窗口2更新显示
                                        }

                  break;
         
          }

         
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         
    case 17://长按3秒设置按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
               case 2:
                                if(ucPart==0) //处于非设置时间的状态下,要第一次激活设置时间,必须是长按3秒才可以
                                        {
                                          ucWd=1;
                     ucPart=1;//进入到设置日期的状态下
                                          ucWd1Update=1;//窗口1更新显示
                                        }
                  break;
         
          }
          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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


/* 注释六:
* 注意,此处就是第一次出现的电平按键程序,跟以往的下降沿按键不一样。
* ucKey4Sr是经过软件滤波处理后,直接反应IO口电平状态的变量.当电平发生
* 变化时,就会切换到不同的显示界面,这里多用了一个ucKey4SrRecord变量
* 记录上一次的电平状态,是为了避免一直刷新显示。
*/
if(ucKey4Sr!=ucKey4SrRecord)//说明S13的切换按键电平状态发生变化
{
   ucKey4SrRecord=ucKey4Sr;//及时记录当前最新的按键电平状态避免一直进来触发

       if(ucKey4Sr==1) //松手后切换到显示时间的窗口
       {
          ucWd=2;    //显示时分秒的窗口
                ucPart=0;//进入到非设置时间的状态下
          ucWd2Update=1;//窗口2更新显示
       }
       else//按下去切换到显示日期的窗口
       {
          ucWd=1;   //显示年月日的窗口
                ucPart=0;//进入到非设置时间的状态下
          ucWd1Update=1;//窗口1更新显示
       }

}
}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}




if(ucDs1302Error>0) //EEPROM出错
{
      if(ucDs1302Lock==0)//原子锁判断
          {
             uiDs1302Cnt++;//间歇性蜂鸣器报警的计时器
          }
}


if(ucDpyTimeLock==0) //原子锁判断
{
   uiDpyTimeCnt++;//数码管的闪烁计时器
}



key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

void initial_myself(void)//第一区 初始化单片机
{

key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断


/* 注释七:
* 检查ds1302芯片的备用电池电量是否用完了。
* 在上电的时候,在一个特定的地址里把数据读出来,如果发现不等于0x5a,
* 则是因为备用电池电量用完了,导致保存在ds1302内部RAM数据区的数据被更改,
* 与此同时,应该重新写入一次0x5a,为下一次通电判断做准备
*/
   ucCheckDs1302=Read1302(READ_CHECK); //判断ds1302内部的数据是否被更改
   if(ucCheckDs1302!=0x5a)
   {
          Write1302 (WRITE_PROTECT,0X00);          //禁止写保护
      Write1302 (WRITE_CHECK,0x5a);            //重新写入标志数据,方便下一次更换新电池后的判断
      Write1302 (WRITE_PROTECT,0x80);          //允许写保护

          ucDs1302Error=1;//表示ds1302备用电池没电了,报警提示更换新电池
   }


}

总结陈词:
下一节开始讲单片机驱动温度传感器芯片的内容,欲知详情,请听下回分解-----利用DS18B20做一个温控器。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-5-28 12:31:31

第四十九节:利用DS18B20做一个温控器。

开场白:
      DS18B20是一款常用的温度传感器芯片,它只占用单片机一根IO口,使用起来也特别方便。需要特别注意的是,正因为它只用一根IO口跟单片机通讯,因此读取一次温度值的通讯时间比较长,而且时序要求严格,在通讯期间不允许被单片机其它的中断干扰,因此在实际项目中,系统一旦选用了这款传感器芯片,就千万不要选用动态扫描数码管的显示方式。否则在关闭中断读取温度的时候,数码管的显示会有略微的“闪烁”现象。
      DS18B20的测温范围是-55度至125度。在-10度至85度的温度范围内误差是+-0.5度,能满足大部分常用的测温要求。
这一节要教会大家三个知识点:
第一个:大概了解一下DS18B20的驱动程序。
第二个:做温控设备的时候,为了避免继电器在临界温度附近频繁跳动切换,应该设置一个缓冲温差。本程序的缓冲温差是2度。
第三个:继续加深了解按键,显示,传感器它们三者是如何紧密关联起来的程序框架。

具体内容,请看源代码讲解。

(1)        硬件平台.
基于坚鸿51单片机学习板。

(2)实现功能:
   本程序只有1个窗口。这个窗口有2个局部显示。
第1个局部是第7,6,5位数码管,显示设定的温度。
第2个局部是第4,3,2,1位数码管,显示实际环境温度。其中第4位数码管显示正负符号位。
S1按键是加键,S5按键是减键。通过它们可以直接设置“设定温度”。
一个LED灯用来模拟工控的继电器。
当实际温度低于或者等于设定温度2度以下时,模拟继电器的LED灯亮。
当实际温度等于或者大于设定温度时,模拟继电器的LED灯灭。
当实际温度处于设定温度和设定温度减去2度的范围内,模拟继电器的LED维持现状,这个2度范围用来做缓冲温差,避免继电器在临界温度附近频繁跳动切换。

(3)源代码讲解如下:
#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间
#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间

#define const_ds18b20_sampling_time    180   //累计主循环次数的时间,每次刷新采样时钟芯片的时间


void initial_myself(void);   
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);


//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01);

void T0_time(void);//定时中断函数

void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void temper_control_service(void); //温控程序
void ds18b20_sampling(void); //ds18b20采样程序

void ds18b20_reset(); //复位ds18b20的时序
unsigned char ds_read_byte(void ); //读一字节
void ds_write_byte(unsigned char dat); //写一个字节
unsigned int get_temper();//读取一次没有经过换算的温度数值

sbit dq_dr_sr=P2^6; //ds18b20的数据驱动线

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键

sbit led_dr=P3^5;//LED灯,模拟工控中的继电器

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口



sbit dig_hc595_sh_dr=P2^0;   //数码管的74HC595程序
sbit dig_hc595_st_dr=P2^1;
sbit dig_hc595_ds_dr=P2^2;
sbit hc595_sh_dr=P2^3;    //LED灯的74HC595程序
sbit hc595_st_dr=P2^4;
sbit hc595_ds_dr=P2^5;


unsigned int uiSampingCnt=0;   //采集Ds1302的计时器,每秒钟更新采集一次
unsigned char ucSignFlag=0; //正负符号。0代表正数,1代表负数,表示零下多少度。
unsigned long ulCurrentTemper=33; //实际温度
unsigned long ulSetTemper=26; //设定温度

unsigned int uiTemperTemp=0; //中间变量

unsigned char ucKeySec=0;   //被触发的按键编号

unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;//第8位数码管要显示的内容
unsigned char ucDigShow7;//第7位数码管要显示的内容
unsigned char ucDigShow6;//第6位数码管要显示的内容
unsigned char ucDigShow5;//第5位数码管要显示的内容
unsigned char ucDigShow4;//第4位数码管要显示的内容
unsigned char ucDigShow3;//第3位数码管要显示的内容
unsigned char ucDigShow2;//第2位数码管要显示的内容
unsigned char ucDigShow1;//第1位数码管要显示的内容

unsigned char ucDigDot8;//数码管8的小数点是否显示的标志
unsigned char ucDigDot7;//数码管7的小数点是否显示的标志
unsigned char ucDigDot6;//数码管6的小数点是否显示的标志
unsigned char ucDigDot5;//数码管5的小数点是否显示的标志
unsigned char ucDigDot4;//数码管4的小数点是否显示的标志
unsigned char ucDigDot3;//数码管3的小数点是否显示的标志
unsigned char ucDigDot2;//数码管2的小数点是否显示的标志
unsigned char ucDigDot1;//数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp=0; //临时中间变量
unsigned char ucDisplayDriveStep=1;//动态扫描数码管的步骤变量


unsigned char ucWd=1;//因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要

unsigned char ucWd1Part1Update=1;//在窗口1中,局部1的更新显示标志
unsigned char ucWd1Part2Update=1; //在窗口1中,局部2的更新显示标志


unsigned char ucTemp1=0;//中间过渡变量
unsigned char ucTemp2=0;//中间过渡变量
unsigned char ucTemp3=0;//中间过渡变量
unsigned char ucTemp4=0;//中间过渡变量
unsigned char ucTemp5=0;//中间过渡变量
unsigned char ucTemp6=0;//中间过渡变量
unsigned char ucTemp7=0;//中间过渡变量
unsigned char ucTemp8=0;//中间过渡变量


//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
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
0x73,//P       序号12
};
void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
      key_service(); //按键服务的应用程序
      ds18b20_sampling(); //ds18b20采样程序
      temper_control_service(); //温控程序
      display_service(); //显示的窗口菜单服务程序
   }
}

/* 注释一:
* 做温控设备的时候,为了避免继电器在临界温度附近频繁跳动切换,应该设置一个
* 缓冲温差。本程序的缓冲温差是2度。
*/
void temper_control_service(void) //温控程序
{
   if(ucSignFlag==0) //是正数的前提下
   {
      if(ulCurrentTemper>=ulSetTemper)//当实际温度大于等于设定温度时
      {
      led_dr=0; //模拟继电器的LED灯熄灭
      }
      else if(ulCurrentTemper<=(ulSetTemper-2))//当实际温度小于等于设定温度2读以下时,这里的2是缓冲温差2度
      {
      led_dr=1; //模拟继电器的LED灯点亮
      }
   }
   else//是负数,说明是零下多少度的情况下
   {
      led_dr=1; //模拟继电器的LED灯点亮
   }

}


void ds18b20_sampling(void) //ds18b20采样程序
{

      ++uiSampingCnt;//累计主循环次数的时间
      if(uiSampingCnt>const_ds18b20_sampling_time)//每隔一段时间就更新采集一次Ds18b20数据
          {
          uiSampingCnt=0;

          ET0=0;//禁止定时中断
          uiTemperTemp=get_temper();//读取一次没有经过换算的温度数值
          ET0=1; //开启定时中断

          if((uiTemperTemp&0xf800)==0xf800) //是负号
          {
                       ucSignFlag=1;

             uiTemperTemp=~uiTemperTemp;//求补码
             uiTemperTemp=uiTemperTemp+1;

          }
          else //是正号
          {
                       ucSignFlag=0;

          }



          ulCurrentTemper=0; //把int数据类型赋给long类型之前要先清零
          ulCurrentTemper=uiTemperTemp;

          ulCurrentTemper=ulCurrentTemper*10; //为了先保留一位小数点,所以放大10倍,
          ulCurrentTemper=ulCurrentTemper>>4;//往右边移动4位,相当于乘以0.0625. 此时保留了1位小数点,

          ulCurrentTemper=ulCurrentTemper+5;//四舍五入
          ulCurrentTemper=ulCurrentTemper/10; //四舍五入后,去掉小数点

          ucWd1Part2Update=1; //局部2更新显示实时温度
          }
}


//ds18b20驱动程序
unsigned int get_temper()//读取一次没有经过换算的温度数值
{
unsigned char temper_H;
unsigned char temper_L;
unsigned int ds18b20_data=0;

ds18b20_reset(); //复位ds18b20的时序
ds_write_byte(0xCC);
ds_write_byte(0x44);

ds18b20_reset(); //复位ds18b20的时序
ds_write_byte(0xCC);
ds_write_byte(0xBE);
temper_L=ds_read_byte();
temper_H=ds_read_byte();

ds18b20_data=temper_H;   //把两个字节合并成一个int数据类型
ds18b20_data=ds18b20_data<<8;
ds18b20_data=ds18b20_data|temper_L;
return ds18b20_data;
}



void ds18b20_reset() //复位ds18b20的时序
{
unsigned char x;
dq_dr_sr=1;
delay_short(8);
dq_dr_sr=0;
delay_short(80);
dq_dr_sr=1;
delay_short(14);
x=dq_dr_sr;
delay_short(20);

}

void ds_write_byte(unsigned char date) //写一个字节
{
unsigned chari;

for(i=0;i<8;i++)
{
dq_dr_sr=0;
dq_dr_sr=date&0x01;
delay_short(5);
dq_dr_sr=1;
date=date>>1;
}
}

unsigned char ds_read_byte(void ) //读一字节
{
unsigned char i;
unsigned char date=0;
for(i=0;i<8;i++)
{
dq_dr_sr=0;
date=date>>1;
dq_dr_sr=1;
if(dq_dr_sr)
{
   date=date|0x80;
}
delay_short(5);
}
return (date);
}



void display_service(void) //显示的窗口菜单服务程序
{

   switch(ucWd)//因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要
   {
       case 1:

                        if(ucWd1Part1Update==1)//局部设定温度更新显示
                        {
                           ucWd1Part1Update=0;

               ucTemp8=10; //显示空

                           if(ulSetTemper>=100)
                           {
                  ucTemp7=ulSetTemper%1000/100; //显示设定温度的百位
                           }
                           else
                           {
                              ucTemp7=10; //显示空
                           }

                           if(ulSetTemper>=10)
                           {
                  ucTemp6=ulSetTemper%100/10; //显示设定温度的十位
                           }
                           else
                           {
                              ucTemp6=10; //显示空
                           }

               ucTemp5=ulSetTemper%10; //显示设定温度的个位


               ucDigShow8=ucTemp8; //数码管显示实际内容
               ucDigShow7=ucTemp7;
               ucDigShow6=ucTemp6;
               ucDigShow5=ucTemp5;
                        }


                        if(ucWd1Part2Update==1)//局部实际温度更新显示
                        {
                           if(ucSignFlag==0)//正数
                           {
                  ucTemp4=10; //显示空
                           }
                           else//负数,说明是零下多少度的情况下
                           {
                  ucTemp4=11; //显示负号-
                           }

                           if(ulCurrentTemper>=100)
                           {
                  ucTemp3=ulCurrentTemper%100/100; //显示实际温度的百位
                           }
                           else
                           {
                        ucTemp3=10; //显示空
                           }


                           if(ulCurrentTemper>=10)
                           {
                  ucTemp2=ulCurrentTemper%100/10;//显示实际温度的十位
                           }
                           else
                           {
                  ucTemp2=10;//显示空
                           }

               ucTemp1=ulCurrentTemper%10; //显示实际温度的个数位

               ucDigShow4=ucTemp4; //数码管显示实际内容
               ucDigShow3=ucTemp3;
               ucDigShow2=ucTemp2;
               ucDigShow1=ucTemp1;
                        }

            break;

      }
   

}

void key_scan(void)//按键扫描函数 放在定时中断里
{
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}





}

void key_service(void) //按键服务的应用程序
{

switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd) //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要
          {
            case 1: //在窗口1下设置设定温度
                   ulSetTemper++;
                                   if(ulSetTemper>125)
                                   {
                                     ulSetTemper=125;
                                   }

                             ucWd1Part1Update=1; //更新显示设定温度
                   break;
          }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd) //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要
          {
               case 1: //在窗口1下设置设定温度
                  if(ulSetTemper>2)//由于缓冲温差是2度,所以我人为规定最小允许设定的温度不能低于2度
                                        {
                                           ulSetTemper--;
                                        }

                      ucWd1Part1Update=1; //更新显示设定温度
                  break;
         
          }

          ucVoiceLock=1;//原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;//原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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


         
}         


}

void display_drive(void)
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   {
      case 1://显示第1位
         ucDigShowTemp=dig_table;
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2://显示第2位
         ucDigShowTemp=dig_table;
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3://显示第3位
         ucDigShowTemp=dig_table;
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4://显示第4位
         ucDigShowTemp=dig_table;
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5://显示第5位
         ucDigShowTemp=dig_table;
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6://显示第6位
         ucDigShowTemp=dig_table;
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7://显示第7位
         ucDigShowTemp=dig_table;
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
         }
         dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8://显示第8位
         ucDigShowTemp=dig_table;
                   if(ucDigDot8==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;//显示小数点
                   }
         dig_hc595_drive(ucDigShowTemp,0x7f);
               break;
   }
   ucDisplayDriveStep++;
   if(ucDisplayDriveStep>8)//扫描完8个数码管后,重新从第一个开始扫描
   {
   ucDisplayDriveStep=1;
   }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   dig_hc595_sh_dr=0;
   dig_hc595_st_dr=0;
   ucTempData=ucDigStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucDigStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)dig_hc595_ds_dr=1;
         else dig_hc595_ds_dr=0;
         dig_hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         dig_hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   dig_hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   dig_hc595_st_dr=1;
   delay_short(1);
   dig_hc595_sh_dr=0;    //拉低,抗干扰就增强
   dig_hc595_st_dr=0;
   dig_hc595_ds_dr=0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01)
{
   unsigned char i;
   unsigned char ucTempData;
   hc595_sh_dr=0;
   hc595_st_dr=0;
   ucTempData=ucLedStatusTemp16_09;//先送高8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   ucTempData=ucLedStatusTemp08_01;//再先送低8位
   for(i=0;i<8;i++)
   {
         if(ucTempData>=0x80)hc595_ds_dr=1;
         else hc595_ds_dr=0;
         hc595_sh_dr=0;   //SH引脚的上升沿把数据送入寄存器
         delay_short(1);
         hc595_sh_dr=1;
         delay_short(1);
         ucTempData=ucTempData<<1;
   }
   hc595_st_dr=0;//ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
   delay_short(1);
   hc595_st_dr=1;
   delay_short(1);
   hc595_sh_dr=0;    //拉低,抗干扰就增强
   hc595_st_dr=0;
   hc595_ds_dr=0;
}


void T0_time(void) interrupt 1   //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(ucVoiceLock==0) //原子锁判断
{
   if(uiVoiceCnt!=0)
   {

      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
      beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
   
   }
   else
   {

      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
      beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
      
   }
}


key_scan(); //按键扫描函数
display_drive();//数码管字模的驱动函数

TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}

void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;   //一个分号相当于执行一条空语句
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{
led_dr=0;//此处的LED灯模拟工控中的继电器
key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
hc595_drive(0x00,0x00);//关闭所有经过另外两个74HC595驱动的LED灯
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;

}
void initial_peripheral(void) //第二区 初始化外围
{

   ucDigDot8=0;   //小数点全部不显示
   ucDigDot7=0;
   ucDigDot6=0;
   ucDigDot5=0;
   ucDigDot4=0;
   ucDigDot3=0;
   ucDigDot2=0;
   ucDigDot1=0;

   EA=1;   //开总中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}

总结陈词:
下一节开始讲单片机采集模拟信号的内容,欲知详情,请听下回分解-----利用ADC0832采集电压的模拟信号。

(未完待续,下节更精彩,不要走开哦)
页: 1 2 [3] 4 5 6 7 8 9 10 11
查看完整版本: 从业将近十年!手把手教你单片机程序框架(连载)