STM32簡單數據傳輸方法與通信協議(適合串口和一般總線)

引言

在一般的項目開發過程中,往往需要兩塊或以上單片機進行通信完成數據傳輸,例如四旋翼無人機在飛行過程中無線傳輸數據回到地面站,治療儀器需要實時將患者和機器運轉情況傳回上位機平臺,糧倉溫控裝置需將各種傳感器通過RS485總線或者CAN總線的方式達到數據傳輸的目的等等,這些數據傳輸往往需要合適穩定的總線和靈活的通信協議,我發現無論什麼數據傳輸,原理大同小異,這裏簡單以stm32的幾種數據傳輸總結下平時項目中用的一些傳輸方法。

通信協議

簡單情況(如一對一)

首先在數據傳輸前一定要想好通信協議,如果傳輸的數據和過程非常簡單,那麼就可以採用簡單的傳輸協議,例如:
這裏寫圖片描述
直接上代碼:

int temp;   
u8 RS485_receive_str[128];   //接收緩衝,最大128個字節.
u8 uart_byte_count=0;        //接收到的數據長度
        ...
/****************************************************************************
* void RS485_Receive_Data(u8 *buf,u8 *len)
* RS485查詢接收到的數據
* 入口參數:buf:接收緩存首地址
            len:讀到的數據長度   
****************************************************************************/
void RS485_Receive_Data(u8 *buf,u8 *len)
{
    u8 rxlen=uart_byte_count;
    u8 i=0;
    *len=0;                //默認爲0
    delay_ms(10);        //等待10ms,連續超過10ms沒有接收到一個數據,則認爲接收結束

    if(rxlen==uart_byte_count&&rxlen) //接收到了數據,且接收完成了
    {
        for(i=0;i<rxlen;i++)
        {
            buf[i]=RS485_receive_str[i];    
        }       
        *len=uart_byte_count;   //記錄本次數據長度
        uart_byte_count=0;        //清零
    }
}
//接收中斷服務函數
int state=0;
void USART2_IRQHandler(void)
{
    u8 rec_data;        
    if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)//接收到數據
    {       
        rec_data =(u8)USART_ReceiveData(USART2);                 //(USART2->DR) 讀取接收到的數據
        if(rec_data=='S'&&state==0)                              //如果是S,表示是命令信息的起始位
        {
            state=1;
            uart_byte_count=0x00; 
        }else if(rec_data=='E'&&state==2)                         //如果E,表示是命令信息傳送的結束位並開始處理數據
        {
            state=0;
            if(RS485_receive_str[0]==0x00)                      //判斷地址 地址正確
            {
                if(RS485_receive_str[1]==0x02)                  //接受溫度數據
                {
                    temp=RS485_receive_str[5]<<24|RS485_receive_str[2]|RS485_receive_str[3]<<8|RS485_receive_str[4]<<16;

                }   else if(RS485_receive_str[1]==0x03)         //led控制回饋
                {
                    led=RS485_receive_str[2];

                }
            }
        }else if(state==1)                                  //一位位接收數據並裝入緩存
        {
            RS485_receive_str[uart_byte_count++]=rec_data;
            if(uart_byte_count==6)
                state=2;
        }
    }                                            
} 

這樣的傳輸協議往往在兩個一對一的傳輸中比較好用,主要在接受緩存部分使用了狀態機機制,並且定義了簡單的幀頭和結束幀,顯然這樣的通信協議並不可靠,遇到複雜的情況就不好辦了。

複雜情況

複雜情況的協議可以先制定協議表,再做細分,幀頭+功能字+長度+數據+校驗位,這樣的協議既能滿足多功能的場合也能避免數據過多出現錯誤,比較通用。
例如 GPS定位下位機協議:
這裏寫圖片描述
這裏寫圖片描述

遙控上位機協議:

這裏寫圖片描述
這裏寫圖片描述

  • SUM所有字節的和:等於從該數據幀第一字節開始,也就是幀頭開始,至該幀數據的最後一字節所有字節的和,只保留低八位,高位捨去。
  • LEN有效數據長度:表示該數據幀內包含數據的字節長度,(所有數據 除了:幀頭、功能字、長度字節和最後的校驗位),只是數據的字節長度和。
    比如該幀數據內容爲3個int16型數據,那麼會以6個char形式發送,那麼LEN等於6
  • 返回校驗是YES的,飛控在收到該幀數據後,需要立即返回CHECK數據幀,也就是AAAAEF數據幀。

設置傳輸速度

一般選用儘可能低的傳輸速度下滿足通信,對於無線數傳來說,傳輸速度越低意味着越遠的傳輸距離。
例如通信的波特率爲38400等等。

代碼實現

由於前面定義了適合的通信協議,所以在代碼部分也必須嚴格按照用通信協議進行編寫

宏定義

在數據傳輸.c文件中,可以預先宏定義一些固定格式的轉換或者標誌位,例如下面這樣:

/* 數據拆分宏定義,在發送大於8位的數據類型時,比如int16、int32等,需要把數據拆分成8位逐個發送 */
#define BYTE0(dwTemp)       ( *( (char *)(&dwTemp) + 0) )
#define BYTE1(dwTemp)       ( *( (char *)(&dwTemp) + 1) )
#define BYTE2(dwTemp)       ( *( (char *)(&dwTemp) + 2) )
#define BYTE3(dwTemp)       ( *( (char *)(&dwTemp) + 3) )
/* 發送幀頭 接收幀頭*/
#define title1_send 0xAA
#define title2_send 0xAA
#define title1_received 0xAA
#define title2_received 0xAF
/* 等待發送數據的標誌 */
u8 wait_for_translate;
/* 等待發送數據的標誌 */
dt_flag_t f;
/* 發送數據緩存數組 */
u8 data_to_send[50];
/* 是否寫入並保存數據 */
u16 flash_save_en_cnt = 0;

數據發送

/*----------------------------------------------------------
 + 實現功能:數傳數據發送
 + 調用參數:要發送的數據組 數據長度
----------------------------------------------------------*/
void DT_Send_Data(u8 *dataToSend , u8 length)
{
    /* 串口2發送 要發送的數據組 數據長度 */
    if(wait_for_translate)
        Usart2_Send(data_to_send, length);
}

/*----------------------------------------------------------
 + 實現功能:校驗累加和回傳
 + 調用參數:字幀 校驗累加和
----------------------------------------------------------*/
static void DT_Send_Check(u8 head, u8 check_sum)
{
    /* 數據內容 */
    data_to_send[0]=title1_send;
    data_to_send[1]=title2_send;
    data_to_send[2]=0xEF;
    data_to_send[3]=2;
    data_to_send[4]=head;
    data_to_send[5]=check_sum;

    /* 校驗累加和計算 */
    u8 sum = 0;
    for(u8 i=0; i<6; i++)
        sum += data_to_send[i];
    data_to_send[6]=sum;
    /* 發送 要發送的數據組 數據長度 */
    DT_Send_Data(data_to_send, 7);
}

/*----------------------------------------------------------
 + 實現功能:發送速度信息
 + 調用參數:向北速度 向西速度 向上速度 單位毫米每秒
----------------------------------------------------------*/
void DT_Send_Speed(float x_s,float y_s,float z_s)
{
    u8 _cnt=0;
    vs16 _temp;

    data_to_send[_cnt++]=title1_send;
    data_to_send[_cnt++]=title2_send;
    data_to_send[_cnt++]=0x0B;
    data_to_send[_cnt++]=0;

    _temp = (int)(x_s*100.0f);
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);
    _temp = (int)(y_s*100.0f);
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);
    _temp = (int)(z_s*100.0f);
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);


    data_to_send[3] = _cnt-4;

    u8 sum = 0;
    for(u8 i=0; i<_cnt; i++)
        sum += data_to_send[i];
    data_to_send[_cnt++]=sum;

    DT_Send_Data(data_to_send, _cnt);
}

/*----------------------------------------------------------
 + 實現功能:發送高度信息
 + 調用參數:發送氣壓計高度 超聲波高度 發送單位釐米
----------------------------------------------------------*/
void DT_Send_Senser2(s32 bar_alt,u16 csb_alt)
{
    u8 _cnt=0;

    data_to_send[_cnt++]=title1_send;
    data_to_send[_cnt++]=title2_send;
    data_to_send[_cnt++]=0x07;
    data_to_send[_cnt++]=0;

    data_to_send[_cnt++]=BYTE3(bar_alt);
    data_to_send[_cnt++]=BYTE2(bar_alt);
    data_to_send[_cnt++]=BYTE1(bar_alt);
    data_to_send[_cnt++]=BYTE0(bar_alt);

    data_to_send[_cnt++]=BYTE1(csb_alt);
    data_to_send[_cnt++]=BYTE0(csb_alt);

    data_to_send[3] = _cnt-4;

    u8 sum = 0;
    for(u8 i=0; i<_cnt; i++)
        sum += data_to_send[i];
    data_to_send[_cnt++] = sum;

    DT_Send_Data(data_to_send, _cnt);
}
/*----------------------------------------------------------
 + 實現功能:自定義發送
----------------------------------------------------------*/
void DT_Send_User()
{
    u8 _cnt=0;
    vs16 _temp;

    data_to_send[_cnt++]=title1_send;
    data_to_send[_cnt++]=title2_send;
    data_to_send[_cnt++]=0xf1; //用戶定義功能字
    data_to_send[_cnt++]=0;

    _temp = 0;           //1
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);

    _temp = 0;           //1
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);

    _temp = 0;
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);

    _temp = 0;
    data_to_send[_cnt++]=BYTE1(_temp);
    data_to_send[_cnt++]=BYTE0(_temp);

    data_to_send[3] = _cnt-4;

    u8 sum = 0;
    for(u8 i=0; i<_cnt; i++)
        sum += data_to_send[i];

    data_to_send[_cnt++]=sum;

    DT_Send_Data(data_to_send, _cnt);
}

/*----------------------------------------------------------
 + 實現功能:任務調度調用週期1ms
----------------------------------------------------------*/
void Call_Data_transfer(void)
{
    /* 定義局部靜態變量控制發送週期 */
    static int cnt = 0;
    /* cnt是從1到10000的數據 */
    if(++cnt>10000) cnt = 1;
    /* 1發送姿態數據,週期49ms */
    if((cnt % 49) == 0)
      //  f.send_status = 1;
          f.send_senser2 = 1;
    /* 2發送速度數據,週期199ms */
    if((cnt % 199) == 0)
        f.send_speed = 1;
        ...
    /* 6發送高度數據,週期399ms */
    if((cnt % 399) == 0)
     //   f.send_senser2 = 1;
          f.send_status = 1;

    /* 1發送姿態數據,週期49ms */
    if(f.send_status)
    {
        f.send_status = 0;
        /* 橫滾、俯仰、航向、氣壓cm高度、控制高度模式、解鎖狀態 */
        DT_Send_Status(IMU_Roll,IMU_Pitch,IMU_Yaw,(0.1f *baro_height),height_ctrl_mode,unlocked_to_fly);
    }
    /* 2發送速度數據,週期199ms */
    else if(f.send_speed)
    {
        f.send_speed = 0;
        /* 向北速度 向西速度 向上速度 單位毫米每秒 */
        DT_Send_Speed(0.1f *north_speed,0.1f *west_speed,0.1f *wz_speed);
    }
...

    /* 6發送高度數據 */
    else if(f.send_senser2)
    {
        f.send_senser2 = 0;
        /* 發送氣壓計高度 超聲波高度 發送單位釐米 */
        DT_Send_Senser2(baro_height*0.1f,ultra_distance/10);
    }
...
}

數據接收

那麼如何對接收到的數據解析?每次接收到的數據長度是多少?

一般寫個USART2_IRQHandler類似函數爲接收中斷,系統會自動調用每次只能接收到單字節數據,通過中斷的方式調用函數DT_Data_Receive_Prepare將接收到的數據完整的組合在一起

/*----------------------------------------------------------
 + 實現功能:串口發送數據
 + 中斷調用
----------------------------------------------------------*/
void USART2_IRQHandler(void)
{
    /* 接收數據臨時變量 */
    u8 com_data;

    /* 判斷過載錯誤中斷 */
    if(USART2->SR & USART_SR_ORE)
        com_data = USART2->DR;

    /* 判斷是否接收中斷 */
    if( USART_GetITStatus(USART2,USART_IT_RXNE) )
    {
        /* 清除中斷標誌 */
        USART_ClearITPendingBit(USART2,USART_IT_RXNE);

        /* 接收數據及後續的任務 */
        com_data = USART2->DR;

        /* 數傳數據處理解析 */
        DT_Data_Receive_Prepare(com_data);
    }

接受數據過程中怎樣處理接收數據的狀態?如何對接收到的數據判斷、校驗?

通過Mooer狀態機的方式:
Mooer狀態機的輸出只與當前的狀態有關,也就是數當前的狀態決定輸出,輸入只決定狀態機的狀態改變。
如何數據校驗:當判斷輸入數據無效時重新等待判斷下一幀數據


/*----------------------------------------------------------
 + 實現功能:數據接收並保存
 + 調用參數:接收到的單字節數據
----------------------------------------------------------*/
void DT_Data_Receive_Prepare(u8 data)
{
    /* 局部靜態變量:接收緩存 */
    static u8 RxBuffer[50];
    /* 數據長度 *//* 數據數組下標 */
    static u8 _data_len = 0,_data_cnt = 0;
    /* 接收狀態 */
    static u8 state = 0;

    /* 幀頭1  一個數據幀中第一個數據並且判斷是否與宏定義幀頭1相等*/        
    if(state==0&&data==title1_received)
    {
        state=1;
        RxBuffer[0]=data;
    }
    /* 幀頭2 一個數據幀中第二個數據並且判斷是否與宏定義幀頭2相等*/
    else if(state==1&&data==title2_received)
    {
        state=2;
        RxBuffer[1]=data;
    }
    /* 功能字 */
    else if(state==2&&data<0XF1)
    {
        state=3;
        RxBuffer[2]=data;
    }
    /* 長度 */
    else if(state==3&&data<50)
    {
        state = 4;
        RxBuffer[3]=data;
        _data_len = data;
        _data_cnt = 0;
    }
    /* 接收數據組*/
    else if(state==4&&_data_len>0)
    {
        _data_len--;
        RxBuffer[4+_data_cnt++]=data;
        if(_data_len==0)
            state = 5;
    }
    /* 校驗累加和 */
    else if(state==5)
    {
        state = 0;
        RxBuffer[4+_data_cnt]=data;
        DT_Data_Receive_Anl(RxBuffer,_data_cnt+5);  //調用數據分析函數,總長比索引+1
    }
    /* 若有錯誤重新等待接收幀頭 */
    else
        state = 0;
}
/*----------------------------------------------------------
 + 實現功能:數據分析
 + 調用參數:傳入接受到的一個數據幀和長度
----------------------------------------------------------*/
void DT_Data_Receive_Anl(u8 *data_buf,u8 num)
{
    u8 sum = 0;
    /* 首先計算校驗累加和 */
    for(u8 i=0; i<(num-1); i++)
        sum += *(data_buf+i);
    /* 判斷校驗累加和 若不同則捨棄*/
    if(!(sum==*(data_buf+num-1)))       return;
    /* 判斷幀頭 */
    if(!(*(data_buf)==title1_received && *(data_buf+1)==title2_received))       return;
    /* 判斷功能字:主要命令集 */
    if(*(data_buf+2)==0X01)
    {
        /* 加速度計校準 */
        if(*(data_buf+4)==0X01)
        {
            mpu6050.Acc_CALIBRATE = 1;
            start_height=0;
        }
        /* 陀螺儀校準 */
        else if(*(data_buf+4)==0X02)
        {
            mpu6050.Gyro_CALIBRATE = 1;
            start_height=0;
        }
...
    }
    /* 判斷功能字:次要命令集 */
    if(*(data_buf+2)==0X02)
    {
     ...
    }
    /* 判斷功能字 接收數據 */
    if(*(data_buf+2)==0X03)
    {
   ...
    }

    /* 回傳校驗累加和 */
    if(*(data_buf+2)==0X14)
    {
        DT_Send_Check(*(data_buf+2),sum);
    }
    /* 回傳校驗累加和 */
    if(*(data_buf+2)==0X15)
    {
        DT_Send_Check(*(data_buf+2),sum);
    }
}

小結

對項目中使用的數據傳輸方法進行了簡單總結,並且針對複雜和簡單情況的通信協議進行了分析彙總,看似複雜的總線通信技術在仔細的推敲下想上手並不難,當然在工業和高要求行業的應用肯定不是這麼簡單,這裏只是爲了方便以後的學習和再利用,與大家共勉! o(∩_∩)o

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章