本文翻译自:ESP32 Programmers’ Memory Model - Amey Inamdar
MCU 中的内存资源可能是其最宝贵的资源,因为它在芯片中占据最大的面积。更新的应用程序对内存的需求正在不断增长。为了充分利用硬件资源,理解内存架构并能针对应用程序的实际用例进行内存优化变得至关重要。特别是对于包含通信子系统( Wi-Fi 和 BT/BLE )的 ESP32 SoC 架构,通信子系统本身需要占用一定数量的内存才能运行,因此有必要明确应用程序的需求并对其进行内存优化。
我们经常会遇到有关应用程序可用内存余量的问题;除非我们深入了解用例,否则这个问题没有简单的答案。但是,当开发人员了解有关内存布局,系统要求和常见优化方法的详细信息时,就会发现 ESP32 可以适应各种有趣的应用程序用例。
本博客旨在为开发者提供 ESP32 SoC 的内存布局概述,介绍不同的内存区域及其特性,并讨论典型 ESP32 固件的内存分配。
请注意,此处提到的所有特定详细信息均与 ESP-IDF 发布版本 4.0 有关,该版本是撰写此博客时最新的稳定发布版本(译者注:原文发布于 2020 年 7 月 3 日)。
上图显示了 ESP32 内部存储器(SRAM)的布局。SRAM 分为 3 个存储块 SRAM0、SRAM1 和SRAM2(以及 RTC 快速和慢速存储器 2 个小块,我们将在后面分别讨论)。
SRAM 以两种方式使用:一种用于指令存储,称为 IRAM(用于执行代码,text 段),另一种用于数据存储,称为 DRAM(用作 BSS 段,Data 段和堆)。SRAM0 和 SRAM1 可以用作连续的 IRAM,而 SRAM1 和 SRAM2 可以用作连续的 DRAM 地址空间。
现在让我们放大 IRAM 分段。
ESP32 中 192 KB 的可用 IRAM 用于代码执行,并且其中一部分作为高速缓存(Cache)用于访问 Flash(和 PSRAM )。
链接脚本将 _iram_text_start 和 _iram_text_end 符号放置在 text 段的两个边界处。 text 段之后的 IRAM 保持未使用状态,并被添加到堆中。
并且,当应用程序配置为单核模式时,CPU1 不工作并且不启用 CPU1 Cache。在这种情况下,CPU1 Cache 的空间(0x40078000–0x4007FFFF)将被添加到堆中。
放置在堆中的未使用的 IRAM 可以通过动态分配访问。
如果应用程序有此要求,它可用于在 IRAM 中放置任何代码。但是,这种情况很少见。
IRAM 也可以用于放置数据,但有两个重要限制条件:
如果应用程序具有可以遵循这两个访问规则的数据,则 IRAM 空间可用于存储该数据。
还有一种方法可以不受此限制的访问 IRAM 空间。但是作为访问速度会变慢。这将在后面的部分中讨论。
上图显示了应用程序的典型(简化)DRAM 布局。由于 DRAM 地址从 SRAM2 的末尾开始,并向后增加,因此链接阶段段空间的分配从 SRAM2 的末尾开始。
请注意,数据段和 BSS 段的大小取决于应用程序。因此,每个应用程序根据其使用的组件和所调用的 API 都有不同的可用堆大小。
堆代码中有两个区域(0x3FFE_0000–0x3FFE_0440 - 共 1088 字节)和(0x3FFE_3F20–0x3FFE_4350 - 共 1072 字节)供 ROM 代码存放数据。这些区域被标记为保留,并且堆分配器不会从这些区域分配内存。
启用蓝牙(BT)功能后, BT 控制器(软件和硬件)需要使用专用的数据空间。该空间作为控制器的 Data/BSS 段,同时作为传输空间用于 BT 数据包在软件和硬件之间传输。因此,链接脚本在默认的 DRAM 空间中保留了 0x3FFB_0000–0x3FFB_DB5C 之间的 54KB 空间,在该区域之后才进行应用程序的数据段和 BSS 段分配。
当应用程序仅使用低功耗蓝牙(BLE)功能时,可以将 BT 控制器内存的一部分交还给堆。释放并添加到堆中的内存大小约为 19KB。
应用程序级的跟踪调试(Trace)启用以后,它将在 DRAM 的末尾保留一个固定为 32KB 的内存空间。请注意,上图显示了未启用 BT 时的内存布局。但是应用程序也可以在启用BT的情况下使用跟踪,在这种情况下,链接脚本中也会保留 BT 控制器的内存空间。
ESP32 提供了在 QSPI 总线上外接伪静态 RAM (PSRAM 又名 SPIRAM)的能力,该总线同时用于访问 flash,二者同时工作时利用片选信号进行切换。该存储器同 flash 一样可直接寻址,访问过程通过 IRAM 中的 Cache 进行。ESP32 在其地址空间 0x3F80_0000 至 0x3FBF_FFFF 最多可映射 4MB SPIRAM (译者注:新版本 IDF 可使用 Himem API 访问最大为 8 MB 的 SPIRAM)。应用程序通过三种方式使用 SPIRAM :
虽然这允许应用程序使用额外的内存,但对 SPIRAM 的使用有以下限制:
此处详细介绍了使用 SPIRAM 的这些方式以及使用限制。
从以上 IRAM 和 DRAM 内存布局可以看到,DRAM 区域 _bss_end 到 0x3FFF_FFFF(或 _heap_end 在跟踪调试启用时)和 IRAM 区 _iram_text_end 到 0x4009_FFFF 是未使用的内存空间。如果系统中有 SPIRAM ,则该内存也属于未使用的内存空间。应用程序和 SDK 组件始终需要按需分配和释放内存。因此,通用内存分配器(也称为堆分配器)用于操作可用内存空间,并为其提供内存分配和释放 API 。
如您所见,堆分配器控制下的内存区域具有不同的功能和访问属性。因此,ESP-IDF 实现了一个基于功能的堆分配器,调用者可以在指定分配大小时同时指定用途。例如,应用程序可能指定分配具有 DMA 功能的内存空间,以便与某些外设一起使用,或者它可以指定从外部 SPIRAM 为音频缓冲区分配内存,因为从内部 DRAM 分配不是更好的选择。
ESP-IDF 还在基于功能的堆分配器的 API 之封装了通用的 malloc 和 free API ,以使应用程序易于从 POSIX 类型的系统移植。应用程序配置项可以包含一组管理规则,使 malloc API 能够根据分配的大小自动选择实际的内存段位置。
从 ESP-IDF 4.2 版本开始,我们增加了使用 IRAM 进行数据存储的功能。如上所述,IRAM 具有地址和大小对齐的访问限制。如果进行未对齐访问,则会导致异常。在 4.2 版之后,ESP-IDF 透明地处理这些异常,以提供调用者所需的 load/store 功能。由于这些未对齐的访问会导致异常,因此访问速度将比 DRAM 慢。通常,每个异常处理大约需要 167 个 CPU 周期(即 240 MHz 时每次访问 0.7 usc 或 160 MHz 时每次访问 1 usec)。应用程序或 SDK 组件可以在链接时将 IRAM 用于 BSS 数据,或者在运行时通过堆分配器使用 IRAM 。使用 IRAM 进行数据有两个限制:
ESP-IDF 4.2 提供了一些现成的配置,可以有效地利用未使用的 IRAM 进行数据操作,例如以单核模式下发送和接收 TLS 片段。
请查看此博客,以获取有关 AWS-IoT 客户端应用程序的内存分析和一些常见优化技术的案例研究。