概述
摘自官方文档:
在嵌入式领域有多种不同 CPU 架构,例如 Cortex-M、ARM920T、MIPS32、RISC-V 等等。为了使 RT-Thread 能够在不同 CPU 架构的芯片上运行,RT-Thread 提供了一个 libcpu 抽象层来适配不同的 CPU 架构。libcpu 层向上对内核提供统一的接口,包括全局中断的开关,线程栈的初始化,上下文切换等。 RT-Thread 的 libcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接口和变量。
函数和变量 | 描述 |
---|
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_t to); | 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用 | void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于线程和线程之间的切换 | void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用 | rt_uint32_t rt_thread_switch_interrupt_flag; | 表示需要在中断里进行切换的标志 | rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; | 在线程进行上下文切换时候,用来保存 from 和 to 线程 |
一共存在6个函数和3个变量,也就是说只要针对开发板的芯片,最多实现这6个函数和3个变量,就可以在RT-Thread架构下屏蔽掉芯片这一层的差异,对上层的代码提供统一的接口来实现与芯片相关的操作。
开关全局中断
摘自官方文档:
无论内核代码还是用户的代码,都可能存在一些变量,需要在多个线程或者中断里面使用,如果没有相应的保护机制,那就可能导致临界区问题。
临界区问题就是在多个线程或中断中共享了同一个或一些数据,当线程切换或中断来临时,当前线程下的这个变量可能会被这个切换过去的线程或中断子程序修改,而在当前线程的上下文无法找到问题所在。 那么就需要对这段数据相关的代码进行一定的保护,以防出现临界区问题。在RT-Thread中,开关全局中断函数的实现就是解决方法。 在 libcpu/arm/cortex-m3 下的 context_rvds.S 文件中,就实现了开关全局中断的函数。
关闭全局中断
;
rt_hw_interrupt_disable PROC ;PROC 伪指令定义函数
EXPORT rt_hw_interrupt_disable ;EXPORT 输出定义的函数,类似于 C 语言 extern
MRS r0, PRIMASK ; 读取 PRIMASK 寄存器的值到 r0 寄存器
CPSID I ; 关闭全局中断
BX LR ; 函数返回
ENDP ;ENDP 函数结束
该汇编代码实现了关闭全局中断的功能。首先将 rt_hw_interrupt_disable 作为一个函数提供给其他的文件域访问,然后将当前的 PRIMASK 值保存到 R0 寄存器中。
PRIMASK 是中断屏蔽寄存器,可读写,该寄存器只有一位。 读取可获取到当前值(0或1)。 写1时会屏蔽所有可屏蔽中断,写0取消屏蔽所有可屏蔽中断。
如果该函数在定义时存在返回值,R0会作为函数的返回值传递出去。也就是将进入这个函数之前系统的中断屏蔽状态存下来,后续等处理完成再将这个值写回去,保证不影响系统原本的值。
CPSID I 其实就是 PRIMASK = 1 ,往上述的中断屏蔽寄存器中写1,来屏蔽所有可屏蔽中断。
开启全局中断
;
rt_hw_interrupt_enable PROC ; PROC 伪指令定义函数
EXPORT rt_hw_interrupt_enable ; EXPORT 输出定义的函数,类似于 C 语言 extern
MSR PRIMASK, r0 ; 将 r0 寄存器的值写入到 PRIMASK 寄存器
BX LR ; 函数返回
ENDP ; ENDP 函数结束
大体上和上面的关闭是一样的,同样,如果函数定义时存在形参,默认会放进R0里,也就是上面说的进入关闭全局中断之前系统的可屏蔽寄存器的状态。
关闭全局中断和开启全局中断必须是成对出现的,在调用时先使用一个变量来存储 rt_hw_interrupt_disable() 函数的返回值,最后使用 rt_hw_interrupt_enable() 时将这个变量传进去。
这是ARM架构下的开关全局中断,别的芯片架构肯定会有不同的操作,但是流程都是差不多的。其实就是对芯片的全局中断开关做了一个封装。ARM架构下存在 PRIMASK 寄存器来开关全局中断,如果开发板不存在这类寄存器,甚至可以直接用C文件来实现,并不一定使用汇编,当然可能会降低性能。 所有与芯片架构相关的函数都定义在了rthw.h中,以确保能够被其他函数调用。如下图:
线程栈初始化
以Cortex-M3为例,其芯片的内部寄存器一共是37个,除了R0-R7是每个模式都一样的通用寄存器外,在不同的模式下可能会存在独有的寄存器。 那么在多线程的RTOS中,每个线程可能都对应着不同的上下文环境(即内部寄存器值),而在同一个上下文环境下寄存器只存在一组,那怎么来区分不同的线程呢? 这就是在内存中构建线程栈的目的。我们可以在线程创建时,将当前的上下文,即当前的内部寄存器的值保存到一个在内存中构建出来的栈中,也就是手动的保存当前代码的上下文。然后当需要进行线程切换时,再把这个保存下来的上下文内容写回到内部寄存器中,让程序回到这个保存下来的线程环境中继续执行。这样就能够实现不同线程的切换。 下图是上下文内容在内存中构建的栈中的排布: 代码具体实现在 arm/cortex-m3/ 下的 cpuport.c 中。结合栈实现的结构体来看: exception_stack_frame 部分是ARM的硬件压栈部分, stack_frame 的R4-R11则是需要自行实现的压栈部分。组合起来就是一个完整的上下文环境。 实现初始化的具体代码如下:
rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);
stack_frame = (struct stack_frame *)stk;
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter;
stack_frame->exception_stack_frame.r1 = 0;
stack_frame->exception_stack_frame.r2 = 0;
stack_frame->exception_stack_frame.r3 = 0;
stack_frame->exception_stack_frame.r12 = 0;
stack_frame->exception_stack_frame.lr = (unsigned long)texit;
stack_frame->exception_stack_frame.pc = (unsigned long)tentry;
stack_frame->exception_stack_frame.psr = 0x01000000L;
return stk;
}
使用传入的地址作为起始地址,从这个地址开始构建出一个以 stack_frame 结构体为结构的栈。 返回的就是这个构建好的栈顶的指针。 在一个线程创建时,最终就会调用这个栈初始化函数,并将返回值存储到thread结构体中的sp指针中。 这个sp指针,在系统的调度行为中,就会作为参数,传入到上下文切换的函数中,通过这个栈内容来对线程做保存和切换的工作。 这是在ARM CM3架构下的栈初始化。在其他架构下,需要根据芯片的内部寄存器特性来对栈结构体的参数极其顺序做一定的修改。其他部分的原理大致也是一样的。
上下文切换
在 Cortex-M 处理器架构里,存在自动部分压栈和 PendSV 的特性。自动部分压栈能够让切换代码更加简洁,PendSV能够保证在中断中切换线程的可行性和稳定性。 上下文切换一般来说需要实现三个函数:
- rt_hw_context_switch_to():没有来源线程,切换到目标线程,在调度器启动第一个线程的时候被调用。
- rt_hw_context_switch():在线程环境下,从当前线程切换到目标线程。
- rt_hw_context_switch_interrupt ():在中断环境下,从当前线程切换到目标线程。
但是由于PendSV的存在,2和3可以使用同一个函数,因为最后都是调用PendSV来进行切换,所以在Cortex系列中只需要实现两个函数即可。
先简单了解下硬件自动压栈。 SP 是 Stack point ,堆栈指针,这个寄存器中保存的是一个地址,这个地址指向的就是一个堆栈的栈顶。在PendSV异常触发时,硬件会自动的将当前环境PSR、PC、LR、R12、R3-R0 寄存器依此压入这个 SP 堆栈中。当PendSV处理完毕,返回主程序时,硬件会自动的将这个 SP 堆栈中的R0-R3、R12、LR、PC、PSR按顺序出栈,并赋值给对应的寄存器。
rt_hw_context_switch_to()
具体实现如下:
;
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
其中R0就是上文说的 rt_hw_stack_init() 函数所返回的,存储在rtthread结构体的sp指针中的地址,如下图。指向的是在内存中构建出的上下文栈顶。所以这个R0实际上是一个32位的地址值,也就是一个指针。 把这个R0的值保存到 rt_interrupt_to_thread 中,以便在PendSV的处理函数中能够使用。然后就是手动触发PendSV异常。在执行完这个函数之后,就会跳转到PendSV处理函数切换上下文了。
rt_hw_context_switch()/rt_hw_context_switch_interrupt()
具体实现如下:
;
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
大体和 rt_hw_context_switch_to() 差不多,这里存在两个线程,就需要将from线程存储到 rt_interrupt_from_thread 中,以便PendSV调用。然后同样是手动触发PendSV。
PendSV
具体实现如下:
; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
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
关键是这两段:
; 检查 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 指针
首先获取到from线程的指针,也就是存储在 rt_interrupt_from_thread 变量中的值。psp是当前的进程堆栈,也就是上文说的 SP 。此时的psp中由于硬件自动压栈的特性,已经压入了 PSR、PC、LR、R12、R3-R0 这些寄存器的值。通过指令MRS r1, psp 将这个堆栈的栈顶指针读出,然后使用STMFD r1!, {r4 - r11} 将 R11-R4 按顺序压入这个堆栈中。 此时R1寄存器中存储的指针指向的栈结构中的数据排布,就和最开始内存中构建的上下文栈是一样的了。如下图。 然后使用Load/Store指令,让 rt_interrupt_from_thread 中的指针所指向的地址,即上文的rtthread结构体中的sp指针,指向R1中的地址。这样就更新了内存中构建出的上下文栈的内容。 此时from线程的上下文栈中存储的就是from线程的芯片内部寄存器值,当需要切换回from线程时,只需要将这些值重新赋给内部寄存器即可。
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
切换线程部分。同上,首先将 rt_interrupt_to_thread 中保存的需要切换的线程的指针的地址获取。然后通过Load操作取出这个线程的sp指针。然后使用LDMFD r1!, {r4 - r11} 来将 R4-R11 按顺序出栈。此时 R4-R11 已经切换到to线程的状态。最后再将r1的值,即此时的栈顶指针(此时指向R0,顺序是R0-R3, R12, LR, PC, PSR),直接赋给psp,即内部的栈指针,那么此时内部的栈指针所指向的内容就被更新成to线程的 R0-R3, R12, LR, PC, PSR 内容了。 最后在PendSV退出时,硬件会自动的将psp中的值出栈,也就是 R0-R3, R12, LR, PC, PSR ,那么这样就会在PendSV函数处理的整个过程中,保存了进入之前的线程的上下文,又更新了需要切换的线程的上下文,完成上下文切换的过程。 这是基于ARM Cortex架构下的上下文切换。如果开发板的芯片架构并非常见的架构,那么首先函数 rt_hw_context_switch() 和函数 rt_hw_context_switch_interrupt() 就不能够使用同样的代码,因为可能并不存在PendSV这样的特性。其次需要根据芯片的汇编指令和内部寄存器的特性来编写上下文的存储与更新。 但是总体的流程是大致相同的。核心思想就是通过将各个线程运行时所对应的内部寄存器的值保存下来,在需要切换时赋值回去。
小结
在芯片移植方面,只需要对最多6个函数和3个参数进行适配:
- 函数
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_t to):上下文切换函数之一,可以直接将to参数所存储的上下文内容赋给内部寄存器。 void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to):上下文切换函数之一,对当前线程的上下文做保存,并切换至目标线程。 void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to):上下文切换函数之一,在中断中触发的上下文切换,需要等待中断结束后才能够进行切换。 - 参数
rt_uint32_t rt_thread_switch_interrupt_flag:中断中进行上下文切换的标志,可以通过判断这个变量来编写不同的上下文储存和切换的代码。 rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread:线程切换过程中临时存储的线程的上下文栈顶指针。内存储存的是一个指针的地址。可以通过这个值来访问到在内存中构建出的存储上下文的栈的内容。从而存储或修改内部寄存器的值
|