Zephyr 下 ESP32S3 镜像加载细节
从 Zephyr 3.6 开始,ESP32 系列的 SoC 在 Zephyr 下不开启 MCUBoot 的情况下,直接使用 ESP32 片上 ROM 引导 Zephyr 的镜像。本文将以 ESP32S3 为例,介绍 Zephyr 下 ESP32S3 镜像加载细节。
镜像生成 链接到标题
Zephyr 下 ESP32S3 构建会根据 zephyr/soc/espressif/esp32s3/default.ld 生成 ELF 文件,ELF 文件中包含各个 section 的数据,从 ELF 中可以获取到各个段的加载地址 (PhysAddr) 和运行地址 (VirtAddr)。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x0001b4 0x40374000 0x00000000 0x0d5a0 0x0d5a0 R E 0x4
LOAD 0x00d758 0x3fc915b0 0x0000d5a0 0x0218c 0x0218c RW 0x8
LOAD 0x000000 0x403815a0 0x0000d5a0 0x00000 0x00010 RW 0x1
LOAD 0x00f8e4 0x3fc9373c 0x0000f72c 0x00494 0x00494 RW 0x4
LOAD 0x000000 0x0000fbc0 0x0000fbc0 0x00000 0x00440 RW 0x1
LOAD 0x000000 0x3fc93bd0 0x0000fbc0 0x00000 0x2d420 RW 0x10
LOAD 0x000000 0x3fcc0ff0 0x0000fbc0 0x00000 0x017d0 RW 0x10
LOAD 0x00fd80 0x42000000 0x00010000 0x3fbfa 0x3fbfa R E 0x10
LOAD 0x050000 0x3c040000 0x00050000 0x156ac8 0x156ac8 RW 0x10000
LOAD 0x000000 0x3c000000 0x3c000000 0x00000 0x40000 RW 0x1
LOAD 0x000000 0x3c000000 0x3c000000 0x00000 0x49f540 RW 0x8
LOAD 0x000000 0x3fc88000 0x3fc88000 0x00000 0x095b0 RW 0x1
Section to Segment mapping:
Segment Sections...
00 .iram0.vectors .iram0.text .loader.text
01 .dram0.data
02 .iram0.text_end
03 .loader.data sw_isr_table device_states log_mpsc_pbuf_area log_msg_ptr_area log_dynamic_area k_heap_area k_mutex_area k_msgq_area k_sem_area log_const_area log_backend_area
04 .flash.text_dummy
05 .dram0.noinit
06 .dram0.bss
07 .flash.text
08 .flash.rodata initlevel device_area _static_thread_data_area gpio_driver_api_area i2c_driver_api_area spi_driver_api_area clock_control_driver_api_area display_driver_api_area mipi_dbi_driver_api_area regulator_driver_api_area uart_driver_api_area input_callback_area shell_area shell_root_cmds_area shell_subcmds_area shell_dynamic_subcmds_area
09 .flash.rodata_dummy
10 .flash.rodata_dummy .ext_ram.dummy .ext_ram.data
11 .dram0.dummy
在构建的最后阶段zephyr/soc/espressif/common/CMakeLists.txt会指定把 zephyr.elf 转换成 ESP32S3 片上 ROM 可引导的 zephyr.bin
if((CONFIG_ESP_SIMPLE_BOOT OR CONFIG_MCUBOOT) AND NOT CONFIG_SOC_ESP32C6_LPCORE)
if(CONFIG_BUILD_OUTPUT_BIN)
set(ELF2IMAGE_ARG "")
if(NOT CONFIG_MCUBOOT)
set(ELF2IMAGE_ARG "--ram-only-header")
endif()
set_property(GLOBAL APPEND PROPERTY extra_post_build_commands
COMMAND esptool
ARGS --chip ${CONFIG_SOC} elf2image ${ELF2IMAGE_ARG}
--flash-mode dio
--flash-freq ${CONFIG_ESPTOOLPY_FLASHFREQ}
--flash-size ${esptoolpy_flashsize}MB
-o ${CMAKE_BINARY_DIR}/zephyr/${KERNEL_BIN_NAME}
${CMAKE_BINARY_DIR}/zephyr/${KERNEL_ELF_NAME})
endif()
endif()
实际执行的命令就是
esptool --chip esp32s3 elf2image \
--ram-only-header \
--flash-mode dio \
--flash-freq 80m \
--flash-size 16MB \
-o zephyr.bin \
zephyr.elf
该命令转换出来的 zephyr.bin 格式,由一个 Header、一个扩展 Header、多个 Segment 和一个 Footer 组成。多字节的字段是小端构成。

Header 链接到标题

--ram-only-header 表示这个镜像的 header 按“RAM 运行镜像”来生成,代码运行地址是 RAM(IRAM/DRAM),因此 header 内会有 IRAM 和 DRAM 两个段的计数,也就是 Number of Segment 为 2。
esptool 会根据命令行参数 --flash-mode dio 选项设置 Flash mode 字段,根据 --flash-freq 80m --flash-size 16MB 设置 Flash size/freq 字段。
esptool 从 zephyr.elf 中解析出 .entry_addr section,将该 section 中存放的 _entry_point 放到 header 内。_entry_point 在 zephyr/soc/espressif/common/loader.c 中定义。
#define HDR_ATTR __attribute__((section(".entry_addr"))) __attribute__((used))
void __start(void);
static HDR_ATTR void (*_entry_point)(void) = &__start;
可见入口地址保存的是 zephyr/soc/espressif/common/loader.c 内的 __start 函数地址。
扩展 Header 链接到标题
根据 --chip esp32s3 设置 CHIP_ID 为 9,其它都是默认值或者计算出来的值,最后一个 hash 标记位用于安全启动,如果为 01,footer 最后就带有一个 SHA256。
WP pin: 0xee (disabled)
Flash pins drive settings: clk_drv: 0x0, q_drv: 0x0, d_drv: 0x0, cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0
Chip ID: 9 (ESP32-S3)
Minimal chip revision: v0.0, (legacy min_rev = 0)
Maximal chip revision: v655.35
Segment 链接到标题

Memory offset 是该段运行时的地址,esptool 会从 zephyr.elf 中读取。 Segment size 是段的大小,esptool 会从 zephyr.elf 中读取。 Data 就是 ELF 实际的数据,直接 copy 过去。
生成 Segment 只会将有实际内容的 ELF Segment 复制到 zephyr.bin 中,前面 Program Headers 中有下面的 Section 会被复制
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
00 .iram0.vectors .iram0.text .loader.text
LOAD 0x0001b4 0x40374000 0x00000000 0x0d5a0 0x0d5a0 R E 0x4
01 .dram0.data
LOAD 0x00d758 0x3fc915b0 0x0000d5a0 0x0218c 0x0218c RW 0x8
03 .loader.data sw_isr_table device_states log_mpsc_pbuf_area log_msg_ptr_area log_dynamic_area k_heap_area k_mutex_area k_msgq_area k_sem_area log_const_area log_backend_area
LOAD 0x00f8e4 0x3fc9373c 0x0000f72c 0x00494 0x00494 RW 0x4
07 .flash.text
LOAD 0x00fd80 0x42000000 0x00010000 0x3fbfa 0x3fbfa R E 0x10
08 .flash.rodata initlevel device_area _static_thread_data_area gpio_driver_api_area i2c_driver_api_area spi_driver_api_area clock_control_driver_api_area display_driver_api_area mipi_dbi_driver_api_area regulator_driver_api_area uart_driver_api_area input_callback_area shell_area shell_root_cmds_area shell_subcmds_area shell_dynamic_subcmds_area
LOAD 0x050000 0x3c040000 0x00050000 0x156ac8 0x156ac8 RW 0x10000
如果段在运行地址上相邻,会将它们合并为一个 segment。Segment 会按实际的段进行生成,其数量和 Header 里面描述的 Segment number 不一定一样。
下图就是前面示例生成的镜像结构:

Footer 链接到标题
整个镜像大小以 16 字节对齐,Footer 就是对齐填充,最后一个字节是所有数据段的校验和,其余字节填充 0。如果扩展 Header 中的 hash appended 是 0x01,则校验和之后会追加一个 SHA256。
启动镜像 链接到标题
zephyr.bin 烧录在 ESP32S3 的外部 Flash 上,启动 zephyr.bin 分为两个阶段:
- 由 ESP32S3 片上 ROM 进行引导
- 由 zephyr.bin 自己的 loader 进行启动
ROM 引导 链接到标题
ROM 会读取 Header 和扩展 Header,根据 Header 中的 Flash 信息,对 Flash 进行初始化,让 Flash 可读。接着 ROM 将 segment(00) 从 Flash 中拷贝到 DRAM 中,将 segment(01+03) 从 Flash 中拷贝到 IRAM 中,然后读出 header 中的 entry point,并跳转执行,也就是从 _start() 开始执行。

自启动 链接到标题
从 ROM 跳到 zephyr/soc/espressif/common/loader.c 的 __start 运行,主要的流程如下:
void __start(void)
{
extern uint32_t _init_start;
/* Move the exception vector table to IRAM. */
//重置中断向量表到 zephyr/soc/espressif/esp32s3/default.ld .iram0.vectors _init_start
__asm__ __volatile__("wsr %0, vecbase" : : "r"(&_init_start));
//将 .dram0.bss 清 0
//zephyr/arch/common/init.c
arch_bss_zero();
//内存屏障,确保清 0 bss 发生在禁止中断前
__asm__ __volatile__("" : : "g"(&__bss_start) : "memory");
/* Disable normal interrupts. */
__asm__ __volatile__("wsr %0, PS" : : "r"(PS_INTLEVEL(XCHAL_EXCM_LEVEL) | PS_UM | PS_WOE));
/* Initialize the architecture CPU pointer. Some of the
* initialization code wants a valid arch_current_thread() before
* arch_kernel_init() is invoked.
*/
__asm__ __volatile__("wsr.MISC0 %0; rsync" : : "r"(&_kernel.cpus[0]));
/* Initialize hardware only during 1st boot */
//硬件初始化,zephyr/soc/espressif/esp32s3/hw_init.c
//时钟配置、cache 初始化、MMU 初始化、更新 Flash 配置、配置看门狗
if (hardware_init()) {
ESP_EARLY_LOGE(TAG, "HW init failed, aborting");
abort();
}
//对 Flash 做 MMU 映射,将 Flash 映射到 irom 和 drom 地址上
//映射信息在 map 内
map_rom_segments(0, &map);
/* Disable RNG entropy source as it was already used */
soc_random_disable();
/* Disable glitch detection as it can be falsely triggered by EMI interference */
ana_clock_glitch_reset_config(false);
ESP_EARLY_LOGI(TAG, "libc heap size %d kB.", libc_heap_size / 1024);
//启动 app
__esp_platform_app_start();
}
在map_rom_segments的主要任务是将 Flash 中的指令和 ro 数据通过 MMU 映射到 irom 和 drom 地址上,此后才能直接在 Flash 上直接运行。map_rom_segments并不会关心 header 中描述有多少个 segment,而是直接跳过 header,逐个去遍历 segment,根据 segment 中 memory offset 和 size 来进行判断,如果落在 irom 和 drom 中,就进行 MMU 映射。
void map_rom_segments(int core, struct rom_segments *map)
{
//map 中存有 irom 和 drom 在 Flash 中默认的偏移地址 irom_flash_offset 和 drom_flash_offset,这是 zephyr 通过 default.ld 链接的时候生成的值
//这里将这两个生成的值先保存到 app_irom_start_align 和 app_drom_start_align 中,后面要根据 segment 中数据的实际偏移地址来进行修正
uint32_t app_irom_vaddr_align = map->irom_map_addr & MMU_FLASH_MASK;
uint32_t app_irom_start_align = map->irom_flash_offset & MMU_FLASH_MASK;
uint32_t app_drom_vaddr_align = map->drom_map_addr & MMU_FLASH_MASK;
uint32_t app_drom_start_align = map->drom_flash_offset & MMU_FLASH_MASK;
esp_image_segment_header_t WORD_ALIGNED_ATTR segment_hdr;
//zephyr.bin 的镜像烧写在 boot_partition 分区中,对于 ROM 直接引导的镜像,boot_partition 分区就是从 Flash 的 0 地址开始,因此这里的 offset 就是 0
size_t offset = FIXED_PARTITION_OFFSET(boot_partition);
bool checksum = false;
unsigned int segments = 0;
unsigned int ram_segments = 0;
//跳过 header 和 ext header
offset += sizeof(esp_image_header_t);
//逐个遍历镜像,ESP32 的 ROM 最多支持 16 个 segment,所以这里最多遍历 16 个 segment
while (segments++ < 16) {
//读取 segment header,也就是 segment 的前 8 个字节,memory offset(load_addr) 和 segment size(data_len)
if (esp_rom_flash_read(offset, &segment_hdr,
sizeof(esp_image_segment_header_t), true) != 0) {
ESP_EARLY_LOGE(TAG, "Failed to read segment header at %x", offset);
abort();
}
if (IS_LAST(segment_hdr)) {
/* Total segment count = (segments - 1) */
break;
}
//当发现 segment 的 load_addr 等于 drom_map_addr 时,说明该 segment 是 drom segment,需要修正 app_drom_start_align 为当前 segment 的 flash 偏移地址,再与 MMU_FLASH_MASK 进行与操作,得到当前 segment 的 flash 偏移地址
if (segment_hdr.load_addr == map->drom_map_addr) {
map->drom_flash_offset = offset + sizeof(esp_image_segment_header_t);
app_drom_start_align = map->drom_flash_offset & MMU_FLASH_MASK;
}
//当发现 segment 的 load_addr 等于 irom_map_addr 时,说明该 segment 是 irom segment,需要修正 app_irom_start_align 为当前 segment 的 flash 偏移地址,再与 MMU_FLASH_MASK 进行与操作,得到当前 segment 的 flash 偏移地址
if (segment_hdr.load_addr == map->irom_map_addr) {
map->irom_flash_offset = offset + sizeof(esp_image_segment_header_t);
app_irom_start_align = map->irom_flash_offset & MMU_FLASH_MASK;
}
if (IS_SRAM(segment_hdr) || IS_RTC(segment_hdr)) {
ram_segments++;
}
//跳到下一个 segment
offset += sizeof(esp_image_segment_header_t) + segment_hdr.data_len;
if (ram_segments == bootloader_image_hdr.segment_count && !checksum) {
offset += (CHECKSUM_ALIGN - 1) - (offset % CHECKSUM_ALIGN) + 1;
checksum = true;
}
}
if (segments == 0 || segments == 16) {
ESP_EARLY_LOGE(TAG, "Error parsing segments");
abort();
}
esp_rom_uart_tx_wait_idle(CONFIG_ESP_CONSOLE_UART_NUM);
cache_hal_disable(CACHE_TYPE_ALL);
/* Clear the MMU entries that are already set up,
* so the new app only has the mappings it creates.
*/
if (core == 0) {
mmu_hal_unmap_all();
}
uint32_t actual_mapped_len = 0;
//将 drom 和 irom 的 Flash 地址映射到 irom 和 drom 的虚拟地址上,此后就可以在 Flash 上运行指令和读到 ro 数据
mmu_hal_map_region(core, MMU_TARGET_FLASH0, app_drom_vaddr_align, app_drom_start_align,
map->drom_size, &actual_mapped_len);
mmu_hal_map_region(core, MMU_TARGET_FLASH0, app_irom_vaddr_align, app_irom_start_align,
map->irom_size, &actual_mapped_len);
/* ----------------------Enable corresponding buses---------------- */
cache_bus_mask_t bus_mask;
bus_mask = cache_ll_l1_get_bus(core, app_drom_vaddr_align, map->drom_size);
cache_ll_l1_enable_bus(core, bus_mask);
bus_mask = cache_ll_l1_get_bus(core, app_irom_vaddr_align, map->irom_size);
cache_ll_l1_enable_bus(core, bus_mask);
#if CONFIG_MP_MAX_NUM_CPUS > 1
bus_mask = cache_ll_l1_get_bus(1, app_drom_vaddr_align, map->drom_size);
cache_ll_l1_enable_bus(1, bus_mask);
bus_mask = cache_ll_l1_get_bus(1, app_irom_vaddr_align, map->irom_size);
cache_ll_l1_enable_bus(1, bus_mask);
#endif
/* ----------------------Enable Cache---------------- */
cache_hal_enable(CACHE_TYPE_ALL);
}
建立的映射关系如下

到此 ESP32S3 运行指令和 RO 数据已经通过 ROM 搬运到 SRAM 中和将 Flash 进行 MMU 映射已经完全可用,最后就通过__esp_platform_app_start踏入到真正的 Zephyr 大门内。
__esp_platform_app_start还会做一些 esp 硬件相关的初始化,然后调用z_prep_c->z_cstart进入到 Zephyr 内核初始化,再往后就是通用的 Zephyr 内核初始化流程和进入到 Main(), 本文就不再做详细说明。
SRAM 还是 Flash XIP 链接到标题
从前面的分析可以看到,代码和数据都存放在 Flash 中,但是有一部分要从 Flash 上 copy 到 SRAM 中 (IRAM/DRAM),但有一部分通过 MMU 映射可以在 Flash 上 XIP 运行。那么选择的标准是什么呢?普遍的遵循下面两条:
- 在 Flash MMU 映射建立前的代码/数据要被搬运到 SRAM 中 (IRAM/DRAM)
- 访问 Flash 的驱动需要放到 SRAM 中
在 Zephyr 中是如何做到上面两点的呢?一共有两种方式:
一是通过 default.ld 对文件进行描述,将 lib 中指定的文件放到 iram 和 dram 中,例如
.iram0.text : ALIGN(4)
{
/* Code marked as running out of IRAM */
_iram_text_start = ABSOLUTE(.);
*(.iram1 .iram1.*)
*(.iram0.literal .iram.literal .iram.text.literal .iram0.text .iram.text)
*libarch__xtensa__core.a:(.literal .text .literal.* .text.*)
*libarch__common.a:(.literal .text .literal.* .text.*)
*libkernel.a:(.literal .text .literal.* .text.*)
*libgcc.a:lib2funcs.*(.literal .text .literal.* .text.*)
*libzephyr.a:cbprintf_packaged.*(.literal .text .literal.* .text.*)
*libdrivers__flash.a:flash_esp32.*(.literal .text .literal.* .text.*)
*libzephyr.a:windowspill_asm.*(.literal .text .literal.* .text.*)
*libzephyr.a:log_noos.*(.literal .text .literal.* .text.*)
*libdrivers__timer.a:xtensa_sys_timer.*(.literal .text .literal.* .text.*)
*libzephyr.a:log_core.*(.literal .text .literal.* .text.*)
*libzephyr.a:cbprintf_complete.*(.literal .text .literal.* .text.*)
二是通过 section 属性对函数进行限定放到 IRAM 和 DRAM 中,例如:
void IRAM_ATTR __esp_platform_app_start(void)
modules/hal/espressif/components/esp_common/include/esp_attr.h中定义有如下,因此可以看到,__esp_platform_app_start被限定放到 iram1 中
// Forces code into IRAM instead of flash
#define IRAM_ATTR _SECTION_ATTR_IMPL(".iram1", __COUNTER__)
// Forces data into DRAM instead of flash
#define DRAM_ATTR _SECTION_ATTR_IMPL(".dram1", __COUNTER__)
参考 链接到标题
https://lgl88911.pages.dev/posts/zephyr%E4%B8%8Besp32c3%E6%9E%84%E5%BB%BA%E5%BC%95%E5%AF%BC%E4%B9%8Besp-bootloader/ https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/firmware-image-format.html