深入理解CH32V307的堆栈内存分配
在学习CH32V307的过程中,我发现其DVP例程中的图像缓冲区是直接用了SRAM中的一片内存地址,由于好奇直接使用这片地址为什么不会产生问题,故研究。最后学习了CH32V307堆栈的分配方式,以及修改堆栈空间大小的方法,学会大大扩展SRAM空间以及栈空间,防止爆栈。
一,芯片介绍
官网在此CH32V307
不得不说这个片子给我带来了很多惊喜,让我学到了很多
我们看一下这个框图,主要关注一下FLASH与SRAM。
这里写的是256KB FLASH+64KB SRAM ,但实际上可以是(192K FLASH+128K SRAM)、(224K FLASH+96K SRAM)、(256K FLASH+64K SRAM)、(288K FLASH+32K SRAM)等和为320KB几种组合中的一种^1。
最大128KB,妈妈再也不用担心我内存不够用了
至于为什么是320KB,这篇文章中说:
其实赤菟V307内部有一块320 KB SRAM,分为A、B两块,A、B块的大小由用户选择字的user的SRAM_CODE_MODE 位决定,A块负责存放用户代码,B块留作单片机真正的SRAM,每次上电运行时,内部自动从Code Flash中加载A块大小的用户代码运行。
至于这段话在官方哪里说的,我没有找到。但结合用户手册中FLASH分为0等待和1等待区的描述,我认为这是正确的。(代码在SRAM中运行当然是0等待区)
二,配置方法
通过MounRiver Studio(MRS)创建的工程默认是288K FLASH+32K SRAM的配置,将配置更改成其他有两种方式。
使用WCHLink下载器直接更改
在MRS中的下载配置,或者WCHISP Studio中可以选择内存分配方式(ROM+RAM)。
改好后下次下载时就可以改变内存分配。
使用代码修改
内存的分配方式在FLASH用户选择字寄存器中可以设置。
使用以下代码可以进行设置(来源于这篇文章):
1 | typedef enum |
修改LD文件
更改了设置后,还需要修改Link.ld
文件来重设程序的地址分配。
1 | MEMORY |
修改栈大小
同样的,在LD文件中可以修改栈大小,在LD文件的开头
1 | ENTRY( _start ) |
其中修改__stack_size = 2048;
就是栈大小,可以自行设置,但要注意不要设太大超过了占了其他数据区(这个下面讲)。
设置栈大小后,除开必要数据区,剩下了就全是堆区了,默认分配情况下你有19K左右的堆,也就是用malloc函数分配的区域,非常的浪费。
- 这个
Link.ld
文件就是这次的重头系,规划了储存器空间的分配。
通常,程序编译的最后一步就是链接,此过程根据“*.ld”链接文件将多个目标文件**(.o)和库文件(.a)输入文件链接成一个可执行输出文件(.elf)。涉及到对空间和地址的分配以及符号解析与重定位**。
而ld链接脚本控制这整个链接过程,主要用于规定各输入文件中的程序、数据等内容段在输出文件中的空间和地址如何分配。通俗的讲,链接脚本用于描述输入文件中的段,将其映射到输出文件中,并指定输出文件中的内存分配。
关于CH32V307连接脚本的说明,可以参考这两篇文章,写的很好很详细。
RISC-V MCU CH32V307/CH32V203/CH32V003等 ld链接脚本说明
本文大部分参考于这两篇文章。
三,内存空间分配方式
在深入理解上面步骤是如何改变堆栈大小之前,我们需要先理解一下内存的分配方式。
通常,一个内存空间会被分为以下区域:
Text
:储存用户代码,为只读区,程序从FLASH读出后在此运行(这个片子中就是上面的A区)。data
:储存已初始化的全局变量,这里data的起始地址怎么配置都是0x20000000。bss
:储存未初始化的全局变量。heap
:堆区,malloc函数分配的动态区域。stack
:栈区,调用函数的传参空间。
在CH32V307中,text大小由设置配置,而bss与data则是由编译器编译后决定大小。最后剩下的区域就是堆栈区,栈区可以由用户配置大小。
在本芯片中,data端的起始地址就是设置的RAM的起始地址0x20000000
,bss端的结束地址根据不同的用户程序决定,该地址后到RAM地址的结束都是堆栈区域,其中heap区从bss结束地址开始,向高地址增长,stac从RAM结束开始,向低地址增长。
- 当栈与堆重合,就爆栈了,会出现各种奇怪的东西。
四,连接脚本中对栈的设置
在连接脚本的开头和结尾有关于栈的配置:
1 | ENTRY( _start ) |
这里解释几个符号:
.
:表示当前地址,随着分配区域递增,也可以进行运算改变PROVIDE()
:定义全局变量.stack
:栈区分配设置
代码解读:
首先,PROVIDE( end = . );
将当前地址(bss结束)赋给了end
变量,这里就是heap的起始地址。
之后,.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
将栈顶的地址给出,就是栈最大可以生长到的地址:RAM结束地址减去栈的大小。
PROVIDE( _heap_end = . );
栈顶的地址同时也是堆结束的地址,这样堆的大小就确定了是:bss结束到栈顶。
PROVIDE(_susrstack = . );
栈顶地址。
PROVIDE( _eusrstack = .);
栈底地址,在上面的运算中其实就是ORIGIN(RAM) + LENGTH(RAM)
。
至此,我们就明白了堆栈是怎么分配了,只需要修改__stack_size
就可以设置栈大小了,默认RAM中除去data、bss、stack等剩余的都为heap空间。
通过访问end
,_heap_end
可以获得heap 的大小与地址。通过访问_susrstack
,_eusrstack
可以获得栈的大小和地址。
五,测试
编写ld文件,修改成以下配置:
1 | __stack_size = 20K; |
在main函数中编写;
1 | extern uint32_t _end;//堆起始地址 |
获得以下输出:
1 | heap start address = 0x20002858 |
将栈地址相减即使栈的大小,将堆地址与栈顶相减即可得到堆区大小。
六,疑问解释
知道上面这些知识后,再反过来看开头的问题,就可以解释了。
首先是DVP缓冲区的地址:
1 | UINT32 JPEG_DVPDMAaddr0 = 0x20005000; |
根据地址范围,我们发现,这就是在heap区随便来了一块区域,这样的话如果使用malloc的话就会导致问题。
什么sb配置完全不考虑例程在其他地方跑是吧
第二个疑惑就是为什么要在debug.c
中重定义_sbrk
.
1 | /********************************************************************* |
heap只有起始地址,没有结束地址约束,这样最终会导致malloc永远都不会返回NULL
而定义_sbrk
后,使用_heap_end
变量就可以判断是否出现空间不足。