☰
C语言中地址与指针的强制转换
2026/10/24 5:41:19 网站建设 项目流程

我们知道,给寄存器地址进行命名的过程叫做寄存器映射。映射只是把编号分配好,但编号对应的地方不一定都有物理存储。那么各位伙伴是否知道*(unsigned int*)(0x4001 080C)代表什么意思?

首先,寄存器地址分为三个部分:

  1. 总线基地址。
  2. 外设基于总线地址的偏移量。
  3. 寄存器相对于外设基地址的偏移量。
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 0000APB2 外设总线基地址,STM32 中 GPIOA 挂载在 APB2 总线上
外设基于总线地址的偏移量0x0001 0800GPIOA 外设相对于 APB2 总线基地址的偏移量
寄存器相对于外设基地址的偏移量0x0000 000CGPIOA_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*。这种转换不改变底层地址值,仅改变编译器对指针行为的解释方式。

指针类型的本质:规定了访问的规则。

  1. 访问一次取多少字节。
  2. 指针自增和自减时,地址跳多少。
  3. 取出数据后,怎么把这一串二进制比特翻译成数值。

最后补充一点:类型语义解释发生在编译期,地址访问发生在 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指令时。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询