RT-Thread启动进入就绪态最高优先级线程的全过程与栈帧分析(下)

电子说

1.3w人已加入

描述

Step 11. 继续单步到rt_hw_context_switch_to函数处。

在rt_system_scheduler_start函数中,会依次获取最高优先级线程的线程控制块,将其复制给to_thread。如图所示,在表达式窗口的to_thread就是main线程。

&to_thread->spthread->sp的地址,在Debug中,地址编号为0x200010C8,即0x200010C8内存单元中存放的数据是0x200018F4。

SVC

Q2. 在单独进入到rt_hw_context_switch_to之前,观察输出结果,main线程被remove。为什么在启动调度器的函数中,要先将线程从就绪列表中移除呢?

A2. 下一步要启动main线程,将其从Ready状态变成Running状态,所以需要将该线程从就绪列表中删除,RT-Thread后续在调度时暂时不考虑该线程,直到该线程状态再次从Running发生变化。

SVC

Step 12. 单步到进入到rt_hw_context_switch_to函数处,该函数位于context_gcc.S文件,由汇编语言编写实现。

rt_hw_context_switch_to仅仅在调度器启动时运行一次。该函数的C语言实现接口中,有一个参数,传入thread->sp变量的地址。

对于参数个数不大于4的C语言接口函数,编译器会按参数在列表中的顺序,自左向右 为参数分配寄存器r0-r3。
对于参数个数大于4的C语言接口函数,编译器会按参数在列表中的顺序,多余参数按自右向左的顺序压入栈中,即参数入栈顺序与参数顺序相反。

如上述Tips,thread->sp的地址通过r0传递。在下图左侧寄存器窗口中,可以看到r0的值为0x200010C8。

165行,将变量rt_interrupt_to_thread变量的地址赋值给r1。

165行,将r0的值赋值给r1指向的单元,即将r0的值赋值给变量rt_interrupt_to_thread。如果此时在表达式窗口观察rt_interrupt_to_thread,会发现它的值为0x200010C8。

SVC

此时,main线程的线程结构体和线程栈空间不变,但是r0, r1, rt_interrupr_to_thread的内容均发生了变化。

SVC

对于rt_hw_context_switch_to函数的其他行,依次分析如下:

168行至172行,处理浮点寄存器入栈控制,与Cortex M4内核的Lazy Stacking有关,但与本文主线无关,不做探讨。

176至178行,将rt_interrupt_from_thread变量清零。因此本次是RT-Thread第一次调度最高优先级线程,只有to,没有from。

181至183行,将rt_thread_switch_interrupt_flag变量至1,该值将在PendSV中断中使用。
186-194行,设置SysTick和PendSV中断的优先级,且触发PendSV,但现在不跳转,因为中断为禁止。

197-201行,很有意思的一段操作,将0x08000000处的栈顶指针放置到MSP中,相当于特权模式的栈顶指针复位了。CPU从汇编编写的启动代码,直到运行到此处,均在特权模式下运行,使用MSP作为栈顶指针。将来切换到线程后,会以PSP作为栈顶指针。启动流程不会重来一次,也没有任何函数再需要返回。所以,对于截止到目前使用的MSP栈,可以舍弃栈中的数据,MSP栈重置。

204-205行,使能中断。首先在context_gcc.S的89行设置断点,然后当PC运行在204行时按F5,会运行至PendSV中断服务程序。

SVC

Step 13. PendSV函数分析。

在PendSV中断服务程序中:

94行-96行,判断rt_thread_switch_interrupt_flag的值,为0则退出,为1则继续;
99行-105行,rt_thread_switch_interrupt_flag清0,判断rt_interrupt_from_thread的值,为0表示OS第一次进行最高优先级就绪状态线程的运行,无需恢复psp,直接跳转到switch_to_thread;为1表示从from线程切换至to线程,需要恢复psp。Debug到此处,rt_interrupt_from_thread的值为0,是第一次进行线程运行。

此处直接分析127行开始的switch_to_thread部分。

128行,将rt_interrupt_to_thread的地址赋值给r1。

129行,从r1指向的地址中取出值,赋值给r1,此时r1指向到main线程的thread->sp。

130行,从r1指向的地址中取出值,赋值给r1,此时r1指向到0x200018F4,如下图所示。

SVC

133行-136行,将r1指向的0x200018F4开始的单元内容,依次装载到r3, r4-r11中。执行完毕后,R3中是flag的值,r4-r11中均为0xDEAFBEEF,且r1指向0x20001918。

SVC

139-140行,由于r3为0,浮点寄存器不做处理。r1保持不变。

143行,将r1的值赋值给PSP,线程栈顶指针PSP目前为0x20001918。后续PSP还会自动更新。

SVC

155行,使得LR寄存器的Bit2为1,确保PendSV异常返回使用的栈指针是PSP。

156行,异常返回。此时,线程栈中剩下内容,即从0x20001918-0x20001934的内容,会自动加载到R0, R1, R2, R3, R12, R14 (线程返回地址), PC (线程入口地址), xPSR。且,PSP会自动更新至0x20001938,即创建main线程时的栈顶指针。

SVC

Step 14. 光标在BX LR上时,按F5,自动运行到main线程入口地址main_thread_entry。

如下图所示,栈帧中的r0-r15, xPSR均已顺利从线程栈中进行了恢复,此时thread->sp = PSP = 0x20001938。开始顺利执行线程。

SVC

通过本文对线程启动过程的了解,对于两个线程/多个线程之间的互相切换能奠定坚实的基础,化繁为简,结合论坛关于上下文切换的代码注释,能帮助快速抓住主线。

使用的软硬件环境如下:

IDE工具 - RT-Thread Studio 2.2.6
硬件 - STM32L431RCT6,Cortex M4内核
软件 - RT-Thread 4.0.5版本
配置 - 仅使能main线程和tidle0线程
一、工程设置
Step 1. 新建名称为EVBMX_RTThread405_Switch的4.0.5版本工程

Step 2. 不使能软件定时器,使能线程状态更改的调试

关闭软件定时器线程,避免干扰。

Step 3. 关闭msh shell,禁用Finsh

关闭tshell线程,避免干扰。仅仅保留main线程和tidle0线程。

Step 4. 修改main函数

修改main函数后,线程进入一次,休眠且切换1次,再次切回且return,然后彻底退出,只留下tidle0线程。

#include
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include
int main(void)
{
rt_thread_mdelay(1000);
return RT_EOK;
}
Step 5. 下载程序,观察输出结果

读完全文后,对下方输出结果的每一行语句所代表的含义和发生时刻,能有更深刻体会。

二、调试运行
Step 6. 在component.c中257行按F9设置断点;F5全速运行到此处后,再按F9关闭此处断点。

Step 7. 依次进入rt_thread_create, _thread_init, 停留在thread.c的164行。

将变量thread添加到表达式窗口,可以查看各个成员的值,其中,thread->stack_addr = 0x20001138, thread->stack_size = 0x800,分别表示栈底位置和栈空间大小。

164行的函数rt_hw_stack_init对于理解线程切换是一个相当重要的函数,其形参分别为:

线程入口函数:main_thread_entry
线程参数RT_NULL:
线程栈栈顶地址:thread->stack_addr + thread->stack_size - 4 = 0x20001138 + 0x800 - 4 = 0x20001934
Q1:为什么此处需要减4?
A2: 很有意思的一个问题。答案可参考本人在论坛的一个回答。RT-Thread-小白求助,关于rtt 的一段源码RT-Thread问答社区 - RT-Thread

Step 8. 单步进入到rt_hw_stack_init函数内部,开展分析

149行,由于传递进来的stack_addr = 0x20001934,执行完毕后,stk为0x20001938。从0x20001138(含)到0x20001934(含),合计是0x800 = 2048字节。STM32使用的满递减栈,所以此处的stk是0x20001938。
150行,此处设置8字节对齐。由于0x20001938 = (536877368)Decimal,该数据除8等于67109671,能被8整除,该语句执行栈对齐操作后,stk依然为0x20001938。

Step 9. 继续了解rt_hw_stack_init函数。

151行,更新stk的值,减去struct stack_frame结构体的大小。执行完毕后,stk = 0x200018F4。
153行,stack_frame指针指向0x200018F4。
156至159行,通过for循环将0x200018F4至0x20001938的所有内存变成0xdeadbeaf魔法字。
161行至168行,将stack_frame成员的exception_stack_frame中的r0~psr共8个寄存器分别设置为:线程参数,4个0,线程返回地址,线程入口地址,0x01000000。
175行,返回stk的值,此时变成0x200018F4。这个值在初始化线程时,将返回给thread->sp,即线程栈的临时栈顶指针。
依次将线程的形参、r1-r3, r12, 线程返回地址、线程入口地址,线程的xPSR写入异常栈帧结构中。
在初入门时,这里是难点。C语言中使用结构体定义的栈结构,如何和实际寄存器的顺序进行一一对应?,后文会通过逐步Debug揭示这个问题答案。

返回的stk指向0x200018F4部分。

至此,main线程创建完毕后,线程结构体和线程栈空间如下所示。

Step 10. 继续单步到rt_system_scheduler_start函数处,并单独跟踪进入到该函数内部。

期间,RT-Thread会调用rt_thread_idle_init函数,在该函数中使用静态创建方式初始化tidle0线程。可以按照上述过程记录tidle0线程的栈空间。

Step 11. 继续单步到rt_hw_context_switch_to函数处。

在rt_system_scheduler_start函数中,会依次获取最高优先级线程的线程控制块,将其复制给to_thread。如图所示,在表达式窗口的to_thread就是main线程。
&to_thread->spthread->sp的地址,在Debug中,地址编号为0x200010C8,即0x200010C8内存单元中存放的数据是0x200018F4。

Q2. 在单独进入到rt_hw_context_switch_to之前,观察输出结果,main线程被remove。为什么在启动调度器的函数中,要先将线程从就绪列表中移除呢?
A2. 下一步要启动main线程,将其从Ready状态变成Running状态,所以需要将该线程从就绪列表中删除,RT-Thread后续在调度时暂时不考虑该线程,直到该线程状态再次从Running发生变化。

Step 12. 单步到进入到rt_hw_context_switch_to函数处,该函数位于context_gcc.S文件,由汇编语言编写实现。

rt_hw_context_switch_to仅仅在调度器启动时运行一次。该函数的C语言实现接口中,有一个参数,传入thread->sp变量的地址。

对于参数个数不大于4的C语言接口函数,编译器会按参数在列表中的顺序,自左向右 为参数分配寄存器r0-r3。
对于参数个数大于4的C语言接口函数,编译器会按参数在列表中的顺序,多余参数按自右向左的顺序压入栈中,即参数入栈顺序与参数顺序相反。

如上述Tips,thread->sp的地址通过r0传递。在下图左侧寄存器窗口中,可以看到r0的值为0x200010C8。

165行,将变量rt_interrupt_to_thread变量的地址赋值给r1。
165行,将r0的值赋值给r1指向的单元,即将r0的值赋值给变量rt_interrupt_to_thread。如果此时在表达式窗口观察rt_interrupt_to_thread,会发现它的值为0x200010C8。

此时,main线程的线程结构体和线程栈空间不变,但是r0, r1, rt_interrupr_to_thread的内容均发生了变化。

对于rt_hw_context_switch_to函数的其他行,依次分析如下:

168行至172行,处理浮点寄存器入栈控制,与Cortex M4内核的Lazy Stacking有关,但与本文主线无关,不做探讨。
176至178行,将rt_interrupt_from_thread变量清零。因此本次是RT-Thread第一次调度最高优先级线程,只有to,没有from。
181至183行,将rt_thread_switch_interrupt_flag变量至1,该值将在PendSV中断中使用。
186-194行,设置SysTick和PendSV中断的优先级,且触发PendSV,但现在不跳转,因为中断为禁止。
197-201行,很有意思的一段操作,将0x08000000处的栈顶指针放置到MSP中,相当于特权模式的栈顶指针复位了。CPU从汇编编写的启动代码,直到运行到此处,均在特权模式下运行,使用MSP作为栈顶指针。将来切换到线程后,会以PSP作为栈顶指针。启动流程不会重来一次,也没有任何函数再需要返回。所以,对于截止到目前使用的MSP栈,可以舍弃栈中的数据,MSP栈重置。
204-205行,使能中断。首先在context_gcc.S的89行设置断点,然后当PC运行在204行时按F5,会运行至PendSV中断服务程序。

Step 13. PendSV函数分析。

在PendSV中断服务程序中:

94行-96行,判断rt_thread_switch_interrupt_flag的值,为0则退出,为1则继续;
99行-105行,rt_thread_switch_interrupt_flag清0,判断rt_interrupt_from_thread的值,为0表示OS第一次进行最高优先级就绪状态线程的运行,无需恢复psp,直接跳转到switch_to_thread;为1表示从from线程切换至to线程,需要恢复psp。Debug到此处,rt_interrupt_from_thread的值为0,是第一次进行线程运行。
此处直接分析127行开始的switch_to_thread部分。

128行,将rt_interrupt_to_thread的地址赋值给r1。
129行,从r1指向的地址中取出值,赋值给r1,此时r1指向到main线程的thread->sp。
130行,从r1指向的地址中取出值,赋值给r1,此时r1指向到0x200018F4,如下图所示。

133行-136行,将r1指向的0x200018F4开始的单元内容,依次装载到r3, r4-r11中。执行完毕后,R3中是flag的值,r4-r11中均为0xDEAFBEEF,且r1指向0x20001918。

139-140行,由于r3为0,浮点寄存器不做处理。r1保持不变。
143行,将r1的值赋值给PSP,线程栈顶指针PSP目前为0x20001918。后续PSP还会自动更新。

155行,使得LR寄存器的Bit2为1,确保PendSV异常返回使用的栈指针是PSP。
156行,异常返回。此时,线程栈中剩下内容,即从0x20001918-0x20001934的内容,会自动加载到R0, R1, R2, R3, R12, R14 (线程返回地址), PC (线程入口地址), xPSR。且,PSP会自动更新至0x20001938,即创建main线程时的栈顶指针。

Step 14. 光标在BX LR上时,按F5,自动运行到main线程入口地址main_thread_entry。

如下图所示,栈帧中的r0-r15, xPSR均已顺利从线程栈中进行了恢复,此时thread->sp = PSP = 0x20001938。开始顺利执行线程。

三、修改rt_hw_context_switch_to函数,使用SVC进入第一个线程

FreeRTOS使用SVC进入第一个线程,通过简单修改,在STM32L431RCT6 Cortex-M4内核上也可以支持用SVC进入第一个线程。 计划在线下课程中,与学生们面对面深入探讨一次。

对rt_hw_context_switch_to函数的修改过程如下:

删除对rt_interrupt_from_thread的清零
删除对rt_thread_switch_interrupt_flag的置1
删除对PendSV的触发
新增dsb isb
新增SVC 0
毫无意义,对R0赋值,通过Debug观察到该语句不会被执行

SVC

修改后的rt_hw_context_switch_to函数和SVC_Handler函数如下:

.global rt_hw_context_switch_to
.type rt_hw_context_switch_to, %function
rt_hw_context_switch_to:
LDR r1, =rt_interrupt_to_thread
STR r0, [r1]
#if defined ( VFP_FP ) && !defined( SOFTFP )
/* CLEAR CONTROL.FPCA /
MRS r2, CONTROL /
read /
BIC r2, #0x04 /
modify /
MSR CONTROL, r2 /
write-back /
#endif
/
set the PendSV and SysTick exception priority /
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 /
/
restore MSP /
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
NOP
MSR msp, r0
/
enable interrupts at processor level /
CPSIE F
CPSIE I
dsb
isb
SVC 0
/
never reach here! /
LDR r0, =0x12345678 / debug according to blta's comment /
.global SVC_Handler
.type SVC_Handler, %function
SVC_Handler:
/
disable interrupt to protect context switch /
MRS r2, PRIMASK
CPSID I
/
get rt_thread_switch_interrupt_flag /
switch_to_first_thread:
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] /
load thread stack pointer /
#if defined ( VFP_FP ) && !defined( SOFTFP )
LDMFD r1!, {r3} /
pop flag /
#endif
LDMFD r1!, {r4 - r11} /
pop r4 - r11 register /
#if defined ( VFP_FP ) && !defined( SOFTFP )
CMP r3, #0 /
if(flag_r3 != 0) */
VLDMIANE r1!, {d8 - d15} /* pop FPU register s16~s31 */
#endif
MSR psp, r1 /* update stack pointer */
#if defined (__VFP_FP__) && !defined(__SOFTFP__)
ORR lr, lr, #0x10 /* lr |= (1 << 4), clean FPCA. */
CMP r3, #0 /* if(flag_r3 != 0) */
BICNE lr, lr, #0x10 /* lr &= ~(1 << 4), set FPCA. */
#endif
svc_exit:
/* restore interrupt */
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr

四、小结

本文简单探讨了RT-Thread 4.0.5版本在STM32L431RCTx Cortex-M4内核上,创建main线程、tidle0线程后,从使用MSP的特权模式,启动至使用PSP线程模式的main线程栈帧恢复全过程。

SP寄存器有两个,分别是MSP和PSP,其中,从复位启动后使用MSP,通过启动代码、RT-Thread初始化、启动调度器的过程,切换至使用PSP的线程中运行。

每个线程均有独立的栈。使用rt_thread_create创建的线程,栈位于heap中;使用rt_thread_init创建的栈,栈位于自定义的数组中。

线程切换,即保存所有寄存器的快照到线程栈中,r0-r15, xPSR,浮点寄存器。线程恢复,即从线程栈中恢复寄存器快照。

在线程模式下,如果发生中断,会继续使用MSP。

Cortex M4发生中断,会有系列寄存器自动入栈处理的操作,本文不展开讨论。

RT-Thread的上下文切换的Context_gcc.S文件中rt_hw_context_switch_to也可以用SVC进行线程处理。

SVC

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分