Zephyr ESP32 Heap实现分析

本文分析esp32的内存如何整合为zephyr管理的heap。

前文参考 [1] Zephyr内存管理之Heap

ESP32内存 链接到标题

本文无意说明ESP32物理内存的情况,我们只是通过ESP32在Zephyr的链接加载文件链接ESP32内存的分布情况,方便后续说明heap所在的内存区域。ESP32的链接加载文件为zephyr/soc/xtensa/esp32/linker.ld,我们可以看到有如下内容

MEMORY
{
  iram0_0_seg(RX): org = 0x40080000, len = 0x20000
  irom0_0_seg(RX): org = 0x400D0020, len = 0x330000-0x20
  /*
   * Following is DRAM memory split with reserved address ranges in ESP32:
   *
   * 0x3FFA_E000 - 0x3FFB_0000 (Reserved: data memory for ROM functions)
   * 0x3FFB_0000 - 0x3FFE_0000 (RAM bank 1 for application usage)
   * 0x3FFE_0000 - 0x3FFE_0440 (Reserved: data memory for ROM PRO CPU)
   * 0x3FFE_3F20 - 0x3FFE_4350 (Reserved: data memory for ROM APP CPU)
   * 0x3FFE_4350 - 0x3F10_0000 (RAM bank 2 for application usage)
   *
   * FIXME:
   *  - Utilize available memory regions to full capacity
   */
  dram0_0_seg(RW): org = 0x3FFB0000 + CONFIG_ESP32_BT_RESERVE_DRAM, len = 0x2c200 - CONFIG_ESP32_BT_RESERVE_DRAM
  dram0_1_seg(RW): org = 0x3FFE4350, len = 0x1BCB0
  drom0_0_seg(R): org = 0x3F400020, len = 0x400000-0x20
  rtc_iram_seg(RWX): org = 0x400C0000, len = 0x2000
  rtc_slow_seg(RW): org = 0x50000000, len = 0x1000
#if defined(CONFIG_ESP_SPIRAM)
  ext_ram_seg(RW): org = 0x3F800000, len = CONFIG_ESP_SPIRAM_SIZE
#endif
#ifdef CONFIG_GEN_ISR_TABLES
  IDT_LIST(RW): org = 0x3ebfe010, len = 0x2000
#endif
}

有三个section可以用于放置heap:

  • dram0_0_seg: 片上DRAM
  • dram0_1_seg: 片上DRAM
  • ext_ram_seg:片外SPIRAM

配置ESP32 Heap 链接到标题

区域配置 链接到标题

配置使用dram0_0_seg 链接到标题

当配置CONFIG_HEAP_MEM_POOL_SIZE时,有heap被放置在dram0_0_seg上,使用片上RAM,例如

CONFIG_HEAP_MEM_POOL_SIZE=40960

配置使用dram0_1_seg 链接到标题

当配置CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE时,有heap被放置在dram0_1_seg上,使用片上RAM,例如

CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE=81920

配置使用ext_ram_seg 链接到标题

当配置CONFIG_ESP_SPIRAM时,有heap被放置在ext_ram_seg上,使用片外SPIRAM,例如

CONFIG_ESP_SPIRAM=y
CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD=1024
CONFIG_ESP_SPIRAM_SIZE=0x400000

该配置表示外部spiram有4M,当分配的内存大于1024字节时才从外部SPIRAM中分配。 CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD的范围是1K到128K,如果不配置默认8K。

分配内存顺序 链接到标题

zephyr使用k_mallc从heap中分配内存,esp32中有以上三个heap,使用方式是:

  1. 当分配的内存小于CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD时,从片上RAM分配
  2. 从片上RAM分配时,先从dram0_1_seg中分配,如果分配不到再从dram0_0_seg中分配
  3. 如果片上RAM无法分配到内存,且CONFIG_ESP_HEAP_SEARCH_ALL_REGIONS=y, 从ext_ram_seg内分配
  4. 当分配的内存大于等于CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD时,从ext_ram_seg内分配
  5. 如果SPIRAM内无法分配到内存,且CONFIG_ESP_HEAP_SEARCH_ALL_REGIONS=y, 从dram0_0_seg/dram0_1_seg内分配

代码分析 链接到标题

Heap创建 链接到标题

CONFIG_HEAP_MEM_POOL_SIZE配置的heap,在文件kernel/mempool.c中定义

K_HEAP_DEFINE(_system_heap, CONFIG_HEAP_MEM_POOL_SIZE);
#define _SYSTEM_HEAP (&_system_heap)

k_malloc将从_system_heap中分配内存,细节参考[1]。 宏K_HEAP_DEFINE在kernel.h中,实现代码如下

#define K_HEAP_DEFINE(name, bytes)				\
	char __aligned(8) /* CHUNK_UNIT */			\
	     kheap_##name[MAX(bytes, Z_HEAP_MIN_SIZE)];		\
	Z_STRUCT_SECTION_ITERABLE(k_heap, name) = {		\
		.heap = {					\
			.init_mem = kheap_##name,		\
			.init_bytes = MAX(bytes, Z_HEAP_MIN_SIZE), \
		 },						\
	}

可以看到_system_heap就是一个全局的数组,由于没有对该数组进行显式的section设定,_system_heap作为未初始化的全局变量将会放在bss段中。从esp32的link.ld文件中可以看到bss是在dram0_0_seg中

#define RAMABLE_REGION dram0_0_seg :dram0_0_phdr

  SECTION_DATA_PROLOGUE(_BSS_SECTION_NAME,(NOLOAD),)
  {
    . = ALIGN (8);
    _bss_start = ABSOLUTE(.);

    _btdm_bss_start = ABSOLUTE(.);
    *libbtdm_app.a:(.bss .bss.* COMMON)
    . = ALIGN (4);
    _btdm_bss_end = ABSOLUTE(.);

    /* Buffer for system heap should be placed in dram0_0_seg */
    *libkernel.a:mempool.*(.noinit.kheap_buf__system_heap .noinit.*.kheap_buf__system_heap)

    *(.dynsbss)
    *(.sbss)
    *(.sbss.*)
    *(.gnu.linkonce.sb.*)
    *(.scommon)
    *(.sbss2)
    *(.sbss2.*)
    *(.gnu.linkonce.sb2.*)
    *(.dynbss)
    *(.bss)
    *(.bss.*)
    *(.share.mem)
    *(.gnu.linkonce.b.*)
    *(COMMON)
    . = ALIGN (8);
    _bss_end = ABSOLUTE(.);
  } GROUP_LINK_IN(RAMABLE_REGION)

CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE配置的heap, 在modules/hal/espressif/zephyr/esp32/src/heap_caps.c中定义

#if (CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE > 0)
char __aligned(sizeof(void *)) __NOINIT_ATTR dram0_seg_1_heap[CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE];
STRUCT_SECTION_ITERABLE(k_heap, _internal_heap_1) = {
    .heap = {
        .init_mem = dram0_seg_1_heap,
        .init_bytes = CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE,
    }
};
#endif

k_malloc将以比较特殊的方式从dram0_seg_1_heap中分配内存(后文代码分析),从上面代码可以看到dram0_seg_1_heap也是一个全局的数组,该数组通过定义在modules\hal\espressif\components\xtensa\include\esp_attr.h的__NOINIT_ATTR限定到.noinit的section中

#define __NOINIT_ATTR _SECTION_ATTR_IMPL(".noinit", __COUNTER__)
#define _SECTION_ATTR_IMPL(SECTION, COUNTER) __attribute__((section(SECTION "." _COUNTER_STRINGIFY(COUNTER))))

从esp32的link.ld文件中可以看到.noinit是在dram0_1_seg中

#define RAMABLE_REGION_1 dram0_1_seg :dram0_1_phdr

  SECTION_DATA_PROLOGUE(_NOINIT_SECTION_NAME, (NOLOAD),)
  {
    . = ALIGN (8);
    *(.noinit)
    *(".noinit.*")
    . = ALIGN (8);
  } GROUP_LINK_IN(RAMABLE_REGION_1)

CONFIG_ESP_SPIRAM配置的heap, 在modules/hal/espressif/zephyr/esp32/src/heap_caps.c中定义

#if defined(CONFIG_ESP_SPIRAM)
EXT_RAM_ATTR int _spiram_data_start;
STRUCT_SECTION_ITERABLE(k_heap, _spiram_heap) = {
    .heap = {
        .init_mem = &_spiram_data_start,
#if (CONFIG_ESP_SPIRAM_SIZE <= 0x400000)
        .init_bytes = CONFIG_ESP_SPIRAM_SIZE,
#else
        .init_bytes = 0x400000,
#endif
    },
};

#define EXT_RAM_ATTR _SECTION_ATTR_IMPL(".ext_ram.bss", __COUNTER__)
#endif

_spiram_heap的内存地址是全局变量_spiram_data_start的地址,而_spiram_data_start是被放在.ext_ram.bss内的,从linker.ld中可以看到.ext_ram.bss是放在ext_ram_seg内的

#if defined(CONFIG_ESP_SPIRAM)
  .ext_ram.bss (NOLOAD):
  {
    _ext_ram_data_start = ABSOLUTE(.);
    *(.ext_ram.bss*)
    _ext_ram_data_end = ABSOLUTE(.) + CONFIG_ESP_SPIRAM_SIZE;
  } > ext_ram_seg
#endif

如果.ext_ram.bss中只有_spiram_data_start一个变量,那么_spiram_data_start的地址就相当于是该section的起始地址。这里其实也可以引用_ext_ram_data_start做为首地址,而无需另外引入_spiram_data_start。

内存分配 链接到标题

Zephyr从heap中分配内存是使用k_malloc函数,但一般情况下只会从_system_heap中分配,在esp32下引入了dram0_1_seg内的_internal_heap_1和ext_ram_seg内的_spiram_heap, 前面提到了k_malloc也能从这两个heap分配内存,这里我们分析如果具体实现,所有实现的代码在modules/hal/espressif/zephyr/esp32/下。

wrap 链接到标题

由于zephyr已经将k_malloc实现,固定的从_system_heap分配内存,且k_mallo不是弱符号,因此esp32的利用gnu ld的特性采用了wrap的方法来替换包装指定符号: 在modules\hal\espressif\zephyr\esp32\CMakeLists.txt下指定要wrap的符号

zephyr_link_libraries_ifdef(
    CONFIG_HEAP_MEM_POOL_SIZE
    gcc
    "-Wl,--wrap=k_calloc"
    "-Wl,--wrap=k_malloc"
    )

实现wrap函数

void *__real_k_malloc(size_t size);
void *__real_k_calloc(size_t nmemb, size_t size);

void *__wrap_k_malloc(size_t size)
{
  //call __real_k_malloc
}

void *__wrap_k_calloc(size_t nmemb, size_t size)
{
  //call __real_k_calloc
}

当编译链接完成后,调用k_malloc的地方就会被替换为调用__wrap_k_malloc,mempool.c中实现的k_malloc符号被替换为__real_k_malloc, esp32基于__wrap_k_malloc对三个heap进行管理,根据需要调用__real_k_malloc从_system_heap中分配内存。

使用heap代码 链接到标题

前面列出了k_malloc使用heap的方式,这里进一步分析说明代码, 从wrap可知,实际使用heap的函数是__wrap_k_malloc

void *__wrap_k_malloc(size_t size)
{
    void *ptr = NULL;

    if (size < CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD) {
      //小于CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD,先从片上RAM分配
       ptr = z_esp_alloc_internal(sizeof(void *), size);
#if defined(CONFIG_ESP_HEAP_SEARCH_ALL_REGIONS)
        //如果允许所有区域分配,在片上RAM分配不到情况下从片外SPIRAM上分配
        if (ptr == NULL) {
            ptr = z_esp_aligned_alloc(&_spiram_heap, sizeof(void *), size);
        }
#endif

    } else {
        //大于等于CONFIG_ESP_HEAP_MIN_EXTRAM_THRESHOLD,先从片外SPIRAM分配
        ptr = z_esp_aligned_alloc(&_spiram_heap, sizeof(void *), size);
#if defined(CONFIG_ESP_HEAP_SEARCH_ALL_REGIONS)
        //如果允许所有区域分配,在片外SPIRAM分配不到情况下从片内RAM上分配
        if (ptr == NULL) {
            ptr = z_esp_alloc_internal(sizeof(void *), size);
        }
#endif
    }
    return ptr;
}

片内RAM分配内存:

static void *z_esp_alloc_internal(size_t align, size_t size)
{
    void *ptr = NULL;
    //从_system_heap分配(mempool.c中的k_malloc)
    ptr = __real_k_malloc(size);

    //如果_system_heap没有分配到,从_internal_heap_1分配
    if (ptr == NULL) {
        ptr = z_esp_aligned_alloc(&_internal_heap_1, align, size);
    }

    return ptr;
}

esp heap分配函数, 用于从esp32自己建立的heap:_internal_heap_1和_spiram_heap上进行内存分配

static void *z_esp_aligned_alloc(struct k_heap *heap, size_t align, size_t size)
{
    void *mem;
    struct k_heap **heap_ref;
    size_t __align;

    //对齐调整
    if (size_add_overflow(size, sizeof(heap_ref), &size)) {
        return NULL;
    }
    __align = align | sizeof(heap_ref);

    //从指定heap中分配内存
    mem = k_heap_aligned_alloc(heap, __align, size, K_NO_WAIT);
    if (mem == NULL) {
        return NULL;
    }

    heap_ref = mem;
    *heap_ref = heap;
    mem = ++heap_ref;
    __ASSERT(align == 0 || ((uintptr_t)mem & (align - 1)) == 0,
             "misaligned memory at %p (align = %zu)", mem, align);

    return mem;
}

参考 链接到标题

https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/memory-types.html https://linux.die.net/man/1/ld