(原创)RTOS-RTX分析系列之前提

2017/07/17 RTX

简介

本系列研究基于RTX-4.3.0版本。硬件平台为cortex-m0内核。编译器采用ARMCC,版本为:ARM Compiler 5.05 update 2 (build 169)。

在开始深入研究RTX源码前,有必要对一些代码上的细节做一些整理和解释。

cortex复位

在离开复位状态后,CM3 做的第一件事就是读取下列两个32 位整数的值:

  1. 从地址 0x0000,0000 处取出MSP 的初始值。
  2. 从地址 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 共享同一个堆栈。这 也是复位后的缺省使用方式,如下图:

img

当CONTROL[1]=1 时,线程模式将不再使用PSP,而改用MSP(handler 模式永远使用 MSP),此时,进入异常时的自动压栈使用的是进程堆栈,进入异常handler 后才自动改为MSP,退出 异常时切换回PSP,并且从进程堆栈上弹出数据,如下图:

img

在进入ISR 时,CM3 会自动把一些寄存器压栈,这里使用的是进入ISR 之前使用的SP 指针(MSP 或者是PSP)。离开ISR 后,只要ISR 没有更改过CONTROL[1],就依然使用先前 的SP 指针来执行出栈操作。

为了避免系统堆栈因应用程序的错误使用而毁坏,你可以给应用程序专门配一个堆栈, 不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用PSP, 而异常服务例程则使用MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程 时由硬件处理。

cortex中断

Cortex‐M3 为了缩短中断延迟,会自动的现场保护和恢复。

进入

当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:

  1. 入栈: 把8个寄存器的值压入栈:

    响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及 R3‐R0由硬件自动压入适当的堆栈中,并且在返回时自动弹出它们:如果当响应异常时,当前的代码正在使用PSP,则压入 PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用 主堆栈。

  2. 取向量:从向量表中找出对应的服务程序入口地址

    当数据总线(系统总线)正在为入栈操作而忙得团团转时,指令总线(I‐Code总线)可 不是凉快地坐着看热闹——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表 中找出正确的异常向量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线 的好处:入栈与取指这两个工作能同时进行。

  3. 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC

    在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:

    1. SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后, 将由MSP负责对堆栈的访问。

    2. PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。

    3. PC:在向量取出完毕后,PC将指向服务例程的入口地址。

    4. LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”, 并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4 位则有另外的含义见下面返回值章节。

返回

当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先 前的系统状态,才能使被中断的程序得以继续执行。用先前储的LR的值就可以返回。

  1. 出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指 针的值也改回去。

  2. 更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若 中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。

有些处理器使用特殊的返回指令来标示中断返回,例如8051就使用reti。但是在CM3中, 是通过把EXC_RETURN往PC里写来识别返回动作的。因此,可以使用上述的常规返回指令, 从而为使用C语言编写服务例程扫清了最后的障碍(无需特殊的编译器命令,如__interrupt)。

返回值

在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,这 是一个高28位全为1的值,只有[3:0]的值有特殊含义,如下图所示,当异常服务例程把这个 值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要 没有特殊需求,就不要改动它。

img

上图整理后再看如下:

img

由EXC_RETURN的格式可见,你不能把0xFFFF_FFF0‐0xFFFF_FFFF中的地址作为任何返回地 址。其实也并不用担心会弄错,因为CM3已经把这个范围标记成“取指不可区”了。

主栈响应ISR时序图如下:

img

线程栈响应ISR时序图如下:

img

其它

细心的读者一定在猜测:为啥袒护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关键字,来进行对比:

img

经过上图,我们发现加了__value_in_regs关键字,编译器会保证返回值从R0开始放,4字节返回值就放在R0,8字节返回值就放在R0、R1,依次类推,最多支持4个寄存器(亲测>=5个寄存器__value_in_regs就失效)。

没有加__value_in_regs关键字,编译器为了优化,不会按上面的规则放置返回值,其中的顺序也是随机的。

至于讲解这个的作用,会在事件,信号量,邮箱等带返回值的地方起作用。


知识共享许可协议
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

站内搜索

    撩我备注-博客

    joinee

    目录结构