jianhong_wu 发表于 2014-4-5 23:06

cjseng 发表于 2014-4-5 21:53 static/image/common/back.gif
鸿哥,你知道你单片机不能做精确定时的原因了吗?
你每次用到定时中断,都是先把定时器停掉,然后执行一端 ...

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

jianhong_wu 发表于 2014-4-5 23:10

mwxpk 发表于 2014-4-5 15:29 static/image/common/back.gif
哄不会的啊,全是没用教程

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

cjseng 发表于 2014-4-6 00:47

本帖最后由 cjseng 于 2014-4-6 00:50 编辑

jianhong_wu 发表于 2014-4-5 23:06 https://bbs.21ic.com/static/image/common/back.gif
其实你说的这些问题之前也有很多人跟我质疑过。
(1)如果不是做实时时钟,我这样的时间精度可以满足绝大 ...

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

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

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

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

mrguoch 发表于 2014-4-6 11:40

这个还不错,学习一下

jianhong_wu 发表于 2014-4-6 12:00

cjseng 发表于 2014-4-6 00:47 static/image/common/back.gif
你这样做对系统的影响就是定时周期不确定。你的周期=定时器周期+中断程序运行时间,而中断程序运行时间是 ...

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

jianhong_wu 发表于 2014-4-6 12:02

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

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

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

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

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

(2)实现功能:

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

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


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

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

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



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

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

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


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



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

}


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

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

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



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

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

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

   }
                        
}


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


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

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

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


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


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

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

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


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


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

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

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


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

}

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

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

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

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

wuzx-61 发表于 2014-4-6 15:27

顶楼主对初学者的贡献!......

gyh974 发表于 2014-4-6 16:49

好文

风雪天殇 发表于 2014-4-6 17:42

曾经在某个论坛上看过吴老师对单片机和嵌入式系统的理解,很强

zxcscm 发表于 2014-4-6 22:22

收益匪浅啊,希望楼主前辈能继续更新下去!

狼牙0354 发表于 2014-4-7 08:25

其实还是蛮不错的

ahczqmz 发表于 2014-4-7 08:30

:lol

sdwflzg 发表于 2014-4-7 10:25

确实讲得不错!开卷有益!

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 55GG HH HH XX XX …YYYY CY其中第1,2,3位EB 00 55就是数据头其中第4位GG就是数据类型。01代表驱动奉命,02代表驱动Led灯。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中从第7位开始,到最后一个字节Cy之前,XX..YY都是具体的有效数据。在本程序中,当数据类型是01时,有效数据代表蜂鸣器鸣叫的时间长度。当数据类型是02时,有效数据代表Led灯点亮的时间长度。最后一个字节CY是累加和,前面所有字节的累加。发送以下测试数据,将会分别控制蜂鸣器和Led灯的驱动时间长度。蜂鸣器短叫发送:eb 00 55 01 00 02 00 28 6b蜂鸣器长叫发送:eb 00 55 01 00 02 00 fa 3dLed灯短亮发送:eb 00 55 02 00 02 00 28 6cLed灯长亮发送:eb 00 55 02 00 02 00 fa3e
(3)源代码讲解如下:
#include "REG52.H"

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

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

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



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

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

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量
/* 注释二:
* 为串口计时器多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
unsigned charucSendCntLock=0; //串口计时器的原子锁

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

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

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

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

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

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

}

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

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

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



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

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


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

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


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

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

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

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

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

                                       }      

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

   }
                        
}


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

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

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

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

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

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

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


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

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

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

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


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


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

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


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

}

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

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

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

sdwflzg 发表于 2014-4-14 08:26

每天学一课,受益匪浅

290813633 发表于 2014-4-15 18:58

zhitao2072 发表于 2014-3-15 11:23 static/image/common/back.gif
顶起来。。。。。。。

SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS厉害

jianhong_wu 发表于 2014-4-19 11:43

第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。

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

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

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

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

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


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

#define const_voice_short80   //蜂鸣器短叫的持续时间
#define const_led_short80    //LED灯亮的持续时间

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



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

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


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

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



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

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

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

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

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

}

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



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

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

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

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

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

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

}

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


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

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

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

                                                        break;

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



                                                        break;
                                           }

                                   }

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


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


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

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


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

}

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

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

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

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

cjseng 发表于 2014-4-19 14:58

唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又有新的数据进来了,要丢数据的啊。

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

jianhong_wu 发表于 2014-4-19 16:14

cjseng 发表于 2014-4-19 14:58 static/image/common/back.gif
唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又 ...

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

cjseng 发表于 2014-4-19 20:51

jianhong_wu 发表于 2014-4-19 16:14 static/image/common/back.gif
(1)你说的没错,把数据处理放在串口中断里,只适合数据量小的情况下。如果数据量大的话,请参考我第40节 ...

你前边用的那种方法也不一定能处理大数据量的串口通讯,除非你事先知道每两帧数据之间的间隔时间的大小。
要确保通讯可靠,除了硬件,完善的协议很重要。你还不如参考一下MODBUS通讯协议呢。
页: 1 2 3 4 5 6 7 8 9 [10] 11 12 13 14 15 16 17 18 19
查看完整版本: 从业将近十年!手把手教你单片机程序框架(连载)