Rt-Thread之内核移植(官网+任务切换总结)

启动流程就不必再说了。详情就看熊谱翔的书吧。这里只是说一下对自己来说比较新鲜的地方及所得。

自动初始化机制:

只要在函数定义处通过宏定义的方式进行声明,就会在系统启动过程中被执行。这里当宏理解成开关就可以了,还有其用法,是放在了函数定义处,注意一下就可以,还是比较好理解的,如:

int rt_hw_usart_init(void)  /* 串口初始化函数 */

{

    . . . . .. 

    /* 注册串口1 设备 */

    rt_hw_serial_register(&serial1, "uart1",

                          RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,

                          uart);

    return 0;

}

INIT_BOARD_EXPORT(rt_hw_usart_init);    /* 使用组件自动初始化机制 */

 

内核对象的管理架构,完全是基于面向对象的方法来设计的。两个特点:

1、内核对象包括线程、信号量、互斥量、事件、邮箱、消息队列和定时器、内存池、设备驱动等。对象容器中包含每类内核对象的信息,包括对象类型、大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上。

Rt-Thread之内核移植(官网+任务切换总结)_第1张图片

2、要了解各种类之的继承关系。当然,这里的类指容器。

Rt-Thread之内核移植(官网+任务切换总结)_第2张图片

 

关于内核移植主要是针对cotex-M的CPU架构:

准备知识:

M核通用寄存器就不用说了,见下图:

Rt-Thread之内核移植(官网+任务切换总结)_第3张图片

程序状态字寄存器里保存算术与逻辑标志,例如负数标志,零结果标志,溢出标志等等。

中断屏蔽寄存器组控制 Cortex-M 的中断除能。

控制寄存器用来定义特权级别和当前使用哪个堆栈指针。

MSP,适用于中断模式下。PSP适用于多任务状态。

如果是具有浮点单元的 Cortex-M4 或者 Cortex-M7,控制寄存器也用来指示浮点单元当前是否在使用,浮点单元包含了 32 个浮点通用寄存器 S0\~S31 和特殊 FPSCR 寄存器(Floating point status and control register)

中断的过程: 

当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样会打断当前运行的中断服务程序,然后把这个中断服务程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自动保存到中断栈中。

具体过程如下图:

中断处理程序的 3 部分

中断前导程序

中断前导程序主要工作如下:

1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。

对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。

2)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。

void rt_interrupt_enter(void)
{
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest ++;
    rt_hw_interrupt_enable(level);
}

 

用户中断服务程序

在用户中断服务程序(ISR)中,分为两种情况,第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。

另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt() 函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。

rt_hw_context_switch_interrupt()函数处理流程:

rt_hw_context_switch_interrupt() 函数实现流程它将设置需要切换到的线程 rt_interrupt_to_thread 变量,然后触发 PendSV 异常(PendSV 异常是专门用来辅助上下文切换的,且被初始化为最低优先级的异常)。PendSV 异常被触发后,不会立即进行 PendSV 异常中断处理程序,因为此时还在中断处理中,只有当中断后续程序运行完毕,真正退出中断处理后,才进入 PendSV 异常中断处理程序。

 

中断后续程序

中断后续程序主要完成的工作是:

1 通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。

void rt_interrupt_leave(void)
{
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest --;
    rt_hw_interrupt_enable(level);

 2 恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程(上文)的 CPU 上下文,如果在中断中进行了线程切换,那么恢复 to (下文)线程的 CPU 上下文。这部分实现跟 CPU 架构相关,不同 CPU 架构的实现方式有差异,在 Cortex-M 架构中实现流程如下图所示。

Rt-Thread之内核移植(官网+任务切换总结)_第4张图片

 

关于RTT的中断栈的处理:

 RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。

在 Cortex-M 处理器内核里有两个堆栈指针,一个是主堆栈指针(MSP),是默认的堆栈指针,在运行第一个线程之前、在中断和异常服务程序里使用;另一个是线程堆栈指针(PSP),在线程里使用。在中断和异常服务程序退出时,修改 LR 寄存器的第 2 位的值为 1,线程的 SP 就由 MSP 切换到 PSP。

 

关于RTT中断的抽象管理

Rt-Thread之内核移植(官网+任务切换总结)_第5张图片

 

中断服务程序挂接

系统把用户的中断服务程序 (handler) 和指定的中断号关联起来,可调用如下的接口挂载一个新的中断服务程序:

rt_isr_handler_t rt_hw_interrupt_install(int vector,
                                        rt_isr_handler_t  handler,
                                        void *param,
                                        char *name);

调用 rt_hw_interrupt_install() 后,当这个中断源产生中断时,系统将自动调用装载的中断服务程序。

 注:

这个 API 并不会出现在每一个移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就没有这个 API。

 

中断源管理

通常在 ISR 准备处理某个中断信号之前,我们需要先屏蔽该中断源,在 ISR 处理完状态或数据以后,及时的打开之前被屏蔽的中断源。

屏蔽中断源可以保证在接下来的处理过程中硬件状态或者数据不会受到干扰,可调用下面这个函数接口:

void rt_hw_interrupt_mask(int vector);

注:

这个 API 并不会出现在每一个移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就没有这个 API。

 

全局中断开关

全局中断开关也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个线程主动放弃了处理器控制权。当需要关闭整个系统的中断时,可调用下面的函数接口:

rt_base_t rt_hw_interrupt_disable(void);

 开中断:

void rt_hw_interrupt_enable(rt_base_t level);

 

移植过程:

RT-Thread 的 libcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接口和变量。

libcpu 移植相关 API

函数和变量 描述
rt_base_t rt_hw_interrupt_disable(void); 关闭全局中断
void rt_hw_interrupt_enable(rt_base_t level); 打开全局中断
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit); 线程栈的初始化,内核在线程创建和线程初始化里面会调用这个函数
void rt_hw_context_switch_to(rt_uint32 to); 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 从 from 线程切换到 to 线程,用于线程和线程之间的切换
void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to); 从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用
rt_uint32_t rt_thread_switch_interrupt_flag; 表示需要在中断里进行切换的标志
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; 在线程进行上下文切换时候,用来保存 from 和 to 线程

 

实现线程栈初始化

在动态创建线程和初始化线程的时候,会使用到内部的线程初始化函数_rt_thread_init(),_rt_thread_init() 函数会调用栈初始化函数 rt_hw_stack_init(),在栈初始化函数里会手动构造一个上下文内容,这个上下文内容将被作为每个线程第一次执行的初始值。上下文在栈里的排布如下图所示:

Rt-Thread之内核移植(官网+任务切换总结)_第6张图片

 

实现上下文切换

在不同的 CPU 架构里,线程之间的上下文切换和中断到线程的上下文切换,上下文的寄存器部分可能是有差异的,也可能是一样的。在 Cortex-M 里面上下文切换都是统一使用 PendSV 异常来完成,切换部分并没有差异。但是为了能适应不同的 CPU 架构,RT-Thread 的 libcpu 抽象层还是需要实现三个线程切换相关的函数:

1) rt_hw_context_switch_to():没有来源线程,切换到目标线程,在调度器启动第一个线程的时候被调用。

2) rt_hw_context_switch():在线程环境下,从当前线程切换到目标线程。

3) rt_hw_context_switch_interrupt ():在中断环境下,从当前线程切换到目标线程。

在线程环境下进行切换和在中断环境进行切换是存在差异的。线程环境下,如果调用 rt_hw_context_switch() 函数,那么可以马上进行上下文切换;而在中断环境下,需要等待中断处理函数完成之后才能进行切换。

 

在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁。

线程之间的上下文切换,如下图表示:

Rt-Thread之内核移植(官网+任务切换总结)_第7张图片

硬件在进入 PendSV 中断之前自动保存了 from (上文)线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后 PendSV 里保存 from 线程的 R11\~R4 寄存器,以及恢复 to 线程的 R4\~R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0\~R3、R12、LR、PC、PSR 寄存器。 

 

中断到线程的上下文切换可以用下图表示:

Rt-Thread之内核移植(官网+任务切换总结)_第8张图片

硬件在进入中断之前自动保存了 from 线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后触发了 PendSV 异常。在 PendSV 异常处理函数里保存 from 线程的 R11\~R4 寄存器,以及恢复 to 线程的 R4\~R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0\~R3、R12、PSR、PC、LR 寄存器。

显然,在 Cortex-M 内核里 rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 功能一致,都是在 PendSV 里完成剩余上下文的保存和回复。所以我们仅仅需要实现一份代码,简化移植的工作。

 

实现时钟节拍

有了开关全局中断和上下文切换功能的基础,RTOS 就可以进行线程的创建、运行、调度等功能了。有了时钟节拍支持,RT-Thread 可以实现对相同优先级的线程采用时间片轮转的方式来调度,实现定时器功能,实现 rt_thread_delay() 延时函数等等。

libcpu 的移植需要完成的工作,就是确保 rt_tick_increase() 函数会在时钟节拍的中断里被周期性的调用,调用周期取决于 rtconfig.h 的宏 RT_TICK_PER_SECOND 的值。

在 Cortex M 中,实现 SysTick 的中断处理函数即可实现时钟节拍功能。

void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

 

 

BSP 移植

实现一个基本的 BSP。主要任务是建立让操作系统运行的基本环境,需要完成的主要工作是:

1)初始化 CPU 内部寄存器,设定 RAM 工作时序。

2)实现时钟驱动及中断控制器驱动,完善中断管理。

3)实现串口和 GPIO 驱动。

4)初始化动态内存堆,实现动态堆内存管理。

 

这些内容,其实都是原理性的,实践中再总结真正的实现!!

总结:任务切换流程:

任务初始化后:如下图:

Rt-Thread之内核移植(官网+任务切换总结)_第9张图片

当该任务被调度执行时,CPU会自动将任务栈中最前面的8个寄存器值加载到CPU寄存器中,PendSV异常中完成下文环境切换,那么如何触发PendSV呢???

答案是寄存器,操作CPU中的寄存器就可以,如同其他中断一样。

你可能感兴趣的:(嵌入式开发零星时间堆记)