ESP32 S3 存储布局:Zephyr 设备树与链接脚本的协作指南

理解存储布局对优化 Zephyr 内存有重要意义,本文将详细介绍 ESP32 S3 的存储布局,以及 Zephyr 上设备树与链接脚本是如何协作体现这一布局的,并优化调整Zephyr在esp32 s3上可用的SRAM大小。

ESP32 S3 地址空间 & 存储简介 链接到标题

ESP32 S3 系统由两个哈佛结构 Xtensa® LX7 CPU 构成,这两个 CPU 能够访问的地址空间范围完全一致。

地址空间 链接到标题

ESP32 S3 有 4GB 的地址空间,地址 0x4000_0000 以下的部分属于数据总线的地址范围,地址 0x4000_0000 ~ 0x4FFF_FFFF 部分为指令总线的地址范围,地址 0x5000_0000 及以上的部分是数据总线与指令总线共用的地址范围。

下面是系统结构与地址映射结构(图片来源:esp32 s3 技术参考手册)

esp32_map

内部存储 链接到标题

ESP32 S3 有 ROM/SRAM/RTC memory 三种内部存储。

内部 ROM 链接到标题

内部 ROM 有 384K,是只读存储器,不可编程。内部 ROM 中存放一些系统底层软件的 ROM 代码(程序指令和一些只读数据)。 地址空间的映射关系是:

  • ROM0 256K: 只映射到一个地址空间上,只能存储指令
    • IBUS: 0x4000_0000~0x4003_FFFF
  • ROM1 128K: 被映射到两个地址空间上,既能存储指令也能存储数据
    • DBUS:0x3FF0_0000~0x3FF1_FFFF
    • IBUS:0x4004_0000~0x4005_FFFF

ROM 区域出厂后内容不会变,因此一般情况下并不关注它。

内部 SRAM 链接到标题

内部 SRAM 有 512K,可读写。可以快速响应 CPU 的访问请求(通常一个 CPU 时钟周期) 地址空间的映射关系是:

  • SRAM0 32K: 映射到一个地址空间,只能存储指令。通过配置,这部分存储器中的 16 KB、或全部 32 KB 可以被 ICache 占用,被 ICache 占用后 CPU 无法访问
    • IBUS:0x4037_0000-0x4037_7FFF
  • SRAM1 416K: 被映射到两个地址空间上,既能存储指令也能存储数据
    • DBUS:0x3FC8_8000~0x3FCE_FFFF
    • IBUS:0x4037_8000~0x403D_FFFF
  • SRAM2 64K: 映射到一个地址空间,只能存储数据。通过配置,这部分存储器中的 32 KB、或全部 64 KB 可以被 DCache 占用,被 DCache 占用后 CPU 无法访问
    • DBUS:0x3FCF_0000~0x3FCF_FFFF

esp32s3_sram

从连续性上来说,数据空间可以连续访问 489K(SRAM1+SRAM2),指令空间可以连续访问 448K(SRAM0+SRAM1), 需要注意的是两个地址空间在映射的实际物理 SRAM 有重叠,这点在后面 Zephyr 链接脚本上的写法是有注意点的。

另外 SRAM 中的一部分可以被配置为外部存储器的 cache,在这种情况下就无法被 CPU 访问。

RTC memory 链接到标题

RTC 存储器总计 16K,以 SRAM 方式实现,因此也是易失性存储器。但是,在 deep sleep 模式下,存放在 RTC 存储器中的数据不会丢失。

  • RTC FAST Memory 8 KB:只可以被 CPU 访问,不可以被协处理器访问,存放在 Deep Sleep 模式下需保持的程序指令和数据。
    • I/DBUS: 0x600F_E000~0x600F_FFFF
  • RTC SLOW Memory 8 KB:既可以被 CPU 访问,又可以被协处理器访问,存放 CPU 和协处理器需要共享的程序指令和数据。
    • I/DBUS: 0x5000_0000~0x5000_1FFF

外部存储 链接到标题

ESP32-S3 支持以 SPI、Dual SPI、Quad SPI、Octal SPI、QPI、OPI 等接口形式连接 flash 和片外 RAM。CPU 借助高速缓存 (Cache) 来访问外部存储器。Cache 根据 MMU 中的信息把 CPU 指令总线或数据总线的地址变换为访问片外 flash 与片外 RAM 的实地址。经过变换的实地址所组成的实地址空间最大支持 1 GB 的片外 flash 与 1 GB 的片外 RAM。但同一时间最多只能支持 32M 的指令地址空间和数字地址空间。

  • IBUS 32M: 以 64 KB 为单位映射到片外 flash 或片外 RAM ,4 字节对齐的读访问或取指访问。
    • 0x4200_0000~0x43FF_FFFF
  • DBUS 32M: 以 64 KB 为单位映射到片外 RAM,支持单字节、双字节、4 字节、16 字节的读写访问。
    • 0x3C00_0000~0x3DFF_FFFF

Zephyr 上 ESP32 S3 设备树对存储布局的体现 链接到标题

ESP32 S3 的存储在 Zephyr 上是通过设备树来体现的。其在设备树中绑定的地址定义了 ESP32 S3 的存储布局,包括内部 SRAM、外部存储等。

内存存储 链接到标题

在设备树中,内存存储被定义为memory节点。该节点包含了 ESP32 S3 的所有内存存储信息,包括内部 SRAM、外部存储等。

内部 ROM 链接到标题

前面我们说明过出厂后内部 ROM 不会再变化,一般情况下也不会去读内部 ROM,所以在设备树中内部 ROM 的定义是不需要的。

内部 SRAM 链接到标题

zephyr/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi中定义

SRAM0 从 0x40370000 开始,大小为 32KB。

sram0: memory@40370000 {
			compatible = "zephyr,memory-region", "mmio-sram";
			reg = <0x40370000 DT_SIZE_K(32)>;
			zephyr,memory-region = "SRAM0";
		};

SRAM1 从 0x3fc88000 开始,大小为 416KB。

sram1: memory@3fc88000 {
			compatible = "zephyr,memory-region", "mmio-sram";
			reg = <0x3fc88000 DT_SIZE_K(416)>;
			zephyr,memory-region = "SRAM1";
		};

SRAM2 从 0x3fcf0000 开始,大小为 64KB。

sram2: memory@3fcf0000 {
			compatible = "zephyr,memory-region", "mmio-sram";
			reg = <0x3fcf0000 DT_SIZE_K(64)>;
			zephyr,memory-region = "SRAM2";
		};

RTC memory 链接到标题

目前的设备树并没有专门对 RTC 的 memory 进行描述,而是直接在链接文件中写死的,这部分后面说明

外部存储 链接到标题

zephyr/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi中原生定义的外部存储的地址空间分别挂在 icache0 和 dchache0 上。psram0 作为 dcache0 的子节点,用于描述 psram 的地址空间,表示大小的 size 属性并没有指定

icache0: memory@42000000 {
    compatible = "zephyr,memory-region";
    reg = <0x42000000 DT_SIZE_M(32)>;
    zephyr,memory-region = "ICACHE0";
};

dcache0: memory@3c000000 {
    compatible = "zephyr,memory-region";
    reg = <0x3c000000 DT_SIZE_M(32)>;
    zephyr,memory-region = "DCACHE0";

    psram0: psram0 {
        compatible = "espressif,esp32-psram";
        size = <0x0>;
    };
};

在确定的硬件下进行 psram 的大小配置,zephyr/dts/xtensa/espressif/esp32s3/esp32s3_wroom_n16r8.dtsi

/* 8MB psram */
&psram0 {
	size = <DT_SIZE_M(8)>;
};

Zephyr 上链接脚本对存储布局的体现 链接到标题

zephyr/soc/espressif/esp32s3/default.ld,用于指导 eps32s3 image 的最终链接情况。其中的 MEMORY 完整的描述了存储大小和存储布局,下面是一个简化的版本

MEMORY
{
  FLASH (R):       org = 0x0, len = FLASH_SIZE - 0x100

  iram0_0_seg(RX): org = procpu_iram_org, len = procpu_iram_len
  dram0_0_seg(RW): org = procpu_dram_org, len = procpu_dram_len

  irom0_0_seg(RX): org = procpu_irom_org, len = procpu_irom_len
  drom0_0_seg(R):  org = procpu_drom_org, len = procpu_drom_len

#if defined(CONFIG_ESP_SPIRAM)
  ext_dram_seg(RW): org = procpu_ext_dram_org, len = procpu_ext_dram_len
  ext_iram_seg(RX): org = procpu_ext_iram_org, len = procpu_ext_iram_len
#endif

  rtc_iram_seg(RWX): org = 0x600fe000, len = 0x2000 - CONFIG_RESERVE_RTC_MEM
  rtc_slow_seg(RW): org = 0x50000000, len = 0x2000

#ifdef CONFIG_GEN_ISR_TABLES
  IDT_LIST(RW): org = 0x3ebfe010, len = 0x2000
#endif
}

MEMORY 中对各 section 的 org 和 len 的具体值,在default.ldzephyr/soc/espressif/esp32s3/memory.h中定义,一些数据是从设备树中读取的节点地址,例如

procpu_iram_org = SRAM_USER_IRAM_START;
#define SRAM_USER_IRAM_START (SRAM0_IRAM_START + CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE)
#define SRAM0_IRAM_START     DT_REG_ADDR(DT_NODELABEL(sram0))

这里不展开分析,只给出最后生成的链接文件linker.cmd

procpu_iram_end = ((0x3fcd7e00 - 0x0) + 0x6f0000) - (0 + 0);
procpu_dram_end = (0x3fcd7e00 - 0x0) - (0 + 0);
procpu_iram_org = ((1077346304) + 0x4000);
procpu_iram_len = procpu_iram_end - procpu_iram_org;
procpu_dram_org = (1070104576);
procpu_dram_len = procpu_dram_end - procpu_dram_org;
procpu_irom_end = (1107296256) + (33554432) - (0 + 0);
procpu_drom_end = (1006632960) + (33554432) - (0 + 0);
procpu_irom_org = (1107296256);
procpu_irom_len = (33554432) - (0 + 0);
procpu_drom_org = (1006632960);
procpu_drom_len = (33554432) - (0 + 0);
procpu_ext_dram_org = procpu_drom_org;
procpu_ext_dram_len = 8388608;
procpu_ext_iram_org = procpu_irom_org;
procpu_ext_iram_len = 8388608;
MEMORY
{
  FLASH (R): org = 0x0, len = 16777216 - 0x100
  iram0_0_seg(RX): org = procpu_iram_org, len = procpu_iram_len
  dram0_0_seg(RW): org = procpu_dram_org, len = procpu_dram_len
  irom0_0_seg(RX): org = procpu_irom_org, len = procpu_irom_len
  drom0_0_seg(R): org = procpu_drom_org, len = procpu_drom_len
  ext_dram_seg(RW): org = procpu_ext_dram_org, len = procpu_ext_dram_len
  ext_iram_seg(RX): org = procpu_ext_iram_org, len = procpu_ext_iram_len
  rtc_iram_seg(RWX): org = 0x600fe000, len = 0x2000 - 0
  rtc_slow_seg(RW): org = 0x50000000, len = 0x2000
  IDT_LIST(RW): org = 0x3ebfe010, len = 0x2000
}

将值替换后就可以得到

  FLASH (R): org = 0x0, len = 0xFFFF00
  iram0_0_seg(RX): org = 0x40370000 + 0x4000, len = 0x53E00
  dram0_0_seg(RW): org = 0x3FC88000, len = 0x4FE00
  irom0_0_seg(RX): org = 0x42000000, len = 0x2000000
  drom0_0_seg(R): org = 0x3C000000, len = 0x2000000
  ext_dram_seg(RW): org = 0x3C000000, len = 0x800000
  ext_iram_seg(RX): org = 0x42000000, len = 0x800000
  rtc_iram_seg(RWX): org = 0x600fe000, len = 0x2000 - 0
  rtc_slow_seg(RW): org = 0x50000000, len = 0x2000
  IDT_LIST(RW): org = 0x3ebfe010, len = 0x2000

对于 FLASH 来说是在链接脚本中用来表明有 FLASH 这个 section,并不涉及到链接的定址,但在链接的时候会参考其大小,避免所放内容超过 FLASH 的大小。实际的执行会在 boot 阶段安装 FLASH 最后 0x100 放的 header,建立 MMU map。

ESP32S3 的大部分指令都是放在 FLASH 上运行,text 会被映射到 irom0_0_seg,rodata 会被映射到 drom0_0_seg,data/bbs 会被搬运到 dram0_0_seg。 要求速度和操作 Flash 的代码不能在 FLASH 上运行,需要搬运到 iram0_0_seg 中运行。

值得注意的是 iram0_0_seg 和 dram0_0_seg 映射到 SRAM 上是有重叠的

esp32s3_zephyr.png

因此在链接的时候需要将指令和数据错开,在default.ld中 会在 dram0_0_seg 的开始插入.dram0.dummy这个 section,将.iram0.text占用的空间跳过

  .dram0.dummy (NOLOAD):
  {
    /* Spacer section is required to skip .iram0.text area because
     * iram0_0_seg and dram0_0_seg reflect the same address space on different buses.
     */
    . = ORIGIN(dram0_0_seg) + (MAX(_iram_end, SRAM1_IRAM_START) - SRAM1_IRAM_START);
    . = ALIGN(16);
  } GROUP_LINK_IN(RAMABLE_REGION)

这意味者 dram0_0_seg 的使用量中含有 iram0_0_seq 的使用量

由于 irom0_0_seg 和 drom0_0_seg 映射到相同的 FLASH 区域,因此在链接的时候需要插入。flash.rodata_dummy 将。flash.text 跳过,再放 rodata 避免和 text sections 重叠

  /* 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)

这意味者 drom0_0_seg 的使用量中含有 drom0_0_seg 的使用量

当启用外部 SRAM 时,外部 RAM 也会被映射到 ext_iram_seg 和 ext_dram_seg,和 FLASH 映射的 irom0_0_seg 和 drom0_0_seg 相同,为了不和 FLASH 重叠,会在 ext_dram_seg 的开头插入。ext_ram.dummy 这个 section,将 flash rodata sections 跳过

  /* 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)

这意味者 ext_dram_seg 的使用量中含有 irom0_0_seg 的使用量

最后 Zephyr esp32s3 image 时会提示每个 section 的大小

Memory region         Used Size  Region Size  %age Used
           FLASH:      699016 B   16776960 B      4.17%
     iram0_0_seg:       60952 B     343552 B     17.74%
     dram0_0_seg:      319784 B     327168 B     97.74%
     irom0_0_seg:      294702 B        32 MB      0.88%
     drom0_0_seg:      567944 B        32 MB      1.69%
    rtc_iram_seg:          0 GB         8 KB      0.00%
    rtc_slow_seg:          0 GB         8 KB      0.00%
        IDT_LIST:          0 GB         8 KB      0.00%
  • FLASH 总计用 699016 字节,映射加载完毕后:
    • 实际加载到 iram0 中的为部分 text:60952
    • 实际加载到在 dram0 中的为 data+bss: 258832(319784-60952)
    • 实际在映射到 irom0 中的为部分 text: 294702
    • 实际在映射到 drom0 中的为 rodata: 273242(567944−294702)

从上面来看 Zephyr 可用 sram 的最大空间为 343552, 与硬件的 512K 不符合。

RTC的内存空间直接在default.ld中设定, 如果从硬件的定义来说,这两者写到设备树中,再由链接脚本读取,会更符合硬件的定义。

  rtc_iram_seg(RWX): org = 0x600fe000, len = 0x2000 - 0
  rtc_slow_seg(RW): org = 0x50000000, len = 0x2000

外篇:Zephyr 下可用的 SRAM 空间 链接到标题

前面提到了链接脚本体现出来的可用SRAM和实际硬件有差距,这里我们探寻差距的来源。

下面是从 memory.h 和配置文件

#define SRAM0_IRAM_START     DT_REG_ADDR(DT_NODELABEL(sram0))
#define SRAM0_SIZE           DT_REG_SIZE(DT_NODELABEL(sram0))
#define SRAM1_DRAM_START     DT_REG_ADDR(DT_NODELABEL(sram1))
#define SRAM1_IRAM_START     (SRAM0_IRAM_START + SRAM0_SIZE)
#define SRAM_USER_IRAM_START (SRAM0_IRAM_START + CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE)

#define SRAM2_DRAM_START      DT_REG_ADDR(DT_NODELABEL(sram2))
#define SRAM2_SIZE            DT_REG_SIZE(DT_NODELABEL(sram2))
#define SRAM2_USER_DRAM_START (SRAM2_DRAM_START + CONFIG_ESP32S3_DATA_CACHE_SIZE)
#define SRAM2_USER_DRAM_SIZE  (SRAM2_SIZE - CONFIG_ESP32S3_DATA_CACHE_SIZE)

配置信息

build/zephyr/.config:CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE=0x4000
build/zephyr/.config:CONFIG_ESP32S3_DATA_CACHE_SIZE=0x8000

可以中可以看到 SRAM0 开头有 16K 用来存放指令缓存,而 SRAM2 开头 32K 用来做数据缓存。因此可以用的连续空间是 SRAM0 剩余的 16K 和 SRAM1 的 412K,总计 428K, 也和 343552 不符合。这是因为 Zephyr esp32s3 的链接文件来源于 esp-idf,这部分并没有做优化,将 0x3fcd7e00 做为了可用 SRAM 的结束地址

/** Simplified memory map for the bootloader.
 *  Make sure the bootloader can load into main memory without overwriting itself.
 *
 *  ESP32-S3 ROM static data usage is as follows:
 *  - 0x3fcd7e00 - 0x3fce9704: Shared buffers, used in UART/USB/SPI download mode only
 *  - 0x3fce9710 - 0x3fceb710: PRO CPU stack, can be reclaimed as heap after RTOS startup
 *  - 0x3fceb710 - 0x3fced710: APP CPU stack, can be reclaimed as heap after RTOS startup
 *  - 0x3fced710 - 0x3fcf0000: ROM .bss and .data (not easily reclaimable)
 *
 *  The 2nd stage bootloader can take space up to the end of ROM shared
 *  buffers area (0x3fce9704). For alignment purpose we shall use value (0x3fce9700).
 */

/* The offset between Dbus and Ibus.
 * Used to convert between 0x403xxxxx and 0x3fcxxxxx addresses.
 */
#define IRAM_DRAM_OFFSET         0x6f0000
#define DRAM_BUFFERS_START       0x3fcd7e00
#define DRAM_BUFFERS_END         0x3fce9704
#define DRAM_PROCPU_STACK_START  0x3fce9710
#define DRAM_STACK_START DRAM_PROCPU_STACK_START
#define DRAM_APPCPU_STACK_START  0x3fceb710
#define DRAM_ROM_BSS_DATA_START  0x3fcf0000

其实 shared buffer/pro cpu stack/app cpu stack 在 zephyr 启动后完全不会在用,因此可以将可用 SRAM 的节结尾延长到 sram2 的开头也就是 0x3fcf0000,由于从 sram2 的开头是 32K dcache,因此 sram2 剩余的 32K 无法被连续使用。将上面修改为

#define IRAM_DRAM_OFFSET         0x6f0000
#define DRAM_BUFFERS_START       0x3fcf0000
#define DRAM_BUFFERS_END         0x3fcf0000
#define DRAM_PROCPU_STACK_START  0x3fcf0000
#define DRAM_STACK_START DRAM_PROCPU_STACK_START
#define DRAM_APPCPU_STACK_START  0x3fcf0000
#define DRAM_ROM_BSS_DATA_START  0x3fcf0000

重编译可以得到如下,内部 SRAM 可用多接近 100K

Memory region         Used Size  Region Size  %age Used
           FLASH:      699016 B   16776960 B      4.17%
     iram0_0_seg:       60952 B       432 KB     13.78%
     dram0_0_seg:      319784 B       416 KB     75.07%
     irom0_0_seg:      294702 B        32 MB      0.88%
     drom0_0_seg:      567944 B        32 MB      1.69%
    rtc_iram_seg:          0 GB         8 KB      0.00%
    rtc_slow_seg:          0 GB         8 KB      0.00%
        IDT_LIST:          0 GB         8 KB      0.00%

如果内存紧张,在 sram2 后还有 32K,可以修改链接文件将这 32K 也利用起来,只是在物理地址上无法和前面的 SRAM 连续。

参考 链接到标题

https://www.espressif.com/sites/default/files/documentation/esp32-s3_technical_reference_manual_cn.pdf

https://github.com/espressif/esp-idf/blob/2044fba6e71422446986f9ae0909b1ab67e57815/components/bootloader/subproject/main/ld/esp32s3/bootloader.ld#L10