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 组成。多字节的字段是小端构成。 espfm_format

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_pointzephyr/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 链接到标题

exheader 根据 --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 链接到标题

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 不一定一样。

下图就是前面示例生成的镜像结构: espimage

整个镜像大小以 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() 开始执行。

esp32rom

自启动 链接到标题

从 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);

}

建立的映射关系如下 esp32flashmap

到此 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