[51单片机] 从单片机基础到程序框架(连载)

[复制链接]
359728|721
eydj2008 发表于 2018-8-17 08:28 | 显示全部楼层
jianhong_wu 发表于 2018-8-5 12:40
第一百二十六节: “单线”的肢体接触通信。

【126.1   同步通信与异步通信。】

讲得 简单明了 非常不错
 楼主| jianhong_wu 发表于 2018-8-26 13:25 | 显示全部楼层
第一百二十七节: 单片机串口接收数据的机制。

【127.1   单片机串口接收数据的底层时序。】

       上一节“单线的肢体接触通信”其实是为本节打基础的,通信线只用了一根“数据”线,没有用到“时钟”线,属于异步通信方式,还分析时序中的“1个开始位,8个数据位,1个停止位”等细节内容,这些时序其实就是本节单片机串口通信的底层时序,一模一样。继续上一节的内容(很有必要重新温习一次上一节的异步通信原理),继续沿用甲乙双方靠各自“心跳”的节拍来异步通信的例子,本节单片机串口接收数据是代表乙方,我把乙方串口接收数据的过程翻译成C语言,代码如下:

  1. sbit USART_RX=P3^0;  //用来接收串口数据的数据线
  2. unsigned char Gu8ReceiveData=0;  //串口接收到的8位数据
  3. unsigned char i; //连续接收8位数据的循环变量
  4. void main()
  5. {
  6.     Gu8ReceiveData=0;
  7.     while(1)  
  8.     {  
  9.         USART_RX=1;   //51单片机的规则,每次读取数据前都执行一条“置1”指令
  10.     Delay();      //乙的心跳间隔时间,待机时,每一个节拍监控一次数据线的状态
  11.         if(0==USART_RX) //如果监控到甲发送的“开始位0”,从下一个节拍开始连续接收8位数据
  12. {
  13.               for(i=0;i<8;i++) //连续循环接收8个“数据位”
  14. {
  15.                   USART_RX=1;   //51单片机的规则,每次读取数据前都执行一条“置1”指令
  16.                   Delay();      //乙的心跳间隔时间,每个节拍判断读取一位数据
  17.                   if(1==USART_RX)  //判断读取数据线上的状态
  18. {
  19.        Gu8ReceiveData=Gu8ReceiveData | 0x80;  
  20. }
  21. else
  22. {
  23.        Gu8ReceiveData=Gu8ReceiveData & 0x7F;
  24. }
  25. Gu8ReceiveData=Gu8ReceiveData>>1; //右移一位,为即将接收下一位做准备
  26.                   }
  27.               Delay();    //乙的心跳间隔时间,这里额外增加一个节拍,作为“停止位”的开销。
  28. }
  29.     }
  30. }


【127.2   单片机内置的“硬件串口模块”。】

       很显然,上面【127.1】分享的时序代码会占用单片机大量的时间,单片机每接收一个字节的数据都会被束缚一次手脚,耽误了其它大事,怎么办?为了把单片机从底层繁琐的时序中解放出来,单片机内置了很多“硬货”,俗称“硬件资源”,“硬件串口模块”便是其中之一。何谓“硬件”,单片机内置的“硬件”可以看作是另外一个独立运行的“核”,这个“核”可以看作是另外一个CPU,可以独立工作,相当于单片机主人在某个领域的一个专用助手。单片机只需要跟这个“核”通信发指令就可以,具体的执行过程由这个“核”独立去完成,这个“核”完成工作之后再把处理结果反馈给单片机。那么,单片机是如何跟这些内置“硬件资源”通信呢?其实它们的通信接口是“寄存器”,不管是单片机给“硬件资源”发送指令,还是单片机从“硬件资源”里读取所需要的结果数据,都是通过“寄存器”来完成。

【127.3   单片机与硬件串口通信的接口“寄存器”。】

       硬件串口的寄存器主要涉及:串口的方式选择,波特率,允许串口接收数据,中断的优先级,中断的允许,等等。比如,51单片机的串口是兼容很多种方式的,可以同步通信,也可以异步通信,异步通信还可以兼容10位(1开始位、8数据位、1停止),11位(1开始位、8数据位、1校验位、1停止),等等,这些就是多选题,我们要在某个特定的寄存器里面做出选择。波特率,是用来衡量通信的速度,比如波特率是9600,就意味着1秒钟能收发9600个二进制的位数据,也就是1秒钟能产生9600个时钟节拍,波特率越高通信的速度越快,这些也需要我们往相关的寄存器填入相应的数据,来告知“硬件串口”以哪种波特率进行通信。
       那么,对于初学者,寄存器如何配置呢?主要有这些思路:查看芯片手册(datasheet),产看C编译器的手册,查看芯片相关的C语言的头文件(比如51单片机的REG.H),在网上参考别人已经配置好的代码,或者购买相关芯片的学习板时所配套的程序例程。
       本节用到的串口,是10位数据长度的异步通信,波特率9600,相关配置的代码如下:

  1. unsigned char u8_TMOD_Temp=0;

  2. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  3. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  4. TMOD=TMOD&0x0f; /此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  5. TMOD=TMOD|u8_TMOD_Temp;  //往高4位的定时器1填入0x2,低4位的定时器0保持不变。
  6. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  7. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  8. TR1=1;  //开启定时器1

  9. SM0=0;  
  10. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  11. REN=1;  //允许串口接收数据

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

  15. ES=1;         //允许串口中断  
  16. EA=1;         //允许总中断


【127.4   硬件串口的中断函数。】

       硬件串口接收完一个字节的数据之后,会及时产生一个串口中断去通知单片机接收数据。单片机在串口中断函数里直接读取“串口专用缓存寄存器”SBUF的数据,就可以直接获得一个完整的8位宽度的数据,省去了繁琐的驱动时序底层。
       串口的中断函数跟定时器的中断函数很类似,只不过中断号不一样而已,比如我们前面章节用的定时器0的中断号是“1”,而本节串口的中断号是“4”。这些其实是C编译器定的游戏规则,我们只要根据它提供的数据手册遵守它的游戏规则就好了。串口中断函数里还有一个地方要注意,硬件串口“接收完一个字节”的数据后产生一次中断,而硬件串口“发送完一个字节”的数据后也产生一次中断,这两个一“收”一“发”的中断都是共用中断号为“4”的中断函数,因此,我们必须在中断函数里通过判断寄存器的RI和TI的标志位来判断到底是“收”的中断,还是“发”的中断,并且软件上要及时把RI或者TI及时清零,避免不断进入中断的情况。参考代码如下:

  1. unsigned char Gu8ReceiveData=0;  //接收到一个字节的数据

  2. void usart(void) interrupt 4   //串口接发的中断,中断号为4        
  3. {        
  4.    if(1==RI)  //接收完一个字节后引起的中断
  5.    {
  6.         RI = 0; //及时清零,避免一直无缘无故的进入中断。
  7.         Gu8ReceiveData=SBUF;  //直接读取“串口专用缓存寄存器”SBUF的8位数据。
  8.    }
  9.    else  //发送数据引起的中断
  10.    {
  11.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  12.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。

  13.    }                                                      
  14. }  

【127.5   上位机与单片机的串口通信例程。】


       上图127.5.1  灌入式驱动8个LED  

       程序功能如下:
       波特率9600,校验位NONE(无),数据位8,停止位1。在上位机的串口助手里,发送一个十六进制(HEX模式)的01,让单片机的一颗LED“亮”。发送一个十六进制(HEX模式)的00,让单片机的一颗LED“灭”。上位机的串口助手的使用,请参考前面第11节的相关内容,或者自己在网上查找串口助手软件的使用方法,串口助手软件网上很多,我们的使用要求不高,随便选用一家都可以。代码如下:

  1. #include "REG52.H"  

  2. void usart(void);

  3. sbit P0_0=P0^0;  //一颗LED灯

  4. unsigned char Gu8ReceiveData=0;  //接收到一个字节的数据

  5. void main()
  6. {
  7. unsigned char u8_TMOD_Temp=0;

  8. P0_0=1;  //LED灭。1代表LED灭, 0代表亮

  9. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  10. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  11. TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  12. TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
  13. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  14. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  15. TR1=1;  //开启定时器1

  16. SM0=0;  
  17. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  18. REN=1;  //允许串口接收数据

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

  22. ES=1;         //允许串口中断  
  23. EA=1;         //允许总中断
  24.     while(1)  //主循环
  25.     {  
  26.     }
  27. }

  28. void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
  29. {        
  30.    if(1==RI)  //接收完一个字节后引起的中断
  31.    {
  32.         RI = 0; //及时清零,避免一直无缘无故的进入中断。
  33.         Gu8ReceiveData=SBUF;  //直接读取“串口专用缓存寄存器”SBUF的8位数据。
  34.         if(0x01==Gu8ReceiveData)
  35. {
  36. P0_0=0;  //LED亮。1代表LED灭, 0代表亮
  37. }
  38. else
  39. {
  40. P0_0=1;  //LED灭。1代表LED灭, 0代表亮
  41. }
  42.    }
  43.    else  //发送数据引起的中断
  44.    {
  45.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  46.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  47.    }                                                      
  48. }  


【127.6   单片机串口电路的简易分析。】




       上图127.6.1  232串口电路

        单片机串口对外的引脚是与IO口的“P3.1、P3.0”共用的。P3.1是串口的TX引脚,即对外发送数据的引脚。P3.0是串口的RX引脚,即接收外部数据的引脚。一旦项目中用了串口,那么这两个引脚就必须“专脚专用”,只给串口单独使用,不再做IO口。平时通信的时候,跟其它单片机或者系统进行串口通信,除了接TX和RX这两根数据线之外,必须一定把双方的负极GND也“共地”接上,否则双方建立不了同样的电压参考点,通信毕然失败。因此,串口通信最低标配是3根线:RX,TX,GND。
        如果两个甲乙单片机都布在一块板子上,距离不超过半米,他们两个要进行串口通信,怎么接线?把他们的GND连起来,然后RX与TX“交叉”对接,甲的RX接到乙的TX,甲的TX接到乙的RX。这种在短距离通信的时候,不用增加任何外部辅助压差信号放大芯片,这种方式叫做“串口的TTL”接线方式。
        如果两个系统串口通信的距离比较远,比如在不同的板子上,1米以上10米以下的距离,这时就不能采用原始的“串口的TTL”接线方式,因为线缆越长电阻越大,本身就要消耗一些压降,而3.3V的压降很容易就会被消耗完,通信的可靠度和抗扰能力就会降低。为了解决这个问题,可以引用232标准的接线方式,外部需接一个压差放大的芯片,把从原来3.3V的压差放大到一两倍左右,通信的距离就大大提高。具体232的细节,大家可以网上搜搜“RS232”。注意,采用232协议通信,也要注意“共地”和数据线“交叉”的两个问题,232通信的最低标配也是3根线:R,T,GND。上图SP232E就是一个压差信号放大的通信专用芯片。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
ztzp 发表于 2018-8-27 10:11 | 显示全部楼层
天天都在看是否更新了,今天终于等到了。
谢谢楼主无私的奉献!
arima 发表于 2018-9-2 20:30 | 显示全部楼层
一周一更新,贵在**!!!!
 楼主| jianhong_wu 发表于 2018-9-9 17:13 | 显示全部楼层
第一百二十八节: 接收“固定协议”的串口程序框架。

【128.1   固定协议。】

       实际项目中,串口一个回合的收发数据量远远不止1个字节,而是往往携带了某种“固定协议”的一串数据(专业术语称“一帧数据”)。一串数据的“固定协议”因为起到类似“校验”和“密码确认”的功能,因此在安全可靠性方面大大增强。但是上一节也提到,单片机利用最底层硬件的串口接口,一次收发的最小单位是“1个字节”,那么,怎么样在此基础上搭建一个能快速收发并且能快速解析数据的程序框架就显得尤为重要。本节我跟大家分享我常用的串口程序框架,此框架主要包含“数据头,数据类型,数据长度,其它数据”这四部分。比如,为了通过串口去控制单片机的蜂鸣器发出不同长度的声音,我专门制定了一串十六进制的数据:EB 01 00 00 00 08 03 E8 ,下面以此串数据来跟大家详细分析。
       数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。只有“接头暗号”吻合,单片机才会进入到接收其它有效数据的步骤,否则一直被“数据头”挡在门外视为无效数据。注意,数据头不能用十六进制的00或者FF,因为00和FF的密码等级太弱,很多单片机一上电的瞬间因为硬件的某种不确定的原因,会直接误发送00或者FF这类干扰数据。
       数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途,比如,01代表用来控制蜂鸣器的,02代表控制LED的,03代表机器启动,等等功能,都可以用这个字节的数据进行分类定义。本例子用01代表控制蜂鸣器发出不同时间长度的声音。
       数据长度(00 00 00 08):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。本例子中,数据长度占用了4个字节,就意味着最大数据长度是一个unsigned long类型的数据范围,从0到4294967295。比如,本例子中一串数据的长度是8个字节(EB 01 00 00 00 08 03 E8 ),因此这“数据长度”四个字节分别是00 00 00 08,十六进制的08代表十进制的8字节。注意,51单片机的内存是属于大端模式,因此十进制的8在四字节unsigned long的内存排列顺序是00 00 00 08,也就是低位放在数组的高下标。如果是stm32的单片机,stm32单片机的内存是属于小端模式,十进制的8在四字节unsigned long的内存排列顺序是08 00 00 00,低位放在数组的低下标。为什么强调这个?因为主要方便我们用指针的方法实现数据的拆分和整合,这个知识点的内容我在前面第62节详细讲解过。
       其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。本例子十六进制的03 E8,代表一个unsigned int的十进制数据1000。此数据的大小用来控制蜂鸣器发声的长度,1000代表长叫1000ms。如果想让蜂鸣器短叫100ms,只需把这两个字节改为:00 64。

【128.2   程序框架的四个要点分析。】

       第一点:先接收后处理,开辟一块专用的内存数组。要处理一串数据,必须先征用一块内存数组专门用来缓存接收到的数据,等接收完此串数据再处理。
       第二点:接头暗号。本节例子的数据头EB就是接头暗号。一旦接头暗号吻合,才会进入到下一步接收其它有效数据的步骤上。
       第三点:如何识别接收一串数据的完毕。本节例子中,是靠“固定协议”提供的“数据长度”来判别是否已经接收完一串数据。中断函数接收完一串数据后,应该用一个全局变量来给外部main函数一个通知,让main函数里面的相关函数来处理此串数据。
       第四点:接收数据中相邻字节之间通信超时的异常处理。如果接头暗号吻合之后,马上切换到“接受其它有效数据”的步骤,但是,如果在此步骤的通信过程中一旦发现通信不连贯,就应该及时退出当下“接受其它有效数据”的步骤,继续返回到刚开始的“接头暗号”的步骤,为下一次接收新的一串数据做准备。那么,如何识别通信不连贯?靠判断接收数据中相邻字节之间的时间是否超时来决定,详细内容请看下面的程序例程。

【128.3   程序例程。】


         
            上图128.3.1  有源蜂鸣器电路





            上图128.3.2  232串口电路

程序功能如下:
      (1)在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
      (2)波特率9600,校验位NONE(无),数据位8,停止位1。
      (3)十六进制的数据格式如下:
       EB 01 00 00 00 08 XX XX
       其中EB是数据头,01是代表数据类型,00 00 00 08代表数据长度是8个(十进制)。XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。比如:
       让蜂鸣器鸣叫1000ms的时间,发送十六进制的: EB 01 00 00 00 08 03 E8
       让蜂鸣器鸣叫100ms的时间,发送十六进制的: EB 01 00 00 00 08 00 64

  1. #include "REG52.H"

  2. #define RECE_TIME_OUT    2000  //通信过程中字节之间的超时时间2000ms
  3. #define REC_BUFFER_SIZE  20    //接收数据的缓存数组的长度


  4. void usart(void);  //串口接收的中断函数
  5. void T0_time();    //定时器的中断函数

  6. void UsartTask(void);    //串口接收的任务函数,放在主函数内

  7. void SystemInitial(void) ;
  8. void Delay(unsigned long u32DelayTime) ;
  9. void PeripheralInitial(void) ;

  10. void BeepOpen(void);   
  11. void BeepClose(void);
  12. void VoiceScan(void);

  13. sbit P3_4=P3^4;  

  14. volatile unsigned char vGu8BeepTimerFlag=0;  
  15. volatile unsigned int vGu16BeepTimerCnt=0;  

  16. unsigned char Gu8ReceBuffer[REC_BUFFER_SIZE]; //开辟一片接收数据的缓存
  17. unsigned long Gu32ReceCnt=0;  //接收缓存数组的下标
  18. unsigned char Gu8ReceStep=0;  //接收中断函数里的步骤变量
  19. unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
  20. unsigned char Gu8ReceType=0; //接收的数据类型
  21. unsigned long Gu32ReceDataLength=0;  //接收的数据长度
  22. unsigned char Gu8FinishFlag=0;  //是否已接收完成一串数据的标志
  23. unsigned long *pu32Data; //用于数据转换的指针
  24. volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
  25. volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

  26. void main()
  27. {
  28. SystemInitial();            
  29. Delay(10000);               
  30. PeripheralInitial();      
  31.     while(1)  
  32. {  
  33.    UsartTask();   //串口接收的任务函数
  34.     }
  35. }

  36. void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
  37. {        
  38.    if(1==RI)  //接收完一个字节后引起的中断
  39.    {
  40.         RI = 0; //及时清零,避免一直无缘无故的进入中断。

  41. /* 注释一:
  42. * 以下Gu8FinishFlag变量的用途。
  43. * 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
  44. * UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
  45. * 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
  46. */
  47.            if(0==Gu8FinishFlag)  //1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
  48.            {

  49. /* 注释二:
  50. * 以下Gu8ReceFeedDog变量的用途。
  51. * 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
  52. * 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
  53. * 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
  54. * 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
  55. * 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
  56. * 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
  57. */
  58.                   Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
  59.                   switch(Gu8ReceStep)
  60.                   {
  61.                           case 0:     //接头暗号的步骤。判断数据头的步骤。
  62.                                    Gu8ReceBuffer[0]=SBUF; //直接读取刚接收完的一个字节的数据。
  63.                                    if(0xeb==Gu8ReceBuffer[0])  //等于数据头0xeb,接头暗号吻合。
  64.                                    {
  65.                                           Gu32ReceCnt=1; //接收缓存的下标
  66.                                           Gu8ReceStep=1;  //切换到下一个步骤,接收其它有效的数据
  67.                                    }
  68.                                    break;               
  69.                                        
  70.                           case 1:     //数据类型和长度
  71.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  72.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
  73.                                    if(Gu32ReceCnt>=6)  //前6个数据。接收完了“数据类型”和“数据长度”。
  74.                                    {
  75.                                             Gu8ReceType=Gu8ReceBuffer[1];  //提取“数据类型”
  76. //以下的数据转换,在第62节讲解过的指针法
  77.                                                 pu32Data=(unsigned long *)&Gu8ReceBuffer[2]; //数据转换
  78.                                                 Gu32ReceDataLength=*pu32Data; //提取“数据长度”
  79.                                             if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
  80.                                                 {
  81.                                                         Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  82.                                                         Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  83.                                                 }
  84.                                                 else   //如果还没结束,继续切换到下一个步骤,接收“其它数据”
  85.                                                 {
  86.                                                         Gu8ReceStep=2;   //切换到下一个步骤
  87.                                                 }                                                        
  88.                                    }
  89.                                    break;               
  90.                           case 2:     //其它数据
  91.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  92.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

  93. //靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
  94.                                    if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC_BUFFER_SIZE)
  95. {
  96.                                           Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  97.                                           Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  98.                                    }
  99.                                    break;        
  100.                   }
  101.        }
  102.    }
  103.    else  //发送数据引起的中断
  104.    {
  105.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  106.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  107.    }                                                      
  108. }  


  109. void UsartTask(void)    //串口接收的任务函数,放在主函数内
  110. {
  111. static unsigned int *pSu16Data; //数据转换的指针
  112. static unsigned int Su16Data;  //转换后的数据

  113.     if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
  114.         {
  115.                 Gu8ReceFeedDog=0;
  116.                                 
  117.                 vGu8ReceTimeOutFlag=0;
  118.         vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
  119.                 vGu8ReceTimeOutFlag=1;
  120.         }
  121.         else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
  122.         {
  123.             Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
  124.     }

  125.         
  126.         if(1==Gu8FinishFlag)  //1代表已经接收完毕一串新的数据,需要马上去处理
  127.         {
  128.                 switch(Gu8ReceType)  //接收到的数据类型
  129.                 {
  130.                         case 0x01:   //驱动蜂鸣器
  131. //以下的数据转换,在第62节讲解过的指针法
  132.                                  pSu16Data=(unsigned int *)&Gu8ReceBuffer[6]; //数据转换。
  133.                                  Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

  134. vGu8BeepTimerFlag=0;  
  135. vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
  136. vGu8BeepTimerFlag=1;  
  137.                   break;
  138.         }

  139.         Gu8FinishFlag=0;  //上面处理完数据再清零标志,为下一次接收新的数据做准备
  140.     }
  141. }



  142. void T0_time() interrupt 1     
  143. {
  144. VoiceScan();  

  145. if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
  146.         {
  147.                  vGu16ReceTimeOutCnt--;        
  148. }  

  149. TH0=0xfc;   
  150. TL0=0x66;   
  151. }


  152. void SystemInitial(void)
  153. {
  154. unsigned char u8_TMOD_Temp=0;

  155. //以下是定时器0的中断的配置
  156. TMOD=0x01;  
  157. TH0=0xfc;   
  158. TL0=0x66;   
  159. EA=1;      
  160. ET0=1;      
  161. TR0=1;   

  162. //以下是串口接收中断的配置
  163. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  164. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  165. TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  166. TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
  167. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  168. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  169. TR1=1;  //开启定时器1

  170. SM0=0;  
  171. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  172. REN=1;  //允许串口接收数据

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

  176. ES=1;         //允许串口中断  
  177. EA=1;         //允许总中断
  178. }

  179. void Delay(unsigned long u32DelayTime)
  180. {
  181.     for(;u32DelayTime>0;u32DelayTime--);
  182. }

  183. void PeripheralInitial(void)
  184. {

  185. }

  186. void BeepOpen(void)
  187. {
  188. P3_4=0;  
  189. }

  190. void BeepClose(void)
  191. {
  192. P3_4=1;  
  193. }

  194. void VoiceScan(void)
  195. {

  196.           static unsigned char Su8Lock=0;  

  197. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  198.           {
  199.                   if(0==Su8Lock)
  200.                   {
  201.                    Su8Lock=1;  
  202. BeepOpen();
  203.      }
  204.     else  
  205. {     

  206.                        vGu16BeepTimerCnt--;         

  207.                    if(0==vGu16BeepTimerCnt)
  208.                    {
  209.                            Su8Lock=0;     
  210. BeepClose();  
  211.                    }

  212. }
  213.           }         
  214. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
wcheng13 发表于 2018-9-11 16:20 | 显示全部楼层
看下看下,好长时间了。
 楼主| jianhong_wu 发表于 2018-9-18 10:58 | 显示全部楼层
第一百二十九节: 接收带“动态密匙”与“累加和”校验数据的串口程序框架。

【129.1   “累加和”与“动态密匙”。】

      上一节讲了串口基本的程序框架,但是没有讲到校验。校验在很多通信项目中是必不可少的。比如,在事关金融或者生命安全的项目,是不允许有任何的数据丢失或错误的;在容易受干扰的工业环境,或者在无线通信的项目中,这些项目往往容易丢失数据;还有一种常见的人为过失是,在编写程序的层面,因为超时重发的时间与从机不匹配,导致反馈的信息延时而造成数据丢失,如果这种情况也加上校验,通信会稳定可靠很多。
      上一节讲到“数据头,数据类型,数据长度,其它数据”这四个元素,本节在此基础上,增加两个校验的元素,分别是“动态密匙”与“累加和”。“动态密匙”占用2个字节,“累加和”占用1个字节,因此,这两个元素一共占用最后面的3个字节。分析如下:
       数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。
       数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途。
       数据长度(00 00 00 0B):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。
       其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。
       动态密匙(00 01):这两个字节代表一个unsigned int类型的数据,数据范围是从0到65535,但是考虑到数据更加安全可靠,一般丢弃了首尾的0(十六进制的00 00)与65535(十六进制的FF FF),只保留从1到65534的变化。大部分的通信模型都是主机对从机的“一问一应答”模式,也就是,主机每发送一条指令给从机,从机才返回一条消息作为应答。如果主机发送了信息后,在规定的时间内,没有收到从机的应答指令,主机就继续发送信息给从机,但是此时,从机本来应该应答主机当前指令的,可能因为某种情况导致反馈的信息发生了延时,导致此时应答的数据是主机的上一条指令,从而造成“一问一应答”的数据帧发送了错位,这种情况加上“动态密匙”就能使问题得到有效的解决。主机每发送一条信息,信息里都携带了2个字节的“动态密匙”,从机每收到主机的一条信息,在应答此信息时都把收到的“动态密匙”原封不动的反馈给主机,主机再查看发送的“动态密匙”与接收到的“动态密匙”是否一致,以此来判断应答数据是否有效。“动态密匙”像流水号一样,每发送一次指令后都累加1,不断发生变化,从1到65534,依次循环。这就是数据校验的一种方式。
       累加和(E3)。“累加和”放在数据串的最后一个字节,是前面所有字节的累加之和(不包括自己本身的字节),累加的结果高于一个字节的那部分自动溢出丢掉,只保留低8位的一个字节的数据。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 E3。其中最后一个字节E3就是“累加和”,前面所有字节相加等于十六进制的0x1E3,只保留低8位的一个字节的数据,因此为十六进制的0xE3。验证“累加和”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“累加和”,把自己计算得到的“累加和”与接收到的最后一个字节的“累加和”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【129.2   程序例程。】


     
       上图129.2.1  有源蜂鸣器电路





       上图129.2.2  232串口电路

程序功能如下:
      (1)单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
      (2)本节因为还没有讲到数据发送的内容,因此应答那部分的代码暂时不写,只写“累加和”那部分的代码。
      (3)波特率9600,校验位NONE(无),数据位8,停止位1。
      (4)十六进制的数据格式:EB 01 00 00 00 0B XX XX YY YY ZZ 。其中:
       EB是数据头。
       01是代表数据类型。
       00 00 00 0B代表数据长度是11个(十进制)。
       XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。
       YY YY代表一个unsigned int的动态密匙,每收发一条指令,此数据累加一次1,范围从1到65534。
       ZZ 代表前面所有字节的累加和。
       比如:
       让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 E3
       让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 01 00 00 00 0B 00 64 00 02 5D

  1. #include "REG52.H"

  2. #define RECE_TIME_OUT    2000  //通信过程中字节之间的超时时间2000ms
  3. #define REC_BUFFER_SIZE  20    //接收数据的缓存数组的长度


  4. void usart(void);  //串口接收的中断函数
  5. void T0_time();    //定时器的中断函数

  6. void UsartTask(void);    //串口接收的任务函数,放在主函数内

  7. void SystemInitial(void) ;
  8. void Delay(unsigned long u32DelayTime) ;
  9. void PeripheralInitial(void) ;

  10. void BeepOpen(void);   
  11. void BeepClose(void);
  12. void VoiceScan(void);

  13. sbit P3_4=P3^4;  

  14. volatile unsigned char vGu8BeepTimerFlag=0;  
  15. volatile unsigned int vGu16BeepTimerCnt=0;  

  16. unsigned char Gu8ReceBuffer[REC_BUFFER_SIZE]; //开辟一片接收数据的缓存
  17. unsigned long Gu32ReceCnt=0;  //接收缓存数组的下标
  18. unsigned char Gu8ReceStep=0;  //接收中断函数里的步骤变量
  19. unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
  20. unsigned char Gu8ReceType=0; //接收的数据类型
  21. unsigned int Gu16ReceYY=0; //接收的动态密匙
  22. unsigned char Gu8ReceZZ=0; //接收的累加和,必须是unsigned char的数据类型
  23. unsigned long Gu32ReceDataLength=0;  //接收的数据长度
  24. unsigned char Gu8FinishFlag=0;  //是否已接收完成一串数据的标志
  25. unsigned long *pu32Data; //用于数据转换的指针
  26. volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
  27. volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

  28. void main()
  29. {
  30. SystemInitial();            
  31. Delay(10000);               
  32. PeripheralInitial();      
  33.     while(1)  
  34. {  
  35.    UsartTask();   //串口接收的任务函数
  36.     }
  37. }

  38. void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
  39. {        
  40.    if(1==RI)  //接收完一个字节后引起的中断
  41.    {
  42.         RI = 0; //及时清零,避免一直无缘无故的进入中断。

  43. /* 注释一:
  44. * 以下Gu8FinishFlag变量的用途。
  45. * 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
  46. * UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
  47. * 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
  48. */
  49.            if(0==Gu8FinishFlag)  //1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
  50.            {

  51. /* 注释二:
  52. * 以下Gu8ReceFeedDog变量的用途。
  53. * 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
  54. * 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
  55. * 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
  56. * 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
  57. * 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
  58. * 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
  59. */
  60.                   Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
  61.                   switch(Gu8ReceStep)
  62.                   {
  63.                           case 0:     //接头暗号的步骤。判断数据头的步骤。
  64.                                    Gu8ReceBuffer[0]=SBUF; //直接读取刚接收完的一个字节的数据。
  65.                                    if(0xeb==Gu8ReceBuffer[0])  //等于数据头0xeb,接头暗号吻合。
  66.                                    {
  67.                                           Gu32ReceCnt=1; //接收缓存的下标
  68.                                           Gu8ReceStep=1;  //切换到下一个步骤,接收其它有效的数据
  69.                                    }
  70.                                    break;               
  71.                                        
  72.                           case 1:     //数据类型和长度
  73.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  74.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
  75.                                    if(Gu32ReceCnt>=6)  //前6个数据。接收完了“数据类型”和“数据长度”。
  76.                                    {
  77.                                             Gu8ReceType=Gu8ReceBuffer[1];  //提取“数据类型”
  78. //以下的数据转换,在第62节讲解过的指针法
  79.                                                 pu32Data=(unsigned long *)&Gu8ReceBuffer[2]; //数据转换
  80.                                                 Gu32ReceDataLength=*pu32Data; //提取“数据长度”
  81.                                             if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
  82.                                                 {
  83.                                                         Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  84.                                                         Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  85.                                                 }
  86.                                                 else   //如果还没结束,继续切换到下一个步骤,接收“其它数据”
  87.                                                 {
  88.                                                         Gu8ReceStep=2;   //切换到下一个步骤
  89.                                                 }                                                        
  90.                                    }
  91.                                    break;               
  92.                           case 2:     //其它数据
  93.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  94.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

  95. //靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
  96.                                    if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC_BUFFER_SIZE)
  97. {
  98.                                           Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  99.                                           Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  100.                                    }
  101.                                    break;        
  102.                   }
  103.        }
  104.    }
  105.    else  //发送数据引起的中断
  106.    {
  107.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  108.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  109.    }                                                      
  110. }  


  111. void UsartTask(void)    //串口接收的任务函数,放在主函数内
  112. {
  113. static unsigned int *pSu16Data; //数据转换的指针
  114. static unsigned int Su16Data;  //转换后的数据
  115. static unsigned int i;
  116. static unsigned char Su8RecZZ=0;  //计算的“累加和”,必须是unsigned char的数据类型



  117.     if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
  118.         {
  119.                 Gu8ReceFeedDog=0;
  120.                                 
  121.                 vGu8ReceTimeOutFlag=0;
  122.         vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
  123.                 vGu8ReceTimeOutFlag=1;
  124.         }
  125.         else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
  126.         {
  127.             Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
  128.     }

  129.         
  130.         if(1==Gu8FinishFlag)  //1代表已经接收完毕一串新的数据,需要马上去处理
  131.         {
  132.                 switch(Gu8ReceType)  //接收到的数据类型
  133.                 {
  134.                         case 0x01:   //驱动蜂鸣器
  135. //以下的数据转换,在第62节讲解过的指针法

  136.                                  pSu16Data=(unsigned int *)&Gu8ReceBuffer[Gu32ReceDataLength-3]; //数据转换
  137. Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

  138. Gu8ReceZZ=Gu8ReceBuffer[Gu32ReceDataLength-1];  //提取“累加和”

  139. Su8RecZZ=0;
  140. for(i=0;i<(Gu32ReceDataLength-1);i++)
  141. {
  142. Su8RecZZ=Su8RecZZ+Gu8ReceBuffer[i];   //计算“累加和”
  143. }

  144. if(Su8RecZZ==Gu8ReceZZ) //验证“累加和”,“计算的”与“接收的”是否一致
  145. {
  146.                               pSu16Data=(unsigned int *)&Gu8ReceBuffer[6]; //数据转换。
  147.                                      Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

  148. vGu8BeepTimerFlag=0;  
  149. vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
  150. vGu8BeepTimerFlag=1;  
  151. }

  152.                
  153.                   break;
  154.         }

  155.         Gu8FinishFlag=0;  //上面处理完数据再清零标志,为下一次接收新的数据做准备
  156.     }
  157. }

  158. void T0_time() interrupt 1     
  159. {
  160. VoiceScan();  

  161. if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
  162.         {
  163.                  vGu16ReceTimeOutCnt--;        
  164. }  

  165. TH0=0xfc;   
  166. TL0=0x66;   
  167. }


  168. void SystemInitial(void)
  169. {
  170. unsigned char u8_TMOD_Temp=0;

  171. //以下是定时器0的中断的配置
  172. TMOD=0x01;  
  173. TH0=0xfc;   
  174. TL0=0x66;   
  175. EA=1;      
  176. ET0=1;      
  177. TR0=1;   

  178. //以下是串口接收中断的配置
  179. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  180. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  181. TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  182. TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
  183. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  184. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  185. TR1=1;  //开启定时器1

  186. SM0=0;  
  187. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  188. REN=1;  //允许串口接收数据

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

  192. ES=1;         //允许串口中断  
  193. EA=1;         //允许总中断
  194. }

  195. void Delay(unsigned long u32DelayTime)
  196. {
  197.     for(;u32DelayTime>0;u32DelayTime--);
  198. }

  199. void PeripheralInitial(void)
  200. {

  201. }

  202. void BeepOpen(void)
  203. {
  204. P3_4=0;  
  205. }

  206. void BeepClose(void)
  207. {
  208. P3_4=1;  
  209. }

  210. void VoiceScan(void)
  211. {

  212.           static unsigned char Su8Lock=0;  

  213. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  214.           {
  215.                   if(0==Su8Lock)
  216.                   {
  217.                    Su8Lock=1;  
  218. BeepOpen();
  219.      }
  220.     else  
  221. {     

  222.                        vGu16BeepTimerCnt--;         

  223.                    if(0==vGu16BeepTimerCnt)
  224.                    {
  225.                            Su8Lock=0;     
  226. BeepClose();  
  227.                    }

  228. }
  229.           }         
  230. }




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
arima 发表于 2018-9-22 22:33 | 显示全部楼层
很有价值的资料. 加油!!!
JAMESCAMPINE 发表于 2018-9-23 11:07 | 显示全部楼层
hjw01 发表于 2018-9-23 17:04 | 显示全部楼层
jianhong_wu 发表于 2018-7-24 11:15
第一百二十五节: “双线”的肢体接触通信。

【125.1   “双线”的肢体接触通信。】

芯片之间通信,都离不开“数据信号”和“时钟信号”,缺一不可
这句不是很正确,需要时钟信号是因为需要同步,异步串行通信就不要时钟,用一根线加GND就可以完成通信。
tianqi911 发表于 2018-9-28 09:47 | 显示全部楼层
就不能打个包吗?高人。
tianqi911 发表于 2018-9-28 16:01 | 显示全部楼层
大神,你出书了吗?我想直接买本书。点下载手指头都肿了。
tianqi911 发表于 2018-9-28 16:49 | 显示全部楼层
非常感谢您的指导。真的很感动。一个小公司的小员工,没人讨论,没人指导,还要做案子,十分苦恼。没处问,没处找。真是没想到,宝藏就在眼前,您的大度和大方,真的惊到我了。非常感谢。只能点个赞!
 楼主| jianhong_wu 发表于 2018-10-4 11:10 | 显示全部楼层
第一百三十节: 接收带“动态密匙”与“异或”校验数据的串口程序框架。

【130.1   “异或”的校验。】

      通信的校验常用有两种,一种是“累加和”,另一种是“异或”。“异或”算法的详细介绍请看前面章节的第32节。
      上一节讲的“累加和”,放在数据串的最后一个字节,是前面所有字节的累加之和(不包括自己本身的字节),累加的结果高于一个字节的那部分自动溢出丢掉,只保留低8位的一个字节的数据。本节讲的“异或”,也是放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。本节在上一节的基础上,只更改以下这段校验算法的代码即可。

      上一节的“累加和”算法如下:

  1. Gu8ReceZZ=Gu8ReceBuffer[Gu32ReceDataLength-1];  //提取“累加和”

  2. Su8RecZZ=0;
  3. for(i=0;i<(Gu32ReceDataLength-1);i++)
  4. {
  5. Su8RecZZ=Su8RecZZ+Gu8ReceBuffer[i];   //计算“累加和”
  6. }

  7. if(Su8RecZZ==Gu8ReceZZ) //验证“累加和”,“计算的”与“接收的”是否一致
  8. {
  9.     //此处省去若干代码
  10. }


      本节的“异或”算法如下:

  1. Gu8ReceZZ=Gu8ReceBuffer[Gu32ReceDataLength-1];  //提取接收到的“异或”

  2. Su8RecZZ=Gu8ReceBuffer[0]; //提取数据串第“i=0”个数据作为异或的原始数据
  3. for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
  4. {
  5. Su8RecZZ=Su8RecZZ^Gu8ReceBuffer[i];   //计算“异或”
  6. }

  7. if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
  8. {
  9.     //此处省去若干代码
  10. }


【130.2   通信协议。】

       数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。
       数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途。
       数据长度(00 00 00 0B):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。
       其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。
       动态密匙(00 01):这两个字节代表一个unsigned int类型的数据,数据范围是从0到65535,但是考虑到数据更加安全可靠,一般丢弃了首尾的0(十六进制的00 00)与65535(十六进制的FF FF),只保留从1到65534的变化。大部分的通信模型都是主机对从机的“一问一应答”模式,也就是,主机每发送一条指令给从机,从机才返回一条消息作为应答。如果主机发送了信息后,在规定的时间内,没有收到从机的应答指令,主机就继续发送信息给从机,但是此时,从机本来应该应答主机当前指令的,可能因为某种情况导致反馈的信息发生了延时,导致此时应答的数据是主机的上一条指令,从而造成“一问一应答”的数据帧发送了错位,这种情况加上“动态密匙”就能使问题得到有效的解决。主机每发送一条信息,信息里都携带了2个字节的“动态密匙”,从机每收到主机的一条信息,在应答此信息时都把收到的“动态密匙”原封不动的反馈给主机,主机再查看发送的“动态密匙”与接收到的“动态密匙”是否一致,以此来判断应答数据是否有效。“动态密匙”像流水号一样,每发送一次指令后都累加1,不断发生变化,从1到65534,依次循环。这是数据校验的一种方式。
       异或(0B)。“异或”放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 0B。其中最后一个字节0B就是“异或”字节,前面所有字节相“异或”等于十六进制的0B。验证“异或”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”,该计算器软件的异或运算按键是“Xor”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“异或”,把自己计算得到的“异或”与接收到的最后一个字节的“异或”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【130.3   程序例程。】


     
       上图130.3.1  有源蜂鸣器电路





       上图130.3.2  232串口电路

程序功能如下:
      (1)单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。
      (2)本节因为还没有讲到数据发送的内容,因此应答“动态密匙”那部分的代码暂时不写,只写验证“异或”那部分的代码。
      (3)波特率9600,校验位NONE(无),数据位8,停止位1。
      (4)十六进制的数据格式:EB 01 00 00 00 0B XX XX YY YY ZZ 。其中:
              EB是数据头。
              01是代表数据类型。
              00 00 00 0B代表数据长度是11个(十进制)。
              XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。
              YY YY代表一个unsigned int的动态密匙,每收发一条指令,此数据累加一次1,范围从1到65534。
              ZZ 代表前面所有字节的异或结果。
比如:
       让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 0B
       让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 01 00 00 00 0B 00 64 00 02 87



  1. #include "REG52.H"

  2. #define RECE_TIME_OUT    2000  //通信过程中字节之间的超时时间2000ms
  3. #define REC_BUFFER_SIZE  20    //接收数据的缓存数组的长度


  4. void usart(void);  //串口接收的中断函数
  5. void T0_time();    //定时器的中断函数

  6. void UsartTask(void);    //串口接收的任务函数,放在主函数内

  7. void SystemInitial(void) ;
  8. void Delay(unsigned long u32DelayTime) ;
  9. void PeripheralInitial(void) ;

  10. void BeepOpen(void);   
  11. void BeepClose(void);
  12. void VoiceScan(void);

  13. sbit P3_4=P3^4;  

  14. volatile unsigned char vGu8BeepTimerFlag=0;  
  15. volatile unsigned int vGu16BeepTimerCnt=0;  

  16. unsigned char Gu8ReceBuffer[REC_BUFFER_SIZE]; //开辟一片接收数据的缓存
  17. unsigned long Gu32ReceCnt=0;  //接收缓存数组的下标
  18. unsigned char Gu8ReceStep=0;  //接收中断函数里的步骤变量
  19. unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
  20. unsigned char Gu8ReceType=0; //接收的数据类型
  21. unsigned int Gu16ReceYY=0; //接收的动态密匙
  22. unsigned char Gu8ReceZZ=0; //接收的异或
  23. unsigned long Gu32ReceDataLength=0;  //接收的数据长度
  24. unsigned char Gu8FinishFlag=0;  //是否已接收完成一串数据的标志
  25. unsigned long *pu32Data; //用于数据转换的指针
  26. volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
  27. volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

  28. void main()
  29. {
  30. SystemInitial();            
  31. Delay(10000);               
  32. PeripheralInitial();      
  33.     while(1)  
  34. {  
  35.    UsartTask();   //串口接收的任务函数
  36.     }
  37. }

  38. void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
  39. {        
  40.    if(1==RI)  //接收完一个字节后引起的中断
  41.    {
  42.         RI = 0; //及时清零,避免一直无缘无故的进入中断。

  43. /* 注释一:
  44. * 以下Gu8FinishFlag变量的用途。
  45. * 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
  46. * UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
  47. * 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
  48. */
  49.            if(0==Gu8FinishFlag)  //1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
  50.            {

  51. /* 注释二:
  52. * 以下Gu8ReceFeedDog变量的用途。
  53. * 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
  54. * 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
  55. * 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
  56. * 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
  57. * 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
  58. * 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
  59. */
  60.                   Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
  61.                   switch(Gu8ReceStep)
  62.                   {
  63.                           case 0:     //接头暗号的步骤。判断数据头的步骤。
  64.                                    Gu8ReceBuffer[0]=SBUF; //直接读取刚接收完的一个字节的数据。
  65.                                    if(0xeb==Gu8ReceBuffer[0])  //等于数据头0xeb,接头暗号吻合。
  66.                                    {
  67.                                           Gu32ReceCnt=1; //接收缓存的下标
  68.                                           Gu8ReceStep=1;  //切换到下一个步骤,接收其它有效的数据
  69.                                    }
  70.                                    break;               
  71.                                        
  72.                           case 1:     //数据类型和长度
  73.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  74.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
  75.                                    if(Gu32ReceCnt>=6)  //前6个数据。接收完了“数据类型”和“数据长度”。
  76.                                    {
  77.                                             Gu8ReceType=Gu8ReceBuffer[1];  //提取“数据类型”
  78. //以下的数据转换,在第62节讲解过的指针法
  79.                                                 pu32Data=(unsigned long *)&Gu8ReceBuffer[2]; //数据转换
  80.                                                 Gu32ReceDataLength=*pu32Data; //提取“数据长度”
  81.                                             if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
  82.                                                 {
  83.                                                         Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  84.                                                         Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  85.                                                 }
  86.                                                 else   //如果还没结束,继续切换到下一个步骤,接收“其它数据”
  87.                                                 {
  88.                                                         Gu8ReceStep=2;   //切换到下一个步骤
  89.                                                 }                                                        
  90.                                    }
  91.                                    break;               
  92.                           case 2:     //其它数据
  93.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  94.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

  95. //靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
  96.                                    if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=REC_BUFFER_SIZE)
  97. {
  98.                                           Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  99.                                           Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  100.                                    }
  101.                                    break;        
  102.                   }
  103.        }
  104.    }
  105.    else  //发送数据引起的中断
  106.    {
  107.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  108.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  109.    }                                                      
  110. }  


  111. void UsartTask(void)    //串口接收的任务函数,放在主函数内
  112. {
  113. static unsigned int *pSu16Data; //数据转换的指针
  114. static unsigned int Su16Data;  //转换后的数据
  115. static unsigned int i;
  116. static unsigned char Su8RecZZ=0;  //计算的“异或”



  117.     if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
  118.         {
  119.                 Gu8ReceFeedDog=0;
  120.                                 
  121.                 vGu8ReceTimeOutFlag=0;
  122.         vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
  123.                 vGu8ReceTimeOutFlag=1;
  124.         }
  125.         else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
  126.         {
  127.             Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
  128.     }

  129.         
  130.         if(1==Gu8FinishFlag)  //1代表已经接收完毕一串新的数据,需要马上去处理
  131.         {
  132.                 switch(Gu8ReceType)  //接收到的数据类型
  133.                 {
  134.                         case 0x01:   //驱动蜂鸣器
  135. //以下的数据转换,在第62节讲解过的指针法

  136.                                  pSu16Data=(unsigned int *)&Gu8ReceBuffer[Gu32ReceDataLength-3]; //数据转换
  137. Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

  138. Gu8ReceZZ=Gu8ReceBuffer[Gu32ReceDataLength-1];  //提取接收到的“异或”

  139. Su8RecZZ=Gu8ReceBuffer[0]; //提取数据串第“i=0”个数据作为异或的原始数据
  140. for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
  141. {
  142. Su8RecZZ=Su8RecZZ^Gu8ReceBuffer[i];   //计算“异或”
  143. }

  144. if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
  145. {
  146.                               pSu16Data=(unsigned int *)&Gu8ReceBuffer[6]; //数据转换。
  147.                                      Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

  148. vGu8BeepTimerFlag=0;  
  149. vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
  150. vGu8BeepTimerFlag=1;  
  151. }

  152.                
  153.                   break;
  154.         }

  155.         Gu8FinishFlag=0;  //上面处理完数据再清零标志,为下一次接收新的数据做准备
  156.     }
  157. }

  158. void T0_time() interrupt 1     
  159. {
  160. VoiceScan();  

  161. if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
  162.         {
  163.                  vGu16ReceTimeOutCnt--;        
  164. }  

  165. TH0=0xfc;   
  166. TL0=0x66;   
  167. }


  168. void SystemInitial(void)
  169. {
  170. unsigned char u8_TMOD_Temp=0;

  171. //以下是定时器0的中断的配置
  172. TMOD=0x01;  
  173. TH0=0xfc;   
  174. TL0=0x66;   
  175. EA=1;      
  176. ET0=1;      
  177. TR0=1;   

  178. //以下是串口接收中断的配置
  179. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  180. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  181. TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  182. TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
  183. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  184. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  185. TR1=1;  //开启定时器1

  186. SM0=0;  
  187. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  188. REN=1;  //允许串口接收数据

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

  192. ES=1;         //允许串口中断  
  193. EA=1;         //允许总中断
  194. }

  195. void Delay(unsigned long u32DelayTime)
  196. {
  197.     for(;u32DelayTime>0;u32DelayTime--);
  198. }

  199. void PeripheralInitial(void)
  200. {

  201. }

  202. void BeepOpen(void)
  203. {
  204. P3_4=0;  
  205. }

  206. void BeepClose(void)
  207. {
  208. P3_4=1;  
  209. }

  210. void VoiceScan(void)
  211. {

  212.           static unsigned char Su8Lock=0;  

  213. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  214.           {
  215.                   if(0==Su8Lock)
  216.                   {
  217.                    Su8Lock=1;  
  218. BeepOpen();
  219.      }
  220.     else  
  221. {     

  222.                        vGu16BeepTimerCnt--;         

  223.                    if(0==vGu16BeepTimerCnt)
  224.                    {
  225.                            Su8Lock=0;     
  226. BeepClose();  
  227.                    }

  228. }
  229.           }         
  230. }


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
xuexb 发表于 2018-10-11 11:41 | 显示全部楼层
牛,定一下
arima 发表于 2018-10-12 23:08 | 显示全部楼层
等待更新...
 楼主| jianhong_wu 发表于 2018-10-14 10:13 | 显示全部楼层
本帖最后由 jianhong_wu 于 2018-12-10 10:26 编辑

第一百三十一节: 灵活切换各种不同大小“接收内存”的串口程序框架。

【131.1   切换各种不同大小“接收内存”。】

      很多32位的单片机,只要外挂SRAM或者SDRAM这类内存芯片,就可以轻松的把一个全局变量的数组开辟到几百K甚至几兆的容量。开辟这么大的数组,往往是用来处理一些文件类的大数据,比如串口接收一张480x272点阵大小的.BMP格式的图片文件,就需要开辟一个几百K的全局变量大数组。串口通信中,从接收内存的容量来划分,常用有两种数据类型,一种是常规控制类(容量小),一种是文件类(容量大),要能做到在这两种“接收内存”中灵活切换,关键是用到“指针的中转切换”技术。
      “常规控制类内存”负责两块事务,一块是接收“前部分的”[数据头,数据类型,数据长度],另一块是“后部分的”[常规控制类的专用数据]。
      “文件类内存”只负责“后部分的”[文件类的专用数据],而“前部分的”[数据头,数据类型,数据长度]是需要借助“常规控制类内存”来实现的。
      本节破题的关键在于,根据不同的数据类型,利用“指针的中转切换”实现不同接收内存的灵活切换。关键代码是串口中断函数这部分的处理,片段代码的讲解如下:



  1. unsigned char Gu8ReceBuffer[20]; //常规控制类的小内存
  2. unsigned char Gu8FileBuffer[40]; //文件类的大内存
  3. unsigned char *pGu8ReceBuffer;  //用来切换接收内存的“中转指针”
  4. unsigned long Gu32ReceCntMax=20;  //最大缓存(初始值20或者40都没关系,因为后续会动态改变)

  5. void usart(void) interrupt 4           
  6. {        
  7.    if(1==RI)  
  8.    {
  9.         RI = 0;

  10.            if(0==Gu8FinishFlag)  
  11.            {
  12.                   Gu8ReceFeedDog=1;
  13.                   switch(Gu8ReceStep)
  14.                   {
  15.                           case 0:     //“前部分的”数据头。接头暗号的步骤
  16.                                    Gu8ReceBuffer[0]=SBUF;
  17.                                    if(0xeb==Gu8ReceBuffer[0])  
  18.                                    {
  19.                                           Gu32ReceCnt=1;
  20.                                           Gu8ReceStep=1;  
  21.                                    }
  22.                                    break;               
  23.                                        
  24.                           case 1:     //“前部分的”数据类型和长度
  25.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF;
  26.                                    Gu32ReceCnt++;
  27.                                    if(Gu32ReceCnt>=6)  //前6个数据。接收完了“数据类型”和“数据长度”。
  28.                                    {
  29.                                             Gu8ReceType=Gu8ReceBuffer[1];  //提取“数据类型”
  30.                                                 pu32Data=(unsigned long *)&Gu8ReceBuffer[2];
  31.                                                 Gu32ReceDataLength=*pu32Data; //提取“数据长度”
  32.                                             if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
  33.                                                 {
  34.                                                         Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  35.                                                         Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  36.                                                 }
  37.                                                 else   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
  38.                                                 {
  39.                              //以下几行代码是本节的破题关键!!!
  40.                              if(0x02==Gu8ReceType) //如果是文件类,把指针关联到Gu8FileBuffer
  41.                               {
  42.                                    pGu8ReceBuffer=(unsigned char *)&Gu8FileBuffer[0];//下标0
  43. Gu32ReceCntMax=40+6;  //最大缓存
  44. }
  45. else //如果是常规类,继续把指针关联到Gu8ReceBuffer本身的数组
  46.                               {
  47.                                    pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer[6];//下标6
  48. Gu32ReceCntMax=20;  //最大缓存
  49. }

  50.                                                         Gu8ReceStep=2;   //切换到下一个步骤
  51.                                                 }                                                       
  52.                                    }
  53.                                    break;               
  54.                           case 2:     //“后部分的”数据
  55.                                    pGu8ReceBuffer[Gu32ReceCnt-6]=SBUF; //这里的指针就是各种不同内存的化身!!!
  56.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

  57. //靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
  58.                                    if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
  59. {
  60.                                           Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  61.                                           Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  62.                                    }
  63.                                    break;       
  64.                   }
  65.        }
  66.    }
  67.    else  //发送数据引起的中断
  68.    {
  69.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  70.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  71.    }                                                      
  72. }  





【131.2   通信协议。】

        数据头(EB):占1个字节,作为“起始字节”,起到“接头暗号”的作用,平时用来过滤无关的数据。
        数据类型(01):占用1个字节。数据类型是用来定义这串数据的用途。
        数据长度(00 00 00 0B):占4个字节。用来告诉通信的对方,这串数据一共有多少个字节。
        其它数据(03 E8):此数据根据不同的“数据类型”可以用来做不同的用途,根据具体的项目而定。
        动态密匙(00 01):这两个字节代表一个unsigned int类型的数据,数据范围是从0到65535,但是考虑到数据更加安全可靠,一般丢弃了首尾的0(十六进制的00 00)与65535(十六进制的FF FF),只保留从1到65534的变化。大部分的通信模型都是主机对从机的“一问一应答”模式,也就是,主机每发送一条指令给从机,从机才返回一条消息作为应答。如果主机发送了信息后,在规定的时间内,没有收到从机的应答指令,主机就继续发送信息给从机,但是此时,从机本来应该应答主机当前指令的,可能因为某种情况导致反馈的信息发生了延时,导致此时应答的数据是主机的上一条指令,从而造成“一问一应答”的数据帧发送了错位,这种情况加上“动态密匙”就能使问题得到有效的解决。主机每发送一条信息,信息里都携带了2个字节的“动态密匙”,从机每收到主机的一条信息,在应答此信息时都把收到的“动态密匙”原封不动的反馈给主机,主机再查看发送的“动态密匙”与接收到的“动态密匙”是否一致,以此来判断应答数据是否有效。“动态密匙”像流水号一样,每发送一次指令后都累加1,不断发生变化,从1到65534,依次循环。这是数据校验的一种方式。
       异或(0B)。“异或”放在数据串的最后一个字节,是前面所有字节的异或结果(不包括自己本身的字节)。比如:本例子中,数据串是:EB 01 00 00 00 0B 03 E8 00 01 0B。其中最后一个字节0B就是“异或”字节,前面所有字节相“异或”等于十六进制的0B。验证“异或”的方法,可以借用电脑“附件”自带的“计算器”软件来实现,打开“计算器”软件后,在“查看”的下拉菜单里,选择“程序员”,然后选择“十六进制”,该计算器软件的异或运算按键是“Xor”。不管是主机还是从机,每接收到一串数据后,都要自己计算一次“异或”,把自己计算得到的“异或”与接收到的最后一个字节的“异或”进行对比,来判断接收到的数据是否发生了丢失或者错误。

【131.3   程序例程。】


     
       上图131.3.1  有源蜂鸣器电路





       上图131.3.2  232串口电路

程序功能如下:
      (1)单片机模拟从机,上位机的串口助手模拟主机。在上位机的串口助手里,发送一串数据,控制蜂鸣器发出不同长度的声音。数据类型为01时,把“后部分的”数据发送给“常规控制类内存”;数据类型为02时,把“后部分的”数据发送给“文件类内存”。
      (2)本节因为还没有讲到数据发送的内容,因此应答“动态密匙”那部分的代码暂时不写,只写验证“异或”那部分的代码。
      (3)波特率9600,校验位NONE(无),数据位8,停止位1。
      (4)十六进制的数据格式:EB 01 00 00 00 0B XX XX YY YY ZZ 。其中:
        EB是数据头。
        01是代表数据类型。
        00 00 00 0B代表数据长度是11个(十进制)。
        XX XX代表一个unsigned int的数据,此数据的大小决定了蜂鸣器发出声音的长度。
        YY YY代表一个unsigned int的动态密匙,每收发一条指令,此数据累加一次1,范围从1到65534。
        ZZ 代表前面所有字节的异或结果。
比如:
        数据类型01,“后部分的”数据发给“常规控制类内存”,让蜂鸣器鸣叫1000毫秒,密匙为00 01,发送十六进制的:EB 01 00 00 00 0B 03 E8 00 01 0B
        数据类型02,“后部分的”数据发给“文件类内存”,让蜂鸣器鸣叫100毫秒, 密匙为00 02,发送十六进制的:EB 02 00 00 00 0B 00 64 00 02 84







  1. #include "REG52.H"

  2. #define RECE_TIME_OUT    2000  //通信过程中字节之间的超时时间2000ms
  3. #define REC_BUFFER_SIZE  20    //常规控制类数组的长度
  4. #define FILE_BUFFER_SIZE  40   //文件类数组的长度


  5. void usart(void);  //串口接收的中断函数
  6. void T0_time();    //定时器的中断函数

  7. void UsartTask(void);    //串口接收的任务函数,放在主函数内

  8. void SystemInitial(void) ;
  9. void Delay(unsigned long u32DelayTime) ;
  10. void PeripheralInitial(void) ;

  11. void BeepOpen(void);   
  12. void BeepClose(void);
  13. void VoiceScan(void);

  14. sbit P3_4=P3^4;  

  15. volatile unsigned char vGu8BeepTimerFlag=0;  
  16. volatile unsigned int vGu16BeepTimerCnt=0;  

  17. unsigned char Gu8ReceBuffer[REC_BUFFER_SIZE]; //常规控制类的小内存
  18. unsigned char Gu8FileBuffer[FILE_BUFFER_SIZE]; //文件类的大内存
  19. unsigned char *pGu8ReceBuffer;  //用来切换接收内存的“中转指针”

  20. unsigned long Gu32ReceCntMax=REC_BUFFER_SIZE;  //最大缓存
  21. unsigned long Gu32ReceCnt=0;  //接收缓存数组的下标
  22. unsigned char Gu8ReceStep=0;  //接收中断函数里的步骤变量
  23. unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
  24. unsigned char Gu8ReceType=0; //接收的数据类型
  25. unsigned int Gu16ReceYY=0; //接收的动态密匙
  26. unsigned char Gu8ReceZZ=0; //接收的异或
  27. unsigned long Gu32ReceDataLength=0;  //接收的数据长度
  28. unsigned char Gu8FinishFlag=0;  //是否已接收完成一串数据的标志
  29. unsigned long *pu32Data; //用于数据转换的指针
  30. volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
  31. volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

  32. void main()
  33. {
  34. SystemInitial();            
  35. Delay(10000);               
  36. PeripheralInitial();      
  37.     while(1)  
  38. {  
  39.    UsartTask();   //串口接收的任务函数
  40.     }
  41. }

  42. void usart(void) interrupt 4   //串口接发的中断函数,中断号为4         
  43. {        
  44.    if(1==RI)  //接收完一个字节后引起的中断
  45.    {
  46.         RI = 0; //及时清零,避免一直无缘无故的进入中断。

  47. /* 注释一:
  48. * 以下Gu8FinishFlag变量的用途。
  49. * 此变量一箭双雕,0代表正处于接收数据的状态,1代表已经接收完毕并且及时通知主函数中的处理函数
  50. * UsartTask()去处理新接收到的一串数据。除此之外,还起到一种“自锁自保护”的功能,在新数据还
  51. * 没有被主函数处理完毕的时候,禁止接收其它新的数据,避免新数据覆盖了尚未处理的数据。
  52. */
  53.            if(0==Gu8FinishFlag)  //1代表已经完成接收了一串新数据,并且禁止接收其它新的数据
  54.            {

  55. /* 注释二:
  56. * 以下Gu8ReceFeedDog变量的用途。
  57. * 此变量是用来检测并且识别通信过程中相邻的字节之间是否存在超时的情况。
  58. * 如果大家听说过单片机中的“看门狗”这个概念,那么每接收到一个数据此变量就“置1”一次,它的
  59. * 作用就是起到及时“喂狗”的作用。每接收到一个数据此变量就“置1”一次,在主函数里,相关
  60. * 的定时器就会被重新赋值,只要这个定时器能不断及时的被补充新的“能量”新的值,那么这个定时器
  61. * 就永远不会变成0,只要不变成0就不会超时。如果两个字节之间通信时间超过了固定的长度,就意味
  62. * 着此定时器变成了0,这时就需要把中断函数里的接收步骤Gu8Step及时切换到“接头暗号”的步骤。
  63. */
  64.                   Gu8ReceFeedDog=1; //每接收到一个字节的数据,此标志就置1及时更新定时器的值。
  65.                   switch(Gu8ReceStep)
  66.                   {
  67.                           case 0:     //“前部分的”数据头。接头暗号的步骤。
  68.                                    Gu8ReceBuffer[0]=SBUF; //直接读取刚接收完的一个字节的数据。
  69.                                    if(0xeb==Gu8ReceBuffer[0])  //等于数据头0xeb,接头暗号吻合。
  70.                                    {
  71.                                           Gu32ReceCnt=1; //接收缓存的下标
  72.                                           Gu8ReceStep=1;  //切换到下一个步骤,接收其它有效的数据
  73.                                    }
  74.                                    break;               
  75.                                        
  76.                           case 1:     //“前部分的”数据类型和长度
  77.                                    Gu8ReceBuffer[Gu32ReceCnt]=SBUF; //直接读取刚接收完的一个字节的数据。
  78.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备
  79.                                    if(Gu32ReceCnt>=6)  //前6个数据。接收完了“数据类型”和“数据长度”。
  80.                                    {
  81.                                             Gu8ReceType=Gu8ReceBuffer[1];  //提取“数据类型”
  82. //以下的数据转换,在第62节讲解过的指针法
  83.                                                 pu32Data=(unsigned long *)&Gu8ReceBuffer[2]; //数据转换
  84.                                                 Gu32ReceDataLength=*pu32Data; //提取“数据长度”
  85.                                             if(Gu32ReceCnt>=Gu32ReceDataLength) //靠“数据长度”来判断是否完成
  86.                                                 {
  87.                                                         Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  88.                                                         Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  89.                                                 }
  90.                                                 else   //如果还没结束,继续切换到下一个步骤,接收“有效数据”
  91.                                                 {
  92.                              //以下几行代码是本节的破题关键!!!
  93.                              if(0x02==Gu8ReceType) //如果是文件类,把指针关联到Gu8FileBuffer
  94.                               {
  95.                                    pGu8ReceBuffer=(unsigned char *)&Gu8FileBuffer[0];//下标0
  96. Gu32ReceCntMax=FILE_BUFFER_SIZE+6;  //最大缓存
  97. }
  98. else //如果是常规类,继续把指针关联到Gu8ReceBuffer本身的数组
  99.                               {
  100.                                    pGu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer[6];//下标6
  101. Gu32ReceCntMax=REC_BUFFER_SIZE;  //最大缓存
  102. }

  103.                                                         Gu8ReceStep=2;   //切换到下一个步骤
  104.                                                 }                                                       
  105.                                    }
  106.                                    break;               
  107.                           case 2:     //“后部分的”数据
  108.                                    pGu8ReceBuffer[Gu32ReceCnt-6]=SBUF; //这里的指针就是各种不同内存的化身!!!
  109.                                    Gu32ReceCnt++; //每接收一个字节,数组下标都自加1,为接收下一个数据做准备

  110. //靠“数据长度”来判断是否完成。也不允许超过数组的最大缓存的长度
  111.                                    if(Gu32ReceCnt>=Gu32ReceDataLength||Gu32ReceCnt>=Gu32ReceCntMax)
  112. {
  113.                                           Gu8FinishFlag=1; //接收完成标志“置1”,通知主函数处理。
  114.                                           Gu8ReceStep=0;   //及时切换回接头暗号的步骤
  115.                                    }
  116.                                    break;       
  117.                   }
  118.        }
  119.    }
  120.    else  //发送数据引起的中断
  121.    {
  122.         TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
  123.         //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
  124.    }                                                      
  125. }  


  126. void UsartTask(void)    //串口接收的任务函数,放在主函数内
  127. {
  128. static unsigned int *pSu16Data; //数据转换的指针
  129. static unsigned int Su16Data;  //转换后的数据
  130. static unsigned int i;
  131. static unsigned char Su8RecZZ=0;  //计算的“异或”



  132.     if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
  133.         {
  134.                 Gu8ReceFeedDog=0;
  135.                                
  136.                 vGu8ReceTimeOutFlag=0;
  137.         vGu16ReceTimeOutCnt=RECE_TIME_OUT;//更新一次“超时检测的定时器”的初值
  138.                 vGu8ReceTimeOutFlag=1;
  139.         }
  140.         else if(Gu8ReceStep>0&&0==vGu16ReceTimeOutCnt) //超时,并且步骤不在接头暗号的步骤
  141.         {
  142.             Gu8ReceStep=0; //串口接收数据的中断函数及时切换回接头暗号的步骤
  143.     }

  144.        
  145.         if(1==Gu8FinishFlag)  //1代表已经接收完毕一串新的数据,需要马上去处理
  146.         {
  147.                 switch(Gu8ReceType)  //接收到的数据类型
  148.                 {
  149.                         case 0x01:   //常规控制类的小内存。驱动蜂鸣器
  150. //以下的数据转换,在第62节讲解过的指针法

  151.                                  pSu16Data=(unsigned int *)&Gu8ReceBuffer[Gu32ReceDataLength-3]; //数据转换
  152. Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

  153. Gu8ReceZZ=Gu8ReceBuffer[Gu32ReceDataLength-1];  //提取接收到的“异或”

  154. Su8RecZZ=Gu8ReceBuffer[0]; //提取数据串第“i=0”个数据作为异或的原始数据
  155. for(i=1;i<(Gu32ReceDataLength-1);i++) //注意,这里是从第“i=1”个数据开始
  156. {
  157. Su8RecZZ=Su8RecZZ^Gu8ReceBuffer[i];   //计算“异或”
  158. }

  159. if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
  160. {
  161.                               pSu16Data=(unsigned int *)&Gu8ReceBuffer[6]; //数据转换。
  162.                                      Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

  163. vGu8BeepTimerFlag=0;  
  164. vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
  165. vGu8BeepTimerFlag=1;  
  166. }
  167.        
  168.                   break;

  169.                         case 0x02:   //文件类的大内存。驱动蜂鸣器。
  170. //以下的数据转换,在第62节讲解过的指针法

  171.                                  pSu16Data=(unsigned int *)&Gu8ReceBuffer[Gu32ReceDataLength-3]; //数据转换
  172. Gu16ReceYY=*pSu16Data; //提取“动态密匙”。本例子中暂时不做返回应答的处理

  173. //注意,请留意以下代码文件类内存数组Gu8FileBuffer的下标位置
  174. Gu8ReceZZ=Gu8FileBuffer[Gu32ReceDataLength-1-6];  //提取接收到的“异或”

  175. //前面6个字节是“前部分的”[数据头,数据类型,数据长度]
  176. Su8RecZZ=Gu8ReceBuffer[0]; //提取数据串第“i=0”个数据作为异或的原始数据
  177. for(i=1;i<6;i++) //注意,这里是从第“i=1”个数据开始
  178. {
  179. Su8RecZZ=Su8RecZZ^Gu8ReceBuffer[i];   //计算“前部分的”“异或”
  180. }

  181. //6个字节之后是“后部分的”“文件类专用的数据”
  182. for(i=0;i<(Gu32ReceDataLength-1-6);i++)
  183. {
  184. Su8RecZZ=Su8RecZZ^Gu8FileBuffer[i];   //计算“后部分的”“异或”
  185. }

  186. if(Su8RecZZ==Gu8ReceZZ) //验证“异或”,“计算的”与“接收的”是否一致
  187. {
  188.                               pSu16Data=(unsigned int *)&Gu8FileBuffer[0]; //数据转换。此处下标0!
  189.                                      Su16Data=*pSu16Data; //提取“蜂鸣器声音的长度”

  190. vGu8BeepTimerFlag=0;  
  191. vGu16BeepTimerCnt=Su16Data;   //让蜂鸣器鸣叫
  192. vGu8BeepTimerFlag=1;  
  193. }
  194.                
  195.                   break;



  196.         }
  197.         
  198.         Gu8FinishFlag=0;  //上面处理完数据再清零标志,为下一次接收新的数据做准备
  199.     }
  200. }

  201. void T0_time() interrupt 1     
  202. {
  203. VoiceScan();  

  204. if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
  205.         {
  206.                  vGu16ReceTimeOutCnt--;       
  207. }  

  208. TH0=0xfc;   
  209. TL0=0x66;   
  210. }


  211. void SystemInitial(void)
  212. {
  213. unsigned char u8_TMOD_Temp=0;

  214. //以下是定时器0的中断的配置
  215. TMOD=0x01;  
  216. TH0=0xfc;   
  217. TL0=0x66;   
  218. EA=1;      
  219. ET0=1;      
  220. TR0=1;   

  221. //以下是串口接收中断的配置
  222. //串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
  223. u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
  224. TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
  225. TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
  226. TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
  227. TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
  228. TR1=1;  //开启定时器1

  229. SM0=0;  
  230. SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
  231. REN=1;  //允许串口接收数据

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

  235. ES=1;         //允许串口中断  
  236. EA=1;         //允许总中断
  237. }

  238. void Delay(unsigned long u32DelayTime)
  239. {
  240.     for(;u32DelayTime>0;u32DelayTime--);
  241. }

  242. void PeripheralInitial(void)
  243. {
  244.    
  245. }

  246. void BeepOpen(void)
  247. {
  248. P3_4=0;  
  249. }

  250. void BeepClose(void)
  251. {
  252. P3_4=1;  
  253. }

  254. void VoiceScan(void)
  255. {

  256.           static unsigned char Su8Lock=0;  

  257. if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
  258.           {
  259.                   if(0==Su8Lock)
  260.                   {
  261.                    Su8Lock=1;  
  262. BeepOpen();
  263.      }
  264.     else  
  265. {     

  266.                        vGu16BeepTimerCnt--;         

  267.                    if(0==vGu16BeepTimerCnt)
  268.                    {
  269.                            Su8Lock=0;     
  270. BeepClose();  
  271.                    }

  272. }
  273.           }         
  274. }








本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

评论

是我搞错了。。坐等大神更新~  发表于 2018-12-9 17:56
鸿哥 这段代码有点不太懂:第一百三十一节: 灵活切换各种不同大小“ 接收内存” 的串口程序框架 case 2: //“ 后部分的” 数据 pGu8ReceBuffer[Gu32ReceCnt-6]=SBUF; //这里的指针就是各种不同内存的化身!!! Gu32ReceCnt++; //每接收一个字节, 数组下标都自加 1, 为接收下一个数据做准备 ****上面把接收的数据赋给直针变量?是不是错了,还有上面直针指向数组第0和第6个到这一步还是不太明白,XX YY的位置是不是写反了?   发表于 2018-12-8 09:28
ztzp 发表于 2018-10-17 15:34 | 显示全部楼层
谢谢,已下载收藏并学习中。
arima 发表于 2018-10-18 21:08 | 显示全部楼层
谢谢分享!
qw329811233 发表于 2018-10-30 01:49 | 显示全部楼层
embassy 发表于 2016-10-8 13:59
很简单的 c语言, 被你用的多么多么牛叉的样子.  大部分在工作中根本不需要这么复杂,      c语言本身就是很 ...

一个零基础的不去普及基础研究芯片有用吗哥们?

评论

你就是属于那种又菜又嘴硬的那种  发表于 2024-7-23 09:56
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部