一. 什么是寄存器
本章参考资料:《STM32F10xx 参考手册》、《STM32F10xx数据手册》
学习本章时,配合《STM32F10xx 参考手册》"存储器和总线架构"及"通用I/O(GPIO)"章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。
1.1. 什么是寄存器
我们经常说寄存器,那么什么是寄存器?这是我们本章需要讲解的内容,在学习的过程中,大家带着这个疑问好好思考下,到最后看看大家能否用一句话给寄存器下一个定义。
1.2. STM32长啥样
以我们的F103-指南者开发板为例,指南者开发板中使用的芯片是100pin(100个引脚)的STM32F103VET6,具体见图1‑1。 这个就是我们接下来要学习的STM32,它将带领我们进入嵌入式的殿堂。
芯片正面是丝印,ARM应该是表示该芯片使用的是ARM的内核,STM32F103VET6是芯片型号,后面的字应该是跟生产批次相关,最下面的是ST的LOGO。
芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。 开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作,通过做实验的方式来学习STM32芯片的各个资源。
开发板是一种评估板,主要用于学习与评估,其板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。 我们的STM32 F1的开发板上面的STM32型号虽然不完全相同,但实际上学习它们所需要的数据手册和参考手册资料是一样的,初学者不必为此担心。

图 1‑1 STM32F103VET6 实物图(红色框中部分)

图 1‑2 STM32F103VET6正面引脚图
1.3. 芯片里面有什么
我们看到的STM32芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。
STM32F103采用的是Cortex-M3内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。 芯片生产厂商(SOC) 如ST、TI、NXP等,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。具体见图 1‑3。

图 1‑3 STM32芯片架构简图
芯片(这里指内核,或者叫CPU)和外设之间通过各种总线连接,其中驱动单元有4个,被动单元也有4个,具体见图 1‑4。为了方便理解,我们都可以把驱动单元理解成是CPU部分,被动单元都理解成外设。下面我们简单介绍下驱动单元和被动单元的各个部件。
1.3.1. ICode总线
ICode中的 I 表示 Instruction,即指令。我们写好的程序经过编译之后都是一条条指令,存放在FLASH中,内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,它是专门用来取指的。
1.3.2. 驱动单元
1.3.2.1. DCode总线
DCode中的 D 表示 Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种, 常量就是固定不变的,用C语言中的const关键字修饰,是放到内部的FLASH当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
1.3.2.2. 系统总线
系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
1.3.2.3. DMA总线
DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的FLASH。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
1.3.3. 被动单元
1.3.3.1. 内部的闪存存储器
内部的闪存存储器即FLASH,我们编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。
1.3.3.2. 内部的SRAM
内部的SRAM,即我们通常说的RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。
1.3.3.3. FSMC
FSMC的英文全称是Flexible static memory controller,叫灵活的静态的存储器控制器,是STM32F10xx中一个很有特色的外设,通过FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点我们要注意的是,FSMC只能扩展静态的内存,即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。
5.3.3.4. AHB到APB的桥
从AHB总线延伸出来的两条APB2和APB1总线,上面挂载着STM32各种各样的特色外设。我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上,这个是我们学习STM32的重点,就是要学会编程这些外设去驱动外部的各种设备。

图 1‑4 STM32F10xx系统框图(不包括互联型)
答疑: DMA1和DMA2是什么?DMA总线与Dcode总线有区别?SDIO又是干嘛的,system干嘛的?总线矩阵干嘛的?
二. 存储器映射是什么?
2.1. 存储器映射
在图 1‑4中,被控单元的FLASH,RAM,FSMC和AHB到APB的桥(即片上外设),这些功能部件共同排列在一个4GB的地址空间内。我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过C语言对它们进行数据的读和写)。
2.1.1. 存储器映射
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配的,给存储器分配地址的过程就称为存储器映射,具体见图 2‑5。如果给存储器再分配一个地址就叫存储器重映射。

图 2‑5 存储器映射(摘自参考手册-存储器映射章节)
下面我用 深入 + 通俗 + 配图式解释 ,让你彻底搞懂 "存储器映射(Memory Mapping)"是什么、为什么要用、怎么用。这是 MCU(尤其是 STM32)最基础又最重要的概念之一。
2.1.什么是"存储器映射"?(Memory Mapping)
一句话:
存储器映射 = 把"外设、Flash、SRAM"等都当成"内存地址"来访问。
CPU 通过读写地址就能控制外设,而不用复杂的指令。
也就是说------
在 MCU 里,所有东西都是"地址",都可以用指针访问。
2.2. 在 MCU 里,什么都被映射到地址上?
以 STM32 为例(非常典型):
cpp
0x0000 0000 ~ 0x1FFF FFFF === Flash(程序存储器)
0x2000 0000 ~ 0x3FFF FFFF === SRAM(运行内存)
0x4000 0000 ~ 0x5FFF FFFF === 外设(GPIO、USART、I2C、SPI)
0x6000 0000 ~ 0x9FFF FFFF === 外部设备(FSMC、外部SRAM、屏幕)
0xA000 0000 ~ 0xDFFF FFFF === 映射的外设/存储设备
例如:
-
GPIOA 的寄存器 在地址:
0x40020000 -
USART1 的寄存器 在地址:
0x40011000 -
Flash存储器 在
0x08000000
这意味着:
你只要读写这些地址,等于在控制外设!
2.3. 为什么 MCU 要用"存储器映射"?
因为 CPU 只会读写内存地址,访问内存比访问外设更简单。
为了让 CPU 控制外设:
-
CPU 读写一个地址 → 外设寄存器被改写
-
外设寄存器改变 → LED亮/灭、串口发送、I2C通信等
MCU 通过 Memory Map 把外设"伪装成"内存。
🍥 图解:MCU 如何把外设映射成内存?
cpp
CPU
|
| 读写地址
|
总线 AHB/APB
|
+---------------------+
| 外设寄存器 |
| (像内存一样访问) |
+---------------------+
所以你操作 GPIO、UART、I2C,本质都是:
往某个"地址"写值
2.4. 存储器映射如何使用?(非常关键)
📌 方式一:直接操作地址(裸机操作)
例:向 GPIOA 的寄存器写 1
cpp
*(uint32_t *)(0x40020014) = 0x00000001;
这意味着:
-
地址
0x40020014是 GPIOA_ODR(输出寄存器) -
写 1 = PA0 输出高电平(LED亮)
完全没用任何库!纯裸机!
📌 方式二:使用 STM32 标准库(常用方式)
外设地址会封装成结构体:
cpp
GPIOA->ODR = 0x01;
等价于写:
cpp
*(uint32_t *)(0x40020014) = 0x01;
只是结构体帮你包装了地址。
📌 方式三:使用 HAL 库(更高级)
cpp
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
底层还是一样:写 GPIOA 寄存器的对应地址。
📌 总结:存储器映射帮助你这样使用 MCU:
| 控制目标 | 映射到的地址 | 行为 |
|---|---|---|
| GPIO | 0x4002 0000 | 控制引脚高低 |
| USART | 0x4001 1000 | 发数据 |
| I2C | 0x4000 5400 | 发送/接收 |
| ADC | 0x4001 2000 | 读取 ADC 值 |
| FLASH | 0x0800 0000 | 程序位置 |
| SRAM | 0x2000 0000 | 变量在这里放 |
MCU 全靠这个机制才能让 CPU 控制所有外围硬件。
2.5. 让你彻底理解的简单例子(LED 点亮)
CPU 要让 LED 亮 → 写 GPIO 寄存器地址
假设 LED 连着 PA5
① 开启 GPIOA 时钟
写 RCC 寄存器地址:
cpp
*(uint32_t *)0x40023830 |= (1 << 0);
② 设置 PA5 为输出模式
cpp
*(uint32_t *)0x40020000 |= (1 << 10);
③ 输出高电平(LED亮)
cpp
*(uint32_t *)0x40020014 |= (1 << 5);
总结:
LED亮灭 = 往 GPIO 寄存器的地址写 1 或 0
而 GPIO 寄存器又是通过 Memory Map 将外设映射到地址从而被 CPU 控制的。
📌 五、你真正需要记住的精华版:
✔ 存储器映射 = 把 Flash、SRAM、外设 都当成内存地址来访问
✔ CPU 通过读写地址就能控制外设
✔ GPIO、串口、I2C、SPI、ADC 全靠写寄存器地址完成
✔ STM32 的结构体访问、HAL 库本质都是在读写这些地址
三、答疑:
下面我将彻底、从底层原理解释:
-
为什么要开启 GPIOA 时钟?
-
为什么要设置 PA5 为输出模式?
-
同样的 LED 示例用三种方式写:裸机 / 标准库 / HAL 库
这个知识点非常重要,是理解 STM32 外设工作原理的核心。
3.1. 为什么必须"开启 GPIOA 时钟"?(非常重要!)
因为:
STM32 的所有外设(GPIO、USART、I2C...)默认都是"断电"的,必须通过 RCC 打开时钟,外设才能工作。
✔ 类比理解:
MCU 把外设当成"房间",RCC 就像"开关总闸"。
-
不开总闸(时钟 OFF) → 外设没有电,寄存器不工作
-
开总闸(时钟 ON) → 外设开始正常工作
✔ 技术解释:
GPIOA 外设属于 APB2 总线 管理。
在 RCC(时钟控制器)中有一个控制寄存器:
bash
RCC_AHB1ENR
其中:
cpp
bit0 = GPIOA 时钟使能
如果不使能时钟:
-
GPIOA 的寄存器不会更新
-
写 GPIOA->ODR 没反应
-
LED 永远不会亮
-
电平读不出
-
外设完全处于低功耗停机状态
所以第一步必须:
cpp
*(uint32_t *)0x40023830 |= (1 << 0);
3.2. 为什么要"设置 PA5 为输出模式"?
因为 GPIO 有 4 种模式:
| 模式 | 用途 |
|---|---|
| 输入模式 | 按键、传感器输入 |
| 输出模式 | 控制 LED、继电器等 |
| 复用模式 | UART/I2C/SPI/定时器等功能 |
| 模拟模式 | ADC、DAC 或低功耗 |
如果不设置为输出模式:
-
默认是"输入模式"
-
输入模式不能驱动 LED
-
即使你写 ODR,也不会改变电平
所以第二步必须把 PA5 改为输出:
cpp
*(uint32_t *)0x40020000 |= (1 << 10);
对应寄存器:
cpp
GPIOA_MODER
PA5 需要设置为 01(输出模式)
⚠️ 如果不设置为输出模式,会怎样?
-
LED 不会亮
-
ODR 写进去但不会输出
-
引脚始终是"高阻态"
-
相当于线悬空
所以:
开时钟 = 外设开始工作
配置输出模式 = 引脚具有输出功能
这两个缺一不可。
3.3. 下面我们把 LED 点亮示例,用三种方式重写:
✅ 方式 1:直接操作地址(裸机操作)
① 使能 GPIOA 时钟
cpp
*(uint32_t *)0x40023830 |= (1 << 0);
② 设置 PA5 为输出模式(MODER)
PA5 = GPIOA_MODER bit(11:10) = 01
cpp
*(uint32_t *)0x40020000 &= ~(3 << 10); // 先清零
*(uint32_t *)0x40020000 |= (1 << 10); // 设置为输出
③ 输出高电平(ODR)
cpp
*(uint32_t *)0x40020014 |= (1 << 5);
✅ 方式 2:STM32 标准库(寄存器结构体)
标准库中已经对寄存器封装为结构体,例如:
-
RCC->AHB1ENR
-
GPIOA->MODER
-
GPIOA->ODR
代码:
cpp
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // 开时钟
GPIOA->MODER &= ~(3 << (5 * 2)); // 清零 PA5 模式
GPIOA->MODER |= (1 << (5 * 2)); // 设置输出模式
GPIOA->ODR |= (1 << 5); // PA5 输出高
这和裸机操作本质完全一样,只是结构体把寄存器地址包装了。
✅ 方式 3:HAL 库(高级封装)
HAL 对 GPIO 做了高级封装,只需要配置结构体:
① 使能时钟
cpp
__HAL_RCC_GPIOA_CLK_ENABLE();
② 配置引脚为输出
cpp
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_5;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
③ 输出电平
cpp
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL 库实际上做了这些事:
-
调 RCC 时钟函数
-
修改 GPIO MODER
-
修改 ODR
只是帮你屏蔽细节。
🎯 总结(最重要的)
✓ 1. 为什么要开启 GPIOA 时钟?
因为 GPIOA 默认"断电",时钟不开 → 外设不工作。
✓ 2. 为什么要设置 PA5 为输出?
因为 GPIO 默认是"输入模式",要驱动 LED 必须切换成"输出模式"。
✓ 3. 三种方式的关系?
| 方式 | 本质 | 难度 | 常用吗? |
|---|---|---|---|
| 裸机(写地址) | 直接操作寄存器地址 | ★★★★★ | 用于底层驱动 |
| 标准库 | 结构体访问寄存器 | ★★★★ | 常用于学习 |
| HAL 库 | 高级函数封装 | ★★★ | 工程常用 |
笔记记录,参考链接:5. 什么是寄存器 --- [野火]STM32 HAL库开发实战指南------基于F103系列开发板 文档