深入理解CH32V307的堆栈内存分配

在学习CH32V307的过程中,我发现其DVP例程中的图像缓冲区是直接用了SRAM中的一片内存地址,由于好奇直接使用这片地址为什么不会产生问题,故研究。最后学习了CH32V307堆栈的分配方式,以及修改堆栈空间大小的方法,学会大大扩展SRAM空间以及栈空间,防止爆栈。

一,芯片介绍

官网在此CH32V307

不得不说这个片子给我带来了很多惊喜,让我学到了很多

image-20230709165615927

我们看一下这个框图,主要关注一下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)。

image-20230709172231090

image-20230709172514319

改好后下次下载时就可以改变内存分配。

使用代码修改

内存的分配方式在FLASH用户选择字寄存器中可以设置。

image-20230709172659522

使用以下代码可以进行设置(来源于这篇文章):

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
typedef enum
{
FlASH_192_SRAM_128 = 0,
FLASH_224_SRAM_96,
FLASH_256_SRAM_64,
FLASH_288_RAM_32
} FLASH_SRAM_DEFIN;

//note: this operation will take effect after reset
void Config_Flash_SRAM(FLASH_SRAM_DEFIN SetFlashSRAM)
{
uint8_t UserByte = FLASH_GetUserOptionByte() & 0xff; //get user option byte

switch(SetFlashSRAM)
{
case 0:
UserByte &= ~(0xc0); // SRAM_CODE_MODE = 00
break;
case 1:
UserByte &= ~(0xc0); // SRAM_CODE_MODE = 00
UserByte |= 0x7f; // SRAM_CODE_MODE = 01
break;
case 2:
UserByte &= ~(0xc0); // SRAM_CODE_MODE = 00
UserByte |= 0xbf; // SRAM_CODE_MODE = 10
break;
case 3:
UserByte |= 0xff; // SRAM_CODE_MODE = 11
break;
default:
break;
}

FLASH_Unlock();
FLASH_ProgramOptionByteData(0x1ffff802, UserByte);
FLASH_Lock();
}

/*********************************************************************
* @fn main
*
* @brief Main program.
*
* @return none
*/
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n", SystemCoreClock);


Config_Flash_SRAM(FLASH_288_RAM_32); //配置Flash为244 KB,SRAM为96KB ,复位后生效

printf("userByte = %02x \r\n",FLASH_GetUserOptionByte() & 0xff);
while(1)
{
;
}
}

修改LD文件

更改了设置后,还需要修改Link.ld文件来重设程序的地址分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MEMORY
{
/* CH32V30x_D8C - CH32V305RB-CH32V305FB
CH32V30x_D8 - CH32V303CB-CH32V303RB
*/
/*
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K
*/

/* CH32V30x_D8C - CH32V307VC-CH32V307WC-CH32V307RC
CH32V30x_D8 - CH32V303VC-CH32V303RC
FLASH + RAM supports the following configuration
FLASH-192K + RAM-128K
FLASH-224K + RAM-96K
FLASH-256K + RAM-64K
FLASH-288K + RAM-32K
*/
/* 在这里修改成设置的大小 */
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 288K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K
}

修改栈大小

同样的,在LD文件中可以修改栈大小,在LD文件的开头

1
2
3
4
5
ENTRY( _start )

__stack_size = 2048;

PROVIDE( _stack_size = __stack_size );

其中修改__stack_size = 2048;就是栈大小,可以自行设置,但要注意不要设太大超过了占了其他数据区(这个下面讲)。

设置栈大小后,除开必要数据区,剩下了就全是堆区了,默认分配情况下你有19K左右的堆,也就是用malloc函数分配的区域,非常的浪费

  • 这个Link.ld文件就是这次的重头系,规划了储存器空间的分配。

通常,程序编译的最后一步就是链接,此过程根据“*.ld”链接文件将多个目标文件**(.o)和库文件(.a)输入文件链接成一个可执行输出文件(.elf)。涉及到对空间和地址的分配以及符号解析与重定位**。

而ld链接脚本控制这整个链接过程,主要用于规定各输入文件中的程序、数据等内容段在输出文件中的空间和地址如何分配。通俗的讲,链接脚本用于描述输入文件中的段,将其映射到输出文件中,并指定输出文件中的内存分配。

-ld链接脚本说明

关于CH32V307连接脚本的说明,可以参考这两篇文章,写的很好很详细。

RISC-V MCU CH32V307/CH32V203/CH32V003等 ld链接脚本说明

RISC-V MCU堆栈机制

本文大部分参考于这两篇文章。


三,内存空间分配方式

在深入理解上面步骤是如何改变堆栈大小之前,我们需要先理解一下内存的分配方式。

通常,一个内存空间会被分为以下区域:

image-20230709175229509

  • Text:储存用户代码,为只读区,程序从FLASH读出后在此运行(这个片子中就是上面的A区)。
  • data:储存已初始化的全局变量,这里data的起始地址怎么配置都是0x20000000。
  • bss:储存未初始化的全局变量
  • heap:堆区,malloc函数分配的动态区域。
  • stack:栈区,调用函数的传参空间。

在CH32V307中,text大小由设置配置,而bss与data则是由编译器编译后决定大小。最后剩下的区域就是堆栈区,栈区可以由用户配置大小。

在本芯片中,data端的起始地址就是设置的RAM的起始地址0x20000000,bss端的结束地址根据不同的用户程序决定,该地址后到RAM地址的结束都是堆栈区域,其中heap区从bss结束地址开始,向高地址增长,stac从RAM结束开始,向低地址增长。

  • 当栈与堆重合,就爆栈了,会出现各种奇怪的东西。

四,连接脚本中对栈的设置

在连接脚本的开头和结尾有关于栈的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ENTRY( _start )

__stack_size = 2048;

PROVIDE( _stack_size = __stack_size );

.........
...
/*bss分配空间的代码*/
...
PROVIDE( _end = _ebss);
PROVIDE( end = . );

.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
{
PROVIDE( _heap_end = . );
. = ALIGN(4);
PROVIDE(_susrstack = . );
. = . + __stack_size;
PROVIDE( _eusrstack = .);
} >RAM

这里解释几个符号:

  • .:表示当前地址,随着分配区域递增,也可以进行运算改变
  • 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
2
3
4
5
6
7
8
__stack_size = 20K;

.....
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 192K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}

在main函数中编写;

1
2
3
4
5
6
7
extern uint32_t _end;//堆起始地址
extern uint32_t _eusrstack; /* 声明外部变量 _eusrstack */
extern uint32_t _susrstack; /* 声明外部变量 _susrstack */

printf("heap start address = 0x%08x\r\n",&_end);//打印堆起始地址
printf("stac start address = 0x%08x\r\n",&_eusrstack);//打印栈底地址
printf("stac end address = 0x%08x\r\n",&_susrstack);//打印栈顶地址

获得以下输出:

1
2
3
heap start address = 0x20002858
stac start address = 0x20020000
stac end address = 0x2001b000

将栈地址相减即使栈的大小,将堆地址与栈顶相减即可得到堆区大小。

六,疑问解释

知道上面这些知识后,再反过来看开头的问题,就可以解释了。

首先是DVP缓冲区的地址:

1
2
UINT32  JPEG_DVPDMAaddr0 = 0x20005000;
UINT32 JPEG_DVPDMAaddr1 = 0x20005000 + OV2640_JPEG_WIDTH;

根据地址范围,我们发现,这就是在heap区随便来了一块区域,这样的话如果使用malloc的话就会导致问题

什么sb配置完全不考虑例程在其他地方跑是吧

第二个疑惑就是为什么要在debug.c中重定义_sbrk.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*********************************************************************
* @fn _sbrk
*
* @brief Change the spatial position of data segment.
*
* @return size: Data length
*/
__attribute__((used)) void *_sbrk(ptrdiff_t incr)
{
extern char _end[];
extern char _heap_end[];
static char *curbrk = _end;

if ((curbrk + incr < _end) || (curbrk + incr > _heap_end))
return NULL - 1;

curbrk += incr;
return curbrk - incr;
}

heap只有起始地址,没有结束地址约束,这样最终会导致malloc永远都不会返回NULL

而定义_sbrk后,使用_heap_end变量就可以判断是否出现空间不足。