简介
本系列研究基于RTX-4.3.0版本。硬件平台为cortex-m0内核。编译器采用ARMCC,版本为:ARM Compiler 5.05 update 2 (build 169)。
在开始深入研究RTX源码前,有必要对一些代码上的细节做一些整理和解释。
cortex复位
在离开复位状态后,CM3 做的第一件事就是读取下列两个32 位整数的值:
- 从地址 0x0000,0000 处取出MSP 的初始值。
- 从地址 0x0000,0004 处取出PC 的初始值——这个值是复位向量,LSB 必须是1。然 后从这个值所对应的地址处取指。
LR寄存器
R14 是连接寄存器(LR)。在一个汇编程序中,你可以把它写作both LR 和R14。LR 用于 在调用子程序时存储返回地址。例如,当你在使用BL(分支并连接,Branch and Link)指令时, 就自动填充LR 的值。
cortex栈
我们已经知道了CM3 的堆栈是分为两个:主堆栈和进程堆栈,CONTROL[1]决定如何选 择。
当CONTROL[1]=0 时,只使用MSP,此时用户程序和异常handler 共享同一个堆栈。这 也是复位后的缺省使用方式,如下图:
当CONTROL[1]=1 时,线程模式将不再使用PSP,而改用MSP(handler 模式永远使用 MSP),此时,进入异常时的自动压栈使用的是进程堆栈,进入异常handler 后才自动改为MSP,退出 异常时切换回PSP,并且从进程堆栈上弹出数据,如下图:
在进入ISR 时,CM3 会自动把一些寄存器压栈,这里使用的是进入ISR 之前使用的SP 指针(MSP 或者是PSP)。离开ISR 后,只要ISR 没有更改过CONTROL[1],就依然使用先前 的SP 指针来执行出栈操作。
为了避免系统堆栈因应用程序的错误使用而毁坏,你可以给应用程序专门配一个堆栈, 不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用PSP, 而异常服务例程则使用MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程 时由硬件处理。
cortex中断
Cortex‐M3 为了缩短中断延迟,会自动的现场保护和恢复。
进入
当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:
-
入栈: 把8个寄存器的值压入栈:
响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及 R3‐R0由硬件自动压入适当的堆栈中,并且在返回时自动弹出它们:如果当响应异常时,当前的代码正在使用PSP,则压入 PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用 主堆栈。
-
取向量:从向量表中找出对应的服务程序入口地址
当数据总线(系统总线)正在为入栈操作而忙得团团转时,指令总线(I‐Code总线)可 不是凉快地坐着看热闹——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表 中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线 的好处:入栈与取指这两个工作能同时进行。
-
选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:
-
SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后, 将由MSP负责对堆栈的访问。
-
PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
-
PC:在向量取出完毕后,PC将指向服务例程的入口地址。
-
LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”, 并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4 位则有另外的含义见下面
返回值
章节。
-
返回
当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先 前的系统状态,才能使被中断的程序得以继续执行。用先前储的LR的值就可以返回。
-
出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指 针的值也改回去。
-
更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若 中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
有些处理器使用特殊的返回指令来标示中断返回,例如8051就使用reti。但是在CM3中, 是通过把EXC_RETURN往PC里写来识别返回动作的。因此,可以使用上述的常规返回指令, 从而为使用C语言编写服务例程扫清了最后的障碍(无需特殊的编译器命令,如__interrupt)。
返回值
在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,这 是一个高28位全为1的值,只有[3:0]的值有特殊含义,如下图所示,当异常服务例程把这个 值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要 没有特殊需求,就不要改动它。
上图整理后再看如下:
由EXC_RETURN的格式可见,你不能把0xFFFF_FFF0‐0xFFFF_FFFF中的地址作为任何返回地 址。其实也并不用担心会弄错,因为CM3已经把这个范围标记成“取指不可区”了。
主栈响应ISR时序图如下:
线程栈响应ISR时序图如下:
其它
细心的读者一定在猜测:为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?原来,在ARM 上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》, AAPCS, Ref5)。其中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使 用被入栈的寄存器来保存中间结果,当然,如果程序过大也可能要用到R4‐R11,此时编译器 负责生成代码来push它们。但是,ISR应该短小精悍,不要让系统如此操心。
当主调函数需要传递参数(实参)时,它们 使用R0‐R3。其中R0传递第一个,R1传递第2个……在返回时,把返回值写到R0中,如果返回值大于4个字节,也可以放在R0、R1、R2、R3寄存器,最多也就支持4个寄存器,再多就要放在栈里了。在子程 序中,可以随心所欲地使用R0‐R3,以及R12。但若 使用R4‐R11,则必须在使用之前先PUSH它们,使用后POP回来。
SVC_X_X宏
初次看代码有很多调用使用了SVC_X_X宏,如SVC_0_1(f,t,…),SVC_1_0(f,t,t1,…)等。
SVC_X1_X2:其中X2代表参数个数,X2代表返回参数个数。
用系统初始化的api举例来说:
#define SVC_0_1(f,t,...) \
__svc_indirect(0) t _##f (t(*)()); \
t f (void); \
__attribute__((always_inline)) \
static __inline t __##f (void) { \
return _##f(f); \
}
.
.
.
SVC_0_1(svcKernelInitialize, osStatus, RET_osStatus)
.
.
.
// Kernel Control Public API
/// Initialize the RTOS Kernel for creating objects
osStatus osKernelInitialize (void) {
if (__get_IPSR() != 0) return osErrorISR; // Not allowed in ISR
if ((__get_CONTROL() & 1) == 0) { // Privileged mode
return svcKernelInitialize();
} else {
return __svcKernelInitialize();
}
}
咋一看,不懂这到底是怎么运行的,让我们分解下知识点。
osKernelInitialize是用户API入口,这点毋庸置疑。
svcKernelInitialize是RTX内核启动入口。
__get_IPSR()
这个函数在core_cmFunc.h被定义,获取IPSR中断状态寄存器的值:
__STATIC_INLINE uint32_t __get_IPSR(void)
{
register uint32_t __regIPSR __ASM("ipsr");
return(__regIPSR);
}
__get_CONTROL()
同上,获取control控制寄存器的值:
__STATIC_INLINE uint32_t __get_CONTROL(void)
{
register uint32_t __regControl __ASM("control");
return(__regControl);
}
__svcKernelInitialize()
这玩意是个什么鬼?不急,我们慢慢分析它。
在SVC_0_1宏定义中有好几个关键字不熟悉,查查手册,把它就出来:
-
__svc_indirect:ARMCC关键字,作用于函数,用于在SVC异常中传送指定的值给R12寄存器。
Syntax:
__svc_indirect(int svc_num) return-type function-name(int real_num[, argument-list]);
Examples:
int __svc_indirect(0) ioctl(int svcino, int fn, void *argp);
Calling:
//compiles to SVC #0 with IOCTL+4 in r12. //不像普通函数,必须定义函数体,这个关键词修饰后可以直接编译。 ioctl(IOCTL+4, RESET, NULL);
-
attribute((always_inline)) :ARMCC关键字,作用于函数,指定函数必须以内联方式编译。
-
__inline:ARMCC关键字,类似于C语言中的inline,只是inline在C90版本中不适用。
SVC_0_1
现在可以把SVC_0_1(svcKernelInitialize, osStatus, RET_osStatus)这个宏给展开了看看了:
__svc_indirect(0) osStatus _svcKernelInitialize (osStatus(*)()); //定义一个SVC调用函数,参数为函数指针。
osStatus svcKernelInitialize (void); //申明需要在SVC中断中调用的函数。
__attribute__((always_inline)) static __inline osStatus __svcKernelInitialize (void) {
return _svcKernelInitialize(svcKernelInitialize); //使用SVC调用函数包装下。
}
现形
分析完有关的知识点,我们可以整体来看看这段代码的真面目了,上汇编,直接看我的汇编注释:
osKernelInitialize PROC
;;;307 /// Initialize the RTOS Kernel for creating objects
;;;308 osStatus osKernelInitialize (void) {
0001ee b490 PUSH {r4,r7} ;进入函数压栈
0001f0 f3ef8005 MRS r0,IPSR ;使用MRS操作码获取IPSR寄存器值给R0,因为IPSR寄存器不能直接获取,只能通过MRS操作码获取。
;;;309 if (__get_IPSR() != 0) return osErrorISR; // Not allowed in ISR
0001f4 2800 CMP r0,#0 ;与0比较,因为IPSR是中断状态寄存器,如果不为0代表当前已经进入某个中断,而osKernelInitialize不能在中断服务中初始化。
0001f6 d002 BEQ |L1.510| ;相等即跳转到 下面的|L1.510|
0001f8 2082 MOVS r0,#0x82 ;0x82使用#修饰,表示立即数,osErrorISR值确实是0x82,把R0写入0x82。
|L1.506|
;;;310 if ((__get_CONTROL() & 1) == 0) { // Privileged mode
;;;311 return svcKernelInitialize();
;;;312 } else {
;;;313 return __svcKernelInitialize();
;;;314 }
;;;315 }
0001fa bc90 POP {r4,r7} ;出栈,恢复现场
0001fc 4770 BX lr ;链接地址保存在LR寄存器中,BX即跳转命令,因此意识为返回函数。
|L1.510|
0001fe f3ef8014 MRS r0,CONTROL ;获取CONTROL寄存器的值,保存在R0寄存器中。
000202 07c0 LSLS r0,r0,#31 ;310 ;R0寄存器值向左移31位,并判断是否为0。为0代表是特权模式,可以直接调用,为1代表线程模式,必须通过SVC调用。
000204 d003 BEQ |L1.526| ;相等即跳转到下面的|L1.526|
000206 4f8b LDR r7,|L1.1076| ;把|L1.1076|地中的中的32位数据放进R7寄存器。
000208 46bc MOV r12,r7 ;313 ;把R7中的值给R12寄存器。
00020a df00 SVC #0x0 ;313 ;执行系统调用指令,调用0号服务。
00020c e7f5 B |L1.506| ;跳转到|L1.506|
|L1.526|
00020e bc90 POP {r4,r7} ;311 ;出栈
000210 e7fe B svcKernelInitialize ; 直接调用svcKernelInitialize函数。
;;;316
ENDP
.
.
.
|L1.1076|
DCD svcKernelInitialize
总结
经过以上分析,发现使用SVC_XX宏其实也就是将一个函数的调用延时到SVC中断里执行。
os_InRegs宏
在获取事件,信号量,邮箱,互斥量函数前都有这个宏定义,我们来看看是什么东西:
#if defined (__CC_ARM)
#define os_InRegs __value_in_regs // Compiler specific: force struct in registers
#else
#define os_InRegs
#endif
因为我们是在armcc平台编译,因此需要看看__value_in_regs关键字。
官方定义为:
The __value_in_regs qualifier instructs the compiler to return a structure of up to four integer words in integer registers or up to four floats or doubles in floating-point registers rather than using memory.
我们先来一段测试代码:
typedef struct {
int32_t data1;
int32_t data2;
int32_t data3;
} STRUCT_TST;
__value_in_regs STRUCT_TST tst()//
{
STRUCT_TST d;
d.data1 = 1;
d.data2 = 2;
d.data3 = 3;
return d;
}
int main (void) {
STRUCT_TST a = tst();
a.data1 += 1;
a.data2 += 1;
a.data3 += 1;
}
我们来解析下这段汇编:
THUMB
AREA ||.text||, CODE, READONLY, ALIGN=1
tst PROC
;;;57
;;;58 __value_in_regs STRUCT_TST tst()
000000 b50e PUSH {r1-r3,lr}
;;;59 {
;;;60 STRUCT_TST d;
;;;61 d.data1 = 1;
000002 2001 MOVS r0,#1 ;把1给R0寄存器
000004 9000 STR r0,[sp,#0];将R0寄存器中的值保存在SP+0地址的内存中
;;;62 d.data2 = 2;
000006 2002 MOVS r0,#2 ;把2给R0寄存器
000008 9001 STR r0,[sp,#4] ;将R0寄存器中的值保存在SP+4地址的内存中
;;;63 d.data3 = 3;
00000a 2003 MOVS r0,#3;把3给R0寄存器
00000c 9002 STR r0,[sp,#8] ;将R0寄存器中的值保存在SP+8地址的内存中
;;;64 return d;
00000e 4668 MOV r0,sp;把sp寄存器的值给R0寄存器
000010 c807 LDM r0,{r0-r2};把sp寄存器的值的0地址偏移的值给R0,4地址偏移的值给R1,8地址偏移的值给R1
;;;65 }
000012 b003 ADD sp,sp,#0xc;栈增加12个字节,也就是3个寄存器的大小
000014 bd00 POP {pc};出栈返回
;;;66
ENDP
main PROC
;;;67 int main (void) {
000016 b50e PUSH {r1-r3,lr}
;;;68 STRUCT_TST a = tst();
000018 f7fffffe BL tst
00001c 9202 STR r2,[sp,#8];将R2寄存器中的值保存在SP+8地址的内存中
00001e 9101 STR r1,[sp,#4];将R1寄存器中的值保存在SP+4地址的内存中
000020 9000 STR r0,[sp,#0];将R0寄存器中的值保存在SP+0地址的内存中
;;;69 a.data1 += 1;
000022 9800 LDR r0,[sp,#0];将保存在SP+0地址的内存中的值加载到R0寄存器中
000024 1c40 ADDS r0,r0,#1;R0寄存器值加1
000026 9000 STR r0,[sp,#0];将R0寄存器中的值保存在SP+0地址的内存中
;;;70 a.data2 += 1;
000028 9801 LDR r0,[sp,#4];将保存在SP+4地址的内存中的值加载到R0寄存器中
00002a 1c40 ADDS r0,r0,#1;R0寄存器值加1
00002c 9001 STR r0,[sp,#4];将R0寄存器中的值保存在SP+4地址的内存中
;;;71 a.data3 += 1;
00002e 9802 LDR r0,[sp,#8];将保存在SP+8地址的内存中的值加载到R0寄存器中
000030 1c40 ADDS r0,r0,#1;R0寄存器值加1
000032 9002 STR r0,[sp,#8];将R0寄存器中的值保存在SP+8地址的内存中
;;;72 }
000034 2000 MOVS r0,#0
000036 bd0e POP {r1-r3,pc}
;;;73
ENDP
AREA ||.data||, DATA, ALIGN=2
||st||
DCD 0x00000000
__ARM_use_no_argv EQU 0
在tst函数前加不加__value_in_regs关键字,来进行对比:
经过上图,我们发现加了__value_in_regs关键字,编译器会保证返回值从R0开始放,4字节返回值就放在R0,8字节返回值就放在R0、R1,依次类推,最多支持4个寄存器(亲测>=5个寄存器__value_in_regs就失效)。
没有加__value_in_regs关键字,编译器为了优化,不会按上面的规则放置返回值,其中的顺序也是随机的。
至于讲解这个的作用,会在事件,信号量,邮箱等带返回值的地方起作用。