概述
概要
前面提到线程的运行状态、时间片、优先级,那么系统怎么判断当前时间该运行哪一个线程呢?这就要通过线程调度来实现。
我们来分析下源码,看下最终是调用哪些函数。
/* 让出处理器资源 */
rt_err_t rt_thread_yield(void)
-> void rt_schedule(void)
/* 线程休眠 */
rt_err_t rt_thread_sleep(rt_tick_t tick)
-> void rt_schedule(void)
可以发现,rt_schedule函数才是调度的核心。
一、rt_schedule函数
该函数定义在 rt-thread/src/scheduler.c 文件中。
void rt_schedule(void)
{
......
/* 关中断 */
level = rt_hw_interrupt_disable();
......
/* 获取优先级最高的线程 */
to_thread = _get_highest_priority_thread(&highest_ready_priority);
......
/* 如果当前线程的优先级低于获取到线程的优先级,则让出处理器资源 */
rt_current_thread->stat &= ~RT_THREAD_STAT_YIELD_MASK;
need_insert_from_thread = 1;
......
/* 开启切换 */
rt_current_priority = (rt_uint8_t)highest_ready_priority;
from_thread = rt_current_thread;
rt_current_thread = to_thread;
......
/* 将要切换线程从ready队列移除 */
rt_schedule_remove_thread(to_thread);
/* 将要切换线程的状态设为运行状态 */
to_thread->stat = RT_THREAD_RUNNING | (to_thread->stat & ~RT_THREAD_STAT_MASK);
......
/* 栈溢出检查 */
_rt_scheduler_stack_check(to_thread);
......
/* 这里假设是正常的切换,不是中断触发,所以进入该分支 */
rt_hw_context_switch((rt_ubase_t)&from_thread->sp,
(rt_ubase_t)&to_thread->sp);
......
}
其实,对于cortex-M4架构来说,无论是正常的切换还是中断触发,最终还是要进入rt_hw_context_switch这个汇编函数的,该函数相关定义在libcpu/arm/cortex-m4目录下,因为我使用的MDK编译,所以具体的文件是context_rvds.S。
(注:目前CSDN还不支持汇编代码高亮…)
/* 声明外部变量,这些变量定义在 libcpu/arm/cortex-m4/cpuport.c中 */
IMPORT rt_thread_switch_interrupt_flag
IMPORT rt_interrupt_from_thread
IMPORT rt_interrupt_to_thread
/* 变量赋值 */
NVIC_INT_CTRL EQU 0xE000ED04 ; interrupt control state register
NVIC_PENDSVSET EQU 0x10000000 ; value to trigger PendSV exception
/* 这里,要注意一下传进来的参数
* r0 - 存放的是被切换线程(from_thread)的sp变量
* r1 - 存放的是要切换线程(to_thread)的sp变量
*/
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch
/* 将rt_thread_switch_interrupt_flag的地址放入r2中
* 读rt_thread_switch_interrupt_flag的值到r3中
* 比较r3是否为1(即发生中断),若为1,则跳入_reswitch中
*/
LDR r2, =rt_thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
/* 将r3赋值为1
* 将r3中的值放入r2的地址中,即将rt_thread_switch_interrupt_flag设1
*/
MOV r3, #1
STR r3, [r2]
/* 将rt_interrupt_from_thread的地址放入r2中
* 将被切换线程的sp地址赋值给rt_interrupt_from_thread
*/
LDR r2, =rt_interrupt_from_thread
STR r0, [r2]
/* 将rt_interrupt_to_thread的地址放入r2中
* 将要切换线程的sp地址赋值给rt_interrupt_to_thread
*/
_reswitch
LDR r2, =rt_interrupt_to_thread
STR r1, [r2]
/* 将NVIC_INT_CTRL的寄存器地址放入r0
* 将触发pendsv异常的值放入r1
* 将r1的值放入r0寄存器中,触发pendsv异常
*/
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
ENDP
二、异常和中断向量表
PenSV,即可挂起的系统服务请求,OS一般用该异常进行上下文切换。
当向ICSR寄存器(Interrupt control and state register)的bit[28]写入1时候,就可以触发PenSV异常,NVIC就会跳入该异常的处理函数中。
NVIC会有一张异常和中断向量表,里面记载的相对偏移的异常处理函数,所以,为了确保能够找到相应的异常处理函数,那么我们需要配置 " 向量表偏移寄存器 " (VTOR),这样,就找到了相对偏移的基地址。
下面内容在startup_stm32l475xx.s文件中。
EXPORT __Vectors
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
......
DCD PendSV_Handler ; PendSV Handler
......
__Vectors_End
/* 代码后面的[WEAK]是符号弱化标识:
* 如果整个代码在链接时遇到名称相同的符号
* 那么代码将使用未被弱化定义的符号(与PendSV_Handler相同名称的函数)
* 而与弱化符号相关的代码将被自动丢弃
*/
PendSV_Handler PROC
EXPORT PendSV_Handler [WEAK]
B .
ENDP
在系统启动的时候,根据链接脚本board/linker_scripts/link.lds,会自动跳到Reset_Handler中执行。
MEMORY
{
ROM (rx) : ORIGIN = 0x08000000, LENGTH = 512k /* flash起始地址与大小(512KB) */
RAM1 (rw) : ORIGIN = 0x20000000, LENGTH = 96k /* sram起始地址与大小(96K) */
RAM2 (rw) : ORIGIN = 0x10000000, LENGTH = 32k /* sram起始地址与大小(32K) 只能CPU访问,不能DMA访问 */
}
ENTRY(Reset_Handler)
_system_stack_size = 0x200;
SECTIONS
{
.text :
{
. = ALIGN(4); //4字节对齐
_stext = .; //text段起始位置
KEEP(*(.isr_vector)) //中断向量表放在rom起始位置,keep关键字保证不被优化掉
......
. = ALIGN(4);
*(.text) //MDK中并没有定义isr_vector段,所以中断向量表放在这里
}
......
}
其中,Reset_Handler在startup_stm32l475xx.s文件中。
/* 定义一个名为.text的代码段,只读,那么下面的全部代码都放在.text段
*/
AREA |.text|, CODE, READONLY
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
而且,向量偏移表在初始化时候就已经设置好了。
/* stm32l475xx.h */
#define FLASH_BASE (0x08000000UL)
/* system_stm32l4xx.c */
#define VECT_TAB_OFFSET 0x00
void SystemInit(void)
{
......
/* 这里代码放在flash上,走下面分支 */
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
}
三、PenSV异常处理函数
这分析函数之前,先引入几个概念。
1、 MSP和PSP
M3/M4内核都具有双堆栈指针。它们的逻辑地址都是R13,但实际上有两个物理寄存器,一个为MSP(主堆栈指针),一个为PSP(进程堆栈指针)。
在RT-Thread中,在特权模式下,堆栈指针使用的是MSP,非特权模式下使用的是PSP。
2、 PRIMASK寄存器
PRIMASK用于禁止除NMI和HardFault之外的所有异常,对于汇编编程,可以利用CPS指令来修改PRIMASK寄存器的值。
3、 STMFD和LDMFD指令
对于LDM和STM指令来说,编号小的寄存器对应堆栈中的低地址。
STMFD,其中STM表示一次将多个寄存器的值存储到存储空间(如栈),FD表示满递减堆栈方式,即开始操作时sp地址 = (SP - 4),结束操作后sp地址 =(SP - 寄存器数量4) 。
LDMFD,其中LDM表示一次将多个存储空间(如栈)的值存储到寄存器,FD表示满递减堆栈方式,即开始操作时sp地址 = SP,结束操作后sp地址 = (SP + (寄存器数量4) - 4)。
4、 ARM寄存器
根据AAPCS,C函数可以修改R0-R3、R12、R14(LR)、PSR寄存器,如果C函数要用到R4-R11,那么就将这个寄存器保存到栈空间中,函数结束前将它们恢复。
R0-R3、R12、R14(LR)、PSR寄存器称为"调用者保存寄存器",调用子程序的代码(当前指被切换的线程)需要将这些寄存器内容保存到栈空间。
R4-R11称为"被调用者保存寄存器",被调用的子程序(当前指要切换的线程)要确保在函数结束时不会发生变化(即与进入函数时一样)。
发生PendSV异常前,内核硬件自动(不用程序操作)把当前线程的上下文(PSR 、PC、LR、R12、R3、R2、R1、R0)压入线程自己的堆栈。
ARM中使用的是满递减堆栈(Full decending,即FD),堆栈首部是高地址,堆栈向低地址增长。栈指针SP总是指向最后一个元素,即最后一个已进入栈的数据。
当PenSV异常退出时,新切入的线程的中断上下文(PSR 、PC、LR、R12、R3、R2、R1、R0)会自动(硬件执行,不用程序)的从线程中弹出,程序指针pc就获得了新线程的pc和这个线程中使用的寄存器的值,程序就运行到新线程中去了。
5、 PenSV的异常处理函数
具体的文件是rt-thread/libcpu/arm/cortex-m4/context_rvds.S。
PendSV_Handler PROC
EXPORT PendSV_Handler
/* 读PRIMASK寄存器的值到r2中 */
MRS r2, PRIMASK
/* 关中断 */
CPSID I
/* 如果rt_thread_switch_interrupt_flag等于0,则直接退出 */
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
/* 清零rt_thread_switch_interrupt_flag */
MOV r1, #0x00
STR r1, [r0]
/* 如果rt_interrupt_from_thread为0,则跳到switch_to_thread */
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread
/* 从psp中获取到被切换线程的sp */
MRS r1, psp
IF {FPU} != "SoftVFP"
TST lr, #0x10 ; if(!EXC_RETURN[4])
VSTMFDEQ r1!, {d8 - d15} ; push FPU register s16~s31
ENDIF
/* sp = sp - 4*8,
* 然后从低地址开始,依次将r4到r11寄存器的数值压入栈中
*/
STMFD r1!, {r4 - r11} ; push r4 - r11 register
IF {FPU} != "SoftVFP"
MOV r4, #0x00 ; flag = 0
TST lr, #0x10 ; if(!EXC_RETURN[4])
MOVEQ r4, #0x01 ; flag = 1
STMFD r1!, {r4} ; push flag
ENDIF
/* 更新被切换的线程的sp */
LDR r0, [r0]
STR r1, [r0]
switch_to_thread
/* 把要切换的线程的sp取到r1中 */
LDR r1, =rt_interrupt_to_thread
/* 加载 rt_interrupt_to_thread 的值到r1, 即sp指针的指针 */
LDR r1, [r1]
/* 加载 rt_interrupt_to_thread 的值到r1, 即sp */
LDR r1, [r1] ; load thread stack pointer
IF {FPU} != "SoftVFP"
LDMFD r1!, {r3} ; pop flag
ENDIF
/* 将要切换的线程的sp中的值依次弹出到R4-R11寄存器中 */
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
IF {FPU} != "SoftVFP"
CMP r3, #0 ; if(flag_r3 != 0)
VLDMFDNE r1!, {d8 - d15} ; pop FPU register s16~s31
ENDIF
/* 将要切换的线程的sp放入psp中 */
MSR psp, r1 ; update stack pointer
IF {FPU} != "SoftVFP"
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
pendsv_exit
/* 将之前读出的值重新放入PRIMASK寄存器中 */
MSR PRIMASK, r2
/* 确保PendSV中断返回后使用的是psp指针
* 此时psp已经指向了所运行任务的堆栈
*/
ORR lr, lr, #0x04
BX lr
ENDP
那么,要切换的线程示意图如下
被切换的线程示意图如下
最后
以上就是潇洒猫咪为你收集整理的【RTT】线程(3):线程的调度概要一、rt_schedule函数二、异常和中断向量表三、PenSV异常处理函数的全部内容,希望文章能够帮你解决【RTT】线程(3):线程的调度概要一、rt_schedule函数二、异常和中断向量表三、PenSV异常处理函数所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复