RISC-V MCU堆栈机制

1、什么是堆栈?

在嵌入式的世界里,堆栈通常指的是,严格来说,堆栈分为堆(Heap)栈(Stack)

  • 栈(Stack): 一种顺序数据结构,满足后进先出(Last-In / First-Out)的原则,由编译器自动分配和释放。使用一级缓存,调用完立即释放。
  • 堆(Heap):类似于链表结构,可对任意位置进行操作,通常由程序员手动分配,使用完需及时释放(free),不然容易造成内存泄漏。使用二级缓存。

image

2、堆栈的作用

  • 函数调用时,如果函数参数和局部变量很多,寄存器放不下,需要开辟栈空间存储。
  • 中断发生时,栈空间用于存放当前执行程序的现场数据(下一条指令地址、各种缓存数据),以便中断结束后恢复现场。

3、堆栈大小定义

RISC-V MCU的堆栈大小通常在ld链接脚本中定义,关于ld链接脚本可查看该文:RISC-V MCU ld链接脚本说明

复制代码
 1 ENTRY( _start ) /* 入口地址 */ 
 2  
 3 __stack_size = 2048; /* 定义栈大小 */
 4 PROVIDE( _stack_size = __stack_size );/* 定义_stack_size符号,类似于全局变量 */
 5  
 6 MEMORY
 7 {
 8     FLASH (rx) : ORIGIN = 0x00000000 , LENGTH = 0x10000
 9     RAM (xrw) : ORIGIN = 0x20000000 , LENGTH = 0x5000
10 }
11 /* 
12 ...
13 中间省略 
14 ...
15 */
16  
17 .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :  /* 分配栈空间0x20004800 ~ 0x20005000,共2KB */
18 {
19     . = ALIGN(4);
20     PROVIDE(_susrstack = . );
21     . = . + __stack_size;
22     PROVIDE( _eusrstack = .);
23 } >RAM 
复制代码

以RISC-V MCU CH32V103为例,在其ld链接脚本中,定义了_stack_size符号,值为 2048 Byte,后面使用该值在.stack段中分配栈空间,可更改此值调整栈空间大小。

CH32V103 的RAM共20KB,除去程序用到的databss段,剩下空间即为动态数据段,供堆栈的动态使用。

ld链接脚本中,没有明确定义heap堆的大小,按照其定义,动态数据段,除了stack占用的,剩下的都可用于heap,通过malloc进行动态管理。

4、压栈出栈过程

以CH32V103 printf函数调用为例,其反汇编代码如下:

复制代码
 1 000007a4 <iprintf>:
 2      7a4:    7139                    addi  sp,sp,-64         # 调整堆栈指针sp,分配64字节的栈空间
 3      7a6:    da3e                    sw    a5,52(sp)       # 压栈,保存a5寄存器的值
 4      7a8:    d22e                    sw    a1,36(sp)       # 压栈,按需保存相应的寄存器
 5      7aa:    d432                    sw    a2,40(sp)
 6      7ac:    d636                    sw    a3,44(sp)
 7      7ae:    d83a                    sw    a4,48(sp)
 8      7b0:    dc42                    sw    a6,56(sp)
 9      7b2:    de46                    sw    a7,60(sp)
10      7b4:    80818793              addi    a5,gp,-2040 # 20000078 <_impure_ptr>
11      7b8:    cc22                    sw    s0,24(sp)       # 压栈,保存帧指针fp(s0)
12      7ba:    4380                    lw    s0,0(a5)
13      7bc:    ca26                    sw    s1,20(sp)
14      7be:    ce06                    sw    ra,28(sp)       # 压栈,保存返回地址(ra寄存器)
15      7c0:    84aa                    mv    s1,a0
16      7c2:    c409                    beqz    s0,7cc <iprintf+0x28>
17      7c4:    4c1c                    lw    a5,24(s0)
18      7c6:    e399                    bnez    a5,7cc <iprintf+0x28>
19      7c8:    8522                    mv    a0,s0
20      7ca:    2315                    jal    cee <__sinit>
21      7cc:    440c                    lw    a1,8(s0)
22      7ce:    1054                    addi    a3,sp,36
23      7d0:    8626                    mv    a2,s1
24      7d2:    8522                    mv    a0,s0
25      7d4:    c636                    sw    a3,12(sp)
26      7d6:    167000ef              jal    ra,113c <_vfiprintf_r>
27      7da:    40f2                    lw    ra,28(sp)      # 出栈,恢复返回地址(ra寄存器)
28      7dc:    4462                    lw    s0,24(sp)      # 出栈,恢复帧指针fp(s0)
29      7de:    44d2                    lw    s1,20(sp)      # 出栈,按需恢复相应的寄存器
30      7e0:    6121                    addi    sp,sp,64       # 释放栈空间
31      7e2:    8082                    ret                    # 函数返回,根据ra寄存器地址返回
复制代码

 

5、malloc使用注意事项

CH32V103默认工程中,heap只有起始地址,没有结束地址约束,这样最终会导致malloc永远都不会返回NULL。

如果使用malloc时,需进行如下操作:

  1. 重写_sbrk函数,代码如下,放在工程任意位置,推荐放在debug.c 文件中。

    _sbrk代码原型:https://github.com/riscv/riscv-newlib/blob/riscv-newlib-3.1.0/libgloss/libnosys/sbrk.c

    复制代码
     1 void *_sbrk(ptrdiff_t incr)
     2 {
     3     extern char _end[];
     4     extern char _heap_end[];
     5     static char *curbrk = _end;
     6  
     7     if ((curbrk + incr < _end) || (curbrk + incr > _heap_end))
     8     return NULL - 1;
     9  
    10     curbrk += incr;
    11     return curbrk - incr;
    12 }
    复制代码

     

  2. 修改ld链接脚本,定义heap大小

    • 默认RAM中除去data、bss、stack等剩余的都为heap空间

      增加PROVIDE( _heap_end = . ); 定义,位置如下:

    复制代码
     1       PROVIDE( _end = _ebss);
     2       PROVIDE( end = . );  /* 定义heap起始位置 */
     3     
     4       .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
     5         {
     6              PROVIDE( _heap_end = . );   /* 定义heap结束位置,默认到栈底结束 */ 
     7         
     8             . = ALIGN(4);
     9             PROVIDE(_susrstack = . );
    10             /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/
    11             . = . + __stack_size;
    12             PROVIDE( _eusrstack = .);
    13         } >RAM 
    复制代码

     

    • 指定heap大小的修改方式如下:

      增加 PROVIDE( _heap_end = . + 0x400); 定义,位置如下:

    复制代码
     1   PROVIDE( _end = _ebss);
     2   PROVIDE( end = . );  /* 定义heap起始位置 */
     3   PROVIDE( _heap_end = . + 0x400);   /* 定义heap结束位置,长度为1KB */ 
     4  
     5   .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
     6     {
     7         . = ALIGN(4);
     8         PROVIDE(_susrstack = . );
     9         /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/
    10         . = . + __stack_size;
    11         PROVIDE( _eusrstack = .);
    12     } >RAM 
    复制代码

     

参考:
https://github.com/riscv/riscv-gnu-toolchain/issues/571
https://github.com/lowRISC/ibex/issues/1415

posted @   Zhu_zzzzzz  阅读(231)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示