寄存器映射你可以这样理解:

STM32 把每个外设的控制开关,都做成了一个个“寄存器”;然后给每个寄存器分配一个固定地址。CPU 通过读写这些地址,就能控制外设。

比如控制 GPIO、定时器、串口,本质上都是在操作寄存器。

 

1. 什么是寄存器?

在 STM32 里,寄存器不是普通变量,而是控制硬件的特殊存储单元。

例如 GPIOA 里面有这些寄存器:

寄存器

作用

CRL

配置 PA0~PA7 的模式

CRH

配置 PA8~PA15 的模式

IDR

读取输入电平

ODR

控制输出电平

BSRR

置位/复位输出电平

比如你想让 PA0 输出高电平,本质就是:

GPIOA->ODR |= (1 << 0);

这句话不是普通赋值,它是在修改 GPIOA 的输出数据寄存器。

 

2. 什么是寄存器映射?

寄存器映射就是:

把外设里的每个寄存器,对应到一个固定的内存地址。

例如:

GPIOA 基地址 = 0x40010800

GPIOA 内部寄存器的偏移如下:

寄存器

偏移地址实际地址CRL

0x000x40010800CRH

0x040x40010804IDR

0x080x40010808ODR

0x0C0x4001080CBSRR

0x100x40010810公式就是:

外设寄存器地址 = 外设基地址 + 寄存器偏移地址

例如 GPIOA 的 ODR:

GPIOA_ODR = 0x40010800 + 0x0C

          = 0x4001080C

所以你写:

GPIOA->ODR

本质上就是在访问:

*(volatile unsigned int *)0x4001080C

 

3. 为什么 CPU 能控制 GPIO?

因为 STM32 把 GPIO 的控制寄存器放到了内存地址里。

比如你想点亮 LED,假设 LED 接在 PA0:

GPIOA->ODR |= (1 << 0);

底层过程是:

CPU 写地址 0x4001080C

这个地址是 GPIOA 的 ODR 寄存器

ODR 第 0 位变成 1

PA0 输出高电平

LED 状态改变

所以重点是:

CPU 不是直接“摸到引脚”,而是通过寄存器间接控制引脚。

 

4. 寄存器映射和存储器映射的区别

这两个很容易混。

存储器映射

是大范围划分。

例如:

0x08000000:Flash

0x20000000:SRAM

0x40000000:外设寄存器区

0xE0000000:内核外设区

它回答的是:

Flash 在哪里?SRAM 在哪里?外设在哪里?

 

寄存器映射

是某个外设内部的详细地址划分。

例如 GPIOA:

GPIOA_BASE = 0x40010800

GPIOA_CRL  = 0x40010800

GPIOA_CRH  = 0x40010804

GPIOA_IDR  = 0x40010808

GPIOA_ODR  = 0x4001080C

它回答的是:

GPIOA 里面每个寄存器在哪里?

可以这样记:

存储器映射:大地图

寄存器映射:某个外设的小地图

 

5. 为什么代码里是

GPIOA->ODR

这是因为官方库帮我们把地址包装成了结构体。

比如 GPIO 的寄存器结构大概是这样:

typedef struct

{

    volatile uint32_t CRL;

    volatile uint32_t CRH;

    volatile uint32_t IDR;

    volatile uint32_t ODR;

    volatile uint32_t BSRR;

    volatile uint32_t BRR;

    volatile uint32_t LCKR;

} GPIO_TypeDef;

然后定义 GPIOA:

#define GPIOA_BASE 0x40010800

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)

所以:

GPIOA->ODR

就等价于:

访问 GPIOA_BASE + ODR 偏移地址

也就是:

0x40010800 + 0x0C = 0x4001080C

 

6. 为什么要有

volatile

寄存器定义里经常有:

volatile uint32_t ODR;

volatile 的意思是:

这个变量可能随时被硬件改变,编译器不要随便优化它。

比如 GPIO 的输入寄存器 IDR,引脚电平可能外部变化。

如果没有 volatile,编译器可能认为这个值不会变,就不重新读取,程序可能出错。

所以寄存器一般都要加 volatile。

 

7. 举个完整例子:开启 GPIOA 时钟

你之前经常写:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

这句话本质上是在操作 RCC 的寄存器。

RCC 基地址:

RCC_BASE = 0x40021000

APB2 外设时钟使能寄存器:

RCC_APB2ENR = 0x40021018

GPIOA 的时钟使能位是第 2 位。

所以最底层可以写成:

*(volatile unsigned int *)0x40021018 |= (1 << 2);

这句话的意思是:

找到 RCC_APB2ENR 寄存器

把第 2 位写 1

打开 GPIOA 时钟

GPIOA 才能正常工作

所以你调用库函数,其实只是官方帮你封装了寄存器操作。

 

8. 举个完整例子:PA0 输出高电平

不用库函数,直接操作寄存器大概是这样:

#define RCC_APB2ENR   (*(volatile unsigned int *)0x40021018)

#define GPIOA_CRL     (*(volatile unsigned int *)0x40010800)

#define GPIOA_ODR     (*(volatile unsigned int *)0x4001080C)

 

int main(void)

{

    // 1. 开启 GPIOA 时钟

    RCC_APB2ENR |= (1 << 2);

 

    // 2. 配置 PA0 为推挽输出,50MHz

    GPIOA_CRL &= ~(0xF << 0);

    GPIOA_CRL |=  (0x3 << 0);

 

    // 3. PA0 输出高电平

    GPIOA_ODR |= (1 << 0);

 

    while (1)

    {

    }

}

这就是最原始的寄存器操作。

如果用库函数,就是:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_SetBits(GPIOA, GPIO_Pin_0);

两种写法本质一样。

区别是:

库函数:好理解,好写

寄存器:更底层,更接近硬件

 

9. 寄存器映射学习重点

这一节你不用死背所有地址,重点理解这几个:

1. 外设寄存器也有地址

2. CPU 通过读写地址控制外设

3. 外设基地址 + 寄存器偏移 = 具体寄存器地址

4. GPIOA->ODR 本质是访问某个固定地址

5. 库函数的底层也是寄存器操作

6. volatile 是为了防止编译器错误优化寄存器访问

 

10. 一句话总结

寄存器映射就是:STM32 把 GPIO、RCC、USART、TIM 等外设内部的控制寄存器,统一分配到固定地址,CPU 通过读写这些地址来控制硬件。

 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐