你知道串口接收完数据,有几种判断方式吗?
导语:你知道串口接收完数据,有几种判断方式吗?你知道串口接收完数据,有几种判断方式吗
说起通信,首先想到的肯定是串口,日常中232和485的使用比比皆是,数据的发送、接收是串口通信最基础的内容。这篇文章主要讨论串口接收数据的断帧操作。
空闲中断断帧
一些mcu(如:stm32f103)在出厂时就已经在串口中封装好了一种中断——空闲帧中断,用户可以通过获取该中断标志位来判断数据是否接收完成,中断标志在中断服务函数中获取,使用起来相对简单。
void UART4_IRQHandler(void){ uint8_t data = 0; data = data; if(USART_GetITStatus(LoraUSARTx, USART_IT_RXNE) == SET) { USART_ClearITPendingBit(LoraUSARTx, USART_IT_RXNE); if(Lora_RecvData.Rx_over == 0) Lora_RecvData.RxBuf[Lora_RecvData.Rx_count++] = LoraUSARTx->DR; } if(USART_GetITStatus(LoraUSARTx, USART_IT_IDLE) == SET) { data = LoraUSARTx->SR; data = LoraUSARTx->DR; Lora_RecvData.Rx_over = 1; //接收完成 }}
例程中,当接收完成标志 Lora_RecvData.Rx_over 为1时,就可以获取 uart4 接收到的一帧数据,该数据存放在 Lora_RecvData.RxBuf 中。
超时断帧
空闲帧中断的使用固然方便,但是并不是每个mcu都有这种中断存在(只有个别高端mcu才有),那么这个时候就可以考虑使用超时断帧了。
Modbus协议中规定一帧数据的结束标志为3.5个字符时长,那么同样的可以把这种断帧方式类比到串口的接收上,这种方法需要搭配定时器使用。
其作用原理就是:串口进一次接收中断,就打开定时器超时中断,同时装载值清零(具体的装载值可以自行定义),只要触发了定时器的超时中断,说明在用户规定的时间间隔内串口接收中断里没有新的数据进来,可以认为数据接收完成。
详解 Modbus 通信协议(清晰易懂)
uint16_t Time3_CntValue = 0;//计数器初值 /******************************************************************************* * TIM3中断服务函数 ******************************************************************************/void Tim3_IRQHandler(void){ if(TRUE == Tim3_GetIntFlag(Tim3UevIrq)) { Tim3_M0_Stop(); //关闭定时器3 Uart0_Rec_Count = 0;//接收计数清零 Uart0_Rec_Flag = 1; //接收完成标志 Tim3_ClearIntFlag(Tim3UevIrq); //清除定时器中断 }} void Time3_Init(uint16_t Frame_Spacing){ uint16_t u16ArrValue;//自动重载值 uint32_t u32PclkValue;//PCLK频率 stc_tim3_mode0_cfg_t stcTim3BaseCfg; //结构体初始化清零 DDL_ZERO_STRUCT(stcTim3BaseCfg); Sysctrl_SetPeripheralGate(SysctrlPeripheralTim3, TRUE); //Base Timer外设时钟使能 stcTim3BaseCfg.enWorkMode = Tim3WorkMode0; //定时器模式 stcTim3BaseCfg.enCT = Tim3Timer; //定时器功能,计数时钟为内部PCLK stcTim3BaseCfg.enPRS = Tim3PCLKDiv1; //不分频 stcTim3BaseCfg.enCntMode = Tim316bitArrMode; //自动重载16位计数器/定时器 stcTim3BaseCfg.bEnTog = FALSE; stcTim3BaseCfg.bEnGate = FALSE; stcTim3BaseCfg.enGateP = Tim3GatePositive; Tim3_Mode0_Init(&stcTim3BaseCfg); //TIM3 的模式0功能初始化 u32PclkValue = Sysctrl_GetPClkFreq(); //获取Pclk的值 //u16ArrValue = 65535-(u32PclkValue/1000); //1ms测试 u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing*10)/RS485_BAUDRATE*u32PclkValue);//根据帧间隔计算超时时间 Time3_CntValue = u16ArrValue; //计数初值 Tim3_M0_ARRSet(u16ArrValue); //设置重载值 Tim3_M0_Cnt16Set(u16ArrValue); //设置计数初值 Tim3_ClearIntFlag(Tim3UevIrq); //清中断标志 Tim3_Mode0_EnableIrq(); //使能TIM3中断(模式0时只有一个中断) EnableNvic(TIM3_IRQn, IrqLevel3, TRUE); //TIM3 开中断 } /**************************此处省略串口初始化部分************************///串口0中断服务函数void Uart0_IRQHandler(void){ uint8_t rec_data=0; if(Uart_GetStatus(M0P_UART0, UartRC)) { Uart_ClrStatus(M0P_UART0, UartRC); rec_data = Uart_ReceiveData(M0P_UART0); if(Uart0_Rec_Count<UART0_BUFF_LENGTH)//帧长度 { Uart0_Rec_Buffer[Uart0_Rec_Count++] = rec_data; } Tim3_M0_Cnt16Set(Time3_CntValue);//设置计数初值 Tim3_M0_Run(); //开启定时器3 超时即认为一帧接收完成 }}
例程所用的是华大的hc32l130系列mcu,其它类型的mcu也可以参考这种写法。其中超时时间的计算尤其要注意数据类型的问题,u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing * 10)/RS485_BAUDRATE * u32PclkValue);其中Frame_Spacing为用户设置的字符个数,uart模式为一个“1+8+1”共10bits。
状态机断帧
状态机,状态机,又是状态机,没办法!谁让它使用起来方便呢?其实这种方法我用的也不多,但是状态机的思想还是要有的,很多逻辑用状态机梳理起来会更加的清晰。
相对于超时断帧,状态机断帧的方法节约了一个定时器资源,一般的mcu外设资源是足够的,但是做一些资源冗余也未尝不是一件好事,万一呢?对吧。
嵌入式软件状态机的实现方法
//状态机断帧void UART_IRQHandler(void) //作为485的接收中断{ uint8_t count = 0; unsigned char lRecDat = 0; if(/*触发接收中断标志*/) { //清中断状态位 rec_timeout = 5; if((count == 0)) //接收数据头,长度可以自定义 { RUart0485_DataC[count++] = /*串口接收到的数据*/; gRecStartFlag = 1; return; } if(gRecStartFlag == 1) { RUart0485_DataC[count++] = /*串口接收到的数据*/; if(count > MAXLEN) //一帧数据接收完成 { count=0; gRecStartFlag = 0; if(RUart0485_DataC[MAXLEN]==CRC16(RUart0485_DataC,MAXLEN)) { memcpy(&gRecFinshData,RUart0485_DataC,13); gRcvFlag = 1; //接收完成标志位 } } } return; } return ;}
这种做法适合用在一直有数据接收的场合,每次接收完一帧有效数据后就把数据放到缓冲区中去解析,同时还不影响下一帧数据的接收。
整个接收状态分为两个状态——接收数据头和接收数据块,如果一帧数据存在多个部分的话还可以在此基础上再增加几种状态,这样不仅可以提高数据接收的实时性,还能够随时看到数据接收到哪一部分,还是比较实用的。
&34;断帧
记得刚毕业面试的时候,面试官还问过我一个问题:如果串口有大量数据要接收,同时又没有空闲帧中断你会怎么做?
没错,就是FIFO(当时并没有回答上来,因为没用过),说白了就是开辟一个缓冲区,每次接收到的数据都放到这个缓冲区里,同时记录数据在缓冲区中的位置,当数据到达要求的长度的时候再把数据取出来,然后放到状态机中去解析。
当然FIFO的使用场合有很多,很多数据处理都可以用FIFO去做,有兴趣的可以多去了解一下。
/********************串口初始化省略,华大mcu hc32l130******************/void Uart1_IRQHandler(void){ uint8_t data; if(Uart_GetStatus(M0P_UART1, UartRC)) //UART0数据接收 { Uart_ClrStatus(M0P_UART1, UartRC); //清中断状态位 data = Uart_ReceiveData(M0P_UART1); //接收数据字节 comFIFO(&data,1); } } /******************************FIFO*******************************/volatile uint8_t fifodata[FIFOLEN],fifoempty,fifofull;volatile uint8_t uart_datatemp=0; uint8_t comFIFO(uint8_t *data,uint8_t cmd){ static uint8_t rpos=0; //当前写的位置 position 0--99 static uint8_t wpos=0; //当前读的位置 if(cmd==0) //写数据 { if(fifoempty!=0) //1 表示有数据 不为空,0表示空 { *data=fifodata[rpos]; fifofull=0; rpos++; if(rpos==FIFOLEN) rpos=0; if(rpos==wpos) fifoempty=0; return 0x01; } else return 0x00; } else if(cmd==1) //读数据 { if(fifofull==0) { fifodata[wpos]=*data; fifoempty=1; wpos++; if(wpos==FIFOLEN) wpos=0; if(wpos==rpos) fifofull=1; return 0x01; } else return 0x00; } return 0x02;} /********************************状态机处理*******************************/void LoopFor485ReadCom(void){ uint8_t data; while(comFIFO(&data,0)==0x01) { if(rEadFlag==SAVE_HEADER_STATUS) //读取头 { if(data==Header_H) { buffread[0]=data; continue; } if(data==Header_L) { buffread[1]=data; if(buffread[0]==Header_H) { rEadFlag=SAVE_DATA_STATUS; } } else { memset(buffread,0,Length_Data); } } else if(rEadFlag==SAVE_DATA_STATUS) //读取数据 { buffread[i485+2]=data; i485++; if(i485==(Length_Data-2)) //数据帧除去头 { unsigned short crc16=CRC16_MODBUS(buffread,Length_Data-2); if((buffread[Length_Data-2]==(crc16>>8))&&(buffread[Length_Data-1]==(crc16&0xff))) { rEadFlag=SAVE_OVER_STATUS; memcpy(&cmddata,buffread,Length_Data); //拷贝Length_Struct个字节,完整的结构体 } else { rEadFlag=SAVE_HEADER_STATUS; } memset(buffread,0,Length_Data); i485=0; break; } } }}
好了,就这些吧,如果有需要补充的地方,欢迎各位看官给出宝贵意见,相互学习,共同进步!