RT-Thread代码启动过程与线程切换的实现

1、RT-Thread代码启动过程

1.1 启动流程图

系统先从启动文件开始运行,然后进入 RT-Thread 的启动 rtthread_startup() ,最后进入用户入口 main(),如下图所示:
在这里插入图片描述

1.2 以MDK为例

系统启动后先从汇编代码 startup_stm32f429xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统启动,最后进入用户程序入口 main()。
在这里插入图片描述
①从系统初始化开始执行,将函数地址赋给R0寄存器,跳转到R0地址执行并返回此处(BLX是带链接的跳转,即带返回的跳转)。
②将main函数地址给R0,将函数地址赋给R0,跳转到R0地址执行,不返回(BX是跳转,不返回)。
③跳转到了$Sub$$main

【在 __CC_ARM 编译器环境下,使用了$Sub$ $ 与 $Super$ $ 的“补丁”功能。这是一种特殊模式:用于有一个已经存在且不能被改变的函数的情况。使用这两个模式可以帮原函数打补丁。如存在一个函数foo();$Sub$ $foo定义的新功能函数,在foo()函数之前或者后使用$Sub$ $foo 可以添加一些新的程序代码。$Super$ $foo就是原始的未修补的foo函数,使用这个$Super$ $foo函数将直接跳转到foo()函数。】

关于 $Sub$ $ 和 $Super$ $ 扩展功能的使用,详见:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0377g/pge1362065967698.html
在这里插入图片描述

$Sub$$main 中主要是一些系统启动代码(系统初始化)。

在这里插入图片描述
④在rtthread_startup中,主要实现了板级初始化(初始化外设和驱动);打印RT-Thread的logo和版本信息;初始化系统定时器;初始化调度器;创建application线程(这里将用户main函数作为一个线程,用户main里面是空的);初始化软件定时器;创建空闲线程;启动系统调度(启用调度后,main函数就会参与调度开始运行)。

【所以说 $Sub$$main在main之前干的活就是进行rt-thread系统初始化,为了让用户更方便的使用,让用户不要操心的太多】

在这里插入图片描述
⑤以下是在rt_application_init()函数中创建的main函数线程:
在这里插入图片描述
在这里插入图片描述

⑥$Super$$mian 可以直接跳到main()函数; 用户可以在main中写一些应用代码:
在这里插入图片描述

1.3 总结

可以这样使用给main函数打补丁:

int $Sub$$main(void)
{
    //添加补丁函数
 
    $Super$$main(); //使用本句直接转到main()运行
}

当然,main()函数也可以是自己的其他函数,操作都是一样的,换一下函数名就好了

2、RT-Thread线程切换过程

首先查看RT-Thread内核架构这一章节,明白RT-Thread链表及线程的实现方法。

2.1 实现就绪列表

线程创建好之后,我们需要把线程添加到就绪列表里面, 表示线程已经就绪,系统随时可以调度。

2.1.1. 实现就绪列表

就绪列表实际上就是一个 rt_list_t 类型的数组,数组的大小由决定最大线程优先级的宏 RT_THREAD_PRIORITY_MAX 决 定 ,RT_THREAD_PRIORITY_MAX 在 rtconfig.h 中默认定义为 32。 数组的下标对应了线程的优先级,同一优先级的线程统一插入到就绪列表的同一条链表中。
定义就绪列表:

rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

初始化就绪列表:

 for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
  {
      rt_list_init(&rt_thread_priority_table[offset]);
  }

在这里插入图片描述

2.1.2 将线程插入就绪列表

rt_thread_startup()----->rt_thread_resume()----->rt_schedule_insert_thread()----->rt_list_insert_before()

void rt_schedule_insert_thread(struct rt_thread *thread)
{
    register rt_base_t temp;

    RT_ASSERT(thread != RT_NULL);

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();

    /* it's current thread, it should be RUNNING thread */
    if (thread == rt_current_thread)
    {
        thread->stat = RT_THREAD_RUNNING | (thread->stat & ~RT_THREAD_STAT_MASK);
        goto __exit;
    }

    /* READY thread, insert to ready queue */
    thread->stat = RT_THREAD_READY | (thread->stat & ~RT_THREAD_STAT_MASK);
    /* insert thread to ready list */
    rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
                          &(thread->tlist));

    RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
                                      RT_NAME_MAX, thread->name, thread->current_priority));

    /* set priority mask */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_thread_ready_table[thread->number] |= thread->high_mask;
#endif
    rt_thread_ready_priority_group |= thread->number_mask;

__exit:
    /* enable interrupt */
    rt_hw_interrupt_enable(temp);
}

在这里插入图片描述

2.2 实现调度器

调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。

2.2.1 调度器初始化

rt_system_scheduler_init();

2.2.2 启动调度器

void rt_system_scheduler_start(void)
{
    register struct rt_thread *to_thread;
    rt_ubase_t highest_ready_priority;

    to_thread = _get_highest_priority_thread(&highest_ready_priority);

#ifdef RT_USING_SMP
    to_thread->oncpu = rt_hw_cpu_id();
#else
    rt_current_thread = to_thread;
#endif /*RT_USING_SMP*/

    rt_schedule_remove_thread(to_thread);
    to_thread->stat = RT_THREAD_RUNNING;

    /* switch to new thread */
#ifdef RT_USING_SMP
    rt_hw_context_switch_to((rt_ubase_t)&to_thread->sp, to_thread);
#else
    rt_hw_context_switch_to((rt_ubase_t)&to_thread->sp);
#endif /*RT_USING_SMP*/

    /* never come back */
}

2.2.2 第一次线程切换

rt_hw_context_switch_to    PROC
    EXPORT rt_hw_context_switch_to
    ; r0 的值是一个指针,该指针指向 to 线程的线程控制块的 SP 成员
    ; 将 r0 寄存器的值保存到 rt_interrupt_to_thread 变量里
    LDR     r1, =rt_interrupt_to_thread
    STR     r0, [r1]

    ; 设置 from 线程为空,表示不需要从保存 from 的上下文
    LDR     r1, =rt_interrupt_from_thread
    MOV     r0, #0x0
    STR     r0, [r1]

    ; 设置标志为 1,表示需要切换,这个变量将在 PendSV 异常处理函数里切换的时被清零
    LDR     r1, =rt_thread_switch_interrupt_flag
    MOV     r0, #1
    STR     r0, [r1]

    ; 设置 PendSV 异常优先级为最低优先级
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI
    LDR.W   r2, [r0,#0x00]       ; read
    ORR     r1,r1,r2             ; modify
    STR     r1, [r0]             ; write-back

    ; 触发 PendSV 异常 (将执行 PendSV 异常处理程序)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 放弃芯片启动到第一次上下文切换之前的栈内容,将 MSP 设置启动时的值
    LDR     r0, =SCB_VTOR
    LDR     r0, [r0]
    LDR     r0, [r0]
    MSR     msp, r0

    ; 使能全局中断和全局异常,使能之后将进入 PendSV 异常处理函数
    CPSIE   F
    CPSIE   I

    ; 不会执行到这里
    ENDP

rt_hw_context_switch_to() 只有目标线程,没有来源线程。这个函数里实现切换到指定线程的功能。

rt_hw_context_switch_to() ;

在这里插入图片描述
在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁。
线程之间的上下文切换,如下图表示:
在这里插入图片描述
中断到线程的上下文切换可以用下图表示:
在这里插入图片描述

2.2.3 系统调度

系统调度就是在就绪列表中寻找优先级最高的就绪线程,然后去执行该线程。

rt_schedule();

使用rt_hw_context_switch/rt_hw_context_switch_interrupt() 进行线程的切换,函数 rt_hw_context_switch() 和函数 rt_hw_context_switch_interrupt() 都有两个参数,分别是 from 线程和 to 线程。它们实现从 from 线程切换到 to 线程的功能。下图是具体的流程图:
在这里插入图片描述

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 变量是否为 1
    ; 如果变量为 1 就跳过更新 from 线程的内容
    LDR     r2, =rt_thread_switch_interrupt_flag
    LDR     r3, [r2]
    CMP     r3, #1
    BEQ     _reswitch
    ; 设置 rt_thread_switch_interrupt_flag 变量为 1
    MOV     r3, #1
    STR     r3, [r2]

    ; 从参数 r0 里更新 rt_interrupt_from_thread 变量
    LDR     r2, =rt_interrupt_from_thread
    STR     r0, [r2]

_reswitch
    ; 从参数 r1 里更新 rt_interrupt_to_thread 变量
    LDR     r2, =rt_interrupt_to_thread
    STR     r1, [r2]

    ; 触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]
    BX      LR

2.2.4 线程切换

PendSV 中断处理函数是 PendSV_Handler()。在 PendSV_Handler() 里完成线程切换的实际工作,下图是具体的流程图:
在这里插入图片描述

PendSV_Handler   PROC
    EXPORT PendSV_Handler

    ; 关闭全局中断
    MRS     r2, PRIMASK
    CPSID   I

    ; 检查 rt_thread_switch_interrupt_flag 变量是否为 0
    ; 如果为零就跳转到 pendsv_exit
    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_thread_switch_interrupt_flag 变量
    ; 如果为 0,就不进行 from 线程的上下文保存
    LDR     r0, =rt_interrupt_from_thread
    LDR     r1, [r0]
    CBZ     r1, switch_to_thread

    ; 保存 from 线程的上下文
    MRS     r1, psp                 ; 获取 from 线程的栈指针
    STMFD   r1!, {r4 - r11}       ; 将 r4~r11 保存到线程的栈里
    LDR     r0, [r0]
    STR     r1, [r0]                ; 更新线程的控制块的 SP 指针

switch_to_thread
    LDR     r1, =rt_interrupt_to_thread
    LDR     r1, [r1]
    LDR     r1, [r1]                ; 获取 to 线程的栈指针

    LDMFD   r1!, {r4 - r11}       ; 从 to 线程的栈里恢复 to 线程的寄存器值
    MSR     psp, r1                 ; 更新 r1 的值到 psp

pendsv_exit
    ; 恢复全局中断状态
    MSR     PRIMASK, r2

    ; 修改 lr 寄存器的 bit2,确保进程使用 PSP 堆栈指针
    ORR     lr, lr, #0x04
    ; 退出中断函数
    BX      lr
    ENDP
发布了23 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sinat_31039061/article/details/104115803