在硬件寄存器中一个字节往往分多个控制位,使用位域读写不用移位,编写代码简洁。那位域是什么?在什么情况下使用?使用需要注意什么呢?本文将一一展开。
一、位域简介
1、定义
位域(Bit Fields)是一种结构体的特殊成员,允许指定一个变量只占用若干个二进制bit位,而不是完整字节。用于在结构体中以位为单位分配存储空间,节省存储空间,特别适用于寄存器操作、协议解析、嵌入式系统等场景。
语法格式:
struct 结构体名{
类型 成员名 :占用位数;//后面数字=该成员占用二进制的位数
}
2、结构体选择位域还是普通char/int成员的原则
全部满足下面3个条件,优先使用位域,否则直接使用普通整型
a变量取值范围极小,只占用少量bit(1-12位以内)
b多个小字段挤在同一个字节/字,目标是节省内存/匹配硬件寄存器/匹配单字节协议位布局
c只在本地固件单一编译器运行,不需要跨编译器、跨平台传输二进制流。
二、位域的使用场景
1、使用场景
硬件寄存器读写(单片机、GNSS、485芯片寄存器)
芯片寄存器一个字节分多个功能标志位,每一位代表一个使能/状态,用位域直接映射寄存器,不用移位与运算。
通信协议报文
协议单个字节内拆分多个小字段,eg:1bit故障位,3bit功能码,不用手动移位拼接字节
大量布尔状态存储
多种故障状态,如果每个都用char需要几十个字节。
配置参数压缩
一些取值范围很小的参数(eg:速度配置只有7种状态,只需要3bit),无需完整字节存储。
2、具体示例
(1)基础标志位(故障状态集合)
#include<stdio.h>#include<windows.h>#include<math.h>structFaultBit{unsignedshortbat_low:1;//电量低unsignedshortwheel_lock:1;//后轮锁故障unsignedshortbat_lock:1;//电池锁故障unsignedshortmotor_err:1;//电机故障unsignedshortover_speed:1;//超速unsignedshortbrake_err:1;//刹车故障unsignedshortreserve:2;//保留位};intmain(){SetConsoleOutputCP(65001);SetConsoleCP(65001);structFaultBiterr;err.bat_lock=1;err.over_speed=1;printf("结构体大小:%zu字节\n",sizeof(err));printf("电池锁状态:%d\n",err.bat_lock);printf("后轮锁状态:%d\n",err.wheel_lock);return0;}运行结果:
(2)用于特定协议:单字节分段
#include<stdio.h>#include<windows.h>#include<math.h>#pragmapack(push,1)structProtocolData{unsignedcharspeed_level:4;//速度档位0-15unsignedcharbike_state:3;//车辆状态unsignedcharfault_flag:1;//故障标志位};#pragmapack(pop)intmain(void){SetConsoleOutputCP(65001);SetConsoleCP(65001);structProtocolDatadata;data.speed_level=6;data.bike_state=1;data.fault_flag=0;printf("结构体大小:%zu字节\n",sizeof(data));printf("车辆速度等级:%d\n",data.speed_level);printf("车辆状态:%d\n",data.bike_state);printf("车辆故障状态:%d\n",data.fault_flag);return0;}运行结果:
(3)硬件寄存器映射
单片机IO寄存器,8位寄存器每一位对应一个IO口高低电平
#include<stdio.h>#include<windows.h>#include<math.h>typedefstruct{unsignedchario0:1;unsignedchario1:1;unsignedchario2:1;unsignedchario3:1;unsignedchario4:1;unsignedchario5:1;unsignedchario6:1;unsignedchario7:1;}IO_REG;//将0x4000强制映射位IO_REG寄存器#defineIO_ADDR(*(volatileIO_REG*)0x4000)intmain(){IO_ADDR.io0=1;//访问0x4000地址里面的io0位域return0;}(4)跨多字节位域和无名位域
当定义超过8bit,自动顺延至下一个字节
冒号前不写变量名,仅占位,用于协议保留bit,无法进行读写
structInfo{unsignedinta:5;unsignedinta:6;//超过8bit自动顺延下一个字节unsignedinta:7;unsignedint:6;//无名位域,填充保留位};三、位域的存储
1、位域是按字节存储的,但不会跨字节(他们通常会放在同一个int或char变量的二进制中)
2、位域的大小受限于基础数据类型,如int或char,不能使用float、double等类型
3、位域的存储方式依赖编译器(大小端、高低位差异),不同CPU可能有不同大小端规则
GCC:低位放低bit
低版本Keil:高位在前,如果直接把位域结构体强转成字节发485/can,跨芯片、跨编译器会解析错误
所以:协议报文尽量不用位域,改用移位手动拼接。
四、位域使用
1、限制变量的值范围
2、调整字段顺序以优化存储
尽量控制1个字节可完成定义,相同类型写在一起
上图中变量a和b存放在一个char里,c存放不下了,放在第二个字节,d类型是int,偏移量在4的倍数上开始存储,所以需要填充2字节,本结构体共8字节。
位域成员的排列除结构体规则外,增加一条新规则:
相同类型、连续的成员在空间允许的情况下可以连续存放,不同类型的成员通常会跨单元存储,不同编译器处理可能不一样。
五、注意事项
1、位域基础类型只能是无符号unsigned,不能用于float、double
2、不能对位域取地址
位域内部没有存储对齐,bit没有独立的内存地址,指针最小寻址单位是字节
3、跨平台问题
位域的存储顺序依赖CPU大小端,不同平台可能不同。
不同编译器对齐可能不同,因此位域结构在不同平台上可能表现不一致。
4、位域成员不能是数组
5、超出bit范围会溢出,无自动报错
eg:某字段最大为15,但是填充为16直接截断变成0,运行静默出错,不会报警
6、跨字节自动填充,大小不直观
7、不要用于通用业务逻辑
仅硬件寄存器、通信协议、大量布尔标记;普通变量直接用char int,CPU操作位域需要额外移位运算,效率更低
8、无名位域仅占位,无法读写
用于协议保留位填充,只能填充不能赋值读取