STM32芯片简介,以及STM32的存储器映射是什么?

一. 什么是寄存器

本章参考资料:《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 库本质都是在读写这些地址

三、答疑:

下面我将彻底、从底层原理解释:

  1. 为什么要开启 GPIOA 时钟?

  2. 为什么要设置 PA5 为输出模式?

  3. 同样的 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系列开发板 文档

相关推荐
别掩16 小时前
MOS防倒灌电路设计
单片机·嵌入式硬件
夜流冰17 小时前
EE - 电容电感电路中电流的变化
单片机·嵌入式硬件
橙露17 小时前
STM32中断配置全解析:从寄存器到HAL库的实战应用
stm32·单片机·嵌入式硬件
c-u-r-ry3018 小时前
ZYNQ7 Processing System各个配置界面介绍
嵌入式硬件
idcardwang18 小时前
esp32s3-pwm介绍与stm32的不同原理
单片机·嵌入式硬件
码咔吧咔18 小时前
Flash 是什么?SRAM 是什么?它们的作用、特点、区别、在 STM32 中如何使用?
stm32·嵌入式硬件
LaoZhangGong12319 小时前
学习TCP/IP的第1步:ARP数据包
网络·stm32·学习·tcp/ip·以太网·arp·uip
三佛科技-1873661339720 小时前
KP521405LGA低功耗5V1A易用高性能BUCK同步降压转换器芯片解析
单片机·嵌入式硬件
Joshua-a20 小时前
FPGA基于计数器的分频器时序违例的解决方法
嵌入式硬件·fpga开发·fpga