这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 从业将近十年!手把手教你单片机程序框架(连载)

共88条 6/9 |‹ 4 5 6 7 8 9 跳转至
菜鸟
2014-06-09 11:59:27     打赏
51楼
第四十七节:操作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_short  40   //蜂鸣器短叫的持续时间
#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间
#define const_key_time3  20    //按键去抖动延时的时间


#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 int  uiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned int  uiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志



unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=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 int  uiSetData1=0;  //本程序中需要被设置的参数1
unsigned int  uiSetData2=0;  //本程序中需要被设置的参数2
unsigned int  uiSetData3=0;  //本程序中需要被设置的参数3
unsigned int  uiSetData4=0;  //本程序中需要被设置的参数4

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

unsigned char ucDelayTimerLock=0; //原子锁
unsigned int  uiDelayTimer=0;

unsigned char ucCheckEeprom=0;  //检查EEPROM芯片是否正常
unsigned char ucEepromError=0; //EEPROM芯片是否正常的标志

unsigned char ucEepromLock=0;//原子锁
unsigned int  uiEepromCnt=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 int  uiReadDate;

   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[ucDigShow1];
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2:  //显示第2位
           ucDigShowTemp=dig_table[ucDigShow2];
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3:  //显示第3位
           ucDigShowTemp=dig_table[ucDigShow3];
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4:  //显示第4位
           ucDigShowTemp=dig_table[ucDigShow4];
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5:  //显示第5位
           ucDigShowTemp=dig_table[ucDigShow5];
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6:  //显示第6位
           ucDigShowTemp=dig_table[ucDigShow6];
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7:  //显示第7位
           ucDigShowTemp=dig_table[ucDigShow7];
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
           }
           dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8:  //显示第8位
           ucDigShowTemp=dig_table[ucDigShow8];
                   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做一个实时时钟  。
(未完待续,下节更精彩,不要走开哦)



菜鸟
2014-06-09 12:01:51     打赏
52楼
第四十八节:利用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_half  200  //数码管闪烁时间的半值
#define const_dpy_time_all   400  //数码管闪烁时间的全值 一定要比const_dpy_time_half 大

#define const_voice_short  40   //蜂鸣器短叫的持续时间
#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间
#define const_key_time3  20    //按键去抖动延时的时间
#define const_key_time4  20    //按键去抖动延时的时间

#define const_key_time17  1200  //长按超过3秒的时间
#define const_ds1302_0_5s  200   //大概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 int  uiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志
unsigned int  uiKeyTimeCnt3=0; //按键去抖动延时计数器
unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

unsigned int uiKey4Cnt1=0;  //在软件滤波中,用到的变量
unsigned int uiKey4Cnt2=0;
unsigned char ucKey4Sr=1;  //实时反映按键的电平状态
unsigned char ucKey4SrRecord=0; //记录上一次按键的电平状态

unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=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 char  ucYear=0;    //原始数据
unsigned char  ucMonth=0;  
unsigned char  ucDate=0;  
unsigned char  ucHour=0;  
unsigned char  ucMinute=0;  
unsigned char  ucSecond=0;  

unsigned char  ucYearBCD=0;   //BCD码的数据
unsigned char  ucMonthBCD=0;  
unsigned char  ucDateBCD=0;  
unsigned char  ucHourBCD=0;  
unsigned char  ucMinuteBCD=0;  
unsigned char  ucSecondBCD=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 int  uiDelayTimer=0;

unsigned char ucCheckDs1302=0;  //检查Ds1302芯片是否正常
unsigned char ucDs1302Error=0; //Ds1302芯片的备用电池是否用完了的报警标志

unsigned char ucDs1302Lock=0;//原子锁
unsigned int  uiDs1302Cnt=0; //间歇性蜂鸣器报警的计时器

unsigned char ucDpyTimeLock=0; //原子锁
unsigned int  uiDpyTimeCnt=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[ucDigShow1];
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2:  //显示第2位
           ucDigShowTemp=dig_table[ucDigShow2];
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3:  //显示第3位
           ucDigShowTemp=dig_table[ucDigShow3];
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4:  //显示第4位
           ucDigShowTemp=dig_table[ucDigShow4];
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5:  //显示第5位
           ucDigShowTemp=dig_table[ucDigShow5];
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6:  //显示第6位
           ucDigShowTemp=dig_table[ucDigShow6];
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7:  //显示第7位
           ucDigShowTemp=dig_table[ucDigShow7];
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
           }
           dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8:  //显示第8位
           ucDigShowTemp=dig_table[ucDigShow8];
                   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做一个温控器  。

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



菜鸟
2014-06-09 12:06:45     打赏
53楼
第四十九节:利用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_short  40   //蜂鸣器短叫的持续时间
#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间

#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 int  uiKeyTimeCnt1=0; //按键去抖动延时计数器
unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志
unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器
unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=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 char  i;

 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[ucDigShow1];
                   if(ucDigDot1==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfe);
               break;
      case 2:  //显示第2位
           ucDigShowTemp=dig_table[ucDigShow2];
                   if(ucDigDot2==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfd);
               break;
      case 3:  //显示第3位
           ucDigShowTemp=dig_table[ucDigShow3];
                   if(ucDigDot3==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xfb);
               break;
      case 4:  //显示第4位
           ucDigShowTemp=dig_table[ucDigShow4];
                   if(ucDigDot4==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xf7);
               break;
      case 5:  //显示第5位
           ucDigShowTemp=dig_table[ucDigShow5];
                   if(ucDigDot5==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xef);
               break;
      case 6:  //显示第6位
           ucDigShowTemp=dig_table[ucDigShow6];
                   if(ucDigDot6==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
                   }
           dig_hc595_drive(ucDigShowTemp,0xdf);
               break;
      case 7:  //显示第7位
           ucDigShowTemp=dig_table[ucDigShow7];
                   if(ucDigDot7==1)
                   {
                      ucDigShowTemp=ucDigShowTemp|0x80;  //显示小数点
           }
           dig_hc595_drive(ucDigShowTemp,0xbf);
               break;
      case 8:  //显示第8位
           ucDigShowTemp=dig_table[ucDigShow8];
                   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采集电压的模拟信号。

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


菜鸟
2014-06-09 12:08:43     打赏
54楼
第五十节:利用ADC0832采集电压信号,用平均法和区间法进行软件滤波处理。

开场白:
ADC0832是一款常用的8位AD采样芯片,通过它可以把外部的模拟电压信号转换成数字信号,然后给单片机进行换算,显示等处理。
这一节要教会大家五个知识点:
第一个:分辨率的算法。有些书上说8位AD最高分辩可达到256级(0xff+1),当输入电压是0---5V时,电压精度为19.53mV(5000mV除以256),我认为这种说法是错误的。8位AD的最高分辨率应该是255级(0xff),当输入电压是0---5V时,电压精度为19.61mV(5000mV除以255)。
第二个:用求平均值的滤波法,可以使AD采样的数据更加圆滑,去除小毛刺。
第三个:用区间滤波法,在一些干扰很大的场合,可以避免末尾小数点的数据频繁跳动。
第四个:如何使系统可以采集到更高的电压。由于ADC0832直接采集的电压最大不能超过5V,如果要采集的最大电压是25V该怎么办?我们只要在外部多增加1个10K的电阻和1个40K的电阻组成分压电路,把25V分压成5V,然后再让ADC0832采样,这时采样到的数据只要乘以5的系数,就可以得到超过5V的实际电压。选择分压电阻时,阻值尽量不要太小,一般要10K级别以上,阻值大一点,对被采样的系统干扰影响就越小。
第五个:如何有效保护AD通道口。我在一些电压不稳定的工控场合,一般是在AD通道口对负极反接一个瞬变二极管SA5.0A。当电压超过5V时,瞬变二极管会导通吸收掉多余的能量,把电压降下来,避免AD通道口烧坏。

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

(1)        硬件平台.
基于朱兆祺51单片机学习板。

(2)实现功能:
     本程序有2个局部显示。
第1个局部是第8,7,6,5位数码管,显示没有经过滤波处理的实际电压值。此时能观察到未经滤波的数据不太稳定,末尾小数点数据会有跳动的现象
第2个局部是第4,3,2,1位数码管,显示经过平均法,区间法滤波的实际电压值。此时能观察到经过滤波后的数据很稳定,没有跳动的现象

系统保留3位小数点。手动调节可调电阻时,可以看到显示的数据在变化。

(3)源代码讲解如下:

#include "REG52.H"

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

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 ad_sampling_service(void); //AD采样与处理的服务程序


sbit led_dr=P3^5;  //LED灯
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;  


sbit adc0832_clk_dr     = P1^2;  // 定义adc0832的引脚
sbit adc0832_cs_dr      = P1^0;
sbit adc0832_data_sr_dr = P1^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 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 char ucAD=0;   //AD值
unsigned char ucCheckAD=0; //用来做校验对比的AD值


unsigned long ulTemp=0;  //参与换算的中间变量
unsigned long ulTempFilterV=0; //参与换算的中间变量
unsigned long ulBackupFilterV=5000;  //备份最新采样数据的中间变量
unsigned char ucSamplingCnt=0; //统计采样的次数  本程序采样8次后求平均值 

unsigned long ulV=0; //未经滤波处理的实时电压值
unsigned long ulFilterV=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)  
   { 
      ad_sampling_service(); //AD采样与处理的服务程序
      display_service(); //显示的窗口菜单服务程序
   }
}

void ad_sampling_service(void) //AD采样与处理的服务程序
{
    unsigned char i;

    ucAD=0;   //AD值
    ucCheckAD=0; //用来做校验对比的AD值


    /* 片选信号置为低电平 */
    adc0832_cs_dr = 0;

        /* 第一个脉冲,开始位 */
        adc0832_data_sr_dr = 1;
        adc0832_clk_dr  = 0;
    delay_short(1);
        adc0832_clk_dr  = 1;

        /* 第二个脉冲,选择通道 */
        adc0832_data_sr_dr = 1;
        adc0832_clk_dr  = 0;
        adc0832_clk_dr  = 1;

        /* 第三个脉冲,选择通道 */
        adc0832_data_sr_dr = 0;
        adc0832_clk_dr  = 0;
        adc0832_clk_dr  = 1;

    /* 数据线输出高电平 */
        adc0832_data_sr_dr = 1;
    delay_short(2);

        /* 第一个下降沿 */
        adc0832_clk_dr  = 1;
        adc0832_clk_dr  = 0;
    delay_short(1);


        /* AD值开始送出 */
        for (i = 0; i < 8; i++)
        {
        ucAD <<= 1;
                adc0832_clk_dr = 1;
                adc0832_clk_dr = 0;
                if (adc0832_data_sr_dr==1)
                {
            ucAD |= 0x01;
                }
        }

        /* 用于校验的AD值开始送出 */
        for (i = 0; i < 8; i++)
        {
        ucCheckAD >>= 1;
                if (adc0832_data_sr_dr==1)
                {
           ucCheckAD |= 0x80; 
                }
                adc0832_clk_dr = 1;
                adc0832_clk_dr = 0;
        }
        
        /* 片选信号置为高电平 */
        adc0832_cs_dr = 1;


        if(ucCheckAD==ucAD)  //检验相等
        {
        
            ulTemp=0;  //把char类型数据赋值给long类型数据之前,必须先清零
        ulTemp=ucAD; //把char类型数据赋值给long类型数据,参与乘除法运算的数据,为了避免运算结果溢出,我都用long类型

/* 注释一:
 * 因为保留3为小数点,这里的5000代表5.000V。ulTemp/255代表分辨率.
 * 有些书上说8位AD最高分辩可达到256级(0xff+1),我认为这种说法是错误的。
 * 8位AD最高分辩应该是255级(0xff),所以这里除以255,而不是256.
 */
        ulTemp=5000*ulTemp/255;  //进行电压换算

        ulV=ulTemp; //得到未经滤波处理的实时电压值
        ucWd1Part1Update=1; //局部更新显示未经滤波处理的电压


                ulTempFilterV=ulTempFilterV+ulTemp;  //累加8次后求平均值
        ucSamplingCnt++;  //统计已经采样累计的次数 
                if(ucSamplingCnt>=8)
                {

/* 注释二:
 * 求平均值滤波法,为了得到的数据更加圆滑,去除小毛刺。
 * 向右边移动3位相当于除以8。
 */

                     ulTempFilterV=ulTempFilterV>>3; //求平均值滤波法


/* 注释三:
 * 以下区间滤波法,为了避免末尾小数点的数据频繁跳动。
 * 这里的20用于区间滤波法的正负偏差,这里的20代表0.020V。
 * 意思是只要最近采集到的数据在正负0.020V偏差范围内,就不更新。
 */
                    if(ulBackupFilterV>=20)  //最近备份的上一次数据大于等于0.02V的情况下
                    {
                       if(ulTempFilterV<(ulBackupFilterV-20)||ulTempFilterV>(ulBackupFilterV+20)) //在正负0.020V偏差范围外,更新
                       {
                           ulBackupFilterV=ulTempFilterV;  //备份最新采样的数据,方便下一次对比判断

                   ulFilterV=ulTempFilterV; //得到经过滤波处理的实时电压值
                   ucWd1Part2Update=1; //局部更新显示经过滤波处理的电压
                              }
                    }
                    else   //最近备份的上一次数据小于0.02V的情况下
                    {
                       if(ulTempFilterV>(ulBackupFilterV+20))  //在正0.020V偏差范围外,更新
                       {
                           ulBackupFilterV=ulTempFilterV;  //备份最新采样的数据,方便下一次对比判断

                   ulFilterV=ulTempFilterV; //得到经过滤波处理的实时电压值
                   ucWd1Part2Update=1; //局部更新显示经过滤波处理的电压
                           }
                  
                    }


                    ucSamplingCnt=0;  //清零,为下一轮采样滤波作准备。
                    ulTempFilterV=0;
                }
        
        }

}

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

                        if(ucWd1Part1Update==1)//未经滤波处理的实时电压更新显示
                        {
                           ucWd1Part1Update=0;

               ucTemp8=ulV%10000/1000;  //显示电压值个位
               ucTemp7=ulV%1000/100;    //显示电压值小数点后第1位
               ucTemp6=ulV%100/10;      //显示电压值小数点后第2位
               ucTemp5=ulV%10;          //显示电压值小数点后第3位


               ucDigShow8=ucTemp8; //数码管显示实际内容
               ucDigShow7=ucTemp7; 
               ucDigShow6=ucTemp6; 
               ucDigShow5=ucTemp5; 
                        }


                        if(ucWd1Part2Update==1)//经过滤波处理后的实时电压更新显示
                        {
                             ucWd1Part2Update=0;

               ucTemp4=ulFilterV%10000/1000;  //显示电压值个位
               ucTemp3=ulFilterV%1000/100;    //显示电压值小数点后第1位
               ucTemp2=ulFilterV%100/10;      //显示电压值小数点后第2位
               ucTemp1=ulFilterV%10;          //显示电压值小数点后第3位


               ucDigShow4=ucTemp4; //数码管显示实际内容
               ucDigShow3=ucTemp3; 
               ucDigShow2=ucTemp2; 
               ucDigShow1=ucTemp1; 
                        }


}



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


  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灯默认关闭
  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=1;   //显示未经过滤波电压的小数点
   ucDigDot7=0;  
   ucDigDot6=0; 
   ucDigDot5=0;  
   ucDigDot4=1;  //显示经过滤波后电压的小数点
   ucDigDot3=0;  
   ucDigDot2=0;
   ucDigDot1=0;

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

}

 

总结陈词:
这节用区间滤波法虽然可以解决小数点后面的数据出现频繁跳动的现象,但是也存在一个小问题,就是精度受到了影响,比如我们设置的正负偏差是0.02V,那就意味着系统存在0.02V的误差。有没有更好的办法解决这个问题?如果系统的末尾数据一直不断处于频繁跳动中,那么只能牺牲一点精度,我认为用区间法已经是最好的解决办法了,但是经过本次实验,我观察到未经过滤波处理的数据只是偶尔跳动,并非频繁跳动,所以下一节我会给大家介绍一种不用牺牲精度,又可以很好滤波的方法。欲知详情,请听下回分解-----利用ADC0832采集电压信号,用连续N次一致性的方法进行滤波处理。


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


菜鸟
2014-06-15 11:41:44     打赏
55楼

第五十一节:利用ADC0832采集电压信号,用连续N次一致性的方法进行滤波处理。


开场白:

连续判断N次一致性的滤波法,是为了避免末尾小数点的数据偶尔跳动。这种滤波方法的原理跟我在按键扫描中去抖动的原理是一模一样的,被我频繁地应用在大量的工控项目中。

这一节要教会大家一个知识点:连续判断N次一致性的滤波法。

具体原理:当某个采样变量发生变化时,有两种可能,一种可能是外界的一个瞬间干扰。另一种可能是变量确实发生变化。为了有效去除干扰,当发现变量有变化时,我会连续采集N次,如果连续N次都是一致的结果,我才认为不是干扰。如果中间只要出现一次不一致,我会马上把计数器清零,这一步是精华,很关键。

 

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

 

(1)硬件平台.

基于朱兆祺51单片机学习板。

 

(2)实现功能:

 本程序有2个局部显示。

第1个局部是第8,7,6,5位数码管,显示没有经过滤波处理的实际电压值。此时能观察到未经滤波的数据不太稳定,末尾小数点数据会有跳动的现象

第2个局部是第4,3,2,1位数码管,显示经过特定算法滤波后的实际电压值。此时能观察到经过滤波后的数据很稳定,没有跳动的现象。而且显示的电压值跟未经过滤波的电压值几乎是完全一致,不会出现上一节用区间滤波法所留下的0.02V误差问题。

系统保留3位小数点。手动调节可调电阻时,可以看到显示的数据在变化。

 

(3)源代码讲解如下:

 

#include "REG52.H"

#define const_N   8  //连续判断N次一致性滤波方法中,N的取值
#define const_voice_short  40   //蜂鸣器短叫的持续时间

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 ad_sampling_service(void); //AD采样与处理的服务程序


sbit led_dr=P3^5;  //LED灯
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;  


sbit adc0832_clk_dr     = P1^2;  // 定义adc0832的引脚
sbit adc0832_cs_dr      = P1^0;
sbit adc0832_data_sr_dr = P1^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 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 char ucAD=0;   //AD值
unsigned char ucCheckAD=0; //用来做校验对比的AD值


unsigned long ulTemp=0;  //参与换算的中间变量
unsigned long ulTempFilterV=0; //参与换算的中间变量
unsigned long ulBackupFilterV=5000;  //备份最新采样数据的中间变量
unsigned char ucSamplingCnt=0; //记录连续N次采样的计数器

unsigned long ulV=0; //未经滤波处理的实时电压值
unsigned long ulFilterV=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)  
   { 
      ad_sampling_service(); //AD采样与处理的服务程序
      display_service(); //显示的窗口菜单服务程序
   }
}

void ad_sampling_service(void) //AD采样与处理的服务程序
{
    unsigned char i;

    ucAD=0;   //AD值
    ucCheckAD=0; //用来做校验对比的AD值


    /* 片选信号置为低电平 */
    adc0832_cs_dr = 0;

        /* 第一个脉冲,开始位 */
        adc0832_data_sr_dr = 1;
        adc0832_clk_dr  = 0;
    delay_short(1);
        adc0832_clk_dr  = 1;

        /* 第二个脉冲,选择通道 */
        adc0832_data_sr_dr = 1;
        adc0832_clk_dr  = 0;
        adc0832_clk_dr  = 1;

        /* 第三个脉冲,选择通道 */
        adc0832_data_sr_dr = 0;
        adc0832_clk_dr  = 0;
        adc0832_clk_dr  = 1;

    /* 数据线输出高电平 */
        adc0832_data_sr_dr = 1;
    delay_short(2);

        /* 第一个下降沿 */
        adc0832_clk_dr  = 1;
        adc0832_clk_dr  = 0;
    delay_short(1);


        /* AD值开始送出 */
        for (i = 0; i < 8; i++)
        {
        ucAD <<= 1;
                adc0832_clk_dr = 1;
                adc0832_clk_dr = 0;
                if (adc0832_data_sr_dr==1)
                {
            ucAD |= 0x01;
                }
        }

        /* 用于校验的AD值开始送出 */
        for (i = 0; i < 8; i++)
        {
        ucCheckAD >>= 1;
                if (adc0832_data_sr_dr==1)
                {
           ucCheckAD |= 0x80; 
                }
                adc0832_clk_dr = 1;
                adc0832_clk_dr = 0;
        }
        
        /* 片选信号置为高电平 */
        adc0832_cs_dr = 1;


        if(ucCheckAD==ucAD)  //检验相等
        {
        
            ulTemp=0;  //把char类型数据赋值给long类型数据之前,必须先清零
            ulTemp=ucAD; //把char类型数据赋值给long类型数据,参与乘除法运算的数据,为了避免运算结果溢出,我都用long类型

/* 注释一:
 * 因为保留3为小数点,这里的5000代表5.000V。ulTemp/255代表分辨率.
 * 有些书上说8位AD最高分辩可达到256级(0xff+1),我认为这种说法是错误的。
 * 8位AD最高分辩应该是255级(0xff),所以这里除以255,而不是256.
 */
            ulTemp=5000*ulTemp/255;  //进行电压换算
            ulV=ulTemp; //得到未经滤波处理的实时电压值
            ucWd1Part1Update=1; //局部更新显示未经滤波处理的电压


/* 注释二:
 * 以下连续判断N次一致性的滤波法,为了避免末尾小数点的数据偶尔跳动。
 * 这种滤波方法的原理跟我在按键扫描中的去抖动原理是一模一样的,被我频繁
 * 地应用在大量的工控项目中。
 * 具体原理:当某个采样变量发生变化时,有两种可能,一种可能是外界的一个瞬间干扰。
 * 另一种可能是变量确实发生变化。为了有效去除干扰,当发现变量有变化时,
 * 我会连续采集N次,如果连续N次都是一致的结果,我才认为不是干扰。如果中间
 * 只要出现一次不一致,我会马上把计数器清零,这一步是精华,很关键。
 * 
 */
	  	    if(ulTempFilterV!=ulTemp) //发现变量有变化
	 	    {
		        ucSamplingCnt++;    //计数器累加
		  	    if(ucSamplingCnt>const_N)  //如果连续N次都是一致的,则认为不是干扰。确实有数据需要更新显示。这里的const_N取值是8
			    {
			        ucSamplingCnt=0;

			        ulTempFilterV=ulTemp;   //及时保存更新了的数据,方便下一次有新数据对比做准备

                    ulFilterV=ulTempFilterV; //得到经过滤波处理的实时电压值
                    ucWd1Part2Update=1; //局部更新显示经过滤波处理的电压			 
			    }
	   	    }
		    else
		    {
		         ucSamplingCnt=0;  //只要出现一次不一致,我会马上把计数器清零,这一步是精华,很关键。
		    }



        
        }

}

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

                        if(ucWd1Part1Update==1)//未经滤波处理的实时电压更新显示
                        {
                           ucWd1Part1Update=0;

               ucTemp8=ulV%10000/1000;  //显示电压值个位
               ucTemp7=ulV%1000/100;    //显示电压值小数点后第1位
               ucTemp6=ulV%100/10;      //显示电压值小数点后第2位
               ucTemp5=ulV%10;          //显示电压值小数点后第3位


               ucDigShow8=ucTemp8; //数码管显示实际内容
               ucDigShow7=ucTemp7; 
               ucDigShow6=ucTemp6; 
               ucDigShow5=ucTemp5; 
                        }


                        if(ucWd1Part2Update==1)//经过滤波处理后的实时电压更新显示
                        {
                             ucWd1Part2Update=0;

               ucTemp4=ulFilterV%10000/1000;  //显示电压值个位
               ucTemp3=ulFilterV%1000/100;    //显示电压值小数点后第1位
               ucTemp2=ulFilterV%100/10;      //显示电压值小数点后第2位
               ucTemp1=ulFilterV%10;          //显示电压值小数点后第3位


               ucDigShow4=ucTemp4; //数码管显示实际内容
               ucDigShow3=ucTemp3; 
               ucDigShow2=ucTemp2; 
               ucDigShow1=ucTemp1; 
                        }


}



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


  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灯默认关闭
  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=1;   //显示未经过滤波电压的小数点
   ucDigDot7=0;  
   ucDigDot6=0; 
   ucDigDot5=0;  
   ucDigDot4=1;  //显示经过滤波后电压的小数点
   ucDigDot3=0;  
   ucDigDot2=0;
   ucDigDot1=0;

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

}

 

总结陈词:

在单片机AD采样的系统中,我常用的滤波方法有求平均值法,区间法,连续判断N次一致性这三种方法。读者可以根据不同的系统特点选择对应的滤波方法,有一些要求高的系统还可以把三种滤波方法混合在一起用。关于AD采样的知识到本节已经讲完,下一节会讲什么新内容呢?欲知详情,请听下回分解-----return语句鲜为人知的用法。


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


菜鸟
2014-06-22 01:37:08     打赏
56楼

第五十二节:程序后续升级修改的利器,return语句鲜为人知的用法。

 

开场白:

return语句经常用在带参数返回的函数中,字面上理解就是返回的意思,因此很多单片机初学者很容易忽略了return语句还有中断强行退出的功能。利用这个强行退出的功能,在项目后续程序的升级修改上很方便,还可以有效减少if语句的嵌套层数,使程序阅读起来很简洁。这一节要教大家return语句三个鲜为人知的用法:

第一个鲜为人知的用法:在空函数里,可以插入很多个return语句,不仅仅是一个。

第二个鲜为人知的用法:return语句可以有效较少程序里条件判断语句的嵌套层数。

第三个鲜为人知的用法:return语句本身已经包含了类似break语句的功能,不管当前处于几层的内部循环嵌套,只要遇到return语句都可以强行退出全部循环,并且直接退出当前子程序,不执行当前子程序后面的任何语句,这个功能实在是太强大,太铁腕了。

 

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

 

1)硬件平台:

基于朱兆祺51单片机学习板。

 

2)实现功能:

本程序实现的功能跟第三十九节是一摸一样的,唯一的差别就是在第三十九节的基础上,插入了几个return语句,用新的return语句替代原来的条件和循环判断语句。

 

波特率是:9600

通讯协议:EB 00 55  XX YY 

加无效填充字节后,上位机实际上应该发送:00  EB 00 55  XX YY

其中第100是无效填充字节,防止由于硬件原因丢失第一个字节。

其中第2,3,4EB 00 55就是数据头

           2XX YY就是有效数据

任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字后面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据头和有效数据都是正确的。

 

也就是说,当在 串口助手往单片机发送十六进制数据串:  eb 00 55 01 02  时,会听到蜂鸣器的一声。

 

3)源代码讲解如下:

 

#include "REG52.H"


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

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

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 int  uiSendCnt=0;     //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned int  uiRcregTotal=0;  //代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf[const_rc_size]; //接收串口中断数据的缓冲区数组
unsigned int  uiRcMoveIndex=0;  //用来解析数据协议的中间变量


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



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

}

/* 注释一:
 * 以下函数说明了,在空函数里,可以插入很多个return语句。
 * 用return语句非常便于后续程序的升级修改。
 */
void usart_service(void)  //串口服务程序,在main函数里
{

        

//     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //原来的语句,现在被两个return语句替代了
//     {

       if(uiSendCnt<const_receive_time)  //延时还没超过规定时间,直接退出本程序,不执行return后的任何语句。
	   {
	      return;  //强行退出本子程序,不执行以下任何语句
	   }

	   if(ucSendLock==0)  //不是最新一次接收到串口数据,直接退出本程序,不执行return后的任何语句。
	   {
	      return;  //强行退出本子程序,不执行以下任何语句
	   }
/* 注释二:
 * 以上两条return语句就相当于原来的一条if(uiSendCnt>=const_receive_time&&ucSendLock==1)语句。
 * 用了return语句后,就明显减少了一个if嵌套。
 */


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

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

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动


 //           while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) //原来的语句,现在被两个return语句替代了
            while(1) //死循环可以被以下return或者break语句中断,return本身已经包含了break语句功能。
            {
               if(uiRcregTotal<5)  //串口接收到的数据太少
			   {
			      uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
				  return;  //强行退出while(1)循环嵌套,直接退出本程序,不执行以下任何语句
			   }

			   if(uiRcMoveIndex>(uiRcregTotal-5)) //数组缓冲区的数据已经处理完
			   {
			      uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
				  return;  //强行退出while(1)循环嵌套,直接退出本程序,不执行以下任何语句
			   }
/* 注释三:
 * 以上两条return语句就相当于原来的一条while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))语句。
 * 以上两个return语句的用法,同时说明了return本身已经包含了break语句功能,不管当前处于几层的内部循环嵌套,
 * 都可以强行退出循环,并且直接退出本程序。
 */


               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {
                  if(ucRcregBuf[uiRcMoveIndex+3]==0x01&&ucRcregBuf[uiRcMoveIndex+4]==0x02)  //有效数据01 02的判断
                  {
                       uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据头和有效数据都接收正确
                  }
                  break;   //退出while(1)循环
               }
               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[uiRcregTotal-1]=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;    //启动定时中断

}

 

总结陈词:

我在第一节就告诉读者了,搞单片机开发如果不会C语言的指针也没关系,不会影响做项目。我本人平时做项目时,也很少用指针,只有在三种场合下我才会用指针,因为在这三种场合下,用了指针感觉程序阅读起来更加清爽了。所以,指针还是有它独到的好处,有哪三种好处?欲知详情,请听下回分解-----指针的第一大好处,让一个函数可以封装多个相当于return语句返回的参数。

 

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


菜鸟
2014-06-29 07:03:44     打赏
57楼

第五十三节:指针的第一大好处,让一个函数可以封装多个相当于return语句返回的参数。

 

开场白:

当我们想把某种算法通过一个函数来实现的时候,如果不会指针,那么只有两种方法。

1种:用不带参数返回的空函数。这是最原始的做法,也是我当年刚毕业就开始做项目的时候经常用的方法。它完全依靠全局变量作为函数的输入和输出口。我们要用到这个函数,就要把参与运算的变量直接赋给对应的输入全局变量,调用一次函数之后,再找到对应的输出变量,这些输出变量就是我们要的结果。这种方法的缺点是阅读不直观,封装性不强,没有面对用户的输入输出接口。

2种:用return返回参数和带输入形参的函数,这种方法已经具备了完整的输入和输出性能,比第1种方法直观多了。但是这种方法有它的局限性,因为return只能返回一个变量,如果要用在返回多个输出结果的函数中,就无能为力了,这时候该怎么办?就必须用指针了,也就是我下面讲到的第3种方法。

这一节要教大家一个知识点:通过指针,让函数可以返回多个变量。

 

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

 

1)硬件平台:

基于朱兆祺51单片机学习板。

 

2)实现功能:

通过电脑串口调试助手,往单片机发送EB 00 55 XX YY  指令,其中EB 00 55是数据头, XX是被除数,YY是除数。单片机收到指令后就会返回6个数据,最前面两个数据是第1种运算方式的商和余数,中间两个数据是第2种运算方式的商和余数,最后两个数据是第3种运算方式的商和余数。

比如电脑发送:EB 00 55 08 02

单片机就返回:04 00 04 00 04 00  (04是商,00是余数)

 

串口程序的接收部分请参考第39节。串口程序的发送部分请参考第42节。

 

波特率是:9600

 

3)源代码讲解如下:

 

#include "REG52.H"


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

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

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


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


void eusart_send(unsigned char ucSendData);
void chu_fa_yun_suan_1(void);//第1种方法 求商和余数
unsigned char get_shang_2(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp); //第2种方法 求商
unsigned char get_yu_2(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp); //第2种方法 求余数
void chu_fa_yun_suan_3(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp,unsigned char *p_ucShangTemp,unsigned char *p_ucYuTemp);//第3种方法 求商和余数

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

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


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


unsigned char ucBeiChuShu_1=0;  //第1种方法中的被除数
unsigned char ucChuShu_1=1;     //第1种方法中的除数
unsigned char ucShang_1=0;      //第1种方法中的商
unsigned char ucYu_1=0;         //第1种方法中的余数

unsigned char ucBeiChuShu_2=0;  //第2种方法中的被除数
unsigned char ucChuShu_2=1;     //第2种方法中的除数
unsigned char ucShang_2=0;      //第2种方法中的商
unsigned char ucYu_2=0;         //第2种方法中的余数

unsigned char ucBeiChuShu_3=0;  //第3种方法中的被除数
unsigned char ucChuShu_3=1;     //第3种方法中的除数
unsigned char ucShang_3=0;      //第3种方法中的商
unsigned char ucYu_3=0;         //第3种方法中的余数

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

}


/* 注释一:
 * 第1种方法,用不带参数返回的空函数,这是最原始的做法,也是我当年刚毕业
 * 就开始做项目的时候经常用的方法。它完全依靠全局变量作为函数的输入和输出口。
 * 我们要用到这个函数,就要把参与运算的变量直接赋给对应的输入全局变量,
 * 调用一次函数之后,再找到对应的输出变量,这些输出变量就是我们要的结果。
 * 在本函数中,被除数ucBeiChuShu_1和除数ucChuShu_1就是输入全局变量,
 * 商ucShang_1和余数ucYu_1就是输出全局变量。这种方法的缺点是阅读不直观,
 * 封装性不强,没有面对用户的输入输出接口,
 */
void chu_fa_yun_suan_1(void)//第1种方法 求商和余数
{
   if(ucChuShu_1==0) //如果除数为0,则商和余数都为0
   {
      ucShang_1=0;
	  ucYu_1=0;
   }
   else
   {
      ucShang_1=ucBeiChuShu_1/ucChuShu_1;  //求商
      ucYu_1=ucBeiChuShu_1%ucChuShu_1;  //求余数
   }

}


/* 注释二:
 * 第2种方法,用return返回参数和带输入形参的函数,这种方法已经具备了完整的输入和输出性能,
 * 比第1种方法直观多了。但是这种方法有它的局限性,因为return只能返回一个变量,
 * 如果要用在返回多个输出结果的函数中,就无能为力了。比如本程序,就不能同时输出
 * 商和余数,只能分两个函数来做。如果要在一个函数中同时输出商和余数,该怎么办?
 * 这个时候就必须用指针了,也就是我下面讲到的第3种方法。
 */
unsigned char get_shang_2(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp) //第2种方法 求商
{
   unsigned char ucShangTemp;
   if(ucChuShuTemp==0) //如果除数为0,则商为0
   {
      ucShangTemp=0;
   }
   else
   {
      ucShangTemp=ucBeiChuShuTemp/ucChuShuTemp;  //求商
   }

   return ucShangTemp; //返回运算后的结果 商
}

unsigned char get_yu_2(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp) //第2种方法 求余数
{
   unsigned char ucYuTemp;
   if(ucChuShuTemp==0) //如果除数为0,则余数为0
   {
      ucYuTemp=0;
   }
   else
   {
      ucYuTemp=ucBeiChuShuTemp%ucChuShuTemp;   //求余数
   }

   return ucYuTemp; //返回运算后的结果 余数
}

/* 注释三:
 * 第3种方法,用带指针的函数,就可以顺心所欲,不受return的局限,想输出多少个
 * 运算结果都可以,赞一个!在本函数中,ucBeiChuShuTemp和ucChuShuTemp是输入变量,
 * 它们不是指针,所以不具备输出接口属性。*p_ucShangTemp和*p_ucYuTemp是输出变量,
 * 因为它们是指针,所以具备输出接口属性。
 */
void chu_fa_yun_suan_3(unsigned char ucBeiChuShuTemp,unsigned char ucChuShuTemp,unsigned char *p_ucShangTemp,unsigned char *p_ucYuTemp)//第3种方法 求商和余数
{
   if(ucChuShuTemp==0) //如果除数为0,则商和余数都为0
   {
      *p_ucShangTemp=0;
	  *p_ucYuTemp=0;
   }
   else
   {
      *p_ucShangTemp=ucBeiChuShuTemp/ucChuShuTemp;  //求商
      *p_ucYuTemp=ucBeiChuShuTemp%ucChuShuTemp;  //求余数
   }

}

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

        

     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

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

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

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {

                  //第1种运算方法,依靠全局变量
                  ucBeiChuShu_1=ucRcregBuf[uiRcMoveIndex+3]; //被除数
                  ucChuShu_1=ucRcregBuf[uiRcMoveIndex+4];  //除数
				  chu_fa_yun_suan_1(); //调用一次空函数就出结果了,结果保存在ucShang_1和ucYu_1全局变量中
				  eusart_send(ucShang_1); //把运算结果返回给上位机观察
				  eusart_send(ucYu_1);//把运算结果返回给上位机观察

                  //第2种运算方法,依靠两个带return语句的返回函数
                  ucBeiChuShu_2=ucRcregBuf[uiRcMoveIndex+3]; //被除数
                  ucChuShu_2=ucRcregBuf[uiRcMoveIndex+4];  //除数
                  ucShang_2=get_shang_2(ucBeiChuShu_2,ucChuShu_2); //第2种方法 求商
                  ucYu_2=get_yu_2(ucBeiChuShu_2,ucChuShu_2); //第2种方法 求余数
				  eusart_send(ucShang_2); //把运算结果返回给上位机观察
				  eusart_send(ucYu_2);//把运算结果返回给上位机观察

                  //第3种运算方法,依靠指针
                  ucBeiChuShu_3=ucRcregBuf[uiRcMoveIndex+3]; //被除数
                  ucChuShu_3=ucRcregBuf[uiRcMoveIndex+4];  //除数
/* 注释四:
 * 注意,由于商和余数是指针形参,我们代入的变量必须带地址符号& 。比如&ucShang_3和&ucYu_3。
 * 因为我们是把变量的地址传递进去的。
 */
				  chu_fa_yun_suan_3(ucBeiChuShu_3,ucChuShu_3,&ucShang_3,&ucYu_3);//第3种方法 求商和余数 
				  eusart_send(ucShang_3); //把运算结果返回给上位机观察
				  eusart_send(ucYu_3);//把运算结果返回给上位机观察


                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                         
}

void eusart_send(unsigned char ucSendData) //往上位机发送一个字节的函数
{

  ES = 0; //关串口中断
  TI = 0; //清零串口发送完成中断请求标志
  SBUF =ucSendData; //发送一个字节

  delay_short(400);  //每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

  TI = 0; //清零串口发送完成中断请求标志
  ES = 1; //允许串口中断

}



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[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
    
   }
   else  //发送中断,及时把发送中断标志位清零
   {
        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 delay_short(unsigned int uiDelayShort) 
{
   unsigned int i;  
   for(i=0;i<uiDelayShort;i++)
   {
     ;   //一个分号相当于执行一条空语句
   }
}


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;    //启动定时中断

}

 

总结陈词:

这节讲了指针的第一大好处,它的第二大好处是什么?欲知详情,请听下回分解-----指针的第二大好处,指针作为数组在函数内部的化身。

 

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


菜鸟
2014-07-06 11:07:03     打赏
58楼

第五十四节:指针的第二大好处,指针作为数组在函数中的输入接口。

 

开场白:

 如果不会指针,当我们想把一个数组的数据传递进某个函数内部的时候,只能通过全局变量的方式,这种方法的缺点是阅读不直观,封装性不强,没有面对用户的输入接口。

针对以上问题,这一节要教大家一个知识点:通过指针,为函数增加一个数组输入接口。

 

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

 

1)硬件平台:

基于朱兆祺51单片机学习板。

 

2)实现功能:

5个随机数据按从大到小排序,用冒泡法来排序。

通过电脑串口调试助手,往单片机发送EB 00 55 08 06 09 05 07  指令,其中EB 00 55是数据头,08 06 09 05 07 是参与排序的5个随机原始数据。单片机收到指令后就会返回13个数据,最前面5个数据是第1种方法的排序结果,中间3个数据EE EE EE是第1种和第2种的分割线,为了方便观察,没实际意义。最后5个数据是第2种方法的排序结果.

 

比如电脑发送:EB 00 55 08 06 09 05 07

单片机就返回:09 08 07 06 05 EE EE EE 09 08 07 06 05

 

串口程序的接收部分请参考第39节。串口程序的发送部分请参考第42节。

 

波特率是:9600

 

3)源代码讲解如下:

 

#include "REG52.H"


#define const_array_size  5  //参与排序的数组大小

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

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

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


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


void eusart_send(unsigned char ucSendData);

void big_to_small_sort_1(void);//第1种方法 把一个数组从大小小排序
void big_to_small_sort_2(unsigned char *p_ucInputBuffer);//第2种方法 把一个数组从大小小排序

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

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

unsigned char ucUsartBuffer[const_array_size];  //从串口接收到的需要排序的原始数据
unsigned char ucGlobalBuffer_1[const_array_size]; //第1种方法,参与具体排序算法的全局变量数组
unsigned char ucGlobalBuffer_2[const_array_size]; //第2种方法,参与具体排序算法的全局变量数组
void main() 
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
       usart_service();  //串口服务程序
   }

}


/* 注释一:
 * 第1种方法,用不带输入输出接口的空函数,这是最原始的做法,它完全依靠
 * 全局变量作为函数的输入和输出口。我们要用到这个函数,就要把参与运算
 * 的变量直接赋给对应的输入全局变量,调用一次函数之后,再找到对应的
 * 输出全局变量,这些输出全局变量就是我们要的结果。
 * 在本函数中,ucGlobalBuffer_1[const_array_size]既是输入全局变量,也是输出全局变量,
 * 这种方法的缺点是阅读不直观,封装性不强,没有面对用户的输入输出接口,
 */
void big_to_small_sort_1(void)//第1种方法 把一个数组从大小小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量

/* 注释二:
 * 以下就是著名的 冒泡法排序。这个方法几乎所有的C语言大学教材都讲过了。大家在百度上可以直接
 * 搜索到它的工作原理和详细的讲解步骤,我就不再详细讲解了。
 */
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(ucGlobalBuffer_1[const_array_size-1-k]>ucGlobalBuffer_1[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=ucGlobalBuffer_1[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             ucGlobalBuffer_1[const_array_size-1-1-k]=ucGlobalBuffer_1[const_array_size-1-k];
             ucGlobalBuffer_1[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }

}

/* 注释三:
 * 第2种方法,为了改进第1种方法的用户体验,用指针为函数增加一个输入接口。
 * 为什么要用指针?因为C语言的函数中,数组不能直接用来做函数的形参,只能用指针作为数组的形参。
 * 比如,你不能这样写一个函数void big_to_small_sort_2(unsigned char a[5]),否则编译就会出错不通过。
 * 在本函数中,*p_ucInputBuffer指针就是输入接口,而输出接口仍然是全局变量数组ucGlobalBuffer_2。
 * 这种方法由于为函数多增加了一个数组输入接口,已经比第1种方法更加直观了。
 */
void big_to_small_sort_2(unsigned char *p_ucInputBuffer)//第2种方法 把一个数组从大小小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量


   for(i=0;i<const_array_size;i++)  
   {
      ucGlobalBuffer_2[i]=p_ucInputBuffer[i];  //参与排序算法之前,先把输入接口的数据全部搬移到全局变量数组中。
   }


   //以下就是著名的 冒泡法排序。详细讲解请找百度。
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(ucGlobalBuffer_2[const_array_size-1-k]>ucGlobalBuffer_2[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=ucGlobalBuffer_2[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             ucGlobalBuffer_2[const_array_size-1-1-k]=ucGlobalBuffer_2[const_array_size-1-k];
             ucGlobalBuffer_2[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }

}



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

     unsigned char i=0;   

     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

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

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

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {


				  for(i=0;i<const_array_size;i++)
				  {
                     ucUsartBuffer[i]=ucRcregBuf[uiRcMoveIndex+3+i]; //从串口接收到的需要被排序的原始数据
				  }


                  //第1种运算方法,依靠全局变量
				  for(i=0;i<const_array_size;i++)
				  {
				     ucGlobalBuffer_1[i]=ucUsartBuffer[i];  //把需要被排列的数据放进输入全局变量数组
				  }
                  big_to_small_sort_1(); //调用一次空函数就出结果了,结果还是保存在ucGlobalBuffer_1全局变量数组中
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_1[i]);  ////把用第1种方法排序后的结果返回给上位机观察
				  }


				  eusart_send(0xee);  //为了方便上位机观察,多发送3个字节ee ee ee作为第1种方法与第2种方法的分割线
				  eusart_send(0xee); 
				  eusart_send(0xee); 

                  //第2种运算方法,依靠指针为函数增加一个数组的输入接口
				  //通过指针输入接口,直接把ucUsartBuffer数组的首地址传址进去,排序后输出的结果还是保存在ucGlobalBuffer_2全局变量数组中
                  big_to_small_sort_2(ucUsartBuffer); 
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_2[i]);  //把用第2种方法排序后的结果返回给上位机观察
				  }





                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                         
}

void eusart_send(unsigned char ucSendData) //往上位机发送一个字节的函数
{

  ES = 0; //关串口中断
  TI = 0; //清零串口发送完成中断请求标志
  SBUF =ucSendData; //发送一个字节

  delay_short(400);  //每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

  TI = 0; //清零串口发送完成中断请求标志
  ES = 1; //允许串口中断

}



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


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



  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[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
    
   }
   else  //发送中断,及时把发送中断标志位清零
   {
        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 delay_short(unsigned int uiDelayShort) 
{
   unsigned int i;  
   for(i=0;i<uiDelayShort;i++)
   {
     ;   //一个分号相当于执行一条空语句
   }
}


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;    //启动定时中断

}

 

总结陈词:

2种方法通过指针,为函数增加了一个数组输入接口,已经比第1种纯粹用全局变量的方法直观多了,但是还有一个小小的遗憾,因为它的输出排序结果仍然要靠全局变量。为了让函数更加完美,我们能不能为函数再增加一个输出接口?当然可以。欲知详情,请听下回分解-----指针的第三大好处,指针作为数组在函数中的输出接口。

 

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


菜鸟
2014-07-10 15:26:51     打赏
59楼

第五十五节:指针的第三大好处,指针作为数组在函数中的输出接口。

 

开场白:

上一节介绍的第2种方法,由于为函数多增加了一个数组输入接口,已经比第1种方法更加直观了,但是由于只有输入接口,没有输出接口,输出接口仍然要靠全局变量数组,所以还是有一个小小的遗憾,这节介绍的第3种方法就是为了改变这个遗憾,为数组在函数中多增加一个输出接口,这样,函数既有输入接口,又有输出接口,这样的函数才算完美直观。这一节要教大家一个知识点:通过指针,为函数增加一个数组输出接口。

 

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

 

1)硬件平台:

基于朱兆祺51单片机学习板。

 

2)实现功能:

5个随机数据按从大到小排序,用冒泡法来排序。

通过电脑串口调试助手,往单片机发送EB 00 55 08 06 09 05 07  指令,其中EB 00 55是数据头,08 06 09 05 07 是参与排序的5个随机原始数据。单片机收到指令后就会返回13个数据,最前面5个数据是第2种方法的排序结果,中间3个数据EE EE EE是第2种和第3种的分割线,为了方便观察,没实际意义。最后5个数据是第3种方法的排序结果.

 

比如电脑发送:EB 00 55 08 06 09 05 07

单片机就返回:09 08 07 06 05 EE EE EE 09 08 07 06 05

 

串口程序的接收部分请参考第39节。串口程序的发送部分请参考第42节。

 

波特率是:9600

 

3)源代码讲解如下:

 

#include "REG52.H"


#define const_array_size  5  //参与排序的数组大小

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

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

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


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


void eusart_send(unsigned char ucSendData);


void big_to_small_sort_2(unsigned char *p_ucInputBuffer);//第2种方法 把一个数组从大到小排序
void big_to_small_sort_3(unsigned char *p_ucInputBuffer,unsigned char *p_ucOutputBuffer);//第3种方法 把一个数组从大到小排序
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

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

unsigned char ucUsartBuffer[const_array_size];  //从串口接收到的需要排序的原始数据

unsigned char ucGlobalBuffer_2[const_array_size]; //第2种方法,参与具体排序算法的全局变量数组
unsigned char ucGlobalBuffer_3[const_array_size]; //第3种方法,用来接收输出接口数据的全局变量数组

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

}



/* 注释一:
 * 第2种方法,为了改进第1种方法的用户体验,用指针为函数增加一个输入接口。
 * 为什么要用指针?因为C语言的函数中,数组不能直接用来做函数的形参,只能用指针作为数组的形参。
 * 比如,你不能这样写一个函数void big_to_small_sort_2(unsigned char a[5]),否则编译就会出错不通过。
 * 在本函数中,*p_ucInputBuffer指针就是输入接口,而输出接口仍然是全局变量数组ucGlobalBuffer_2。
 * 这种方法由于为函数多增加了一个数组输入接口,已经比第1种方法更加直观了,但是由于只有输入接口,
 * 没有输出接口,输出接口仍然要靠全局变量,所以还是有点小遗憾,以下第3种方法就是为了改变这个遗憾。
 */
void big_to_small_sort_2(unsigned char *p_ucInputBuffer)//第2种方法 把一个数组从大到小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量


   for(i=0;i<const_array_size;i++)  
   {
      ucGlobalBuffer_2[i]=p_ucInputBuffer[i];  //参与排序算法之前,先把输入接口的数据全部搬移到全局变量数组中。
   }


   //以下就是著名的 冒泡法排序。详细讲解请找百度。
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(ucGlobalBuffer_2[const_array_size-1-k]>ucGlobalBuffer_2[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=ucGlobalBuffer_2[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             ucGlobalBuffer_2[const_array_size-1-1-k]=ucGlobalBuffer_2[const_array_size-1-k];
             ucGlobalBuffer_2[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }

}



/* 注释二:
 * 第3种方法,为了改进第2种方法的用户体验,用指针为函数多增加一个数组输出接口。
 * 这样,函数的数组既有输入接口,又有输出接口,已经堪称完美了。
 * 本程序中*p_ucInputBuffer输入接口,*p_ucOutputBuffer是输出接口。
 */
void big_to_small_sort_3(unsigned char *p_ucInputBuffer,unsigned char *p_ucOutputBuffer)//第3种方法 把一个数组从大到小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量
   unsigned char ucBuffer_3[const_array_size]; //第3种方法,参与具体排序算法的局部变量数组

   for(i=0;i<const_array_size;i++)  
   {
      ucBuffer_3[i]=p_ucInputBuffer[i];  //参与排序算法之前,先把输入接口的数据全部搬移到局部变量数组中。
   }


   //以下就是著名的 冒泡法排序。详细讲解请找百度。
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(ucBuffer_3[const_array_size-1-k]>ucBuffer_3[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=ucBuffer_3[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             ucBuffer_3[const_array_size-1-1-k]=ucBuffer_3[const_array_size-1-k];
             ucBuffer_3[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }


   for(i=0;i<const_array_size;i++)  
   {
      p_ucOutputBuffer[i]=ucBuffer_3[i];  //参与排序算法之后,把运算结果的数据全部搬移到输出接口中,方便外面程序调用
   }
}




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

     unsigned char i=0;   

     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

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

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

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {


				  for(i=0;i<const_array_size;i++)
				  {
                     ucUsartBuffer[i]=ucRcregBuf[uiRcMoveIndex+3+i]; //从串口接收到的需要被排序的原始数据
				  }


                  //第2种运算方法,依靠指针为函数增加一个数组的输入接口
				  //通过指针输入接口,直接把ucUsartBuffer数组的首地址传址进去,排序后输出的结果还是保存在ucGlobalBuffer_2全局变量数组中
                  big_to_small_sort_2(ucUsartBuffer); 
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_2[i]);  //把用第2种方法排序后的结果返回给上位机观察
				  }


				  eusart_send(0xee);  //为了方便上位机观察,多发送3个字节ee ee ee作为第2种方法与第3种方法的分割线
				  eusart_send(0xee); 
				  eusart_send(0xee); 

                  //第3种运算方法,依靠指针为函数增加一个数组的输出接口
				  //通过指针输出接口,排序运算后的结果直接从这个输出口中导出到ucGlobalBuffer_3数组中
                  big_to_small_sort_3(ucUsartBuffer,ucGlobalBuffer_3);   //ucUsartBuffer是输入的数组,ucGlobalBuffer_3是接收排序结果的数组
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_3[i]);  //把用第3种方法排序后的结果返回给上位机观察
				  }



                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                         
}

void eusart_send(unsigned char ucSendData) //往上位机发送一个字节的函数
{

  ES = 0; //关串口中断
  TI = 0; //清零串口发送完成中断请求标志
  SBUF =ucSendData; //发送一个字节

  delay_short(400);  //每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

  TI = 0; //清零串口发送完成中断请求标志
  ES = 1; //允许串口中断

}



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


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



  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[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
    
   }
   else  //发送中断,及时把发送中断标志位清零
   {
        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 delay_short(unsigned int uiDelayShort) 
{
   unsigned int i;  
   for(i=0;i<uiDelayShort;i++)
   {
     ;   //一个分号相当于执行一条空语句
   }
}


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;    //启动定时中断

}

 

总结陈词:

通过本节程序的讲解,一部分细心的读者可能会发现一个规律,其实所谓指针作为数组在函数中的输入接口和输出接口,输入接口的指针跟输出接口的指针在语法上没有任何区别,我没有用到C语言中专门的关键词去限定某个指针是输入,某个指针是输出,因此,这个告诉我们什么道理?指针在函数的接口中,天生就是既可以做输入,也可以是做输出,它是双向性的,不像普通的函数变量形参只能做输入。发现了这个秘密,我们可不可以把本节程序中的输入接口和输出接口合并成一个输入输出接口呢?当然可以。欲知详情,请听下回分解-----指针的第四大好处,指针作为数组在函数中的输入输出接口。

 

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


菜鸟
2014-07-10 17:36:45     打赏
60楼

第五十六节:指针的第四大好处,指针作为数组在函数中的输入输出接口。

 

开场白:

通过前面几个章节的学习,我们知道指针在函数的接口中,天生就是既可以做输入,也可以是做输出,它是双向性的,类似全局变量的特点。我们根据实际项目的情况,在必要的时候可以直接把输入接口和输出接口合并在一起,这种方法的缺点是没有把输入和输出分开,没有那么直观。但是优点也是很明显的,就是比较省程序ROM容量和数据RAM容量,而且运行效率也比较快。这一节要教大家一个知识点:指针作为数组在函数中输入输出接口的特点。

 

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

 

1)硬件平台:

基于朱兆祺51单片机学习板。

 

2)实现功能:

5个随机数据按从大到小排序,用冒泡法来排序。

通过电脑串口调试助手,往单片机发送EB 00 55 08 06 09 05 07  指令,其中EB 00 55是数据头,08 06 09 05 07 是参与排序的5个随机原始数据。单片机收到指令后就会返回13个数据,最前面5个数据是第3种方法的排序结果,中间3个数据EE EE EE是第3种和第4种的分割线,为了方便观察,没实际意义。最后5个数据是第4种方法的排序结果.

 

比如电脑发送:EB 00 55 08 06 09 05 07

单片机就返回:09 08 07 06 05 EE EE EE 09 08 07 06 05

 

串口程序的接收部分请参考第39节。串口程序的发送部分请参考第42节。

 

波特率是:9600

 

3)源代码讲解如下:

 

#include "REG52.H"


#define const_array_size  5  //参与排序的数组大小

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

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

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


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


void eusart_send(unsigned char ucSendData);

void big_to_small_sort_3(unsigned char *p_ucInputBuffer,unsigned char *p_ucOutputBuffer);//第3种方法 把一个数组从大到小排序
void big_to_small_sort_4(unsigned char *p_ucInputAndOutputBuffer);//第4种方法 把一个数组从大到小排序
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

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

unsigned char ucUsartBuffer[const_array_size];  //从串口接收到的需要排序的原始数据

unsigned char ucGlobalBuffer_3[const_array_size]; //第3种方法,用来接收输出接口数据的全局变量数组
unsigned char ucGlobalBuffer_4[const_array_size]; //第4种方法,用来输入和输出接口数据的全局变量数组
void main() 
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
       usart_service();  //串口服务程序
   }

}




/* 注释一:
 * 第3种方法,为了改进第2种方法的用户体验,用指针为函数多增加一个数组输出接口。
 * 这样,函数的数组既有输入接口,又有输出接口,已经堪称完美了。
 * 本程序中*p_ucInputBuffer输入接口,*p_ucOutputBuffer是输出接口。
 */
void big_to_small_sort_3(unsigned char *p_ucInputBuffer,unsigned char *p_ucOutputBuffer)//第3种方法 把一个数组从大到小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量
   unsigned char ucBuffer_3[const_array_size]; //第3种方法,参与具体排序算法的局部变量数组

   for(i=0;i<const_array_size;i++)  
   {
      ucBuffer_3[i]=p_ucInputBuffer[i];  //参与排序算法之前,先把输入接口的数据全部搬移到局部变量数组中。
   }


   //以下就是著名的 冒泡法排序。详细讲解请找百度。
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(ucBuffer_3[const_array_size-1-k]>ucBuffer_3[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=ucBuffer_3[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             ucBuffer_3[const_array_size-1-1-k]=ucBuffer_3[const_array_size-1-k];
             ucBuffer_3[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }


   for(i=0;i<const_array_size;i++)  
   {
      p_ucOutputBuffer[i]=ucBuffer_3[i];  //参与排序算法之后,把运算结果的数据全部搬移到输出接口中,方便外面程序调用
   }
}


/* 注释二:
 * 第4种方法.指针在函数的接口中,天生就是既可以做输入,也可以是做输出,它是双向性的,类似全局变量的特点。
 * 我们可以根据实际项目的情况,在必要的时候可以直接把输入接口和输出接口合并在一起,
 * 这种方法的缺点是没有把输入和输出分开,没有那么直观。但是优点也是很明显的,就是比较
 * 省程序ROM容量和数据RAM容量,而且运行效率也比较快。现在介绍给大家。
 * 本程序的*p_ucInputAndOutputBuffer是输入输出接口。
 */
void big_to_small_sort_4(unsigned char *p_ucInputAndOutputBuffer)//第4种方法 把一个数组从大到小排序
{
   unsigned char i;
   unsigned char k;
   unsigned char ucTemp; //在两两交换数据的过程中,用于临时存放交换的某个变量

   //以下就是著名的 冒泡法排序。详细讲解请找百度。
   for(i=0;i<(const_array_size-1);i++)  //冒泡的次数是(const_array_size-1)次
   {
      for(k=0;k<(const_array_size-1-i);k++) //每次冒泡的过程中,需要两两比较的次数是(const_array_size-1-i)
	  {
	     if(p_ucInputAndOutputBuffer[const_array_size-1-k]>p_ucInputAndOutputBuffer[const_array_size-1-1-k])  //后一个与前一个数据两两比较
		 {
		     ucTemp=p_ucInputAndOutputBuffer[const_array_size-1-1-k];     //通过一个中间变量实现两个数据交换
             p_ucInputAndOutputBuffer[const_array_size-1-1-k]=p_ucInputAndOutputBuffer[const_array_size-1-k];
             p_ucInputAndOutputBuffer[const_array_size-1-k]=ucTemp;
		 }
	  
	  }
   }


}


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

     unsigned char i=0;   

     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

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

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

            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {


				  for(i=0;i<const_array_size;i++)
				  {
                     ucUsartBuffer[i]=ucRcregBuf[uiRcMoveIndex+3+i]; //从串口接收到的需要被排序的原始数据
				  }


                  //第3种运算方法,依靠指针为函数增加一个数组的输出接口
				  //通过指针输出接口,排序运算后的结果直接从这个输出口中导出到ucGlobalBuffer_3数组中
                  big_to_small_sort_3(ucUsartBuffer,ucGlobalBuffer_3);   //ucUsartBuffer是输入的数组,ucGlobalBuffer_3是接收排序结果的数组
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_3[i]);  //把用第3种方法排序后的结果返回给上位机观察
				  }

				  eusart_send(0xee);  //为了方便上位机观察,多发送3个字节ee ee ee作为第2种方法与第3种方法的分割线
				  eusart_send(0xee); 
				  eusart_send(0xee); 

                  //第4种运算方法,依靠一个指针作为函数的输入输出接口。
				  //通过这个指针输入输出接口,ucGlobalBuffer_4数组既是输入数组,也是输出数组,排序运算后的结果直接存放在它本身,类似于全局变量的特点。
				  for(i=0;i<const_array_size;i++)
				  {
                     ucGlobalBuffer_4[i]=ucUsartBuffer[i]; //把需要被排序的原始数据传递给接收输入输出数组ucGlobalBuffer_4,
				  }
                  big_to_small_sort_4(ucGlobalBuffer_4);  
                  for(i=0;i<const_array_size;i++)
				  {
				    eusart_send(ucGlobalBuffer_4[i]);  //把用第4种方法排序后的结果返回给上位机观察
				  }


                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                         
}

void eusart_send(unsigned char ucSendData) //往上位机发送一个字节的函数
{

  ES = 0; //关串口中断
  TI = 0; //清零串口发送完成中断请求标志
  SBUF =ucSendData; //发送一个字节

  delay_short(400);  //每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

  TI = 0; //清零串口发送完成中断请求标志
  ES = 1; //允许串口中断

}



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


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



  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[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
    
   }
   else  //发送中断,及时把发送中断标志位清零
   {
        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 delay_short(unsigned int uiDelayShort) 
{
   unsigned int i;  
   for(i=0;i<uiDelayShort;i++)
   {
     ;   //一个分号相当于执行一条空语句
   }
}


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;    //启动定时中断

}

 

总结陈词:

通过本章的学习,我们知道指针在函数接口中的双向性,这个双向性是一把双刃剑,既给我们带来便捷,也给我们在以下两个场合中带来隐患。

第一个场合:当需要把输入接口和输出接口分开时,我们希望输入接口的参数不要被意外改变,改变的仅仅只能是输出接口的数据。但是指针的双向性,就有可能导致我们在写函数内部代码的时候一不小心改变而没有发觉。

第二个场合:如果是一个现成封装好的函数直接给我们调用,当我们发现是指针作为接口的时候,我们就不敢确定这个接口是输入接口,还是输出接口,或者是输入输出接口,我们传递进去的参数可能会更改,除非用之前进行数据备份,否则是没有安全感可言的。

有没有办法巧妙的解决以上两个问题?当然有。欲知详情,请听下回分解-----为指针加上紧箍咒const,避免意外修改了只做输入接口的数据。

 

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


共88条 6/9 |‹ 4 5 6 7 8 9 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]