以前在mcu编程的时候没有太注意堆栈的情况,只知道需要将堆栈设置的大一点。现在逐步使用freertos,在freertos中也有关于堆栈的设置,freertos的堆栈和启动文件中的堆栈关系是什么?为了以后使用的无误,本次一次性把这些弄清楚。
1、定义
堆栈是一个特定的存储区或者寄存器。一般在内存总开辟一块区域作为堆栈,叫做软件堆栈;用寄存器构成的堆栈,叫硬件堆栈。大多数情况下,我们使用的都是软件堆栈。
在stm或者gd32的启动文件中的堆栈就是软件堆栈。
堆栈中数据的存储,都要遵循先进后出的原则,可以类比为木桶,先放进去的数据在桶的底部,后放进去的数据在桶的顶部。取数据的时候是先在桶的顶部取数据,在从桶的底部取数据。
单片机应用中,堆栈是个特殊存储区,堆栈属于RAM空间的一部分,堆栈用于函数调用、中断切换时保存和恢复现场数据。堆栈中的物体具有一个特性:第一个放入堆栈中的物体总是被最后拿出来, 这个特性通常称为先进后出 (FILO—First-In/Last-Out)。 堆栈中定义了一些操作, 两个最重要的是PUSH和POP。 PUSH(入栈)操作:堆栈指针(SP)加1,然后在堆栈的顶部加入一 个元素。POP(出栈)操作相反,出栈则先将SP所指示的内部ram单元中内容送入直接地址寻址的单元中(目的位置),然后再将堆栈指针(SP)减1。这两种操作实现了数据项的插入和删除。
关于堆栈的理论知识就简述这么多,部分来自于百度百科。在mcu中我们只要知道上图设置的堆栈实际是位于mcu的ram区就可以了。在mcu编程中,貌似对ram的关注都不多,更多的是关注flash的容量,falsh大小能不能存储的了我的固件之类的。对ram就没有知道有这个东西,但真没有细致关注。
2、堆栈的空间分配
在mcu中,heap和stack的使用者是不同的。 stack(栈):由系统自动分配释放,存放的函数的参数值,局部变量的值。这个空间用户操作不了的。
heap(堆):由用户分配及释放,也就是调用malloc 和free,时操作的空间就是堆空间。
站原子论坛上,建议,如果没有调用系统的malloc和free函数,heap空间可以设置为0.
从空间分配上,最好理解的就是heap,完全由用户操作。比较难理解的是stack,那么系统到底是怎么使用的栈空间的呢。
3、stm32栈空间及编译的固件大小
用keil编译工程完成后,output窗口会显示如下信息: 上图的中的code ro-data rw-data zi-data是什么意思,哪些是站ram空间,哪些是站flash空间呢?在上文中我们说到stack是有系统自动分配的,那么我把启动文件中的stack_size改变一下,再看下编译输出结果。 将stack_size 由0x2000改为0x1000,有8k字节改为4k字节。编译后的结果如下图。
与上一次的编译对比发现,只有zi-data段变化了由10508变成了6412.二者的差值刚好是4096,就是stack_size的减少值。
在将heap_size设置为0x1000,发现编译出来的没有任何变化。
3.1 存储数据段
Code是代码占用的空间;
RO-data是 Read Only 只读常量的大小,如const型;
RW-data是(Read Write) 初始化了的可读写变量的大小;
ZI-data是(Zero Initialize) 没有初始化的可读写变量的大小。
最终,烧写时flash被占有的空间为:
falsh = Code + RO-Data + RW-Data
程序运行时ram被占有的空间为
** ram = RW-Data + ZI-Data**
计算一下falsh占有空间 = 12568+ 368+ 180 = 13116字节 看下最终生成的bin文件刚好是13116字节大小。
找到了flash占用的空间,我们再来看看ram占用的空间。ram占用空间在生成的.map文件中。 map文件中显示ram占用的size为0x19c0,转换为10进制为6592,而生成RW-Data + ZI-Data=180+6412=6592.这个也得到了验证。
根据上面的图还可以得出:
** RW-Data= .data** data的空间累加即为RW-Data ** ZI-Data= .bss+STACK** .bss的空间累加+STACK即为ZI-Data
上图中也可以看出STACK=0x1000刚好是在启动文件中设定的值。也可以看出当前芯片的ram总空间为0x18000=96kB。
到这里我们基本梳理出来的ram和falsh空间的构成。接下来看看freertos的堆栈是啥。
3.2freertos堆栈
我使用的是freertos是v10版本,内存分配采用的heap4.c 在freertos的FreeRTOSConfig.h中,需要设置堆的大小
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 1 * 1024 ) )
这里做测试我就只设置为1024字节。全局搜索下configTOTAL_HEAP_SIZE ,发现是在heap_4.c中使用了。 按照当前的设置可以看到,freertos是直接定义了一个静态数组 hcHeap大小为configTOTAL_HEAP_SIZE。
configAPPLICATION_ALLOCATED_HEAP这个宏定义一般是将堆设置为外部sram才会用到。这里暂时不多讲。
从上图可以得出这样的结论,默认情况下,freertos的堆就是自定义的一个大型数组,与启动文件中设置heap_size没有任何关系。所以只要只要没有调用系统的malloc函数,启动文件heap_size还真可以设置为0.
接下来,将configTOTAL_HEAP_SIZE改成20Kb进行编译。结果如下 zi-data由6142变成了25868,二者差值刚好是19kB。编译后的map文件内容如下:
与上一次的对比,.bss段的heap_4.o占用的空间由0x400(1kB)变成了0x5000(20kB),刚好是configTOTAL_HEAP_SIZE宏定义设定的大小。
4、结论
经过了上面的实验分析可以得出如下结论:
- 当freertos采用heap_4内存分配方案时,stm32启动文件中的stack_size 和heap_size与freertos中设置的堆大小没有任何关系。
- 只要代码中没有使用系统malloc函数,启动文件heap_size可以设置为0
- mcu运行时的ram空间= RW-Data+ZI-Data+启动文件中的heap_size,所以可以根据这个公式来设置freertos堆的大小。freertos的堆尽量要设置的大一点。
5、freertos堆常用函数heap_4
上还是那个图来自freertos的官方文档,可以看出freertos的栈实际实际上也是放到了堆空间。任务控制块TCB、queue 、pvPortMalloc等使用都是堆空间,对空间大小有configTOTAL_HEAP_SIZE决定。
5.1常用heap相关函数
获取剩余heap空间大小。
size_t xPortGetFreeHeapSize( void );
获取最小未分配的空间大小
size_t xPortGetMinimumEverFreeHeapSize( void );
动态内存分配及释放函数
void * pvPortMalloc( size_t xWantedSize )
void vPortFree( void * pv )
5.2任务栈空间占用的大小。
freertos中每个任务新建的时候都要设置栈空间。栈空间可以在FreeRTOSConfig.h中设定一个最小值
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
记住这里设定的和任务新建时设定的大小单位并不是字节,而是字,字占4个字节,所以上述设定的最小栈空间为256字节。由上文可知,freertos的栈空间实际是在堆里面。
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) )
#define pvPortMallocStack pvPortMalloc
在新建任务函数中,可以看到,实际上使用pvPortMalloc分配的一段空间作为栈空间,大小为usStackDepth *sizeof( StackType_t ) 。而StackType_t 大小实际上是uint32_t 为4字节。
|