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

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。开始顺利执行线程。
通过本文对线程启动过程的了解,对于两个线程/多个线程之间的互相切换能奠定坚实的基础,化繁为简,结合论坛关于上下文切换的代码注释,能帮助快速抓住主线。
使用的软硬件环境如下:
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观察到该语句不会被执行
修改后的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进行线程处理。

三星s8最新消息:三星S8价格仅次于华为Mate9保时捷版,性能却更强
带有直流电动机的小型电池供电汽车的制作
简述该如何防止高温线进水?
中微爱芯通用逻辑芯片AIP74HC14,可替代恩智浦74HC14、德州仪器CD74HC14
关于PSP-SOI模型在RF SOI工艺上的优势分析和应用
RT-Thread启动进入就绪态最高优先级线程的全过程与栈帧分析(下)
工控主板如何安装,具体安装步骤是怎样的
大数据在支付领域怎样改善用户体验
FreeRTOS_003 _让系统在板子上跑起来
可穿戴设备中的传感器:尺寸越来越“小”,左右越来越“大”
莱迪思半导体针对工业市场提供增强的视频桥接解决方案
行业首个基于地理位置感知控制智能家居系统曝光
变频电机的接线方法
豪威发布国内首款支持功能安全ASIL B的汽车摄像头PMIC
电动势的定义
追觅多款扫地机器人通过TÜV莱茵防缠绕和高效自清洁认证
DFM在PCBA设计中的作用
中国300多家智能手机品牌,半数已阵亡
电桩测试仪的单/三相适配器METRALINE PRO-TYP EM I介绍
基于DSP和μC/OS-II操作系统的双CPU架构实现微机保护装置的设计