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