我应该使用位字段来映射传入的串行数据吗?

Should I use bit-fields for mapping incoming serial data?

我们有数据通过串行(蓝牙)传入,映射到特定结构。该结构的某些部分是子字节大小,因此 "obvious" 解决方案是将传入数据映射到位域。我无法解决的是机器或编译器的位字节顺序是否会影响它(这很难测试),以及我是否应该完全放弃位域。

比如我们有一段1.5字节的数据,所以我们使用结构体:

{
    uint8_t data1; // lsb
    uint8_t data2:4; // msb
    uint8_t reserved:4;
} Data;

保留位始终为1

所以比如传入的数据是0xD2,0xF4,则值为0x04D2,即1234。

我们使用的结构始终适用于我们测试过的系统,但我们需要它尽可能具有可移植性。

我的问题是:

如果是:

尽管从代码角度来说,位域是映射传入数据的最巧妙方式,但我想我只是想知道放弃它们并使用类似以下内容是否更安全:

struct {
    uint8_t data1; // lsb (0xFF)
    uint8_t data2; // msb (0x0F) & reserved (0xF0)
} Data;

Data d;

int value = (d.data2 & 0x0F) << 16 + d.data1

我们之所以没有这样做,首先是因为许多数据字段小于 1 个字节,而不是大于 1 个字节 - 这意味着通常我们没有位字段做任何掩蔽和移位,所以 post- 处理更简单。

Should I use bit-fields for mapping incoming serial data?

没有。位域有很多实现指定的行为,这使得使用它们成为一场噩梦。

Will data1 always represent the correct value as expected regardless of endianness.

是的,但那是因为 uint8_t 是最小的可寻址单位:一个字节。对于较大的数据类型,您需要注意字节序。

Could data2 and reserved be the wrong way around, with data2 representing the upper 4 bits instead of the lower 4 bits?

是的。它们也可以在不同的字节上。此外,编译器不必为位域支持 uint8_t,即使它会支持其他类型。

Is the bit endianness (generally) dependent on the byte endianness, or can they differ entirely?

最低有效位总是在最低有效字节中,但在 C 中无法确定该位在字节中的位置

移位运算符提供足够好的顺序的可靠抽象:对于数据类型 uint8_t(1u << 0) 始终是最低有效位,而 (1u << 7) 始终是最高有效位,对于所有编译器和所有架构。

另一方面,位域的定义非常糟糕,以至于您无法根据定义的域的顺序来确定位的顺序。

Is the bit-endianness determined by the hardware or the compiler?

编译器规定数据类型如何映射到实际位,但硬件对其影响很大。对于位字段,同一硬件的两个不同编译器可以按不同顺序排列字段。

Is there a simple way to determine in the compiler which way around it is, and reserve the bit-fields entries if needed?

不是真的。这取决于你的编译器如何去做,如果可能的话。

Although bit-fields are the neatest way, code-wise, to map the incoming data, I suppose I am just wondering if it's a lot safer to just abandon them, and use something like:

绝对放弃位域,但我也建议为此完全放弃结构,因为:

  • 您需要使用编译器扩展或手动工作来处理字节顺序。

  • 您需要使用编译器扩展来禁用填充以避免由于对齐限制而产生间隙。这会影响某些系统上的成员访问性能。

  • 不能有可变宽度或可选字段。

  • 如果您没有意识到这些问题,就很容易出现严重的别名违规。如果您为数据框定义字节数组并将其转换为指向结构的指针,然后取消引用它,那么在很多情况下都会遇到问题。

相反,我建议手动进行。定义字节数组,然后在必要时通过使用移位和屏蔽将它们分开来手动将每个字段写入其中。您可以为基本数据类型编写一个简单的可重用转换函数。