ESP32存储器

概述

ESP32 采用两个哈佛结构 Xtensa LX6 CPU 构成双核系统。所有的片上存储器、片外存储器以及外设都分布在两个 CPU 的数据总线和/或指令总线上。
除个别情况外,两个 CPU 的地址映射呈对称结构,即使用相同的地址访问同一目标。系统中多个外设能够通过 DMA 访问片上存储器。
两个 CPU 的名称分别是 PRO_CPU 和 APP_CPU。PRO 代表 protocol(协议),APP 代表 application(应用)。在大多数情况下,两个 CPU 的功能是相同的。
ESP-IDF 区分了指令总线(IRAM、IROM、RTC FAST memory)和数据总线 (DRAM、DROM)。指令存储器是可执行的,只能通过 4 字节对齐字读取或写入。数据存储器不可执行,可以通过单独的字节操作访问。

主要特性

  • 地址空间

    • 对称地址映射
    • 数据总线与指令总线分别有 4 GB (32-bit) 地址空间
    • 1296 KB 片上存储器地址空间
    • 19704 KB 片外存储器地址空间
    • 512 KB 外设地址空间
    • 部分片上存储器与片外存储器既能被数据总线也能被指令总线访问
    • 328 KB DMA 地址空间
  • 片上存储器

    • 448 KB Internal ROM
    • 520 KB Internal SRAM
    • 8 KB RTC FAST Memory
    • 8 KB RTC SLOW Memory

注意
ESP32 上有 520 KB 的可用 SRAM(320 KB 的 DRAM 和 200 KB 的 IRAM)。 但是,由于技术限制,用于静态分配的 DRAM 最多可为 160 KB。 剩余的 160 KB(DRAM 总共 320 KB)只能在运行时分配为堆。

  • 片外存储器
    片外 SPI 存储器可作为片外存储器被映射到可用的地址空间。部分片上存储器可用作片外存储器的
    Cache。

    • 最大支持 16 MB 片外 SPI Flash
    • 最大支持 8 MB 片外 SPI SRAM
  • 外设

    • 41 个外设模块
  • DMA

    • 13 个具有 DMA 功能的模块

存储器速度

ROM 和 SRAM 的时钟源都是 CPU_CLK,CPU 可在单个时钟周期内访问这两个存储器。由于 RTC FAST Memory 的时钟源是 APB_CLOCK,RTC SLOW Memory 的时钟源是 FAST_CLOCK,所以 CPU 访问这两个存储器的速度稍慢。DMA 在 APB_CLK 时钟下访问存储器。
SRAM 每 32K 为一个块。只要同时访问的是不同的块,那么 CPU 和 DMA 可以同时以最快速度访问 SRAM。

存储器类型

DRAM(数据 RAM)

非常量静态数据(.data 段)和零初始化数据(.bss 段)由链接器放入内部 SRAM 作为数据存储。此区域中的剩余空间可在程序运行时用作堆。
通过应用 EXT_RAM_BSS_ATTR 宏,零初始化数据也可以放入外部 RAM。详见允许 .bss 段放入片外存储器
如果使用蓝牙堆栈,内部 DRAM 区域的可用大小将减少 64 KB(由于起始地址移动到 0x3FFC0000)。如果使用内存跟踪功能,该区域的长度还会减少 16 KB 或 32 KB。由于 ROM 引起的一些内存碎片问题,不可能将所有可用的 DRAM 用于静态分配,但是剩余的 DRAM 在运行时仍可用作堆。

IRAM(指令 RAM)

ESP-IDF 将内部 sram0 的部分区域分配为指令 RAM。该内存中第一个 64 KB 块用于 PRO 和 APP MMU 缓存,其余部分(即从 0x400800000x400A0000)用于存储需要从 RAM 运行的应用程序部分。

何时需要将代码放入 IRAM

以下情况时应将部分应用程序放入 IRAM:

  • 如果在注册中断处理程序时使用了 ESP_INTR_FLAG_IRAM,则中断处理程序必须要放入 IRAM。
  • 可将一些时序关键代码放入 IRAM,以减少从 flash 中加载代码造成的相关损失。ESP32 通过 MMU 缓存从 flash 中读取代码和数据。在某些情况下,将函数放入 IRAM 可以减少由缓存未命中造成的延迟,从而显著提高函数的性能。

ESP32 中的每个外设和存储器通过 MMU(存储器管理单元)或 MPU(存储器保护单元)被访问。根据 OS 给予应用的权限,MPU 和 MMU 可以允许或禁止应用访问存储器某部分和外设。MMU 还可以进行虚地址和实地址的转换,将片上或片外存储器的地址范围映射到某个虚拟存储器区域。这些映射可根据应用程序配置,因此每个应用程序的内存可根据其运行所需进行配置。OS 和应用程序运行时,分别配有一个进程号(即 PID),用于区分彼此。进程号共有 8 个。每个 OS 和应用程序都有自己的映射和权限。详见存储器管理和保护单元 (MMU, MPU)

如何将代码放入 IRAM

借助链接器脚本,一些代码会被自动放入 IRAM 区域中。
如果需要将某些特定的应用程序代码放入 IRAM,可以使用 链接器脚本生成机制 功能并在组件中添加链接器脚本片段文件,在该片段文件中,可以给整个目标源文件或其中的个别函数打上 noflash 标签。更多信息可参考链接器脚本生成机制。
或者,也可以通过使用 IRAM_ATTR 宏在源代码中指定需要放入 IRAM 的代码:

1
2
3
4
5
6
#include "esp_attr.h"

void IRAM_ATTR gpio_isr_handler(void* arg)
{
// ...
}

IRAM_ATTR 函数中的字符串或常量可能没有自动放入 RAM 中,这时可以使用 DRAM_ATTR 属性进行标记,或者也可以使用链接器脚本方法将它们自动放入 RAM 中。

1
2
3
4
5
void IRAM_ATTR gpio_isr_handler(void* arg)
{
const static DRAM_ATTR uint8_t INDEX_DATA[] = { 45, 33, 12, 0 };
const static char *MSG = DRAM_STR("I am a string stored in RAM");
}

注意,具体哪些数据需要被标记为 DRAM_ATTR 可能很难确定。如果没有被标记为 DRAM_ATTR,某些变量或表达式有时会被编译器识别为常量(即使它们没有被标记为 const)并将其放入 flash 中。

IROM(代码从 flash 中运行)

如果一个函数没有被显式地声明放在 IRAM 或者 RTC 存储器中,则它会放在 flash 中。由于 IRAM 空间有限,应用程序的大部分二进制代码都需要放入 IROM 中。
在 启动 过程中,从 IRAM 中运行的引导加载程序配置 MMU flash 缓存,将应用程序的指令代码区域映射到指令空间。通过 MMU 访问的 flash 使用一些内部 SRAM 进行缓存,访问缓存的 flash 数据与访问其他类型的内部存储器一样快。

DROM(数据存储在 flash 中)

默认情况下,链接器将常量数据放入一个映射到 MMU flash 缓存的区域中。这与 IROM(代码从 flash 中运行) 部分相同,但此处用于只读数据而不是可执行代码。
唯一没有默认放入 DROM 的常量数据是被编译器嵌入到应用程序代码中的字面常量。这些被放置在周围函数的可执行指令中。DRAM_ATTR 属性可以用来强制将常量从 DROM 放入 DRAM(数据 RAM) 部分。

RTC Slow memory(RTC 慢速存储器)

从 RTC 存储器运行的代码中使用的全局和静态变量必须放入 RTC Slow memory 中。例如 深度睡眠 变量可以放在 RTC Slow memory 中,而不是 RTC FAST memory,或者也可以放入由ULP 协处理器编程访问的代码和变量。
RTC_NOINIT_ATTR 可以用来将数据放入 RTC Slow memory。放入此类型存储器的值从深度睡眠模式中醒来后会保持值不变。

1
RTC_NOINIT_ATTR uint32_t rtc_noinit_data;

RTC FAST memory(RTC 快速存储器)

RTC FAST memory 的同一区域既可以作为指令存储器也可以作为数据存储器进行访问。从深度睡眠模式唤醒后必须要运行的代码要放在 RTC 存储器中。

具备 DMA 功能

大多数的 DMA 控制器(比如 SPI、sdmmc 等)都要求发送/接收缓冲区放在 DRAM 中,并且按字对齐。我们建议将 DMA 缓冲区放在静态变量而不是堆栈中。使用 DMA_ATTR 宏可以声明该全局/本地的静态变量具备 DMA 功能,例如:

1
2
3
4
5
6
7
8
9
10
11
12
DMA_ATTR uint8_t buffer[]="I want to send something";

void app_main()
{
// 初始化代码
spi_transaction_t temp = {
.tx_buffer = buffer,
.length = 8 * sizeof(buffer),
};
spi_device_transmit(spi, &temp);
// 其它程序
}

或者

1
2
3
4
5
6
7
8
9
10
11
void app_main()
{
DMA_ATTR static uint8_t buffer[] = "I want to send something";
// 初始化代码
spi_transaction_t temp = {
.tx_buffer = buffer,
.length = 8 * sizeof(buffer),
};
spi_device_transmit(spi, &temp);
// 其它程序
}

在堆栈中放置 DMA 缓冲区

可以在堆栈中放置 DMA 缓冲区,但建议尽量避免。如果实在有需要的话,请注意以下几点:

  • 如果堆栈在 PSRAM 中,则不建议将 DRAM 缓冲区放在堆栈上。如果任务堆栈在 PSRAM 中,则必须执行 片外 RAM 中描述的几个步骤。
  • 在函数中使用 WORD_ALIGNED_ATTR 宏来修饰变量,将其放在适当的位置上,比如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void app_main()
    {
    uint8_t stuff;
    WORD_ALIGNED_ATTR uint8_t buffer[] = "I want to send something"; //否则 buffer 会被存储在 stuff 变量后面
    // 初始化代码
    spi_transaction_t temp = {
    .tx_buffer = buffer,
    .length = 8 * sizeof(buffer),
    };
    spi_device_transmit(spi, &temp);
    // 其它程序
    }