本帖最后由 DKENNY 于 2025-6-29 11:48 编辑
#申请原创# #技术资源# @21小跑堂
前言
在嵌入式开发中,特别是在翻芯片 SDK 的时候,经常会遇到 `union` 关键字嵌在结构体里,配合位域(bit-field)使用,SDK的库函数中基本上全是这玩意儿,新手可能会比较头疼。比如下面这段代码,是我从apm32f407xx.h中copy过来的,其中定义了一个 CRC 模块的寄存器映射:
typedef struct {
/** [url=home.php?mod=space&uid=247401]@brief[/url] DATA register */
union {
__IOM uint32_t DATA;
struct {
__IOM uint32_t DATA : 32;
} DATA_B;
};
/** [url=home.php?mod=space&uid=247401]@brief[/url] independent DATA register */
union {
__IOM uint32_t INDATA;
struct {
__IOM uint32_t INDATA : 8;
__IM uint32_t RESERVED : 24;
} INDATA_B;
};
/** @brief Control register */
union {
__OM uint32_t CTRL;
struct {
__OM uint32_t RST : 1;
__IM uint32_t RESERVED : 31;
} CTRL_B;
};
} CRC_T;
代码里,`union` 和位域可能会让人出现几个疑问:`union` 到底是啥?为啥 SDK 这么设计?本文会从基础讲起,层层深入,解答这些疑问,确保搞懂 `union` 的用法和底层逻辑。
一、`union` 是什么?跟 `struct` 有什么区别?
1. `union` 的定义
`union`(联合体)是 C 语言的一种数据结构,允许在同一块内存存储不同类型的变量。注意,是同一块内存!这跟 `struct`(结构体)完全不同。
- 结构体(`struct`):每个成员有自己的内存空间,内存依次排列。比如:
struct Example {
uint32_t a; // 占 4 字节
uint8_t b; // 占 1 字节
};
这个结构体大小是 `4 + 1 = 5` 字节(忽略对齐)。`a` 和 `b` 各占一块独立内存,互不干扰。
- 联合体(`union`):所有成员共享同一块内存,大小由最大的成员决定。比如:
union Example {
uint32_t a; // 占 4 字节
uint8_t b; // 占 1 字节
};
这个 `union` 大小是 4 字节(`uint32_t` 是最大成员)。`a` 和 `b` 共享这 4 字节,改 `a` 会影响 `b`,因为它们是同一块内存的不同“视图”。
2. `union` 的核心特点
- 内存共享:所有成员从同一地址开始,改一个成员,其他成员的值会变。
- 大小确定:`union` 的大小是最大成员的大小(包括对齐)。
- 多视角访问:可以用不同类型或结构访问同一块内存。
二、SDK 中的 `union` 在干嘛?
我们来看 SDK 的 `CRC_T` 结构体。为了方便分析,我们还是把开头的代码copy过来哈。
typedef struct {
/** @brief DATA register */
union {
__IOM uint32_t DATA;
struct {
__IOM uint32_t DATA : 32;
} DATA_B;
};
/** @brief independent DATA register */
union {
__IOM uint32_t INDATA;
struct {
__IOM uint32_t INDATA : 8;
__IM uint32_t RESERVED : 24;
} INDATA_B;
};
/** @brief Control register */
union {
__OM uint32_t CTRL;
struct {
__OM uint32_t RST : 1;
__IM uint32_t RESERVED : 31;
} CTRL_B;
};
} CRC_T;
分析一下,它描述了一个 CRC(循环冗余校验)硬件模块的寄存器映射,包含三个寄存器:`DATA`、`INDATA` 和 `CTRL`。每个寄存器用 `union` 定义,提供两种访问方式:
1. 整体访问:直接读写整个 32 位寄存器,比如 `CRC_T.CTRL`。
2. 位域访问:用位域(bit-field)操作具体位,比如 `CRC_T.CTRL_B.RST`(只操作第 0 位)。
例子:
- `CRC_T.CTRL = 0x00000001;` 写整个 32 位,设置复位位。
- `CRC_T.CTRL_B.RST = 1;` 只设置第 0 位(复位位),其他位不动。
- `CRC_T.INDATA_B.INDATA = 0xFF;` 设置低 8 位,`RESERVED` 的 24 位保持不变。
`union` 的作用:让同一块 32 位内存(硬件寄存器)既可以整体读写,也可以按位操作,提供了两种“视角”。
三、为什么 SDK 用 `union` 这么设计?
SDK 用 `union` 是为了灵活性、可读性和内存效率,尤其在嵌入式系统中操作硬件寄存器时:
1. 多视角访问
- 整体写寄存器(`CRC_T.CTRL`)适合初始化或快速设置。
- 位域(`CRC_T.CTRL_B.RST`)适合精确控制某些位,代码更直观。
2. 代码可读性
- `CRC_T.CTRL_B.RST = 1;` 比 `CRC_T.CTRL |= (1 << 0);` 更清晰,名字直接对应芯片手册的位定义。
3. 内存效率
- `union` 让 `CTRL` 和 `CTRL_B` 共享内存,整个 `CRC_T` 结构体大小是三个寄存器(12 字节),没有冗余。
4. 硬件映射
- `CRC_T` 通常通过指针映射到硬件寄存器地址(比如 `CRC_T *crc = (CRC_T *)0x4000_1000;`)。`union` 确保整体和位域操作都作用于同一地址。
为什么不用单独的 `struct`?
- 不用 `union`,只能用位运算(`&`、`|`、移位)操作寄存器,代码复杂,易出错。
- `union` 结合位域让代码更简洁,减少手动位操作。
四、什么时候用 `union`?
`union` 在嵌入式开发中很常见,适合以下场景:
1. 硬件寄存器操作
- 映射控制、状态或数据寄存器,提供整体和按位访问。
- 如 SDK 的 `CRC_T` 结构。
2. 节省内存
- 当多种数据类型同一时间只用一种,`union` 节省空间。比如通信协议:
union Message {
uint32_t raw_data;
struct {
uint8_t type;
uint8_t id;
uint16_t value;
} parsed;
};
3. 数据转换
- 在 `float` 和 `uint32_t` 间转换,分析位模式:
union FloatConverter {
float f;
uint32_t u;
};
4. 协议解析
- 解析网络数据包,用不同结构体访问同一块数据。
啥时候不用?
- 如果成员需要同时存储不同值,用 `struct`。
- 如果不需要多视角访问,直接用简单变量或位域。
五、位域(bit-field)的基础知识
位域是结构体中指定成员占用位数的机制,用 `: 数字` 表示。比如:
struct {
uint32_t RST : 1; // 占 1 位
uint32_t RESERVED : 31; // 占 31 位
} CTRL_B;
规则
- 指定位数:如 `RST : 1` 占 1 位,位数不能超过类型宽度(`uint32_t` 是 32 位)。
- 不指定位数:用类型宽度,比如 `uint16_t RST;` 占 16 位。
- 分配顺序:按声明顺序依次分配,通常在一个存储单元(storage unit,常见为 `int` 或类型大小)内。
- 对齐和端序:分配受编译器和硬件端序(大端/小端)影响。
在 `union` 中,位域和整体变量共享同一内存,需确保总位数不超过 `union` 大小(这里是 32 位)。
六、扩展1:把 `RESERVED` 改成 33 位会怎样?
这里,我们进行一个改动,把 `CTRL` 的 `union` 改成像下面这样,看看会发生什么。
union {
__OM uint32_t CTRL;
struct {
__OM uint32_t RST : 1;
__IM uint32_t RESERVED : 33;
} CTRL_B;
};
嗯,意料之中,这玩意儿它报错了!
1. 为什么会报错?
- 位域总位数是 `RST`(1 位)+ `RESERVED`(33 位)= 34 位。
- 但 `uint32_t` 只有 32 位,34 位超出了类型宽度。
- 结果:编译器报错(比如 GCC 会说 `error: width of 'RESERVED' exceeds its type`)。
2. `RESERVED` 表示什么?
`RESERVED` 是位域的“占位符”,对应硬件寄存器的保留位(通常不可写,读可能是 0 或未定义)。原代码中,`RESERVED : 31` 表示 `CTRL` 的高 31 位是保留位,`RST` 是第 0 位。
3. RESERVED这玩意儿会不会溢出到下一个地址?
不会!`union` 的所有成员共享同一块内存(比如 `0x4000_1008`)。位域 `CTRL_B` 必须装在 `CTRL` 的 32 位里,`RESERVED : 33` 不会溢出到下一个地址(比如 `0x4000_100C`),而是直接报错。
修复方法:
- 确保位域总位数不超过 32 位(比如 `: 31`)。
- 如果硬件有多个寄存器,用两个 `uint32_t`:
union {
struct {
uint32_t CTRL;
uint32_t CTRL_EXT;
};
struct {
__OM uint32_t RST : 1;
__IM uint32_t RESERVED : 31;
__IM uint32_t RESERVED2 : 2;
} CTRL_B;
};
七、扩展2:去掉 `: 数字`,用 `uint16_t` 会怎样?
我们再想想,如果去掉位域的 `: 数字`,改成下面这样,又会发生什么?
union {
__OM uint32_t CTRL;
struct {
__OM uint16_t RST;
__IM uint16_t RESERVED;
} CTRL_B;
};
这样改完后,有一个疑问,联合体(union)包含的struct结构体,其中的RST和RESERVED的位域位是会顺延吗?也就是说,是RST占CTRL的前面16位,RESERVED占后面的16位这样?还是说一起共用CTRL的前面16位或者都是后面16位?
不知道啊,怎么办呢,咱们写个代码验证一下:
#include <stdio.h>
#include <stdint.h>
union {
uint32_t CTRL;
struct {
uint16_t RST;
uint16_t RESERVED;
} CTRL_B;
} test;
int main() {
test.CTRL = 0x56781234;
printf("CTRL: 0x%08X\n", test.CTRL);
printf("RST: 0x%04X\n", test.CTRL_B.RST); // 预期 0x1234
printf("RESERVED: 0x%04X\n", test.CTRL_B.RESERVED); // 预期 0x5678
test.CTRL_B.RST = 0xABCD;
printf("After RST = 0xABCD, CTRL: 0x%08X\n", test.CTRL); // 预期 0x5678ABCD
test.CTRL_B.RESERVED = 0xEF01;
printf("After RESERVED = 0xEF01, CTRL: 0x%08X\n", test.CTRL); // 预期 0xEF01ABCD
return 0;
}
结果:
1. 位域分配规则
- 没写 `: 数字`,编译器根据类型分配:
- `uint16_t RST;` 占 16 位。
- `uint16_t RESERVED;` 占 16 位。
- 总共 16 + 16 = 32 位,刚好等于 `union` 的 32 位(由 `uint32_t CTRL` 决定)。
- 分配顺序:
- 位域按声明顺序顺延分配。
- 假设小端序(常见于 ARM),`RST` 占低 16 位(bit 0-15),`RESERVED` 占高 16 位(bit 16-31)。
- 内存布局:
- `CTRL`:整个 32 位(bit 0-31)。
- `CTRL_B.RST`:低 16 位(bit 0-15)。
- `CTRL_B.RESERVED`:高 16 位(bit 16-31)。
2.地址顺延还是共享?
顺延!`RST` 占前 16 位(bit 0-15),`RESERVED` 占后 16 位(bit 16-31)。它们**不共享**同一块 16 位,因为位域按顺序依次分配,填满 32 位内存。
3. 与 `CTRL` 的关系
`CTRL` 和 `CTRL_B` 共享同一地址:
- 写 `CTRL_B.RST = 0x1234;` 改 `CTRL` 的低 16 位。
- 写 `CTRL_B.RESERVED = 0x5678;` 改 `CTRL` 的高 16 位。
- 写 `CTRL = 0x56781234;` 影响 `RST`(0x1234)和 `RESERVED`(0x5678)。
4. 会不会有问题?
代码合法,不会报错,因为 16 + 16 = 32 位,匹配 `union` 大小。但需注意:
- 硬件匹配:确认寄存器是否真的分成低 16 位(控制)和高 16 位(保留)。
- 端序:小端序下,`RST` 是低 16 位;大端序可能反过来,需查手册。
- 编译器差异:位域分配依赖编译器(GCC、Keil 等),可能有细微差异。
八、总结
核心要点:
- `union` 让同一块内存(寄存器)支持多视角访问(整体和位域),提高灵活性和可读性。
- 位域按声明顺序顺延分配,需确保总位数不超过类型或 `union` 大小。
- 扩展 1(`RESERVED : 33`):超 32 位,编译报错,不会溢出到下一地址。
- 扩展 2(`uint16_t RST` 和 `RESERVED`):顺延分配,`RST` 占低 16 位,`RESERVED` 占高 16 位,合法但需匹配硬件。
- 始终参考硬件手册,确认寄存器定义和端序。
最后的扩展:不同编译器(GCC/Keil/IAR)对位域分配的差异有哪些?(见下表)
特性
| GCC
| Keil
| IAR
| 存储单元大小
| 通常使用位域声明的类型大小(如 uint32_t 为 32 位)。可能因架构(如 ARM)调整存储单元。
| 基于目标架构,通常以 int 或声明类型(如 uint32_t)为存储单元。倾向于紧凑分配。
| 通常以声明类型(如 uint32_t)为存储单元,优化嵌入式设备紧凑性。
| 位顺序
| 小端序:从低位(LSB)到高位(MSB)分配。大端序可能反过来。依赖目标架构(如 ARM Cortex-M)。
| 小端序:通常从低位到高位分配,匹配 ARM 架构。明确支持硬件寄存器映射。
| 小端序:从低位到高位分配,优化 ARM 架构。支持硬件寄存器位域映射。
| 对齐方式
| 可能插入填充以满足架构对齐要求(如 4 字节对齐)。可通过 #pragma pack 或 -fpack-struct 控制。
| 倾向于紧凑分配,减少填充,适合嵌入式资源限制。对齐依赖目标(如 ARM)。
| 优化紧凑分配,尽量减少填充。支持 __packed 等扩展控制对齐。
| 未命名位域(如 : 0)
| 强制换到新存储单元(如下一个 int 或类型大小)。行为明确,但依赖架构对齐。
| 类似 GCC,强制新存储单元。可能因目标(如 Cortex-M)调整对齐。
| 支持 : 0 强制新存储单元,行为与硬件寄存器对齐一致。
| 超大位域处理
| 超大位域(如 : 33 在 uint32_t)通常报警告或错误,具体取决于 -Werror 设置。
| 严格检查,超大位域(如 : 33)报错,防止溢出。强调硬件一致性。
| 严格检查,超大位域报错。支持硬件寄存器映射,行为更可预测。
| 可移植性
| 高度依赖目标架构和端序。需明确指定选项(如 -mstrict-align)提高一致性。
| 针对 ARM 优化,跨平台一致性较好,但仍需测试不同目标。
| 针对多种架构(ARM、8051 等)优化,跨平台一致性较高。
| 扩展支持
| 支持标准 C 位域,扩展较少。需手动配置(如 linker 脚本)。
| 支持 sfr、sbit 等扩展,简化寄存器操作。
| 支持 sfr、sbit、__packed 等扩展,优化嵌入式寄存器操作。
| 优缺点
| 优点:免费,灵活,支持多种架构。缺点:需手动配置,位域行为可能不一致。
| 优点:集成 IDE,优化 ARM 寄存器操作。缺点:商业授权,成本高。
| 优点:紧凑代码,硬件支持强。缺点:昂贵,C++ 标准支持较旧(如 C++14)。
|
|