寄存器映射
寄存器映射你可以这样理解:
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 通过读写这些地址来控制硬件。
更多推荐


所有评论(0)