RTT学习

发布于:2024-04-19 ⋅ 阅读:(23) ⋅ 点赞:(0)

中断管理

什么是中断?简单的解释就是系统正在处理某一个正常事件,忽然被另一个需要马上处理的紧急事件打断,系统转而处理这个紧急事件,待处理完毕,再恢复运行刚才被打断的事件。生活中,我们经常会遇到这样的场景:

当你正在专心看书的时候,忽然来了一个电话,于是记下书的页码,去接电话,接完电话后接着刚才的页码继续看书,这是一个典型的中断的过程。

电话是老师打过来的,让你赶快交作业,你判断交作业的优先级比看书高,于是电话挂断后先做作业,等交完作业后再接着刚才的页码继续看书,这是一个典型的在中断中进行任务调度的过程。

当CPU正在处理内部数据时,外界发生了紧急情况,要求CPU暂停当前的工作转去处理这个异步事件。

处理完毕后,再回到原来被中断的地址,继续原来的工作。

中断是一种异常,异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性地瘫痪。所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环。如下图是一个简单的中断示意图。

在这里插入图片描述
中断处理与CPU架构密切相关,所以本章先介绍ARM Cortex-M的CPU架构,然后结合Cortex-M CPU架构来介绍RTT的中断管理机制。

Cortex-M CPU架构基础

不同于老的经典的ARM处理器(例如ARM7,ARM9),ARM Cortex-M处理器有一个非常不同的架构,Cortex-M是一个家族系列,其中包括M0/M3/M4/M7多个不同型号,每个型号之间会有些区别,例如Cortex-M4比Cortex-M3多了浮点计算功能等,但它们的编程模型基本是一致的。

寄存器简介

Cortex-M系列CPU的寄存器组里有R0-R15共16个通用寄存器组和若干特殊功能寄存器。
在这里插入图片描述
通用寄存器组里的R13作为堆栈指针寄存器(Stack Pointer,SP);R14作为连接寄存器(Link Register,LR),用于在调用子程序时,存储返回地址;R15作为程序计数器(Program Counter,PC),其中堆栈指针寄存器可以是主堆栈指针(MSP),也可以是进程堆栈指针(PSP)。

在这里插入图片描述
特殊功能寄存器包括程序状态字寄存器组(PSRs)、中断屏蔽寄存器组(PRIMASK,FAULTMASK,BASEPRI)、控制寄存器(CONTROL),可以通过MSR/MRS指令访问特殊功能寄存器。

MRS R0,CONTROL ;读取CONTROL到R0
MSR CONTROL,R0 ;写入R0到CONTROL寄存器中

程序状态字寄存器里保存算术与逻辑标志,例如负数标志,零结果标志,溢出标志等等。
中断屏蔽寄存器组控制Cortex-M的中断使能。控制寄存器用来定义特权级别和当前使用哪个堆栈指针。

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

操作模式和特权级别

Cortex-M引入了操作模式和特权级别的概念,分别为线程模式和处理模式,如果进入异常或中断处理则进入处理模式,其它情况是线程模式。

在这里插入图片描述
Cortex-M有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级。可通过CONTROL特殊寄存器控制。

Cortex-M的堆栈寄存器SP对应两个物理寄存器MSP和PSP,MSP为主堆栈,PSP为进程堆栈,处理模式总是使用MSP作为堆栈,线程模式可以选择使用MSP或PSP作为堆栈,同样通过CONTROL特殊寄存器控制。

复位后,Cortex-M默认进入线程模式、特权级、使用MSP堆栈。

嵌套向量中断控制器

Cortex-M中断控制器名为NVIC(嵌套向量中断控制器),支持中断嵌套功能。
当一个中断触发并且系统进行响应时,处理器会将当前运行位置的上下文寄存器自动压入中断栈中,这部分的寄存器包括PSR、PC、LR、R12、R3-R0寄存器。

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

PendSV系统调用

PendSV也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。
PendSV异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发Pendsv异常,在PendSV异常处理函数中进行上下文切换。

中断向量表

中断向量表是所有中断处理程序的入口,把用户中断服务程序同一个虚拟中断向量表中的中断向量联系在一起。当中断向量对应中断发生的时候,被挂接的用户中断服务程序就会被调用执行。

在这里插入图片描述
在Cortex-M内核上,所有中断都采用中断向量表的方式进行处理,当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,每个中断服务程序必须排列在一起放在统一的地址上(这个地址必须要设置到NVIC的中断向量偏移寄存器中)。

__Vectors       DCD     __initial_sp                      ; Top of Stack
                DCD     Reset_Handler                     ; Reset Handler
                DCD     NMI_Handler                       ; NMI Handler
                DCD     HardFault_Handler                 ; Hard Fault Handler
                DCD     MemManage_Handler                 ; MPU Fault Handler
                DCD     BusFault_Handler                  ; Bus Fault Handler
                DCD     UsageFault_Handler                ; Usage Fault Handler
                DCD     0                                 ; Reserved
                DCD     0                                 ; Reserved
                DCD     0                                 ; Reserved
                DCD     0                                 ; Reserved
                DCD     SVC_Handler                       ; SVCall Handler
                DCD     DebugMon_Handler                  ; Debug Monitor Handler
                DCD     0                                 ; Reserved
                DCD     PendSV_Handler                    ; PendSV Handler
                DCD     SysTick_Handler                   ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler                   ; Window WatchDog
                DCD     PVD_PVM_IRQHandler                ; PVD and PVM detector
                DCD     TAMP_STAMP_LSECSS_SSRU_IRQHandler ; RTC Tamper, RTC TimeStamp, LSECSS and RTC SSR Underflow Interrupts
                DCD     RTC_WKUP_IRQHandler               ; RTC Wakeup Interrupt
                DCD     FLASH_IRQHandler                  ; FLASH global Interrupt

请注意代码后面的 [WEAK] 标识,它是符号弱化标识,在 [WEAK] 前面的符号(如 NMI_Handler、HardFault_Handler)将被执行弱化处理,如果整个代码在链接时遇到了名称相同的符号(例如与 NMI_Handler 相同名称的函数),那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),而与弱化符号相关的代码将被自动丢弃。

以SysTick中断为例,在系统启动代码中,需要填上SysTick_Handler中断入口函数,然后实现该函数即可对SysTick中断进行响应

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

    if(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)
        HAL_IncTick();

    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

中断处理过程

RTT中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分。
在这里插入图片描述

中断前导程序

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()函数进行上下文切换。

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

中断后续程序

通知内核立刻中断状态。

void rt_interrupt_leave(void){
	rt_base_t level;
	level = rt_hw_interrupt_disable();
	rt_interrupt_nest--;
	rt_hw_interrupt_enable(level);
}

恢复中断前的CPU上下文,如果在中断处理过程中未进行线程切换,那么恢复from线程的CPU上下文。如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文。

中断嵌套

在允许中断嵌套的情况下,在执行中断服务程序的过程中,如果出现高优先级的中断,当前中断服务程序的执行将被打断,以执行高优先级中断的中断服务程序,当高优先级中断的处理完成后,被打断的中断服务程序才又得到继续执行,如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生,如下图所示。

在这里插入图片描述

中断栈

在中断处理过程中,在系统响应中断前,处理器需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。

在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。

中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。

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

在Cortex-M处理器内核里有两个堆栈指针,一个是主堆栈指针(MSP),是默认的堆栈指针,在运行第一个线程之前和在中断和异常服务程序里使用;另一个是线程堆栈指针(PSP),在线程里使用。

在中断和异常服务程序退出时,修改LR寄存器的第2位的值为1,线程的SP就由MSP切换到PSP。

中断的底半处理

用户需要保证所有的中断服务程序在尽可能短的时间内完成(中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行)。

当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如 CPU 时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分和底半部分。

在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是RTT提供的信号量、事件),然后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称为底半处理。

以一个虚拟的网络设备接收网络数据包作为范例,假设接到数据报文后,系统对报文的分析,处理是一个相对耗时的,比外部中断源信号重要性小许多的,且在不屏蔽中断源信号情况下也能处理的过程。

创建了一个nwt线程,这个线程在启动运行后,将阻塞在nw_bh_sem信号上,一旦这个信号量被释放,将执行接下来的nw_packet_parser过程,开始Bottom Half的事件处理。

rt_sem_t nw_bh_sem;

void demo_nw_thread(void *param){
	/*首先对设备进行必要的初始化工作*/
	device_init_setting();
	/*其它的一些操作*/
	/*创建一个semaphore来响应Bottom Half的事件*/
	nw_bh_sem = rt_sem_create("bh_sem", 0, RT_IPC_FLAG_PRIO);

	while(1){
		rt_sem_take("nw_bh_sem", RT_WAITING_FOREVER);
		nw_packet_parser(packet_buffer);
		nw_packet_process(packet_buffer);
	}
}

int main(void){
	rt_thread_t thread;
	thread = rt_thread_create("nwt", demo_nw_thread, RT_NULL, 1024, 20, 5);
	if(thread != RT_NULL){
		rt_thread_startup(thread);
	}
}

接下来看一下demo_nw_isr中如何处理Top Half,并开启Bottom Half的。

void demo_nw_isr(int vector, void *param){
	nw_device_status_read();
	rt_sem_release(nw_bh_sem);
}

从上面例子的两个代码片段可以看出,中断服务程序通过对一个信号量对象的等待和释放,来完成中断 Bottom Half 的起始和终结。由于将中断处理划分为 Top 和 Bottom 两个部分后,使得中断处理过程变为异步过程。这部分系统开销需要用户在使用 RT-Thread 时,必须认真考虑中断服务的处理时间是否大于给 Bottom Half 发送通知并处理的时间。

RTT中断管理接口

为了把操作系统和系统底层的异常、中断硬件隔离开来,RTT把中断和异常封装为一组抽象接口。
在这里插入图片描述

全局中断开关

全局中断开关也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关中断的方式,来保证当前线程不会被其它事件打断(因为整个系统不再响应那些可以触发线程重新调度的外部事件),当前线程不会被强制,除非这个线程主动放弃处理器控制器。

rt_base_t rt_hw_interrupt_disable(void);

恢复中断也称开中断,rt_hw_interrupt_enable()这个函数用于“使能”中断,它恢复了调用rt_hw_interrupt_disable()函数前的中断状态。
如果调用 rt_hw_interrupt_disable()函数前是关中断状态,那么调用此函数后依然是关中断状态。恢复中断往往是和关闭中断成对使用的。

void rt_hw_interrupt_enable(rt_base_t level);

使用中断锁来操作临界区的方法可以应用于任何场合,且其它几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。
使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。

为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量。

函数 rt_base_t rt_hw_interrupt_disable(void) 和函数 void rt_hw_interrupt_enable(rt_base_t level) 一般需要配对使用,从而保证正确的中断状态。

在RTT中,开关全局中断的API支持多级嵌套使用,简单嵌套中断的代码如下所示:

#include <rthw.h>

void global_interrupt_demo(void)
{
    rt_base_t level0;
    rt_base_t level1;

    /* 第一次关闭全局中断,关闭之前的全局中断状态可能是打开的,也可能是关闭的 */
    level0 = rt_hw_interrupt_disable();
    /* 第二次关闭全局中断,关闭之前的全局中断是关闭的,关闭之后全局中断还是关闭的 */
    level1 = rt_hw_interrupt_disable();

    do_something();

    /* 恢复全局中断到第二次关闭之前的状态,所以本次 enable 之后全局中断还是关闭的 */
    rt_hw_interrupt_enable(level1);
    /* 恢复全局中断到第一次关闭之前的状态,这时候的全局中断状态可能是打开的,也可能是关闭的 */
    rt_hw_interrupt_enable(level0);
}

这个特性可以给代码的开发带来很大的遍历。
例如在某个函数里关闭了中断,然后调用某些子函数,再打开中断。
这些子函数里面也可能存在开关中断的代码。由于全局中断的 API 支持嵌套使用,用户无需为这些代码做特殊处理。

中断通知

当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态。

void rt_interrupt_enter(void);
void rt_interrupt_leave(void);

这两个接口分别用在中断前导程序和中断后续程序中,均会对rt_interrupt_nest(中断嵌套深度)的值进行修改:

每当进入中断时,可以调用rt_interrupt_enter()函数,用于通知内核,当前已经进入了中断状态,并增加中断嵌套深度(执行rt_interrupt_nest++)

每当退出中断时,可以调用rt_interrupt_leave()函数,用于通知内核,当前已经离开了中断状态,并减少中断嵌套深度(执行rt_interrupt_nest–)。

注意不要在应用程序中调用这两个接口函数。

使用rt_interrupt_enter/leave()的作用是,在中断服务程序中,如果调用了内核相关的函数(如释放信号量等操作),则可以通过判断当前中断状态,让内核及时调整相应的行为。

例如:在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略,而不是立即进行切换。

但如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave() 函数。

在上层应用中,在内核需要知道当前已经进入到中断状态或当前嵌套的中断深度时,可调用 rt_interrupt_get_nest() 接口,它会返回 rt_interrupt_nest。如下:

rt_uint8_t rt_interrupt_get_nest(void);

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到