位域(Bit Field)与位操作正是“以位为单位”描述数据结构的利器,但 C 语言 原生位域具有实现相关性(implementation-defined)与未定义行为风险。
适用场景
在 MCU/SoC、RTOS 与裸机项目中,我们往往需要精细控制硬件寄存器、通信协议字段与存储布局,目标包括:
- 降低 RAM/Flash 占用,缩短数据帧长度
- 以最少的访存实现寄存器原子修改
- 确保跨平台/跨编译器的数据表示一致
- 提升可读性、可维护性与可测试性
位、掩码与位域
- 概念
- 位(bit):二进制最小存储单元;位位置从最低位 bit0 递增
- 掩码(mask):用 1 指示目标位,用 0 屏蔽无关位
- 位域(bit field):一段连续的位区间,用作一个数值字段
- 三大基本操作(以无符号整型
y 与掩码 mask 为例)
#define BIT(n) ( (uint32_t)1u << (n) )
#define BIT_SET(y, mask) ( (y) |= (mask) )
#define BIT_CLEAR(y, mask) ( (y) &= ~(mask) )
#define BIT_FLIP(y, mask) ( (y) ^= (mask) )
- 位域的“取值/赋值”通式
- 取值:
value = (y >> start) & mask
- 赋值:
y = (y & ~(mask << start)) | ((x & mask) << start)
寄存器映射的安全访问
目标:对硬件寄存器的某个位域进行“读-改-写”,且不破坏其他位。
示例:假设有 32 位寄存器 CTRL,其字段如下:
- bit[0]
EN:使能位
- bit[3:1]
MODE:工作模式(3 位)
- bit[7:4]
DIV:分频系数(4 位)
推荐使用“手工位操作 + 明确掩码”的方式,避免 C 位域的不确定布局。
#include <stdint.h>
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define CTRL_BASE (0x40000000u)
#define CTRL REG32(CTRL_BASE)
// 字段位置与掩码(面向移植独立)
#define CTRL_EN_POS (0u)
#define CTRL_EN_MASK (0x1u)
#define CTRL_MODE_POS (1u)
#define CTRL_MODE_MASK (0x7u) // 3 bits
#define CTRL_DIV_POS (4u)
#define CTRL_DIV_MASK (0xFu) // 4 bits
// 通用位域操作(可复用宏,见后文宏库)
#define BF_PREP(x, pos, mask) ( ((uint32_t)(x) & (mask)) << (pos) )
#define BF_GET(y, pos, mask) ( ((uint32_t)(y) >> (pos)) & (mask) )
#define BF_CLR(y, pos, mask) ( (uint32_t)(y) & ~((mask) << (pos)) )
#define BF_SET(y, x, pos, mask) ( BF_CLR((y), (pos), (mask)) | BF_PREP((x), (pos), (mask)) )
static inline void ctrl_enable(uint8_t en)
{
uint32_t r = CTRL;
r = BF_SET(r, en ? 1u : 0u, CTRL_EN_POS, CTRL_EN_MASK);
CTRL = r;
}
static inline void ctrl_set_mode(uint32_t mode)
{
uint32_t r = CTRL;
r = BF_SET(r, mode, CTRL_MODE_POS, CTRL_MODE_MASK);
CTRL = r;
}
static inline uint32_t ctrl_get_div(void)
{
return BF_GET(CTRL, CTRL_DIV_POS, CTRL_DIV_MASK);
}
要点:
- 所有位置与掩码集中定义,便于查阅与自动生成(SVD/CSV/DSL 转换)。
- 读-改-写前先读取寄存器镜像到局部变量,减少总线读次数。
- 宏
BF_SET 先清后置,保证只修改目标位域。
协议/存储打包的跨端一致性
常见的做法,是把多个小字段压缩到紧凑的字节流中,再进行传输或持久化。
推荐做法:以“明确字节序 + 手工位操作”的方式构建/解析报文。
示例:将 10-bit tile、2-bit prio、4-bit pal 打包为 16 位字段(与 GBA OAM attr2 类似的布局)。
#include <stdint.h>
#define ATTR2_TILE_POS (0u)
#define ATTR2_TILE_MASK (0x3FFu) // 10 bits
#define ATTR2_PRIO_POS (10u)
#define ATTR2_PRIO_MASK (0x3u) // 2 bits
#define ATTR2_PAL_POS (12u)
#define ATTR2_PAL_MASK (0xFu) // 4 bits
static inline uint16_t pack_attr2(uint32_t tile, uint32_t prio, uint32_t pal)
{
uint32_t y = 0u;
y = BF_SET(y, tile, ATTR2_TILE_POS, ATTR2_TILE_MASK);
y = BF_SET(y, prio, ATTR2_PRIO_POS, ATTR2_PRIO_MASK);
y = BF_SET(y, pal, ATTR2_PAL_POS, ATTR2_PAL_MASK);
return (uint16_t)y;
}
static inline void unpack_attr2(uint16_t v, uint32_t *tile, uint32_t *prio, uint32_t *pal)
{
if (tile) *tile = BF_GET(v, ATTR2_TILE_POS, ATTR2_TILE_MASK);
if (prio) *prio = BF_GET(v, ATTR2_PRIO_POS, ATTR2_PRIO_MASK);
if (pal) *pal = BF_GET(v, ATTR2_PAL_POS, ATTR2_PAL_MASK);
}
如需跨字节序传输:统一采用 网络序(大端),在入/出站时用 htons/ntohs 或自定义字节翻转。
通用位操作/位域宏库
#ifndef BITFIELD_UTILS_H
#define BITFIELD_UTILS_H
#include <stdint.h>
// 单比特与多比特掩码
#define BIT_U32(n) ( (uint32_t)1u << (uint32_t)(n) )
// 生成 len 位的低位掩码;对 len==32 做安全处理
#define U32_MASK_LEN(len) ( ((uint32_t)(len) >= 32u) ? 0xFFFFFFFFu : ((uint32_t)1u << (uint32_t)(len)) - 1u )
// 基础位操作
#define BIT_SET_U32(y, mask) ( (y) |= (uint32_t)(mask) )
#define BIT_CLEAR_U32(y, mask) ( (y) &= ~(uint32_t)(mask) )
#define BIT_FLIP_U32(y, mask) ( (y) ^= (uint32_t)(mask) )
// 位域预处理/清除/获取/设置(pos: 起始位;len: 位宽)
#define BF_MASK_U32(pos, len) ( U32_MASK_LEN(len) << (uint32_t)(pos) )
#define BF_PREP_U32(x, pos, len) ( ((uint32_t)(x) & U32_MASK_LEN(len)) << (uint32_t)(pos) )
#define BF_GET_U32(y, pos, len) ( ((uint32_t)(y) >> (uint32_t)(pos)) & U32_MASK_LEN(len) )
#define BF_CLR_U32(y, pos, len) ( (uint32_t)(y) & ~BF_MASK_U32((pos), (len)) )
#define BF_SET_U32(y, x, pos, len) ( BF_CLR_U32((y), (pos), (len)) | BF_PREP_U32((x), (pos), (len)) )
// 若已手工定义了掩码,用下列宏避免重复计算掩码
#define BF_PREP_MASK_U32(x, pos, mask) ( ((uint32_t)(x) & (uint32_t)(mask)) << (uint32_t)(pos) )
#define BF_GET_MASK_U32(y, pos, mask) ( ((uint32_t)(y) >> (uint32_t)(pos)) & (uint32_t)(mask) )
#define BF_CLR_MASK_U32(y, pos, mask) ( (uint32_t)(y) & ~((uint32_t)(mask) << (uint32_t)(pos)) )
#define BF_SET_MASK_U32(y, x, pos, mask) ( BF_CLR_MASK_U32((y), (pos), (mask)) | BF_PREP_MASK_U32((x), (pos), (mask)) )
#endif // BITFIELD_UTILS_H
这些宏已经可以直接复制进 BSP 或驱动层,配合寄存器映射和协议打包使用。云栈社区上也有不少嵌入式开发者分享过类似的位域封装实践,遇到移植性问题时不妨对照一下。