我们知道,给寄存器地址进行命名的过程叫做寄存器映射。映射只是把编号分配好,但编号对应的地方不一定都有物理存储。那么各位伙伴是否知道*(unsigned int*)(0x4001 080C)代表什么意思?
首先,寄存器地址分为三个部分:
- 总线基地址。
- 外设基于总线地址的偏移量。
- 寄存器相对于外设基地址的偏移量。
flowchart TD A["总线基地址<br/>0x4000 0000"] --> B["+ 外设偏移量<br/>0x0001 0800"] B --> C["= GPIOA 外设基地址<br/>0x4001 0800"] C --> D["+ 寄存器偏移量<br/>0x0000 000C"] D --> E["= GPIOA_ODR 寄存器地址<br/>0x4001 080C"]上图展示了 GPIOA_ODR 寄存器地址的推导过程:总线基地址0x4000 0000加上外设偏移量0x0001 0800得到 GPIOA 外设基地址0x4001 0800,再加上寄存器偏移量0x0000 000C,最终得到0x4001 080C。
下面以 STM32 的 GPIOA 为例,把这三部分地址实际相加一次,看看0x4001 080C是怎么来的。
| 地址组成 | 数值 | 含义 |
|---|---|---|
| 总线基地址 | 0x4000 0000 | APB2 外设总线基地址,STM32 中 GPIOA 挂载在 APB2 总线上 |
| 外设基于总线地址的偏移量 | 0x0001 0800 | GPIOA 外设相对于 APB2 总线基地址的偏移量 |
| 寄存器相对于外设基地址的偏移量 | 0x0000 000C | GPIOA_ODR(输出数据寄存器)相对于 GPIOA 外设基地址的偏移量 |
| 寄存器物理地址 | 0x4001 080C | 三段地址相加的结果,即 GPIOA_ODR 寄存器的实际地址 |
把三段地址按十六进制逐位相加:0x4000 0000 + 0x0001 0800 + 0x0000 000C = 0x4001 080C。也就是说,GPIOA 外设的基地址是0x4001 0800,而 ODR 寄存器在 GPIOA 内部又偏移了0x0C,最终得到0x4001 080C。这个地址就是后面*(unsigned int*)(0x4001 080C)中那个十六进制常数的来源。
在*(unsigned int*)(0x4001 080C)中,0x4001 080C是一个十六进制常数。外设寄存器的物理地址(总线基地址 + 外设基于总线地址的偏移量 + 寄存器相对于外设基地址的偏移量)本身是一个数字,并不是指针。
(unsigned int*)(0x4001 080C)的含义是把该数字转换成一个无符号 int 型指针,也就是声明这个地址里存放的数据类型是 unsigned int(32 位)。注意,在这个过程中并没有开辟内存空间来存放这个地址,编译器也没有做任何内存读取或硬件查询,只是完成了类型标记替换,绑定了一套访问规则,即要取这个地址上无符号 4 字节的值,因为硬件地址本质上只是比特流。
最后,*(unsigned int*)(0x4001 080C)中的*表示指针解引用。编译器会生成 LDR 和 STR(读数据和写数据的指令)等汇编指令,按下图的操作顺序生成可执行程序,代表 CPU 取这个指针指向的地址,读取该地址上 32 bit 的值。
- 编译器:把 C 语言代码编译成汇编指令,确定了指针类型、解引用语义和访问宽度。
- 汇编器:把汇编指令翻译成机器码。
- 链接器:把多个目标文件合并成可执行程序。
程序加载到 Flash 或内存后,CPU 取指令、译码、执行。
指针变量存储的是内存地址,其类型决定了如何解释该地址所指向的数据。不同类型的指针在解引用时会按照对应的数据大小进行操作。例如,int*指向一个整型数据,访问时按 4 字节处理;char*则按 1 字节处理。
使用(type*)语法可将任意指针转换为另一种指针类型。例如:(int*)p将void*指针转换为int*。这种转换不改变底层地址值,仅改变编译器对指针行为的解释方式。
指针类型的本质:规定了访问的规则。
- 访问一次取多少字节。
- 指针自增和自减时,地址跳多少。
- 取出数据后,怎么把这一串二进制比特翻译成数值。
最后补充一点:类型语义解释发生在编译期,地址访问发生在 CPU 执行期。
下面用一段 C 语言代码把前面的地址推导和指针语义串起来:用宏定义 GPIOA_ODR 的寄存器地址,再通过*(unsigned int*)(0x4001 080C)完成读和写。
#include <stdio.h> /* 1. 总线基地址:APB2 外设总线基地址 */ #define PERIPH_BASE 0x40000000UL /* 2. 外设偏移量:GPIOA 相对于 APB2 总线基地址的偏移 */ #define APB2_GPIOA_OFFSET 0x00010800UL /* GPIOA 外设基地址 = 总线基地址 + 外设偏移量 = 0x4001 0800 */ #define GPIOA_BASE (PERIPH_BASE + APB2_GPIOA_OFFSET) /* 3. 寄存器偏移量:ODR 相对于 GPIOA 外设基地址的偏移 = 0x0C */ #define GPIOA_ODR_OFFSET 0x0CUL /* 寄存器物理地址 = 0x4001 0800 + 0x0C = 0x4001 080C */ #define GPIOA_ODR_ADDR (GPIOA_BASE + GPIOA_ODR_OFFSET) /* 把地址常量转换成 unsigned int* 指针,再解引用,得到可读写的 32 位寄存器 */ #define GPIOA_ODR (*(unsigned int *)(GPIOA_ODR_ADDR)) int main(void) { unsigned int value; /* 读:CPU 从 0x4001 080C 取 32 bit 数据,对应 LDR 指令 */ value = GPIOA_ODR; printf("GPIOA_ODR = 0x%08X\n", value); /* 写:把第 5 位置 1,其余位保持不变,对应 STR 指令 */ /* 1UL << 5 = 0x0000 0020,即 PA5 输出高电平 */ GPIOA_ODR |= (1UL << 5); /* 写:把第 5 位清 0,其余位保持不变,即 PA5 输出低电平 */ /* ~(1UL << 5) = 0xFFFF FFDF,按位与后仅第 5 位被清零 */ GPIOA_ODR &= ~(1UL << 5); /* 写:整体赋值,一次性改写 ODR 的全部 16 个输出位 */ GPIOA_ODR = 0x00000020UL; return 0; }代码中的每一步都能对应回前面的地址推导:PERIPH_BASE是总线基地址,APB2_GPIOA_OFFSET是外设偏移量,GPIOA_ODR_OFFSET是寄存器偏移量,三者相加得到0x4001 080C。而(unsigned int *)只负责告诉编译器“按 4 字节无符号整数访问”,真正的读写动作发生在 CPU 执行LDR和STR指令时。