在一般的项目开发过程中,往往需要两块或以上单片机进行通信完成数据传输,例如四旋翼无人机在飞行过程中无线传输数据回到地面站,治疗仪器需要实时将患者和机器运转情况传回上位机平台,粮仓温控装置需将各种传感器通过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//记录本次数据长度
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定位下位机协议:
遥控上位机协议:
一般选用尽可能低的传输速度下满足通信,对于无线数传来说,传输速度越低意味着越远的传输距离。
例如通信的波特率为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