Zephyr ESP32 Heap实现分析

Creative Commons
本作品采用知识共享署名

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

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

ESP32内存

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,例如

1
CONFIG_HEAP_MEM_POOL_SIZE=40960

配置使用dram0_1_seg

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

1
CONFIG_ESP_HEAP_MEM_POOL_REGION_1_SIZE=81920

配置使用ext_ram_seg

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

1
2
3
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中定义

1
2
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中,实现代码如下

1
2
3
4
5
6
7
8
9
#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中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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中定义

1
2
3
4
5
6
7
8
9
#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中

1
2
#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中

1
2
3
4
5
6
7
8
9
#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中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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内的

1
2
3
4
5
6
7
8
#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的符号

1
2
3
4
5
6
zephyr_link_libraries_ifdef(
CONFIG_HEAP_MEM_POOL_SIZE
gcc
"-Wl,--wrap=k_calloc"
"-Wl,--wrap=k_malloc"
)

实现wrap函数

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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分配内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
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上进行内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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