RT-Thread底层汇编及在arm与riscv上的差异


  本文的目的是分析RT-Thread的底层汇编,以Cortex-M3内核为例分析,在RT-Thread的kernel源码中,这部分内容属于libcpu,在libcpu中,有不同cpu架构的底层实现,如riscv、arm等。kernel中的这部分代码负责线程堆栈的初始化、上下文切换(也可以叫线程切换)、HardFault异常处理、全局中断的控制。掌握这部分代码需要理解三个中断,一个过程。三个中断指的是全局中断、PendSV中断、HardFault中断。一个过程指的是线程切换的过程。读完文章后,你一定对RT-Thread底层做的那些与CPU架构相关的内容有了了解,之后根据不同架构的指令集按照三个中断、一个过程的分析方法都能分析和理解,可以将RT-Thread移植到任意内核的cpu上。后面的内容就是按照三个中断、一个过程进行展开。

一、内核寄存器

在这里插入图片描述
  cortex-m3的内核寄存器如上图。R0-R15是通用寄存器,其中的R13-R15都有各自的含义,SP是堆栈指针。每个线程都有自己独立的堆栈,LR是返回寄存器,保存返回的地址,PC是程序计数寄存器,保存下一条需要执行的指令。最下面的那五个特殊功能寄存器在中断和异常中有作用。后面用到的时候会加以说明。

二、三个中断

2.1 第一个关键中断:全局中断

/*
 * rt_base_t rt_hw_interrupt_disable();
 */
    .global rt_hw_interrupt_disable
    .type rt_hw_interrupt_disable, %function
rt_hw_interrupt_disable:
    MRS     R0, PRIMASK
    CPSID   I
    BX      LR

/*
 * void rt_hw_interrupt_enable(rt_base_t level);
 */
    .global rt_hw_interrupt_enable
    .type rt_hw_interrupt_enable, %function
rt_hw_interrupt_enable:
    MSR     PRIMASK, R0
    BX      LR

内核寄存器说明

PRIMASK(Priority Mask Register 优先级掩码寄存器)
寄存器作用:PRIMASK寄存器屏蔽具有可配置优先级的所有异常的激活,关闭除了HMI异常外的所有中断。从内核寄存器的架构可以看到PRIMASK属于特殊功能寄存器,可以通过指令MRS和MSR操作。

指令说明

在这里插入图片描述
MRS:Rd, 读取特殊功能寄存器的值到通用寄存器
MSR:,Rn, 把通用寄存器的值写入特殊功能寄存器
理解:M是MOV,R是通用寄存器R0-R15,S是特殊功能寄存器,汇编一般左边是目标,右边是源,MRS按顺序展开就是MRS:Rd,
在这里插入图片描述
CPSID I 设置PRIMASK寄存器的值为1,关闭中断
中断处理过程
关闭全局中断:把PRIMASK的值0写入R0,调用CPSID I关闭全局中断
使能全局中断:把R0的值0写入PRIMASK,使能全局中断
关闭全局中断的意义
由于关闭了systick中断,系统节拍暂停,相当于时间禁止,系统停止调度,当前只有这个线程有MCU的控制权,有效处理多线程对共享资源的使用。

2.2 第二个关键中断:PendSV异常

2.2.1 PendSV异常介绍

  PendSV全称Pendable request for system service,SV是server服务的缩写,PendSV用于操作系统的上下文切换,上下文从内存的角度看就是线程的堆栈,在初始化或者创建线程的时候都需要设置线程的堆栈,上下文指的就是这个堆栈,每个线程都有独立的线程堆栈,在线程的堆栈中保存了内核寄存器的内容,如PC、LR、R0-R12的内容,PC、LR、R0-R12的内容保存了一个时刻的现场,这个现场就是上下文,上下文保存了线程切出和切入的现场信息。
  区别于SVC,二者的区别是PendSV是异步的,SVC是同步的。挂起(pending)的中断请求会等待比它优先级更高的中断处理完才能处理,这就用到可嵌套中断控制器NVIC了,NVIC将挂起的中断根据优先级的大小逐个执行。而PendSV异常被设置为优先级最小,所以PendSV会在所有中断都处理完后才去执行,这就保证上下文的切换不会影响到中断。我们知道中断需要快进快出,所以上下文切换不能在Systick中断里处理,把它给放到优先级最低的PendSV做处理。

在这里插入图片描述

2.2.2 代码分析

/* R0 --> switch from thread stack
 * R1 --> switch to thread stack
 * psr, pc, LR, R12, R3, R2, R1, R0 are pushed into [from] stack
 */
    .global PendSV_Handler
    .type PendSV_Handler, %function
PendSV_Handler:
    /* disable interrupt to protect context switch */
    MRS     R2, PRIMASK
    CPSID   I

    /* get rt_thread_switch_interrupt_flag */
    LDR     R0, =rt_thread_switch_interrupt_flag
    LDR     R1, [R0]
    CBZ     R1, pendsv_exit         /* pendsv aLReady handled */

    /* clear rt_thread_switch_interrupt_flag to 0 */
    MOV     R1, #0
    STR     R1, [R0]

    LDR     R0, =rt_interrupt_from_thread
    LDR     R1, [R0]
    CBZ     R1, switch_to_thread    /* skip register save at the first time */

    MRS     R1, PSP                 /* get from thread stack pointer */
    STMFD   R1!, {R4 - R11}         /* push R4 - R11 register */
    LDR     R0, [R0]
    STR     R1, [R0]                /* update from thread stack pointer */

switch_to_thread:
    LDR     R1, =rt_interrupt_to_thread
    LDR     R1, [R1]
    LDR     R1, [R1]                /* load thread stack pointer */

    LDMFD   R1!, {R4 - R11}         /* pop R4 - R11 register */
    MSR     PSP, R1                 /* update stack pointer */

pendsv_exit:
    /* restore interrupt */
    MSR     PRIMASK, R2

    ORR     LR, LR, #0x04
    BX      LR

  在rt_hw_context_switch或者rt_hw_context_switch_to中设置rt_thread_switch_interrupt_flag标志,表示要处理PendSV异常,在进入PendSV后先清除rt_thread_switch_interrupt_flag标志,这种用法是裸机里面很常用的前后台处理,在中断外面设置一个标志,然后在中断中先清除标志再做处理。看到下方的流程图,清除完标志后先保存from线程的上下文,再切出to线程的上下文。有一种情况是没有from线程的,那就是rt_hw_context_switch_to,这个线程切换函数用在系统刚开始调度的时候rt_system_scheduler_start,所以对rt_interrupt_from_thread做了一个判空,如果为空则直接切换到to线程的上下文。下面的汇编代码的模式用于获取变量的值,在汇编代码里用得比较多。

    LDR     R0, =rt_thread_switch_interrupt_flag
    LDR     R1, [R0]
    //把rt_thread_switch_interrupt_flag变量的值赋值给R1

在这里插入图片描述

2.3 第三个关键中断:HardFault异常

2.3.1 HardFault介绍

在这里插入图片描述

2.3.2 代码分析

  PSP是进程堆栈,MSP是主堆栈,在系统调度之前使用的是MSP,异常处理程序(例如 PendSV)可以通过改变其在退出时使用的 EXC_RETURN 值来改变使用哪个堆栈。在pendsv_exit中通过 ORR LR, LR, #0x04将使用的堆栈设置为进程栈。
  在HarldFalut中通过TST lr, #0x04判断是MSP还是PSP,把上下文入栈后通过rt_hw_hard_fault_exception把上下文中的内容打印到终端,根据dump的寄存器值判断触发系统HardFault的原因。下见面的代码。

    .global HardFault_Handler
    .type HardFault_Handler, %function
HardFault_Handler:
    /* get current context */
    MRS     r0, msp                 /* get fault context from handler. */
    TST     lr, #0x04               /* if(!EXC_RETURN[2]) */
    BEQ     _get_sp_done
    MRS     r0, psp                 /* get fault context from thread. */
_get_sp_done:

    STMFD   r0!, {r4 - r11}         /* push r4 - r11 register */
    STMFD   r0!, {lr}               /* push exec_return register */

    TST     lr, #0x04               /* if(!EXC_RETURN[2]) */
    BEQ     _update_msp
    MSR     psp, r0                 /* update stack pointer to PSP. */
    B       _update_done
_update_msp:
    MSR     msp, r0                 /* update stack pointer to MSP. */
_update_done:

    PUSH    {LR}
    BL      rt_hw_hard_fault_exception
    POP     {LR}

    ORR     LR, LR, #0x04
    BX      LR

三、RT-Thread线程切换过程

3.1 寄存器说明

在这里插入图片描述

3.2 线程切换过程

SysTick_Handler->rt_tick_increase->rt_schedule->rt_hw_context_switch((rt_ubase_t)&from_thread->sp,(rt_ubase_t)&to_thread->sp);

  在systick中断里面判断时间片是否用完来决定是否需要切换线程,如果需要切换线程则执行rt_hw_context_switch,在rt_hw_context_switch中把from线程和to线程的栈顶地址保存在全局变量rt_interrupt_from_thread和rt_interrupt_to_thread,最后触发PendSV异常。在适当时机处理PendSV异常(等待其它中断都处理完),PendSV中保存from线程的现场,切出to线程的现场。总结就是始于systick,终于MSR PSP, R1。见如下汇编代码和程序流程图

/*
 * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
 * R0 --> from
 * R1 --> to
 */
    .global rt_hw_context_switch_interrupt
    .type rt_hw_context_switch_interrupt, %function
    .global rt_hw_context_switch
    .type rt_hw_context_switch, %function
rt_hw_context_switch_interrupt:
rt_hw_context_switch:
    /* set rt_thread_switch_interrupt_flag to 1 */
    LDR     R2, =rt_thread_switch_interrupt_flag
    LDR     R3, [R2]
    CMP     R3, #1
    BEQ     _reswitch
    MOV     R3, #1
    STR     R3, [R2]

    LDR     R2, =rt_interrupt_from_thread   /* set rt_interrupt_from_thread */
    STR     R0, [R2]

_reswitch:
    LDR     R2, =rt_interrupt_to_thread     /* set rt_interrupt_to_thread */
    STR     R1, [R2]

    LDR     R0, =ICSR           /* trigger the PendSV exception (causes context switch) */
    LDR     R1, =PENDSVSET_BIT
    STR     R1, [R0]
    BX      LR

在这里插入图片描述
在这里插入图片描述

四、在arm与riscv上的差异

1、riscv中没有类似于PendSV的中断,上下文切换只有一个过程,而在arm中有两个过程:一是rt_hw_context_switch,二是PendSV异常处理。
2、在arm中,rt_hw_context_switch_interrupt和rt_hw_context_switch是一样的,因为上下文切换是在PendSV中进行的,切换过程不会对中断造成影响。而在riscv中没有类似于PendSV的机制,所以这两个切换函数需要做区分,在中断中只能使用rt_hw_context_switch_interrupt。除了arm以外的其它cpu内核,都与riscv类似,rt_hw_context_switch_interrupt和rt_hw_context_switch需要做区分。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值