打印
[APM32F4]

嵌入式C:揭秘 union 关键字在 SDK 寄存器位域中的应用

[复制链接]
127|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2025-6-29 11:49 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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)。



使用特权

评论回复
沙发
FrostShimmer| | 2025-6-29 16:47 | 只看该作者
我就挺不喜欢使用union类型的。
确实是省空间,但感觉这点空间也算不了什么?
要是从定义的角度来讲,union可更好说明数据结构的关系,唉!真是难办。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

53

主题

96

帖子

12

粉丝