說起通信,首先想到的肯定是串口,日常中232和485的使用比比皆是,數據的發送、接收是串口通信最基礎的內容。這篇文章主要討論串口接收數據的斷幀操作。
空閑中斷斷幀
一些mcu(如:stm32f103)在出廠時就已經在串口中封裝好了一種中斷——空閑幀中斷,用戶可以通過獲取該中斷標志位來判斷數據是否接收完成,中斷標志在中斷服務函數中獲取,使用起來相對簡單。
例程中,當接收完成標志 Lora_RecvData.Rx_over 為1時,就可以獲取 uart4 接收到的一幀數據,該數據存放在 Lora_RecvData.RxBuf 中。
超時斷幀
空閑幀中斷的使用固然方便,但是并不是每個mcu都有這種中斷存在(只有個別高端mcu才有),那么這個時候就可以考慮使用超時斷幀了。
Modbus協議中規定一幀數據的結束標志為3.5個字符時長,那么同樣的可以把這種斷幀方式類比到串口的接收上,這種方法需要搭配定時器使用。
其作用原理就是:串口進一次接收中斷,就打開定時器超時中斷,同時裝載值清零(具體的裝載值可以自行定義),只要觸發了定時器的超時中斷,說明在用戶規定的時間間隔內串口接收中斷里沒有新的數據進來,可以認為數據接收完成。
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_Spacing10)/RS485_BAUDRATEu32PclkValue);//根據幀間隔計算超時時間
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 ;
}
這種做法適合用在一直有數據接收的場合,每次接收完一幀有效數據后就把數據放到緩沖區中去解析,同時還不影響下一幀數據的接收。
整個接收狀態分為兩個狀態——接收數據頭和接收數據塊,如果一幀數據存在多個部分的話還可以在此基礎上再增加幾種狀態,這樣不僅可以提高數據接收的實時性,還能夠隨時看到數據接收到哪一部分,還是比較實用的。
"狀態機+FIFO"斷幀
如果串口有大量數據要接收,同時又沒有空閑幀中斷你會怎么做?
沒錯,就是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;} elsereturn 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;} elsereturn 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;}}
}
}