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 搬运到 PSRAMCONFIG_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 的映射关系如下图

在 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 的映射关系如下图

运行时可以看到打印,先拷贝指令并进行映射,再拷贝只读数据并进行映射,最后将 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