STM32F103移植FreeRTOS必须搞明白的系列知识—3(堆栈)

STM32F103移植FreeRTOS必须搞明白的系列知识—1(Cortex-CM3中断优先级)

STM32F103移植FreeRTOS必须搞明白的系列知识—2(FreeRTOS任务优先级)

STM32F103移植FreeRTOS必须搞明白的系列知识—3(堆栈)

STM32F103移植FreeRTOS必须搞明白的系列知识—4(FreeRTOSConfig.h配置文件)
 

目录

一、堆和栈概念

        堆栈的示例

二、裸机编程时的堆栈问题

        1、栈

        2、堆

        3、裸机时STM32(KEIL)ram(内存)分布情况

        4、KEIL编译器分配的栈和堆

三、FreeRTOS编程时的堆栈问题

        1、系统栈

        2、用户栈(任务栈)

四、MSP主堆栈指针和PSP进程堆栈指针

        1、MSP指针

        2、PSP指针

五、系统栈大小的确定

        1、64 字节

        2、200 字节

六、任务栈大小的确定

七、函数栈大小确定

八、栈溢出

九、FreeRTOS的栈溢出检测机制

        1、方法一

        2、方法二


一、堆和栈概念

        虽然“堆栈“这个词大多数时候是连在一起使用的,但堆和栈其实是不同的概念。

栈(stack):由编译器自动分配和释放,如存放函数的参数值,局部变量的值等
堆(heap):一般由程序员分配和释放,分配方式类似于数据结构中的链表

        栈的空间有限,堆有很大的自由存储区(最大值由SRAM区决定)。通常我们习惯用malloc和free等API申请分配和释放堆上的空间,但频繁使用会造成大量的内存碎片进而降低系统的整体性能。同时这些API不是线程安全的,有机率会导致系统的不稳定。 

        堆栈的示例

        如上图的示例程序所示,全局变量和常量属于静态区(Static),由编译器事先分配好生命周期贯穿整个程序;函数的参数值,局部变量的值属于栈(Stack),由编译器自动分配和释放程序员用malloc函数动态请求分配的内存空间属于堆(Heap)值得注意的是,如果在动态分配的内存用完之后忘记使用free函数释放内存,则会导致内存泄漏,并且当堆和栈无止境的增长到互相覆盖对方区域时则会出现很多无法预料的问题。程序可能运行着就跑飞了。 

二、裸机编程时的堆栈问题

        内存被分为许多区域,其中包含堆和栈,当然还有其他一些区域,见内存各区域区别

        1、栈

        栈中存储的是函数调用的形参、非静态局部变量以及函数调用信息。栈由编译器自动进行管理,无需人为干预。如果函数的局部变量定义大小过大(比如定义在函数内部定义超大型数组等),超过了用户定义的栈区大小将会发生溢出,程序将会崩溃!

        2、堆

        堆是用于动态内存分配的,由编程人员手动进行管理。使用malloc函数手动申请内存和使用free手动释放,申请的内存在使用结束之后没有得到释放将会产生内存泄漏,导致程序崩溃!
        当使用malloc申请的内存大于堆剩余的容量时,将会导致申请失败!

        3、裸机时STM32(KEIL)ram(内存)分布情况

                          

        ram(内存)中包含了如下几个部分:
    1、data : 存放初始化为非 0 值的全局变量
    2、bss : 存放未初始化或者是初始化为 0 的全局变量
    3、堆 (heap) : 由malloc申请,由free释放(由程序员用malloc函数动态请求分配的内存空间,由程序员用free函数释放内存空间)。
    4、栈 (Stack) : 存放局部变量和函数调用时的返回地址(由编译器自动分配和释放)。
        其中:data和bss比较好理解就是一些全局变量。

        4、KEIL编译器分配的栈和堆

        以stm32为例,在Keil编译器工程项目的启动文件中定义栈和堆空间的大小,如下所示。

        裸机时Keil编译器工程项目的启动文件中定义的栈为:系统栈+用户栈(2个合二为一,作为一个整体)。系统栈+用户栈:单片机系统(中断函数和中断嵌套)使用这个栈区进行入栈和出栈操作;系统栈+用户栈:用户程序(函数调用的形参、非静态局部变量以及函数调用信息)也使用这个栈区进行入栈和出栈操作。       

总结: 

        裸机时Keil编译器工程项目的启动文件中定义的栈:系统栈+用户栈,系统栈和用户栈合二为一,作为一个整体。

        博主使用的是STM32F103芯片是32位单片机,所以栈和堆的基本单位是WORD(32位),栈设置为0x800,表示0x800 *4 (字节);堆设置为0x200,表示0x200 *4 (字节)。

提示: 

        STM32F103芯片是32位单片机,千万不要认为给堆分配的0x200是512字节哈!!!,而应该是0x0200=(0x0200*4)字节哈!       

        为了满足项目需求,可以按需对堆和栈空间进行调整。当然,调整的大小不能超过内存RAM总容量。     

提示:        

        裸机时Keil编译器工程项目的启动文件中定义的栈为:系统栈和用户栈。就是说,单片机系统(中断函数和中断嵌套)在使用这个区域进行入栈和出栈操作;用户程序(函数调用的形参、非静态局部变量以及函数调用信息)也在使用这个区域进行入栈和出栈操作。

三、FreeRTOS编程时的堆栈问题

        在FreeRTOS下,stm32的栈被分为:系统栈和任务栈。

        1、系统栈

        在FreeRTOS下以stm32为例,Keil编译器工程项目的启动文件中定义栈和堆空间的大小,如下所示。

        FreeRTOS下Keil编译器工程项目的启动文件中定义的栈为:系统栈。只有单片机系统(中断函数和中断嵌套使用系统栈)在使用这个栈区(系统栈)进行入栈和出栈操作,这个栈不包含用户栈。

提示:

        FreeRTOS下Keil编译器工程项目的启动文件中定义的栈仅仅为:系统栈 ,不包含用户栈。

        2、用户栈(任务栈)

        FreeRTOS下用户栈(任务栈)的总容量由FreeRTOSConfig.h文件中的宏定义configTOTAL_HEAP_SIZE确定,参见下图。

#define configTOTAL_HEAP_SIZE		( ( size_t ) ( 20 * 1024 ) )

        FreeRTOS内核heap_4.c中定义的用户栈(也称之为:HEAP区)存在于数据段(具体为.bss段),是一个静态数组,参见下图。

PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

         工程项目中创建的每一个任务的栈区(包括:每个任务的函数调用的形参、非静态局部变量以及函数调用信息)都是从 FreeRTOS内核的heap_4.c中定义HEAP区中进行申请和分配。

          这个HEAP(用户栈区)来自于静态数组,所以它存在于数据段(具体为.bss段), 它和Keil编译器工程项目的启动文件中定义的不是一个概念。

提示:

        FreeRTOS下Keil编译器工程项目的启动文件中配置的堆区Heap_Size EQU 0x200和FreeRTOS内核heap_4.c中的HEAP(堆区)并无任何关系。

           (1)、Keil编译器工程项目的启动文件中配置的堆区Heap_Size EQU 0x200使用malloc函数申请内存,使用free函数释放内存。

           (2)、FreeRTOS内核heap_4.c中的HEAP(堆区)位于内存的.bss段(参见下图),是FreeRTOS内核分配给任务的栈区(包括:每个任务的函数调用的形参、非静态局部变量以及函数调用信息)。

           (3)、裸机编程时,使用C语言的库函数malloc函数动态请求分配堆区(Keil编译器工程项目的启动文件中配置的堆区)空间,使用库函数free函数释放空间,没有任何问题。

           (4)、FreeRTOS编程时,使用C语言的库函数malloc函数动态请求分配堆区,对于微控制器并不是线程安全的。使用库函数free函数释放空间,对于微控制器并不是线程安全的。为了线程安全,在使用FreeRTOS编程时,进行动态内存申请(Keil编译器工程项目的启动文件中配置的堆区)时,请使用FreeRTOS提供的pvPortMallocvPortFree函数。

四、MSP主堆栈指针和PSP进程堆栈指针

        在FreeRTOS中维护着两个栈的指针,分别是MSP主堆栈指针(Main stack pointer)和PSP进程堆栈指针(Process stack pointer)。

        1、MSP指针

  • 用于操作内核以及处理异常和中断
  • 由编译器分配
  •        在FreeRTOS下,MSP指针专门用于系统栈区。(一旦进入了中断函数以及可能发生的中断嵌套都是用的 MSP 指针)。

            2、PSP指针

  • 用于每个任务的独立的栈指针
  • 在任务调度上下文切换(context switch)中,PSP会初始化为相对应的任务的栈指针。
  •        在FreeRTOS下,PSP指针专门用于用户栈(任务栈)区。(在 FreeRTOS 任务中,所有栈空间的使用都是通过PSP 指针进行指向的)。

    提示:

            通常MSP指针用于系统内核和中断服务函数(是为系统栈服务的),PSP指针用于用户的任务栈(是为任务服务的)。

                                                    MSP指针和PSP指针

    五、系统栈大小的确定

            在FreeRTOS编程模式下,实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况进行考虑,所有的寄存器都需要入栈,此时分为两种情况:

            1、64 字节

            对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需
    要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。
            可能发生几次中断嵌套就是要 64 乘以几即可。 当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
            (注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈) 。

            2、200 字节

            对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是(16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
            (注:任务执行的过程中发送中断的话,有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈)

    六、任务栈大小的确定

            在基于FreeRTOS 的应用设计中,每个任务都需要自己的栈空间,应用不同,每个任务需要的栈大小也是不同的。 将如下的几个选项简单的累加就可以得到一个粗略的栈大小:

    1、函数的嵌套调用,针对每一级函数用到栈空间的有如下四项:
          (1)、函数局部变量。
          (2)、函数形参。一般情况下函数的形参是直接使用的 CPU 寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数的话,这个存储了函数形参的 CPU 寄存器内容是要入栈的。 所以建议大家也把这部分算在栈大小中。
          (3)、函数返回地址。针对M3和M4内核的MCU,一般函数的返回地址是专门保存到 LR(LinkRegister)寄存器里面的,如果这个函数里面还调用了一个函数的话,这个存储了函数返回地址的 LR 寄存器内容是要入栈的。 所以建议大家也把这部分算在栈大小中。
          (4)、函数内部的状态保存操作也需要额外的栈空间。
    2、任务切换,任务切换时所有的寄存器都需要入栈,对于带 FPU 浮点处理单元的 M4 内核 MCU 来说,FPU 寄存器也是需要入栈的。

    3、针对M3内核和M4内核的MCU来说,在任务执行过程中,如果发生中断:

          (1)、M3内核的MCU有8个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈。 

          (2)、M4内核的MCU有8个通用寄存器和18个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈

    4、进入中断以后使用的局部变量以及可能发生的中断嵌套都是用的系统栈

            实际应用中将这些都加起来是一件非常麻烦的工作,上面这些栈空间加起来的总和只是栈的最小需求,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取 1.5-2。上面的计算是我们用户可以确定的栈大小,项目应用中还存在无法确定的栈大小,比如调用printf函数就很难确定实际的栈消耗。又比如通过函数指针实现函数的间接调用,因为函数指针不是固定的指向一个函数进行调用,而是根据不同的程序设计可以指向不同的函数,使得栈大小的计算变得比较麻烦。
            另外还要注意一点,建议不要编写递归代码,因为我们不知道递归的层数,栈的大小也是不好确定的。

    七、函数栈大小确定

            函数的栈大小计算起来是比较麻烦的, 那么有没有简单的办法来计算呢? 有的,一般 IDE 开发环境都有这样的功能,比如 MDK 会生成一个 htm 文件,通过这个文件用户可以知道每个被调用函数的最大栈需求以及各个函数之间的调用关系。但是 MDK 无法确定通过函数指针实现函数调用时的栈需求。另外,发生中断或中断嵌套时的现场保护需要的栈空间也不会统计。 

    八、栈溢出

            简单的说就是用户分配的栈空间不够用了,溢出了。 下面我们举一个简单的实例,栈生长方向从高地址向低地址生长(M4和M3是这种方式)。

            (1)、上图标识1的位置是FreeRTOS的某个任务调用了函数test()前的SP栈指针位置。 

                                             

            (2)、上图标识2的位置是调用了函数test 需要保存返回地址到栈空间。这一步不是必须的,对于M3和M4内核是先将其保存到LR寄存器中,如果LR寄存器中有保存上一级函数的返回地址,需要将LR寄存器中的内容先入栈。
            (3)、上图标识3的位置是局部变量inti和int array[10]占用的栈空间,但申请了栈空间后已经越界了。这个就是所谓的栈溢出了。如果用户在函数test中通过数组 array 修改了这部分越界区的数据且这部分越界的栈空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
            (4)、上图标识4的位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址+变量i+10个数组元素)*4=48个字节。 

            (5)、上图标识5的位置可能是其它任务的栈空间,也可能是全局变量或者其它用途的存储区,如果test函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃

    九、FreeRTOS的栈溢出检测机制

            FreeRTOS 提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行:

            1、方法一

            使用方法一需要用户在FreeRTOSConfig.h文件中配置如下宏定义:

    #define configCHECK_FOR_STACK_OVERFLOW 1 

            在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。

    void vApplicationStackOverflowHook( TaskHandle_t xTask,
    signed char *pcTaskName );

            用户可以在钩子函数里面做一些处理。这种方法不能保证所有的栈溢出都能检测到。比如任务在执行的过程中出现过栈溢出。任务切换前栈指针又恢复到了正常水平,这种情况在任务切换的时候是检测不到的。又比如任务栈溢出后,把这部分栈区的数据修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。

            2、方法二

            使用方法二需要用户在FreeRTOSConfig.h文件中配置如下宏定义:

    #define configCHECK_FOR_STACK_OVERFLOW 2

            任务创建的时候将任务栈所有数据初始化为 0xa5,任务切换时进行任务栈检测的时候会检测末尾的16个字节是否都是0xa5,通过这种方式来检测任务栈是否溢出了。相比方法一,这种方法的速度稍慢些,但是这样就有效地避免了方法一里面的部分情况。不过依然不能保证所有的栈溢出都能检测到,比如任务栈末尾的16个字节没有用到,即没有被修改,但是任务栈已经溢出了,这种情况是检测不到的。 另外任务栈溢出后,任务栈末尾的16个字节没有修改,但是溢出部分的栈区数据被修改了,这部分栈区的数据不重要或者暂时没有用到还好,但如果是重要数据被修改将直接导致系统进入硬件异常,这种情况下,栈溢出检测功能也是检测不到的。

    致谢:

    (17条消息) 【FreeRTOS】关于FreeRTOS中堆栈的一些思考_Liangtao`的博客-CSDN博客_freertos任务堆栈设置

    FreeRTOS —— 栈、堆、任务栈 – 走看看 (zoukankan.com)

    FreeRTOS —— 栈、堆、任务栈 – 流水灯 – 博客园 (cnblogs.com)

    (10条消息) STM32 内存分配、堆栈以及变量存储位置理解与分析_水水爱污污的博客-CSDN博客_stm32变量存储方式 freertos与STM32分析栈、堆、全局区、常量区、代码区、RAM、ROM,及如何分配堆栈空间_一剃解千愁的博客-CSDN博客_freertos 全局变量存放位置

    freeRTOS中文实用教程3–中断管理之中断嵌套 – 走看看 (zoukankan.com)

    FreeRTOS 从入门到精通4–堆栈管理知多少 – 知乎 (zhihu.com)

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32F103移植FreeRTOS必须搞明白的系列知识—3(堆栈)

    发表评论