追风赶月莫停留,平芜尽处是春山。这篇文章主要讲述RT-Thread快速入门-内核移植相关的知识,希望能为你提供帮助。
RT-Thread 快速入门系列前面的文章介绍了内核相关的知识,以及内核提供的接口函数和如何使用。
本篇文章主要介绍如何将 RT-Thread 内核移植到某个硬件平台之上。移植分为两部分:
- CPU 架构移植
- BSP 移植
CPU 架构移植,会用到 CPU 架构的汇编指令,因此如果要自己动手移植,需要熟悉一下目标 CPU 的汇编指令。
备注:在介绍移植过程的各个部分时,会以 Cortex-M 为例进行说明。
CPU 架构移植为了方便将 RT-Thread 移植到不同 CPU 架构的芯片上运行,RT-Thread 提供了一个 libcpu 抽象层。该抽象层用来适配不同的 CPU 架构,起到承上启下的作用。
- 向上对内核提供统一的函数接口。包括全局中断的开关、线程初始化、上下文切换等等。
- 向下提供了统一的 CPU 架构移植接口,包括全局中断开关函数、线程上下文切换函数、时钟节拍配置、Cache 等等。
函数和变量 | 描述 |
---|---|
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); | 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用 |
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 为了解决临界区问题,提供了一系列的线程间同步和同步机制。这些机制内部实现都需要用到 libcpu 里提供的全局中断开关函数,他们分别是:
/* 关闭全局中断 */
rt_base_t rt_hw_interrupt_disable(void);
/* 打开全局中断 */
void rt_hw_interrupt_enable(rt_base_t level);
以 Cortex-M 为例,其快速开关中断的指令如下(汇编代码):
CPSID I ;
PRIMASK=1, ;
关中断
CPSIE I ;
PRIMASK=0, ;
开中断
关闭全局中断
关闭全局中断函数
rt_hw_interrupt_disable()
内部需要依次完成如下功能:- 保存当前的全局中断状态,并把状态作为函数的返回值
- 关闭全局中断。
;
/*
;
* rt_base_t rt_hw_interrupt_disable();
;
*/
rt_hw_interrupt_disablePROC;
PROC 伪指令定义函数
EXPORTrt_hw_interrupt_disable;
EXPORT 输出定义的函数,类似于C语言extern
MRSr0, PRIMASK;
读取PRIMASK寄存器的值到 r0 寄存器
CPSIDI;
关闭全局中断
BXLR;
函数返回
ENDP;
ENDP 函数结束
上面代码中,寄存器
r0
存储的数据就是函数的返回值。打开全局中断
打开全局中断函数
rt_hw_interrupt_enable(rt_base_t level)
中,参数 level
为需要恢复的全局中断状态。基于 MDK,在 Cortex-M 内核上打开全局中断的代码如下(已经添加注释):
;
/*
;
* void rt_hw_interrupt_enable(rt_base_t level);
;
*/
rt_hw_interrupt_enablePROC;
PROC 伪指令定义函数
EXPORTrt_hw_interrupt_enable;
EXPORT 输出定义的函数,类似于C语言extern
MSRPRIMASK, r0;
将 r0 寄存器的值写入到 PRIMASK 寄存器
BXLR;
函数返回
ENDP;
ENDP 函数结束
该函数用
MSR
指令将寄存器 r0
的值写入到 PRIMASK
寄存器中,恢复之前的中断状态。2. 实现线程栈初始化
在线程创建过程中,会调用栈初始化函数
rt_hw_stack_init()
,对线程栈进行初始化。该函数内部,会构造一个上下文内容,这个上下文内容被当作每个线程第一次执行的初始值。Cortex-M 的栈初始化代码如下:
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;
/* 把所有寄存器的默认值设置为 0xdeadbeef */
for (i = 0;
i <
sizeof(struct stack_frame) / sizeof(rt_uint32_t);
i ++)((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
/* 根据 ARM APCS 调用标准,将第一个参数保存在 r0 寄存器 */
stack_frame->
exception_stack_frame.r0 = (unsigned long)parameter;
/* 将剩下的参数寄存器都设置为 0 */
stack_frame->
exception_stack_frame.r1 = 0;
/* r1 寄存器 */
stack_frame->
exception_stack_frame.r2 = 0;
/* r2 寄存器 */
stack_frame->
exception_stack_frame.r3 = 0;
/* r3 寄存器 *//* 将 IP(Intra-Procedure-call scratch register.) 设置为 0 */
stack_frame->
exception_stack_frame.r12 = 0;
/* r12 寄存器 */
/* 将线程退出函数的地址保存在 lr 寄存器 */
stack_frame->
exception_stack_frame.lr = (unsigned long)texit;
/* 将线程入口函数的地址保存在 pc 寄存器 */
stack_frame->
exception_stack_frame.pc = (unsigned long)tentry;
/* 设置 psr 的值为 0x01000000L, 表示默认切换过去是 Thumb 模 式 */
stack_frame->
exception_stack_frame.psr = 0x01000000L;
/* 返回当前线程的栈地址 */
return stk;
3. 实现上下文切换
CPU 架构不同,线程之间的上下文切换和中断到线程的上下文切换,上下文的寄存器部分可能会有所差异。在 Cortex-M 里面上下文切换都是统一使用 PendSV 异常来完成,切换部分并没有差异。
为了能适应不同的 CPU 架构, RT-Thread 的 libcpu 抽象层需要实现三个线程切换相关的函数:
rt_hw_context_switch_to()
:没有来源线程,切换到目标线程,在调度器启动第一个线程的时候
被调用。rt_hw_context_switch()
:在线程环境下,从当前线程切换到目标线程。rt_hw_context_switch_interrupt()
:在中断环境下,从当前线程切换到目标线程。
rt_hw_context_switch()
函数,可以立即进行上下文切换。在中断环境下,需要等待中断服务函数完成之后才能进行切换。实现 rt_hw_context_switch_to()
该函数只有目标线程,没有来源线程。实现流程图:
文章图片
在 Cortex-M3 内核上的
rt_hw_context_switch_to()
实现(基于 MDK),其代码如下:;
/*
;
* void rt_hw_context_switch_to(rt_uint32 to);
;
* r0 -->
to
;
* this fucntion is used to perform the first thread switch
;
*/
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()/ rt_hw_context_switch_interrupt()
这两个函数都有两个参数
from
和 to
,他们实现从 from
线程切换到 to
线程的功能。流程图如下:文章图片
在 Cortex-M3 内核上的
rt_hw_context_switch()
和 rt_hw_context_switch_interrupt()
实现(基于MDK),如下代码的所示:
;
/*
;
* void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
;
* r0 -->
from
;
* r1 -->
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
ENDP
实现 PendSV 中断
在 Cortex-M3 里, PendSV 中断处理函数是
PendSV_Handler()
。在该函数里完成线程切换的实际工作,下图是具体的流程图:文章图片
PendSV_Handler
的代码实现如下:;
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
4. 实现系统时钟节拍
开关全局中断和上下文切换功能实现之后,就可以完成 RTOS 的线程/任务创建、运行、调度等功能。
除此之外,还需要实现系统的时钟节拍。系统定时器、延时,以及相同优先级线程之间的时间片轮转调度,都需要时钟节拍的支持。
libcpu 的移植需要完成
rt_tick_increase()
函数会在时钟节拍的中断里被周期性的调用,调用周期取决于 rtconfig.h 的宏 RT_TICK_PER_SECOND
的值。在 Cortex M 中,实现
SysTick
的中断处理函数即可实现时钟节拍功能。void SysTick_Handler(void)/* 进入中断 */
rt_interrupt_enter();
rt_tick_increase();
/* 离开中断 */
rt_interrupt_leave();
BSP 移植相同的 CPU 架构在实际项目中,不同的板卡上可能使用相同的 CPU 架构,搭载不同的外设资源,完成不同的产品,所以我们也需要针对板卡做适配工作。 RT-Thread 提供了 BSP 抽象层来适配常见的板卡。
如果希望在一个板卡上使用 RT-Thread 内核,除了需要有相应的芯片架构的移植,还需要有针对板卡的移植,也就是实现一个基本的 BSP。
主要任务是建立让操作系统运行的基本环境,需要完成的主要工作是:
- 1)初始化 CPU 内部寄存器,设定 RAM 工作时序。
- 2)实现时钟驱动及中断控制器驱动,完善中断管理。
- 3)实现串口和 GPIO 驱动。
- 4)初始化动态内存堆,实现动态堆内存管理。
文章图片
小结本篇文章重点介绍了 RT-Thread 移植的具体流程,以及移植工作涉及到的原理知识等。
通过学习 RT-Thread 的移植过程,可以了解到 RTOS 移植需要完成的工作。真正的移植工作,需要了解 CPU 架构、系统内核移植需要完成的函数等等,对开发者的技术要求比较高。
但是,对于初学者来说,不用担心。RTOS 官方一般会将主流的 CPU 移植适配好,直接拿来用即可。
另外,若想将一款 RTOS 用到项目中,可以先看看官方使用文档,会有详细的指导说明。主流的 CPU 架构一般都会适配,按照文档说明,可以快速将系统用起来。
OK,今天先到这,下次继续。加油~
推荐阅读
- Linux驱动开发-编写PCF8591(ADC)芯片驱动
- 最通俗易懂的Redis发布订阅及代码实战
- JAVA线程详解
- PHP 零基础入门笔记(安装PHP)
- #yyds干货盘点#CyclicBarrier详解
- Kubernetes 1.15 安装及组件关系(前期准备工作篇)
- 网络安全最全面试题
- 对功能测试的一些思考
- MySQL千万数据方案调研,一不小心直接打挂我系统