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

共146条 6/15 |‹ 4 5 6 7 8 9 ›| 跳转至
院士
2014-06-09 11:10:32     打赏
51楼
教科书不行吗?

菜鸟
2014-06-09 11:12:07     打赏
52楼
第四十节:常用的自定义串口通讯协议。

开场白:
上一节讲了判断数据头的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,数据地址,有效数据长度,有效数据,数据校验的通讯协议。这一节要教会大家三个知识点:
第一个:常用自定义串口通讯协议的程序框架。
第二个:累加校验和的校验方法。累加和的意思是前面所有字节的数据相加,超过一个字节的溢出部分会按照固定的规则自动丢弃,不用我们管。比如以下数据:
      eb 00 55 01 00 02 0028 6b  
      其中eb 00 55为数据头,01为数据类型,00 02为有效数据长度,00 28 分别为具体的有效数据,6b为前面所有字节的累加和。累加和可以用电脑系统自带的计算器来验证。打开电脑上的计算器,点击“查看”下拉的菜单,选“科学型”,然后选左边的“十六进制”,最后选右边的“字节”,然后把前面所有的字节相加,它们的和就是6b,没错吧。
第三个:原子锁的使用方法,实际上是借鉴了"红金龙吸味"关于原子锁的建议,专门用来保护中断与主函数的共享数据。
具体内容,请看源代码讲解。

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

(2)实现功能:
  波特率是:9600.
通讯协议:EB 00 55  GG HH HH XX XX …YYYY CY
其中第1,2,3位EB 00 55就是数据头
其中第4位GG就是数据类型。01代表驱动奉命,02代表驱动Led灯。
其中第5,6位HH就是有效数据长度。高位在左,低位在右。
其中第5,6位HH就是有效数据长度。高位在左,低位在右。
其中从第7位开始,到最后一个字节Cy之前,XX..YY都是具体的有效数据。
在本程序中,当数据类型是01时,有效数据代表蜂鸣器鸣叫的时间长度。当数据类型是02时,有效数据代表Led灯点亮的时间长度。
最后一个字节CY是累加和,前面所有字节的累加。
发送以下测试数据,将会分别控制蜂鸣器和Led灯的驱动时间长度。
蜂鸣器短叫发送:eb 00 55 01 00 02 00 28 6b  
蜂鸣器长叫发送:eb 00 55 01 00 02 00 fa 3d  
Led灯短亮发送:eb 00 55 02 00 02 00 28 6c
Led灯长亮发送:eb 00 55 02 00 02 00 fa3e  

(3)源代码讲解如下:

#include "REG52.H"

/* 注释一:
 * 请评估实际项目中一串数据的最大长度是多少,并且留点余量,然后调整const_rc_size的大小。
 * 本节程序把上一节的缓冲区数组大小10改成了20
 */
#define const_rc_size  20  //接收串口中断数据的缓冲区数组大小

#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函数里
void led_service(void);  //Led灯的服务程序。

sbit led_dr=P3^5;  //Led的驱动IO口
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  ucSendCntLock=0; //串口计时器的原子锁

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

unsigned char  ucVoiceLock=0;  //蜂鸣器鸣叫的原子锁

unsigned char ucRcType=0;  //数据类型
unsigned int  uiRcSize=0;  //数据长度
unsigned char ucRcCy=0;  //校验累加和

unsigned int  uiRcVoiceTime=0;  //蜂鸣器发出声音的持续时间

unsigned int  uiRcLedTime=0; //在串口服务程序中,Led灯点亮时间长度的中间变量
unsigned int  uiLedTime=0;  //Led灯点亮时间的长度
unsigned int  uiLedCnt=0;   //Led灯点亮的计时器
unsigned char ucLedLock=0;  //Led灯点亮时间的原子锁

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

}

void led_service(void)
{
   if(uiLedCnt<uiLedTime)
   {
      led_dr=1; //开Led灯
   }
   else
   {
      led_dr=0; //关Led灯
   }
}

void usart_service(void)  //串口服务程序,在main函数里
{
/* 注释三:
 * 我借鉴了朱兆祺的变量命名习惯,单个字母的变量比如i,j,k,h,这些变量只用作局部变量,直接在函数内部定义。
 */
     unsigned int i;  
        
     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

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



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

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


            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {
                         ucRcType=ucRcregBuf[uiRcMoveIndex+3];   //数据类型  一个字节

                                         uiRcSize=ucRcregBuf[uiRcMoveIndex+4];   //数据长度  两个字节
                                         uiRcSize=uiRcSize<<8;
                                         uiRcSize=uiRcSize+ucRcregBuf[uiRcMoveIndex+5];
                                                                 
                                         ucRcCy=ucRcregBuf[uiRcMoveIndex+6+uiRcSize];   //记录最后一个字节的校验 
                     ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=0;  //清零最后一个字节的累加和变量
/* 注释四:
 * 计算校验累加和的方法:除了最后一个字节,其它前面所有的字节累加起来,
 * 溢出的不用我们管,C语言编译器会按照固定的规则自动处理。
 * 以下for循环里的(3+1+2+uiRcSize),其中3代表3个字节数据头,1代表1个字节数据类型,
 * 2代表2个字节的数据长度变量,uiRcSize代表实际上一串数据中的有效数据个数。
 */
                                          for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                                         {
                                                 ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=ucRcregBuf[uiRcMoveIndex+6+uiRcSize]+ucRcregBuf[i];
                     }        


                                         if(ucRcCy==ucRcregBuf[uiRcMoveIndex+6+uiRcSize])  //如果校验正确,则进入以下数据处理
                                         {                                                  
                         switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                                             {

                             case 0x01:   //驱动蜂鸣器发出声音,并且可以控制蜂鸣器持续发出声音的时间长度
        
                                  uiRcVoiceTime=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiRcVoiceTime=uiRcVoiceTime<<8;  
                                  uiRcVoiceTime=uiRcVoiceTime+ucRcregBuf[uiRcMoveIndex+7];

                                  ucVoiceLock=1;  //共享数据的原子锁加锁
                                  uiVoiceCnt=uiRcVoiceTime; //蜂鸣器发出声音
                                  ucVoiceLock=0;  //共享数据的原子锁解锁

                                                              break;        
                                                                        
                             case 0x02:   //点亮一个LED灯,并且可以控制LED灯持续亮的时间长度
                                  uiRcLedTime=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiRcLedTime=uiRcLedTime<<8;  
                                  uiRcLedTime=uiRcLedTime+ucRcregBuf[uiRcMoveIndex+7];

                                  ucLedLock=1;  //共享数据的原子锁加锁
                                  uiLedTime=uiRcLedTime; //更改点亮Led灯的时间长度
                                                                  uiLedCnt=0;  //在本程序中,清零计数器就等于自动点亮Led灯
                                  ucLedLock=0;  //共享数据的原子锁解锁
                                                              break;
                                                                         
                         }

                                         }        

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


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

 /* 注释五:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  
  if(ucSendCntLock==0)  //原子锁判断
  {
     ucSendCntLock=1; //加锁
     if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
     {
        uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
        ucSendLock=1;     //开自锁标志
     }
     ucSendCntLock=0; //解锁
  }

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

  if(ucLedLock==0)  //原子锁判断
  {
     if(uiLedCnt<uiLedTime)
         {
            uiLedCnt++;  //Led灯点亮的时间计时器
         }
  }

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


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

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

            ++uiRcregTotal;
        if(uiRcregTotal>const_rc_size)  //超过缓冲区
        {
           uiRcregTotal=const_rc_size;
        }
        ucRcregBuf[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

        if(ucSendCntLock==0)  //原子锁判断
        {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;  //及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
                    ucSendCntLock=0; //解锁
                }
    
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;
   }
                                                         
}                                


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


void initial_myself(void)  //第一区 初始化单片机
{
  led_dr=0; //关Led灯
  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

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


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

}

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

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

}

 

总结陈词:
这一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,我会用另外一种响应速度更快的串口程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----在串口接收中断里即时解析数据头的特殊程序框架。

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


菜鸟
2014-06-09 11:18:41     打赏
53楼
好贴 楼主神人  

菜鸟
2014-06-09 11:21:42     打赏
54楼
别误会,我本人没有批判教科书的意思。我写这个帖子,只是分享我个人一些经验和体会,我哪些说的有道理,哪些说的很片面,希望读者可以有自己独立的判断,不强求读者完全赞同我的观点。

菜鸟
2014-06-09 11:22:30     打赏
55楼
别误会,我本人没有批判教科书的意思。我写这个帖子,只是分享我个人一些经验和体会,我哪些说的有道理,哪些说的很片面,希望读者可以有自己独立的判断,不强求读者完全赞同我的观点。

菜鸟
2014-06-09 11:38:45     打赏
56楼
第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。

开场白:
上一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,这样的程序框架可能会满足不了系统对速度的要求,这一节就是要介绍另外一种响应速度更加快的串口程序框架,要教会大家一个知识点:在串口接收中断里即时解析数据头的特殊程序框架。我在这种程序框架里,会尽量简化数据头和数据尾,同时也简化校验,目的都是为了提高响应速度。
具体内容,请看源代码讲解。

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

(2)实现功能:
  波特率是:9600.
通讯协议:EB  GG XX XX XX XX ED
其中第1位EB就是数据头.
其中第2位GG就是数据类型。01代表驱动蜂鸣器,02代表驱动Led灯。
其中第3,4,5,6位XX就是有效数据长度。高位在左,低位在右。
其中第7位ED就是数据尾,在这里也起一部分校验的作用,虽然不是累加和的方式。

在本程序中,
当数据类型是01时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么蜂鸣器就鸣叫一声表示正确。
当数据类型是02时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么LED灯就会闪烁一下表示正确。
十进制的123456789等于十六进制的75bcd15 。
发送以下测试数据,将会分别控制蜂鸣器Led灯。
控制蜂鸣器发送:eb 01 07 5b cd 15 ed
控制LED灯发送:eb 02 07 5b cd 15 ed

(3)源代码讲解如下:


#include "REG52.H"


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

#define const_voice_short  80   //蜂鸣器短叫的持续时间
#define const_led_short  80    //LED灯亮的持续时间

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



void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void led_service(void);  //Led灯的服务程序。

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


unsigned int  uiRcregTotal=0;  //代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf[const_rc_size]; //接收串口中断数据的缓冲区数组

unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=0;  //蜂鸣器鸣叫的原子锁



unsigned int  uiRcVoiceTime=0;  //蜂鸣器发出声音的持续时间

unsigned int  uiLedCnt=0;   //Led灯点亮的计时器
unsigned char ucLedLock=0;  //Led灯点亮时间的原子锁

unsigned long ulBeepData=0; //蜂鸣器的数据
unsigned long ulLedData=0; //LED的数据

unsigned char ucUsartStep=0; //串口接收字节的步骤变量

void main() 
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
       led_service(); //Led灯的服务程序
   }

}

void led_service(void)
{
   if(uiLedCnt<const_led_short)
   {
      led_dr=1; //开Led灯
   }
   else
   {
      led_dr=0; //关Led灯
   }
}



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

 /* 注释一:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

  if(ucLedLock==0)  //原子锁判断
  {
     if(uiLedCnt<const_led_short)
     {
            uiLedCnt++;  //Led灯点亮的时间计时器
     }

  }

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


void usart_receive(void) interrupt 4                 //串口接收数据中断        
{        
 /* 注释二:
  * 以下就是吴坚鸿在串口接收中断里即时解析数据头的特殊程序框架,
  * 它的特点是靠数据头来启动接受有效数据,靠数据尾来识别一串数据接受完毕,
  * 这里的数据尾也起到一部分的校验作用,让数据更加可靠。这种程序结构适合应用
  * 在传输的数据长度不是很长,而且要求响应速度非常高的实时场合。在这种实时要求
  * 非常高的场合中,我就不像之前一样做数据累加和的复杂运算校验,只用数据尾来做简单的
  * 校验确认,目的是尽可能提高处理速度。
  */  

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

        switch(ucUsartStep) //串口接收字节的步骤变量
        {
            case 0:
                             ucRcregBuf[0]=SBUF;     
                 if(ucRcregBuf[0]==0xeb)  //数据头判断
                 {
                                     ucRcregBuf[0]=0;  //数据头及时清零,为下一串数据的接受判断做准备
                                     uiRcregTotal=1;  //缓存数组的下标初始化
                     ucUsartStep=1;  //如果数据头正确,则切换到下一步,依次把上位机来的数据存入数组缓冲区
                 }
                 break;
            case 1:
                             ucRcregBuf[uiRcregTotal]=SBUF;  //依次把上位机来的数据存入数组缓冲区
                                 uiRcregTotal++; //下标移动
                                 if(uiRcregTotal>=7)  //已经接收了7个字节
                                 {
                   if(ucRcregBuf[6]==0xed)  //数据尾判断,也起到一部分校验的作用,让数据更加可靠,虽然没有用到累加和的检验方法
                                   {
                                       ucRcregBuf[6]=0;  //数据尾及时清零,为下一串数据的接受判断做准备                                       
                                       switch(ucRcregBuf[1]) //根据不同的数据类型来做不同的数据处理
                                           {
                                               case 0x01:  //与蜂鸣器相关
                                ulBeepData=ucRcregBuf[2]; //把四个字节的数据合并成一个long型的数据
                                                            ulBeepData=ulBeepData<<8;
                                                            ulBeepData=ulBeepData+ucRcregBuf[3];
                                                            ulBeepData=ulBeepData<<8;
                                                            ulBeepData=ulBeepData+ucRcregBuf[4];
                                                            ulBeepData=ulBeepData<<8;
                                                            ulBeepData=ulBeepData+ucRcregBuf[5];
                                                                if(ulBeepData==123456789)  //如果此数据等于十进制的123456789,表示数据正确
                                                                {
                                                                    ucVoiceLock=1;  //共享数据的原子锁加锁
                                    uiVoiceCnt=const_voice_short; //蜂鸣器发出声音
                                    ucVoiceLock=0;  //共享数据的原子锁解锁
                                                                }

                                                        break;

                                               case 0x02:  //与Led灯相关
                                ulLedData=ucRcregBuf[2]; //把四个字节的数据合并成一个long型的数据
                                                            ulLedData=ulLedData<<8;
                                                            ulLedData=ulLedData+ucRcregBuf[3];
                                                            ulLedData=ulLedData<<8;
                                                            ulLedData=ulLedData+ucRcregBuf[4];
                                                            ulLedData=ulLedData<<8;
                                                            ulLedData=ulLedData+ucRcregBuf[5];
                                                                if(ulLedData==123456789)  //如果此数据等于十进制的123456789,表示数据正确
                                                                {
                                                                    ucLedLock=1;  //共享数据的原子锁加锁
                                    uiLedCnt=0;  //在本程序中,清零计数器就等于自动点亮Led灯
                                    ucLedLock=0;  //共享数据的原子锁解锁
                                                                }



                                                        break;
                                           }

                                   }

                   ucUsartStep=0;     //返回上一步数据头判断,为下一次的新数据接收做准备
                                 }
                 break;
        }
    
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;
   }
                                                         
}                                


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


void initial_myself(void)  //第一区 初始化单片机
{
  led_dr=0; //关Led灯
  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

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


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

}

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

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

}

 

总结陈词:
    前面花了4节内容仔细讲了各种串口接收数据的常用框架,从下一节开始,我开始讲串口发送数据的程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----通过串口用delay延时方式发送一串数据。

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



菜鸟
2014-06-09 11:41:24     打赏
57楼
第四十二节:通过串口用delay延时方式发送一串数据。

开场白:
   上一节讲了在串口接收中断里即时解析数据头的特殊程序框架。这节开始讲串口发送数据需要特别注意的地方和程序框架,要教会大家一个知识点:根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。

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

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

(2)实现功能:
  波特率是:9600.
按一次按键S1,单片机就往上位机发送以下一串数据:
eb 00 55 01 00 00 00 00 41

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


#define const_send_size  10  //串口发送数据的缓冲区数组大小

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

#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);

void eusart_send(unsigned char ucSendData);  //发送一个字节,内部自带每个字节之间的延时

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

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

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

sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平



unsigned char ucSendregBuf[const_send_size]; //接收串口中断数据的缓冲区数组


unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=0;  //蜂鸣器鸣叫的原子锁

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

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

void main() 
{
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
      key_service(); //按键服务的应用程序
   }

}



void eusart_send(unsigned char ucSendData)
{

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

 /* 注释一:
  * 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
  * 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
  * 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
  * 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
  * 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
  */  

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

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

}

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

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



}


void key_service() //第三区 按键服务的应用程序
{
  unsigned int i;

  switch(ucKeySec) //按键服务状态切换
  {
    case 1:// 1号键 对应朱兆祺学习板的S1键
          ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
          ucSendregBuf[1]=0x00;
          ucSendregBuf[2]=0x55;
          ucSendregBuf[3]=0x01;
          ucSendregBuf[4]=0x00;
          ucSendregBuf[5]=0x00;
          ucSendregBuf[6]=0x00;
          ucSendregBuf[7]=0x00;
          ucSendregBuf[8]=0x41;

                  for(i=0;i<9;i++)
                  {
                     eusart_send(ucSendregBuf[i]);  //发送一串数据给上位机
                  }

          ucVoiceLock=1;  //原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
                  ucVoiceLock=0; //原子锁解锁

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



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

 /* 注释二:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

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


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


void usart_receive(void) interrupt 4                 //串口中断        
{        

   if(RI==1)  
   {
        RI = 0;   //接收中断,及时把接收中断标志位清零

       
    
   }
   else 
   {
        TI = 0;    //发送中断,及时把发送中断标志位清零
   }
                                                         
}                                

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


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


void initial_myself(void)  //第一区 初始化单片机
{
/* 注释三:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
  key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

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

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


  //配置串口
  SCON=0x50;
  TMOD=0X21;
  TH1=TL1=-(11059200L/12/32/9600);  //串口波特率9600。
  TR1=1;

}

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

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

}

 

总结陈词:
这节在每个字节之间都添加了delay延时来等待每个字节的发送完成,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时,这种程序框架是什么样的?欲知详情,请听下回分解-----通过串口用计数延时方式发送一串数据。

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


菜鸟
2014-06-09 11:44:11     打赏
58楼
第四十三节:通过串口用计数延时方式发送一串数据。

开场白:
上一节讲了通过串口用delay延时方式发送一串数据,这种方式要求发送一串数据的时候一气呵成,期间不能执行其它任务,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时。本节要教会大家两个知识点:
第一个:用计数延时方式发送一串数据的程序框架。
第二个:环形消息队列的程序框架。

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

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

(2)实现功能:
     波特率是:9600.
用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
    按一次按键S1,发送EB 00 55 01 00 00 00 00 41
按一次按键S5,发送EB 00 55 02 00 00 00 00 42
按一次按键S9,发送EB 00 55 03 00 00 00 00 43
按一次按键S13,发送EB 00 55 04 00 00 00 00 44

(3)源代码讲解如下:


#include "REG52.H"


#define const_send_time  100  //累计主循环次数的计数延时 请根据项目实际情况来调整此数据大小

#define const_send_size  10  //串口发送数据的缓冲区数组大小

#define const_Message_size  10  //环形消息队列的缓冲区数组大小

#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间
#define const_key_time3  20    //按键去抖动延时的时间
#define const_key_time4  20    //按键去抖动延时的时间

#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);

void eusart_send(unsigned char ucSendData);  //发送一个字节,内部没有每个字节之间的延时
void send_service(void);  //利用累计主循环次数的计数延时方式来发送一串数据

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

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


void insert_message(unsigned char ucMessageTemp);  //插入新的消息到环形消息队列里
unsigned char get_message(void);  //从环形消息队列里提取消息



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

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

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



unsigned char ucSendregBuf[const_send_size]; //串口发送数据的缓冲区数组

unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
unsigned int  uiMessageCurrent=0;  //环形消息队列的取数据当前位置
unsigned int  uiMessageInsert=0;  //环形消息队列的插入新消息时候的位置
unsigned int  uiMessageCnt=0;  //统计环形消息队列的消息数量  等于0时表示消息队列里没有消息

unsigned char ucMessage=0; //当前获取到的消息

unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock=0;  //蜂鸣器鸣叫的原子锁

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


unsigned char ucSendStep=0;  //发送一串数据的运行步骤
unsigned int  uiSendTimeCnt=0; //累计主循环次数的计数延时器

unsigned int uiSendCnt=0; //发送数据时的中间变量

void main() 
{
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
      key_service(); //按键服务的应用程序
	  send_service();  //利用累计主循环次数的计数延时方式来发送一串数据
   }

}

 /* 注释一:
  * 通过判断数组下标是否超范围的条件,把一个数组的首尾连接起来,就像一个环形,
  * 因此命名为环形消息队列。环形消息队列有插入消息,获取消息两个核心函数,以及一个
  * 统计消息总数的uiMessageCnt核心变量,通过此变量,我们可以知道消息队列里面是否有消息需要处理.
  * 我在做项目中很少用消息队列的,印象中我只在两个项目中用过消息队列这种方法。大部分的单片机
  * 项目其实直接用一两个中间变量就可以起到传递消息的作用,就能满足系统的要求。以下是各变量的含义: 
  * #define const_Message_size  10  //环形消息队列的缓冲区数组大小
  * unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
  * unsigned int  uiMessageCurrent=0;  //环形消息队列的取数据当前位置
  * unsigned int  uiMessageInsert=0;  //环形消息队列的插入新消息时候的位置
  * unsigned int  uiMessageCnt=0;  //统计环形消息队列的消息数量  等于0时表示消息队列里没有消息
  */  

void insert_message(unsigned char ucMessageTemp)  //插入新的消息到环形消息队列里
{
   if(uiMessageCnt<const_Message_size)  //消息总数小于环形消息队列的缓冲区才允许插入新消息
   {
      ucMessageBuf[uiMessageInsert]=ucMessageTemp;

	  uiMessageInsert++;  //插入新消息时候的位置
	  if(uiMessageInsert>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
	  {
	     uiMessageInsert=0;
	  }
      uiMessageCnt++; //消息数量累加  等于0时表示消息队列里没有消息
   }
}

unsigned char get_message(void)  //从环形消息队列里提取消息
{
   unsigned char ucMessageTemp=0;  //返回的消息中间变量,默认为0

   if(uiMessageCnt>0)  //只有消息数量大于0时才可以提取消息
   {
      ucMessageTemp=ucMessageBuf[uiMessageCurrent];
	  uiMessageCurrent++;  //环形消息队列的取数据当前位置
	  if(uiMessageCurrent>=const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
	  {
	     uiMessageCurrent=0;
	  }
      uiMessageCnt--; //每提取一次,消息数量就减一  等于0时表示消息队列里没有消息
   }

   return ucMessageTemp;
}


void send_service(void)  //利用累计主循环次数的计数延时方式来发送一串数据
{
  switch(ucSendStep)  //发送一串数据的运行步骤
  {
    case 0:   //从环形消息队列里提取消息
         if(uiMessageCnt>0)  //说明有消息需要处理
		 {
		    ucMessage=get_message();
            switch(ucMessage)   //消息处理
			{ 
			   case 1:
                    ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1]=0x00;
                    ucSendregBuf[2]=0x55;
                    ucSendregBuf[3]=0x01;    //01代表1号键
                    ucSendregBuf[4]=0x00;
                    ucSendregBuf[5]=0x00;
                    ucSendregBuf[6]=0x00;
                    ucSendregBuf[7]=0x00;
                    ucSendregBuf[8]=0x41;

                    uiSendCnt=0; //发送数据的中间变量清零
                    uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                    ucSendStep=1; //切换到下一步发送一串数据
			        break;
			   case 2:
                    ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1]=0x00;
                    ucSendregBuf[2]=0x55;
                    ucSendregBuf[3]=0x02;    //02代表2号键
                    ucSendregBuf[4]=0x00;
                    ucSendregBuf[5]=0x00;
                    ucSendregBuf[6]=0x00;
                    ucSendregBuf[7]=0x00;
                    ucSendregBuf[8]=0x42;

                    uiSendCnt=0; //发送数据的中间变量清零
                    uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                    ucSendStep=1; //切换到下一步发送一串数据
			        break;
			   case 3:
                    ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1]=0x00;
                    ucSendregBuf[2]=0x55;
                    ucSendregBuf[3]=0x03;    //03代表3号键
                    ucSendregBuf[4]=0x00;
                    ucSendregBuf[5]=0x00;
                    ucSendregBuf[6]=0x00;
                    ucSendregBuf[7]=0x00;
                    ucSendregBuf[8]=0x43;

                    uiSendCnt=0; //发送数据的中间变量清零
                    uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                    ucSendStep=1; //切换到下一步发送一串数据
			        break;
			   case 4:
                    ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1]=0x00;
                    ucSendregBuf[2]=0x55;
                    ucSendregBuf[3]=0x04;    //04代表4号键
                    ucSendregBuf[4]=0x00;
                    ucSendregBuf[5]=0x00;
                    ucSendregBuf[6]=0x00;
                    ucSendregBuf[7]=0x00;
                    ucSendregBuf[8]=0x44;

                    uiSendCnt=0; //发送数据的中间变量清零
                    uiSendTimeCnt=0; //累计主循环次数的计数延时器清零
                    ucSendStep=1; //切换到下一步发送一串数据
			        break;

               default:  //如果没有符合要求的消息,则不处理

			        ucSendStep=0; //维持现状,不切换
			        break;
			}
		 }
	     break;

    case 1:  //利用累加主循环次数的计数延时方式来发送一串数据

 /* 注释二:
  * 这里的计数延时为什么不用累计定时中断次数的延时,而用累计主循环次数的计数延时?
  * 因为本程序定时器中断一次需要500个指令时间,时间分辨率太低,不方便微调时间。因此我
  * 就用累计主循环次数的计数延时方式,在做项目的时候,各位读者应该根据系统的实际情况
  * 来调整const_send_time的大小。
  */  
         uiSendTimeCnt++;  //累计主循环次数的计数延时,为每个字节之间增加延时,
		 if(uiSendTimeCnt>const_send_time)  //请根据实际系统的情况,调整const_send_time的大小
		 {
		    uiSendTimeCnt=0;

			eusart_send(ucSendregBuf[uiSendCnt]);  //发送一串数据给上位机
            uiSendCnt++;
			if(uiSendCnt>=9) //说明数据已经发送完毕
			{
			   uiSendCnt=0;
               ucSendStep=0; //返回到上一步,处理其它未处理的消息
			}
		 }

	     break;  
  }

}


void eusart_send(unsigned char ucSendData)
{

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

 /* 注释三:
  * 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
  * 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
  * 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
  * 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
  * 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
  */  

//  delay_short(400);  //因为外部在每个发送字节之间用了累计主循环次数的计数延时,因此不要此行的delay延时

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

}


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


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

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

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

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


}


void key_service(void) //第三区 按键服务的应用程序
{


  switch(ucKeySec) //按键服务状态切换
  {
    case 1:// 1号键 对应朱兆祺学习板的S1键

          insert_message(0x01);  //把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;  //原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
		  ucVoiceLock=0; //原子锁解锁

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

          insert_message(0x02);  //把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;  //原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
		  ucVoiceLock=0; //原子锁解锁

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

          insert_message(0x03);  //把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;  //原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
		  ucVoiceLock=0; //原子锁解锁

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

          insert_message(0x04);  //把新消息插入到环形消息队列里等待处理

          ucVoiceLock=1;  //原子锁加锁,保护中断与主函数的共享数据
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
		  ucVoiceLock=0; //原子锁解锁

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

  }        
}



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

 /* 注释四:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

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


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


void usart_receive(void) interrupt 4                 //串口中断        
{        

   if(RI==1)  
   {
        RI = 0;   //接收中断,及时把接收中断标志位清零

       
    
   }
   else 
   {
        TI = 0;    //发送中断,及时把发送中断标志位清零
   }
                                                         
}                                



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


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


void initial_myself(void)  //第一区 初始化单片机
{
/* 注释五:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
  key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平

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

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


  //配置串口
  SCON=0x50;
  TMOD=0X21;
  TH1=TL1=-(11059200L/12/32/9600);  //串口波特率9600。
  TR1=1;

}

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

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

}

 

总结陈词:
       前面几个章节中,每个章节要么独立地讲解串口收数据,要么独立地讲解发数据,实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。读者只要把我的串口收发程序结合起来,就很容易实现这样的功能,我就不再详细讲解了。从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。 

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



菜鸟
2014-06-09 11:47:30     打赏
59楼
第四十四节:从机的串口收发综合程序框架

开场白:
根据上一节的预告,本来这一节内容打算讲“利用AT24C02进行掉电后的数据保存”的,但是由于网友“261854681”强烈建议我讲一个完整的串口收发程序实例,因此我决定再花两节篇幅讲讲这方面的内容。
实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。
这节先讲从机的收发端程序实例。要教会大家三个知识点:
第一个:为了保证串口中断接收的数据不丢失,在初始化时必须设置IP = 0x10,相当于把串口中断设置为最高优先级,这个时候,串口中断可以打断任何其他的中断服务函数,实现中断嵌套。
第二个:从机端的收发端程序框架。
第三个:从机的状态指示程序框架。可以指示待机,通讯中,超时出错三种状态。

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

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

(2)实现功能:
显示和独立按键部分根据第29节的程序来改编,用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。有两种更改参数的方式:
第一种:按键更改参数:
    第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
    第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。S13是复位按键,当通讯超时蜂鸣器报警时,可以按下此键清除报警。

第二种:通过串口来更改参数:
     波特率是:9600.
通讯协议:EB 00 55  GG 00 02 XX XX  CY
其中第1,2,3位EB 00 55就是数据头
其中第4位GG就是数据类型。01代表更改参数1,02代表更改参数2,03代表更改参数3,04代表更改参数4,
其中第5,6位00 02就是有效数据长度。高位在左,低位在右。
其中从第7,8位XX XX是被更改的参数。高位在左,低位在右。
第9位CY是累加和,前面所有字节的累加。
一个完整的通讯必须接收完4串数据,每串数据之间的间隔时间不能超过10秒钟,否则认为通讯超时出错引发蜂鸣器报警。如果接收到得数据校验正确,
则返回校验正确应答:eb 00        55 f5 00 00 35,
否则返回校验出错应答::eb 00        55 fa 00 00 3a。
   系统处于待机状态时,LED灯一直亮,
   系统处于非待机状态时,LED灯闪烁,
   系统处于通讯超时出错状态时,LED灯闪烁,并且蜂鸣器间歇鸣叫报警。

通过电脑的串口助手,依次发送以下测试数据,将会分别更改参数1,参数2,参数3,参数4。注意,每串数据之间的时间最大不能超过10秒,否则系统认为通讯超时报警。
把参数1更改为十进制的1:   eb 00 55 01 00 02 00 01 44
把参数2更改为十进制的12:  eb 00 55 02 00 02 00 0c 50
把参数3更改为十进制的123: eb 00 55 03 00 02 00 7b c0 
把参数4更改为十进制的1234:eb 00 55 04 00 02 04 d2 1c

(3)源代码讲解如下:

#include "REG52.H"

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

#define const_led_0_5s  200   //大概0.5秒的时间
#define const_led_1s    400   //大概1秒的时间

#define const_send_time_out   4000  //通讯超时出错的时间 大概10秒

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

#define const_send_size  10  //串口发送数据的缓冲区数组大小

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

void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);  //串口服务程序,在main函数里
void eusart_send(unsigned char ucSendData); //发送一个字节,内部自带每个字节之间的delay延时

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

void status_service(void);  //状态显示的应用程序


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;  //作为状态指示灯 亮的时候表示待机状态.闪烁表示非待机状态,处于正在发送数据或者出错的状态

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

unsigned char ucSendregBuf[const_send_size]; //发送的缓冲区数组

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

unsigned char  ucSendCntLock=0; //串口计时器的原子锁
unsigned char ucRcType=0;  //数据类型
unsigned int  uiRcSize=0;  //数据长度
unsigned char ucRcCy=0;  //校验累加和

unsigned int  uiLedCnt=0;  //控制Led闪烁的延时计时器
unsigned int  uiSendTimeOutCnt=0; //用来识别接收数据超时的计时器
unsigned char ucSendTimeOutLock=0; //原子锁


unsigned char ucStatus=0; //当前状态变量 0代表待机 1代表正在通讯过程 2代表发送出错

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  uiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=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;  //中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
0x3f,  //0       序号0
0x06,  //1       序号1
0x5b,  //2       序号2
0x4f,  //3       序号3
0x66,  //4       序号4
0x6d,  //5       序号5
0x7d,  //6       序号6
0x07,  //7       序号7
0x7f,  //8       序号8
0x6f,  //9       序号9
0x00,  //无      序号10
0x40,  //-       序号11
0x73,  //P       序号12
};
void main() 
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
      key_service(); //按键服务的应用程序
	  usart_service();  //串口服务程序
      display_service(); //显示的窗口菜单服务程序
	  status_service();  //状态显示的应用程序
   }
}

void status_service(void)  //状态显示的应用程序
{
   if(ucStatus!=0) //处于非待机的状态,Led闪烁
   {
      if(uiLedCnt<const_led_0_5s)  //大概0.5秒
	  {
	     led_dr=1;  //前半秒亮

		 if(ucStatus==2)  //处于发送数据出错的状态,则蜂鸣器间歇鸣叫报警
		 {
             ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
             uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
             ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
		 }
	  }
	  else if(uiLedCnt<const_led_1s)  //大概1秒
	  {
	     led_dr=0; //前半秒灭
	  }
	  else
	  {
	     uiLedCnt=0; //延时计时器清零,让Led灯处于闪烁的反复循环中
	  }
   
   }
   else  //处于待机状态,Led一直亮
   {
      led_dr=1;
   
   }



}



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

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

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            //下面的代码进入数据协议解析和数据处理的阶段

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

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {

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

                   ucRcType=ucRcregBuf[uiRcMoveIndex+3];   //数据类型  一个字节
                   uiRcSize=ucRcregBuf[uiRcMoveIndex+4];   //数据长度  两个字节
                   uiRcSize=uiRcSize<<8;
                   uiRcSize=uiRcSize+ucRcregBuf[uiRcMoveIndex+5];
                                                                 
                   ucRcCy=ucRcregBuf[uiRcMoveIndex+6+uiRcSize];   //记录最后一个字节的校验 
                   ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=0;  //清零最后一个字节的累加和变量

                   for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                   {
                      ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=ucRcregBuf[uiRcMoveIndex+6+uiRcSize]+ucRcregBuf[i];
                   }        


                   if(ucRcCy==ucRcregBuf[uiRcMoveIndex+6+uiRcSize])  //如果校验正确,则进入以下数据处理
                   {                                                  
                       switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                       {
                             case 0x01:   //设置参数1

							      ucStatus=1; //从设置参数1开始,表示当前处于正在发送数据的状态

                                  uiSetData1=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiSetData1=uiSetData1<<8;  
                                  uiSetData1=uiSetData1+ucRcregBuf[uiRcMoveIndex+7];
                                  ucWd1Update=1; //窗口1更新显示
                                  break;        
                                                                        
                             case 0x02:   //设置参数2

                                  uiSetData2=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiSetData2=uiSetData2<<8;  
                                  uiSetData2=uiSetData2+ucRcregBuf[uiRcMoveIndex+7];
                                  ucWd2Update=1; //窗口2更新显示
                                  break;   

                             case 0x03:   //设置参数3

                                  uiSetData3=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiSetData3=uiSetData3<<8;  
                                  uiSetData3=uiSetData3+ucRcregBuf[uiRcMoveIndex+7];
                                  ucWd3Update=1; //窗口3更新显示
                                  break;  

                             case 0x04:   //设置参数4

							      ucStatus=0; //从设置参数4结束发送数据的状态,表示发送数据的过程成功,切换回待机状态

                                  uiSetData4=ucRcregBuf[uiRcMoveIndex+6];  //把两个字节合并成一个int类型的数据
                                  uiSetData4=uiSetData4<<8;  
                                  uiSetData4=uiSetData4+ucRcregBuf[uiRcMoveIndex+7];
                                  ucWd4Update=1; //窗口4更新显示
                                  break;  

                                                                         
                        }


						ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0xf5;  //代表校验正确
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x00;
                        ucSendregBuf[6]=0x35;

                        for(i=0;i<7;i++)  //返回校验正确的应答指令
                        {
                           eusart_send(ucSendregBuf[i]);  //发送一串数据给上位机
                        }

                     }   
     				 else
					 {
                        ucSendTimeOutLock=1; //原子锁加锁
					    uiSendTimeOutCnt=0;  //超时计时器计时清零
                        ucSendTimeOutLock=0; //原子锁解锁

						ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0xfa;   //代表校验错误
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x00;
                        ucSendregBuf[6]=0x3a;   

                        for(i=0;i<7;i++)  //返回校验错误的应答指令
                        {
                           eusart_send(ucSendregBuf[i]);  //发送一串数据给上位机
                        }					 
					 
					 }

					 ucSendTimeOutLock=1; //原子锁加锁
					 uiSendTimeOutCnt=0;  //超时计时器计时清零
                     ucSendTimeOutLock=0; //原子锁解锁

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


void eusart_send(unsigned char ucSendData) //发送一个字节,内部自带每个字节之间的delay延时
{

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

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

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

}


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

   switch(ucWd)  //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)  //窗口1要全部更新显示
   {
               ucWd1Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;  //第5位数码管显示无

              //先分解数据
                       ucTemp4=uiSetData1/1000;     
                       ucTemp3=uiSetData1%1000/100;
                       ucTemp2=uiSetData1%100/10;
                       ucTemp1=uiSetData1%10;
  
                          //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
            }
            break;
        case 2:  //显示P--2窗口的数据
            if(ucWd2Update==1)  //窗口2要全部更新显示
   {
               ucWd2Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=2;  //第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData2/1000;     //分解数据
                       ucTemp3=uiSetData2%1000/100;
                       ucTemp2=uiSetData2%100/10;
                       ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
    }
             break;
        case 3:  //显示P--3窗口的数据
            if(ucWd3Update==1)  //窗口3要全部更新显示
   {
               ucWd3Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=3;  //第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData3/1000;     //分解数据
                       ucTemp3=uiSetData3%1000/100;
                       ucTemp2=uiSetData3%100/10;
                       ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
   }
            break;
        case 4:  //显示P--4窗口的数据
            if(ucWd4Update==1)  //窗口4要全部更新显示
   {
               ucWd4Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=4;  //第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData4/1000;     //分解数据
                       ucTemp3=uiSetData4%1000/100;
                       ucTemp2=uiSetData4%100/10;
                       ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
    }
             break;
           }
   

}

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

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

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

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

void key_service(void) //按键服务的应用程序
{

  switch(ucKeySec) //按键服务状态切换
  {
    case 1:// 加按键 对应朱兆祺学习板的S1键 
          switch(ucWd)  //在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发
          break;    
    
    case 2:// 减按键 对应朱兆祺学习板的S5键 
          switch(ucWd)  //在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)  
                                  {
                                     uiSetData1=0;  //最小值是0
                                  }
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;  //最小值是0
                                  }
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999) 
                                  {
                                     uiSetData3=0;  //最小值是0
                                  }
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999) 
                                  {
                                     uiSetData4=0;  //最小值是0
                                  }
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;  //切换窗口
                  if(ucWd>4)
                  {
                    ucWd=1;
                  }
          switch(ucWd)  //在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 4:// 复位按键 对应朱兆祺学习板的S13键
          switch(ucStatus)  //在不同的状态下,进行不同的操作
          {
             case 0:  //处于待机状态
                  break;

             case 1:  //处于正在通讯的过程
                  break;

             case 2: //发送数据出错,比如中间超时没有接收到数据
                  ucStatus=0; //切换回待机的状态
                  break;
          }
          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

          break;   
         
  }                
}

void display_drive(void)  
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   { 
      case 1:  //显示第1位
           ucDigShowTemp=dig_table[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 usart_receive(void) interrupt 4   //串口接收数据中断        
{        

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

         ++uiRcregTotal;
        if(uiRcregTotal>const_rc_size)  //超过缓冲区
        {
           uiRcregTotal=const_rc_size;
        }
        ucRcregBuf[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

        if(ucSendCntLock==0)  //原子锁判断
        {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;  //及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
            ucSendCntLock=0; //解锁
        }
    
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;  //如果不是串口接收中断,那么必然是串口发送中断,及时清除发送中断的标志,否则一直发送中断
   }
                                                         
}  

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


 /* 注释一:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  
  if(ucSendCntLock==0)  //原子锁判断
  {
     ucSendCntLock=1; //加锁
     if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
     {
        uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
        ucSendLock=1;     //开自锁标志
     }
     ucSendCntLock=0; //解锁
  }

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

  if(ucStatus!=0) //处于非待机的状态,Led闪烁
  {
     uiLedCnt++; //Led闪烁计时器不断累加
  }

  if(ucStatus==1) //处于正在通讯的状态,
  {
     if(ucSendTimeOutLock==0)  //原子锁判断
	 {
        uiSendTimeOutCnt++;   //超时计时器累加
	    if(uiSendTimeOutCnt>const_send_time_out)  //超时出错
	    {
	       uiSendTimeOutCnt=0;
	       ucStatus=2;  //切换到出错报警状态
     	}
	 }
  }



  key_scan(); //按键扫描函数
  display_drive();  //数码管字模的驱动函数

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

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

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

void initial_myself(void)  //第一区 初始化单片机
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1就是本程序中用到的一个独立按键。
*/
  key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
  led_dr=1;  //点亮独立LED灯
  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
  hc595_drive(0x00,0x00);  //关闭所有经过另外两个74HC595驱动的LED灯
  TMOD=0x01;  //设置定时器0为工作方式1
  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;

  //配置串口
  SCON=0x50;
  TMOD=0X21;

/* 注释三:
 * 为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
 * 这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
 */
  IP =0x10;  //把串口中断设置为最高优先级,必须的。

  TH1=TL1=-(11059200L/12/32/9600);  //串口波特率为9600。
  TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{

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

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










 

总结陈词:
   这节详细讲了从机收发端的程序框架,而主机端的程序则用电脑的串口助手来模拟。实际上,主机端的程序也有很多内容,它包括依次发送每一串数据,根据返回的应答来决定是否需要重发数据,重发三次如果没反应则进行报错,以及超时没接收到数据等等内容。主机收发端的程序框架是什么样的?欲知详情,请听下回分解-----主机的串口收发综合程序框架
  
(未完待续,下节更精彩,不要走开哦)


菜鸟
2014-06-09 11:50:24     打赏
60楼
第四十五节:主机的串口收发综合程序框架

开场白:
在大部分的项目中,串口都需要“一收一应答”的握手协议,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。
     上一节已经讲了从机,这节就讲主机的收发端程序实例。要教会大家四个知识点:

第一个:为了保证串口中断接收的数据不丢失,在初始化时必须设置IP = 0x10,相当于把串口中断设置为最高优先级,这个时候,串口中断可以打断任何其他的中断服务函数,实现中断嵌套。
第二个:主机端的收发端程序框架。包括重发,超时检测等等。
第三个:主机的状态指示程序框架。可以指示待机,通讯中,超时出错三种状态。
第四个:其实上一节的LED灯闪烁的时间里,我忘了加原子锁,不加原子锁的后果是,闪烁的时间有时候会不一致,所以这节多增加一个原子锁变量ucLedLock,再次感谢“红金龙吸味”关于原子锁的建议,真的很好用。

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

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

(2)实现功能:
显示和独立按键部分根据第29节的程序来改编,用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
      一共有4个窗口。每个窗口显示一个参数。串口可以把当前设置的4个数据发送给从机。从机端可以用电脑的串口助手来模拟。
第一:按键更改参数:
    第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
    第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。S13是启动发送数据和复位按键,当系统处于待机状态时,按下此按键会启动发送数据;当通讯超时蜂鸣器报警时,可以按下此键清除报警,返回到待机的状态。

第二:通过串口把更改的参数发送给从机。
波特率是:9600.
通讯协议:EB 00 55  GG 00 02 XX XX  CY
其中第1,2,3位EB 00 55就是数据头
其中第4位GG就是数据类型。01代表更改参数1,02代表更改参数2,03代表更改参数3,04代表更改参数4,
其中第5,6位00 02就是有效数据长度。高位在左,低位在右。
其中从第7,8位XX XX是被更改的参数。高位在左,低位在右。
第9位CY是累加和,前面所有字节的累加。
一个完整的通讯必须发送完4串数据,每串数据之间的间隔时间不能超过10秒钟,否则认为通讯超时主机会重发数据,如果连续三次都没有返回,则引发蜂鸣器报警。如果接收到得数据校验正确,主机继续发送新的一串数据,直到把4串数据发送完毕为止。

   系统处于待机状态时,LED灯一直亮,
   系统处于非待机状态时,LED灯闪烁,
   系统处于出错状态时,LED灯闪烁,并且蜂鸣器间歇鸣叫报警。

通过电脑的串口助手来模拟从机,返回不同的应答
从机返回校验正确应答:eb 00 55 f5 00 00 35
从机返回校验出错应答:eb 00 55 fa 00 00 3a

(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_key_time4  20    //按键去抖动延时的时间

#define const_led_0_5s  200   //大概0.5秒的时间
#define const_led_1s    400   //大概1秒的时间

#define const_send_time_out   4000  //通讯超时出错的时间 大概10秒

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

#define const_send_size  10  //串口发送数据的缓冲区数组大小

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

void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);  //串口接收服务程序,在main函数里
void communication_service(void); //一发一收的通讯服务程序
void eusart_send(unsigned char ucSendData); //发送一个字节,内部自带每个字节之间的delay延时

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

void status_service(void);  //状态显示的应用程序


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键
sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
sbit led_dr=P3^5;  //作为状态指示灯 亮的时候表示待机状态.闪烁表示非待机状态,处于正在发送数据或者出错的状态

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

unsigned char ucSendregBuf[const_send_size]; //发送的缓冲区数组

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

unsigned char  ucSendCntLock=0; //串口计时器的原子锁
unsigned char ucRcType=0;  //数据类型
unsigned int  uiRcSize=0;  //数据长度
unsigned char ucRcCy=0;  //校验累加和

unsigned char ucLedLock=0; //原子锁
unsigned int  uiLedCnt=0;  //控制Led闪烁的延时计时器
unsigned int  uiSendTimeOutCnt=0; //用来识别接收数据超时的计时器
unsigned char ucSendTimeOutLock=0; //原子锁


unsigned char ucStatus=0; //当前状态变量 0代表待机 1代表正在通讯过程 2代表发送出错
unsigned char ucSendStep=0; //发送数据的过程步骤
unsigned char ucErrorCnt=0; //累计错误总数
unsigned char ucSendTotal=0; //记录当前已经发送了多少串数据
unsigned char ucReceiveStatus=0; //返回的数据状态 0代表待机 1代表校验正确 2代表校验出错

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

unsigned 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  uiKeyTimeCnt4=0; //按键去抖动延时计数器
unsigned char ucKeyLock4=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;  //中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[]=
{
0x3f,  //0       序号0
0x06,  //1       序号1
0x5b,  //2       序号2
0x4f,  //3       序号3
0x66,  //4       序号4
0x6d,  //5       序号5
0x7d,  //6       序号6
0x07,  //7       序号7
0x7f,  //8       序号8
0x6f,  //9       序号9
0x00,  //无      序号10
0x40,  //-       序号11
0x73,  //P       序号12
};
void main() 
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral(); 
   while(1)  
   { 
      key_service(); //按键服务的应用程序
      usart_service();  //串口接收服务程序
      communication_service(); //一发一收的通讯服务程序
      display_service(); //显示的窗口菜单服务程序
      status_service();  //状态显示的应用程序
   }
}


void communication_service(void) //一发一收的通讯服务程序
{
   unsigned int i;

   if(ucStatus==1)  //处于正在通讯的过程中
   {
       switch(ucSendStep)
	   {
	       case 0: //通讯过程0  发送一串数据
                switch(ucSendTotal)  //根据当前已经发送到第几条数据来决定发送哪些参数
				{
				   case 0:   //发送参数1
                        ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0x01;    //代表发送参数1
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x02;    //代表发送2个字节的有效数据

						ucSendregBuf[6]=uiSetData1>>8;  //把int类型的参数分解成两个字节的数据
						ucSendregBuf[7]=uiSetData1;
				        break;

				   case 1:  //发送参数2
                        ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0x02;    //代表发送参数2
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x02;    //代表发送2个字节的有效数据

						ucSendregBuf[6]=uiSetData2>>8;  //把int类型的参数分解成两个字节的数据
						ucSendregBuf[7]=uiSetData2;
				        break; 

				   case 2:  //发送参数3
                        ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0x03;    //代表发送参数3
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x02;    //代表发送2个字节的有效数据

						ucSendregBuf[6]=uiSetData3>>8;  //把int类型的参数分解成两个字节的数据
						ucSendregBuf[7]=uiSetData3;
				        break;

				   case 3:  //发送参数4
                        ucSendregBuf[0]=0xeb;    //把准备发送的数据放入发送缓冲区
                        ucSendregBuf[1]=0x00;
                        ucSendregBuf[2]=0x55;
                        ucSendregBuf[3]=0x04;    //代表发送参数4
                        ucSendregBuf[4]=0x00;
                        ucSendregBuf[5]=0x02;    //代表发送2个字节的有效数据

						ucSendregBuf[6]=uiSetData4>>8;  //把int类型的参数分解成两个字节的数据
						ucSendregBuf[7]=uiSetData4;
				        break;
				}
				

                ucSendregBuf[8]=0x00;  
                for(i=0;i<8;i++)  //最后一个字节是校验和,是前面所有字节累加,溢出部分不用我们管,系统会有规律的自动处理
                {
                  ucSendregBuf[8]=ucSendregBuf[8]+ucSendregBuf[i];
                }

                for(i=0;i<9;i++)  
                {
                    eusart_send(ucSendregBuf[i]);  //把一串完整的数据发送给下位机
                }

                ucSendTimeOutLock=1; //原子锁加锁
                uiSendTimeOutCnt=0;  //超时计时器计时清零
                ucSendTimeOutLock=0; //原子锁解锁

				ucReceiveStatus=0;  //返回的数据状态清零
				ucSendStep=1;  //切换到下一个步骤,等待返回的数据
		        break;
	       case 1: //通讯过程1  判断返回的指令
		        if(ucReceiveStatus==1)  //校验正确
				{

			  	   ucErrorCnt=0; //累计校验错误总数清零

				   ucSendTotal++;  //累加当前发送了多少串数据

				   if(ucSendTotal>=4) //已经发送完全部4串数据,结束
				   {
                      ucStatus=0;  //切换到结束时的待机状态
				   }
				   else  //还没发送完4串数据,则继续发送下一串新数据
				   {
				   	  ucSendStep=0;  //返回上一个步骤,继续发送新数据
				   }

				}
		        else if(ucReceiveStatus==2||uiSendTimeOutCnt>const_send_time_out)  //校验出错或者超时出错
				{

		  	       ucErrorCnt++; //累计错误总数
                   if(ucErrorCnt>=3)  //累加重发次数3次以上,则报错
				   {
                      ucStatus=2;  //切换到出错报警状态
				   }
				   else  //重发还没超过3次,继续返回重发
				   {
				   	  ucSendStep=0;  //返回上一个步骤,重发一次数据
				   }
				}
		        break;	   
	   
	   }
   
   }

}

void status_service(void)  //状态显示的应用程序
{
   if(ucStatus!=0) //处于非待机的状态,Led闪烁
   {
      if(uiLedCnt<const_led_0_5s)  //大概0.5秒
          {
             led_dr=1;  //前半秒亮

                 if(ucStatus==2)  //处于发送数据出错的状态,则蜂鸣器间歇鸣叫报警
                 {
             ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
             uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
             ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
                 }
          }
          else if(uiLedCnt<const_led_1s)  //大概1秒
          {
             led_dr=0; //前半秒灭
          }
          else
          {
		     ucLedLock=1; //原子锁加锁
             uiLedCnt=0; //延时计时器清零,让Led灯处于闪烁的反复循环中
			 ucLedLock=0; //原子锁解锁
          }
   
   }
   else  //处于待机状态,Led一直亮
   {
      led_dr=1;
   
   }



}



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

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

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            //下面的代码进入数据协议解析和数据处理的阶段

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

            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5)) 
            {

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

                   ucRcType=ucRcregBuf[uiRcMoveIndex+3];   //数据类型  一个字节
                   uiRcSize=ucRcregBuf[uiRcMoveIndex+4];   //数据长度  两个字节
                   uiRcSize=uiRcSize<<8;
                   uiRcSize=uiRcSize+ucRcregBuf[uiRcMoveIndex+5];
                                                                 
                   ucRcCy=ucRcregBuf[uiRcMoveIndex+6+uiRcSize];   //记录最后一个字节的校验 
                   ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=0;  //清零最后一个字节的累加和变量

                   for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
                   {
                      ucRcregBuf[uiRcMoveIndex+6+uiRcSize]=ucRcregBuf[uiRcMoveIndex+6+uiRcSize]+ucRcregBuf[i];
                   }        


                    if(ucRcCy==ucRcregBuf[uiRcMoveIndex+6+uiRcSize])  //如果一串数据校验正确,则进入以下数据指令的判断
                    {                                                  
                       switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                       {
                             case 0xf5:   //返回的是正确的校验指令

                                  ucReceiveStatus=1;//代表校验正确
                                  break;        
                                                                        
                             case 0xfa:   //返回的是错误的校验指令

                                  ucReceiveStatus=2;//代表校验错误
                                  break;                                          
                        }

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


void eusart_send(unsigned char ucSendData) //发送一个字节,内部自带每个字节之间的delay延时
{

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

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

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

}


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

   switch(ucWd)  //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
   {
       case 1:   //显示P--1窗口的数据
            if(ucWd1Update==1)  //窗口1要全部更新显示
   {
               ucWd1Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=1;   //第6位数码管显示1
               ucDigShow5=10;  //第5位数码管显示无

              //先分解数据
                       ucTemp4=uiSetData1/1000;     
                       ucTemp3=uiSetData1%1000/100;
                       ucTemp2=uiSetData1%100/10;
                       ucTemp1=uiSetData1%10;
  
                          //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

               if(uiSetData1<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData1<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData1<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
            }
            break;
        case 2:  //显示P--2窗口的数据
            if(ucWd2Update==1)  //窗口2要全部更新显示
   {
               ucWd2Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=2;  //第6位数码管显示2
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData2/1000;     //分解数据
                       ucTemp3=uiSetData2%1000/100;
                       ucTemp2=uiSetData2%100/10;
                       ucTemp1=uiSetData2%10;

               if(uiSetData2<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData2<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData2<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
    }
             break;
        case 3:  //显示P--3窗口的数据
            if(ucWd3Update==1)  //窗口3要全部更新显示
   {
               ucWd3Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=3;  //第6位数码管显示3
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData3/1000;     //分解数据
                       ucTemp3=uiSetData3%1000/100;
                       ucTemp2=uiSetData3%100/10;
                       ucTemp1=uiSetData3%10;
               if(uiSetData3<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData3<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData3<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
   }
            break;
        case 4:  //显示P--4窗口的数据
            if(ucWd4Update==1)  //窗口4要全部更新显示
   {
               ucWd4Update=0;  //及时清零标志,避免一直进来扫描
               ucDigShow8=12;  //第8位数码管显示P
               ucDigShow7=11;  //第7位数码管显示-
               ucDigShow6=4;  //第6位数码管显示4
               ucDigShow5=10;   //第5位数码管显示无
                       ucTemp4=uiSetData4/1000;     //分解数据
                       ucTemp3=uiSetData4%1000/100;
                       ucTemp2=uiSetData4%100/10;
                       ucTemp1=uiSetData4%10;

               if(uiSetData4<1000)   
                           {
                              ucDigShow4=10;  //如果小于1000,千位显示无
                           }
               else
                           {
                  ucDigShow4=ucTemp4;  //第4位数码管要显示的内容
                           }
               if(uiSetData4<100)
                           {
                  ucDigShow3=10;  //如果小于100,百位显示无
                           }
                           else
                           {
                  ucDigShow3=ucTemp3;  //第3位数码管要显示的内容
                           }
               if(uiSetData4<10)
                           {
                  ucDigShow2=10;  //如果小于10,十位显示无
                           }
                           else
                           {
                  ucDigShow2=ucTemp2;  //第2位数码管要显示的内容
               }
               ucDigShow1=ucTemp1;  //第1位数码管要显示的内容
    }
             break;
           }
   

}

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

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

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

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

void key_service(void) //按键服务的应用程序
{

  switch(ucKeySec) //按键服务状态切换
  {
    case 1:// 加按键 对应朱兆祺学习板的S1键 
          switch(ucWd)  //在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1++;   
                                  if(uiSetData1>9999) //最大值是9999
                                  {
                                     uiSetData1=9999;
                                  }
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                  uiSetData2++;
                                  if(uiSetData2>9999) //最大值是9999
                                  {
                                     uiSetData2=9999;
                                  }
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                  uiSetData3++;
                                  if(uiSetData3>9999) //最大值是9999
                                  {
                                     uiSetData3=9999;
                                  }
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                  uiSetData4++;
                                  if(uiSetData4>9999) //最大值是9999
                                  {
                                     uiSetData4=9999;
                                  }
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

          ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发
          break;    
    
    case 2:// 减按键 对应朱兆祺学习板的S5键 
          switch(ucWd)  //在不同的窗口下,设置不同的参数
                  {
                     case 1:
                  uiSetData1--;   

                                  if(uiSetData1>9999)  
                                  {
                                     uiSetData1=0;  //最小值是0
                                  }
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                  uiSetData2--;
                                  if(uiSetData2>9999)
                                  {
                                     uiSetData2=0;  //最小值是0
                                  }
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                  uiSetData3--;
                                  if(uiSetData3>9999) 
                                  {
                                     uiSetData3=0;  //最小值是0
                                  }
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                  uiSetData4--;
                                  if(uiSetData4>9999) 
                                  {
                                     uiSetData4=0;  //最小值是0
                                  }
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }

          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
          ucWd++;  //切换窗口
                  if(ucWd>4)
                  {
                    ucWd=1;
                  }
          switch(ucWd)  //在不同的窗口下,在不同的窗口下,更新显示不同的窗口
                  {
                     case 1:
                           ucWd1Update=1;  //窗口1更新显示
                              break;
                     case 2:
                           ucWd2Update=1;  //窗口2更新显示
                              break;
                     case 3:
                           ucWd3Update=1;  //窗口3更新显示
                              break;
                     case 4:
                           ucWd4Update=1;  //窗口4更新显示
                              break;
                  }
          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 4:// 启动发送数据和复位按键 对应朱兆祺学习板的S13键
          switch(ucStatus)  //在不同的状态下,进行不同的操作
          {
             case 0:  //处于待机状态,则启动发送数据


                  ucErrorCnt=0; //累计错误总数清零
                  ucSendTotal=0; //已经发送串数据总数清零

				  ucSendStep=0; //发送数据的过程步骤清零,返回开始的步骤待命
                  ucStatus=1; //启动发送数据,1代表正在通讯过程
                  break;

             case 1:  //处于正在通讯的过程
                  break;

             case 2: //发送数据出错,比如中间超时没有接收到数据
                  ucStatus=0; //切换回待机的状态
                  break;
          }
          ucVoiceLock=1;  //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucVoiceLock=0;  //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

          break;   
         
  }                
}

void display_drive(void)  
{
   //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
   switch(ucDisplayDriveStep)
   { 
      case 1:  //显示第1位
           ucDigShowTemp=dig_table[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 usart_receive(void) interrupt 4   //串口接收数据中断        
{        

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

         ++uiRcregTotal;
        if(uiRcregTotal>const_rc_size)  //超过缓冲区
        {
           uiRcregTotal=const_rc_size;
        }
        ucRcregBuf[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里

        if(ucSendCntLock==0)  //原子锁判断
        {
            ucSendCntLock=1; //加锁
            uiSendCnt=0;  //及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
            ucSendCntLock=0; //解锁
        }
    
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;  //如果不是串口接收中断,那么必然是串口发送中断,及时清除发送中断的标志,否则一直发送中断
   }
                                                         
}  

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


 /* 注释一:
  * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
  */  
  if(ucSendCntLock==0)  //原子锁判断
  {
     ucSendCntLock=1; //加锁
     if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
     {
        uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
        ucSendLock=1;     //开自锁标志
     }
     ucSendCntLock=0; //解锁
  }

  if(ucVoiceLock==0) //原子锁判断
  {
     if(uiVoiceCnt!=0)
     {

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

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

  if(ucStatus!=0) //处于非待机的状态,Led闪烁
  {
     if(ucLedLock==0)//原子锁判断
	 {
        uiLedCnt++; //Led闪烁计时器不断累加
	 }
  }

  if(ucStatus==1) //处于正在通讯的状态,
  {
     if(ucSendTimeOutLock==0)  //原子锁判断
     {
         uiSendTimeOutCnt++;   //超时计时器累加
     }
  }



  key_scan(); //按键扫描函数
  display_drive();  //数码管字模的驱动函数

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

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

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

void initial_myself(void)  //第一区 初始化单片机
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1就是本程序中用到的一个独立按键。
*/
  key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
  led_dr=1;  //点亮独立LED灯
  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
  hc595_drive(0x00,0x00);  //关闭所有经过另外两个74HC595驱动的LED灯
  TMOD=0x01;  //设置定时器0为工作方式1
  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;

  //配置串口
  SCON=0x50;
  TMOD=0X21;

/* 注释三:
 * 为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
 * 这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
 */
  IP =0x10;  //把串口中断设置为最高优先级,必须的。

  TH1=TL1=-(11059200L/12/32/9600);  //串口波特率为9600。
  TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{

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

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

 

总结陈词:
前面花了大量篇幅详细地讲解了串口收发数据的程序框架,从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。
  
(未完待续,下节更精彩,不要走开哦)



共146条 6/15 |‹ 4 5 6 7 8 9 ›| 跳转至

回复

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