文章目录
前言
在之前的文章中,我们对FreeRTOS中的任务是这样理解的:
- 任务是 FreeRTOS 进行资源分配的基本单元,每一个任务都拥有一套相对独立的内存空间;
- 任务是 FreeRTOS 进行 CPU 调度的基本单元,调度器通过在不同任务之间切换,实现 CPU 的合理使用。
想必你已经产生了很多疑惑:
- 任务所拥有的"独立内存空间"究竟包含哪些内容?
- 任务之间切换时,上下文又是如何被保存与恢复的?
要想搞清楚这些问题,我们就要从STM32的内存映射机制入手,来看一看STM32的内存使用究竟是一个什么情况。
什么是内存映射
STM32 单片机是一台微型计算机,麻雀虽小、五脏俱全,CPU、存储器、外设等核心组成部分一应俱全。
在整个系统中,CPU 处于核心控制地位:
- 无论是程序的执行、数据的读写,
- 还是对 GPIO、USART、SPI 等外设的操作,
- 最终都必须由 CPU 来完成。
那么问题就来了:
- CPU 本身不可能直接"认识"外设
- CPU也不可能知道某一次操作到底是操作SRAM内存,还是Flash闪存
- 亦或者是操控某个外设
CPU 唯一能够识别和使用的东西,只有------地址。
为了让 CPU 能够统一、有效地管理整个片上系统。
STM32 引入了一个极其重要的核心机制:内存映射(Memory Mapping)。
所谓内存映射,就是将 片上存储器和外设的寄存器,统一映射到同一片连续地址空间中。
使 CPU 可以通过访问指定地址的方式,来完成对内存或外设的各种操作。
从 CPU 的角度来看:
访问操作RAM 和Flash这种存储器,亦或者访问操作外设寄存器,都只是访问这段连续映射空间的某个地址罢了。
CPU 并不需要区分这些地址背后连接的是哪一类硬件,所有硬件层面上的差异,都由芯片内部的构造和设计自动来完成解析。
这正是 STM32 采用 内存映射机制 的核心意义。
实际上,不论是 STM32 这类单片机,还是传统的通用计算机系统,底层都采用了内存映射(Memory-Mapped)的设计机制。
通过这种机制,CPU 可以使用统一地址来访问 RAM、Flash 以及各类外设资源。
可以说,内存映射已经成为现代计算机体系结构中一种必不可少、且被广泛采用的资源管理机制。
当然,单片机毕竟要简陋简单很多。
STM32的内存映射机制是直接暴露给程序员的,程序员可以直接看到。
而通用计算机这些底层细节,被操作系统封装是看不见的。
我们所使用的单片机是STM32F103C8T6,打开《数据手册》,找到内存映射章节。
首先映入眼帘的就是下面这张图:

下面,我们来详细解释一下这张图。
STM32F103系列中等密度芯片内存映射
从上面的内存映射图中可以看到:
STM32F103 系列中等密度芯片的整体内存映射布局,以及各类存储区域和外设在地址空间中的分布情况。
当然,这张图还展示了STM32F103中等密度芯片所集成的外设。
所以如果你想知道STM32F103C8T6具体有哪些片上外设,除了查引脚定义表,查这一张图也是可以的。
总地址空间
STM32是32位单片机,这不仅意味着寄存器是32位的,其地址空间也是32位的。
从图中可以看到,内存映射的地址范围是:
0x0000_0000 ~ 0xFFFF_FFFF
每一个地址对应1个字节,总共4GB的连续地址空间。
当然,这 4GB 的地址空间只是"逻辑上的映射空间",并不等同于芯片中真实存在的物理存储容量。
实际上,STM32F103C8T6只有64KB的Flash闪存容量,以及20KB的RAM内存容量。
也就是说,在这整整 4GB 的地址空间中,只有极小的一部分被实际的存储器和外设所占用。
并且从图中,我们可以明显看出,这4G的映射地址空间,一大部分都是保留空置的。
Flash映射区域
在内存映射图中可以看到如下地址范围:
0x0800_0000 ~ 0x0801_FFFF
这属于单片机Flash闪存映射的内存区域。
可以计算一下,这段区域的大小是:128KB
上面已经讲过了,这张图是STM32F103系列中等密度(64~128KB)芯片的内存映射,所以图中展示的是128KB的Flash闪存容量。
我们所使用的STM32F103C8T6只有64KB的Flash闪存容量,实际映射区域是:
0x0800_0000 ~ 0x0800_FFFF
Flash 闪存主要用于存储以下内容:
- 工程编译后生成的程序代码(指令)
- 程序中使用的只读常量数据
特点是:掉电不丢失,但不能像SRAM那样随意写(擦除写入,只能将1写成0)
单片机在运行程序时,CPU 正是从 Flash 闪存中,顺序读取下一条需要执行的程序指令并加以执行。
SRAM映射区域
STM32F103 系列芯片,其 SRAM 的映射起始地址是:
0x2000_0000
再加上,STM32F103C8T6的实际SRAM容量是20KB。
所以STM32F103C8T6 的 SRAM 实际有效映射范围是:
0x2000_0000 ~ 0x2000_4FFF
SRAM可以理解成通用计算机的内存,存储以下内容:
- 全局变量
- 静态变量
- 栈
- 堆
特点是:断电即失,速度快,随意读写,容量小。
SRAM是程序运行时,用于保存运行时数据的区域,是程序运行的"施工现场"。
无论是裸机应用,还是基于FreeRTOS,运行时产生的数据都需要放入SRAM中。
从这个角度出发,任务的内存分配必然是在SRAM中进行的。
其余外设映射区域
从图中,以下面地址开头的映射区域:
0x4000_0000 ~
就是各类外设的寄存器映射地址。
在STM32阶段,我们讲过基于寄存器的开发方式,实际上已经使用过外设寄存器地址了。
除此之外,还有一块小区域,也可以留意一下:
图中靠左上的一块区域,起始地址是:
0xE000_0000 ~
标注为 Cortex-M3 Internal Peripherals
这些地址区域,就是内核外设的寄存器映射区域。
比如NVIC、SysTick等组件的寄存器映射。
STM32的CPU内核寄存器
在前面学习 STM32 内存映射时,我们已经明确了一点:
CPU 通过访问映射地址来使用 Flash、SRAM 和 各类外设。
但需要注意的是,CPU 并不仅仅是一个"访问内存的机器"。
CPU 的核心职责是进行运算,按照程序指令的顺序完成一系列计算与控制操作。
在指令执行的过程中,CPU 需要频繁地:
- 暂存正在参与运算的数据
- 保存中间计算结果
- 记录当前执行到哪一条指令
- ...
如果这些数据都通过访问 SRAM 来完成,不仅效率低下,也无法满足指令执行过程对速度和时序的要求。
因此:
在 CPU 内部,还存在着一组速度最快、距离运算单元最近的存储单元,用于辅助指令的执行与运算过程,这些存储单元,便是 CPU 内核寄存器。
简单来说:
内存(Flash / SRAM)解决的是"数据和程序放在哪"的问题
而 CPU 内核寄存器解决的是"指令如何被高效执行"的问题。
在 STM32 所采用的 Cortex-M 内核中,这些寄存器由内核直接管理。
既不属于 Flash,也不属于 SRAM,而是 CPU 内部结构中不可或缺的一部分。
需要注意的是:
CPU内核寄存器属于CPU的一部分,由CPU直接管理,所以就不再需要映射地址空间了。
内核寄存器的分类
我们可以使用Keil5软件的调试模式,随便调试一段普通代码(不引入FreeRTOS)
如下图所示:

其中,R0 ~ R12 这13个寄存器,被称之为"通用寄存器",用作:
- 保存临时数据
- 保存运算结果
- 存放函数参数和返回值
- ...
在 C 语言层面:
- 所有的局部变量
- 所有的中间计算结果
最终都会被编译器安排到这些寄存器中使用。
R13 ~ R15这三个属于特殊寄存器:
第一个特殊寄存器:SP寄存器。
SP(Stack Pointer,栈指针),SP 用来指向,当前正在使用栈的栈顶地址。
- 在不引入RTOS的情况下,系统只有一条执行路径,SP就始终指向SRAM中栈的栈顶,也就是**主栈(Main Stack)**的栈顶。
- 如果你仔细看图的话,会发现下面还有一个MSP,这就是MSP(Main Stack Pointer)主栈栈指针。
- 可以看到图中,它的值和SP是一样的,因为当前工程只是一个标准库工程,只存在主栈,SP就是MSP。
- 如果引入RTOS,系统中存在多任务,每个任务都有自身独立的任务栈。
- CPU 在运行某个任务时,SP 会指向 当前正在执行任务的任务栈栈顶。
- 此时SP将会和图中的"PSP(Process Stack Pointer)"保持一致。(当前PSP是空的,因为系统中不存在任务)
- PSP直译过来是"进程栈指针",但在RTOS场景下,应该把它理解成TSP(Thread/Task Stack Pointer),建议直接理解成任务栈指针。
- 关于SP、MSP、PSP三个栈顶指针,待到下一章节讲完任务的内存布局后,我们再详谈。
第二个特殊寄存器:LR寄存器。
LR(Link Register,链接寄存器),是内核中的一个特殊寄存器。作用是在函数调用过程中保存返回地址。
具体来说,当发生函数调用时:
- CPU 会将函数调用点之后的下一条指令地址保存到 LR 寄存器中
- 被调用函数执行完毕后,通过 LR 中保存的地址返回到调用点,继续执行后续指令
因此,在函数调用的场景下,LR 中保存的是一条程序指令的地址。
而程序指令都存储在Flash闪存中,正如图中所示:
LR寄存器,存储的地址是:0x0800_xxxx
这正好位于 Flash 闪存的映射地址范围内,说明 LR 指向的是一条存放在 Flash 中的程序指令。
第三个特殊寄存器:PC寄存器。
PC(Program Counter,程序计数器),作用是在程序运行时,保存下一条将要执行指令的地址。
程序能顺序执行、跳转、循环,本质上都是 PC 的变化。
PC 决定了 CPU 下一步"干什么"。
三个特殊寄存器的下面,还有一个xPSR寄存器,该寄存器用于保存 CPU 的运行状态信息,可以通过该寄存器了解CPU是否正常工作。
对于一般应用层程序员而言,该寄存器不需要深入了解。
CPU与SRAM的数据交互
在程序运行过程中,CPU 并不会直接在 SRAM 中对数据进行运算。
例如:
c
int num = 10;
num++;
实际执行过程可以简单理解为:
CPU 先从 SRAM 中,把变量 num 的值读入某个通用寄存器(如 R0)
CPU 在通用寄存器中完成 +1 运算
CPU 再将运算后的结果,从通用寄存器写回到 SRAM 中 num 对应的位置
也就是说:
SRAM 负责存数据,CPU 负责算数据,
而内核寄存器充当CPU运算过程中的"草稿纸",用于保存临时数据。
上下文切换的原理
经过前面的讲解,大家已经对 STM32 的内存映射机制 以及 CPU 内核寄存器 有了基本认识。
在此基础上,我们就可以进一步深入,来讲解 上下文切换(Context Switch) 的工作原理。
什么是上下文
首先,我们要解释一下什么叫上下文(Context)。
所谓 上下文(Context),也称为 CPU 的执行上下文,指的是 某一时刻 CPU 内部整套寄存器所保存的数据状态。
说得通俗一些:
CPU 内核寄存器中那一整套寄存器的取值,就完整描述了 CPU 在某一时刻的执行状态。
这一套通过寄存器快照,所描述的CPU运行状态,就是上下文。
上下文切换要做什么
以FreeRTOS为例,在多任务系统中,CPU 并不是只为某一个任务服务,而是在多个任务之间不断切换执行。
当CPU切换到另一个任务时,考虑到可能重新回到该任务:
所以不可能直接不管不顾,直接切换CPU。
当 CPU 从一个任务切换到另一个任务时,必须完成上下文的切换:
需要先保存旧任务的上下文状态,然后再切换CPU,并且在任务需要重新上CPU时,恢复上下文状态。
上下文切换的基本流程
我们以两个任务 A 和 B 为例,来看上下文切换是如何发生的。
当 CPU 正在执行任务 A 时,如果任务 A 因故进入阻塞状态,CPU就需要切换到另一个可运行(就绪态)的任务B。
此时需要做任务A的上下文保存:
- 将当前通用寄存器(用到的部分)、SP、LR、PC 等关键寄存器的值
- 写入到SRAM中一块该任务的专用区域内。
完成任务 A 的上下文保存后,任务B上CPU:
- CPU 的寄存器内容被切换为任务 B 对应的寄存器状态,也就是将B任务保存的上下文信息恢复到CPU中。
- CPU 开始执行任务 B 的函数指令。
如果任务 B 在执行过程中也进入阻塞状态:
- 同样需要将任务 B 当前的寄存器状态
- 保存到 SRAM 中,完成一次新的上下文保存。
假如任务A此时结束阻塞,进入就绪态,于是调度器就会让任务A上CPU:
- CPU 会从 SRAM 中,将之前保存的 任务 A 的寄存器数据 读回;
- 覆盖当前 CPU 内核寄存器的内容;
- CPU 从任务 A 上一次被切换的位置继续执行。
这就是上下文恢复的过程。
在系统运行过程中:
- 任务 A、任务 B、甚至更多任务
- 会不断经历 保存上下文 → 恢复上下文 的过程
从而实现 多任务"并发"运行的效果。
总结
上下文切换过程,可以概括为三个核心步骤:
- 保存当前任务的上下文:将当前任务运行时寄存器中的关键内容保存到SRAM中,本质上就是把当前任务的运行现场保存到 SRAM 中。
- 切换当前任务对象:调度器根据就绪任务情况,选择下一个要运行的任务,这个过程由FreeRTOS任务调度器基于调度机制完成。
- 恢复新任务的上下文:从SRAM中找到新任务的上下文信息,恢复到 CPU 寄存器中,使 CPU 从该任务上次暂停的位置继续执行;
总之,所谓上下文切换,本质上就是:
总之,所谓上下文切换,本质上就是 CPU 在不同的寄存器快照之间来回切换,而这些寄存器快照都保存在SRAM中。
思考
在 FreeRTOS 系统中,任务往往不止一个。
这也意味着,系统在运行过程中,可能需要在不同任务之间频繁进行上下文切换。
而在上下文切换过程中:
每一个任务,都需要将当下 CPU 寄存器中的瞬时状态(快照),保存到 SRAM 中,以便后续能够准确恢复。
那么问题就来了:
多个任务,能否把各自的上下文状态, 都保存到同一块 SRAM 区域中?
答案显然是否定的。
如果所有任务都把寄存器快照保存在同一个位置,那么一旦发生切换:
新任务的上下文状态,就会直接覆盖掉旧任务的上下文信息。
这样一来,之前任务的执行状态将彻底丢失,上下文保存也就失去了意义。
因此,一个非常明确的结论是:
每一个任务,都必须拥有一块独立的、专属的 SRAM 区域,用来保存与该任务相关的运行状态和工作数据。
那么问题又来了:
- 任务在 SRAM 中的这块专属区域究竟长什么样?
- 这块专属区域,究竟是如何工作的呢?
这些问题会在下一文章中讲解