本文的内容是关于RT-Thread中断管理的学习总结,包括简单地介绍了什么是中断,裸机中断与RT-Thread中断有什么区别,RT-Thread是如何处理中断的,RT-Thread内核提供哪些中断相关的接口,等等。
关于RT-Thread中断管理相关的内容,官方提供了比较丰富的文档作为参考,具体可以查看以下链接:
https://www.rt-thread.org/document/site/programming-manual/interrupt/interrupt/#rt-thread
本文尝试从以下几个方面总结一下RT-Thread中断管理的学习过程。
中断相关的概念描述
什么是中断?中断,顾名思义就是一项正在进行的工作,突然间被其他事情打断,导致原来正在进行的工作不能继续正常进行,而需要去把其他事情处理完,才能回来继续进行原来的工作。
如何通俗地理解中断?想象一下这样的场景,周末你正在家里愉快地写着代码,突然间你的手机铃声响了,你必须停下手里的工作,记录代码写到哪个阶段,然后就去接这个电话了。“写代码”就是正在进行的工作,“电话响起”就是中断事件。
这个电话是媳妇打过来的,她让你去菜市场买点韭菜和猪肉,晚上包饺子吃,媳妇的话哪敢不听,于是你觉得菜市场买东西比较重要,挂掉电话后就去买东西了,买完东西回来后,再接着写刚刚还没完成的代码。“菜市场买东西”就是中断服务程序,这就是一个典型的中断处理过程。
关于中断的操作模式和特权级别,Cortex-M的处理器有三种状态划分,分别是:特权级处理模式,特权级线程模式,用户级线程模式。这三种状态的关系,如下图所示。
从上图可以看出,中断或异常的服务程序,总是处于特权级处理模式的。而RT-Thread系统内核复位上电时启动的主线程(main线程),是运行在特权级线程模式的。其他用户创建的线程,是运行在用户级线程模式的。
为什么处理器要区分特权级和用户级?特权,顾名思义就是处理器如果工作在这个级别下,权限就会比较高,就可以访问一些特殊的寄存器,以防止用户级的代码访问这些特殊寄存器,对数据进行破坏。中断由于其特殊性,所以,中断函数是工作在特权级别下的。
裸机中断与操作系统中断两者有什么区别呢?我们在裸机代码中处理硬件中断的时候,一般只要编写中断处理函数就可以了,这种方式处理中断,简单且直接。
然而,有了操作系统之后,所有的东西都变了,要考虑的问题就多了很多。因为操作里面运行了很多线程,中断来了之后,就要告知操作系统,把当前运行线程的信息保存到栈里面,再去处理中断服务程序,处理完中断要再回去处理线程,此时又可能涉及到线程切换调度,而线程切换本身又需要PendSV中断参与。
所以,在裸机处理中断和在操作系统中处理中断,简直就是天壤之别。
RT-Thread 中断处理机制
了解过Cortex-M系列单片机的工程师,一般都知道在芯片的汇编启动文件startup_xxx.s里面,有一个中断向量表,所有的中断都是通过这个中断向量表来进行处理的。
当一个中断异常触发的时候,处理器将会判断是哪个中断源,然后跳转到固定位置进行处理,每个中断服务程序的地址入口必须是放到统一的地址上,也就是需要设置到NVIC的中断向量偏移寄存器里面,中断向量表如下图所示。
其实,不管有没有操作系统的参与,一旦硬件发送中断和异常之后,中断的入口都是在这个中断向量表的。区别无非就是在裸机环境下,直接处理中断服务程序,而在有操作系统的情况下,需要先保留线程的运行情况,然后再处理中断,处理完中断后,再恢复线程的运行环境。
硬件中断的优先级是最高的,任何线程的优先级都要低于硬件中断,因此,只要发生了硬件中断事件,系统就必须要进行相应的处理。
RT-Thread在处理中断的时候,一般都会有三个阶段:中断前导程序,中断服务程序,中断后续程序,这三个阶段,如下图所示。
中断前导程序的主要工作是,当中断事件发生的时候,处理器的硬件会把当前CPU相关的寄存器参数自动压入中断栈里面。程序需要调用rt_interrupt_enter()函数,把全局变量rt_interrupt_nest进行加1操作,这个全局变量是用来记录中断的嵌套层数的。
用户中断服务程序的主要工作分两种情况,一种是不进行线程切换,另一种是进行线程切换。不进行线程切换的话,中断服务程序和中断后续程序运行完成后,将返回被中断的线程。
而如果要进行线程切换,则会调用rt_hw_context_switch_interrupt() 函数进行上下文切换,这个函数主要是设置变量rt_interrupt_to_thread,然后触发PendSV中断。
在这里要注意一下:由于PendSV中断的优先级最低,不能进行中断抢占,因此即使触发了该中断,但由于此时还在用户中断处理函数里面,所以PendSV中断还处于等待阶段,只有退出了中断后续程序,才会进行PendSV中断处理,才会进行线程的上下文切换。所以,线程的上下文切换是不会在用户中断里面进行的,是在中断结束后进行的。
中断后续程序的主要工作是,通知系统内核离开中断状态,通过调用rt_interrupt_leave()函数,将全局变量rt_interrupt_nest进行减1操作,然后从中断栈里面恢复恢复CPU相关的寄存器参数。
这里恢复CPU寄存器参数的时候需要注意,如果在用户中断里面涉及到线程切换,那么这个时候就需要恢复到新的线程CPU寄存器参数,而不是恢复到被中断打断的线程CPU寄存器参数。
RT-Thread操作系统在处理中断的时候,通常采用“上半部分(Top Half)”和“底半部分(Bottom Half)”这种方式。原因在于,操作系统本身不会对中断服务程序的处理时间做任何假设和限制,但为了保证系统的实时性,用户需要保证中断服务程序在尽可能短的时间内完成。
如何理解“上半部分(Top Half)”和“底半部分(Bottom Half)”这种中断处理方式?还是以买菜为例。媳妇来电话让你到菜市场买菜(中断事件),但你考虑到如果长时间中断不写代码,会导致思路断链,为了避免这种情况(避免长时间处理中断服务),完全可以在网上下单购买(短时间的中断处理),生鲜超市收到下单信息(信号量、邮件、消息队列),就会安排快递小哥送货上门,买菜这么耗时的工作就由其他人(其他线程)去完成了。
“上半部分(Top Half)”和“底半部分(Bottom Half)”这种中断处理方式,主要是应用在一些需要耗时处理中断事务的场合,比如数据的接收和处理。通常接收数据的时间比较短,只要把接收到的数据保存下来即可,但处理数据的过程就可能比较耗时,这样就需要分开来处理,上半部分就是接收数据,底半部分就是耗时的数据处理。
RT-Thread中断相关的API函数接口
为了把操作系统和硬件底层的中断异常隔离开来,RT-Thread系统内核把中断和异常封装为一组抽象的接口,具体的函数接口如下图所示。
RT-Thread中断相关的应用示例
RT-Thread中断相关的应用示例,主要是为了验证中断相关的API接口函数,例如全局中断开关的使用示例,通过按键中断示例来验证“上半部分(Top Half)”和“底半部分(Bottom Half)”这种中断处理方式。
示例源码下载链接:
https://github.com/embediot/rtthread_study_notes
全局中断开关示例,主要是为了验证多线程访问同一个变量时,使用开关全局中断的方式对该全局变量进行临界区保护。
按键中断示例,主要是为了验证“上半部分(Top Half)”和“底半部分(Bottom Half)”这种中断处理方式。通过按键触发中断事件,在中断服务函数里面发送邮件,通知线程进行相应的处理。
在irq_test.h头文件里面,通过打开相应的宏定义开关,重新编译工程源码,下载到开发板即可验证实验现象,如下图所示。
RT-Thread中断应用的注意事项
中断是一种异常,当系统发生中断异常的时候就必须要进行处理,在RT-Thread实时操作系统里面处理中断的时候,如果不及时处理或对中断处理不当,轻则会造成系统出错或逻辑混乱,重则会导致系统毁灭性地瘫痪。
在处理RT-Thread中断异常的时候,有以下注意事项:
1、中断服务程序工作在特权级处理模式,优先级比任何线程要高,任何线程都不能抢占中断服务程序。
2、在操作系统里面,可以支持中断嵌套,高优先级中断可以抢占低优先级中断,线程的重新调度是在所有中断都处理完之后才重新启动的。
3、在Cortex-M架构里面,中断发生时CPU的寄存器入栈是由硬件自动完成的,中断的前导程序通常只是记录中断的嵌套层数。
4、RT-Thread采用独立的内存空间作为中断栈,而不是采用线程栈作为中断栈,这种方式随着线程的增加,减少内存占用的效果也越明显。
5、建议采用“上半部分(Top Half)”和“底半部分(Bottom Half)”这种方式来处理中断异常,中断服务程序的处理时间应尽可能短。
6、使用全局中断开关是禁止多线程访问临界区最简单的一种方式,这种方式可以应用在任何场合,但要注意这种方式对系统实时性影响巨大,使用不当会破坏系统的实时性能。使用全局中断锁的时间应尽可能短。
7、全局中断开关支持多级中断嵌套使用,每次调用rt_hw_interrupt_enable()函数,可以让系统恢复到关中断之前的状态(这个状态有可能是关中断也有可能是开中断)。
8、中断服务程序是运行在特权处理模式下的,在这种运行模式里面是不能使用挂起当前线程操作的相关函数的,因为中断服务程序的运行环境里面根本不存在线程。