Zephyr下esp32s3外部psram配置细节

ESP32S3 本身有 512KB 的内部 RAM,在内部 RAM 不够的情况下,可以通过配置外部 PSRAM 来扩展内存容量。Zephyr目前已经完整的支持ESP32S3的PSRAM。本文从使用入手,通过分析配置和实现来剖析esp32s3的PSRAM在Zephyr下的细节。

使用 链接到标题

设备树配置 链接到标题

要启用 PSRAM,需要在设备树中按照 PSRAM 的大小配置 PSRAM 节点,8M 的 PSRAM 配置如下:

&psram0 {
	size = <DT_SIZE_M(8)>;
};

Zephyr 在 zephyr/dts/xtensa/espressif/esp32s3 下提供 ESP32S3 相关模块的 dtsi 文件,其中已经根据模块配置了 PSRAM 节点,文件名中 r 后面的数字表示 PSRAM 的大小:

esp32s3_mini_n4r2.dtsi  esp32s3_r2.dtsi         esp32s3_wroom_n16r2.dtsi  esp32s3_wroom_n4r8.dtsi
esp32s3_mini_n8.dtsi    esp32s3_r8.dtsi         esp32s3_wroom_n16r8.dtsi  esp32s3_wroom_n8.dtsi
esp32s3_pico_n8r2.dtsi  esp32s3_r8v.dtsi        esp32s3_wroom_n4.dtsi     esp32s3_wroom_n8r2.dtsi
esp32s3_fn8.dtsi     esp32s3_pico_n8r8.dtsi  esp32s3_wroom_n16.dtsi  esp32s3_wroom_n4r2.dtsi   esp32s3_wroom_n8r8.dts

用户可以根据使用的模块在板级 dts 中包含 dtsi 文件来配置 PSRAM

#include <espressif/esp32s3/esp32s3_wroom_n16r8.dtsi>

Kconfig配置 链接到标题

Kconfig 配置决定是否要启用 PSRAM,在 prj.conf 添加 CONFIG_ESP_SPIRAM=y 即可启用 PSRAM。 默认情况下 Zephyr 以 4 线 40M 的 PSRAM 模式工作。通过下面的配置项可以修改 PSRAM 工作模式:

  • 连线模式二选一

    • CONFIG_SPIRAM_MODE_QUAD=y:使用 4 线模式
    • CONFIG_SPIRAM_MODE_OCT=y:使用 8 线模式
  • 时钟二选一

    • CONFIG_SPIRAM_SPEED_40M=y:使用 40M 的时钟
    • CONFIG_SPIRAM_SPEED_80M=y:使用 80M 的时钟

ESP32S3 的 PSRAM 支持 ECC,开启后将会占用总量的 1/16 来存放 ECC

  • CONFIG_SPIRAM_ECC_ENABLE=y:开启 ECC 功能

使用PSRAM 链接到标题

配置 PSRAM 后,Zephyr 允许将以下内容放到 PSRAM 上:

  • Flash 上的指令
  • Flash 上的只读数据
  • 一些指定的 BSS
  • 专用的 heap(lvgl、mbedtls)
  • shared_multi_heap

但需要注意,data 段的数据以目前的 ld 文件来说是无法放到 PSRAM 上的,会直接放到片上 SRAM 内

通过以下三种方式进行使用

1. XIP Flash上指令和数据搬到PSRAM 链接到标题

Zephyr 支持将 XIP Flash 上的指令和数据搬到 PSRAM 上执行,以获得更高的执行速度:

  • CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y:将指令从 XIP Flash 搬运到 PSRAM
  • CONFIG_SPIRAM_RODATA=y:将只读数据从 XIP Flash 搬运到 PSRAM

2. 在ld脚本中指定 链接到标题

在 ld 脚本中指定,一些 bss 数据可以放到 PSRAM 上

#ifdef CONFIG_ESP32_WIFI_NET_ALLOC_SPIRAM
    *libdrivers__wifi.a:(.noinit .noinit.*)
    *libsubsys__net__l2__ethernet.a:(.noinit .noinit.*)
    *libsubsys__net__lib__config.a:(.noinit .noinit.*)
    *libsubsys__net__ip.a:(.noinit .noinit.*)
    *libsubsys__net.a:(.noinit .noinit.*)
#endif
    . = ALIGN(16);
    *(.ext_ram_noinit.*)
    . = ALIGN(16);
    _ext_ram_noinit_end = ABSOLUTE(.);

    _ext_ram_bss_start = ABSOLUTE(.);
    *(.ext_ram.bss*)
    . = ALIGN(16);

专用的 heap 可以放到 PSRAM 上,例如 lvgl 和 mbedtls 的 heap

KEEP(*(.lvgl_buf*))
. = ALIGN(16);
KEEP(*(.lvgl_heap*))
. = ALIGN(16);
KEEP(*(.mbedtls_heap*))
. = ALIGN(16);

3. 使用shared_multi_heap 链接到标题

链接脚本中指定 shared_multi_heap 的大小

 /* Used by Shared Multi Heap */
    _ext_ram_heap_start = ABSOLUTE(.);
    . += CONFIG_ESP_SPIRAM_HEAP_SIZE;
    . = ALIGN(16);
    _ext_ram_heap_end = ABSOLUTE(.);

启用 PSRAM 后,Zephyr 会为 ESP32S3 建立一个 shared_multi_heap,用户可以从该 heap 中分配到 PSRAM 的内存来使用

char *m_ext = shared_multi_heap_aligned_alloc(SMH_REG_ATTR_EXTERNAL, 32, BUF_SIZE);
memset(m_ext, 0, BUF_SIZE);
shared_multi_heap_free(SMH_REG_ATTR_EXTERNAL, m_ext);

PSRAM初始化过程 链接到标题

ESP32S3 在 __esp_platform_app_start 中执行 PSRAM 初始化过程,在 z_prep_c 进入 Zephyr 主程序前执行:

  	//初始化psram
	esp_init_psram();

  	//在psram上建立共享堆
	int err = esp_psram_smh_init();

	if (err) {
		printk("Failed to initialize PSRAM shared multi heap (%d)\n", err);
	}

esp_init_psram 实现在 zephyr/soc/espressif/common/esp_psram.c 中,

void esp_init_psram(void)
{
    //初始化psram
	if (esp_psram_init()) {
		ets_printf("Failed to Initialize external RAM, aborting.\n");
		return;
	}

    //检查psram大小是否符合配置
	if (esp_psram_get_size() < CONFIG_ESP_SPIRAM_SIZE) {
		ets_printf("External RAM size is less than configured.\n");
	}

    //检查psram是否通过内存测试
	if (IS_ENABLED(CONFIG_ESP_SPIRAM_MEMTEST)) {
		if (esp_psram_is_initialized()) {
			if (!esp_psram_extram_test()) {
				ets_printf("External RAM failed memory test!");
				return;
			}
		}
	}

    //初始化psram的bss
	memset(&_ext_ram_bss_start, 0,
	       (&_ext_ram_bss_end - &_ext_ram_bss_start) * sizeof(_ext_ram_bss_start));
}

esp_psram_init 实现在 modules/hal/espressif/components/esp_psram/esp_psram.c 中,主要任务是将 PSRAM 通过 MMU 映射到 ext_iram 和 ext_dram 内存空间,如果要在 PSRAM 运行指令和放置 rodata,就将 Flash 上的指令和 data 搬运到 PSRAM。

拷贝与映射实现说明 链接到标题

所有与 PSRAM 相关的配置项都放在 zephyr/soc/espressif/common/Kconfig.spiram 中进行配置,除了 ESP_SPIRAM_SIZE 从设备树中获取外,其他配置项都有默认值。

config ESP_SPIRAM_SIZE
	int "Size of SPIRAM part"
	default $(dt_node_int_prop_int,$(ESP32_PSRAM0_NODE_PATH),size) if $(dt_nodelabel_enabled,psram0)
	default 0
	help
	  Specify size of SPIRAM part.
	  NOTE: In ESP32, if SPIRAM size is greater than 4MB,
	  only lower 4MB can be allocated using k_malloc().

Zephyr 对 ESP32S3 的 PSRAM 配置初始化主要是通过 zephyr/soc/espressif/common/esp_psram.c 调用 modules/hal/espressif/components/esp_psram/esp_psram.c 中的函数实现。

Flash指令和只读数据不搬运到PSRAM的情况 链接到标题

Flash 指令和数据不搬运到 PSRAM 的情况:Zephyr 将 Flash 中的指令映射到 ibus 总线上,将 rodata 映射到 dbus 总线上,将 PSRAM 整体 8M 一起映射到 dbus 总线上,MMU 的映射关系如下图

alt text

在 ESP32S3 中采用简单的 MMU 管理方式,为了保持线性关系,不同 bus 之间会使用 dummy 占位。例如 ibus 映射了 text,dbus 就会跳过 text 的长度再进行 rodata 的映射,在 ld 文件中体现为

  /* This dummy section represents the .flash.text section but in default_rodata_seg.
   * Thus, it must have its alignment and (at least) its size.
   */
  .flash.rodata_dummy (NOLOAD):
  {
    _flash_rodata_dummy_start = ABSOLUTE(.);
    . += SIZEOF(.flash.text);
    . = ALIGN(CACHE_ALIGN);
  } GROUP_LINK_IN(RODATA_REGION)

    /* This section is required to skip flash rodata sections, because SPIRAM
   * and `drom0_0_seg` are on the same bus */
  .ext_ram.dummy (NOLOAD):
  {
    . = ADDR(.flash.rodata_end) - ORIGIN(ext_dram_seg);
    . = ALIGN (CACHE_ALIGN);
  } GROUP_LINK_IN(EXT_DRAM_REGION)

运行时可以看到打印,PSRAM 映射跳过 .ext_ram.dummy(0x1a0000),从 0x3c1a0000 开始映射全部 8M

I (193) esp_psram: 8bit-aligned-region: actual_mapped_len is 0x800000 bytes
I (199) esp_psram: 8bit-aligned-range: 0x800000 B, starting from: 0x3c1a0000
I (206) esp_psram: ext_bss_size is 2094400
I (210) esp_psram: ext_noinit_size is 0

Flash指令和只读数据搬运到PSRAM的情况 链接到标题

Flash 指令和数据搬运到 PSRAM 的情况:Zephyr 将 Flash 中的指令和只读数据分别拷贝到 PSRAM,再分别映射到 ibus 和 dbus 总线上,最后将 PSRAM 剩余部分映射到 dbus 总线上,拷贝和 MMU 的映射关系如下图 alt text

运行时可以看到打印,先拷贝指令并进行映射,再拷贝只读数据并进行映射,最后将 PSRAM 剩余部分 0x660000 映射到 dbus 总线上

I (224) mmu_psram: after copy instruction, page_id is 4
I (224) mmu_psram: Instructions copied and mapped to SPIRAM
I (224) esp_psram: after copy .text, used page is 4, start_page is 4, psram_available_size is 8126464 B
I (411) mmu_psram: after copy rodata, page_id is 26
I (411) mmu_psram: Read only data copied and mapped to SPIRAM
I (412) esp_psram: after copy .rodata, used page is 22, start_page is 26, psram_available_size is 6684672 B
I (420) esp_psram: 8bit-aligned-region: actual_mapped_len is 0x660000 bytes
I (426) esp_psram: 8bit-aligned-range: 0x660000 B, starting from: 0x3c1a0000
I (433) esp_psram: ext_bss_size is 2094400
I (437) esp_psram: ext_noinit_size is 0

代码解析 链接到标题

以上流程都在 modules/hal/espressif/components/esp_psram/esp_psram.c 中的 esp_psram_init 实现,下面的代码进行了简化,只显示主要流程

esp_err_t esp_psram_init(void)
{
	//启用psram
    ret = esp_psram_impl_enable(PSRAM_MODE);
    if (ret != ESP_OK) {
#if CONFIG_SPIRAM_IGNORE_NOTFOUND
        ESP_EARLY_LOGE(TAG, "PSRAM enabled but initialization failed. Bailing out.");
#endif
        return ret;
    }
    s_psram_ctx.is_initialised = true;

    uint32_t psram_physical_size = 0;
    ret = esp_psram_impl_get_physical_size(&psram_physical_size);
    assert(ret == ESP_OK);

    ESP_EARLY_LOGI(TAG, "Found %dMB PSRAM device", psram_physical_size / (1024 * 1024));
    ESP_EARLY_LOGI(TAG, "Speed: %dMHz", CONFIG_SPIRAM_SPEED);

	//获取有效size,如果开启了ECC,获取的大小就会按比例扣除ECC占用部分
    uint32_t psram_available_size = 0;
    ret = esp_psram_impl_get_available_size(&psram_available_size);
    assert(ret == ESP_OK);

    __attribute__((unused)) uint32_t total_available_size = psram_available_size;

    __attribute__((unused)) uint32_t start_page = 0;
#if CONFIG_SPIRAM_FETCH_INSTRUCTIONS || CONFIG_SPIRAM_RODATA
    uint32_t used_page = 0;
#endif

    //从flash上将指令拷贝到psram并进行映射到ibus总线上
#if CONFIG_SPIRAM_FETCH_INSTRUCTIONS
    ret = mmu_config_psram_text_segment(start_page, total_available_size, &used_page);
    if (ret != ESP_OK) {
        ESP_EARLY_LOGE(TAG, "No enough psram memory for instructon!");
        abort();
    }
    start_page += used_page;
    psram_available_size -= MMU_PAGE_TO_BYTES(used_page);
    ESP_EARLY_LOGI(TAG, "after copy .text, used page is %d, start_page is %d, psram_available_size is %d B", used_page, start_page, psram_available_size);
#endif  //#if CONFIG_SPIRAM_FETCH_INSTRUCTIONS

    //从flash上将只读数据拷贝到psram并进行映射到dbus总线上
#if CONFIG_SPIRAM_RODATA
    ret = mmu_config_psram_rodata_segment(start_page, total_available_size, &used_page);
    if (ret != ESP_OK) {
        ESP_EARLY_LOGE(TAG, "No enough psram memory for rodata!");
        abort();
    }
    start_page += used_page;
    psram_available_size -= MMU_PAGE_TO_BYTES(used_page);
    ESP_EARLY_LOGI(TAG, "after copy .rodata, used page is %d, start_page is %d, psram_available_size is %d B", used_page, start_page, psram_available_size);
#endif  //#if CONFIG_SPIRAM_RODATA

    //计算剩余的psram地址和大小
    size_t total_mapped_size = 0;
    size_t size_to_map = 0;
    size_t byte_aligned_size = 0;
    ret = esp_mmu_map_get_max_consecutive_free_block_size(MMU_MEM_CAP_READ | MMU_MEM_CAP_WRITE | MMU_MEM_CAP_8BIT | MMU_MEM_CAP_32BIT, MMU_TARGET_PSRAM0, &byte_aligned_size);
    assert(ret == ESP_OK);
    size_to_map = MIN(byte_aligned_size, psram_available_size);

    const void *v_start_8bit_aligned = NULL;
    ret = esp_mmu_map_reserve_block_with_caps(size_to_map, MMU_MEM_CAP_READ | MMU_MEM_CAP_WRITE | MMU_MEM_CAP_8BIT | MMU_MEM_CAP_32BIT, MMU_TARGET_PSRAM0, &v_start_8bit_aligned);
    assert(ret == ESP_OK);

	//将psram剩余部分映射到dbus总线上
    uint32_t actual_mapped_len = 0;
    mmu_hal_map_region(0, MMU_TARGET_PSRAM0, (intptr_t)v_start_8bit_aligned, MMU_PAGE_TO_BYTES(start_page), size_to_map, &actual_mapped_len);
    start_page += BYTES_TO_MMU_PAGE(actual_mapped_len);
    ESP_EARLY_LOGI(TAG, "8bit-aligned-region: actual_mapped_len is 0x%x bytes", actual_mapped_len);

    cache_bus_mask_t bus_mask = cache_ll_l1_get_bus(0, (uint32_t)v_start_8bit_aligned, actual_mapped_len);
    cache_ll_l1_enable_bus(0, bus_mask);

	//之后只是将相关信息进行保存

    s_psram_ctx.mapped_regions[PSRAM_MEM_8BIT_ALIGNED].size = size_to_map;
    s_psram_ctx.mapped_regions[PSRAM_MEM_8BIT_ALIGNED].vaddr_start = (intptr_t)v_start_8bit_aligned;
    s_psram_ctx.mapped_regions[PSRAM_MEM_8BIT_ALIGNED].vaddr_end = (intptr_t)v_start_8bit_aligned + size_to_map;
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].size = size_to_map;
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].vaddr_start = (intptr_t)v_start_8bit_aligned;
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].vaddr_end = (intptr_t)v_start_8bit_aligned + size_to_map;
    ESP_EARLY_LOGI(TAG, "8bit-aligned-range: 0x%x B, starting from: 0x%x", s_psram_ctx.mapped_regions[PSRAM_MEM_8BIT_ALIGNED].size, v_start_8bit_aligned);
    total_mapped_size += size_to_map;


    if (total_mapped_size < psram_available_size) {
        ESP_EARLY_LOGW(TAG, "Virtual address not enough for PSRAM, map as much as we can. %dMB is mapped", total_mapped_size / 1024 / 1024);
    }

    //------------------------------------Configure .bss in PSRAM-------------------------------------//
#if CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
    //should never be negative number
    uint32_t ext_bss_size = ((intptr_t)&_ext_ram_bss_end - (intptr_t)&_ext_ram_bss_start);
    ESP_EARLY_LOGI(TAG, "ext_bss_size is %d", ext_bss_size);
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].vaddr_start += ext_bss_size;
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].size -= ext_bss_size;
#endif  //#if CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY

#if CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY
    uint32_t ext_noinit_size = ((intptr_t)&_ext_ram_noinit_end - (intptr_t)&_ext_ram_noinit_start);
    ESP_EARLY_LOGI(TAG, "ext_noinit_size is %d", ext_noinit_size);
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].vaddr_start += ext_noinit_size;
    s_psram_ctx.regions_to_heap[PSRAM_MEM_8BIT_ALIGNED].size -= ext_noinit_size;
#endif

    return ESP_OK;
}

堆的实现说明 链接到标题

特定的堆和buffer 链接到标题

特定的堆通过 ld 文件将要使用的内存分配到 PSRAM 中,以 lvgl 为例,ld 文件如下:

KEEP(*(.lvgl_buf*))
. = ALIGN(16);
KEEP(*(.lvgl_heap*))
. = ALIGN(16);

zephyr/modules/lvgl/lvgl_mem.c 中,通过段属性将 lvgl 的堆初始化到 PSRAM 中:

#define HEAP_MEM_ATTRIBUTES Z_GENERIC_SECTION(.lvgl_heap) __aligned(8)
static char lvgl_heap_mem[CONFIG_LV_Z_MEM_POOL_SIZE] HEAP_MEM_ATTRIBUTES;

zephyr/modules/lvgl/lvgl.c 中,通过段属性将 lvgl 的 buffer 初始化到 PSRAM 中:

#define LV_BUF_SECTION	Z_GENERIC_SECTION(.lvgl_buf)

#define LV_BUFFERS_DEFINE(n)									\
	static DISPLAY_BUFFER_ALIGN(LV_DRAW_BUF_ALIGN) uint8_t buf0_##n[BUFFER_SIZE(n)]		\
	LV_BUF_SECTION __aligned(CONFIG_LV_Z_VDB_ALIGN);					\
												\
	IF_ENABLED(CONFIG_LV_Z_DOUBLE_VDB, (							\
	static DISPLAY_BUFFER_ALIGN(LV_DRAW_BUF_ALIGN) uint8_t buf1_##n[BUFFER_SIZE(n)]		\
	LV_BUF_SECTION __aligned(CONFIG_LV_Z_VDB_ALIGN);					\
	))											\
												\
	IF_ENABLED(ALLOC_MONOCHROME_CONV_BUFFER, (						\
	static uint8_t mono_vtile_buf_##n[BUFFER_SIZE(n)]					\
	LV_BUF_SECTION __aligned(CONFIG_LV_Z_VDB_ALIGN);					\
	))

共享的堆和buffer 链接到标题

zephyr/soc/espressif/common/esp_psram.c 中通过 esp_psram_smh_init 初始化属性为 SMH_REG_ATTR_EXTERNAL 的共享堆,之后就可以通过 SMH_REG_ATTR_EXTERNAL 属性进行共享堆的分配和释放。

struct shared_multi_heap_region smh_psram = {
	.addr = (uintptr_t)&_ext_ram_heap_start,
	.size = CONFIG_ESP_SPIRAM_HEAP_SIZE,
	.attr = SMH_REG_ATTR_EXTERNAL,
};

int esp_psram_smh_init(void)
{
	shared_multi_heap_pool_init();
	return shared_multi_heap_add(&smh_psram, NULL);
}

共享堆的原理可以参考 Zephyr 内存管理之共享 Heap

参考 链接到标题

https://lgl88911.pages.dev/zephyr/zephyr%E4%B8%8Besp32s3%E9%95%9C%E5%83%8F%E5%8A%A0%E8%BD%BD%E7%BB%86%E8%8A%82/