Skip to content

01 · FreeRTOS 内存管理:先看懂 MCU 的内存世界

上来就讨论 FreeRTOS 内存管理,可能会稍微有些晦涩。

但如果我们连 MCU 里面到底有哪些内存、CPU 怎么访问这些内存都不知道,就直接开始背 heap_1heap_2heap_4,最后很容易变成“会用 API,但是不知道内存到底去了哪里”。

所以这一节我们先不着急进入 FreeRTOS,而是从 MCU 本身开始,把 总线 → 总线矩阵 → Memory Map → Flash/SRAM 这一条底层链路建立起来。

陌生的前置知识不用怕,我们一步一步来。


1. 前置知识:总线到底是什么

总线(Bus)暂时可以理解为:

MCU 内部不同模块之间传递信息的通道。

一次典型的总线访问中,我们通常关心三类信息:

  • 地址:我要访问哪里?
  • 数据:我要读取什么,或者我要写入什么?
  • 控制信息:这是一次读操作还是写操作?访问宽度是多少?

例如:

c
uint32_t value;

value = *(uint32_t *)0x20000000;

先不用纠结这个指针写法,如果指针基础不熟,可以先把这里理解成:

CPU 想读取地址 0x20000000 里面存放的数据。

那么从硬件角度,大致会发生:

text
CPU 发起读取请求

地址 = 0x20000000

通过 CPU 总线接口发出

总线系统根据地址进行译码

发现该地址属于 SRAM1

请求被送到 SRAM1

SRAM1 返回数据

CPU 得到 value

这里先记住一个很重要的思想:

CPU 访问内存和外设,本质上都是在“访问地址”。

后面我们看到:

c
GPIOA->ODR

你会发现它和访问 SRAM 本质上没有想象中那么不同。


2. 为什么又出现了“总线矩阵”

如果 MCU 里面只有 CPU 和一块内存,那么事情其实很简单:

text
CPU


总线


SRAM

CPU 想访问 SRAM,就沿着这条总线访问。

但是现代 MCU 里面,能够主动访问系统资源的东西远远不只有 CPU。

例如:

text
CPU

DMA1

DMA2

Ethernet

USB

DMA2D
...

这些模块都有可能需要访问:

text
Flash
SRAM
外设
外部存储器
...

这时候问题就出现了。

假设:

text
CPU 正在从 Flash 取指令

与此同时

DMA 正在把 ADC 数据搬运到 SRAM

如果所有访问都只能挤在一条公共通道上:

text
CPU ──┐

DMA ──┼──→ 一条公共总线 ──→ Flash / SRAM / 外设

USB ──┘

那么即使:

text
CPU 想访问 Flash

DMA 想访问 SRAM

两个人访问的明明不是同一个资源,却仍然可能因为共享同一条通道而互相等待。

这显然非常浪费性能。

于是 STM32F4 引入了:

Multi-layer AHB Bus Matrix

多层 AHB 总线矩阵。

我们暂时把它想象成一个大型高速交通枢纽:

text
                 Flash



CPU ───────┐


      ┌───────────┐
      │           │
      │ Bus Matrix│────────→ SRAM
      │           │
      └───────────┘


DMA ───────┘



                 外设

它最重要的能力之一就是:

允许多个访问者,在条件允许的情况下,同时访问不同的目标资源。

例如:

text
CPU → Flash

同时

DMA → SRAM

这两个事务就有机会并行进行。

所以目前我们可以先这样理解:

总线矩阵 = 管理多个访问发起者与多个目标资源之间连接、路由和冲突仲裁的高速交换中心。


3. Master 和 Slave 到底是什么意思

既然存在访问,就自然存在:

text
谁主动发起访问?

谁被访问?

于是总线系统里经常出现两个概念。

Master —— 主设备 / 主接口

能够主动发起一次总线访问。

例如:

text
Cortex-M4

DMA

Ethernet DMA

USB DMA

都可能作为 Master。

Slave —— 从设备 / 从接口

负责响应别人发起的访问。

例如:

text
SRAM

Flash

外设寄存器

各种控制器寄存器

都可以成为访问目标。

但是这里千万不要形成:

“一个硬件模块天生只能是 Master 或者只能是 Slave。”

这种认知。

实际上一个复杂模块可以同时提供不同性质的接口。

例如 DMA:

CPU 配置 DMA 时:

text
CPU

访问 DMA 控制寄存器

DMA 的寄存器接口

此时 CPU 是访问发起者。

但是 DMA 真正开始搬运数据以后:

text
DMA

主动读取外设

主动写入 SRAM

DMA 自己又成为了总线访问发起者。

所以更加准确地说:

Master / Slave 描述的是总线接口和访问关系,而不是简单给整个硬件模块永久贴一个标签。


4. 开始真正看 STM32F429 的系统架构

现在我们终于可以打开 STM32F429 的参考手册。

STM32F429系统架构

第一次看到这种图,不要试图一根线一根线全部看懂。

先把它分成三个区域:

text
             【访问发起者】

 Cortex-M4   DMA1   DMA2   Ethernet
     │        │      │       │
     └────────┴──────┴───────┘


        ┌────────────────┐
        │                │
        │   Bus Matrix   │
        │                │
        └────────────────┘



              【资源】

     Flash / SRAM / AHB外设 / FMC

所以整张图最外层的逻辑其实非常简单:

text
谁想访问资源

总线矩阵

它想访问哪个资源

不同 STM32F4 型号拥有的 Master 数量并不完全一样。

例如 STM32F429 相比一些 STM32F407 型号,还拥有 LTDC、DMA2D 等模块,所以不要死记:

“STM32F4 永远有 8 条主控总线、7 条被控总线。”

正确思维应该是:

具体有多少接口,要看当前芯片的数据手册和系统架构图。


5. Cortex-M4 为什么伸出来三条总线

把目光放到:

text
ARM Cortex-M4

会发现下面并不是只有一条线,而是:

text
            Cortex-M4
          /      |      \
         /       |       \
      I-Bus    D-Bus    S-Bus

这非常重要。

也说明一个需要纠正的认知:

不能理解成“单核 CPU 同一时间只能走一条总线”。

Cortex-M4 内部本身就具有多个用于不同类型访问的总线接口。


5.1 I-Bus:Instruction Bus

I = Instruction。

它主要负责:

取指令。

CPU 想运行一个程序,就必须不断执行:

text
取指令

译码

执行

取下一条指令

……

假设我们写:

c
int main(void)
{
    HAL_Init();

    while (1)
    {
        LED_ON();
    }
}

C 语言经过:

text
编译

汇编

链接

最终会生成 Cortex-M4 能够执行的机器指令。

通常情况下,这些程序代码会存放在:

text
Flash

所以 CPU 必须不断从 Flash 中读取下一条机器指令。

经典路径就是:

text
        Cortex-M4

            │ 我要下一条指令

          I-Bus


           S0



           M0

          ICODE

     ART Accelerator

          Flash

这里的:

text
ICODE

不要理解成一块新的内存。

它更像是:

Flash 提供给“指令访问”的一条入口。

真正存储程序的仍然是:

text
Flash Memory

5.2 D-Bus:Data Bus

D = Data。

D-Bus 主要用于 Cortex-M4 对代码区域中的数据进行访问,例如:

c
const uint32_t table[] =
{
    10,
    20,
    30,
    40
};

这样的只读常量通常也可能存放在 Flash。

假设 CPU 正在读取:

c
value = table[0];

这时候:

text
地址虽然仍然位于 Flash

但 CPU 不是把它当“指令”读取

而是在进行“数据读取”

于是可以经过:

text
Cortex-M4

  D-Bus

   S1

 DCODE

  Flash

所以同一块 Flash 可以存在:

text
               Flash
              /     \
             /       \
          ICODE     DCODE
            ↑         ↑
            │         │
         I-Bus      D-Bus
            │         │
         取指令     读数据

另外,Cortex-M4 的 D-Bus 还直接连接到了 CCM Data RAM。

这个我们后面单独讲。


5.3 S-Bus:System Bus

S = System。

System Bus 主要用于访问:

text
SRAM

外设寄存器

AHB / APB 外设

外部存储器

例如:

c
GPIOA->ODR |= (1 << 6);

GPIOA 是 AHB1 外设。

所以这次访问大致会经过:

text
Cortex-M4

  S-Bus

   Bus Matrix

   AHB1

  GPIOA

   ODR

再比如普通变量位于 SRAM:

c
uint32_t value = 10;

CPU 访问它时也主要通过 System Bus。

需要注意:

S-Bus 不仅能够访问数据和外设,在某些地址区域它同样可以取指令,只不过通常没有专门的 I-Code 路径高效。

所以我们目前可以先记:

text
I-Bus
主要:取指令

D-Bus
主要:访问代码区域中的数据、CCM 等

S-Bus
主要:SRAM 和外设访问

不要把它们理解成完全互斥、永远不能访问其他内容的三条死规则。


6. 总线矩阵里的空心圆是什么意思

仔细看 Bus Matrix:

text
S0


 ○──────── M0

你会发现有些十字交叉的位置存在:

text

有些位置只是两条线交叉,却没有圆。

这里非常关键:

空心圆表示这两个 Bus Matrix 端口之间存在合法的硬件连接。

例如:

text
S0 × M0

存在:

text

说明:

text
S0 → M0

存在合法路径。

如果某一个交叉点没有:

text

则表示:

这两个端口之间没有直接连接关系。

所以看总线矩阵时,可以沿着某一个 Master 的竖线一路往下找:

text
哪里有 ○

说明它能够到达哪些目标端口

7. S0、S1 和 M0 到底是什么意思

这里还有一个很容易绕进去的地方。

例如 Cortex-M4:

text
I-Bus → S0

D-Bus → S1

S-Bus → S2

你可能会奇怪:

CPU 明明是 Master,为什么连接到一个叫 S0 的东西?

原因是:

S0 / M0 的命名,是站在 Bus Matrix 本身的角度命名的。

例如:

text
CPU Master

Bus Matrix 的 Slave Port S0

Bus Matrix 内部路由

Bus Matrix 的 Master Port M0

Flash Slave

所以:

text
CPU 是 Master

和:

text
它连接 Bus Matrix 的 S0

并不矛盾。

只是观察角度不同。

初学阶段如果觉得这个术语比较绕,也不用死记,只需要能沿着:

text
CPU → Sx → ○ → Mx → 资源

找到访问路径即可。


8. 如果两个 Master 同时访问一个资源怎么办

前面说:

text
CPU → Flash

DMA → SRAM

由于访问目标不同,它们有机会并行工作。

但是如果:

text
CPU

 ├────→ SRAM1

DMA

CPU 和 DMA 同时想访问 SRAM1 呢?

这时候就出现了:

Bus Arbitration

总线仲裁。

可以先理解成:

text
CPU ─────┐


      仲裁器


       SRAM1


DMA ─────┘

总线系统需要决定:

当前这次访问先让谁通过?

被选中的访问先进行。

没有获得访问权的一方需要等待。

需要注意,这和我们之前学 I²C 时的“仲裁”在思想上有相似之处:

都是在发生资源冲突时决定谁能够继续。

但两者具体硬件机制完全不同,不要直接把它们理解成同一套协议。

另外,也不要简单理解成:

“高优先级设备一旦获得总线,就必须把所有事情全部干完以后其他人才能访问。”

真正的仲裁会结合:

text
目标 Slave
访问事务
Burst
Bus Matrix 的仲裁策略

等因素决定。

现在我们只需要建立:

多个 Master 同时访问同一个 Slave → 会产生竞争 → 需要仲裁。

这个模型就够了。


9. CPU 的“取指—译码—执行”到底发生了什么

这里把一个容易写错的地方单独拿出来。

CPU 最核心的工作循环可以简化为:

text
Fetch
取指

Decode
译码

Execute
执行

取指阶段:

text
CPU

I-Bus

读取机器指令

译码阶段:

text
机器指令

Cortex-M4 内核内部译码器

识别这是 ADD、LDR、STR……

执行阶段:

可能由:

text
ALU

寄存器

FPU

Load/Store Unit

等内核单元完成。

因此不能写成:

“执行阶段通过 D-Bus 完成。”

只有当执行的这条指令需要访问内存数据时,才可能进一步通过:

text
D-Bus

或者

S-Bus

发起数据访问。

例如:

c
a = b + c;

如果 b、c 已经在 CPU 寄存器中:

text
CPU ALU直接计算

甚至根本不需要额外访问总线。


10. 一个比较特殊的存在:CCM RAM

仔细看系统架构图,还会发现:

text
64 KB
CCM Data RAM

它的位置非常特殊。

它不像普通 SRAM1 / SRAM2 / SRAM3 那样挂在 Bus Matrix 的右侧。

而是直接与 Cortex-M4 的 D-Bus 联系得非常紧密。

CCM:

Core Coupled Memory

内核耦合存储器。

可以先把它理解成:

为了给 CPU 提供快速数据访问,而专门贴近 CPU 内核设计的一块 RAM。

这同时带来了一个非常重要的特点:

普通 DMA 不能像访问 SRAM1 那样访问 CCM RAM。

所以以后我们真正进入工程以后,会遇到:

text
为什么 DMA Buffer 不能随便放 CCM?

任务栈能不能放 CCM?

FreeRTOS Heap 能不能放 CCM?

高速算法变量放 CCM 有没有好处?

这些问题。

现在先把这个伏笔留下。


11. Memory Map:地址到底是怎么找到硬件的

理解完总线以后,接下来终于可以讨论:

CPU 到底怎么知道 0x20000000 是 SRAM?

答案就是:

Memory Map

存储器映射。

STM32F429 使用的是 32 位 Cortex-M4。

所以 CPU 地址宽度为:

text
32 bit

能够表示:

text
0x00000000



0xFFFFFFFF

总共有:

text
2^32 Byte

也就是:

text
4 GB 地址空间

这里非常容易犯一个错误。

这并不是说:

STM32F429 内部真的拥有 4GB RAM。

而是:

CPU 理论上能够产生 4GB 范围内的不同地址。

然后 ARM 和 ST 会把这个巨大的地址空间切成很多区域:

text
某一段地址 → Flash

某一段地址 → SRAM

某一段地址 → GPIO

某一段地址 → USART

某一段地址 → Cortex-M4 内部系统资源

这就是:

Memory Mapping。


12. 地址只是“门牌号”

例如:

text
0x20000000

并不是 SRAM1 本身。

它只是:

CPU 地址空间中的一个门牌号。

ST 的硬件地址译码逻辑规定:

text
0x20000000

映射到

SRAM1

所以 CPU 发出:

text
READ 0x20000000

硬件发现:

text
这个地址属于 SRAM1

于是访问被送到 SRAM1。

整个过程:

text
CPU

 │ address = 0x20000000

S-Bus


Bus Matrix

 │ 地址译码

SRAM1

13. STM32F429 中几个目前最值得记的地址

我们现在完全没有必要背完整 Memory Map。

先认识几个以后经常出现的区域。

text
0x08000000

Flash Memory 起始地址


0x10000000

64 KB CCM Data RAM


0x20000000

SRAM1
112 KB


0x2001C000

SRAM2
16 KB


0x20020000

SRAM3
64 KB


0x40000000 ...

外设地址区域

其中:

text
SRAM1
0x20000000 ~ 0x2001BFFF

SRAM2
0x2001C000 ~ 0x2001FFFF

SRAM3
0x20020000 ~ 0x2002FFFF

STM32F429 比我们前面经常使用的某些 STM32F407 型号还多了一块 SRAM3。

因此这里再次提醒:

不同 MCU 的内存容量和具体组织形式会变化,学习 Memory Map 时必须以当前芯片的数据手册为准。

但是底层思想基本是一致的。


14. 怎么阅读 Memory Map 表格

例如我们可能看到:

text
0xA0000000 ~ 0xA0000FFF

对应 FMC/FSMC 的某些控制寄存器区域。

这是什么意思?

不是说:

“这里真的摆了一块叫 0xA0000000 的内存。”

而是 CPU 如果访问:

text
0xA000xxxx

地址译码逻辑会把这次访问:

text
送到 FMC/FSMC 对应的控制器

所以:

text
访问一个地址

本质上

就是在访问这个地址映射到的硬件资源

15. 再来看 RNG 的例子

例如:

text
0x50060800 ~ 0x50060BFF

被分配给:

text
RNG
随机数发生器

那么:

text
CPU访问 0x500608xx

硬件地址译码以后:

text
请求会被送到 RNG 外设

所以我们可以说:

这段地址是 RNG 的寄存器地址空间。

这里需要特别注意:

text
0x50060800

并不属于:

text
0xE0000000 ~ 0xFFFFFFFF

的 Cortex/System 内部资源区域。

RNG 在 STM32F429 中属于:

text
AHB2 外设

所以不要仅仅看到一个很大的十六进制地址,就判断它属于 System 区域。

最终仍然要看 Memory Map。


16. 把 GPIOA->ODR 和总线矩阵连接起来

现在重新看我们以前每天都在写的一句话:

c
GPIOA->ODR |= (1 << 6);

以前可能只知道:

“这是在操作 GPIOA 的 ODR 寄存器。”

现在可以往底层继续追。

GPIOA 的基地址位于:

text
0x40020000

于是访问:

c
GPIOA->ODR

最终其实会变成:

text
CPU执行 Store / Load 指令

产生 0x4002xxxx 地址

Cortex-M4 System Bus

Bus Matrix

地址译码

AHB1 Peripheral

GPIOA

ODR 寄存器

也就是说:

text
一行 C 代码



CPU 指令



32 位地址



Memory Map



Bus Matrix



真实硬件

这条链非常重要。

以后我们学习寄存器、DMA、FreeRTOS、MPU,甚至 Cache 时都会重新遇到它。


17. 地址决定了“访问谁”,那从哪条 Bus 走?

这里把目前学过的东西串起来。

CPU 发起一次访问时,我们可以先问两个问题:

text
① CPU 正在干什么?

② 它访问的是什么地址?

例如:

text
PC = 0x08001234

CPU 当前在执行:

Instruction Fetch

同时地址:

text
0x08001234

属于 Flash Code 区域。

那么典型访问路径是:

text
Cortex-M4

I-Bus

ICODE

Flash

但如果:

c
value = *(uint32_t *)0x08001234;

虽然地址还是:

text
0x08001234

但是这一次 CPU 把它当作:

数据

来读取。

那么可以通过:

text
D-Bus

DCODE

Flash

所以不能简单说:

“地址单独决定 CPU 使用哪条总线。”

更加准确的是:

地址决定目标资源,而访问类型和地址区域共同影响 Cortex-M4 从哪条总线接口发起这次访问。


18. Memory Remap:重映射不是“搬家”

接下来会遇到一个很容易让人迷惑的东西:

Memory Remap

内存重映射。

“重映射”这个名字很容易让人误以为:

text
Flash 原来在 0x08000000

重映射以后

Flash 被搬去了 0x00000000

这是错误的。

真正发生变化的是:

CPU 地址和物理存储器之间的映射关系。

物理 Flash 根本没有搬家。

例如正常情况下:

text
0x08000000

Flash

这条映射仍然存在。

与此同时,STM32 可以让:

text
0x00000000

这个启动区域也指向 Flash。

于是:

text
CPU 地址

0x00000000 ──────┐

                 ├────→ 同一块物理 Flash

0x08000000 ──────┘

也就是说:

同一块物理存储器可以通过不同地址被访问。

这种额外地址也可以理解成:

Alias

别名地址。


19. 为什么需要重映射

Cortex-M4 复位启动以后,需要首先读取:

text
0x00000000

0x00000004

附近的启动信息。

其中包含:

text
初始 MSP

Reset_Handler 地址

也就是启动时非常重要的:

中断向量表。

但是 STM32 又支持不同启动来源。

例如:

text
Main Flash

System Memory

SRAM

如果 Cortex-M4 每种启动方式都要记不同地址,会非常麻烦。

于是 ST 做了一层映射:

text
                       Flash


0x00000000 ─────→【地址映射选择】


                  System Memory



                       SRAM

CPU 仍然从:

text
0x00000000

寻找启动信息。

至于:

text
0x00000000

当前到底对应 Flash、System Memory 还是 SRAM,由 STM32 的启动和重映射配置决定。

最关键的是:

被映射的存储器原来的地址并不会消失。

例如 SRAM 被映射到:

text
0x00000000

以后:

text
0x20000000

仍然可以正常访问 SRAM。

只是多了一个能够访问同一物理资源的地址入口。


20. 为什么总线矩阵里的 I-Bus 能连接 SRAM

现在重新回头看系统架构图。

我们曾经发现:

text
I-Bus / S0

居然存在连接:

text
○ → SRAM

当时会很疑惑:

I-Bus 不是取 Flash 指令的吗?

为什么还能去 SRAM?

现在就能理解了。

因为:

SRAM 中同样可以存放并执行代码。

并且在某些 Boot / Remap 场景下,SRAM 可以映射到 Code 区域。

于是:

text
CPU

I-Bus

Bus Matrix

SRAM

这条硬件路径就有存在的意义。

所以总线矩阵里的:

text

告诉我们的只是:

硬件存在这条合法通路。

而:

text
Memory Map / Remap

决定:

某个地址当前究竟会被送到哪个资源。

这两个概念千万不要混在一起。


21. Flash Memory 和 Flash Interface 不是同一个东西

查看 Memory Map 时还有一个特别容易懵的地方。

可能会发现手册里面出现:

text
Flash Interface

于是产生疑问:

“不是说 Flash 在 0x08000000 吗?”

“为什么这里又出现一个 Flash Interface?”

原因是:

text
Flash Memory



Flash Interface

完全不是同一个概念。


21.1 Flash Memory

Flash Memory 是:

真正用于保存程序代码、常量等内容的非易失性存储器。

它的正常映射起始地址是:

text
0x08000000

例如 CPU 的:

text
PC = 0x08001234

意味着 CPU 正准备执行 Flash 中这个地址附近的程序代码。


21.2 Flash Interface

Flash 并不像 SRAM 一样可以随随便便直接改写。

Flash 存在:

text
擦除

编程

等待周期

Cache

Prefetch

读保护

写保护

等复杂操作。

所以 STM32 内部专门设计了:

Embedded Flash Memory Interface

我们可以把它暂时理解成:

Flash 的管理控制器。

里面存在:

text
FLASH_ACR

FLASH_KEYR

FLASH_SR

FLASH_CR
...

这些控制寄存器。

CPU 可以通过这些寄存器完成:

text
解锁 Flash

设置等待周期

启动 Sector 擦除

Flash 编程

查询 Busy

配置 Cache

开启 Prefetch

所以整个关系应该理解成:

text
CPU

 │ 读取程序

Flash Interface / ICODE/DCODE


Flash Memory


CPU

 │ 配置擦写操作

Flash Interface 控制寄存器


Flash Controller


真正操作 Flash Memory

因此:

Memory Map 中看到的 Flash Interface,往往是在描述 Flash 控制器的寄存器空间。

而:

text
0x08000000

描述的是:

真正的 Flash Memory 映射区域。

两者不要混为一谈。


22. 到目前为止,我们应该建立什么模型

这一大段前置知识,不要求一次全部背下来。

真正重要的是脑子里形成下面这条链:

text
C代码



CPU机器指令



CPU发起访问



I-Bus / D-Bus / S-Bus



32位地址



Memory Map / 地址译码



Bus Matrix



Flash / SRAM / Peripheral



真正的硬件资源

再把几个概念分清:

text
Memory Map
回答:
“这个地址代表谁?”


I / D / S Bus
回答:
“Cortex-M4 从哪个接口发起这类访问?”


Bus Matrix
回答:
“这个访问者有没有路能够到目标资源?
如果多人抢同一个资源怎么办?”


Memory Remap
回答:
“某一段 CPU 地址当前被映射到了哪个物理资源?”


Flash Interface
回答:
“Flash Memory 是怎么被管理、擦除和编程的?”

到这里,我们才真正拥有继续讨论:

text
裸机程序的代码放在哪里?

全局变量放在哪里?

局部变量放在哪里?

栈是什么?

堆是什么?

FreeRTOS 的任务栈又是什么?

pvPortMalloc() 到底从哪里拿内存?

这些问题的底层基础。

下一步,我们正式从:

> **一个普通裸机程序编译完成以后,Flash 和 SRAM 到底被分成了哪些区域?**

开始继续往 FreeRTOS 内存管理靠近。

FreeRTOS 内核与工程实践