简介
移植部分其实也是针对不同硬件做些接口,这部分其实也是系统的很重要的组成部分。
我们把一些关键的地方列出来解释下,移植一款芯片,基本上也就修改这些地方。
为什么我们先介绍移植,再去介绍系统其它模块呢?个人喜好自下而上的认知方式,和建房子一样,从地基慢慢累积上来。
初始化任务的栈空间
任何RTOS,在分配任务的栈时,必定要做一些必要的初始化,以便在使用时方便操作。
在分析rt_init_stack之前,我们先来看看cortex中断入栈顺序以及栈空间的内容:
发现在进入中断后,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中断服务
为了方便,再贴个图,方便查看:
另外在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文件:
上图蓝色的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中断服务的过程:
- 先获取SVC服务号,如果是0,代表RTX的服务,如果大于0,表示是用户自定义的服务。
- 如果是RTX的服务,那么先执行该服务号下面的指定函数。
- 如果执行完函数后,需要切换任务,就开始保存当前任务的工作环境。
- 恢复为最新要切换的任务的工作环境。
- 离开中断,恢复为线程模式。
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
}