当前位置:首页 > 公众号精选 > 嵌入式IoT

rt-thread中的压栈与出栈

1.说明

本文主要想分析一下rt-thread中线程的压栈与入栈的相关操作。从而更好的掌握线程切换与线程恢复的相关知识。

2.使用场景

首先需要明白的是什么情况下需要进行压栈与出栈的操作?对于这个问题可以做这样的设想,当程序一直做一件事的时候,是顺序执行的,不会有任何干扰。但是此时来了一个中断,那么程序逻辑肯定会优先去处理中断。那么这时需要做哪些事情?

也许这个例子有点脱离实际,讲的通俗明白一些,就是一个人在专注的完成一件事,此时应该是很顺利的进行。但是当事情还没有做完,但是又有一个更加紧急的事情需要你去处理,这时应该怎么做?

对于一个人来讲:

(1)将手里没有做完的事情保留起来,保留当前的进度

(2)将大脑清空,全力完成更加重要的事情

(3)恢复到没有做完的事情的现场,去接着完成没有做完的事情

人的大脑是这样工作的,其实芯片的逻辑也需要这样执行,我们知道芯片如何知道当前程序的状态,无外乎几个重要的寄存器,sp(程序指针寄存器),通用寄存器Rx,以及LR链接寄存器等等。有了这些信息,就可以知道程序当前运行的状态了,这个就是程序的现场。

对于armv7来说,寄存器可以分为以下几种:

armasm_pge1464343210583

在rt-thread操作系统中,涉及到压栈与出栈操作的有两个地方,第一个是中断的进入与中断处理完成后的退出,第二个是线程的切换。

3.简单分析一下rt-thread线程栈的初始化

对于/bsp/qemu-vexpress-a9来说,系统上电后执行rtt的第一行代码在/libcpu/arm/cortex-a/start_gcc.S文件。

然后执行_reset函数,这个函数是汇编函数写的,因为前期没有栈空间,所以代码需要采用汇编指令完成。

然后分配栈空间等等。执行到rtt的其他部分逻辑。这里就不赘述了。这里主要分析的是线程的初始化。

每一个线程在初始化的时候,需要分配栈空间

rt_thread_create/rt_thread_init --> _rt_thread_init --> rt_hw_stack_init

最后调用到了/libcpu/arm/cortex-a/stack.c文件。

rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit)
{ rt_uint32_t *stk;

 stack_addr += sizeof(rt_uint32_t);
 stack_addr  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stack_addr, 8);
 stk      = (rt_uint32_t *)stack_addr;
 *(--stk) = (rt_uint32_t)tentry; /* entry point */ *(--stk) = (rt_uint32_t)texit; /* lr */ *(--stk) = 0xdeadbeef; /* r12 */ *(--stk) = 0xdeadbeef; /* r11 */ *(--stk) = 0xdeadbeef; /* r10 */ *(--stk) = 0xdeadbeef; /* r9 */ *(--stk) = 0xdeadbeef; /* r8 */ *(--stk) = 0xdeadbeef; /* r7 */ *(--stk) = 0xdeadbeef; /* r6 */ *(--stk) = 0xdeadbeef; /* r5 */ *(--stk) = 0xdeadbeef; /* r4 */ *(--stk) = 0xdeadbeef; /* r3 */ *(--stk) = 0xdeadbeef; /* r2 */ *(--stk) = 0xdeadbeef; /* r1 */ *(--stk) = (rt_uint32_t)parameter; /* r0 : argument */ /* cpsr */ if ((rt_uint32_t)tentry & 0x01)
 *(--stk) = SVCMODE | 0x20; /* thumb mode */ else *(--stk) = SVCMODE; /* arm mode   */ #ifdef RT_USING_LWP *(--stk) = 0; /* user lr */ *(--stk) = 0; /* user sp*/ #endif #ifdef RT_USING_FPU *(--stk) = 0; /* not use fpu*/ #endif /* return task's current stack address */ return (rt_uint8_t *)stk;
}

初始化线程的时候,每个线程都是有一个栈空间的,这个栈空间不仅仅保存一下参数变量,还在栈地址的首地址处保存了线程执行需要的现场。而且每个线程都有一个独立的栈内存,这个内存就是在栈的入口处。

当线程发生切换的时候,需要取出这些寄存器

.globl rt_thread_switch_interrupt_flag
.globl rt_interrupt_from_thread
.globl rt_interrupt_to_thread
.globl rt_hw_context_switch_interrupt
rt_hw_context_switch_interrupt: #ifdef RT_USING_SMP /* r0 :svc_mod context
 * r1 :addr of from_thread's sp
 * r2 :addr of to_thread's sp
 * r3 :to_thread's tcb
 */ str     r0, [r1]

 ldr     sp, [r2]
 mov     r0, r3
 bl      rt_cpus_lock_status_restore

 b       rt_hw_context_switch_exit

执行到rt_hw_context_switch_exit函数

.global rt_hw_context_switch_exit
rt_hw_context_switch_exit: #ifdef RT_USING_SMP #ifdef RT_USING_SIGNALS mov     r0, sp
 cps #Mode_IRQ
 bl      rt_signal_check
 cps #Mode_SVC
 mov     sp, r0 #endif #endif #ifdef RT_USING_FPU /* fpu context */ ldmfd sp!, {r6}
 vmsr fpexc, r6
 tst  r6, #(1<<30)
 beq 1f ldmfd sp!, {r5}
 vmsr fpscr, r5
 vldmia sp!, {d16-d31}
 vldmia sp!, {d0-d15} 1: #endif #ifdef RT_USING_LWP ldmfd   sp, {r13, r14}^ /* usr_sp, usr_lr */ add     sp, #8 #endif ldmfd   sp!, {r1}
 msr     spsr_cxsf, r1 /* original mode */ ldmfd   sp!, {r0-r12,lr,pc}^ /* irq return */ 

该函数可能看起来有些费劲,我来解释一下大概的内容:

当线程间要从上一个线程切换到下一个线程的时候,首先会将切换之前现场保存起来,也就是将这些寄存器的知保存到内存中,然后将sp指向下线程的地址。此时需要恢复下一个需要切换的线程的寄存器。

4.总结

如果需要理清楚rt-thread的栈空间的压栈与入栈,其实最根本的问题就是如何去处理现场状态的问题。也就是每个线程都需要有一个独立的栈空间,然后这些栈空间除了保存数据,还需要保存寄存器。当进行任务切换的时候,当前线程的寄存器需要保存该线程的栈内存中,而下个线程的栈空间则会从自己的栈空间的起始地址处恢复。这个就是rt-thread栈运作的实现逻辑。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭