• RT-Thread 中断管理学习(二)


    中断的底半处理

    RTT不对中断服务程序所需要的处理时间做任何假设、限制,但如图其它实时操作系统或非实时操作系统一样,用户需要保证所有的中断服务程序在尽可能短的时间内完成(中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行)。这样在发生中断嵌套,或屏蔽了中断源的过程中,不会耽误嵌套的其它中断处理过程,或自身中断源的下一次中断信号。

    当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如CPU时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。

    但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将中断分割成两部分,上半部分底半部分

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

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

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

    rt_sem_t nw_bh_sem;
    
    void demo_nw_thread(void *param)
    {
    	//首先对设备进行必要的初始化工作
    	device_init_setting();
    	nw_hb_sem = rt_sem_create("bh_sem", 0, RT_IPC_FLAG, PRIO);
    	while(1)
    	{
    		rt_sem_take(nw_bh_sem, RT_WAITING_FOREVER);
    		//接收到信号量后,开始真正的Bottom Half处理过程
    		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);
    }
    
    • 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

    demo_nw_isr是如何处理Top Half,并开启Bottom Half的。

    void demo_nw_isr(int vector, void *param)
    {
    	//当network设备接收到数据后,陷入中断异常,开始执行此ISR
    	//开始Top Half部分的处理,如读取硬件设备的状态以判断发生了何种中断
    	nw_device_status_read();
    	rt_sem_release(nw_bh_sem);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    中断服务程序通过对一个信号量对象的等待和释放,来完成中断Bottom Half的起始和终结。
    由于将中断处理划分为Top和Bottm两个部分后,使得中断处理过程变为异步过程。
    必须认真考虑中断服务的处理时间是否大于给Bottom Half发送通知并处理的时间。

    中断管理接口

    为了把操作系统和系统底层的异常、中断硬件隔离开发,RTT把中断和异常封装为一组抽象接口。

    在这里插入图片描述

    中断服务程序挂接

    系统把用户的中断服务程序(Handler)和指定的中断号关联起来,可调用如下的接口挂载一个新的中断服务程序:

    rt_isr_handler_t rt_hw_interrupt_install(int vector,
                                            rt_isr_handler_t  handler,
                                            void *param,
                                            char *name);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用这个接口后,当中断源产生中断时,系统将自动调用装载的中断服务程序。

    1. vector:挂载的中断号。
    2. handler:新挂载的中断服务程序。
    3. param:param会作为参数传递给中断服务程序。
    4. name:中断的名称。
    5. return :挂载这个中断服务程序之前挂载的中断服务程序的句柄

    这个 API 并不会出现在每一个移植分支中,例如通常 Cortex-M0/M3/M4 的移植分支中就没有这个 API。

    中断服务程序是一种需要特别注意的运行环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(特权模式)),在这个运行环境中不能使用挂起当前线程的操作,因为当前线程并不存在,执行相关的操作会有类似打印提示信息,“Function [abc_func] shall not used in ISR”,含义是不应该在中断服务程序中调用的函数)。

    中断源管理

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

    屏蔽中断源可以保证接下来的处理过程中硬件状态或者数据不会受到干扰。

    可调用下面这个函数接口:

    void rt_hw_interrupt_mask(int vector)
    
    • 1

    调用这个接口后,相应的中断会被屏蔽(通常当这个中断触发时,中断状态寄存器会有相应的变化,但并不送达到处理器进行处理)。

    为了尽可能的不丢失硬件中断信号,可调用下面的函数接口打开屏蔽的中断源:

    void rt_hw_interrupt_unmask(int vector);
    
    • 1

    全局中断开关

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

    rt_base_t rt_hw_interrupt_disable(void);
    
    • 1

    恢复中断也称为开中断,使能中断,它恢复了调用rt_hw_interrupt_disable()函数前的中断状态。如果调用之前是关中断状态,那么调用后依旧是关中断。

    void rt_hw_interrupt_enable(rt_base_t level);
    
    • 1

    level:前一次rt_hw_interrupt_disable返回的中断状态

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

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

    level = rt_hw_interrupt_disable();
    a = a + value;
    rt_hw_interrupt_enbale(level);
    
    • 1
    • 2
    • 3

    在使用中断锁时,需要确保关闭中断的时间非常短,例如上面代码中的 a = a + value; 也可换成另外一种方式,例如使用信号量

    rt_sem_take(sem_lock, RT_WAITING_FOREVER);
    a = a + value;
    rt_sem_release(sem_lock);
    
    • 1
    • 2
    • 3

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

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

    #include 
    
    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();
    
    	//恢复到全局中断第二次关闭之前的状态,所以本次enbale之后全局中断还是关闭的
    	rt_hw_interrupt_enbale(level1);
    	//恢复到全局中断第一次关闭之前的状态,这时候的全局中断状态可能是打开的,也可能是关闭的
    	rt_hw_interrupt_enable(level0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    中断通知

    当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态。针对这种情况,可通过以下接口:

    void rt_interrupt_enter(void);
    void rt_interrupt_leave(void);
    
    • 1
    • 2

    这两个接口分别用在中断前导程序和中断后续程序中,均会对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);
    
    • 1

    在这里插入图片描述

    中断与轮询

    当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。

    轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。

    例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。相应的代码可以是这样的:

    while(size)
    {
    	while(!(uart->uart_device->SR & USART_FLAG_TXE)); //发送完成置1,退出循环
    	uart->uart_device->DR = (*ptr & 0x1FF);
    	++ptr;
    	--size;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所有的线程会一直运行,比它低优先级的线程不会得到运行。

    而分时系统中,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。

    所以通常情况下,实时系统中更多采用的是中断模式来驱动外设
    当数据达到时,由中断唤醒相关的处理线程,再进行后续的动作。

    例如一些携带FIFO的串口外设,其写入过程是这样的:
    在这里插入图片描述
    线程先向串口的FIFO写入数据,当FIFO满时,线程主动挂起。串口控制器持续地从FIFO中取出数据并以配置的波特率发送出去。当FIFO中所有数据都发送完成时,将向处理器触发一个中断;当中断服务程序得到执行时,可以唤醒这个线程。这里举例的是 FIFO 类型的设备,在现实中也有 DMA 类型的设备,原理类似。

    对于低速设备来说,运用这种模式非常好,因为在串口外设把FIFO中的数据发送出去前,处理器可以运行其它的线程,这样就提高了系统的整体运行效率。

    但是对于一些高速设备,例如传输速度达到10Mbps的时候,假设一次发送的数据量是32字节,我们可以计算出发送这样一段数据量需要的时间是:(32 X 8) X 1/10Mbps = 25us。当数据需要继续传输时,系统将在25us后触发一个中断以唤醒上层线程继续下次传递。
    假设系统的线程切换是8us,那么当整个系统运行时,对于数据带宽利用率将只有 25/(25+8) =75.8%。但是采用轮询模式,数据带宽的利用率则可能达到 100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了线程切换上

    发送数据量越小,发送速度越快,对于数据吞吐量的影响也越大。当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:

    1. 增加每次数据发送的长度,每次尽量让外设尽量多地发送数据。
    2. 必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。

    全局中断开关使用示例

    在多线程访问同一个变量时,使用开关全局中断对该变量进行保护。

    #include 
    #include 
    
    #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 interrupt_sample(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;
    }
    
    /* 导出到 msh 命令列表中 */
    MSH_CMD_EXPORT(interrupt_sample, interrupt sample);
    
    • 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
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断作为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间。

  • 相关阅读:
    shell脚本中的运算符和条件判断
    YOLOv8 windows下的离线安装 offline install 指南 -- 以 带有CUDA版本的pytorch 为例
    职业性格在求职应聘和跳槽中的作用
    asp.net core MVC 上传单文件与多文件
    如何快速做跨业务测试?
    羽夏看Linux内核——环境搭建
    【期末复习笔记】知识产权法——著作权、专利法、商标权
    Python3中的算术运算符
    Windows平台上安装MySql 5.6 /8.0等的各种问题解决办法汇总
    Mac Pro在重装系统时提示“未能与恢复服务器取得联系”
  • 原文地址:https://blog.csdn.net/Caramel_biscuit/article/details/132641188