引言
在一般的項目開發過程中,往往需要兩塊或以上單片機進行通信完成數據傳輸,例如四旋翼無人機在飛行過程中無線傳輸數據回到地面站,治療儀器需要實時將患者和機器運轉情況傳回上位機平臺,糧倉溫控裝置需將各種傳感器通過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