Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread
线程栈stack
RT-Thread启动进入就绪态最高优先级线程的全过程与栈帧分析
发布于 2023-05-05 12:54:42 浏览:3001
订阅该版
[tocm] | 版本 | 1.0 | | ---- | :--------- | | 作者 | lchnu | | 第一版日期 | 2023-05-05 15:00| | 第二版日期 | 2023-05-07 17:55 在小结前新增第三节讨论SVC| 本文简单讨论RT-Thread在启动后,逐步进入到处于就绪态最高优先级main线程的全过程。部分内容涉及到汇编指令,但通俗易懂。通过简化工程,配合Debug过程,逐步观察寄存器的变化、绘制栈帧结构、绘制线程控制块和rt_interrupt_from_thread、rt_interrupt_to_thread等典型变量取值(指向,虽然是rt_uint32_t类型,但实际在汇编中是作为指针使用),能有效帮助理解RTOS的线程栈的恢复与启动过程。 通过本文对线程启动过程的了解,对于两个线程/多个线程之间的互相切换能奠定坚实的基础,化繁为简,结合论坛关于上下文切换的代码注释,能帮助快速抓住主线。 使用的软硬件环境如下: - 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线程。 ```c #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](https://club.rt-thread.org/ask/question/f77c1f9530c9a05c.html)  **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->sp`thread->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进入第一个线程 @blta发表过一篇很有意思的文章,探讨rt_hw_context_switch_to函数中`Never reach here`的注释问题。 https://club.rt-thread.org/ask/article/75d632b1a6dd513b.html FreeRTOS使用SVC进入第一个线程,`通过简单修改,在STM32L431RCT6 Cortex-M4内核上也可以支持用SVC进入第一个线程。` 计划在线下课程中,与学生们面对面深入探讨一次。 对rt_hw_context_switch_to函数的修改过程如下: 1. 删除对rt_interrupt_from_thread的清零 2. 删除对rt_thread_switch_interrupt_flag的置1 3. 删除对PendSV的触发 4. 新增dsb isb 5. 新增SVC 0 6. `毫无意义`,对R0赋值,通过Debug观察到该语句不会被执行   修改后的rt_hw_context_switch_to函数和SVC_Handler函数如下: ```c .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进行线程处理。 
2
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
lchnu
Witness, Understand, Skill
文章
10
回答
229
被采纳
88
关注TA
发私信
相关文章
1
RT-THREAD在STM32H747平台上移植lwip
2
正点原子miniSTM32开发板读写sdcard
3
反馈rtt串口驱动对低功耗串口lpuart1不兼容的问题
4
Keil MDK 移植 RT-Thread Nano
5
RT1061/1052 带 RTT + LWIP和LPSPI,有什么坑要注意吗?
6
RT thread HID 如何收发数据
7
求一份基于RTT系统封装好的STM32F1系列的FLASH操作程序
8
RT-Thread修改项目名称之后不能下载
9
rt-studio编译c++
10
有木有移植rt-thread(nano)到riscv 32位MCU上
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
DMA
USB
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
rt-smart
FAL
I2C_IIC
UART
ESP8266
cubemx
WIZnet_W5500
ota在线升级
PWM
BSP
flash
freemodbus
packages_软件包
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
编译报错
中断
Debug
rt_mq_消息队列_msg_queue
keil_MDK
ulog
SFUD
msh
C++_cpp
MicroPython
本月问答贡献
RTT_逍遥
8
个答案
2
次被采纳
KunYi
8
个答案
1
次被采纳
三世执戟
7
个答案
1
次被采纳
winfeng
2
个答案
1
次被采纳
chenyaxing
2
个答案
1
次被采纳
本月文章贡献
catcatbing
2
篇文章
5
次点赞
swet123
1
篇文章
3
次点赞
YZRD
1
篇文章
2
次点赞
Days
1
篇文章
2
次点赞
阳光的掌控者
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部