首页 技术 正文
技术 2022年11月15日
0 收藏 556 点赞 4,604 浏览 4023 个字

单片机数据拼包

对于数据包拼包方式常规方式有

  • 数组
  • 指针
  • 结构体

下文将此三种方式分别列举此数据包的实现。

然后对比优缺点。

本文举例数据包协议

包头 长度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 charbyte;
typedef unsigned shortushort;
typedef unsigned intuint;
typedef long long intint64;
typedef unsigned long long intuint64;

基本定义

/// <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 还定义了一些带扩容的方法。可以在外部不传入缓冲的情况下完成数据包构建。

由于内部使用了堆,所以需要手动释放内存。

自带扩容的方式,属于另一种使用方式了,这里不做对比。

对比总结

以下评判为个人经验判断,欢迎讨论。

执行速度:指针>结构体>数组>流

技术难度:指针>结构体>数组>流

写错可能性:指针>数组>结构体>流

易读性:结构体>流>数组>指针

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,907
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,432
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,247
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,058
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,690
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,727