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

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:

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

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

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 的优先级
接下来分别将PendSV和SysTick异常的优先级设置为最低优先级(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

我们可以发现当执行完206行后
MSP 的数值已经恢复成初值了(0x200010E0)

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

保护上下文切换:
通过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),即目标线程的当前堆栈顶部。

通过上述步骤,获取目标线程的堆栈指针,为后续从堆栈中恢复上下文做准备。
针对栈帧的处理:
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中的线程栈帧是完全对应的,见下图:

►►►
Pendsv_Exit
作用:
恢复中断状态:
通过MSR PRIMASK, r2,恢复进入 PendSV 异常前的中断状态(使能或禁用),确保系统中断配置不受影响。
设置异常返回堆栈:
通过ORR lr, lr, #0x04,设置 EXC_RETURN[2] = 1,确保异常返回时使用PSP,切换到目标线程的堆栈。(线程模式使用 PSP,特权模式使用 MSP)。
完成异常退出:
通过BX lr,触发异常返回,处理器切换到线程模式,恢复目标线程的执行上下文(包括 PC、寄存器和堆栈)。
通过调试我们可以查看要切换的第一个线程(main)的入口地址是:0x080050A1

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

- 随机文章
- 热门文章
- 热评文章
- 播撒更多科学的种子(讲述・弘扬科学家精神(特别策划))
- 省油口碑好的亚洲龙和智能科技感强的君越艾维亚怎么选?
- 长城魏建军:水军没底线带节奏 友商传播逻辑确实技高一筹
- 春立医疗取得3D打印骨连接棒系统专利,新骨生长提供充足的支撑
- 黄山风景区零碳景区示范基地授牌 系全国首个山岳型零碳景区
- 五粮液亮相首届链博会,以“绿色”“和美”链接世界
- 四川力争到2027年全省万户优质企业融资规模破10000亿元
- 我实现单个自由基量子自旋转换调控