微控制器資料拼包

對於資料包拼包方式常規方式有

  • 陣列
  • 指標
  • 結構體

下文將此三種方式分別列舉此資料包的實現。

然後對比優缺點。

本文舉例資料包協議

包頭 長度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 還定義了一些帶擴容的方法。可以在外部不傳入緩衝的情況下完成資料包構建。

由於內部使用了堆,所以需要手動釋放記憶體。

自帶擴容的方式,屬於另一種使用方式了,這裡不做對比。

對比總結

以下評判為個人經驗判斷,歡迎討論。

執行速度:指標>結構體>陣列>流

技術難度:指標>結構體>陣列>流

寫錯可能性:指標>陣列>結構體>流

易讀性:結構體>流>陣列>指標