• RT-Thread 内核移植(学习)


    内核移植

    内核移植就是指将RT-Thread内核在不同的芯片架构、不同的板卡上运行起来,能够具备线程管理和调度,内存管理,线程间同步和通信、定时器管理等功能。

    移植可分为CPU架构移植和BSP(Board support package,板级支持包)移植两部分。

    CPU架构移植

    在嵌入式领域有多种不同CPU架构,例如Cortex-M、ARM920T、MIPS32、RISC-V等等。
    为了使RT-Thread能够在不同CPU架构的芯片上运行,RT-Thread提供了一个libcpu抽象层来适配不同的CPU架构

    libcpu层向上对内核提供统一的接口,包括全局中断的开关,线程栈的初始化,上下文切换等。

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

    CPU架构移植需要实现的接口和变量。

    • rt_base_t rt_hw_interrupt_disable(void); 关闭全局中断
    • void rt_hw_interrupt_enbale(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 线程

    实现全局中断开关

    无论内核代码还是用户的代码,都可能存在一些变量,需要在多个线程或中断里面使用,如果没有相应的保护机制,那就可能导致临界区问题。
    RT-Thread里为了解决这个问题,提供了一系列的线程间同步和通信机制来解决,但是这些机制都需要用到libcpu里提供的全局中断开关函数。

    rt_base_t rt_hw_interrupt_disable(void);
    
    void rt_hw_interrupt_enable(rt_base_t level);
    
    • 1
    • 2
    • 3

    Cortex-M为了快速开关中断,实现了CPS指令,可以用在此处。

    CPSID I;PRIMASK=1, ;关中断
    CPSIE I;PRIMASK=0, ;开中断
    
    • 1
    • 2

    关闭全局中断

    在rt_hw_interrupt_disable()函数里面需要依序完成的功能是:

    1. 保存当前的全局中断状态,并把状态作为函数的返回值。
    2. 关闭全局中断。

    基于MDK,在Cortex-M内核上实现关闭全局中断:

    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函数结束
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    r0存储的就是函数的返回值。
    中断可以发生在 “MRS r0, PRIMASK” 指令和 “CPSID I” 之间,这并不会导致全局中断状态的错乱。

    实现线程栈初始化

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

    上下文在栈里的排入如图:
    在这里插入图片描述
    在栈里构建上下文

    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;
    	}
    
    	//根据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;   
        // 将线程退出函数的地址保存在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    实现上下文切换

    不同的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()函数,那么可以马上进行上下文切换。
    在中断环境下,需要等待中断处理函数完成之后才能进行切换。

    由于这种差异,在ARM9等平台,rt_hw_context_switch()和rt_hw_context_switch_interrupt()的实现不一样。

    在中断处理程序里如果触发了线程的调度,调度函数里会调用rt_hw_context_switch_interrupt()触发上下文切换。

    中断处理程序处理完中断事务之后,中断退出之前,检查rt_thread_switch_interrupt_flag变量,如果该变量的值为1,就根据rt_interrupt_from_thread 变量和 rt_interrupt_to_thread 变量,完成线程的上下文切换。

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

    在这里插入图片描述
    硬件在进入PendSV中断之前自动保存了from线程的PSR、PC、LR、R12、R3R0寄存器,然后PendSV里保存from线程的R11-R4寄存器,以及回复to线程的R4R11寄存器,最后硬件在退出PendSV中断之后,自动恢复to线程的R0~R3、R12、LR、PC、PSR寄存器。

    中断到线程的上下文切换可以用下图表示:
    在这里插入图片描述
    硬件在进入中断之前自动保存了from线程的PSR、PC、LR、R12、R3-R0寄存器,然后触发了PendSV异常。在PendSV异常处理函数里保存from线程的R11-R4寄存器,以及恢复to线程的R11R4寄存器,最后硬件在退出PendSV中断之后,自动恢复to线程的R0R3、R12、PSR、PC、LR寄存器。

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

    实现rt_hw_context_switch_to()

    1. 将参数to保存到rt_interrupt_to_thread变量
    2. 将rt_interrupt_from_thread变量设置为0
    3. 将rt_thread_switch_interrupt_flag变量设置为1
    4. 设置PendSV异常优先级,触发PendSV中断
    5. 恢复MSP的默认值
    6. 使能全局中断
    7. 结束
    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
    	M0V 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    实现 rt_hw_context_switch()/ rt_hw_context_switch_interrupt()

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

    实现PendSV中断

    在Cortex-M3里,PendSV中断处理函数是PendSV_Handler()。
    在这里插入图片描述

    PendSV_Handler PROC
    	EXPORT PendSV_Handler
    	
    	;关闭全局中断
    	MRS r2,PRIMASK
    	CPSID I
    	
    	;检查rt_thread_switch_interrupt_flag变量是否为0
    	;如果为0,就跳转到pendsv_exit
    	LDR r0, =rt_thread_switch_interrupt_flag
    	LDR r1, [r0]
    	CBZ r1,pendsv_exit
    
    	;清零 rt_thread_switch_interrupt_flag 变量
    	MOV r1, #0x00
    	STR r1, [r0]
    
    	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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    实现时钟节拍

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

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    BSP移植

    相同的CPU架构在实际项目中,不同的板卡上可能使用相同的CPU架构,搭载不同的外设资源,完成不同的产品,所以需要针对板卡做适配工作。

    RT-Thread提供了BSP抽象层来适配常见的板卡。
    如果希望在一个板卡上使用RT-Thread内核,除了需要有相应的芯片架构的移植,还需要有针对板卡的移植,也就是实现一个基本的BSP。

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

    1. 初始化CPU内部寄存器,设定RAM工作时序。
    2. 实现时钟驱动以及中断控制器驱动,完善中断管理。
    3. 实现串口和GPIO驱动。
    4. 初始化动态内存堆,实现动态堆内存管理
  • 相关阅读:
    SOA和ESB介绍
    AI:62-基于深度学习的人体CT影像肺癌的识别与分类
    Flutter 上传文件和保存在应用程序存储目录
    Nomad系列-Nomad网络模式
    老卫带你学---leetcode刷题(10. 正则表达式匹配)
    echarts:nuxt项目使用echarts
    Zynq UltraScale+ XCZU3EG 纯VHDL解码 IMX214 MIPI 视频,2路视频拼接输出,提供vivado工程源码和技术支持
    【字符函数】
    2022NUSTCTF--web
    【ASM】字节码操作 Opcodes 介绍
  • 原文地址:https://blog.csdn.net/Caramel_biscuit/article/details/133786169