在进行嵌入式开发的过程中,经常会遇到通信协议是按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
参考: