RTOS Thread stack and MSP/PSP registers in ARM Cortex-M3
Background
使用Keil RTX RTOS的项目开发过程中,在加入一些新的代码之后,发现在线程们被创建并被启动之后,程序就跑飞了。
借助Keil的RTOS debug窗口,发现有其中2个线程有stack overflow的现象。
于是开始思考RTOS thread stack size的设置问题。
以前一直就对有了RTOS之后,线程栈和内核栈是个什么情况。Cortex-M3的MSP和PSP该如何使用,这些都不是很清楚。
正好借此机会,好好研究一番。
本文基于以下开发环境:
Cortex-M3,Keil MDK 5。
C memory Modle
先上一张图,看看一般意义上的无OS情况下,ARM C语言执行环境下,RAM的布局。明显这张图是基于“向下增长”的栈结构来设计的。
从上图可以看到:
RW空间,位于RAM的最低地址区域。用来存放,已经被初始化了的,可读写的,全局变量的值;(提个问题,到底什么叫全局变量,什么叫全局变量的内容?)
ZI空间,位于RW的上方。用来存放,未被初始化的,可读写的全局变量的值;
Heap空间,位于ZI的上方。c函数malloc和calloc等内存分配函数,会从这块区域里面取一块内存区域,给函数的调用者;
Stack空间,位于RAM的最上方。C函数的自动变量(也叫局部变量),以及函数返回地址,会被保存在这里。
Stack空间从高位向低位增长,Heap从低位向高位增长。所以如果控制不好,会出现Heap和Stack overlap的情况。
可以参考:http://stackoverflow.com/questions/39113658/when-does-malloc-return-null-in-a-bare-metal-environment
那么这个c memory model是怎么形成的呢?
当我们写好了一个c程序,开始build这个程序。
这个程序会被预处理、编译、链接,形成一份image。编译器会分辨出:代码指令,常量,已初始化的可读写全局变量,未初始化的可读写全局变量,自动变量,内存分配函数。
链接脚本(link script)会告诉链接器,在链接的时候这些东西在flash里面放哪里,在RAM里面放哪里。
那么我们关心的栈,是在哪里设置栈顶是在RAM的最高位,而heap在ZI的上面呢?
CPU里面有个SP寄存器,又叫栈指针寄存器,指向程序运行时栈的实时位置。
往栈里面PUSH东西啦,SP寄存器的数值就减小;从栈里面POP东西啦,SP寄存器的数值就增加。
在CPU刚开始复位,开始从复位中断向量执行第一条指令的时候,都是汇编,但我们也得先把CPU初始化、栈指针初始化啊,这样后面的c程序才能开始跑。
一般在这里,我们都是通过汇编指令设置SP寄存器为RAM的最高地址,也就是指定了栈顶在哪里。
而Heap底部,则是链接器自动根据RW/ZI空间大小计算出来的,等于ZI空间的顶部。
Keil MDK下Cortex-M3的C Memory Model, without RTOS
那么在Keil MDK下,Cortex-M3的C memory model又是个什么样子呢。
为了说明,先上图。
在不跑RTOS的情况下,这个时候,整个C程序都只会用到main stack,为什么?请去翻翻cortex-m3 技术手册。
当然高级一点,可以自己去设置process stack,然后让用户程序在process stack里面跑,让中断函数再main stack里面跑。
先看没有RTOS,并且代码里面调用了malloc或者calloc这类内存分配函数的情况:
+--------+ Last Address of RAM | not |
| used | +--------+ MSP RAM | main |
| stck | | | +--------+ Heal_limit | ^ | | | | | Heap | +--------+ | ZI | +--------+ | RW | +========+ First Address of RAM
从图上可以看到RAM的顶部有一段空间,是没有被c程序用到的,相当于浪费掉了。
在这段空间的下面才是Main stack,MSP指向了这段空间的最上面。
Heap 在main stack的下面。
startup.s里面设置了heap和main stack的size,Keil toolchain会计算heap的起始地址,main stack的起始地址,防止出现heap和stack overlap的情况。
设置见下图的“Stack Configuration”和“Heap Configuration”,默认都是0x400 = 1k bytes。
以后c程序,就从heap里面去取memory,往main stack里面存自动变量和函数返回地址。
注意,如果你的c代码里面没有调用malloc这类函数,那么是不会最后生成的image,去看map file,是不会有heap这段空间的。
我们来看看没有RTOS的时候,并且没有调用malloc这类函数,最后image的map file是什么样子的。没错,没有heap,只有stack。
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000418, Max: 0x00002000, ABSOLUTE) Base Addr Size Type Attr Idx E Section Name Object 0x20000000 0x00000004 Data RW 4 .data main.o 0x20000004 0x00000014 Data RW 236 .data system_gd32f1x0.o 0x20000018 0x00000400 Zero RW 3328 STACK startup_gd32f1x0.o
那我们再来看看没有RTOS的时候,有调用malloc这类函数,最后image的map file是什么样子的。看到没有,有heap,也有stack。并且size都是在之前的startup.s中设置的。
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000820, Max: 0x00002000, ABSOLUTE) Base Addr Size Type Attr Idx E Section Name Object 0x20000000 0x00000004 Data RW 4 .data main.o 0x20000004 0x00000014 Data RW 239 .data system_gd32f1x0.o 0x20000018 0x00000004 Data RW 3387 .data mc_w.l(mvars.o) 0x2000001c 0x00000004 Data RW 3388 .data mc_w.l(mvars.o) 0x20000020 0x00000400 Zero RW 3332 HEAP startup_gd32f1x0.o 0x20000420 0x00000400 Zero RW 3331 STACK startup_gd32f1x0.o
再来看这段调用malloc代码经过Keil编译之后,统计的code大小,data大小。
看到没有RW size = 2080 bytes。而上面显示的所有RAM中被用到的空间大小为0x00000820 = 2080 bytes。
是的,ZI Data包含了stack和heap空间的。所以keil编译出来的结果,就是最后ram占用的空间大小。而这个和c memory model里面讲的是不一样的。
============================================================================== Total RO Size (Code + RO Data) 1400 ( 1.37kB) Total RW Size (RW Data + ZI Data) 2080 ( 2.03kB) Total ROM Size (Code + RO Data + RW Data) 1432 ( 1.40kB) ==============================================================================
Keil MDK下Cortex-M3的C Memory Model, with Keil RTX RTOS
在有了RTOS之后,就多了RTOS内核和线程。RTOS内核代码,中断函数,以及启动代码,使用main stack。线程使用process stack。
整个C程序就有2个栈指针寄存器,MSP/PSP。MSP指向main stack的top address,PSP指向线程stack的top address。
于是就有了下面的图。
可以看到heap在main stack的下面,process stack在heap的下面。
process stack其实是RTOS代码里面的一个全局数组。
每创建一个线程,就从数组里面拿一块空间作为这个线程的栈空间。
这个数组的大小,由RTX_Conf_CM.C里面的stack number,以及每个stack的栈大小这两者的乘积决定。
- 当从线程陷入到RTOS内核时,就使用MSP;
- 当在RTOS内核中,需要从线程1调度掉线程2时,则修改PSP,将其值从线程1的栈顶,修改到线程2的栈顶。
+--------+ Last Address of RAM | not |
| used | +--------+ MSP RAM | main |
| stck | | | +--------+ Heal_limit | ^ | | | | | Heap | +--------+ PSP
| Process|
| Stack |
| |
+--------+ | ZI | +--------+ | RW | +========+ First Address of RAM
我们再来看看有RTOS的时候,map file是什么样子的吧。因为内容比较多,只截取了其中一部分:
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x000018a8, Max: 0x00002000, ABSOLUTE, COMPRESSED[0x00000084])
Base Addr Size Type Attr Idx E Section Name Object
0x2000016c 0x00000001 Data RW 7640 .data RTX_CM3.lib(hal_cm.o) 0x2000016d 0x00000003 PAD 0x20000170 0x00000008 Data RW 7684 .data RTX_CM3.lib(rt_robin.o) 0x20000178 0x00000004 Data RW 8029 .data mc_w.l(stdout.o) 0x2000017c 0x0000003c Zero RW 250 .bss pse_hostcomm.o 0x200001b8 0x000001b0 Zero RW 484 .bss pse_pwrmgmt.o 0x20000368 0x00000010 Zero RW 6324 .bss thread.o 0x20000378 0x00000010 Zero RW 6325 .bss thread.o 0x20000388 0x00000010 Zero RW 6326 .bss thread.o 0x20000398 0x00000020 Zero RW 6401 .bss rtx_conf_cm.o 0x200003b8 0x00000110 Zero RW 6402 .bss rtx_conf_cm.o 0x200004c8 0x00000c90 Zero RW 6403 .bss rtx_conf_cm.o 0x20001158 0x00000250 Zero RW 6404 .bss rtx_conf_cm.o 0x200013a8 0x00000084 Zero RW 6405 .bss rtx_conf_cm.o 0x2000142c 0x00000014 Zero RW 6406 .bss rtx_conf_cm.o 0x20001440 0x00000034 Zero RW 7141 .bss RTX_CM3.lib(rt_task.o) 0x20001474 0x00000030 Zero RW 7361 .bss RTX_CM3.lib(rt_list.o) 0x200014a4 0x00000004 PAD 0x200014a8 0x00000400 Zero RW 6457 STACK startup_gd32f1x0.o ==============================================================================
这个里面标STACK的,其实是main stack,采用默认值为0x400. 而下面这段则是所有线程栈空间的总和:
0x200004c8 0x00000c90 Zero RW 6403 .bss rtx_conf_cm.o
Keil的统计数据如下,其中RW size刚好等于上面所占的RAM空间大小:Size = 0x000018a8 = 6312 bytes。也就是所Keil给出的结果,RW data + ZI data就是最后代码运行时占用的RAM大小,包含了所有的stack以及RW/ZI data空间。
============================================================================== Total RO Size (Code + RO Data) 38196 ( 37.30kB) Total RW Size (RW Data + ZI Data) 6312 ( 6.16kB) Total ROM Size (Code + RO Data + RW Data) 38328 ( 37.43kB) ==============================================================================