说起通信,首先想到的肯定是串口,日常中232和485的使用比比皆是,数据的发送、接收是串口通信最基础的内容。这篇文章主要讨论串口接收数据的断帧操作。

空闲中断断帧

一些mcu(如:stm32f103)在出厂时就已经在串口中封装好了一种中断——空闲帧中断,用户可以通过获取该中断标志位来判断数据是否接收完成,中断标志在中断服务函数中获取,使用起来相对简单。
  1. void UART4_IRQHandler(void)
  2. {
  3.     uint8_t data = 0;
  4.     data = data;
  5.     if(USART_GetITStatus(LoraUSARTx, USART_IT_RXNE) == SET)
  6.     {
  7.         USART_ClearITPendingBit(LoraUSARTx, USART_IT_RXNE);
  8.         if(Lora_RecvData.Rx_over == 0)
  9.             Lora_RecvData.RxBuf[Lora_RecvData.Rx_count++] = LoraUSARTx->DR;
  10.     }
  11.     if(USART_GetITStatus(LoraUSARTx, USART_IT_IDLE) == SET)
  12.     {
  13.         data = LoraUSARTx->SR;
  14.         data = LoraUSARTx->DR;
  15.         
  16.         Lora_RecvData.Rx_over = 1; //接收完成
  17.     }
  18. }
例程中,当接收完成标志 Lora_RecvData.Rx_over 为1时,就可以获取 uart4 接收到的一帧数据,该数据存放在 Lora_RecvData.RxBuf 中。

超时断帧
空闲帧中断的使用固然方便,但是并不是每个mcu都有这种中断存在(只有个别高端mcu才有),那么这个时候就可以考虑使用超时断帧了。
Modbus协议中规定一帧数据的结束标志为3.5个字符时长,那么同样的可以把这种断帧方式类比到串口的接收上,这种方法需要搭配定时器使用。
其作用原理就是:串口进一次接收中断,就打开定时器超时中断,同时装载值清零(具体的装载值可以自行定义),只要触发了定时器的超时中断,说明在用户规定的时间间隔内串口接收中断里没有新的数据进来,可以认为数据接收完成。
  1. uint16_t Time3_CntValue = 0;//计数器初值
  2. /*******************************************************************************
  3. * TIM3中断服务函数
  4. ******************************************************************************/
  5. void Tim3_IRQHandler(void)
  6. {
  7.     if(TRUE == Tim3_GetIntFlag(Tim3UevIrq))
  8.     {
  9.         Tim3_M0_Stop();    //关闭定时器3
  10.         Uart0_Rec_Count = 0;//接收计数清零
  11.         Uart0_Rec_Flag = 1; //接收完成标志
  12.         Tim3_ClearIntFlag(Tim3UevIrq); //清除定时器中断
  13.     }
  14. }
  15. void Time3_Init(uint16_t Frame_Spacing)
  16. {
  17.     uint16_t u16ArrValue;//自动重载值
  18.     uint32_t u32PclkValue;//PCLK频率
  19.    
  20.     stc_tim3_mode0_cfg_t     stcTim3BaseCfg;
  21.    
  22.     //结构体初始化清零
  23.     DDL_ZERO_STRUCT(stcTim3BaseCfg);
  24.    
  25.     Sysctrl_SetPeripheralGate(SysctrlPeripheralTim3, TRUE); //Base Timer外设时钟使能
  26.    
  27.     stcTim3BaseCfg.enWorkMode = Tim3WorkMode0;              //定时器模式
  28.     stcTim3BaseCfg.enCT       = Tim3Timer;                  //定时器功能,计数时钟为内部PCLK
  29.     stcTim3BaseCfg.enPRS      = Tim3PCLKDiv1;               //不分频
  30.     stcTim3BaseCfg.enCntMode  = Tim316bitArrMode;           //自动重载16位计数器/定时器
  31.     stcTim3BaseCfg.bEnTog     = FALSE;
  32.     stcTim3BaseCfg.bEnGate    = FALSE;
  33.     stcTim3BaseCfg.enGateP    = Tim3GatePositive;
  34.    
  35.     Tim3_Mode0_Init(&stcTim3BaseCfg);             //TIM3 的模式0功能初始化
  36.         
  37.     u32PclkValue = Sysctrl_GetPClkFreq();          //获取Pclk的值
  38.    //u16ArrValue = 65535-(u32PclkValue/1000);      //1ms测试
  39.     u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing*10)/RS485_BAUDRATE*u32PclkValue);//根据帧间隔计算超时时间
  40.     Time3_CntValue = u16ArrValue;             //计数初值
  41.     Tim3_M0_ARRSet(u16ArrValue);              //设置重载值
  42.     Tim3_M0_Cnt16Set(u16ArrValue);            //设置计数初值
  43.    
  44.     Tim3_ClearIntFlag(Tim3UevIrq);            //清中断标志
  45.     Tim3_Mode0_EnableIrq();                   //使能TIM3中断(模式0时只有一个中断)
  46.     EnableNvic(TIM3_IRQn, IrqLevel3, TRUE);   //TIM3 开中断  
  47. }
  48. /**************************此处省略串口初始化部分************************/
  49. //串口0中断服务函数
  50. void Uart0_IRQHandler(void)
  51. {
  52.     uint8_t rec_data=0;
  53.    
  54.     if(Uart_GetStatus(M0P_UART0, UartRC))         
  55.     {
  56.         Uart_ClrStatus(M0P_UART0, UartRC);        
  57.         rec_data = Uart_ReceiveData(M0P_UART0);     
  58.         if(Uart0_Rec_Count<UART0_BUFF_LENGTH)//帧长度
  59.         {
  60.             Uart0_Rec_Buffer[Uart0_Rec_Count++] = rec_data;        
  61.         }
  62.         Tim3_M0_Cnt16Set(Time3_CntValue);//设置计数初值
  63.         Tim3_M0_Run();   //开启定时器3 超时即认为一帧接收完成
  64.     }
  65. }
例程所用的是华大的hc32l130系列mcu,其它类型的mcu也可以参考这种写法。其中超时时间的计算尤其要注意数据类型的问题,u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing * 10)/RS485_BAUDRATE * u32PclkValue);其中Frame_Spacing为用户设置的字符个数,uart模式为一个“1+8+1”共10bits。

状态机断帧
状态机,状态机,又是状态机,没办法!谁让它使用起来方便呢?其实这种方法我用的也不多,但是状态机的思想还是要有的,很多逻辑用状态机梳理起来会更加的清晰。
相对于超时断帧,状态机断帧的方法节约了一个定时器资源,一般的mcu外设资源是足够的,但是做一些资源冗余也未尝不是一件好事,万一呢?对吧。
  1. //状态机断帧
  2. void UART_IRQHandler(void)  //作为485的接收中断
  3. {
  4.     uint8_t count = 0;
  5.     unsigned char lRecDat = 0;
  6.     if(/*触发接收中断标志*/)  
  7.     {
  8.         //清中断状态位
  9.         rec_timeout = 5;
  10.         if((count == 0)) //接收数据头,长度可以自定义
  11.         {
  12.             RUart0485_DataC[count++] = /*串口接收到的数据*/;
  13.             gRecStartFlag = 1;
  14.             return;
  15.         }
  16.         if(gRecStartFlag == 1)
  17.         {
  18.             RUart0485_DataC[count++] = /*串口接收到的数据*/;
  19.         
  20.             if(count > MAXLEN) //一帧数据接收完成
  21.             {
  22.                 count=0;
  23.                 gRecStartFlag = 0;
  24.                
  25.                 if(RUart0485_DataC[MAXLEN]==CRC16(RUart0485_DataC,MAXLEN))
  26.                 {
  27.                     memcpy(&gRecFinshData,RUart0485_DataC,13);
  28.                     gRcvFlag = 1; //接收完成标志位                    
  29.                 }
  30.             }   
  31.         }
  32.         return;
  33.     }
  34.     return ;
  35. }
这种做法适合用在一直有数据接收的场合,每次接收完一帧有效数据后就把数据放到缓冲区中去解析,同时还不影响下一帧数据的接收。
整个接收状态分为两个状态——接收数据头和接收数据块,如果一帧数据存在多个部分的话还可以在此基础上再增加几种状态,这样不仅可以提高数据接收的实时性,还能够随时看到数据接收到哪一部分,还是比较实用的。

"状态机+FIFO"断帧
记得刚毕业面试的时候,面试官还问过我一个问题:如果串口有大量数据要接收,同时又没有空闲帧中断你会怎么做?
没错,就是FIFO(当时并没有回答上来,因为没用过),说白了就是开辟一个缓冲区,每次接收到的数据都放到这个缓冲区里,同时记录数据在缓冲区中的位置,当数据到达要求的长度的时候再把数据取出来,然后放到状态机中去解析。
当然FIFO的使用场合有很多,很多数据处理都可以用FIFO去做,有兴趣的可以多去了解一下。
  1. /********************串口初始化省略,华大mcu hc32l130******************/
  2. void Uart1_IRQHandler(void)
  3. {
  4.     uint8_t data;
  5.     if(Uart_GetStatus(M0P_UART1, UartRC))      //UART0数据接收
  6.     {
  7.         Uart_ClrStatus(M0P_UART1, UartRC);    //清中断状态位
  8.         data = Uart_ReceiveData(M0P_UART1);   //接收数据字节
  9.         comFIFO(&data,1);
  10.     }
  11. }
  12. /******************************FIFO*******************************/
  13. volatile uint8_t     fifodata[FIFOLEN],fifoempty,fifofull;
  14. volatile uint8_t     uart_datatemp=0;
  15. uint8_t comFIFO(uint8_t *data,uint8_t cmd)
  16. {
  17.     static uint8_t rpos=0;  //当前写的位置 position 0--99
  18.     static uint8_t wpos=0; //当前读的位置
  19.     if(cmd==0) //写数据
  20.     {
  21.         if(fifoempty!=0)       //1 表示有数据 不为空,0表示空
  22.         {
  23.             *data=fifodata[rpos];
  24.             fifofull=0;
  25.             rpos++;
  26.             if(rpos==FIFOLEN)
  27.                 rpos=0;
  28.             if(rpos==wpos)
  29.                 fifoempty=0;
  30.             return 0x01;
  31.         }
  32.         else
  33.             return 0x00;
  34.     }
  35.     else if(cmd==1) //读数据
  36.     {
  37.         if(fifofull==0)
  38.         {
  39.             fifodata[wpos]=*data;
  40.             fifoempty=1;
  41.             wpos++;
  42.             if(wpos==FIFOLEN)
  43.                 wpos=0;
  44.             if(wpos==rpos)
  45.                 fifofull=1;
  46.             return 0x01;
  47.         } else
  48.             return 0x00;
  49.     }
  50.     return 0x02;
  51. }
  52. /********************************状态机处理*******************************/
  53. void LoopFor485ReadCom(void)
  54. {
  55.     uint8_t data;
  56.     while(comFIFO(&data,0)==0x01)
  57.     {
  58.         if(rEadFlag==SAVE_HEADER_STATUS) //读取头
  59.         {
  60.             if(data==Header_H)
  61.             {
  62.                 buffread[0]=data;
  63.                 continue;
  64.             }
  65.             if(data==Header_L)
  66.             {
  67.                 buffread[1]=data;
  68.                 if(buffread[0]==Header_H)
  69.                 {
  70.                     rEadFlag=SAVE_DATA_STATUS;
  71.                 }
  72.             }
  73.             else
  74.             {
  75.                 memset(buffread,0,Length_Data);
  76.             }
  77.         }
  78.         else if(rEadFlag==SAVE_DATA_STATUS)  //读取数据
  79.         {
  80.             buffread[i485+2]=data;
  81.             i485++;
  82.             if(i485==(Length_Data-2)) //数据帧除去头
  83.             {
  84.                 unsigned short crc16=CRC16_MODBUS(buffread,Length_Data-2);
  85.                 if((buffread[Length_Data-2]==(crc16>>8))&&(buffread[Length_Data-1]==(crc16&0xff)))
  86.                 {
  87.                     rEadFlag=SAVE_OVER_STATUS;
  88.                     memcpy(&cmddata,buffread,Length_Data);  //拷贝Length_Struct个字节,完整的结构体
  89.                 }
  90.                 else
  91.                 {
  92.                     rEadFlag=SAVE_HEADER_STATUS;
  93.                 }
  94.         
  95.                 memset(buffread,0,Length_Data);
  96.                 i485=0;
  97.                 break;
  98.             }
  99.         }
  100.     }
  101. }