C++使用union+struct实现bit协议的赋值与解析

在进行嵌入式开发的过程中,经常会遇到通信协议是按bit位定义的情况,比如协议一共6个byte字节,每个byte一共8bit位,但是传输的很多数据用1、2个bit就足够了,这时协议会按bit定义,如何方便快捷的进行bit位的赋值和读取,即为本文讲解的内容。

一、协议示例

为了方便一次示范多种可能遇到的场景,定义了下方的协议示例:

  • byte0::协议头,service/type各占4bit
  • byte1:传感器状态,每个传感器各占1bit,并预留2个bit空位
  • byte2:前边4个bit预留给传感器状态,后4个字节为故障数量
  • byte3-…:故障信息,每个故障对应2byte,有多少故障数量后边就跟多少个故障信息

可以看到,示例协议覆盖了1bit、4bit、6bit、循环bit结构,基本上包含了我们可能遇到的协议场景。为了方便测试,我们这里假设故障数量最大为8个,那么数据的长度在3 + 0-8*2个byte区间。接下来我们就看下实现层面怎么作比较好。

二、实现方案

1.基本位操作

在计算机中所有数据都是以二进制的形式储存的,位运算其实就是直接对在内存中的二进制数据进行操作,因此处理数据的速度非常快。

基本的位操作符有与、或、异或、取反、左移、右移这6种,它们的运算规则如下所示:

  • 与 & :两个位都为1时,结果才为1
  • 或 | :两个位都为0时,结果才为0
  • 异或 ^ :两个位相同为0,相异为1
  • 取反 ~ :0变1,1变0
  • 左移 << :各二进位全部左移若干位,高位丢弃,低位补0
  • 右移 >> :各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)
int a = -15, b = 15;  
printf("%d %d\n", a >> 2, b >> 2);  

-4 3

-15 = 1111 0001(二进制),右移二位,最高位由符号位填充将得到1111 1100即-4。

15=0000 1111(二进制),右移二位,最高位由符号位填充将得到0000 0011即3。

位操作可以帮我们实现一部分需求。

2.std::bitset

std::bitset 是 C++ 标准库中的一个类,用于表示二进制位序列。它提供了一种方便的方式来处理二进制数据,尤其适用于位运算操作。 std::bitset 类型表示一个固定长度的位序列,每个位都只能是 0 或 1。这个固定长度在创建对象时指定,并且不能在运行时更改。类似于整数类型,std::bitset 支持多种操作,包括位运算、位查询和位设置。

下面是 std::bitset 类型的一些常用操作:

  • size() 返回 bitset 的长度
  • count() 返回 bitset 中值为 1 的位的数量
  • any() 返回 bitset 中是否存在值为 1 的位
  • none() 返回bitset 中是否所有位都是 0
  • all() 返回 bitset 中是否所有位都是 1
  • test(pos) 返回 bitset 中位于 pos 位置的值
  • set(pos) 将 bitset 中位于 pos 位置的值设为 1
  • reset(pos)bitset 中位于 pos 位置的值设为 0
  • flip(pos) 将 bitset 中位于 pos 位置的值取反
  • to_ulong() 返回bitset 转换成的无符号整数值
  • to_ullong() 返回bitset 转换成的无符号长整数值
#include <bitset>

int main(int argc, char* argv[]) {
    // 使用二进制字符串初始化一个长度为 N 的 bitset序列
    std::bitset<(3+8*2)*8> bitset1("10100011000000000000000000000000000000000000000000000000000000000000000000000000"); 
    bitset1.set(8);         // 设置指定位 值为1
    bitset1.reset(9);       // 设置指定位 值为0
    bitset1.to_ulong();     // 转换为无符号整数

    return 0;
}

3.struct

struct相信大家都比较熟悉了,我们这里利用它能设置占用bit长度的特性,来实现不同字段分别占用1bit、4bit、6bit的长度,并实现循环failures的赋值:

#include <bitset>
#include <memory>
#include <sstream>
#include <iomanip>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iostream>

template <uint8_t N>
struct Result {
    // byte 0
    uint8_t service : 4;  // 此变量占用4个bit
    uint8_t type : 4;

    // byte 1
    uint8_t sensor_0 : 1;  // 此变量占用1个bit
    uint8_t sensor_1 : 1;
    uint8_t sensor_2 : 1;
    uint8_t sensor_3 : 1;
    uint8_t sensor_4 : 1;
    uint8_t sensor_5 : 1;
    uint8_t obligate_0 : 2;  // 此变量占用2个bit

    // byte 2
    uint8_t obligate_1 : 4;  // 此变量占用4个bit
    uint8_t failure_count : 4;

    // byte3-...
    uint8_t failures[2 * N];  // 故障信息 占用2*N个byte
};

struct LidarFailReason {
    // byte 0
    uint8_t sersor_id : 4;
    uint8_t install_fail : 1;
    uint8_t point_cloud_too_few : 1;
    uint8_t localization_fail : 1;
    uint8_t no_model_data : 1;

    // byte 1
    uint8_t obligate_0 : 8;  //占位
};

struct RadarFailReason {
    // byte 0
    uint8_t sersor_id : 4;
    uint8_t no_point_data : 1;
    uint8_t point_to_few : 1;
    uint8_t install_fail : 1;
    uint8_t obligate_0 : 1;

    // byte 1
    uint8_t obligate_1 : 8;  //占位
};
std::string format_to_string(const std::vector<uint8_t>& bytes) {
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (size_t i = 0; i < bytes.size(); ++i) {
        ss << std::hex << std::setw(2) << static_cast<int>(bytes[i]);
    }
    return ss.str();
}

int main(int argc, char* argv[]) {
    // struct test
    Result<8> result;
    std::memset(&result, 0, sizeof(result)); // 必须先全部位都置为零,否则下边不手动设置的字段值会是随机的
    result.service = 10;
    result.type = 3;
    result.sensor_0 = 1;
    result.sensor_1 = 0;

    // failures
    uint8_t count = 0;
    auto lidar = reinterpret_cast<LidarFailReason*>(&(result.failures[2U * count++]));
    lidar->sersor_id = 3;
    lidar->point_cloud_too_few = 1;
    auto radar = reinterpret_cast<RadarFailReason*>(&(result.failures[2U * count++]));
    radar->sersor_id = 4;
    radar->no_point_data = 1;
    // failure count
    result.failure_count = count;

    // struct to bytes
    std::vector<uint8_t> bytes(sizeof(result));
    std::memcpy(bytes.data(), &result, sizeof(result));

    // print bytes
    std::cout << "struct test: " << format_to_string(bytes);
    return 0;
}

struct test: 3a012023001400000000000000000000000000

4.union+struct

使用struct进行位赋值,并转换成字节流还是很方便的,只是byte流、16进制的转换需要额外做。那有没有方法可以在设置struct的同时自动得到byte流、16进制数据呢?用union就可以实现。

union的基本示例:

union var {
    long int l;
    int i;
    char c[4];
    uint8_t c[4];
};
int main(int argc, char* argv[]) {
    
    // union test
    var v;
    v.l = 5;
    std::cout << "l: " << v.l << std::endl;
    v.i = 6;
    std::cout << "i: " << v.i << std::endl;
    std::cout << "l: " << v.l << std::endl;
    v.c[0] = 0x04;
    v.c[1] = 0x03;
    v.c[2] = 0x02;
    v.c[3] = 0x11;
    printf("i: %x\n", v.i);  // 0x11020304
    std::cout << "l: " << v.l << std::endl;
    std::cout << "i: " << v.i << std::endl;

    return 0;
}
l: 5
l: 6
i: 6
ix: 11020304   # 
l: 285344516
i: 285344516

通过上方的示例可以看到,union共同体的特性是,共同体内的所有变量完全共用同一个内存首地址,内存大小按照占用量最大的那个变量分配,各变量都可以分别读写,操作的是同一块内存,所以一个变量的内存修改会导致另一个变量的值同步发生变化。

我们按照这个思路,把前边的stuct改造为union+struct:

#include <bitset>
#include <memory>
#include <sstream>
#include <iomanip>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iostream>

template <uint8_t N>
union ResponseResult {
    struct Result {
        // byte 0
        uint8_t service : 4;  // 此变量占用4个bit
        uint8_t type : 4;

        // byte 1
        uint8_t sensor_0 : 1;  // 此变量占用1个bit
        uint8_t sensor_1 : 1;
        uint8_t sensor_2 : 1;
        uint8_t sensor_3 : 1;
        uint8_t sensor_4 : 1;
        uint8_t sensor_5 : 1;
        uint8_t obligate_0 : 2;  // 此变量占用2个bit

        // byte 2
        uint8_t obligate_1 : 4;  // 此变量占用4个bit
        uint8_t failure_count : 4;

        // byte3-...
        uint8_t failures[2 * N];  // 故障信息 占用2*N个byte
    } data;
    uint8_t u8data[2 * N + 3];  // 占用N*2个故障信息byte + 3个固定 byte
};

struct LidarFailReason {
    // byte 0
    uint8_t sersor_id : 4;
    uint8_t install_fail : 1;
    uint8_t point_cloud_too_few : 1;
    uint8_t localization_fail : 1;
    uint8_t no_model_data : 1;

    // byte 1
    uint8_t obligate_1 : 8;  //占位
};

struct RadarFailReason {
    // byte 0
    uint8_t sersor_id : 4;
    uint8_t no_point_data : 1;
    uint8_t point_to_few : 1;
    uint8_t install_fail : 1;
    uint8_t obligate_0 : 1;

    // byte 1
    uint8_t obligate_1 : 8;  //占位
};

int main(int argc, char* argv[]) {
    // union+struce test
    ResponseResult<8> result;
    std::memset(result.u8data, 0, sizeof(result));    // 必须先全部位都置为零,否则下边不手动设置的字段值会是随机的
    std::cout << "struct size: " << sizeof(result) << std::endl;
    //result.u8data[0] = 0x3a;
    result.data.service = 10;
    result.data.type = 3;
    result.data.sensor_0 = 1;
    result.data.sensor_1 = 0;

    // failures
    uint8_t count = 0;
    auto lidar = reinterpret_cast<LidarFailReason*>(&(result.data.failures[2U * count++]));
    lidar->sersor_id = 3;
    lidar->point_cloud_too_few = 1;
    auto radar = reinterpret_cast<RadarFailReason*>(&(result.data.failures[2U * count++]));
    radar->sersor_id = 4;
    radar->no_point_data = 1;
    // failure count
    result.data.failure_count = count;

    // print
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    std::cout << "u8data size: " << sizeof(result.u8data) << " " << sizeof(result.u8data[0]) << std::endl;
    for (size_t i = 0; i < sizeof(result.u8data) / sizeof(result.u8data[0]); ++i) {
        ss << std::hex << std::setw(2) << static_cast<int>(result.u8data[i]);
    }
    std::cout << "union+struce: " << ss.str() << std::endl;

    // 系统大小端判断(不同端字节内的高低位的前后顺序不同)
    if (result.u8data[0] == 0x3a) {
        std::cout << "this is little endian system" << std::endl;
    } else {
        std::cout << "this is big endian system" << std::endl;
    }
    return 0;
}

struct size: 19
u8data size: 19 1
union+struce: 3a012023001400000000000000000000000000
this is little endian system

可以看到,通过union的.data结构体,或.u8data,均可读写相关数据,我们可以用struct来方便赋值,使用u8data用来按字节读取,对实际的使用场景来说方便了很多。

三、常见问题

1. 随机值问题

union+struct使用时,建议先全部位都置为零,否则一旦不能为所有字段显式设置值,则未设置的字段值将会在每次编译时随机。

2.大小端问题

不同的操作系统平台上,大小端不同,如涉及相互的数据通信,则需要特别关注他们生成的字节流顺序差异。

3.union的内存大小差异

union仅共用同一个内存首地址,如果union变量内存长度不同,则容易出现意想不到的问题,因此建议union内的多个变量内存长度保持一致。

 

yan 23.9.3

 

参考:

C++:位操作基础篇之位操作全面总结

C++ std::bitset

结构体struct和联合体union最全讲解

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。