我是靠谱客的博主 潇洒猫咪,最近开发中收集的这篇文章主要介绍【RTT】线程(3):线程的调度概要一、rt_schedule函数二、异常和中断向量表三、PenSV异常处理函数,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

概要

  前面提到线程的运行状态、时间片、优先级,那么系统怎么判断当前时间该运行哪一个线程呢?这就要通过线程调度来实现。
  我们来分析下源码,看下最终是调用哪些函数。

/* 让出处理器资源 */
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异常处理函数所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部