结构体对齐

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

134.5.6结构体对齐

/*
 * 整体目的:用普通结构体观察对齐与填充,再理解 CMSIS 为什么能用 TIM_TypeDef 精确映射硬件寄存器。
 * 核心规则:成员按各自对齐要求放置;结构体总大小补齐到最大成员对齐值的整数倍。
 * 硬件意义:TIM1 基地址加上成员偏移必须等于参考手册规定的寄存器地址,
 *           因而寄存器结构体的成员顺序、宽度和 RESERVED 占位都不能随意改变。
 * 代码范围:前半部分是教学示例,后半部分是 STM32F103 TIM_TypeDef 的相关概念摘录;
 *           本节不涉及需要逐行解释的 HAL 库函数。
 */

#include <stdint.h>                         /* 提供 uint8_t、uint32_t 等位宽明确的整数类型。 */
#include <stddef.h>                         /* 提供 offsetof(),可在编译期取得结构体成员偏移。 */

typedef struct                               /* 定义成员顺序较容易产生填充的示例结构体。 */
{                                            /* ExampleA 成员列表开始。 */
    uint8_t a;                              /* 占 1 字节,通常位于偏移 0。 */
    uint32_t b;                             /* 通常要求 4 字节对齐,因此 a 后插入 3 字节填充。 */
    uint8_t c;                              /* 占 1 字节,通常位于偏移 8。 */
} ExampleA;                                 /* 末尾通常再补 3 字节,使总大小成为 12。 */

typedef struct                               /* 定义功能相同但成员排序更紧凑的结构体。 */
{                                            /* ExampleB 成员列表开始。 */
    uint8_t a;                              /* 第一个 1 字节成员位于偏移 0。 */
    uint8_t c;                              /* 第二个 1 字节成员紧随其后,位于偏移 1。 */
    uint32_t b;                             /* 为满足 4 字节对齐,前面通常只需补 2 字节。 */
} ExampleB;                                 /* 典型 32 位 ARM ABI 下总大小通常为 8。 */

_Static_assert(                             /* 在支持 C11 的编译器中验证 ExampleA 的典型布局。 */
    offsetof(ExampleA, b) == 4U,            /* b 应从结构体起始地址后的第 4 字节开始。 */
    "Unexpected ExampleA layout");          /* 若 ABI 或打包选项不同,则让编译器报告布局变化。 */
_Static_assert(                             /* 再验证紧凑排序后的典型结构体大小。 */
    sizeof(ExampleB) == 8U,                 /* a、c、2 字节填充和 b 合计通常为 8 字节。 */
    "Unexpected ExampleB size");            /* 不满足时提示不能继续套用当前布局假设。 */

/* ==================== CMSIS 寄存器映射概念摘录 ==================== */

typedef struct                               /* 按参考手册顺序描述定时器寄存器地址。 */
{                                            /* TIM_TypeDef 相关成员摘录开始。 */
    volatile uint32_t CR1;                  /* 偏移 0x00:控制寄存器 1。 */
    volatile uint32_t CR2;                  /* 偏移 0x04:控制寄存器 2。 */
    volatile uint32_t SMCR;                 /* 偏移 0x08:从模式控制寄存器。 */
    volatile uint32_t DIER;                 /* 偏移 0x0C:DMA/中断使能寄存器。 */
    volatile uint32_t SR;                   /* 偏移 0x10:状态寄存器。 */
    volatile uint32_t EGR;                  /* 偏移 0x14:事件产生寄存器。 */
    volatile uint32_t CCMR1;                /* 偏移 0x18:捕获/比较模式寄存器 1。 */
} TIM_TypeDef_Excerpt;                      /* 使用不同类型名表明这里只是教学摘录,并非完整 CMSIS 定义。 */

#define PERIPH_BASE 0x40000000UL            /* STM32F1 外设地址空间的基地址。 */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL) /* APB2 外设区域从 0x40010000 开始。 */
#define TIM1_BASE (APB2PERIPH_BASE + 0x00002C00UL) /* TIM1 寄存器块基地址为 0x40012C00。 */
#define TIM1_EXCERPT ((TIM_TypeDef_Excerpt *)TIM1_BASE) /* 把数值地址解释为寄存器结构体指针。 */

uint32_t dier_value = TIM1_EXCERPT->DIER;   /* 访问成员偏移 0x0C,即读取地址 0x40012C0C。 */

一、TIM1和TIM8控制寄存器

STM32 的外设采用存储器映射 I/O:CPU 用普通地址读写指令访问外设寄存器。CMSIS 用结构体把连续寄存器描述出来,让 TIM1->PSC 这样的写法既直观又能保持偏移准确。

1.1 寄存器描述

真正描述寄存器布局的是 TIM_TypeDef,不是初始化参数结构体。其开头部分可概括为:

偏移成员寄存器
0x00CR1控制寄存器 1
0x04CR2控制寄存器 2
0x08SMCR从模式控制寄存器
0x0CDIERDMA/中断使能寄存器
0x10SR状态寄存器
0x14EGR事件产生寄存器
0x18CCMR1捕获/比较模式寄存器 1

每个成员是 __IO uint32_t,占 4 字节。__IO 最终带有 volatile 语义,告诉编译器这些数值可能由硬件改变,每一次访问都不能凭空省略。

1.2 存储器映像

TIM1 地址的宏展开关系为:

PERIPH_BASE      = 0x40000000
APB2PERIPH_BASE  = PERIPH_BASE + 0x00010000
                 = 0x40010000
TIM1_BASE        = APB2PERIPH_BASE + 0x00002C00
                 = 0x40012C00
TIM1             = (TIM_TypeDef *)TIM1_BASE

所以 TIM1->DIER 的地址为 0x40012C00 + 0x0C = 0x40012C0C。结构体成员偏移必须和芯片手册完全一致,一旦在中间少了成员、类型宽度不对或顺序改变,后面的访问就会落到错误寄存器。

二、结构体对齐

结构体对齐是编译器为了满足处理器访问要求而插入空隙的规则。它会影响普通数据结构的大小,更会直接决定外设寄存器结构体的地址偏移。

2.1 结构体对⻬概念

假设某成员要求按 4 字节对齐,那么它的起始地址通常应是 4 的整数倍。编译器可能在前一个成员后添加填充字节:

低地址                                                     高地址
┌──────┬──────────────┬────────────────────┬────────────────────┐
│ char │ padding 3B   │ int                │ float              │
│ 0x00 │ 0x01~0x03   │ 0x04~0x07         │ 0x08~0x0B         │
└──────┴──────────────┴────────────────────┴────────────────────┘
总大小:12 字节(典型 ARM EABI 环境)

填充字节不保存成员数据,却能让 CPU 以合适的边界访问 int 和 float。

2.2 结构体对⻬规则

常见 ABI 下可按三步判断:

  1. 每个成员从满足其对齐要求的下一个地址开始;成员的自然对齐还可能受到 ABI 或 pack 上限约束。
  2. 结构体整体对齐要求通常取所有成员对齐要求的最大值。
  3. 结构体末尾补齐,使总大小成为整体对齐值的整数倍,便于结构体数组中每个元素都正确对齐。

不能简单死记“32 位一定按 4 字节、64 位一定按 8 字节”。不同编译器、ABI、编译选项和 #pragma pack 可能改变规则。需要确认时,应使用 sizeof、offsetof 和编译器 ABI 文档。

2.3 结构体对⻬例子

在典型 32 位 ARM 环境中:

typedef struct {
    char a;
    int  b;
    char c;
} A;             // 常见大小:12

typedef struct {
    char a;
    char c;
    int  b;
} B;             // 常见大小:8

只调整成员顺序就可能减少填充。但外设寄存器结构体不能为了省空间随意排序,因为成员位置就是硬件地址。寄存器地址之间若存在空洞,CMSIS 会显式放置 RESERVED 数组占位。

还要避免把寄存器结构体强行设为 1 字节紧凑对齐。这样可能破坏 32 位寄存器的地址边界,并生成不合适的访问指令。寄存器布局应由芯片厂商提供的 CMSIS 头文件定义。

例如一个含 15 个 uint32_t 的结构体通常占 60 字节。若把其中一个成员换成 uint8_t,成员有效数据之和虽然变成 57 字节,结构体仍可能因后续成员和末尾对齐而占 60 字节。总大小不变不代表偏移没变,仍应逐项检查 offsetof。

三、思维导图

思维导图节点可添加课程资料或手册链接,用于把概念与依据连接起来。打开链接的具体动作取决于所用软件,视频中的软件可通过右键单击链接进入。整理本节时,应让“对齐规则”与“外设寄存器映射”成为两条主分支,避免只记普通结构体大小而忽略硬件地址。

flowchart TD
    A[结构体布局] --> B[成员自身对齐]
    A --> C[成员间填充]
    A --> D[末尾填充]
    B --> E[受类型 ABI pack约束]
    C --> F[下一个成员落在合法边界]
    D --> G[结构体数组仍保持对齐]
    A --> H[外设寄存器映射]
    H --> I[基地址加成员偏移]
    H --> J[volatile保证真实读写]
    H --> K[RESERVED保持手册偏移]
    I --> L[TIM1指向0x40012C00]