(原创)RTOS-RTX分析系列之移植部分

2017/07/18 RTX

简介

移植部分其实也是针对不同硬件做些接口,这部分其实也是系统的很重要的组成部分。

我们把一些关键的地方列出来解释下,移植一款芯片,基本上也就修改这些地方。

为什么我们先介绍移植,再去介绍系统其它模块呢?个人喜好自下而上的认知方式,和建房子一样,从地基慢慢累积上来。

初始化任务的栈空间

任何RTOS,在分配任务的栈时,必定要做一些必要的初始化,以便在使用时方便操作。

在分析rt_init_stack之前,我们先来看看cortex中断入栈顺序以及栈空间的内容:

img

发现在进入中断后,cortex会自动把这8个寄存器压入栈区,剩余的8个(R4-R11)需要用户自己手动压入,前提是需要用到,如果用不到,当然不用压入。

/*--------------------------- rt_init_stack ---------------------------------*/

void rt_init_stack (P_TCB p_TCB, FUNCP task_body) {
  /* Prepare TCB and saved context for a first time start of a task. */
  U32 *stk,i,size;

  /* Prepare a complete interrupt frame for first task start */
  size = p_TCB->priv_stack >> 2;
  if (size == 0) {
    size = (U16)os_stackinfo >> 2;// 关键地方,下面会解释
  }

  /* Write to the top of stack. */
  stk = &p_TCB->stack[size];//指向分配的堆的最高地址处,因为cortex的栈模型为向下生长的满栈模型。

  /* Auto correct to 8-byte ARM stack alignment. */
  if ((U32)stk & 0x04) {//强制8字节对齐
    stk--;
  }

  stk -= 16;//直接分配16个寄存器的大小,以便模拟主栈模型。

  /* Default xPSR and initial PC */
  stk[15] = INITIAL_xPSR;//模拟进入中断后的栈空间
  stk[14] = (U32)task_body;//模拟进入中断后的栈空间

  /* Clear R4-R11,R0-R3,R12,LR registers. */
  for (i = 0; i < 14; i++) {//模拟进入中断后的栈空间
    stk[i] = 0;
  }

  /* Assign a void pointer to R0. */
  stk[8] = (U32)p_TCB->msg;//事先向任务写入的邮箱事件

  /* Initial Task stack pointer. */
  p_TCB->tsk_stack = (U32)stk;//初始化任务的私有栈指针,相当于SP指针

  /* Task entry point. */
  p_TCB->ptask = task_body;//任务的函数入口

  /* Initialize stack with magic pattern. */
  if (os_stackinfo & 0x10000000U) { //检测是否使能初始化栈空间标志,
    if (size > (16+1)) {
      for (i = (size - 16)/2 - 1; i; i--) { //8个字节的初始化,每个4字节都被初始胡为0xCCCCCCCC
	stk -= 2;
	stk[1] = MAGIC_PATTERN;
	stk[0] = MAGIC_PATTERN;
      }
      if (--stk > p_TCB->stack) { //剩余不够8字节的就按4字节初始化,每个4字节都被初始胡为0xCCCCCCCC
	*stk = MAGIC_PATTERN;
      }
    }
  }

  /* Set a magic word for checking of stack overflow. */
  p_TCB->stack[0] = MAGIC_WORD; //在堆基地址,放置一个魔数,用来标记栈空间的极限,可以检测栈是否溢出。
}

注意上面的

  if (size == 0) {
    size = (U16)os_stackinfo >> 2;
  }

我们先来看看os_stackinfo定义:

#ifndef OS_STKINIT
#define OS_STKINIT      0
#endif

#ifndef OS_STKCHECK
 #define OS_STKCHECK    1
#endif

#ifndef OS_PRIVCNT
 #define OS_PRIVCNT     0
#endif

#define OS_PRIV_CNT (OS_PRIVCNT + 1) //or +2

#ifndef OS_STKSIZE
 #define OS_STKSIZE     50      // this stack size value is in words
#endif
 
uint32_t const os_stackinfo  = (OS_STKINIT<<28) | (OS_STKCHECK<<24) | (OS_PRIV_CNT<<16) | (OS_STKSIZE*4);

这其实是把一个32位的无符号整形分成4段,第28位表示是否栈初始化,第24位表示是否开启栈检查,第16位表示私有栈的大小,第0-15位的16位宽数据表示栈大小, 也就是为什么os_stackinfo前要加一个(U16)强制转换类型。

注意看代码,要使用栈溢出检测,就必须先使能栈初始化标志。

设置返回值寄存器

在前面RTOS-RTX分析系列之前提章节中,介绍了os_InRegs宏的定义,但没有讲何时使用,现在配合下面的代码就可以明白了:

/*--------------------------- rt_ret_val ----------------------------------*/

static __inline U32 *rt_ret_regs (P_TCB p_TCB) {
  /* Get pointer to task return value registers (R0..R3) in Stack */
#if (__TARGET_FPU_VFP)
  if (p_TCB->stack_frame) {
    /* Extended Stack Frame: R4-R11,S16-S31,R0-R3,R12,LR,PC,xPSR,S0-S15,FPSCR */
    return (U32 *)(p_TCB->tsk_stack + 8*4 + 16*4);
  } else {
    /* Basic Stack Frame: R4-R11,R0-R3,R12,LR,PC,xPSR */
    return (U32 *)(p_TCB->tsk_stack + 8*4);
  }
#else
  /* Stack Frame: R4-R11,R0-R3,R12,LR,PC,xPSR */
  return (U32 *)(p_TCB->tsk_stack + 8*4);//因为是按 R4-R11,R0-R3,R12,LR,PC,xPSR 排序的,移动8个寄存器,也就是移动到R0寄存器的位置。
#endif
}

void rt_ret_val (P_TCB p_TCB, U32 v0) {
  U32 *ret;

  ret = rt_ret_regs(p_TCB);
  ret[0] = v0; //用来设置4字节返回值的内容
}

void rt_ret_val2(P_TCB p_TCB, U32 v0, U32 v1) {
  U32 *ret;

  ret = rt_ret_regs(p_TCB);
  ret[0] = v0; //用来设置0-4字节的返回值内容
  ret[1] = v1; //用来设置3-8字节的返回值内容 结合```RTOS-RTX分析系列之前提```内容,这里就可以设置简单类型结构体中的值。
}

设置用户栈指针

__asm void rt_set_PSP (U32 stack) {
	MSR     PSP,R0 ;在汇编时,编译器会默认把stack传递给R0寄存器
	BX      LR
}

获取用户栈指针

__asm U32 rt_get_PSP (void) {
	MRS     R0,PSP ;编译器会默认把返回值写进R0寄存器
	BX      LR
}

SVC中断服务

为了方便,再贴个图,方便查看:

img

另外在SVC 服务例程执行后,上次执行的SVC 指令地址可以根据自动入栈的返回地址计算出。

找到了SVC 指令后,就可以读取该SVC 指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。如果用户程序使用的是PSP,服务例程还需要使用MRS指令来获取PSP。

举个栗子:

    0x00000650:    bf00        ..      NOP      
    0x00000652:    4f4f        OO      LDR      r7,[pc,#316] ; [0x790] = 0x523
    0x00000654:    46bc        .F      MOV      r12,r7
    0x00000656:    df00        ..      SVC      #0x0 ; formerly SWI
    0x00000658:    bf00        ..      NOP      

注意看SVC #0x0指令,对应df00机器码,df是svc的代号,00就是svc请求中断号。

再来看看实际的bin文件:

img

上图蓝色的pc标识就在0x00000658位置,由于采用小端模式,因此bf00,变成了00bf,df00变成了00df。

为了方便,把svc表的汇编也贴上,虽然系统中没有用到:

		AREA    SVC_TABLE, CODE, READONLY

		EXPORT  SVC_Count

SVC_Cnt         EQU    (SVC_End-SVC_Table)/4
SVC_Count       DCD     SVC_Cnt

; Import user SVC functions here.
;               IMPORT  __SVC_1

		EXPORT  SVC_Table
SVC_Table
; Insert user SVC functions here. SVC 0 used by RTL Kernel.
;               DCD     __SVC_1                 ; user SVC function

SVC_End

		END

下面就是svc中断汇编:

__asm void SVC_Handler (void) {
	PRESERVE8

	IMPORT  SVC_Count
	IMPORT  SVC_Table
	IMPORT  rt_stk_check

	MRS     R0,PSP                  ; Read PSP 把线程的栈指针写进R0寄存器
	LDR     R1,[R0,#24]             ; Read Saved PC from Stack LDR:从存储器中加载字到一个寄存器中 根据上图容易理解,RO往上移动24/4=6个字,就指向了PC寄存器。
	SUBS    R1,R1,#2                ; Point to SVC Instruction 举例来说,按上面给的栗子,PC地址应该在```0x00000658```,减去两个字节,指向```0x00000656```也就是红色方框
	LDRB    R1,[R1]                 ; Load SVC Number LDRB:从存储器中加载字节到一个寄存器中 这里就把0给加载进R1寄存器了。
	CMP     R1,#0			; 0 是RTX系统统一用的系统调用服务号,用0来判断是否是用户自定义的服务。
	BNE     SVC_User                ; User SVC Number > 0 如果不等于0,则跳转到用户自定义的svc服务号表。

	MOV     LR,R4			; 这里把LR寄存器当做普通寄存器使用
	LDMIA   R0,{R0-R3,R4}           ; Read R0-R3,R12 from stack 把栈中的R0,R1,R2,R3,R12保存在R0,R1,R2,R3,R4中。
	MOV     R12,R4			; 把R4也就是R12的值恢复给R12,这里有个疑问,不知道为什么不能直接恢复R12的值?
	MOV     R4,LR			; 恢复R4的值
	BLX     R12                     ; Call SVC Function 因为SVC指令除了会指定服务号之外,还可以把函数入口地址放在R12寄存器,这里就是执行指定的函数。

	MRS     R3,PSP                  ; Read PSP 获取线程的栈指针
	STMIA   R3!,{R0-R2}             ; Store return values 将执行的结果保存在线程栈里的R0,R1,R2位置。

	;typedef struct OS_TSK {
	;P_TCB  run;                     /* Current running task                    */
	;P_TCB  new;                     /* Scheduled task to run                   */
	;} *P_TSK;

	LDR     R3,=__cpp(&os_tsk)	;获取os_tsk变量的地址 struct OS_TSK os_tsk;
	LDMIA   R3!,{R1,R2}             ;将os_tsk.run保存在R1, os_tsk.new保存在R2
	CMP     R1,R2			;比较两个任务是否相同。
	BEQ     SVC_Exit                ; no task switch 如果相同,表示当前没有任务切换,直接退出中断。

	SUBS    R3,#8			; 执行完LDMIA指令后,R3值会改变,这里减去8/4=2个单位,意思是恢复到os_tsk变量的地址。
	CMP     R1,#0                   ; Runtask deleted? 如果当前任务为空,也就是NULL,则直接跳到运行new的任务。
	BEQ     SVC_Next

	;这里开始就是任务切换的开始
	MRS     R0,PSP                  ; Read PSP 获取被打断的线程栈指针
	SUBS    R0,R0,#32               ; Adjust Start Address 压栈32/4=8个寄存器的大小。因为进入中断会自动压入xPSR,PC,LR,R12,R3,R2,R1,R0 8个寄存器值。因此再压入8个寄存器就全了(SP寄存器不需要压入,专门有个变量保存)。
	STR     R0,[R1,#TCB_TSTACK]     ; Update os_tsk.run->tsk_stack 这里TCB_TSTACK=40,初次看不知道40怎么来的,这里得去看看OS_TCB的结构体,tsk_stack刚好在该结构体40个字节的偏移处,因此这里是保存线程SP指针的值到TCB的tsk_stack变量中。
	STMIA   R0!,{R4-R7}             ; Save old context (R4-R7) 保存R4-R7寄存器
	MOV     R4,R8
	MOV     R5,R9
	MOV     R6,R10
	MOV     R7,R11
	STMIA   R0!,{R4-R7}             ; Save old context (R8-R11)保存R8-R11寄存器 这里很奇怪,为什么不直接R0!,{R4-R11},有空验证下。
	
	PUSH    {R2,R3}
	BL      rt_stk_check            ; Check for Stack overflow 检测栈是否溢出
	POP     {R2,R3}

	;这里开始就是切换到下一个任务
SVC_Next
	STR     R2,[R3]                 ; os_tsk.run = os_tsk.new 更新当前运行的任务

	LDR     R0,[R2,#TCB_TSTACK]     ; os_tsk.new->tsk_stack 加载新任务的栈指针
	ADDS    R0,R0,#16               ; Adjust Start Address 因为R4-R7放在栈的低地址,因此这里先加上16/4=4个寄存器大小,指针刚好移动到R8寄存器地址,从而先恢复R8-R11
	LDMIA   R0!,{R4-R7}             ; Restore new Context (R8-R11)
	MOV     R8,R4
	MOV     R9,R5
	MOV     R10,R6
	MOV     R11,R7
	MSR     PSP,R0                  ; Write PSP 重要的一步,更新PSP
	SUBS    R0,R0,#32               ; Adjust Start Address
	LDMIA   R0!,{R4-R7}             ; Restore new Context (R4-R7) 恢复R4-R7寄存器

SVC_Exit
	MOVS    R0,#:NOT:0xFFFFFFFD     ; Set EXC_RETURN value
	MVNS    R0,R0
	BX      R0                      ; RETI to Thread Mode, use PSP 返回线程模式,使用PSP

	/*------------------- User SVC ------------------------------*/

SVC_User
	PUSH    {R4,LR}                 ; Save Registers 虽然这里的代码从不执行,带顺便讲解下算了。
	LDR     R2,=SVC_Count		;SVC_Count也就是SVC_Cnt,也等于(SVC_End-SVC_Table)/4,由于SVC_End=SVC_Table,因此也就是0.
	LDR     R2,[R2]			;把0写进R2寄存器
	CMP     R1,R2			;用svc调用服务号和svc表里的svc服务总数作对比。
	BHI     SVC_Done                ; Overflow BHI:大于(无符号数) 如果当前svc调用服务号比总数还大,表明不支持的服务号。直接跳转到done

	LDR     R4,=SVC_Table-4
	LSLS    R1,R1,#2		;LSLS 逻辑左移2位,也就是乘以4,因为函数入口是32位的地址,所以乘以4好计算。
	LDR     R4,[R4,R1]              ; Load SVC Function Address 把以svc_table-4为起点,移动R1的字节数,刚好就等于该服务号里的函数入口了。
	MOV     LR,R4			; 把该函数入口写入LR寄存器。

	LDMIA   R0,{R0-R3,R4}           ; Read R0-R3,R12 from stack 因为在函数入口就```MRS     R0,PSP ```了,因此R0保存着用户现场的栈指针。这里把入栈时的R0,R1,R2,R3,R12保存在R0,R1,R2,R3,R4里。
	MOV     R12,R4			; 恢复R12寄存器的值
	BLX     LR                      ; Call SVC Function 调用SVC_Table里用户的服务函数,这里实际上使用的就是MSP栈了,因此PSP栈保持不变

	MRS     R4,PSP                  ; Read PSP 获取PSP栈指针
	STMIA   R4!,{R0-R3}             ; Function return values 把用户函数执行结果写入之前被暂停的用户栈里,这样线程一恢复就能获取到结果。
SVC_Done
	POP     {R4,PC}                 ; RETI 中断正常返回

	ALIGN
}

上面注释一大堆,我们来总结下SVC中断服务的过程:

  1. 先获取SVC服务号,如果是0,代表RTX的服务,如果大于0,表示是用户自定义的服务。
  2. 如果是RTX的服务,那么先执行该服务号下面的指定函数。
  3. 如果执行完函数后,需要切换任务,就开始保存当前任务的工作环境。
  4. 恢复为最新要切换的任务的工作环境。
  5. 离开中断,恢复为线程模式。

PendSV_Handler 中断服务

理解了上面的SVC中断服务,那么看这个PendSV_Handler就非常简单了。

注释就不写了,和上面几乎一样:

__asm void PendSV_Handler (void) {
	PRESERVE8

	BL      __cpp(rt_pop_req)

Sys_Switch
	LDR     R3,=__cpp(&os_tsk)
	LDMIA   R3!,{R1,R2}             ; os_tsk.run, os_tsk.new
	CMP     R1,R2
	BEQ     Sys_Exit                ; no task switch

	SUBS    R3,#8

	MRS     R0,PSP                  ; Read PSP
	SUBS    R0,R0,#32               ; Adjust Start Address
	STR     R0,[R1,#TCB_TSTACK]     ; Update os_tsk.run->tsk_stack
	STMIA   R0!,{R4-R7}             ; Save old context (R4-R7)
	MOV     R4,R8
	MOV     R5,R9
	MOV     R6,R10
	MOV     R7,R11
	STMIA   R0!,{R4-R7}             ; Save old context (R8-R11)
	
	PUSH    {R2,R3}
	BL      rt_stk_check            ; Check for Stack overflow
	POP     {R2,R3}

	STR     R2,[R3]                 ; os_tsk.run = os_tsk.new

	LDR     R0,[R2,#TCB_TSTACK]     ; os_tsk.new->tsk_stack
	ADDS    R0,R0,#16               ; Adjust Start Address
	LDMIA   R0!,{R4-R7}             ; Restore new Context (R8-R11)
	MOV     R8,R4
	MOV     R9,R5
	MOV     R10,R6
	MOV     R11,R7
	MSR     PSP,R0                  ; Write PSP
	SUBS    R0,R0,#32               ; Adjust Start Address
	LDMIA   R0!,{R4-R7}             ; Restore new Context (R4-R7)

Sys_Exit
	MOVS    R0,#:NOT:0xFFFFFFFD     ; Set EXC_RETURN value
	MVNS    R0,R0
	BX      R0                      ; RETI to Thread Mode, use PSP

	ALIGN
}

唯一的不同就是在中断服务进入时调用了rt_pop_req函数:

void rt_pop_req (void) {
  /* Process an ISR post service requests. */
  struct OS_XCB *p_CB;
  P_TCB next;
  U32  idx;

  os_tsk.run->state = READY;
  rt_put_rdy_first (os_tsk.run);

  idx = os_psq->last;
  while (os_psq->count) {
    p_CB = os_psq->q[idx].id;
    if (p_CB->cb_type == TCB) {
      /* Is of TCB type */
      rt_evt_psh ((P_TCB)p_CB, (U16)os_psq->q[idx].arg);
    }
    else if (p_CB->cb_type == MCB) {
      /* Is of MCB type */
      rt_mbx_psh ((P_MCB)p_CB, (void *)os_psq->q[idx].arg);
    }
    else {
      /* Must be of SCB type */
      rt_sem_psh ((P_SCB)p_CB);
    }
    if (++idx == os_psq->size) idx = 0;
    rt_dec (&os_psq->count);
  }
  os_psq->last = idx;

  next = rt_get_first (&os_rdy);
  rt_switch_req (next);
}

这个队列在后续RTOS-RTX分析系列之链表管理中会讲解,主要是处理在中断中对信号量,邮箱等的操作。

SysTick_Handler 中断服务

每个系统都有一个心跳时钟,用来给超时任务计时用。

这里的中断服务就非常简单了,现实回调下rt_systick的函数,以便标记处理给个延时的任务。 然后就看看是否有任务切换,有的话就切换下任务。不做解释了:

__asm void SysTick_Handler (void) {
	PRESERVE8

	BL      __cpp(rt_systick)
	B       Sys_Switch

	ALIGN
}

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

站内搜索

    撩我备注-博客

    joinee

    目录结构