1、利用protobuf通讯原理
最近项目中须要用到stm32与Orange Pi(移植了linux)进行数据交互,H6端是用C++编写的串口底层驱动,与stm32的串口链接并通讯。串口间的通讯协议定为采用protobuf打包数据并经过串口发出的形式,即发送端编码数据并序列化成数组经过串口发出,接收端接收到一帧数据,进行解码并解析数据。linux
2、 移植protobuf(nanopb-0.3.8)到stm32工程
protobuf是一种打包数据的工具,和JSON打包数据的做用是同样的。在C++下用protobuf传递数据,要先写一个.proto文件,而后在linux环境下编译该文件,或者直接放在CMake里面编译,即可以生成出来一个类(.cpp 和 .h),利用protobuf打包即是打包这个类。数组
能够理解成把这个类的全部数据加上帧头帧尾帧校验,而后经过串口,网络等通讯格式将数据发送出去,这个过程称为序列化。解包就是把收到序列化的数据反序列化,而后把有效数据放入生成的类中。网络
通常开发stm32的环境是在Windows下,基于Keil开发,要经过.proto文件生成结构体(.c和.h)须要下载一个官方protobuf的轮子,而后在命令行下编译便可生成咱们须要的结构体文件。这个轮子的下载地址放在文末。并发
3、编写.proto文件
编写.proto文件很简单,开头先写protobuf的版本号,和包名(命名空间)工具
// A very simple protocol definition, consisting of only // one message. // 02 syntax = "proto3"; package STM32;
而后写message,就和写结构体(枚举)的格式很类似。优化
message GyroOffset { float gyrooffsetX = 1; float gyrooffsetY = 2; float gyrooffsetZ = 3; } message GyroAccData { uint32 accX = 1; //加速度计x轴加速度 uint32 accY = 2; //加速度计y轴加速度 uint32 accZ = 3; //加速度计z轴加速度 uint32 gryoX = 4; //陀螺仪x轴原始数据 uint32 gryoY = 5; //陀螺仪y轴原始数据 uint32 gryoZ = 6; //陀螺仪z轴原始数据 } message IsGetGyroOffset { bool IsGetStatus = 1; } message Bmi160ToData { GyroOffset gyroOffset = 1; GyroAccData gyroAccData = 2; IsGetGyroOffset isGetGyroOffset = 3; }
到这里为止,咱们最终只须要Bmi160ToData这个结构体中包含的数据便可。ui
写完之后咱们把.proto文件放在桌面上,而后打开cmd命令行解释器,cd到.proto文件的目录下,而后运行protoc 这个脚本去编译.proto文件,编译完成后便可生成两个文件,一个.c,一个.h。这里有一点,就是最好把这个脚本的可执行文件路径放到系统环境变量下,这样才能够在任何路径下编译.proto文件。具体的命令以下图所示:
编译完成后,在该路径下会生成一个.c一个.h文件,其内容以下:
this
/* Automatically generated nanopb constant definitions */ /* Generated by nanopb-0.3.8 at Fri Sep 11 16:29:22 2020. */ //.c #include "Bmi160ToData.pb.h" /* @@protoc_insertion_point(includes) */ #if PB_PROTO_HEADER_VERSION != 30 #error Regenerate this file with the current version of nanopb generator. #endif const pb_field_t STM32_GyroOffset_fields[4] = { PB_FIELD( 1, FLOAT , SINGULAR, STATIC , FIRST, STM32_GyroOffset, gyrooffsetX, gyrooffsetX, 0), PB_FIELD( 2, FLOAT , SINGULAR, STATIC , OTHER, STM32_GyroOffset, gyrooffsetY, gyrooffsetX, 0), PB_FIELD( 3, FLOAT , SINGULAR, STATIC , OTHER, STM32_GyroOffset, gyrooffsetZ, gyrooffsetY, 0), PB_LAST_FIELD }; const pb_field_t STM32_GyroAccData_fields[7] = { PB_FIELD( 1, UINT32 , SINGULAR, STATIC , FIRST, STM32_GyroAccData, accX, accX, 0), PB_FIELD( 2, UINT32 , SINGULAR, STATIC , OTHER, STM32_GyroAccData, accY, accX, 0), PB_FIELD( 3, UINT32 , SINGULAR, STATIC , OTHER, STM32_GyroAccData, accZ, accY, 0), PB_FIELD( 4, UINT32 , SINGULAR, STATIC , OTHER, STM32_GyroAccData, gryoX, accZ, 0), PB_FIELD( 5, UINT32 , SINGULAR, STATIC , OTHER, STM32_GyroAccData, gryoY, gryoX, 0), PB_FIELD( 6, UINT32 , SINGULAR, STATIC , OTHER, STM32_GyroAccData, gryoZ, gryoY, 0), PB_LAST_FIELD }; const pb_field_t STM32_IsGetGyroOffset_fields[2] = { PB_FIELD( 1, BOOL , SINGULAR, STATIC , FIRST, STM32_IsGetGyroOffset, IsGetStatus, IsGetStatus, 0), PB_LAST_FIELD }; const pb_field_t STM32_Bmi160ToData_fields[4] = { PB_FIELD( 1, MESSAGE , SINGULAR, STATIC , FIRST, STM32_Bmi160ToData, gyroOffset, gyroOffset, &STM32_GyroOffset_fields), PB_FIELD( 2, MESSAGE , SINGULAR, STATIC , OTHER, STM32_Bmi160ToData, gyroAccData, gyroOffset, &STM32_GyroAccData_fields), PB_FIELD( 3, MESSAGE , SINGULAR, STATIC , OTHER, STM32_Bmi160ToData, isGetGyroOffset, gyroAccData, &STM32_IsGetGyroOffset_fields), PB_LAST_FIELD }; /* Check that field information fits in pb_field_t */ #if !defined(PB_FIELD_32BIT) /* If you get an error here, it means that you need to define PB_FIELD_32BIT * compile-time option. You can do that in pb.h or on compiler command line. * * The reason you need to do this is that some of your messages contain tag * numbers or field sizes that are larger than what can fit in 8 or 16 bit * field descriptors. */ PB_STATIC_ASSERT((pb_membersize(STM32_Bmi160ToData, gyroOffset) < 65536 && pb_membersize(STM32_Bmi160ToData, gyroAccData) < 65536 && pb_membersize(STM32_Bmi160ToData, isGetGyroOffset) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_STM32_GyroOffset_STM32_GyroAccData_STM32_IsGetGyroOffset_STM32_Bmi160ToData) #endif #if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) /* If you get an error here, it means that you need to define PB_FIELD_16BIT * compile-time option. You can do that in pb.h or on compiler command line. * * The reason you need to do this is that some of your messages contain tag * numbers or field sizes that are larger than what can fit in the default * 8 bit descriptors. */ PB_STATIC_ASSERT((pb_membersize(STM32_Bmi160ToData, gyroOffset) < 256 && pb_membersize(STM32_Bmi160ToData, gyroAccData) < 256 && pb_membersize(STM32_Bmi160ToData, isGetGyroOffset) < 256), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_STM32_GyroOffset_STM32_GyroAccData_STM32_IsGetGyroOffset_STM32_Bmi160ToData) #endif /* @@protoc_insertion_point(eof) */
/* Automatically generated nanopb header */ /* Generated by nanopb-0.3.8 at Fri Sep 11 16:29:22 2020. */ //.h #ifndef PB_STM32_BMI160TODATA_PB_H_INCLUDED #define PB_STM32_BMI160TODATA_PB_H_INCLUDED #include <pb.h> /* @@protoc_insertion_point(includes) */ #if PB_PROTO_HEADER_VERSION != 30 #error Regenerate this file with the current version of nanopb generator. #endif #ifdef __cplusplus extern "C" { #endif /* Struct definitions */ typedef struct _STM32_GyroAccData { uint32_t accX; uint32_t accY; uint32_t accZ; uint32_t gryoX; uint32_t gryoY; uint32_t gryoZ; /* @@protoc_insertion_point(struct:STM32_GyroAccData) */ } STM32_GyroAccData; typedef struct _STM32_GyroOffset { float gyrooffsetX; float gyrooffsetY; float gyrooffsetZ; /* @@protoc_insertion_point(struct:STM32_GyroOffset) */ } STM32_GyroOffset; typedef struct _STM32_IsGetGyroOffset { bool IsGetStatus; /* @@protoc_insertion_point(struct:STM32_IsGetGyroOffset) */ } STM32_IsGetGyroOffset; typedef struct _STM32_Bmi160ToData { STM32_GyroOffset gyroOffset; STM32_GyroAccData gyroAccData; STM32_IsGetGyroOffset isGetGyroOffset; /* @@protoc_insertion_point(struct:STM32_Bmi160ToData) */ } STM32_Bmi160ToData; /* Default values for struct fields */ /* Initializer values for message structs */ #define STM32_GyroOffset_init_default {0, 0, 0} #define STM32_GyroAccData_init_default {0, 0, 0, 0, 0, 0} #define STM32_IsGetGyroOffset_init_default {0} #define STM32_Bmi160ToData_init_default {STM32_GyroOffset_init_default, STM32_GyroAccData_init_default, STM32_IsGetGyroOffset_init_default} #define STM32_GyroOffset_init_zero {0, 0, 0} #define STM32_GyroAccData_init_zero {0, 0, 0, 0, 0, 0} #define STM32_IsGetGyroOffset_init_zero {0} #define STM32_Bmi160ToData_init_zero {STM32_GyroOffset_init_zero, STM32_GyroAccData_init_zero, STM32_IsGetGyroOffset_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define STM32_GyroAccData_accX_tag 1 #define STM32_GyroAccData_accY_tag 2 #define STM32_GyroAccData_accZ_tag 3 #define STM32_GyroAccData_gryoX_tag 4 #define STM32_GyroAccData_gryoY_tag 5 #define STM32_GyroAccData_gryoZ_tag 6 #define STM32_GyroOffset_gyrooffsetX_tag 1 #define STM32_GyroOffset_gyrooffsetY_tag 2 #define STM32_GyroOffset_gyrooffsetZ_tag 3 #define STM32_IsGetGyroOffset_IsGetStatus_tag 1 #define STM32_Bmi160ToData_gyroOffset_tag 1 #define STM32_Bmi160ToData_gyroAccData_tag 2 #define STM32_Bmi160ToData_isGetGyroOffset_tag 3 /* Struct field encoding specification for nanopb */ extern const pb_field_t STM32_GyroOffset_fields[4]; extern const pb_field_t STM32_GyroAccData_fields[7]; extern const pb_field_t STM32_IsGetGyroOffset_fields[2]; extern const pb_field_t STM32_Bmi160ToData_fields[4]; /* Maximum encoded size of messages (where known) */ #define STM32_GyroOffset_size 15 #define STM32_GyroAccData_size 36 #define STM32_IsGetGyroOffset_size 2 #define STM32_Bmi160ToData_size 59 /* Message IDs (where set with "msgid" option) */ #ifdef PB_MSGID #define BMI160TODATA_MESSAGES \ #endif #ifdef __cplusplus } /* extern "C" */ #endif /* @@protoc_insertion_point(eof) */ #endif f
至此,protobuf的C文件格式的代码已经生成。编码
4、开始通讯!!!
把刚才生成的两个文件拉到项目里面,同时把官方的protoc所用到的三个文件和其对应的.h文件也拉到项目中来,文件格式以下图:
这三个文件回合protoc脚本一块儿放在文末。
spa
咱们先来看打包并发送一帧protobuf数据的代码:
/******************************************************************************* * Function Name : vProto_Encode_Send_FastPack() * Description : 编码protobuf数据,并将编码事后的数组经过串口1发送给上层 * Input : STM32_Stm32ToState 类型的结构体指针 * Output : None * Return : true:编码成功 false:编码失败 *******************************************************************************/ bool vProto_Encode_Send_FastPack(void) { STM32_Stm32ToState STM32_Stm32ToState_Fast = STM32_Stm32ToState_init_default;//快包 int message_length; bool status; pb_ostream_t op_stream;//建立一个编码对象,保存发送buf的数据长度,数据首地址,最大字节等信息 STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.accX = bmi160_protobuf.accx; STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.accY = bmi160_protobuf.accy; STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.accZ = bmi160_protobuf.accz; STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.gryoX = bmi160_protobuf.gryx; STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.gryoY = bmi160_protobuf.gryy; STM32_Stm32ToState_Fast.bmi160ToData.gyroAccData.gryoZ = bmi160_protobuf.gryz; //清空发送缓冲数组 memset(ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex] , 0 , sizeof(ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex])); //初始化这个编码对象,并填充发送数组首地址,发送数据长度 op_stream = pb_ostream_from_buffer(ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex] , sizeof(ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex])); //调用编码API,将形参结构体的值赋给编码对象(数组首地址和长度) status = pb_encode(&op_stream,STM32_Stm32ToState_fields, &STM32_Stm32ToState_Fast); if(!status) return status; //打包之后数据的长度 message_length = op_stream.bytes_written; DEBUG("零飘结构体编码后的大小为:%d" , message_length); ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex][0] = ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex][0]; //发送该帧数据 memcpy(ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex],(uint8_t *)&ucUSART1TrainsmitBuffer[ucWtiteDataToUSART1TransmitGrooveIndex],message_length); ucUSART1TrainsmitLength[ucWtiteDataToUSART1TransmitGrooveIndex] = message_length; WriteDataToUSART1TraismitBufferDone( );//发送下标移位 return status; }
其实就是先把咱们须要发送的结构体填充对应的值,而后经过protoc的API将这个结构体序列化之后的值放在一个buf数组里面,最后把这个数组经过串口DMA发送出去。这时linux端会从串口接收到该帧数据并存放在一个数组里面,而后经过API去将这个数组反序列化成.proto生成的类,这时类里的值,就是stm32发送上去的值。
同理可得,stm32接收到linux端发送的一帧数据,能够经过串口IDLE+DMA接收到一个数组里面,而后调用API去反序列化这个数组,并将结果填充到接收结构体里面,便可完成一次数据的收发,接收代码以下:
/******************************************************************************* * Function Name : vProto_Decode_Receive_Lslam() * Description : 解码protobuf数据,并分析数据和改变坐标全局变量 * Input : buf:接收一帧数组,buf_length:接收一帧数组的数组长度, STM32_Stm32ToStete 类型的结构体指针 * Output : None * Return : true:解码成功 false:解码失败 *******************************************************************************/ bool vProto_Decode_Receive(u8* buf , u16 buf_length) { bool status; STM32_Stm32ToState STM32_Stm32ToState_t = STM32_Stm32ToState_init_default; pb_istream_t ip_stream;//建立一个解码对象,用于保存接收到的数组地址和数组长度 ip_stream = pb_istream_from_buffer(buf,buf_length);//把接收到的一帧数据和帧长度给到解码对象 if(pb_decode(&ip_stream , STM32_Stm32ToState_fields ,&STM32_Stm32ToState_t)) { g_tRouteCoord.Self_Coox = LSLAM_PlanMsgToState_t.curpos.x; g_tRouteCoord.Self_Cooy = LSLAM_PlanMsgToState_t.curpos.y; g_tRouteCoord.Self_Angle = LSLAM_PlanMsgToState_t.curpos.theta; g_tRouteCoord.CurrentCoo.cooY = LSLAM_PlanMsgToState_t.point1.y; g_tRouteCoord.Coo1.cooX = LSLAM_PlanMsgToState_t.point2.x; g_tRouteCoord.Coo1.cooY = LSLAM_PlanMsgToState_t.point2.y; g_tRouteCoord.Coo2.cooX = LSLAM_PlanMsgToState_t.point3.x; g_tRouteCoord.Coo2.cooY = LSLAM_PlanMsgToState_t.point3.y; g_tRouteCoord.Coo3.cooX = LSLAM_PlanMsgToState_t.point4.x; g_tRouteCoord.Coo3.cooY = LSLAM_PlanMsgToState_t.point4.y; g_tRouteCoord.Coo4.cooX = LSLAM_PlanMsgToState_t.point5.x; g_tRouteCoord.Coo4.cooY = LSLAM_PlanMsgToState_t.point5.y; IsGetRoute = true; } g_tRouteCoord.CurrentCoo.cooX = LSLAM_PlanMsgToState_t.point1.x; if(pb_decode(&ip_stream , IsGetGyroOffset_fields ,&IsGetGyroOffset_t)) { H6IsGetGyroOffset = IsGetGyroOffset_t.IsGetStatus; } return status; }
经过这个解码对象生成的值,能够判断数据对应的哪一个结构体,从而填充不一样的结构体出来。
5、protobuf的缺点和不足
protobuf终究是面向上层开发出来的数据打包协议,可是正如.proto文件限制的那样,它打包数据的最小单位是32位,也就是说咱们想经过protobuf传输一个bool量的数据也要用一个32位的结构体成员去承载它。
尽管protobuf拥有优化打包数据内存的功能,也就是说当一个数据很小的时候(小于255),protobuf会将其打包成uint8_t类型的数据序列化到数组里,可是这样的特性也意味着数据打包长短的不肯定性,这在一个稳定的通讯系统里面是很致命的一点。咱们须要定义一个最大长度的数组去承载protobuf序列化先后的数据。
protobuf打包数据其实和咱们本身定义协议同样,咱们能够把序列化之后的数据经过串口打印出来,就能够发现所谓的序列化也只不过是对一帧数据加上帧头帧尾帧校验而后发送出去。利用protobuf协议只是方便和linux端的通讯,这样项目里的每一个程序块均可以用同一个.proto文件进行数据的通讯,这在一个大型项目里是颇有好处的。
至此,基于protobuf完成stm32和Linux的数据通讯为你们介绍完毕。脚本和protoc公共文件见连接
protobuf