STM32:rtthread_schedule调度

发布时间 2023-06-19 13:35:13作者: caesura_k

rtthread作为多线程管理的实时操作系统,那么线程与线程之间又是如何切换管理的呢?

rtthread中对于多线程切换是通过优先级表搭配优先级组进行调度的,优先级表中存储切换的上下线程节点,优先级组用来判断当前的最高优先级;

为了方便理解,在引入优先级表和优先级组之前,需要先了解一下什么是临界段保护,优先级表中的节点又是如何增删查改的;

优先级表的调度函数中调用了context_switch汇编函数配置pendsv中断,所以优先级表之后还需要了解一下context环境切换;

1 临界段保护

  cm3内核配置了pendsv中断来实现上下线程的切换,而代码处于线程切换的临界段时需要保护线程切换不被打断;

  cm3内核又配置了中断屏蔽寄存器组和对应的CPS指令用来快速开关所有中断,这个操作称之为临界段保护;

  临界段保护同时也可以保护实时性要求高的代码段执行的时候不被打断;

  1.1 内核中断屏蔽寄存器组

    1.1.1 PRIMASK 寄存器:   1bit的寄存器,初始值为0;置1后可屏蔽除NMI、hard fault之外的所有中断响应;

    1.1.2 FAULTMASK 寄存器:   1bit的寄存器,初始值为0;置1后可屏蔽除NMI之外的所有中断响应;

    1.1.3 BASEPRI 寄存器:     9bit的寄存器,初始值为0;可屏蔽所有优先级大于它的的中断响应;值为0则不屏蔽;

  1.2 CPS指令

    1.2.1 CPSIE:Change Processor State Interrupt or abort enable;

      CPSIE   I       ;PRIMASK=0,也是初始值;除NMI、hard fault之外处理器中断使能;

      CPSIE   F      ;FAULTMASK=0,也是初始值;除NMI之外处理器异常使能;

    1.2.2 CPSID:Change Processor State Interrupt or abort disable;

      CPSID   I      ;PRIMASK=1,屏蔽中断;除NMI、hard fault之外处理器中断禁能;

      CPSID   F     ;FAULTMASK=1,屏蔽中断;除NMI之外处理器异常禁能;

  1.3 例程

rt_hw_interrupt_disable    PROC
    EXPORT  rt_hw_interrupt_disable
    MRS     r0, PRIMASK
    CPSID   I
    BX      LR         ;子函数返回,将pc还给主调函数;
    ENDP

rt_hw_interrupt_enable    PROC
    EXPORT  rt_hw_interrupt_enable
    MSR     PRIMASK, r0
    BX      LR
    ENDP
    
;把PRIMASK作为参数传递,这样重新开中断的时候返回的状态是关中断之前的状态;
;把PRIMASK作为参数传递,是为了防止中断嵌套的时候外层中断还没执行完,PRIMASK就被内层中断清0了;

2 节点函数

  当插入的list_node前后都有node时,插入list_node的代码顺序应该如图,如果是after则从后往前写代码,如果是before则从前往后写代码,完美;

  

  2.1 第一次插入node

    list_head和list_node在初始化的时候,都初始化成了自身地址;如果只有一个list_head,那么在插入第一个list_node的时候节点代码又是如何处理的呢?

    以insert_after函数举例,步骤1的l->next->prev = l->prev = n;即list_head->prev = list_rear;  list首可寻址尾;

                步骤3的n->next = l->next = l;           即list_rear->next = list_head;list尾可寻址首;

    这个首尾寻址十分巧妙嘞,既不用多余的代码处理第一次插入,又使得链表可以时间片轮询,一举两得;

#ifndef __RT_SERVICE_H__
#define __RT_SERVICE_H__
//rtservice.h


//已知结构体节点,反推结构体首地址;
#define rt_container_of(ptr, type, member)    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))			
#define rt_list_entry(node, type, member)    rt_container_of(node, type, member)

//简单整理了一下上面的宏变成下面这样方便理解;只有最外层和node的括号可以去,其他括号都有用;
//#define  rt_list_entry(node, struct, member)   (struct *)(   (char *)node - (unsigned long)( &((struct *)0)->member )   )
		

//将list_head初始化成自身,这样第二个node插入之后,会让两个节点首位相连十分地妙;
rt_inline void rt_list_init(rt_list_t *l)
{
    l->next = l->prev = l;
}

//将n_node插入l_node之后,代码顺序如图,如果先修改l节点则存储在l节点中的数据就会丢失;
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
    l->next->prev = n;
    n->prev = l;
    n->next = l->next;
    l->next = n;   
}

//将n_node插入l_node之前,代码顺序如图;
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
    l->prev->next = n;
    n->next = l;
    n->prev = l->prev;
    l->prev = n;
}

//将前后节点连起来后,再初始化n_node为自身地址;
rt_inline void rt_list_remove(rt_list_t *n)
{
    n->prev->next = n->next;
    n->next->prev = n->prev;  
    n->next = n->prev = n;
}

//空节点的next和prev都被初始化成了自身,判断一个即可;
rt_inline int rt_list_isempty(const rt_list_t *l)
{
    return l->next == l;
}

#endif /* __RT_SERVICE_H__ */

3 线程调度scheduler

  rtthread中对于多线程切换是通过优先级表搭配优先级组进行调度的,优先级表中存储所有线程的node,优先级组中存储线程的优先级;

  程序一开始通常都是先对系统的硬件软件初始化,对于线程调度的逻辑功能而言,此时需要先初始化优先级表和组、定义线程调度时会用到的中间量;

  程序接下来通常就是对rt_object如线程的初始化,线程在初始化完之后会将自身优先级配置对应优先级表和组;

  线程调度函数比想象中简单的多,主要分为两个函数,一个是第一次执行调度的函数、一个是之后执行调度的函数;

  3.1 线程就绪优先级组

    线程就绪优先级组是一个32bits常数,每1bit对应一个优先级,通过对优先级组中最低位的判断可以知道当前线程的最高优先级;

    优先级组用来配合优先级表进行系统调度;

    3.1.1 优先级组定义

//scheduler.c
rt_uint32_t rt_thread_ready_priority_group;   //线程就绪优先级组
rt_uint8_t rt_current_priority;               //当前线程优先级,是个全局变量,但是没用过,应该以后会用到,先放着;

    3.1.2 优先级组查询

      如果对优先级组按bit判断取出最高优先级,rtthread觉得那样浪费时间,所以rtthread采用了一种以内存换效率的数组寻址优先级,搭配逻辑判断使用;

      为什么不32bit全部采用数组寻址,可能全部采用数组寻址内存占用又太大划不来;

//scheduler.c 优先级组调用方式;
    highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
    to_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next, struct rt_thread, tlist);

//kservice.c 
//判断函数中对优先级都进行了+1处理,所以调用函数中又-1处理;
int __rt_ffs(int value)
{
    if (value == 0) 
        return 0;                                                    //优先级组为空,调用函数中-1处理得到-1 error;
    if (value & 0xff)
        return __lowest_bit_bitmap[value & 0xff] + 1;                //bit[7:0],优先级为0也会+1处理;
    if (value & 0xff00)
        return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9;       //bit[15:8]
    if (value & 0xff0000)
        return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17;   //bit[23:16]
    
    return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25;     //bit[31:24]
}
//咋一看还以为是什么复杂的东西,其实这个数组纯粹力气活,把可能性(0:255)依次穷举列出,然后再把该数值的最高优先级存入在数值所在位;
//下面的优先级是所在字节的实际优先级
const rt_uint8_t __lowest_bit_bitmap[] =
{
    /* 00 */ 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};

  3.2 线程就绪优先级表

    线程就绪优先级表是一个有32个数组对象的数组,每一个对应一个优先级的list_node,这些list_node上挂载的节点就像是排队一样先进先出等着依次调用;

    rtthread中的系统调度就是优先级表搭配优先级组进行线程切换的过程,对优先级表的操作即系统调度;

    3.2.1 优先级表定义

//scheduler.c
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]; //优先级表;
struct rt_thread *rt_current_thread;                        //当前线程指针;

    3.2.2 scheduler初始化

//在main.c中程序一开始初始化系统软件的时候调用;此时线程对象还没初始化呢;
void rt_system_scheduler_init(void)
{	
    register rt_base_t offset;	
	for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
        rt_list_init(&rt_thread_priority_table[offset]);
    rt_current_thread = RT_NULL;                            //初始化优先级表,以及优先级表相关的rt_current_thread
    
    rt_thread_ready_priority_group = 0; 
    rt_current_priority = RT_THREAD_PRIORITY_MAX - 1;       //初始化优先级组,以及优先级组相关的rt_current_priority
}

    3.2.3 scheduler增删

      为什么shceduler只有增删操作,而没有查改操作呢?因为list先进先出的排队属性,所以不需要查改操作;

//将node插入list之后,置位list所对应优先级组bit;
void rt_schedule_insert_thread(struct rt_thread *thread)
{
    register rt_base_t temp;
    temp = rt_hw_interrupt_disable();
    
    thread->stat = RT_THREAD_READY;
    rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]), &(thread->tlist));
    rt_thread_ready_priority_group |= thread->number_mask;
    
    rt_hw_interrupt_enable(temp);
}

//将node移出list之后,如果list为空那么清零list所对应优先级组bit;
void rt_schedule_remove_thread(struct rt_thread *thread)
{
    register rt_base_t temp;
    temp = rt_hw_interrupt_disable();
    
    rt_list_remove(&(thread->tlist));
    if (rt_list_isempty(&(rt_thread_priority_table[thread->current_priority])))
        rt_thread_ready_priority_group &= ~thread->number_mask;
    
    rt_hw_interrupt_enable(temp);
}

    3.2.4 scheduler调度

       在优先级组中取出rt_current_priority,根据rt_current_priority在优先级表中取出to_thread;然后to_thread放入rt_current_thread中;启动调度函数

//第一次启动系统调度;
void rt_system_scheduler_start(void)
{
    register struct rt_thread *to_thread;

    rt_hw_interrupt_disable(); 	     //这个关中断是从main函数中搬过来的,感觉应该放这儿;没有enable是因为这个函数不会返回可以不写;                             
    rt_current_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
    to_thread = rt_list_entry(rt_thread_priority_table[rt_current_priority].next,  struct rt_thread,  tlist);
    rt_current_thread = to_thread;

    rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);                       
}

//系统调度切换;
void rt_schedule(void)
{
    rt_base_t level;
    struct rt_thread *to_thread;
    struct rt_thread *from_thread;

    level = rt_hw_interrupt_disable();
    rt_current_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
    to_thread = rt_list_entry(rt_thread_priority_table[rt_current_priority].next,  struct rt_thread,  tlist);
	
    //此时to_thread是优先级表中最高优先级了,要么切换过去,要么正在执行的就是to_thread;
    if (to_thread != rt_current_thread)
    {
        from_thread = rt_current_thread;
        rt_current_thread = to_thread;

        rt_hw_context_switch((rt_uint32_t)&from_thread->sp, (rt_uint32_t)&to_thread->sp);   
        rt_hw_interrupt_enable(level);
    }
    else 
        rt_hw_interrupt_enable(level);
}

/***野火代码中系统调度时,最高优先级用的是highest_ready_priority变量,觉得不整齐我改成了使用rt_current_priority;备注一下;***/

    3.2.5 **

rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]), &(thread->tlist)); 
to_thread = rt_list_entry(rt_thread_priority_table[rt_current_priority].next,  struct rt_thread,  tlist);
//如果node都是插入到list_head之前,那么第一个插入的node在最前面,即table[list_head].next;
//如果node都是插入到list_head之后,那么第一个插入的node在最后面,即table[list_head].prev;
//原理见2.1节,这个问题困扰了我相当长的时间,我决定给这个问题足够的关注度,两个小节都是你呢;

4 context环境切换

  在scheduler调度中只是从优先级组和优先级表中取出了就绪线程,然后将就绪线程的sp指针的地址作为参数调用context环境切换函数;

  在context环境切换函数中配置pendsv异常,然后触发pendsv异常,在pendsv中断函数中执行堆栈的保存与切换;

  感觉没啥用还是忍不住提一句,这个"context环境切换"就是许多文章和博客里的"上下文切换"; 

  rtthread中的环境切换函数和pendsv中断函数都是汇编函数,存储在context_rvds.s文件中,本节对context_rvds.s文件解构;

;***context_rvds.s文件***
import  rt_thread_switch_interrupt_flag
import  rt_interrupt_from_thread
import  rt_interrupt_to_thread
		
;STM32F10xxx Cortex-M3 programming manual SCB系统控制块
SCB_VTOR        equ     0xE000ED08     ; 向量表偏移寄存器
NVIC_INT_CTRL   equ     0xE000ED04     ; 中断控制状态寄存器
NVIC_SYSPRI2    equ     0xE000ED20     ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI equ     0x00FF0000     ; PendSV 优先级值 (lowest)
NVIC_PENDSVSET  equ     0x10000000     ; 触发PendSV exception的值
	
area |.text|, code, readonly, align=2
thumb
require8
preserve8

;......此处省略多个汇编函数;
;......省略的汇编函数在后面部分;

align 4
end
//cpuport.c;两个sp指针相关变量,一个标志量;
//在context_switch函数中对这三个变量处理,然后在pendsv中使用他们push和pop堆栈;
rt_uint32_t rt_interrupt_from_thread;
rt_uint32_t rt_interrupt_to_thread;
rt_uint32_t rt_thread_switch_interrupt_flag;

  4.1 中断函数

    见1.3节;

  4.2 rt_hw_context_switch_to函数

rt_hw_context_switch_to    proc
    export  rt_hw_context_switch_to
		
    ldr     r1,     =rt_interrupt_to_thread
    str     r0,     [r1]                  ;&to_thread->sp放入rt_interrupt_to_thread;
    ;ldr    [r1],   r0                    ;ldr和str都是寄存器放中间,地址放后面的,所以这样不行;
	
    mov     r0,    #0x00
    ldr     r1,    =rt_interrupt_from_thread
    str     r0,	  [r1]                    ;#0放入rt_interrupt_from_thread表示第一次初始化;
	
    mov     r0,    #0x01
    ldr     r1,    =rt_thread_switch_interrupt_flag
    str     r0,	  [r1]                    ;#1放入rt_thread_switch_interrupt_flag作为pendSV的中断标志
 
                                          ;配置pendSV的优先级,开启pendSV异常;原理见NVIC_SCB部分,先放着;
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]                ; 读  .W的意思是将ldr指令强制转换成32bit地址宽度的thumb-2指令,搞不懂为什么要特地转化,先放着;
    ORR     r1,r1,r2                      ; 改
    STR     r1, [r0]                      ; 写
 
                                          ; 触发 PendSV 异常 (产生上下文切换)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]
	
    cpsie  F
    cpsie  I                              ;不明白这里为啥要开两个中断,可能为了防止其中一个没清0导致中断没开;
    endp

  4.3 rt_hw_context_switch函数

rt_hw_context_switch    proc
    export rt_hw_context_switch
		
    ldr    r2,    =rt_thread_switch_interrupt_flag
    ldr    r3,    [r2]
	
    cmp    r3,    #0x01
    beq    _reswitch                      ;如果**_interrupt_flag为1,那么from_thread和interrupt_flag都是前面的switch_to函数配置的,就跳过不配置了;
                                          ;否则顺序执行,配置rt_thread_switch_interrupt_flag和rt_interrupt_from_thread;
    mov    r3,    #0x01
    str    r3,    [r2]                    ;如果不为1,则rt_thread_switch_interrupt_flag置1;
	
    ldr    r2,    =rt_interrupt_from_thread
    str    r0,    [r2]	
	
_reswitch		
    ldr    r2,    =rt_interrupt_to_thread
    str    r1,    [r2]
 
                                          ;触发PendSV异常,实现上下文切换;原理见NVIC_SCB部分,先放着;
    LDR     r0, =NVIC_INT_CTRL              
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]	
 
    bx lr
    endp

  4.4 PendSV_Handler函数

PendSV_Handler proc
    export PendSV_Handler

    msr    r2,    primask
    cpsid  I

    ldr    r0,    =rt_thread_switch_interrupt_flag
    ldr    r1,    [r0]
    cbz    r1,    pendsv_exit             ;中断标志位为0,不是中断退出;

    mov    r1,    #0x00
    str    r1,    [r0]                    ;中断标志位位1,那么清0中断标志位后继续;

    ldr    r0,    =rt_interrupt_from_thread
    ldr    r1,    [r0]
    cbz    r1,    switch_to_thread        ;如果是等于0表示第一遍启动,不需要保存from_thread的栈参数;跳过下面部分

;push from_thread
    mrs   r1,    psp                      ;psp此时为栈底r0地址,栈底的前几个寄存器已经自动push了;
    stmfd r1!,   {r4-r11}                 ;将r11-r4依次存入栈中;此时r1为r4地址
    ldr   r0,    [r0]                     ;r0里的数据为 &rt_interrupt_from_thread,
                                          ;[r0]为将r0里的数据作为地址,对地址内的数据操作,数据为&from_thread->sp
    str   r1,    [r0]                     ;地址内的数据即from_thread->sp,该数据为sp指针值,即栈底r4地址;
                                          ;保存r4的地址到from_thread->sp指针的地址;这个指针本来也指向栈r4地址呀,为什么还要再保存一遍呢?
                                          ;可能这个sp指针在其他地方可能被更改,所以这里又给它初始化一下;初始化成r4地址;
;pop to_thread	
switch_to_thread
    ldr   r0,    =rt_interrupt_to_thread
    ldr   r0,    [r0]                     ;地址内的数据为 &to_thread->sp
    ldr   r0,    [r0]                     ;地址内的数据为 to_thread->sp,该指针内的数据为栈底r4地址
    ldmfd r0!,   {r4-r11}                 ;加载完后,此时r0内的数据为栈底r0地址;
    msr   psp,   r0                       ;为什么要将栈底r0的地址放入psp指针呢?

pendsv_exit
    msr   primask,   r2
    orr   lr,   lr,#0x04                  ;lr的bit[2]为1,异常返回的是psp指针;
    bx    lr

endp

5 小结

  数据结构都有一个rt_object对象,这个rt_object对象中有list节点,rt_object对象初始化的时候顺便把list节点挂载到容器列表上了;

  数据结构本身还有一个节点;比如线程的节点将线程挂载到优先级表,计时器的节点将本身挂载到计时器链表;

  线程进行切换,堆栈的保存实现是使用pendSV_Handler中断实现的,对地址内的地址内的地址进行操作的操作在此特别提一下;

  那个psp是在context_rvds.s的下文切换的时候,把线程栈地址MSR给psp的,第二次pendSV时,上文保存的psp就已经是线程栈的psp了;

  CPSR程序状态字寄存器,SCB系统控制单元寄存器,占坑;