微控制器資料拼包
對於資料包拼包方式常規方式有:
- 陣列
- 指標
- 結構體
- 流
下文將此三種方式分別列舉此資料包的實現。
然後對比優缺點。
本文舉例資料包協議:
包頭 | 長度Length | 訊息型別 | 訊息序列號Seq | 負載資料 | 校驗 |
---|---|---|---|---|---|
2位元組 | 1位元組 | 1位元組 | 1位元組 | N位元組 | 2位元組 |
名稱 | 描述 | 其他 |
---|---|---|
包頭 | 固定 0X0A,0X0A | 對於乙太網資料包可以不設立此段。串列埠一般需要使用,對解包有利,這裡不贅述。 |
長度 Length | 資料包長度,(除去包頭和自身) | |
訊息型別 | - | 低7bit是訊息型別,最高bit標記是否是回覆訊息 |
訊息序列號Seq | 訊息編號,用於回覆訊息與請求訊息的匹配 | |
負載資料 | 訊息型別對應的負載資料 | 負載資料長度 = Length - 4 |
校驗 | 前面所有位元組的校驗值 |
程式碼中使用型別如下定義:
// https://github.com/NewLifeX/microCLib.git Core 目錄 Type.h 內定義。
typedef char sbyte;
typedef unsigned char byte;
typedef unsigned short ushort;
typedef unsigned int uint;
typedef long long int int64;
typedef unsigned long long int uint64;
基本定義:
/// <summary>訊息型別</summary>
typedef enum
{
/// <summary></summary>
Ping = 0x01,
/// <summary>註冊</summary>
Reg = 0x02,
/// <summary>登入</summary>
Login = 0x03,
}MsgType_e;
// 資料包頭
static byte PktHead[] = {0x0A,0x0A};
// 函式原型
/// <summary>建立訊息</summary>
/// <param name="seq">訊息序列號Seq</param>
/// <param name="payload">負載資料內容指標</param>
/// <param name="payloadlen">負載資料長度</param>
/// <param name="data">訊息輸出緩衝區</param>
/// <param name="len">緩衝區長度</param>
/// <returns>返回訊息真實長度</returns>
int Buil(byte seq, byte* payload, int payloadlen, byte* data, int len);
// 下列程式碼,會根據事項方式在函式名加尾綴 ByXXX
陣列
int BuilByteArray(byte seq, byte* payload, int payloadlen, byte* data, int len)
{
if (data == NULL)return -1;
// 判斷緩衝區長度是否足夠
if (len < payloadlen + 4 + 3)return -1;
// 用於記錄長度/寫入位置
int idx = 0;
// 寫資料包頭
// memcpy(&data[idx], PktHead, sizeof(PktHead)); // idx=0 可以直接寫data
memcpy(data, PktHead, sizeof(PktHead));
idx += sizeof(PktHead);
// 長度
data[idx++] = payloadlen + 4;
// 型別
data[idx++] = (byte)Reg;
// 序列號
data[idx++] = seq;
// 負載
memcpy(&data[idx], payload, payloadlen);
idx += payloadlen;
// 計算crc
ushort crc = CaclcCRC16(data, idx);
// 寫入crc
memcpy(&data[idx], (byte*)&crc, sizeof(crc));
idx += sizeof(crc);
return idx;
}
- 常規操作,在各種c專案中最為常見。
- 容易出錯的點在 idx 維護。
- 基本無難度。
- 閱讀難度很高,如果不寫好備註。基本頭禿。
指標
int BuilByPoint(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)
{
if (data == NULL)return -1;
// 判斷緩衝區長度是否足夠
if (len < payloadlen + 4 + 3)return -1;
byte* p = data;
// 寫資料包頭
// memcpy(&data[idx], PktHead, sizeof(PktHead)); // idx=0 可以直接寫data
memcpy(p, PktHead, sizeof(PktHead));
p += sizeof(PktHead);
// 長度
*p++ = payloadlen + 4;
// 型別
*p++ = (byte)type;
// 序列號
*p++ = seq;
// 負載
memcpy(p, payload, payloadlen);
p += payloadlen;
// 計算crc
ushort crc = CaclcCRC16(data, p - data);
// 寫入crc
memcpy(p, (byte*)&crc, sizeof(crc));
p += sizeof(crc);
return p - data;
}
- 基本就是陣列方式的翻版。
- 在執行效率上優於陣列方式。
- 指標對於 c 來說一直都是難點。
- 容易寫出錯。
- 閱讀難度非常高,如果不寫好備註。基本頭禿。
結構體
// 壓棧編譯器配置
#pragma pack(push)
// 告訴編譯按照1位元組對齊排布記憶體。
#pragma pack(1)
/// <summary>固定位置的資料部分</summary>
typedef struct
{
/// <summary>包頭</summary>
ushort PktHead;
/// <summary>長度</summary>
byte Length;
/// <summary>訊息型別,enum長度不確定,所以寫個基礎型別</summary>
byte Type;
/// <summary>訊息序列號</summary>
byte Seq;
}MsgBase_t;
// 恢復編譯器配置(彈棧)
#pragma pack(pop)
int BuilByStruct(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)
{
if (data == NULL)return -1;
// 判斷緩衝區長度是否足夠
if (len < payloadlen + 4 + 3)return -1;
// 直接寫入能描述的部分。
MsgBase_t* mb = (MsgBase_t*)data;
memcpy((byte*)&(mb->PktHead), PktHead, sizeof(PktHead));
mb->Length = payloadlen + 4;
mb->Type = (byte)type;
mb->Seq = seq;
int idx = sizeof(MsgBase_t);
// 負載
memcpy(&data[idx], payload, payloadlen);
idx += payloadlen;
// 計算crc
ushort crc = CaclcCRC16(data, idx);
// 寫入crc
memcpy(&data[idx], (byte*)&crc, sizeof(crc));
idx += sizeof(crc);
return idx;
}
- 很少出現在各種開源軟體中。
- 需要掌握一個高階知識點,涉及編譯器和 cpu 特徵。
cpu位寬、非對齊訪問以及對應的編譯器知識。 - 對於固定長度的指令來說,非常方便。
- cpu執行效率非常高,跟陣列方式的速度一致。
- 寫好結構體,數值填充順序就跟協議內容無關了。
- 很好理解,閱讀無壓力。
- 對於讀非固定格式資料來說,0靈活度。只能抽取相同部分做部分處理。非常頭禿。
(本文主體是寫資料,詳細討論)
資料流
// https://github.com/NewLifeX/microCLib.git
#include "Stream.h"
int BuildByStream(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)
{
if (data == NULL)return -1;
// 判斷緩衝區長度是否足夠
if (len < payloadlen + 4 + 3)return -1;
// 初始化流
Stream_t st;
StreamInit(&st, data, len);
// 包頭
StreamWriteBytes(&st, PktHead, sizeof(PktHead));
// 長度
StreamWriteByte(&st, payloadlen + 4);
// 型別
StreamWriteByte(&st, (byte)type);
// 序列號
StreamWriteByte(&st, seq);
// 負載
StreamWriteBytes(&st, payload, payloadlen);
// 計算crc
ushort crc = CaclcCRC16(st.MemStart, st.Position);
// 寫入crc
StreamWriteBytes(&st, (byte*)&crc, sizeof(crc));
return st.Position;
}
- 上位機處理常規方式。算是面對物件程式設計的範疇了。
- 閱讀難度很小。
- Stream 內部已做邊界判斷,基本不會出現bug。
- 缺點,效率低。每個操作都是函式呼叫,此處產生大量消耗。
Stream 還定義了一些帶擴容的方法。可以在外部不傳入緩衝的情況下完成資料包構建。
由於內部使用了堆,所以需要手動釋放記憶體。
自帶擴容的方式,屬於另一種使用方式了,這裡不做對比。
對比總結
以下評判為個人經驗判斷,歡迎討論。
執行速度:指標>結構體>陣列>流
技術難度:指標>結構體>陣列>流
寫錯可能性:指標>陣列>結構體>流
易讀性:結構體>流>陣列>指標