ESP32-IDF03-1 系统-内存管理
Posted Ciaran-byte
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32-IDF03-1 系统-内存管理相关的知识,希望对你有一定的参考价值。
ESP32内存结构
1. 内存分段
操作系统分配给进程的内存空间中包含五种段:数据段、代码段、BSS、堆、栈。
- 数据段:存放程序中的静态变量和已初始化且不为零的全局变量。
- 代码段:存放可执行文件的操作指令,代码段是只读的,不可进行写操作。这部分的区域在运行前已知其大小。
- BSS段( Block Started BySymbol):存放未初始化的全局变量,在变量使用前由运行时初始化为零。
- 堆:存放进程运行中被动态分配的内存,其大小不固定。
- 栈:存放程序中的临时的局部变量和函数的参数值。
2. ESP32 内存模型
2.1 片内RAM
2.1.1 片内RAM空间布局
上图是ESP32内部存储器的布局,SRAM分为3个存储块SRAM0,SRAM1和SRAM2。(还有RTC快速和慢速存储器)
按照功能,SRAM可以分为两种:
- IRAM: 存储指令的RAM,存储的是代码段(text段)。IRAM可以使用SRAM0和SRAM1
- DRAM: 存储数据的RAM,主要用来存储BSS段,数据段,堆区和栈区的数据。DRAM可以使用SRAM1和SRAM2
虽然理论上,SRAM1可以用于存储IRAM和DRAM,但是实际上,RAM的默认分配情况是
- IRAM:192KB,占用SRAM0
- DRAM:328KB 占用SRAM1和SRAM2
但是值得注意的是,IRAM和DRAM的地址范围方向是相反的。
2.1.2 IRAM组织结构
接下来单独看看IRAM里面包括什么
ESP32 中 192 KB 的可用 IRAM 用于代码执行,并且其中一部分作为高速缓存(Cache)用于访问 Flash(和 PSRAM )。
- 前 32KB IRAM 用作 CPU0 的高速缓存,接下来的 32KB 用作 CPU1 高速缓存。这是在硬件中静态配置的,无法更改。
- 在第一个 64KB 之后,链接脚本开始将 text 段放置在 IRAM 中。它首先放置所有中断向量,然后放置已编译应用程序中所有标记为放置在 IRAM 中的 text 段。在通常情况下,大多数应用程序代码从 flash (XiP)执行,但某些代码对执行时间有较高要求,或者本身需要操作 flash,需要将它们放置在 IRAM 中。这项操作通过对这些函数或代码文件添加特定属性标识实现,链接程序脚本将据属性标识将它们放置在 IRAM 中。链接脚本将 _iram_text_start 和 _iram_text_end 符号放置在 text 段的两个边界处。
- text 段之后的 IRAM 保持未使用状态,并添加到堆中。
当应用程序配置为单核模式时,CPU1 不工作并且不启用 CPU1 Cache。在这种情况下,CPU1 Cache 的空间(0x40078000–0x4007FFFF)将被添加到堆中。
IRAM 也可以用于放置数据,但有两个重要限制条件:
- 用于访问 IRAM 中数据的地址必须是 32 位对齐的;
- 访问的数据大小也必须是 32 位对齐的。
2.1.3 DRAM
上图显示了应用程序的典型(简化)DRAM 布局。由于 DRAM 地址从 SRAM2 的末尾开始,并向后增加,因此链接阶段段空间的分配从 SRAM2 的末尾开始。
- 前 8KB(0x3FFA_E000–0x3FFA_FFFF)用作某些 ROM 内置函数的数据空间;
链接器紧接着将已初始化的数据段放在第一个 8KB 存储器之后; - 接下来是未初始化的 BSS 段;
- 数据段和 BSS 段之后剩余的内存被配置为堆,典型的动态内存分配一般分配至该位置。
2.1.4 启用蓝牙之后的DRAM
启用蓝牙(BT)功能后, BT 控制器(软件和硬件)需要使用专用的数据空间。该空间作为控制器的 Data/BSS 段,同时作为传输空间用于 BT 数据包在软件和硬件之间传输。因此,链接脚本在默认的 DRAM 空间中保留了 0x3FFB_0000–0x3FFB_DB5C 之间的 54KB 空间,在该区域之后才进行应用程序的数据段和 BSS 段分配。
当应用程序仅使用低功耗蓝牙(BLE)功能时,可以将 BT 控制器内存的一部分交还给堆。释放并添加到堆中的内存大小约为 19KB。
2.1.5 启用跟踪调试空间之后 DRAM 组织结构
应用程序级的跟踪调试(Trace)启用以后,它将在 DRAM 的末尾保留一个固定为 32KB 的内存空间。请注意,上图显示了未启用 BT 时的内存布局。但是应用程序也可以在启用BT的情况下使用跟踪,在这种情况下,链接脚本中也会保留 BT 控制器的内存空间。
2.2 片外RAM
2.2.1 PSRAM (也叫SPIRAM)
ESP32 提供了在 QSPI 总线上外接伪静态 RAM (PSRAM 又名 SPIRAM)的能力,该总线同时用于访问 flash,二者同时工作时利用片选信号进行切换。该存储器同 flash 一样可直接寻址,访问过程通过 IRAM 中的 Cache 进行。ESP32 在其地址空间 0x3F80_0000 至 0x3FBF_FFFF 最多可映射 4MB SPIRAM (译者注:新版本 IDF 可使用 Himem API 访问最大为 8 MB 的 SPIRAM)。应用程序通过三种方式使用 SPIRAM :
- 使用 SPIRAM 保存特定软件模块的 BSS 段;
- 使用堆分配器从 SPIRAM 动态分配内存;
- 通过直接内存映射,在应用程序中使用静态地址访问 SPIRAM。
虽然这允许应用程序使用额外的内存,但对 SPIRAM 的使用有以下限制:
- SPIRAM 不支持 DMA,在需要使用 DMA 向/从外设传输数据的情况下,不能使用它;
由于 flash 和 SPIRAM 使用同一 QSPI 总线与 ESP32 通信,因此在执行禁用 XiP 模式的代码中不能使用 SPIRAM; - 由于 SPIRAM 访问比内部 SRAM 慢,因此建议对性能有要求的代码使用内部 SRAM 保存数据。
此处详细介绍了使用 SPIRAM 的这些方式以及使用限制。
3. ESP32 内存的使用
3.1 IRAM
ESP-IDF 将内部 SRAM0 区域(在技术参考手册中有定义)的一部分分配为指令 RAM。除了开始的 64kB 用作 PRO CPU 和 APP CPU 的高速缓存外,剩余内存区域(从 0x40080000 至 0x400A0000 )被用来存储应用程序中部分需要在RAM中运行的代码。
一些 ESP-IDF 的组件和 WiFi 协议栈的部分代码通过链接脚本文件被存放到了这块内存区域。
如果一些应用程序的代码需要放在 IRAM 中运行,可以使用 IRAM_ATTR 宏定义进行声明。
#include "esp_attr.h"
void IRAM_ATTR gpio_isr_handler(void* arg)
{
// ...
}
下面列举了应用程序中可能或者应该放入 IRAM 中运行例子。
-
当注册中断处理程序的时候设置了 ESP_INTR_FLAG_IRAM ,那么中断处理程序就必须要放在 IRAM 中运行。这种情况下,ISR 只能调用存放在 IRAM 或者 ROM 中的函数。 注意 :目前所有 FreeRTOS 的 API 都已经存放到了 IRAM 中,所以在中断中调用 FreeRTOS 的中断专属 API 是安全的。如果将 ISR 放在 IRAM 中运行,那么必须使用宏定义 DRAM_ATTR 将该 ISR 用到所有常量数据和调用的函数(包括但不限于 const char 数组)放入 DRAM 中。
-
可以将一些时间关键的代码放在 IRAM 中,这样可以缩减从 Flash 加载代码所消耗的时间。ESP32 是通过 32kB 的高速缓存来从外部 Flash 中读取代码和数据的,将函数放在 IRAM 中运行可以减少由高速缓存未命中引起的时间延迟。
3.2 IROM
如果一个函数没有被显式地声明放在 IRAM 或者 RTC 内存中,则将其置于 Flash 中。Flash 技术参考手册中介绍了 Flash MMU 允许代码从 Flash 执行的机制。ESP-IDF 将从 Flash 中执行的代码放在 0x400D0000 — 0x40400000 区域的开始,在启动阶段,二级引导程序会初始化 Flash MMU,将代码在 Flash 中的位置映射到这个区域的开头。对这个区域的访问会被透明地缓存到 0x40070000 — 0x40080000 范围内的两个 32kB 的块中。
3.3 RTC快速内存
从深度睡眠模式唤醒后必须要运行的代码要放在 RTC 内存中
3.4 DRAM
链接器将非常量静态数据和零初始化数据放入 0x3FFB0000 — 0x3FFF0000 这 256kB 的区域。注意,如果使用蓝牙堆栈,此区域会减少 64kB(通过将起始地址移至 0x3FFC0000 )。如果使用了内存跟踪的功能,该区域的长度还要减少 16kB 或者 32kB。放置静态数据后,留在此区域中的剩余空间都用作运行时堆。
常量数据也可以放在 DRAM 中,例如,用在 ISR 中的常量数据(参见上面 IRAM 部分的介绍),为此需要使用 DRAM_ATTR 宏来声明。
DRAM_ATTR const char[] format_string = "%p %x";
char buffer[64];
sprintf(buffer, format_string, ptr, val);
宏 __NOINIT_ATTR 可以用来声明将数据放在 .noinit 段中,放在此段中的数据不会在启动时被初始化,并且在软件重启后会保留原来的值。
例如
__NOINIT_ATTR uint32_t noinit_data;
3.5 DROM
默认情况下,链接器将常量数据放入一个 4MB 区域 (0x3F400000 — 0x3F800000) ,该区域用于通过 Flash MMU 和高速缓存来访问外部 Flash。一种特例情况是,字面量会被编译器嵌入到应用程序代码中。
3.6 RTC慢速内存
从 RTC 内存运行的代码(例如深度睡眠模块的代码)使用的全局和静态变量必须要放在 RTC 慢速内存中。更多详细说明请查看文档 深度睡眠 。
宏 RTC_NOINIT_ATTR 用来声明将数据放入 RTC 慢速内存中,该数据在深度睡眠唤醒后将保持不变。
例如
RTC_NOINIT_ATTR uint32_t rtc_noinit_data;
3.7 DMA
大多数的 DMA 控制器(比如 SPI,SDMMC 等)都要求发送/接收缓冲区放在 DRAM 中,并且按字对齐。我们建议将 DMA 缓冲区放在静态变量中而不是堆栈中。使用 DMA_ATTR 宏可以声明该全局/本地的静态变量具备 DMA 能力,例如:
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 );
// 其他程序
}
或者
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 缓冲区仍然是允许的,但是你必须记住:
- 如果堆栈在 pSRAM 中,切勿尝试这么做,因为堆栈在 pSRAM 中的话就要按照
menuconfig 中使能 SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY ),所以请确保你的任务不在 pSRAM 中。 - 在函数中使用 WORD_ALIGNED_ATTR
宏来修饰变量,将其放在适当的位置上,比如:
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 );
// 其他程序
}
4. 堆的分配与调试
4.1 相关API
4.1.1 分配特定用途的内存
heap_caps_malloc(size, MALLOC_CAP_8BIT)函数能够在DRAM中分配一块指定功能的内存
#define MALLOC_CAP_EXEC (1<<0) ///< Memory must be able to run executable code
#define MALLOC_CAP_32BIT (1<<1) ///< Memory must allow for aligned 32-bit data accesses
#define MALLOC_CAP_8BIT (1<<2) ///< Memory must allow for 8/16/...-bit data accesses
#define MALLOC_CAP_DMA (1<<3) ///< Memory must be able to accessed by DMA
#define MALLOC_CAP_PID2 (1<<4) ///< Memory must be mapped to PID2 memory space (PIDs are not currently used)
#define MALLOC_CAP_PID3 (1<<5) ///< Memory must be mapped to PID3 memory space (PIDs are not currently used)
#define MALLOC_CAP_PID4 (1<<6) ///< Memory must be mapped to PID4 memory space (PIDs are not currently used)
#define MALLOC_CAP_PID5 (1<<7) ///< Memory must be mapped to PID5 memory space (PIDs are not currently used)
#define MALLOC_CAP_PID6 (1<<8) ///< Memory must be mapped to PID6 memory space (PIDs are not currently used)
#define MALLOC_CAP_PID7 (1<<9) ///< Memory must be mapped to PID7 memory space (PIDs are not currently used)
#define MALLOC_CAP_SPIRAM (1<<10) ///< Memory must be in SPI RAM
#define MALLOC_CAP_INTERNAL (1<<11) ///< Memory must be internal; specifically it should not disappear when flash/spiram cache is switched off
#define MALLOC_CAP_DEFAULT (1<<12) ///< Memory can be returned in a non-capability-specific memory allocation (e.g. malloc(), calloc()) call
#define MALLOC_CAP_INVALID (1<<31) ///< Memory can't be used / list end marker
4.1.2 在运行时查询DRAM剩余空间
heap_caps_get_free_size(MALLOC_CAP_8BIT)
4.1.3 释放空间
heap_caps_malloc()分配的空间可以用free来释放
4.2 启动时查看内存使用情况
4.2.1 DRAM
在启动时,可以通过idf.size 命令查看静态分配的内存。一般DRAM最大静态内存是160KB,剩余的只能作为堆区
同时,运行时DRAM大小可能比启动时小,因为运行时加载FreeRTOS也占用空间
4.2.2 IRAM
在启动时,可以通过idf.size查看IRAM使用情况
5. 片外RAM的配置和使用
5.1 简介
ESP32 提供了好几百 KB 的片上 RAM,可以满足大部分需求。但有些场景可能需要更多 RAM,因此 ESP32 另外提供了高达 4 MB 的片外 SPI RAM 存储器以供用户使用。片外 RAM 被添加到内存映射中,在某些范围内与片上 RAM 使用方式相同。
ESP32 支持与 SPI Flash 芯片并联的 SPI PSRAM。虽然 ESP32 支持多种类型的 RAM 芯片,但 ESP-IDF 当前仅支持乐鑫品牌的 PSRAM 芯片,如 ESP-PSRAM32、ESP-PSRAM64 等。
PSRAM 芯片的工作电压分为 1.8 V 和 3.3 V。其工作电压必须与 flash 的工作电压匹配。请查询您 PSRAM 芯片以及 ESP32 的技术规格书获取准确的工作电压。对于 1.8 V 的 PSRAM 芯片,请确保在启动时将 MTDI 管脚设置为高电平,或者将 ESP32 中的 eFuses 设置为始终使用 1.8 V 的 VDD_SIO 电平,否则有可能会损坏 PSRAM 和/或 flash 芯片。
乐鑫同时提供模组和系统级封装芯片,集成了兼容的 PSRAM 和 flash,可直接用于终端产品 PCB 中。如需了解更多信息,请前往乐鑫官网。
5.2 配置
5.2.1 集成片外RAM带ESP32内存映射
在 CONFIG_SPIRAM_USE 中选择 “Integrate RAM into memory map(集成片外 RAM 到 ESP32 内存映射)” 选项。
这是集成片外 RAM 最基础的设置选项,大多数用户需要用到其他更高级的选项。
ESP-IDF 启动过程中,片外 RAM 被映射到以 0x3F800000 起始的数据地址空间(字节可寻址),空间大小正好为 SPI RAM 的大小 (4 MB)。
应用程序可以通过创建指向该区域的指针手动将数据放入片外存储器,同时应用程序全权负责管理片外 SPI RAM,包括协调 Buffer 的使用、防止发生损坏等。
5.2.2 添加片外RAM到堆内存分配器
在 CONFIG_SPIRAM_USE 中选择 “Make RAM allocatable using heap_caps_malloc(…, MALLOC_CAP_SPIRAM)” 选项。
启用上述选项后,片外 RAM 被映射到地址 0x3F800000,并将这个区域添加到 堆内存分配器 里携带 MALLOC_CAP_SPIRAM 的标志
程序如果想从片外存储器分配存储空间,则需要调用 heap_caps_malloc(size, MALLOC_CAP_SPIRAM),之后可以调用 free() 函数释放这部分存储空间。
5.2.3 调用malloc分配片外RAM
在 CONFIG_SPIRAM_USE 中选择 “Make RAM allocatable using malloc() as well” 选项,该选项为默认选项。
启用此选项后,片外存储器将被添加到内存分配程序(与上一选项相同),同时也将被添加到由标准 malloc() 函数返回的 RAM 中。
这允许应用程序使用片外 RAM,无需重写代码就能使用 heap_caps_malloc(…, MALLOC_CAP_SPIRAM)
如果某次内存分配偏向于片外存储器,您也可以使用 CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL 设置分配空间的大小阈值,控制分配结果:
-
如果分配的空间小于阈值,分配程序将首先选择内部存储器。
-
如果分配的空间等于或大于阈值,分配程序将首先选择外部存储器。
如果优先考虑的内部或外部存储器中没有可用的存储块,分配程序则会选择其他类型存储。
由于有些 Buffer 仅可在内部存储器中分配,因此需要使用第二个配置项 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL 定义一个内部存储池,仅限显式的内部存储器分配使用(例如用于 DMA 的存储器)。常规 malloc() 将不会从该池中分配,但可以使用 MALLOC_CAP_DMA 和 MALLOC_CAP_INTERNAL 旗标从该池中分配存储器。
5.2.4 允许BSS段放入片外存储器
通过检查 CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY 启用该选项,此选项配置与上面三个选项互不影响。
启用该选项后,从 0x3F800000 起始的地址空间将用于存储来自 lwip、net80211、libpp 和 bluedroid ESP-IDF 库中零初始化的数据(BSS 段)。
EXT_RAM_ATTR 宏应用于任何静态声明(未初始化为非零值)之后,可以将附加数据从内部 BSS 段移到片外 RAM。
也可以使用链接器片段方案 extram_bss 将组件或库的 BSS 段放到片外 RAM 中。
启用此选项可以减少 BSS 段占用的内部静态存储。
剩余的片外 RAM 也可以通过上述方法添加到堆分配器中。
5.3 使用限制
- Flash cache 禁用时(比如,正在写入 flash),片外 RAM 将无法访问;同样,对片外 RAM 的读写操作也将导致 cache 访问异常。出于这个原因,ESP-IDF 不会在片外 RAM 中分配任务堆栈(详见下文)。
- 片外 RAM 不能用于储存 DMA 事物描述符,也不能用作 DMA 读写操作的缓冲区 (Buffer)。与 DMA 搭配使用的 Buffer 必须先使用 heap_caps_malloc(size, MALLOC_CAP_DMA) 进行分配,之后可以调用标准 free() 回调释放 Buffer。
- 片外 RAM 与片外 flash 使用相同的 cache 区域,这意味着频繁在片外 RAM 访问的变量可以像在片上 RAM 中一样快速读取和修改。但访问大块数据时(大于 32 KB),cache 空间可能会不足,访问速度将回落到片外 RAM 访问速度。此外,访问大块数据可以挤出 flash cache,可能会降低代码执行速度。
- 片外 RAM 不可用作任务堆栈存储器。因此 xTaskCreate() 及类似函数将始终为堆栈和任务 TCB 分配片上储存器,而 xTaskCreateStatic() 类型的函数将检查传递的 Buffer 是否属于片上存储器。
6. 内存空间查看例程
/*
Name: Sketch1.ino
Created: 2021/4/12 18:46:05
Author: hp
*/
// the setup function runs once when you press reset or power the board
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
//使用 spi flash
void setup() {
//获取剩余的dram大小
size_t dram = heap_caps_get_free_size(MALLOC_CAP_8BIT);
//获取剩余的iram大小
size_t iram = heap_caps_get_free_size(MALLOC_CAP_32BIT) - heap_caps_get_free_size(MALLOC_CAP_8BIT);
//获取剩余的dram大小与heap_caps_get_free_size函数一样
uint32_t data = xPortGetFreeHeapSize();
//获取最大的连续的堆区空间
size_t heapmax = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
int* p = (int*)malloc(heapmax);
free(p);
//获取栈区的最高水位线(也就是栈顶,意味着最大栈区空间)
int stackmark=uxTaskGetStackHighWaterMark(NULL);
char stack[6000];
memset(stack, 1, 6000);
printf("data=%d\\n", data);
printf("dram=%d\\n", dram);
printf("iram=%d\\n", iram);
printf("max=%d\\n", heapmax);
printf("stackmark=%d\\n", stackmark);
while(1){}
}
void loop() {
}
参考资料
以上是关于ESP32-IDF03-1 系统-内存管理的主要内容,如果未能解决你的问题,请参考以下文章