存储器类型

[English]

ESP32-C6 芯片具有不同类型的存储器和灵活的存储器映射特性,本小节将介绍 ESP-IDF 默认如何使用这些功能。

ESP-IDF 区分了指令总线(IRAM、IROM、RTC FAST memory)和数据总线 (DRAM、DROM)。指令存储器是可执行的,只能通过 4 字节对齐字读取或写入。数据存储器不可执行,可以通过单独的字节操作访问。有关总线的更多信息,请参阅 ESP32-C6 技术参考手册 > 系统和存储器 [PDF]。

DRAM(数据 RAM)

非常量静态数据(.data 段)和零初始化数据(.bss 段)由链接器放入内部 SRAM 作为数据存储。此区域中的剩余空间可在程序运行时用作堆。

备注

静态分配的 DRAM 的最大值也会因编译应用程序的 IRAM(指令 RAM) 大小而减小。运行时可用的堆内存会因应用程序的总静态 IRAM 和 DRAM 使用而减少。

常量数据也可能被放入 DRAM,例如当它被用于 non-flash-safe ISR 时(具体请参考 如何将代码放入 IRAM)。

"noinit" DRAM

可以将 __NOINIT_ATTR 宏用作属性,从而将数据放入 .noinit 部分。放入该部分的值在启动时不会被初始化,在软件重启后也会保持值不变。

示例:

__NOINIT_ATTR uint32_t noinit_data;

IRAM(指令 RAM)

备注

内部 SRAM 中不用于指令 RAM 的部分都会作为 DRAM(数据 RAM) 供静态数据和动态分配(堆)使用。

何时需要将代码放入 IRAM

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

  • 如果在注册中断处理程序时使用了 ESP_INTR_FLAG_IRAM,则中断处理程序必须要放入 IRAM。更多信息可参考 IRAM 安全中断处理程序

  • 可将一些时序关键代码放入 IRAM,以减少从 flash 中加载代码造成的相关损失。ESP32-C6 通过 MMU 缓存从 flash 中读取代码和数据。在某些情况下,将函数放入 IRAM 可以减少由缓存未命中造成的延迟,从而显著提高函数的性能。

如何将代码放入 IRAM

借助链接器脚本,一些代码会被自动放入 IRAM 区域中。

如果需要将某些特定的应用程序代码放入 IRAM,可以使用 链接器脚本生成机制 功能并在组件中添加链接器脚本片段文件,在该片段文件中,可以给整个目标源文件或其中的个别函数打上 noflash 标签。更多信息可参考 链接器脚本生成机制

或者,也可以通过使用 IRAM_ATTR 宏在源代码中指定需要放入 IRAM 的代码:

#include "esp_attr.h"

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

放入 IRAM 后可能会导致 IRAM 安全中断处理程序出现问题:

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

    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 中。

  • GCC 的优化会自动生成跳转表或 switch/case 查找表,并将这些表放在 flash 中。IDF 默认在编译所有文件时使用 -fno-jump-tables -fno-tree-switch-conversion 标志来避免这种情况。

可以为不需要放置在 IRAM 中的单个源文件重新启用跳转表优化。关于如何在编译单个源文件时添加 -fno-jump-tables -fno-tree-switch-conversion 选项,请参考 组件编译控制

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 FAST memory(RTC 快速存储器)

备注

对于 ESP32-C6,RTC 存储器已被重新重命名为 LP(低功耗)存储器。在与 ESP32-C6 相关的 IDF 代码、文档以及技术参考手册中,可能会出现这两个术语混用的情况。

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

除非禁用 CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP 选项,否则剩余的 RTC FAST memory 会被添加到堆中。该部分内存可以和 DRAM(数据 RAM) 互换使用,但是访问速度稍慢一点。

具备 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);
    // 其它程序
}

也可以通过使用 MALLOC_CAP_DMA 标志来动态分配具备 DMA 能力的内存缓冲区。

在堆栈中放置 DMA 缓冲区

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

  • 在函数中使用 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);
        // 其它程序
    }