一、利用protobuf通信原理
最近项目中需要用到stm32与Orange Pi(移植了linux)进行数据交互,H6端是用C++编写的串口底层驱动,与stm32的串口连接并通信。串口间的通信协议定为采用protobuf打包数据并通过串口发出的形式,即发送端编码数据并序列化成数组通过串口发出,接收端接收到一帧数据,进行解码并解析数据。
二、 移植protobuf(nanopb-0.3.8)到stm32工程
protobuf是一种打包数据的工具,和JSON打包数据的作用是一样的。在C++下用protobuf传递数据,要先写一个.proto文件,然后在linux环境下编译该文件,或者直接放在CMake里面编译,便可以生成出来一个类(.cpp 和 .h),利用protobuf打包便是打包这个类。
可以理解成把这个类的所有数据加上帧头帧尾帧校验,然后通过串口,网络等通信格式将数据发送出去,这个过程称为序列化。解包就是把收到序列化的数据反序列化,然后把有效数据放入生成的类中。
一般开发stm32的环境是在Windows下,基于Keil开发,要通过.proto文件生成结构体(.c和.h)需要下载一个官方protobuf的轮子,然后在命令行下编译即可生成我们需要的结构体文件。这个轮子的下载地址放在文末。
三、编写.proto文件
编写.proto文件很简单,开头先写protobuf的版本号,和包名(命名空间)
// A very simple protocol definition, consisting of only
// one message.
// 02syntax = "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这个结构体中包含的数据即可。
写完以后我们把.proto文件放在桌面上,然后打开cmd命令行解释器,cd到.proto文件的目录下,然后运行protoc 这个脚本去编译.proto文件,编译完成后即可生成两个文件,一个.c,一个.h。这里有一点,就是最好把这个脚本的可执行文件路径放到系统环境变量下,这样才可以在任何路径下编译.proto文件。具体的命令如下图所示:
编译完成后,在该路径下会生成一个.c一个.h文件,其内容如下:
/* 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.
#endifconst 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文件格式的代码已经生成。
四、开始通信!!!
把刚才生成的两个文件拉到项目里面,同时把官方的protoc所用到的三个文件和其对应的.h文件也拉到项目中来,文件格式如下图:
这三个文件回合protoc脚本一起放在文末。
我们先来看打包并发送一帧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;
}
通过这个解码对象生成的值,可以判断数据对应的哪个结构体,从而填充不同的结构体出来。
五、protobuf的缺点和不足
protobuf终究是面向上层开发出来的数据打包协议,但是正如.proto文件限制的那样,它打包数据的最小单位是32位,也就是说我们想通过protobuf传输一个bool量的数据也要用一个32位的结构体成员去承载它。
尽管protobuf拥有优化打包数据内存的功能,也就是说当一个数据很小的时候(小于255),protobuf会将其打包成uint8_t类型的数据序列化到数组里,但是这样的特性也意味着数据打包长短的不确定性,这在一个稳定的通信系统里面是很致命的一点。我们需要定义一个最大长度的数组去承载protobuf序列化前后的数据。
protobuf打包数据其实和我们自己定义协议一样,我们可以把序列化以后的数据通过串口打印出来,就可以发现所谓的序列化也只不过是对一帧数据加上帧头帧尾帧校验然后发送出去。利用protobuf协议只是方便和linux端的通信,这样项目里的每个程序块都可以用同一个.proto文件进行数据的通信,这在一个大型项目里是很有好处的。
至此,基于protobuf完成stm32和Linux的数据通信为大家介绍完毕。脚本和protoc公共文件见链接
protobuf