什么是位域
C语言的位域(bit-field)是一种特殊的结构体成员,允许我们按bit位对成员进行定义,指定其占用的位数,主要用于节省内存或精确映射硬件寄存器 。我们先看一下示例:
typedef struct
{
uint32_t widthValidated : 1;
uint32_t heightValidated : 1;
}canvas_status_t;在上面的示例中,原来结构体中定义了2个uint32_t的变量会占用8字节的内存空间,而现在只占用了4个字节的内存空间。不过,只有其中的2个bit位被用来存储变量值,其实我也可以把32bit全部写全,我这里省略了而已。
位域的优势
位域的定义方式如下:
struct {
type member_name : width;
};type是数据类型,常为uint32_t或uint8_t;member_name由程序员自定义的有意义的成员变量名;width数字,1-64,表示该成员占用的位数,不能高于type类型占用的内存空间大小。从它的定义方式下,我们可以得到位域的使用优势:
节省内存空间。在位域的结构体下,多个成员变量以bit位的方式共同占用一个变量的内存空间,从而达到节省内存的目的;
增强可读性。在硬件寄存器定义和总线通讯协议定义时采用位域可提升程序代码的可读性;比如下面的代码示例:
/**
\brief Union type to access the Special-Purpose Program Status Registers (xPSR).
*/
typedef union
{
struct
{
uint32_t ISR:9; /*!< bit: 0.. 8 Exception number */
uint32_t _reserved0:15; /*!< bit: 9..23 Reserved */
uint32_t T:1; /*!< bit: 24 Thumb bit (read 0) */
uint32_t _reserved1:3; /*!< bit: 25..27 Reserved */
uint32_t V:1; /*!< bit: 28 Overflow condition code flag */
uint32_t C:1; /*!< bit: 29 Carry condition code flag */
uint32_t Z:1; /*!< bit: 30 Zero condition code flag */
uint32_t N:1; /*!< bit: 31 Negative condition code flag */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} xPSR_Type;上面的代码在使用的时候psp.b.N = 1就直接对bit31赋值了,而且可以一目了然的看出N所占的bit位宽为1。
位域的劣势
位域的内存布局完全由编译器决定,不可控!
C/C++ 标准对位域的内存布局几乎没有强制规定,以下关键行为完全由编译器自行决定,这在跨平台,文件存储与多编译器版本下将发生Fatal Error:
位域成员的排列顺序不明确。 位域成员在存储单元中是从高位到低位排列,还是从低位到高位排列,标准未定义。同一份代码在GCC和ARMClang下可能产生完全相反的内存布局。
是否允许跨字节边界不统一。当一个位域成员无法完全放入当前存储单元时,GCC通常允许其跨越字节边界继续存放,而其它编译则可能会直接跳转到下一个存储单元,产生填充空洞。这导致同一结构体在不同编译器下的 sizeof 结果可能不同。
字节序差异导致数据错乱。在大端模式和小端模式的平台上,位域成员的物理存储顺序完全不同。将包含位域的结构体直接用于网络传输或文件写入,接收方解析时必然出现数据错位。
位域的可移植性存在根本性缺陷,这也是大厂们纷纷严禁使用的核心原因。
位域的替代操作
既然大厂严禁使用位域进行位操作,那么应该如何实现呢?规范要求使用手动位操作配合命名常量来替代位域,确保二进制布局完全可控。
/******************* Bit definition for USART_RQR register ******************/ #define USART_RQR_ABRRQ ((uint16_t)0x0001) /*!< Auto-Baud Rate Request */ #define USART_RQR_SBKRQ ((uint16_t)0x0002) /*!< Send Break Request */ #define USART_RQR_MMRQ ((uint16_t)0x0004) /*!< Mute Mode Request */ #define USART_RQR_RXFRQ ((uint16_t)0x0008) /*!< Receive Data flush Request */ #define USART_RQR_TXFRQ ((uint16_t)0x0010) /*!< Transmit data flush Request */
当我们使用位操作时,完全可以保留位域的优势,规避位域的劣势。
布局完全可控:每一位的物理位置由掩码和移位量明确定义,不受编译器影响。
跨平台一致:无论大端小端、GCC还是ARMClang,生成的二进制数据完全相同。
语义清晰:通过命名常量暴露每一位的功能含义,不会产生Ghost Number。
总结
对于嵌入式驱动开发和应用开发,使用联合体配合位域结构体来映射硬件寄存器是常见做法,但前提是代码完全绑定在特定芯片和编译器上,比如CMSIS的头文件上面。
我认为除了上述应用使用位域下,在我们个人开发的项目里面就不要使用位域了,即便它看着是那么的美丽。
我要赚赏金
