zsky · 2022年04月11日

RT-Thread快速入门-中断管理

首发,公众号【一起学嵌入式

经过前面文章的学习,对于 RT-Thread 处理多任务或者说线程的处理机制,基本上入门了。能够上手用 RT-thread 进行日常开发了。

但是,还有一个重要的部分,那就是 RT-Thread 如何处理中断。

说到中断,大家都不会陌生,对于裸机编程,很简单,编写指定的中断服务函数就可以了。

如果工程建立在 RTOS 基础之上,中断是如何管理的呢?本文带你了解 RT-Thread 的中断处理过程,以及如何添加中断服务程序和相关的注意事项。

RT-Thread 中断工作机制

中断处理的一般过程如下:定义中断服务函数;将其与 MCU 的中断向量表中的中断向量建立联系;中断发生时,中断服务程序开始执行,执行完成后,退出中断。

202203122129855.png

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

RT-Thread 中断处理

RT-Thread 将中断处理程序分为三部分:

  • 中断前导程序
  • 用户中断服务程序
  • 中断后续程序

202203122240625.png

1. 中断前导程序

中断前导程序完成的工作为:

  • 保存 CPU 中断现场。不同 CPU 架构的实现方式有差异
  • 通知内核进入中断状态。通过调用 rt_interrupt_enter() 函数来完成。

rt_interrupt_enter() 函数定义如下:

void rt_interrupt_enter(void)
{
  rt_base_t level;
  level = rt_hw_interrupt_disable();
  rt_interrupt_nest ++;
  rt_hw_interrupt_enable(level);
}

2. 用户中断服务程序

RT-Thread 的用户中断服务程序的内部实现,有两种情况:

  • 中断服务程序不进行线程切换。退出中断模式后,返回被中断的线程。
  • 中断处理过程中需要进行线程切换。这种情况下,会调用 rt_hw_context_switch_interrupt() 函数进行上下文切换,该函数跟 CPU 架构相关。

以 Cortex-M 架构为例,rt_hw_context_switch_interrupt() 函数的实现流程如下图所示。将设置需要切换的线程 rt_interrupt_to_thread 变量,然后触发 PendSV 异常。PendSV 异常是专门用来辅助上下文切换的,且被初始化为最低优先级的异常 。

202203130003149.png

3. 中断后续程序

中断后续程序主要完成的工作为:

  • 通知内核离开中断状态。通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1。代码如下:
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 上下文。

中断的底半部处理

一个中断发生时,中断服务程序处理过程需要的时间有时候比较耗时。这种情况可以将该终端分割为两部分:

  • 中断上半部分。
  • 中断下半部分。

上半部分取得硬件状态和数据,打开被屏蔽的中断,给相关的线程发送一条通知,然后退出中断服务程序。

下半部分通过用户线程实现,线程接收到通知后,对状态或数据进行处理。

RT-Thread 中断管理

RT-Thread 提供了一系列的中断管理接口函数,用于将操作系统和系统底层的异常、中断硬件隔离开来。如下图所示:

202203130025761.png

1. 中断服务程序安装

把用户的中断服务程序和指定的中断号关联起来,可用如下的接受函数,安装一个中断服务程序。当中断源发生中断时,系统会自动调用装载的终端服务程序。

rt_isr_handler_t rt_hw_interrupt_install(int vector,
                                          rt_isr_handler_t handler,
                                          void *param,
                                          char *name);

参数 vetor 为挂载的终端号;handler 为中断服务程序;param 为传递给中断服务程序的参数;name 为中断名称。

函数返回挂载这个中断服务程序之前挂载的中断服务程序句柄。

rt_isr_handler_t 定义如下:

typedef void (*rt_isr_handler_t)(int vector, void *param);

注意:这个函数只会出现在特定的移植分支中。通常 Cortex-M0/M3/M4 的移植分支中就没有这个 API

2. 中断源管理

通常在 ISR 准备处理某个中断信号之前,我们需要先屏蔽该中断源,在 ISR 处理完状态或数据以后,及时地打开之前被屏蔽的中断源。

屏蔽中断源可以保证在接下来的处理过程中硬件状态或者数据不会受到干扰,可调用下面这个函数接口:

void rt_hw_interrupt_mask(int vector)

参数 vector 需要屏蔽的终端号。

打开被屏蔽的中断源,是为了尽可能地不丢失硬件中断信号。

void rt_hw_interrupt_umask(int vector)

注意:这两个函数只会出现在特定的移植分支中。通常 Cortex-M0/M3/M4 的移植分支中就没有这个 API

3. 全局中断开关

禁止多线程访问临界区最简单的一种方式,是通过关闭系统中断来保证当前线程不会被其他事件打断。全局中断开关也称为中断锁。

RT-Thread 中关闭全局中断的函数接口如下:

rt_base_t rt_hw_interrupt_disable(void)

该函数返回调用之前的中断状态。

恢复全局中断的函数接口为 rt_hw_interrupt_enable(),如下。该函数恢复了调用 rt_hw_interrupt_disable() 函数前的中断状态。

void rt_hw_interrupt_enable(rt_base_t level)

关于全局中断开关的几点说明:

  • 中断锁可以用于任何需要操作临界区的场合。中断锁对系统的实时性影响非常大,要慎重使用。当然,若使用得当,则会变成一种快速、高效的同步方式。
  • 使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。因此在使用中断锁时,需要确保关闭中断的时间非常短。
  • 函数 rt_hw_interrupt_disable() 和函数 rt_hw_interrupt_enable() 一般需要配对使用,从而保证正确的中断状态。
  • RT-Thread 支持全局中断的 API 多级嵌套使用。如下示例代码:
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);
}

4. 中断通知

当中断发生,进入中断处理函数时,需要通知内核当前已经进入到中断状态。可以调用如下接口函数:

/* 进入中断时,通知内核当前已经进入中断状态 */
void rt_interrupt_enter(void);
/* 退出中断时,用于通知内核当前已经退出中断状态 */
void rt_interrupt_leave(void);

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

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

。所以中断锁对系统的实时性影响非常巨大,当
使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则
会变成一种快速、高效的同步方式

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

实例演示

该例程创建两个线程,这两个线程访问同一个变量,使用开关全局中断对该变量进行保护:

#include <rthw.h>
#include <rtthread.h>
#define THREAD_PRIORITY 20
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5

/* 同时访问的全局变量 */
static rt_uint32_t cnt;

void thread_entry(void *parameter)
{
    rt_uint32_t no;
    rt_uint32_t level;

    no = (rt_uint32_t) parameter;

    while (1)
    {
        /* 关闭全局中断 */
        level = rt_hw_interrupt_disable();

        cnt += no;
        
        /* 恢复全局中断 */
        rt_hw_interrupt_enable(level);
        
        rt_kprintf("protect thread[%d]'s counter is %d\n", no, cnt);
        
        rt_thread_mdelay(no * 10);
    }
}

/* 用户应用程序入口 */
int main(void)
{
    rt_thread_t thread;
    /* 创建 t1 线程 */
    thread = rt_thread_create("thread1", thread_entry, (void *)10,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY, THREAD_TIMESLICE);
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }

    /* 创建 t2 线程 */
    thread = rt_thread_create("thread2", thread_entry, (void *)20,
                                THREAD_STACK_SIZE,
                                THREAD_PRIORITY, THREAD_TIMESLICE);
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }
    
    return 0;
}

编译运行结果如下:

202203130126274.png

小结

至此,关于 RT-Thread 内核使用的相关内容全部介绍完毕。希望通过这个系列文章,能够帮助有需要的朋友快速入门,可以使用 RT-Thread 完成自己的需求。

后续,会写 RT-Thread 其他方面的文章(内核源码解析、项目实战等等),欢迎大家关注,一起学习,一起进步。

加油~

公众号【一起学嵌入式】,分享 RTOS、Linux、C技术知识
推荐阅读
关注数
2392
内容数
31
公众号【一起学嵌入式】专注嵌入式软件技术分享,RTOS、Linux、C/C++,一起学习,一起进步。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息