嵌入式实时系统的基石:ARM+Linux中断设计实战
在当今数字化时代,高效计算是推动科技发展的核心动力。而 ARM 与 Linux 中断系统,宛如开启这扇高效计算大门的关键钥匙。ARM 架构,以其低功耗、高性能的特性,广泛应用于从智能手机到工业控制等各类设备,为系统运行奠定了坚实基础。Linux 操作系统,则凭借开源、灵活的优势,成为众多开发者的首选。
那么,中断系统在其中扮演着怎样的角色呢?简单来说,中断系统如同一个智能管家,当外部设备(如键盘、鼠标)或内部事件(如定时器溢出)有服务请求时,它能迅速通知 CPU 暂停当前任务,转而去处理紧急事务,处理完毕后又巧妙地让 CPU 回到原来的工作中。这种机制大大提升了系统的响应速度与运行效率。特别是在 ARM 与 Linux 的组合中,中断系统经过深度优化,进一步挖掘出两者的潜力。在接下来的内容中,我们将深入剖析 ARM+Linux 中断系统的工作原理、技术细节,一同探寻它是如何为高效计算赋能的。
一、中断系统:计算世界的 “紧急响应者”
在日常生活中,我们难免会遭遇各种紧急状况。当火灾发生时,烟雾报警器会瞬间感应到异常,随即发出尖锐的警报声。这一警报就如同给消防系统发送了 “紧急信号”,消防员们收到信号后,会即刻停下手中的其他工作,迅速登上消防车,风驰电掣般赶赴火灾现场。到达后,他们会争分夺秒地展开灭火行动,直到成功扑灭大火,才会返回消防站,继续待命,等待下一次任务。又比如在繁忙的交通道路上,突然发生了严重的交通事故,伤者急需救治。
这时,路人会马上拨打 120 急救电话,急救中心接到电话后,会立即调度救护车,医护人员放下手头正在处理的事务,迅速前往事故地点,对伤者进行紧急救治,随后将伤者送往医院,完成任务后再回到岗位,准备应对下一次的急救需求。
在计算机的世界里,也存在着类似的 “紧急响应机制”,那就是中断系统 。计算机的 CPU 就如同消防员或医护人员,它平时有条不紊地执行着各种程序任务。而当外部设备(如键盘、鼠标、网卡等)或者内部事件(如定时器溢出)有紧急事情需要 CPU 处理时,就会向 CPU 发送中断信号。CPU 在接收到中断信号后,会暂时停下当前正在执行的任务,转而去处理这个中断请求。等处理完中断后,再回到原来被中断的任务处,继续执行后续的指令。
ARM+Linux 中断系统,正是基于 ARM 架构的硬件平台与 Linux 操作系统相结合的中断处理体系。ARM 架构凭借其低功耗、高性能以及丰富的外设接口等优势,在嵌入式领域应用广泛,从手机、平板电脑到工业控制设备、智能家居系统,到处都有它的身影。Linux 操作系统则以开源、稳定、可定制性强等特点,深受开发者的喜爱,在服务器领域占据着重要地位,同时也在嵌入式系统开发中被大量采用。ARM+Linux 中断系统的高效运行,对于保障这些设备和系统的稳定运行、实时响应至关重要,是理解和开发基于 ARM+Linux 平台应用的关键所在。
二、ARM 中断的硬件基石
2.1ARM 的运行模式与异常分类
ARM 处理器拥有七种运行模式,就像是一位多才多艺的演员,可以根据不同的场景切换不同的 “角色” ,每种模式都有其独特的用途和权限,共同协作维持着系统的稳定运行。
用户模式(User Mode):这是用户程序正常执行的模式,就像普通市民在自己的生活空间内活动,权限相对有限。在这个模式下,程序只能执行自己的数据处理任务,不能随意操作其他硬件资源,也无法直接切换到其他模式。例如,我们在手机上运行的各类 APP,它们大多运行在用户模式下,受到系统的限制,不能随意访问底层硬件,以保障系统的安全和稳定。系统模式(System Mode):属于特权模式,类似于城市管理者,虽然和用户模式共用一套寄存器,但却拥有更高的权限,可以不受用户模式的限制,自由访问系统资源。操作系统的一些特权任务会利用这个模式来访问受控资源,比如对系统关键文件的读写操作等。中断模式(IRQ Mode):用于处理一般的中断请求,当硬件产生中断信号后,就会自动进入该模式,如同城市中的普通应急响应部门,随时准备处理各种常规的紧急情况。比如键盘按键按下、鼠标移动等操作产生的中断,都会由中断模式来处理。快速中断模式(FIQ Mode):主要用于处理对时间要求紧急的中断请求,就像是城市中的特种应急部队,在高速数据传输及通道处理等场景中发挥关键作用。例如,在音视频数据的实时传输过程中,为了保证数据的流畅性和及时性,相关的中断就会由快速中断模式来处理。它比普通中断模式速度更快,这是因为它拥有更多的独立寄存器(r8 - r14 以及 SPSR 都有对应的 banked 寄存器),模式切换时 CPU 自动保存和恢复这些寄存器的值,无需中断处理程序手动操作,节省了时间 。而且它的入口地址(0x1c)与普通中断(0x18)不同,由于跳转范围限制,至少少了一条跳转指令,也提高了处理速度。管理模式(Supervisor Mode):这是 CPU 上电后的默认模式,是操作系统的保护模式,软中断(SWI)处理函数就在此模式下执行,就像城市的最高管理决策层。当用户模式下的程序请求使用硬件资源时,会通过软件中断进入该模式。比如,用户程序需要读取磁盘文件时,就会触发软中断,进入管理模式,由操作系统来完成具体的磁盘读取操作。系统复位或开机时也会进入该模式,通常在此模式下进行系统的初始化工作。数据访问终止模式(Abort Mode):当程序访问非法地址或没有权限读取的内存地址时,就会进入该模式,类似于城市中的安全检查部门在发现非法访问时介入。在 Linux 下编程时经常出现的 segment fault 错误,通常就是在这个模式下抛出返回的。例如,程序试图访问一个未分配给它的内存区域,就会触发数据访问终止异常,进入该模式。未定义指令终止模式(Undefined Mode):当 CPU 在指令的译码阶段不能识别该指令操作时,会进入此模式,用于支持硬件协处理器的软件仿真,就像是城市中对未知情况的探索研究部门。比如,在不支持特定浮点运算指令的硬件上执行该指令时,就会进入未定义模式在这七种模式中,除了用户模式外,其他六种模式都属于特权模式,拥有更高的权限,可以访问系统的所有资源,并且能够自由切换处理器模式。而特权模式中,除了系统模式外,其余五种模式(中断模式、快速中断模式、管理模式、数据访问终止模式、未定义指令终止模式)又被统称为异常模式,它们不仅可以通过特权程序切换进入,还能由特定的异常情况触发进入。
中断模式是ARM异常模式之一(IRQ模式,FIQ模式),是一种异步事件,如外部按键产生中断,内部定时器产生中断,通信数据口数据收发产生中断等。
当一个异常产生时,以FIQ为例,CPU切入FIQ模式时
①将原来执行程序的下一条指令地址保存到LR中,就是将R14保存到R14_fiq里面。②拷贝CPSR到SPSR_fiq。③改变CPSR模式位的值,改到FIQ模式。④改变PC值,将其指向相应的异常处理向量表。离开异常处理的时候:
①将LR(R14_fiq)赋给PC。②将SPSR(SPSR_fiq)拷贝到CPSR。③清除中断禁止标志(如果开始时置位了)。当一个外部IRQ中断产生时:
①处理器切换到IRQ模式②PC跳到0x18处运行,因为这是IRQ的中断入口。③通过0x18:LDR PC, IRQ_ADDR,跳转到相应的中断服务程序。这个中断服务程序就要确定中断源,每个中断源会有自己独立的中断服务程序。④得到中断源,然后执行相应中断服务程序⑤清除中断标志,返回这就是一个外部中断完整的执行流程了,下面以具体寄存器来更具体的了解ARM的中断机制。
假设ARM核有两个中断引脚,一根是irq pin,一根是fiq pin,正常情况下,ARM核只是机械地随着PC指示去执行,当CPSR中的I位和F位都为1时,IRQ和FIQ都处于禁止状态,这时候无论发什么信号,ARM都不会理睬。
当I位或F位为0时,irq pin有中断信号过来时,ARM当前工作就会被打断,切换到IRQ模式,并且跳转到异常向量表的中断入口0x18,SRCPND中相应位置1,经过检查中断优先级寄存器以及屏蔽寄存器,确定中断源,INTPND相应位置1(经过仲裁,只有一位置1),这过程由ARM自动完成。0x18存放的是总的中断处理函数,在这个函数里,可以建立一个二级中断向量表,先清除SRCPND相应位,然后根据中断源执行相应中断服务程序,清除INTPND,返回。
及时清除中断 Pending 寄存器的标志位是为了避免两个问题:①发生中断返回后,立即又被中断,不断的重复响应②丢失中断处理过程中发生的中断,返回后不响应。
在某个IRQ中断程序执行过程中,有另外一个外部IRQ中断产生,会将SRCPND相应位置1,等该中断服务执行完,经过仲裁决定下一个要响应的中断。但是假如当产生的是FIQ,则保存当前IRQ的现场,嵌套响应FIQ,FIQ服务程序执行完,再继续执行IRQ服务。那么当一个FIQ正在服务,产生另外一个FIQ,会怎样呢,答案是不会被打断,跟IRQ一样等当前中断服务完成,再仲裁剩余需要相应的中断。
所以得出这样的结论:
①关于中断嵌套:IRQ模式只能被FIQ模式打断,FIQ模式下谁也打不断。
②关于优先级:ARM核对中断优先级,有明确的可编程管理。
下面再来看看Linux对ARM是怎么处理的,记住一个前提:Linux对ARM的硬件特性可以取舍,但不可更改。
建立异常向量表:系统从arch/arm/kernel/head.S的ENTRY(stext)开始执行,__lookup_processor_type检查处理器ID,__lookup_machine_type检查机器ID,__create_page_tables创建页表,启动MMU,然后由arch/arm/kernel/head_common.S 跳到start_kernel()->trap_init()
CONFIG_VECTORS_BASE在autoconf.h定义,在ARM V4及V4T以后的大部分处理器中,中断向量表的位置可以有两个位置:一个是0,另一个是0xffff0000。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:
__vectors_end 至 __vectors_start之间为异常向量表,位于arch/arm/kernel/entry-armv.S
stubs_offset值如下:
stubs_offset是如何确定的呢?
当汇编器看到B指令后会把要跳转的标签转化为相对于当前PC的偏移量(±32M)写入指令码。从上面的代码可以看到中断向量表和stubs都发生了代码搬移,所以如果中断向量表中仍然写成b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。我们把搬移前的中断向量表中的irq入口地址记irq_PC,它在中断向量表的偏移量就是irq_PC-vectors_start, vector_irq在stubs中的偏移量是vector_irq-stubs_start,这两个偏移量在搬移前后是不变的。
搬移后 vectors_start在0xffff0000处,而stubs_start在0xffff0200处,所以搬移后的vector_irq相对于中断向量中的中断入口地址的偏移量就是,200+vector_irq在stubs中的偏移量再减去中断入口在向量表中的偏移量,即200+ vector_irq-stubs_start-irq_PC+vectors_start = (vector_irq-irq_PC) + vectors_start+200-stubs_start,对于括号内的值实际上就是中断向量表中写的vector_irq,减去irq_PC是由汇编器完成的,而后面的 vectors_start+200-stubs_start就应该是stubs_offset,实际上在entry-armv.S中也是这样定义的。
2.2ARM 中断引脚与中断使能控制
在 ARM 架构中,中断的硬件实现依赖于特定的引脚和控制机制。我们可以假设 ARM 核心有两根中断引脚(实际中是集成在芯片内部,从外部不可见),一根叫 irq pin,用于普通中断请求;另一根叫 fiq pin,用于快速中断请求 。这两根引脚就像是连接外部设备与 CPU 的 “紧急联络线”,一旦外部设备有紧急事务需要 CPU 处理,就会通过这两根引脚向 CPU 发送中断信号。
而 CPU 是否响应这些中断信号,由当前程序状态寄存器(CPSR)中的 I 位和 F 位来控制。当 CPSR 中的 I 位为 1 时,IRQ 中断被禁止,此时即使 irq pin 上有中断信号,CPU 也不会理会,就如同消防员将火警铃声设置为静音,对火灾警报充耳不闻;当 F 位为 1 时,FIQ 中断被禁止,fiq pin 上的中断信号也无法引起 CPU 的注意 。只有当 I 位和 F 位都为 0 时,irq pin 和 fiq pin 上的中断信号才能成功打断 CPU 当前正在执行的任务,使 CPU 切换到相应的中断模式进行处理。例如,在系统进行一些关键的、不能被中断干扰的操作时,就可以通过设置 CPSR 中的 I 位和 F 位来禁止中断,确保操作的完整性和正确性。
当有外部中断产生时,跳转到异常向量表的“b vector_irq + stubs_offset //普通中断异常”,进入异常处理函数,跳转的入口位置 arch\arm\kernel\entry-armv.S 代码简略如下:
vector_stub是个函数调用宏,根据中断前的工作模式决定进入__irq_usr,__irq_svc。这里入__irq_svc,同时看到这里FIQ产生时,系统未做任何处理,直接返回,即Linux没有提供对FIQ的支持,继续跟进代码
svc_entry是一个宏,主要实现了将SVC模式下的寄存器、中断返回地址保存到堆栈中。然后进入最核心的中断响应函数irq_handler,irq_handler实现过程arch\arm\kernel\entry-armv.S
get_irqnr_and_base中断号判断过程,include/asm/arch-s3c2410/entry-macro.s
asm_do_IRQ实现过程,arch/arm/kernel/irq.c
上述asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)使用了asmlinkage标识。那么这个标识的含义如何理解呢?
该符号定义在kernel/include/linux/linkage.h中,如下所示:
对于ARM处理器的,没有定义asmlinkage,所以没有意义(不要以为参数是从堆栈传递的,对于ARM平台来说还是符合ATPCS过程调用标准,通过寄存器传递的)。
但对于X86处理器的中是这样定义的:
表示函数的参数传递是通过堆栈完成的。
中断处理过程代码就跟到这了,那么最后一个问题desc->handle_irq(irq, desc);是怎么跟我们注册的中断函数相关联的呢?再从中断模型注册入手:
中断相关的数据结构:在include/asm/arch/irq.h中定义。
irq_desc[]是一个指向irq_desc_t结构的数组, irq_desc_t结构是各个设备中断服务例程的描述符。Irq_desc_t结构体中的成员action指向该中断号对应的irqaction结构体链表。Irqaction结构体定义在include/linux/interrupt.h中,如下:
在注册中断号为irq的中断服务程序时,系统会根据注册参数封装相应的irqaction结构体。并把中断号为irq的irqaction结构体写入irq_desc [irq]->action。这样就把设备的中断请求号与该设备的中断服务例程irqaction联系在一起了。当CPU接收到中断请求后,就可以根据中断号通过irq_desc []找到该设备的中断服务程序。
2.3中断向量表与中断执行流程
中断向量表在 ARM 中断系统中扮演着至关重要的角色,它是中断处理的 “导航地图”。在 32 位 ARM 系统中,中断向量表一般位于内存的特定地址,有两个可选位置:一个是低地址 0x00000000 - 0x0000001c(Low vector) ;另一个是高地址 0Xffff0000 - 0xffff001c(High vector) ,具体使用哪个位置由 CPU 决定。中断向量表中存放着各个中断和异常对应的处理程序的入口地址,当有中断或异常发生时,CPU 会自动将程序计数器(PC)指向中断向量表中的相应地址,从而跳转到对应的中断服务例程进行处理。
以外部 IRQ 中断为例,当 irq pin 上有中断信号到来时,整个中断执行流程如下:
模式切换:ARM 核心检测到 irq pin 上的中断信号后,首先会检查 CPSR 中的 I 位,如果 I 位为 0(即 IRQ 中断未被禁止),CPU 会自动切换到 IRQ 模式,就像消防员接到火灾警报后,迅速换上消防服,进入战斗状态。跳转至中断向量表:CPU 切换到 IRQ 模式后,PC 会跳到中断向量表中 IRQ 对应的入口地址 0x18 处运行 。这个过程是硬件自动完成的,就像是消防系统根据警报类型,自动将消防员引导到对应的火灾现场位置。跳转到中断服务程序:在 0x18 地址处,通常存放着一条跳转指令(如 LDR PC, IRQ_ADDR),通过这条指令,CPU 会跳转到具体的中断服务程序。这个中断服务程序的任务是确定中断源,因为可能有多个外部设备共享 irq pin,每个设备产生的中断都需要有独立的处理方式。比如,键盘和鼠标都可能通过 irq pin 发送中断信号,中断服务程序需要判断到底是键盘按键按下产生的中断,还是鼠标移动产生的中断,然后再执行相应的处理操作。执行中断服务程序:确定中断源后,CPU 会执行相应的中断服务程序,完成对中断事件的处理。例如,如果是键盘中断,中断服务程序可能会读取键盘输入的字符,并将其传递给相应的应用程序;如果是定时器中断,可能会更新系统时间、触发定时任务等。清除中断标志与返回:中断服务程序执行完毕后,需要清除中断标志,通知系统中断已经处理完毕。然后,CPU 会恢复到中断前的状态,将程序执行流程返回到被中断的地方继续执行,就像消防员扑灭火灾后,脱下消防服,回到原来的工作岗位,继续之前未完成的任务。在这个过程中,需要将之前保存的寄存器值恢复,将 CPSR 从 SPSR 中恢复,将 LR 中的值赋给 PC 等,确保程序能够无缝衔接继续执行。三、ARM+Linux 中断系统基础解析
3.1ARM 架构中断机制
在 ARM 架构的璀璨星空中,中断机制宛如一条贯穿其中的璀璨银河,连接着处理器与外部设备,为系统的高效运行提供了关键支撑。ARM 架构下的中断,犹如灵动的使者,根据其特性和应用场景,主要分为两类:IRQ(Interrupt Request,外部中断请求)和 FIQ(Fast Interrupt Request,快速中断请求) ,它们各具特色,在不同的场景中发挥着独特的作用。
IRQ,作为通用中断请求的代表,是 ARM 架构中较为常见的中断类型。当外部设备如键盘、鼠标、网络接口等有事件需要处理器关注时,就会发出 IRQ 中断请求。它就像是一位勤劳的信使,在系统中频繁穿梭,将外部设备的各种请求传递给处理器。在我们日常使用的智能手机中,当用户点击屏幕时,触摸屏控制器会产生 IRQ 中断,通知处理器进行相应的处理,如打开应用程序、滑动页面等。IRQ 中断的特点是通用性强,能够灵活地响应各种外部设备的请求,就像一个万能的接口,连接着各种各样的外部设备。而且,它还可以处理多个中断请求的并发情况,当多个外部设备同时发出中断请求时,IRQ 能够有条不紊地将这些请求传递给处理器,由处理器按照一定的优先级顺序进行处理,从而保证系统的实时性和可靠性 。
而 FIQ,则是中断世界中的 “闪电侠”,以其快速响应的特点而著称。它主要用于处理那些对时间要求极为苛刻、需要快速响应的中断请求,如高速数据传输、通道处理等场景。在高速网络通信中,数据的接收和发送需要及时处理,否则可能会导致数据丢失或延迟。此时,FIQ 就发挥了重要作用,它能够在极短的时间内响应中断请求,迅速处理数据,确保网络通信的顺畅。FIQ 之所以能够如此快速地响应,得益于它拥有更高的优先级。
当 FIQ 和 IRQ 同时产生时,处理器会毫不犹豫地优先响应 FIQ 中断,就像在紧急情况下,人们会优先处理最紧急的事务一样。此外,FIQ 模式还提供了更多的 banked 寄存器,这些寄存器就像是为 FIQ 量身定制的 “快速通道”,在模式切换时,CPU 能够自动保存和恢复相关寄存器的值,大大减少了上下文切换的开销,从而提高了中断处理的速度 。
在实际应用中,我们需要根据不同的场景需求,合理地选择使用 IRQ 和 FIQ。对于那些对实时性要求不高的普通外部设备,如打印机、摄像头等,使用 IRQ 中断即可满足需求;而对于那些对时间要求极高的高速数据传输设备,如高速 USB 接口、以太网控制器等,则需要使用 FIQ 中断来确保系统的性能。
3.2Linux 对 ARM 中断的管理
Linux 内核,就像一位智慧的指挥官,对 ARM 中断进行着有条不紊的管理,确保系统的稳定运行。在这个过程中,中断向量表的建立和中断注册是两个关键的环节,它们就像是 Linux 内核管理 ARM 中断的两把 “利刃”,发挥着至关重要的作用。
中断向量表,是中断处理的 “导航地图”,它记录了每个中断对应的处理程序的入口地址。在 ARM 架构中,中断向量表的位置可以通过 CP15 协处理器的控制寄存器 C1 的 bit13 来设置,当该位为 0 时,中断向量表起始于 0x00000000;当该位为 1 时,起始于 0xffff0000 。在 Linux 内核启动的过程中,会精心地建立中断向量表。它首先会确定中断向量表的基地址,然后将异常向量表从内存的特定位置复制到中断向量表的基地址处。异常向量表中包含了各种中断和异常的处理入口,如复位异常、未定义指令异常、中断异常等。
在 ARMv8 架构中,中断向量表的建立过程涉及到多个步骤,包括初始化中断控制器、设置中断向量表的基地址、将异常向量表复制到中断向量表的基地址等。通过这些步骤,Linux 内核为中断处理搭建了一个坚实的基础,使得处理器在接收到中断请求时,能够迅速找到对应的处理程序入口,就像在地图上找到目的地的路线一样。
中断注册,则是将设备的中断处理函数与中断号进行绑定的过程,就像是为每个中断请求找到对应的 “处理专家”。在 Linux 内核中,设备驱动程序开发者需要使用 request_irq 函数来注册中断。这个函数的原型如下:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
其中,irq 是要申请的硬件中断号,它就像是设备的 “专属号码”,用于唯一标识一个中断请求;handler 是向系统注册的中断处理函数,当该中断发生时,系统会调用这个函数来处理中断,它是处理中断的核心代码;irqflags 是中断处理的属性,如设置了 IRQF_DISABLED,则表示中断处理程序是快速处理程序,在被调用时会屏蔽所有中断,以确保快速处理中断;如设置了 IRQF_SHARED,则表示多个设备可以共享这个中断,这在一些资源有限的系统中非常有用;devname 是设置的中断名称,方便在系统中识别和管理中断,就像给每个设备取一个名字一样;dev_id 在中断共享时会用到,一般设置为这个设备的设备结构体或者 NULL,它用于在共享中断时区分不同的设备 。
当设备驱动程序调用 request_irq 函数成功注册中断后,Linux 内核会将中断处理函数与中断号关联起来,并记录在相应的中断管理数据结构中。当中断发生时,内核会根据中断号找到对应的中断处理函数,并调用它来处理中断。在一个包含多个 GPIO 设备的嵌入式系统中,每个 GPIO 设备都可能产生中断。设备驱动程序会为每个 GPIO 设备的中断号注册相应的中断处理函数,当某个 GPIO 设备产生中断时,内核能够准确地找到对应的中断处理函数,对中断进行处理,从而实现设备与系统之间的高效通信 。通过中断注册,Linux 内核实现了对中断的精细管理,使得系统能够灵活地响应各种设备的中断请求。
四、Linux在ARM上的中断构建
4.1异常向量表的建立与设置
在 Linux 系统基于 ARM 架构启动的过程中,异常向量表的建立是一个关键步骤,就像是搭建一座城市的应急指挥中心,为后续的中断处理奠定基础 。
ARM 架构支持将异常向量表放置在两个不同的内存位置,具体由 CP15 协处理器 C1 寄存器的 bit [13](即 V 位)来控制 。当 V = 0 时,异常向量表位于低地址 0x00000000 - 0x0000001c;当 V = 1 时,异常向量表位于高地址 0Xffff0000 - 0xffff001c 。在大多数 ARM V4 及 V4T 以后的处理器中,通常会选择使用高地址的异常向量表。
在 Linux 内核启动时,start_kernel () 函数是整个内核初始化的起点,其中 trap_init ()(在一些内核版本中可能是 early_trap_init ())函数负责异常向量表的初始化工作 。以 early_trap_init () 函数为例,其内部实现过程如下:
首先,获取 CONFIG_VECTORS_BASE 的值,该值通常在各个平台的配置文件中设定,如在 arch/arm/configs/s3c2410_defconfig 中,CONFIG_VECTORS_BASE = 0xffff0000 ,它指定了异常向量表的目标地址。然后,通过 memcpy 函数将定义在 arch/arm/kernel/entry-armv.S 中的异常向量表(__vectors_end 至__vectors_start 之间的内容)拷贝到目标地址 vectors 处 。
同时,将异常处理程序的 stub(__stubs_end 至__stubs_start 之间的内容)拷贝到 vectors + 0x200 的位置,将 kuser helpers 拷贝到 vectors + 0x1000 - kuser_sz 的位置 。接着,把信号返回处理函数拷贝到特定位置。之后,调用 flush_icache_range 函数刷新指令缓存,确保 CPU 能够获取到最新的代码。最后,使用 modify_domain 函数修改异常向量表占据页面的访问权限,使得只有核心态可以访问该页,保障系统的安全性。
在 arch/arm/kernel/entry-armv.S 文件中,定义了异常向量表的具体内容:
这里,每个异常类型都对应着一个特定的跳转指令,例如普通中断(IRQ)对应的是 b vector_irq + stubs_offset 。其中,stubs_offset 的值通过.equ stubs_offset, __vectors_start + 0x200 - __stubs_start 定义,它的作用是确保在异常向量表和 stubs 发生代码搬移后,跳转指令能够正确地跳转到搬移后的异常处理程序位置 。当 ARM 处理器发生异常时,会根据异常类型跳转到异常向量表中对应的位置,进而跳转到相应的异常处理程序处执行。
4.2中断处理函数的流程解析
当硬件中断触发后,整个中断处理流程就像是一场有条不紊的接力赛,涉及多个关键函数和操作,从硬件层面逐步过渡到软件层面进行处理。
假设一个外部设备(如按键)产生了中断信号,该信号首先会被 ARM 芯片的中断控制器接收 。以常见的 ARM 芯片中断控制器为例,它会将中断信号进行初步处理,检查中断屏蔽寄存器和中断优先级寄存器等,确定是否要将该中断信号传递给 CPU 。如果中断未被屏蔽且具有足够的优先级,中断控制器会通过 irq pin 向 CPU 发送中断请求信号。
CPU 在执行当前指令的过程中,会不断检查是否有中断信号到来 。当检测到 irq pin 上的中断信号后,且当前程序状态寄存器(CPSR)中的 I 位为 0(表示 IRQ 中断未被禁止)时,CPU 会立即做出响应 。首先,CPU 会自动切换到 IRQ 模式,这是硬件自动完成的操作,就像是消防员迅速切换到战斗模式。然后,程序计数器(PC)会跳转到异常向量表中 IRQ 对应的入口地址 0x18 处运行 。在 0x18 地址处,存放着一条跳转指令(例如 LDR PC, IRQ_ADDR),通过这条指令,CPU 会跳转到具体的中断处理程序。
在 Linux 内核中,中断处理程序的入口是 vector_irq,它定义在 arch/arm/kernel/entry-armv.S 文件中 。vector_irq 首先会进行一些现场保护操作,保存当前寄存器的值,以便在中断处理完成后能够恢复到中断前的状态 。然后,通过一系列的判断和跳转,最终会调用到 irq_handler 这个宏 。
irq_handler 宏实际上是调用 handle_arch_irq 函数,而 handle_arch_irq 函数是在中断控制器初始化时注册的处理函数 。在不同的 ARM 平台中,handle_arch_irq 函数可能会有所不同,例如在使用 GIC(Generic Interrupt Controller)中断控制器的平台中,handle_arch_irq 函数通常会指向 gic_handle_irq 函数 。
gic_handle_irq 函数是 GIC 中断控制器的核心处理函数,它的主要工作流程如下:
读取中断确认寄存器:首先,读取 GIC 的中断确认寄存器(GIC_CPU_INTACK),这个操作实际上是向 GIC 确认 CPU 已经接收到中断信号 。同时,该寄存器会返回一个唯一的中断号,用于标识产生中断的设备。处理中断:根据返回的中断号,查找对应的中断处理函数,并调用该函数进行中断处理 。在这个过程中,可能会涉及到对中断源的进一步判断和处理,例如读取设备的状态寄存器,获取中断的具体原因等。通知中断结束:当中断处理完成后,需要向 GIC 发送中断结束信号 。这通过写入 GIC 的中断结束寄存器(GIC_CPU_EOI)来实现,告知 GIC 本次中断已经处理完毕,GIC 可以再次接受这个中断号的中断请求。在驱动层面,当设备驱动注册时,会使用 request_irq 函数来注册中断处理函数 。例如:
其中,irq 是中断号,handler 是具体的中断处理函数,flags 用于指定中断的属性(如中断触发方式、是否共享中断等),name 是中断的名称,dev 是传递给中断处理函数的上下文数据 。当硬件中断发生并经过上述的一系列处理后,最终会调用到驱动中注册的中断处理函数 handler 。在中断处理函数中,会根据设备的具体需求进行相应的处理,例如读取设备的数据、更新设备的状态等 。处理完成后,中断处理函数会返回一个 irqreturn_t 类型的值,用于告知系统中断处理的结果。
当中断处理函数执行完毕后,会进行现场恢复操作,将之前保存的寄存器值恢复,将 CPSR 从 SPSR 中恢复,将 LR 中的值赋给 PC 等,使 CPU 回到中断前的状态,继续执行被中断的任务 。
五、ARM的GIC中断控制器剖析
5.1GIC 的基本概念与版本介绍
在 ARM 架构的中断体系中,GIC(Generic Interrupt Controller)即通用中断控制器,扮演着至关重要的角色,堪称中断管理的 “总指挥” 。它就像是城市中的交通指挥中心,负责收集、管理和分配来自各个外部设备以及内部事件的中断信号,确保 CPU 能够有条不紊地处理这些中断请求。
目前,GIC 共有四个版本,分别为 V1 至 V4 。其中,V1 是最早期的版本,如今已逐渐被淘汰,不再广泛应用;V2 版本应用较为广泛,它最多支持 8 个 ARM core,常用于 ARMv7 - A 架构的芯片,如 Cortex - A7、Cortex - A9、Cortex - A15 等 ,像一些早期的智能手机、平板电脑等设备中,就常常能看到基于 GIC V2 的中断控制;V3 和 V4 版本则主要面向 ARM64 服务器系统结构,支持更多的 ARM core,能够满足大规模数据处理和多任务并发的需求 ,在数据中心、云计算等领域发挥着重要作用。
例如,GIC - 500 最多支持 128 个 cpu core,它要求 ARM core 必须是 ARMV8 指令集的(如 Cortex - A57),符合 GIC architecture specification version 3 。在实际应用中,不同的芯片厂商会根据自身产品的需求,选择合适版本的 GIC。例如,一些中低端的嵌入式设备可能会采用 GIC V2 版本,以降低成本并满足基本的中断处理需求;而高端的服务器芯片则会选用 GIC V3 或 V4 版本,以提供强大的中断管理能力和高性能的计算支持。
5.2GIC 的中断输入信号类型
GIC 接收的中断输入信号主要分为三种类型,每种类型都有其独特的特点和应用场景,就像是不同类型的紧急事件,需要不同的应对方式。
PPI(私有外设中断,Private Peripheral Interrupt):这是每个 CPU 私有的中断,就如同每个人专属的紧急联络方式。PPI 最多支持 16 个中断,硬件中断号从 ID16~ID31 。它通常会被发送到指定的 CPU 上,应用场景之一是 CPU 本地时钟,比如每个核的 tick 中断,用于进程调度使用 。以一个多核处理器为例,每个核心都有自己独立的 PPI 中断,用于处理与该核心紧密相关的特定外设事件,确保各个核心能够高效地处理自身的事务,互不干扰。SPI(公用外设中断,Shared Peripheral Interrupt):SPI 是所有 Core 共享的中断,来自于外设,类似于公共的紧急求助通道。它最多可以支持 988 个外设中断,硬件中断号从 ID32~ID1019 。像我们常见的按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core 。在一个智能家居系统中,多个传感器(如温度传感器、湿度传感器、门窗传感器等)产生的中断信号,都可以通过 SPI 中断发送给 CPU,由 CPU 统一进行处理,实现对家居环境的智能监控和控制。5.3GIC 的工作机制与寄存器功能
GIC 的工作机制主要依赖于两个关键部分:仲裁单元(Distributor)和 CPU 接口模块(CPU Interface) ,它们相互协作,共同完成中断的管理和处理,就像是一个高效的团队,各自发挥着重要的职责。
仲裁单元(Distributor)
仲裁单元就像是交通指挥中心的调度员,负责收集所有的中断源,控制每个中断的优先级,并将中断事件分发到相应的 CPU 接口端 。它的主要工作包括:
全局中断使能控制:通过 GIC_DIST_CTRL 寄存器,可以对全局中断进行控制 。一旦禁用了全局中断,那么任何中断源产生的中断事件都不会被传递到 CPU 接口,就像是交通指挥中心关闭了所有的紧急通道,不再接收任何紧急信号。单个中断使能或关闭控制:利用 GIC_DIST_ENABLE_CLEAR 寄存器,可以针对各个中断源进行控制 。禁用某一个中断源会导致该中断事件不会被分发到 CPU 接口,但不影响其他中断源产生的中断事件的分发,就如同关闭了某个特定区域的紧急通道,但其他区域的紧急通道仍然畅通。中断优先级设置:仲裁单元会为每个中断分配优先级,它总是将优先级最高的中断事件发送到 CPU 接口端 。例如,在一个同时有按键中断和串口接收中断的系统中,如果按键中断被设置为更高的优先级,那么当两个中断同时发生时,仲裁单元会优先将按键中断发送给 CPU 接口,确保关键事件能够得到及时处理。中断目标处理器列表设置:可以设置每个中断应该发送到哪些 CPU 核心上,实现中断的合理分配 。在一个多核处理器系统中,对于一些计算密集型的中断任务,可以将其分配到性能较强的核心上进行处理,以提高系统的整体性能。中断触发模式设置:可以设定每个外部中断的触发模式,是电平触发还是边沿触发 。例如,对于一些对时间精度要求较高的中断事件,可以设置为边沿触发模式,确保能够及时捕捉到中断信号。中断分组设置:设置每个中断属于组 0 还是组 1 。在安全系统中,Group0 通常用于安全相关的中断,连接 FIQ;Group1 作为非安全系统使用,连接 IRQ 。通过这种分组方式,可以增强系统的安全性和稳定性。CPU 接口模块(CPU Interface)CPU 接口模块则像是连接交通指挥中心和消防员(CPU)的通信员,是分发器和 CPU Core 之间的桥梁 。它的主要功能如下:
中断请求信号使能或关闭:可以控制是否将中断请求信号发送到 CPU Core 。如果禁用了中断,那么即使仲裁单元分发了中断事件到 CPU 接口,也不会向 CPU 发送中断信号,就像是通信员关闭了与消防员的通信通道,消防员无法收到紧急任务通知。中断应答:当 CPU 接收到中断信号并开始处理时,会向 CPU 接口模块应答中断 。中断一旦被应答,仲裁单元就会把该中断的状态从 pending 状态修改成 active 。如果没有后续 pending 的中断,那么 CPU 接口就会取消中断信号的发送;如果在这个过程中又产生了新的中断,仲裁单元会把新中断的状态从 pending 状态修改成 pending and active ,CPU 接口仍然会保持中断信号的发送,通知 CPU 有新的中断到来,就像是消防员向通信员回复已收到任务,并开始执行,通信员根据任务情况决定是否继续发送新的任务通知。中断处理完成通知:当中断处理程序完成对一个中断的处理后,会向 CPU 接口模块的寄存器写入信息,通知 GIC CPU 已经处理完该中断 。这样做一方面是通知仲裁单元将中断状态修改为 deactive,另一方面,可以允许其他 pending 的中断向 CPU 接口提交,就像是消防员完成任务后向通信员报告任务完成,通信员收到报告后,允许新的紧急任务进入处理流程。优先级掩码设置:通过设置优先级掩码,可以屏蔽掉一些优先级比较低的中断,这些中断不会被通知到 CPU 。例如,在系统进行一些关键操作时,可以通过设置优先级掩码,暂时屏蔽掉一些不重要的中断,确保关键操作的顺利进行,就像是在执行重要任务时,暂时忽略一些次要的紧急事件。抢占策略定义:定义中断的抢占策略,当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core 。比如,当一个高优先级的中断到来时,即使当前 CPU 正在处理一个低优先级的中断,也会立即暂停当前处理,转而处理高优先级的中断,确保重要事件能够得到优先处理,就像是在多个紧急任务同时出现时,优先安排消防员处理最紧急的任务。在 GIC 的工作过程中,这些寄存器就像是交通指挥中心的各种控制按钮和显示屏,通过对它们的配置和操作,实现对中断的精确管理和高效处理 。例如,在系统初始化阶段,需要对 GIC 的寄存器进行正确的配置,包括中断使能、优先级设置、触发模式设置等,以确保系统能够正常响应各种中断请求 。在中断处理过程中,CPU 通过读取和写入相关寄存器,与 GIC 进行交互,完成中断的接收、处理和结束等操作 。
六、ARM+Linux中断系统的应用与优化
6.1中断处理程序的优化
中断处理程序的优化,就像是对精密仪器的精细调校,每一个细节都关乎着系统的性能表现。从代码结构和算法选择等角度入手,能够显著提高中断处理程序的响应速度,让系统在面对各种中断请求时更加游刃有余 。
在代码结构方面,要遵循简洁高效的原则。将中断处理程序划分为关键部分和非关键部分,是一种行之有效的策略。关键部分负责处理紧急且必须立即完成的任务,如读取硬件寄存器状态、清除中断标志等,这些任务对时间要求极高,需要在最短的时间内完成 。而非关键部分则可以将一些相对耗时但并非紧急的任务,如数据处理、通知其他模块等,放到中断处理程序之外执行,避免在中断处理过程中占用过多时间,影响系统对其他中断的响应 。
在一个网络通信设备的中断处理程序中,当接收到数据包时,关键部分迅速读取数据包的关键信息,如源地址、目的地址等,并清除中断标志,以确保能够及时响应下一个中断请求;而非关键部分则将数据包的解析和处理工作放到一个专门的线程中执行,这样既保证了中断处理的及时性,又不影响数据包的后续处理。
算法选择也是优化中断处理程序的重要一环。选择高效的算法能够大大减少中断处理的时间开销,提高系统的性能。在数据处理任务中,不同的算法在时间复杂度和空间复杂度上存在差异,选择合适的算法可以显著提高处理效率 。假设需要对中断处理过程中接收到的大量数据进行排序,冒泡排序算法的时间复杂度为 O (n^2),而快速排序算法的平均时间复杂度为 O (n log n) 。
在数据量较大的情况下,使用快速排序算法能够比冒泡排序算法节省大量的时间,从而提高中断处理程序的执行效率。因此,在编写中断处理程序时,要根据具体的任务需求,仔细分析和选择最优的算法,以达到提高性能的目的 。
6.2硬件资源的合理配置
硬件资源的合理配置,是提升中断系统整体性能的关键所在,就像是为一台高性能电脑精心挑选和搭配各个硬件组件,使其发挥出最佳性能。在 ARM+Linux 中断系统中,GIC(通用中断控制器)作为核心硬件组件,对它的合理配置至关重要 。
GIC 的配置涉及多个方面,其中中断优先级的设置是关键环节之一。中断优先级决定了系统在处理多个中断请求时的先后顺序。在实际应用中,要根据不同中断源的重要性和实时性要求,合理地分配中断优先级 。对于那些对实时性要求极高的中断源,如高速数据传输设备的中断请求,应将其优先级设置为较高值,确保系统能够在第一时间响应并处理这些中断,避免数据丢失或延迟 。
而对于一些对实时性要求相对较低的中断源,如打印机等设备的中断请求,可以将其优先级设置为较低值。在一个工业自动化控制系统中,传感器的数据采集中断和电机控制中断对实时性要求很高,将它们的优先级设置为较高,而系统日志记录等辅助功能的中断优先级则设置为较低,这样可以保证系统在关键任务上的实时性,同时也不影响其他非关键任务的正常执行 。
中断路由的配置也不容忽视。中断路由决定了中断请求如何被分发到相应的 CPU 核心进行处理。在多核处理器系统中,合理的中断路由配置可以充分利用各个 CPU 核心的处理能力,提高系统的并行处理能力 。可以根据不同中断源的负载情况和 CPU 核心的利用率,将中断请求均匀地分配到各个 CPU 核心上,避免某个 CPU 核心负载过高,而其他核心闲置的情况 。
在一个多线程的网络服务器应用中,将网络接收中断和网络发送中断分别路由到不同的 CPU 核心上处理,能够充分发挥多核处理器的优势,提高网络数据的处理速度 。通过合理配置 GIC 的中断优先级和中断路由等硬件资源,可以有效地提升中断系统的整体性能,使系统更加稳定、高效地运行 。
6.3代码实现
中断注册与注销代码示例:
在 ARM+Linux 的世界里,中断注册与注销是与中断系统交互的基础操作,就像在一场盛大的音乐会上,为每一位演奏者安排好出场顺序和离场流程。让我们通过一段具体的代码示例:
在这段代码中,request_irq函数是中断注册的核心。它的第一个参数irq_num是要申请的硬件中断号,就像是为设备分配的一个独特的 “身份证号码”,在实际应用中,这个号码需要根据硬件的实际连接和配置来确定。第二个参数my_interrupt_handler是指向中断处理函数的指针,当中断发生时,系统就会调用这个函数来处理中断,它就像是一位专业的 “问题解决者”,负责处理中断带来的各种任务。
第三个参数IRQF_TRIGGER_RISING是中断处理的属性,这里表示该中断是上升沿触发,就像运动员听到发令枪响(上升沿)后开始起跑一样,系统在检测到中断信号的上升沿时,会触发中断处理程序。第四个参数"my_device"是与中断相关联的设备名称,方便在系统中识别和管理中断,就像给每个设备贴上一个标签。最后一个参数NULL是设备的私有数据字段,在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL 。
free_irq函数则用于中断注销,它的第一个参数是要释放的中断号,第二个参数与注册时的dev_id相对应,用于确保正确地释放中断资源。在中断注销时,就像是让演奏者有序地离开舞台,系统会清理与该中断相关的资源,确保系统的稳定运行 。
中断处理函数编写:
中断处理函数是中断系统的核心,它就像是一位经验丰富的指挥官,在中断发生时,迅速做出决策,指挥系统应对各种情况。让我们来看一个完整的中断处理函数代码示例,并深入分析其中的处理逻辑。
在这个中断处理函数中,首先通过if - else if语句来检查中断源。这就像是指挥官在战场上,首先要明确敌人的来源,才能做出正确的战略部署。如果中断源是按键(IRQ_BUTTON),则读取按键状态,并根据状态执行相应的操作,就像根据敌人的行动做出不同的应对策略。如果是定时器中断(IRQ_TIMER),则更新定时器相关的变量,并执行定时器到期的任务。
如果遇到未知的中断源,函数会返回IRQ_NONE,表示该中断未被处理,就像指挥官遇到无法识别的情况时,选择暂时不采取行动,等待进一步的信息。而当所有的中断处理完成后,函数返回IRQ_HANDLED,表示中断已被成功处理,就像指挥官成功完成了一次战斗任务,向总部汇报胜利的消息 。通过这样的处理逻辑,中断处理函数能够有条不紊地应对各种中断情况,确保系统的稳定和高效运行。