想深入了解RTOS内核线程切换原理--请看此文

发布于 2020-02-23 22:35:07
本文转自我的博客csdn fhqlongteng,转载过来,代码的格式发生混乱,你可以到我的博客中查看原版。
1、简介

本文主要介绍RT Thread操作系统在cortex-m3内核上的移植接口文件,通过本篇博客你将深入了解RTOS操作系统是怎么通过触发软中断实现任务切换的,怎么实现内核异常信息的打印功能。
2、移植的接口文件

RT-Thread操作系统的移植接口文件主要用cpuport.c,context_rvds.s,backtrace.c,div0.c,showmem.c。其中最重要的文件是cpuport.c和context_rvds.s这两个文件,其他三个文件在cortex-M3内核移植时没有实际的应用,这三个文件实际一些辅助的功能,打印内存,除数为0,后台跟踪等操作,内容很简单,可以自行查看。

3、任务切换context_rvds.s

这是一个汇编语言的文件,这个文件实现了任务切换,触发软件中断,硬件异常错误处理等操作,是操作系统移植时要实现的最重要的功能。程序的内部逻辑根cortex-m3内核的编程模型有关,想了解此程序逻辑,需要对cortex-m3内核的编程模块有一定的了解。
操作系统进行初始化芯片的时钟,必要的外设后,开始进行第一个任务/线程调度时,会调用rt_hw_context_switch_to,函数的输入参数是进行切换的任务(线程)的堆栈指针。这个函数的具体功能如下:
请详细查看我增加的中文注释。
请详细查看我增加的中文注释,有你不会的干货。
请详细查看我增加的中文注释。


;/*
; * void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * this fucntion is used to perform the first thread switch
; */
rt_hw_context_switch_to PROC
EXPORT rt_hw_context_switch_to
; set to thread
; 把要切换到的线程的堆栈指针记录到变量rt_interrupt_to_thread中来
LDR r1, =rt_interrupt_to_thread
STR r0, [r1]

; set from thread to 0
; 第一次进行线程切换,没有上一次切换所以把rt_interrupt_from_thread设置为0
LDR r1, =rt_interrupt_from_thread
MOV r0, #0x0
STR r0, [r1]

; set interrupt flag to 1
; 进行线程切换,把线程切换标志变量rt_thread_switch_interrupt_flag设置为1
LDR r1, =rt_thread_switch_interrupt_flag
MOV r0, #1
STR r0, [r1]

; set the PendSV exception priority
; 设置pendsv软件中断的优先级为最低
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] ; read
ORR r1,r1,r2 ; modify
STR r1, [r0] ; write-back

; trigger the PendSV exception (causes context switch)
;触发pendsv软件中断,此时中断关闭,并不会产生中断
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]

; restore MSP
;这段代码实际是可以没有的,移植时这样做有了一个好处就是增加了MSP堆栈的使用空间
;cortex-m3内核复位时使用msp堆栈,从复位到进行初始化操作时会调用很多函数,会进行一些压栈操作
;占用一部分msp堆栈,由于程序不会退出到复位的位置,压栈占用的msp空间永远不会释放,产生了堆栈的
;的空间浪费一小部分。
; 下面的代码实现的功能是读取SCB_VTOR寄存器,这个寄存器保存了中断向量表的起始位置,此位置的字
; 就是MSP堆栈的指针,即启动代码里面分配出来的堆栈的栈顶。经过2次 LDR r0, [r0]就是相当于取到
;堆栈的栈顶,最后设置msp为栈顶
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
MSR msp, r0

; enable interrupts at processor level
; 打开中断
CPSIE F
CPSIE I

; never reach here!
ENDP



在已经进行了一次线程切换后,再次进行线程切换时会调用void rt_hw_context_switch(rt_uint32 from, rt_uint32 to)这个函数,这个函数与上面rt_hw_context_switch_to函数的功能相比大同小异。

;/*
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; */
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch

; set rt_thread_switch_interrupt_flag to 1
; 判断线程切换标志rt_thread_switch_interrupt_flag是否为1
LDR r2, =rt_thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
;不为1时,设置为1,把from的线程的堆栈指针记录到rt_interrupt_from_thread中
MOV r3, #1
STR r3, [r2]

LDR r2, =rt_interrupt_from_thread ; set rt_interrupt_from_thread
STR r0, [r2]

_reswitch
;为1时,把to的线程的堆栈指针记录到rt_interrupt_to_thread中来
LDR r2, =rt_interrupt_to_thread ; set rt_interrupt_to_thread
STR r1, [r2]

;触发pendsv中断
LDR r0, =NVIC_INT_CTRL ; trigger the PendSV exception (causes context switch)
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
ENDP


pendsv中断是真正进行了线程切换操作的,前面介绍的2个函数主要在进行线程切换前,把要切换的线程的堆栈指针记录到这个汇编的程序的变量中,在pendsv中断中进行线程切换时使用,并且触发中断。下面介绍pendsv中断内部实现线程切换的原理,一定要仔细看呀。

; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler PROC
EXPORT PendSV_Handler
;根据cortext-m3内核的编程模型,进行pendsv中断前,内核已经自动的psr, pc, lr, r12, r3, r2, r1, r0把这些寄存器压入到发生切换的线程的堆栈psp中的去了,跳到中断程序,使用的堆栈自动切换成msp

; disable interrupt to protect context switch
; 记录primask中断开关寄存器的值到r2寄存器中,用于退出中断后再打开中断用
MRS r2, PRIMASK
;关闭中断
CPSID I

; get rt_thread_switch_interrupt_flag
; 判断rt_thread_switch_interrupt_flag标志为1时,才进行线程切换,为0时直接退出中断
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled

; clear rt_thread_switch_interrupt_flag to 0
; 进行线程切换,清除rt_thread_switch_interrupt_flag变量
MOV r1, #0x00
STR r1, [r0]

;判断从哪个线程切换出去,要切换到第一个线程时,rt_interrupt_from_thread变量为0
;判断此变量为0表示切入第一个线程
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread ; skip register save at the first time

;不为0时,把r4-r11这8个寄存器保存到当前要切换出去的线程堆栈psp中去,并且把当前线程的堆栈指针psp记录到rt_interrupt_from_thread变量中来
MRS r1, psp ; get from thread stack pointer
STMFD r1!, {r4 - r11} ; push r4 - r11 register
LDR r0, [r0]
STR r1, [r0] ; update from thread stack pointer

switch_to_thread
;把要切入的线程的堆栈指针取出到r1寄存器中
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; load thread stack pointer

;从要切入线程堆栈中弹出这个线程中的寄存器r4-r11,把线程堆栈指针赋值到psp中。
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
MSR psp, r1 ; update stack pointer

pendsv_exit
; restore interrupt
; 打开中断
MSR PRIMASK, r2

;cortex-m3内核中发生中断时,在中断程序中使用的堆栈是msp,操作系统线程设计使用的是psp线程,所以上面的线程切换就是实现是两个线程的堆栈指针的切换,即把当前线程的堆栈psp保存到rt_interrupt_from_thread
为量中,把要切入的堆栈赋值到psp中去。
;由于中断中使用的msp堆栈,退出中断是如果不做任何操作还是使用msp堆栈,而线程使用的是psp堆栈,所以对lr寄存的位3进行置1就控制退出中断后使用psp中断。
ORR lr, lr, #0x04
BX lr
ENDP



[img=15,15]data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==[/img]
线程切换的核心就在上面的代码注释中,不懂的话要多看几次同时参考cortex-m3的内核编程手册来看。上面的代码主要实现的是对切换进入和切换退出的线程堆栈指针的变换,即保存当前线程的psp,把要切入的线程的堆栈指针赋值到psp中去。可能读者关心线程切换不仅要切换线程的上下文,还要从一个线程跳到另外一个线程,这个是怎么实现的呢?从一个线程跳到另外一个线程中上面的代码确实没有实现,实际是靠cortex-m3内核自动完成的。发生pendsv中断前,内核硬件自动(不用程序操作)把当前线程的上下文(psr, pc, lr, r12, r3, r2, r1, r0)压入线程自己的堆栈,可以看到发生中断时的程序位置的pc指针已经自动保存到堆栈中,pendsv中断程序把新切入的线程堆栈换到psp中,当中断程序退出,新切入的线程的中断上下文(psr, pc, lr, r12, r3, r2, r1, r0)会自动(硬件执行,不用程序)的从线程中弹出,程序指针pc就获得了新线程的pc和这个线程中使用的寄存器的值,程序就运行到新线程中去了。这就是cortex-m3线程切换的核心与精髓,你明白了么?

还有两个函数rt_hw_interrupt_enable和rt_hw_interrupt_disable是实现开中断和关中断,功能很简单,不再详细描述。

HardFault_Handler    PROC

; get current context
;中断程序中lr表示的是EXC_RETURN寄存器的状态,这个寄存器的位2表示进入中断前使用的是psp还是
;msp堆栈,在rt-thread中,如果硬件错误中断发生在线程中使用的是psp,如果是从另外一个中断发生
;硬件故障产生的中断,使用的是msp
TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
;把发生中断前的堆栈指针赋值到r0寄存器中去
MRSEQ r0, msp ; [2]=0 ==> Z=1, get fault context from handler.
MRSNE r0, psp ; [2]=1 ==> Z=0, get fault context from thread.

;手动把r4-r11压入堆栈中,再压入lr寄存器,记住这里多压入了9个寄存器的值
; 这里这样操作的原因是为了rt_hw_hard_fault_exception函数中定义的结构体能对齐访问到全部寄存器

STMFD r0!, {r4 - r11} ; push r4 - r11 register
STMFD r0!, {lr} ; push exec_return register

TST lr, #0x04 ; if(!EXC_RETURN[2])
ITE EQ
;上面压完堆栈后,把更新后的堆栈指针重新写入到psp或msp中去,r0寄存器保存的是发生hardfault中
; 断前使用的堆栈指针,做为参数会传入函数rt_hw_hard_fault_exception中去
MSREQ msp, r0 ; [2]=0 ==> Z=1, update stack pointer to MSP.
MSRNE psp, r0 ; [2]=1 ==> Z=0, update stack pointer to PSP.

PUSH {lr}
BL rt_hw_hard_fault_exception
POP {lr}

ORR lr, lr, #0x04
BX lr
ENDP

ALIGN 4

END


4、cpu接口程序cpuport.c

这个程序主要有2个函数
void rt_hw_hard_fault_exception(struct exception_info * exception_info),
rt_uint8_t *rt_hw_stack_init(void *tentry,void *parameter, rt_uint8_t *stack_addr,void *texit)

比较重要,另外几个函数的功能都很简单不做详细介绍。

rt_hw_hard_fault_exception函数中实现打印发生错误中断前的程序的位置的上下位,即发生中断时程序的出现故障的位置。还记得上面的程序段中如下的这些操作,这些操作是向堆中多压入了9个寄存器,进入此函数中使用结构体来struct exception_info来进行访问使用的。
;手动把r4-r11压入堆栈中,再压入lr寄存器,记住这里多压入了9个寄存器的值
; 这里这样操作的原因是为了rt_hw_hard_fault_exception函数中定义的结构体能对齐访问到全部寄存器
    STMFD   r0!, {r4 - r11}         ; push r4 - r11 register
STMFD r0!, {lr} ; push exec_return register


结构体struct exception_info的定义如下,
struct exception_stack_frame
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;
};
struct stack_frame
{
/* r4 ~ r11 register */
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;
struct exception_stack_frame exception_stack_frame;
};
struct exception_info
{
rt_uint32_t exc_return;
struct stack_frame stack_frame;
};



从结构体的定义可以看出r0成员变量前面还有exc_return,r4-r11这9个成员变量,所以手动向堆栈中压入9个寄存器,使用这个结构体来访问发生中断前的程序位置的pc,通过pc值就能找到哪段程序发生了错误中断。
void rt_hw_hard_fault_exception(struct exception_info * exception_info)
{
extern long list_thread(void);
struct stack_frame* context = &exception_info->stack_frame;

if (rt_exception_hook != RT_NULL)
{
rt_err_t result;

result = rt_exception_hook(exception_info);
if (result == RT_EOK)
return;
}

rt_kprintf("psr: 0x%08x\n", context->exception_stack_frame.psr);

rt_kprintf("r00: 0x%08x\n", context->exception_stack_frame.r0);
rt_kprintf("r01: 0x%08x\n", context->exception_stack_frame.r1);
rt_kprintf("r02: 0x%08x\n", context->exception_stack_frame.r2);
rt_kprintf("r03: 0x%08x\n", context->exception_stack_frame.r3);
rt_kprintf("r04: 0x%08x\n", context->r4);
rt_kprintf("r05: 0x%08x\n", context->r5);
rt_kprintf("r06: 0x%08x\n", context->r6);
rt_kprintf("r07: 0x%08x\n", context->r7);
rt_kprintf("r08: 0x%08x\n", context->r8);
rt_kprintf("r09: 0x%08x\n", context->r9);
rt_kprintf("r10: 0x%08x\n", context->r10);
rt_kprintf("r11: 0x%08x\n", context->r11);
rt_kprintf("r12: 0x%08x\n", context->exception_stack_frame.r12);
rt_kprintf(" lr: 0x%08x\n", context->exception_stack_frame.lr);
rt_kprintf(" pc: 0x%08x\n", context->exception_stack_frame.pc);

if(exception_info->exc_return & (1 << 2) )
{
rt_kprintf("hard fault on thread: %s\r\n\r\n", rt_thread_self()->name);

#ifdef RT_USING_FINSH
list_thread();
#endif /* RT_USING_FINSH */
}
else
{
rt_kprintf("hard fault on handler\r\n\r\n");
}

#ifdef RT_USING_FINSH
hard_fault_track();
#endif /* RT_USING_FINSH */

while (1);
}


rt_hw_stack_init函数在创建线程时,对分配的线程的堆栈进行初始化,一个线程中使用全部的cortex-m3的16个寄存器,所以这个函数在线程的堆栈的栈顶位置向下的16个字进行初始化,按照内核进入中断时压入堆栈的寄存器顺序排列进行初始化,特别说明一下lr是返回地址,即线程退出后返回到rt_thread_exit函数中,pc是线程的入口函数地址。
/**
* This function will initialize thread stack
*
* @param tentry the entry of thread
* @param parameter the parameter of entry
* @param stack_addr the beginning stack address
* @param texit the function will be called when thread exit
*
* @return stack address
*/
rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;

stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);

stack_frame = (struct stack_frame *)stk;

/* init all register */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)
    = 0xdeadbeef;
    }

    stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
    stack_frame->exception_stack_frame.r1 = 0; /* r1 */
    stack_frame->exception_stack_frame.r2 = 0; /* r2 */
    stack_frame->exception_stack_frame.r3 = 0; /* r3 */
    stack_frame->exception_stack_frame.r12 = 0; /* r12 */
    stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
    stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
    stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */

    /* return task's current stack address */
    return stk;
    }


    至此已经完成了全部cortext-m3内核移植部分的关键代码的讲解,如有不懂的地方可以在下面留言联楼主




查看更多

关注者
1
被浏览
671
2 个回答
andychen
andychen 2020-02-24
帮你重新排了下版,这样阅读效果不比博客差了
fhqmcu
fhqmcu 认证专家 2020-02-24
andychen 发表于 2020-2-24 09:44
帮你重新排了下版,这样阅读效果不比博客差了


谢谢的,我正要手工排版来的,昨天太晚就没有整了

撰写答案

请登录后再发布答案,点击登录

发布
问题

分享
好友