深度剖析 RT-Thread 线程调度流程

频道:消费点评 日期: 浏览:42087

RT-Thread 调度第一个线程的主要流程分如下:

97b0dbf8-51ae-11f0-986f-92fbcf53809c.png

rtthread_startup:RTT的启动函数,主要负责板级驱动,调度器,系统线程初始化,启动调度的工作

rt_system_scheduler_start:调度系统第一个线程

rt_hw_context_switch_to:初始化上下文切换环境,触发 PendSV 异常

first_thread:系统中优先级最高的线程的入口函数

►►►

rt_hw_context_switch_to

rt_hw_context_switch_to((rt_uintptr_t)&to_thread->sp):

加载目标线程的堆栈状态,使CPU开始执行目标线程的代码

(rt_uintptr_t)&to_thread->sp表示目标线程的堆栈指针地址to_thread->sp是目标线程的堆栈指针,此时SP=0x2000198C

函数原型:

rt_hw_context_switch_to PROC EXPORT rt_hw_context_switch_to ; set to thread LDR r1, =rt_interrupt_to_thread STR r0, [r1]

这段汇编可以类比为C语言写法:

rt_uintptr_tr0 = (rt_uintptr_t)&to_thread->sp;// r0 传入的栈指针地址rt_uintptr_t*r1 = &rt_interrupt_to_thread;// r1 加载 rt_interrupt_to_thread 的地址*r1 = r0;// 将 r0 的值赋给 r1 指向的地址

此时rt_interrupt_to_thread地址存储的是目标线程SP的地址,通过内存窗口查看0x2000113c地址中内容,内容即为0x2000198C

97d77920-51ae-11f0-986f-92fbcf53809c.png

接下来设置rt_interrupt_from_thread的值为0,表示启动第一次线程切换:

980c80d4-51ae-11f0-986f-92fbcf53809c.png

设置中断标志位rt_thread_switch_interrupt_flag的值为1,因为 Cortex-M 是通过 PendSV 异常进行线程切换的,所以这里置位表示要进行切换,稍后会用到。

98219514-51ae-11f0-986f-92fbcf53809c.png

NVIC_SYSPRI2 EQU0xE000ED20;system priorityregister(2)NVIC_PENDSV_PRI EQU 0xFFFF0000 ;PendSVandSysTick priorityvalue(lowest);setthe PendSVandSysTick exception priorityLDR r0, =NVIC_SYSPRI2 ; 将 NVIC_SYSPRI2 的地址加载到 r0LDR r1, =NVIC_PENDSV_PRI ; 将 NVIC_PENDSV_PRI 的值加载到 r1LDR.W r2, [r0,#0x00] ; 读取 NVIC_SYSPRI2 的当前值到 r2ORR r1, r1, r2 ; 将 r1 和 r2 的值进行按位或操作,保持其他位不变STR r1, [r0] ; 将修改后的值写回 NVIC_SYSPRI2,更新 PendSV 和 SysTick 的优先级

接下来分别将PendSVSysTick异常的优先级设置为最低优先级(0xFFFF0000)

最低优先级的意义:

延迟上下文切换:通过将 PendSV 的优先级设置为最低(0xFF),确保它在所有其他中断(例如硬件中断或更高优先级的异常)处理完成后才会执行。这避免了上下文切换被高优先级中断打断,保证了中断处理程序的实时性。

避免抢占:在 RTOS 中,上下文切换是一个相对耗时的操作(涉及保存/恢复寄存器、栈操作等)。将 PendSV 设置为最低优先级,确保它不会抢占其他关键中断(如定时器、外部设备中断),从而提高系统的实时性和稳定性。

;trigger the PendSVexception(causes contextswitch)LDR r0, =NVIC_INT_CTRL ; 将 NVIC_INT_CTRL 的地址加载到 r0LDR r1, =NVIC_PENDSVSET ; 将 NVIC_PENDSVSET 的值加载到 r1STR r1, [r0] ; 将 r1 的值写入 NVIC_INT_CTRL 寄存器

这段代码通过向 NVIC 的 ICSR 寄存器写入NVIC_PENDSVSET值,手动的触发 PendSV 异常。触发后,处理器会在当前中断或异常处理完成后调用PendSV_Handler,执行线程上下文切换。


SCB_VTOR EQU0xE000ED08; VectorTableOffsetRegisterLDR r0,=SCB_VTOR ; 将 SCB_VTOR 的地址加载到 r0LDR r0, [r0] ; 从 SCB_VTOR 地址读取向量表地址到 r0LDR r0, [r0] ; 从向量表起始地址读取初始 MSP 值到 r0MSR msp, r0 ; 将 r0 的值写入 MSP 寄存器

这段代码从向量表中提取初始主堆栈指针(MSP)的值,并将其恢复到 MSP 寄存器。

作用是重置主堆栈指针(MSP)到系统启动时的初始状态,通常用于初始化或上下文切换的场景,确保处理器在特权模式下使用正确的堆栈。

芯片启动时(Reset_Handler),MSP的数值为:0x200010E0

98329738-51ae-11f0-986f-92fbcf53809c.png

我们可以发现当执行完206行后

MSP 的数值已经恢复成初值了(0x200010E0)

984779aa-51ae-11f0-986f-92fbcf53809c.png


CPSIE F ; 启用故障异常(Fault)CPSIEI; 启用中断(Interrupt)

CPSIE I:

指令含义:CPSIE I 用于启用中断。

作用:该指令清除PRIMASK寄存器的值(将其置为 0),从而启用所有配置为可屏蔽的中断(包括外部中断和系统异常)。

背景

PRIMASK 是一个特殊寄存器,用于控制中断的屏蔽状态。当 PRIMASK = 1 时,除 NMI 和 HardFault 外的所有中断被禁用。

因此到了这里代表即将要触发 PendSV 中断来进行线程切换


; clear the BASEPRI register todisablemasking priorityMOV r0,#0x00 ; 将 r0 设置为 0MSR BASEPRI, r0 ; 将 r0 的值(0)写入 BASEPRI 寄存器

作用:

在触发 PendSV 异常后,清除 BASEPRI 确保不会因为之前的优先级屏蔽设置而阻止任何中断的触发。

这是初始化线程环境的一部分,确保目标线程运行时,处理器能够响应所有可屏蔽中断。

►►►

PendSV_Handler

PendSV_Handler PROCEXPORT PendSV_Handler; disable interrupt to protect contextswitchMRS r2, PRIMASK ; 将 PRIMASK 寄存器的值读取到 r2CPSID I ; 禁用中断(设置 PRIMASK =1);getrt_thread_switch_interrupt_flagLDR r0, =rt_thread_switch_interrupt_flag ; 将 rt_thread_switch_interrupt_flag 的地址加载到 r0LDR r1, [r0] ; 从该地址读取值到 r1CBZ r1, pendsv_exit ; 如果 r1 为0,跳转到 pendsv_exit

985c8796-51ae-11f0-986f-92fbcf53809c.png

保护上下文切换:

通过CPSID I禁用中断,确保上下文切换过程不被其他中断打断。

保存PRIMASK(通过 MRS r2, PRIMASK)允许在切换完成后恢复原始中断状态,避免影响系统的中断配置。

检查切换请求:

通过读取rt_thread_switch_interrupt_flag,检查是否需要执行上下文切换。如果标志为 0,说明无需切换,直接退出,减少不必要的开销。


LDR r0, =rt_interrupt_from_thread ; 将 rt_interrupt_from_thread 的地址加载到 r0LDR r1, [r0] ; 从该地址读取值到 r1CBZ r1, switch_to_thread ; 如果 r1 为 0,跳转到 switch_to_thread

这段代码检查rt_interrupt_from_thread是否为 0,以决定是否需要保存当前线程的上下文。

如果 r1 为 0,说明无需保存上下文,直接跳转到switch_to_thread加载目标线程的上下文。

如果 r1 非零,说明有来源线程需要保存上下文,代码会继续执行后续的寄存器保存操作(如保存 r4-r11 和 FPU 寄存器)。

联系上文,rt_interrupt_from_thread在系统启动时在函数rt_hw_context_switch_to中已经置为0,所以此时应该切换到switch_to_thread函数


switch_to_threadLDR r1, =rt_interrupt_to_thread ; 将 rt_interrupt_to_thread 的地址加载到 r1LDR r1, [r1] ; 从该地址读取值到 r1LDR r1, [r1] ; 从该值指向的地址读取线程堆栈指针到 r1

作用:

rt_interrupt_to_thread存储目标线程的堆栈指针地址(rt_hw_context_switch_to函数传入的第一个参数)。

第一次 LDR r1, [r1] 从rt_interrupt_to_thread读取目标线程的堆栈指针地址(例如,&to_thread->sp)。

第二次 LDR r1, [r1] 从该地址读取实际的堆栈指针值(to_thread->sp),即目标线程的当前堆栈顶部。

9871eadc-51ae-11f0-986f-92fbcf53809c.png

通过上述步骤,获取目标线程的堆栈指针,为后续从堆栈中恢复上下文做准备。


针对栈帧的处理:

IF{FPU} !="SoftVFP"LDMFDr1!, {r3} ; 弹出 FPU 使用标志到 r3ENDIFLDMFDr1!, {r4 - r11} ; 弹出 r4 - r11 寄存器IF{FPU} !="SoftVFP"CMPr3, #0; 如果 flag_r3 !=0VLDMFDNEr1!, {d8 - d15} ; 弹出 FPU 寄存器 s16~s31ENDIFMSRpsp, r1 ; 更新堆栈指针到 PSPIF{FPU} !="SoftVFP"ORRlr, lr, #0x10 ; lr |= (1<< 4),清除 FPCACMP r3, #0 ; 如果 flag_r3 != 0BICNE lr, lr, #0x10 ; lr &= ~(1 << 4),设置 FPCAENDIF

作用:

如果处理器支持硬件浮点单元,从目标线程的堆栈中弹出 FPU 使用标志。

从目标线程的堆栈中弹出寄存器 r4 到 r11 的值,恢复目标线程的通用寄存器状态。

r4-r11 是 ARM 架构中需要手动保存的寄存器,r0-r3 和 r12 等由硬件自动压栈

可见压栈的顺序和cpuport.c中的线程栈帧是完全对应的,见下图:

98896554-51ae-11f0-986f-92fbcf53809c.png

►►►

Pendsv_Exit

作用:

恢复中断状态

通过MSR PRIMASK, r2,恢复进入 PendSV 异常前的中断状态(使能或禁用),确保系统中断配置不受影响。

设置异常返回堆栈

通过ORR lr, lr, #0x04,设置 EXC_RETURN[2] = 1,确保异常返回时使用PSP,切换到目标线程的堆栈。(线程模式使用 PSP,特权模式使用 MSP)。

完成异常退出

通过BX lr,触发异常返回,处理器切换到线程模式,恢复目标线程的执行上下文(包括 PC、寄存器和堆栈)。

通过调试我们可以查看要切换的第一个线程(main)的入口地址是:0x080050A1

989fbba6-51ae-11f0-986f-92fbcf53809c.png

当执行完成pendsv_exit函数后,进入main_thread_entry线程入口函数,可以看到PC指向的地址是移植的,至此已经完成了 RT-Thread 的第一个线程切换。

98b699e8-51ae-11f0-986f-92fbcf53809c.png

  • 随机文章
  • 热门文章
  • 热评文章
关键词:优先级地址芯片