打印
[51单片机]

从业将近十年!手把手教你单片机程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
181
jianhong_wu|  楼主 | 2014-4-5 23:06 | 只看该作者 回帖奖励 |倒序浏览
cjseng 发表于 2014-4-5 21:53
鸿哥,你知道你单片机不能做精确定时的原因了吗?
你每次用到定时中断,都是先把定时器停掉,然后执行一端 ...

其实你说的这些问题之前也有很多人跟我质疑过。
(1)如果不是做实时时钟,我这样的时间精度可以满足绝大多数的项目要求的。如果要求再高一点的,就不要把按键扫描放在定时中断里,也不要选用动态数码管扫描的方案。
(2)如果是做实时时钟等精度非常高的,我还是觉得不适合用单片机来做。应该直接用时钟芯片或者cpld/FPGA来做。
(3)至于我每次进入定时中断之后都关闭中断,退出的时候才打开中断,这个是我多年的使用习惯,我觉得不会影响我对时间的要求。当然,在这一点上也许不关闭定时中断才是最佳的方法,但是我个人还是会**先关闭中断,退出的时候再打开,因为我以前印象中吃过这样的亏,但是也不敢完全确定是不是由于这个原因引起的。反正关了定时中断再处理也不会对我系统造成什么影响。

使用特权

评论回复
182
jianhong_wu|  楼主 | 2014-4-5 23:10 | 只看该作者
mwxpk 发表于 2014-4-5 15:29
哄不会的啊,全是没用教程

是的,正如你所说“哄不会的”,我分享的经验主要是针对初学者,对于像您这样的比较有经验的工程师来说,确实是没用的教程。你批评的有道理,我接受。

使用特权

评论回复
183
cjseng| | 2014-4-6 00:47 | 只看该作者
本帖最后由 cjseng 于 2014-4-6 00:50 编辑
jianhong_wu 发表于 2014-4-5 23:06
其实你说的这些问题之前也有很多人跟我质疑过。
(1)如果不是做实时时钟,我这样的时间精度可以满足绝大 ...


你这样做对系统的影响就是定时周期不确定。你的周期=定时器周期+中断程序运行时间,而中断程序运行时间是不确定的,会因为各种条件变化,忽长忽短。

你要关闭中断,到退出前再打开,否则会吃亏,这是因为你的定时周期经常小于程序运行时间。

比如你那个给下棋计时的程序,定时是很不精确的,累积起来,误差不是一般的大,可能会达到秒级。这是你的程序造成的,实际上完全可以做得更好,在几十秒内,做到毫秒级的误差一点儿问题也没有。

至于时钟芯片,那个误差并不小,一年的误差在几分钟范围内的,用单片机的定时器做,可以做到比时钟芯片更小的误差(可以用程序修正,比如隔上几天,调快或调慢一秒)。

使用特权

评论回复
评分
参与人数 1威望 +6 收起 理由
dong_abc + 6
184
mrguoch| | 2014-4-6 11:40 | 只看该作者
这个还不错,学习一下

使用特权

评论回复
185
jianhong_wu|  楼主 | 2014-4-6 12:00 | 只看该作者
cjseng 发表于 2014-4-6 00:47
你这样做对系统的影响就是定时周期不确定。你的周期=定时器周期+中断程序运行时间,而中断程序运行时间是 ...

没错。我的周期=定时器周期+中断程序运行时间,而中断程序运行的时间虽然会因为各种条件变化,忽长忽短,但实际上“忽长”的时候它不会长很多,“忽短”的时候也不会短很多,所以我默认它是固定不会变化的,对于大多数的项目是够用的。我的象棋计数器通过修正比例系数的方法矫正时间后,2个小时内精度可以控制在1秒钟以内,所以我认为这样的精度是绰绰有余了。当然,我相信你还能把精度做得更高。

使用特权

评论回复
186
jianhong_wu|  楼主 | 2014-4-6 12:02 | 只看该作者
本帖最后由 jianhong_wu 于 2014-4-7 08:49 编辑

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

开场白:
上一节讲了判断数据尾的程序框架,但是在大部分的项目中,都是通过判断数据头来接收数据的,这一节要教会大家两个知识点:
第一个:如何在已经接收到的一串数据中解析数据头协议并且提取有效数据。
第二个:无论是判断数据头还是判断数据尾,无论是单片机还是上位机,最好在固定协议前多发送一个填充的无效字节0x00,因为硬件原因,第一个字节往往容易丢失。

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

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

(2)实现功能:

波特率是:9600 。
通讯协议:EB 00 55  XX YY  
加无效填充字节后,上位机实际上应该发送:00  EB 00 55  XX YY
其中第1位00是无效填充字节,防止由于硬件原因丢失第一个字节。
其中第2,3,4位EB 00 55就是数据头
           后2位XX YY就是有效数据
任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字后面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据头和有效数据都是正确的。

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


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

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

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



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

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

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


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



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

}


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

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

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



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

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

/* 注释二:
* 判断数据头,进入循环解析数据协议必须满足两个条件:
* 第一:最大接收缓冲数据必须大于一串数据的长度(这里是5。包括2个有效数据,3个数据头)
* 第二:游标uiRcMoveIndex必须小于等于最大接收缓冲数据减去一串数据的长度(这里是5。包括2个有效数据,3个数据头)
*/
            while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {
                              if(ucRcregBuf[uiRcMoveIndex+3]==0x01&&ucRcregBuf[uiRcMoveIndex+4]==0x02)  //有效数据01 02的判断
                                  {
                                      uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据头和有效数据都接收正确
                                  }
                  break;   //退出循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                        
}


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


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

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

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


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


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

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

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


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


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

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

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


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

}

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

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

}

总结陈词:
     这一节讲了常用的判断数据头来接收一串数据的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,有效数据长度,有效数据,数据校验的通讯协议。这样的程序该怎么写?欲知详情,请听下回分解-----常用的自定义串口通讯协议。

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

使用特权

评论回复
187
wuzx-61| | 2014-4-6 15:27 | 只看该作者
顶楼主对初学者的贡献!......

使用特权

评论回复
188
gyh974| | 2014-4-6 16:49 | 只看该作者
好文

使用特权

评论回复
189
风雪天殇| | 2014-4-6 17:42 | 只看该作者
曾经在某个论坛上看过吴老师对单片机和嵌入式系统的理解,很强

使用特权

评论回复
190
zxcscm| | 2014-4-6 22:22 | 只看该作者
收益匪浅啊,希望楼主前辈能继续更新下去!

使用特权

评论回复
191
狼牙0354| | 2014-4-7 08:25 | 只看该作者
其实还是蛮不错的

使用特权

评论回复
192
ahczqmz| | 2014-4-7 08:30 | 只看该作者
:lol

使用特权

评论回复
193
sdwflzg| | 2014-4-7 10:25 | 只看该作者
确实讲得不错!开卷有益!

使用特权

评论回复
194
jianhong_wu|  楼主 | 2014-4-7 13:06 | 只看该作者
本帖最后由 jianhong_wu 于 2014-7-21 00:05 编辑

第四十节:常用的自定义串口通讯协议。

开场白:
上一节讲了判断数据头的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,数据地址,有效数据长度,有效数据,数据校验的通讯协议。这一节要教会大家三个知识点:
第一个:常用自定义串口通讯协议的程序框架。
第二个:累加校验和的校验方法。累加和的意思是前面所有字节的数据相加,超过一个字节的溢出部分会按照固定的规则自动丢弃,不用我们管。比如以下数据:
      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[uiRcMoveIndex+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;    //启动定时中断

}

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

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

使用特权

评论回复
195
sdwflzg| | 2014-4-14 08:26 | 只看该作者
每天学一课,受益匪浅

使用特权

评论回复
196
290813633| | 2014-4-15 18:58 | 只看该作者
zhitao2072 发表于 2014-3-15 11:23
顶起来。。。。。。。

SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS厉害

使用特权

评论回复
197
jianhong_wu|  楼主 | 2014-4-19 11:43 | 只看该作者
第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。

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

(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延时方式发送一串数据。

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

使用特权

评论回复
198
cjseng| | 2014-4-19 14:58 | 只看该作者
唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又有新的数据进来了,要丢数据的啊。

还有,为什么 TI=0; 不这样做的话,岂不是要一直进入串口中断?

使用特权

评论回复
199
jianhong_wu|  楼主 | 2014-4-19 16:14 | 只看该作者
cjseng 发表于 2014-4-19 14:58
唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又 ...

(1)你说的没错,把数据处理放在串口中断里,只适合数据量小的情况下。如果数据量大的话,请参考我第40节的内容。
(2)感谢你的提醒,让我终于想起来为什么要加TI=0了,就是为了及时清除发送数据产生的中断。

使用特权

评论回复
200
cjseng| | 2014-4-19 20:51 | 只看该作者
jianhong_wu 发表于 2014-4-19 16:14
(1)你说的没错,把数据处理放在串口中断里,只适合数据量小的情况下。如果数据量大的话,请参考我第40节 ...

你前边用的那种方法也不一定能处理大数据量的串口通讯,除非你事先知道每两帧数据之间的间隔时间的大小。
要确保通讯可靠,除了硬件,完善的协议很重要。你还不如参考一下MODBUS通讯协议呢。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则