1. 什么是按键
1.1 按键原理与连接
电路分析:
① 按键没有按下时,按键内部断开,GPIO引脚处为高电平
② 按键按下时,按键内部闭合,GPIO引脚处为低电平。此时SoC可以检测对应GPIO的电平高低来判断按键是否被按下。
因此按键属于输入设备,用于外界向SoC发送信号
对应关系:
SW5:GPH0_2
SW6:GPH0_3
SW7:GPH2_0
SW8:GPH2_1
SW9:GPH2_2
SW10:GPH2_3
1.2 按键的2种响应方式
1.2.1 轮询方式
SoC每隔一段时间去读取按键对应GPIO的电平高低,以此获得按键信息
1.2.2 中断方式
SoC事先设定好GPIO触发的中断处理程序ISR,当按键按下时自动触发GPIO对应的外部中断,进而导致ISR被执行,从而自动处理按键信息。
1.3 轮询方式处理按键解析
使用轮询方式处理按键信息需要完成以下步骤,
① 将按键对应的GPIO管脚设置为Input模式
② 在循环中反复检查按键对应GPIO管脚的电平值,如果为低电平,说明按键被按下
1.4 按键消抖
1.4.1 按键抖动
按键抖动是指在电平由高到低(按键按下)或者由低到高(按键弹起)过程中,电平的变化不是立刻发生,而是经历一段不稳定期,在此期间电平高低反复变化。
1.4.2 硬件消抖
尽量减少抖动时间,通过添加电容元件或使用RS触发器来实现。一般在按键数较少时才使用硬件消抖。
1.4.3 软件消抖
如果按键较多,常用软件方法消抖,即检测出键闭合后执行一个延时程序,5ms~10ms的延时,让前沿抖动消失后再一次检测按键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。当检测到按键释放后,也要给5ms~10ms的延时,待后沿抖动消失后才能转入该键的处理程序。
2. S5PV210异常体系详解
2.1 什么是异常
2.1.1 概述
异常:因为内部或者外部的一些事件(硬件或软件原因导致),导致处理器停下正在处理的工作,转而去处理这些发生的事件。
在ARM体系结构中,中断是异常的一种,下文中将不做严格区分。
2.1.2 异常种类和优先级
① 复位异常(Reset)
复位异常通常用在系统上电和系统复位(e.g. 看门狗到期时发出reset信号)两种情况下。
注意:虽然系统上电和系统复位都会运行到reset标号,但是还是有所不同的。以S5PV210为例,系统上电会先运行iROM中的BL0;而系统复位,只是跳转到中断向量表的reset标签处运行
② 数据访问中止异常(Data Abort)
数据访问中止异常是由存储器发出的数据中止信号,他由Load/Store指令产生;当数据访问指令的目标地址不存在或该地址不允许当前指令访问时触发。
③ 快速中断(FIQ)
当处理器的FIQ请求引脚有效,且CPSR的F位为0时,处理器产生FIQ异常
④ 外部中断(IRQ)
当处理器的IQR请求引脚有效,且CPSR的I位为0时,处理器产生IRQ异常
⑤ 预期指令异常(Prefetch Abort)
预取指令异常由系统存储器报告,当处理器试图取一条被标记为预取无效的指令时,会发生该异常
⑥ 未定义指令异常(Undefined instruction)
当ARM处理器执行协处理器指令时,他必须等待一个外部协处理器应答后,才能真正执行这条指令。若协处理器没有响应,则发生未定义指令异常。
未定义指令异常可用于没有物理协处理器的系统上,对协处理器进行软件仿真或通过软件仿真实现指令集扩展(e.g. 在没有浮点运算协处理器的系统中模拟浮点运算)。
⑦ 软中断(SWI)
软中断异常发生时,处理器进入SVC模式,执行一些SVC模式下的操作系统功能。arm-linux平台上的系统调用,就是通过软中断实现的。
异常优先级起作用的2种场景:
① 两个中断同时产生
② 一个中断已经产生,又有一个中断产生
注意:Imprecise Abort从ARM v6架构才引入
2.2 异常向量表
异常向量表本质上是与CPU约定的一段特殊地址,当某种异常发生时,PC将从该异常对应的固定地址取指运行。具体流程如下图所示
说明1:异常返回地址和CPSR保存在异常模式的LR和SPSR中,用于实现异常返回
说明2:注意异常发生时会关闭哪些中断。Reset和FIQ会同时关闭FIQ和IRQ,其余中断只会关闭IRQ。也就是说,在进入IRQ的中断处理函数时是关中断状态。
说明3:PC会被赋值为异常向量表中的地址,并从该地址开始取指执行
注意事项:
① 各异常向量的相对位置是固定的,但是整个异常向量表的起始地址是可以通过CP15的c12寄存器配置的。在有操作系统的情况下,还涉及异常向量表区域的地址映射(e.g. 可以nuttx为例)
② 由于流水线的存在,在使用异常模式LR保存的返回地址时可能需要就行修正,具体情况可参考《ARM Architecture Reference Manual》A2.6.2 ~ 2.6.9,常用的CPU异常处理流程如下
注1:reset是SoC的上电启动/复位流程,所以无需异常返回(也不存在所谓返回)
注2:进入reset异常时,CPU会关闭CPU端的所有中断
b. irq
注1:LR_irq中保存的就是下条指令 + 4,所以在中断处理函数中需要修正
注2:CPU检测IRQ中断的是在指令边界处(at instruction boundaries),所以中断不会打断一条正在执行的指令。
c. swi
注:从CPU对swi中断的处理流程中可知,LR_svc中保存的地址无需修正
③ 每个异常向量只有4B空间,因此一般只部署一条跳转指令,跳转到具体的异常处理函数。
④ 系统刚启动时,DRAM尚未初始化,程序都在iRAM中运行。所以S5PV210在iRAM中设置了异常向量表,供暂时性使用。(实际就是与CPU约定,此时发生中断时从0xD0037400处开始取异常向量)
补充:对iRAM中Exception Vector Table使用与机制的讨论
示例代码对iRAM中异常向量表的使用方式如下,
a. 实现汇编阶段中断处理代码
该过程完成如下工作,
· IRQ栈的设置
· 中断现场保护
· 调用C语言中断处理函数
· 中断现场恢复
b. 将IRQ_handle地址赋值到iRAM中断向量表
至此,就将IRQ_handle的地址部署在iRAM中断向量表中IRQ中断对应的位置
这就导致理解上的一个问题,IRQ_handle是如何被执行的 ?
在IRQ中断发生后,PC是被置为IRQ对应的中断向量的,在后续的DDR实现中可见,该位置部署了一条跳转指令,跳转到IRQ_handle运行
但是在iRAM实现中,只是将IRQ_handle地址拷贝到指定位置,显然本身无法完成到IRQ_handle的跳转。如果iRAM实现中就是将0xD0037400设置为异常向量表的起始地址,那么IRQ中断发生后,会将PC置为(0xD0037400 + 0x18),并从该地址开始取指执行,而此处4B的内容为IRQ_handle的地址,显然无法实现跳转
因此合理的解释是在iRAM实现中,从0xD0037400开始的位置仅用于存储中断异常要跳转的目标地址,而异常向量部署在其他位置。IRQ中断发生时,实际异常向量从对应位置取出跳转的目的地址
为了验证上述猜想,设计如下实验,
a. 在start.S中增设全局变量iram_vector & ddr_vector
其中iram_vector用于存储异常向量表重定向之前的位置,ddr_vector用于存储重定向之后的位置
b. 在set_exception_vector_addr函数中获取异常向量表重定向前后的位置
c. 在start_main函数中输出iram_vector & ddr_vector
实验方案改进:
原方案通过全局变量iram_vector & ddr_vector记录异常向量表地址的方案并不可行,因为在iRAM运行阶段设置的全局变量值会被后续的代码重定位覆盖,因此start_main函数中读取到的始终是该全局变量的初始值
下面我们使用iRAM保存异常向量表地址,具体如下,
由于当前没有使用Signature区域,所以借用该区域存储异常向量表设置前后的地址值
验证结果如下:
可见之前的推论是正确的,在iRAM实现中,异常向量表的位置其实在0x0处,也就是在iROM中,而iRAM中只是部署中断处理函数的hook点
⑤ 软硬件合作完成中断处理
CPU硬件操作只是保存了中断返回地址和CPSR,其余中断现场的保护,以及中断的返回均需要软件完成。而且由于流水线的存在,硬件保存的中断返回地址可能还要修正。也就是说软件需要完成如下工作,
a. 进一步保护现场
b. 运行中断服务程序(ISR)
c. 中断返回
⑥ 关于CP15中c1寄存器VE(vectored interrupt enable)位的说明
如果VE位没有使能,当发生IRQ或FIQ时,PC会从异常向量表处取指执行;当VE使能时,可以通过VIC port获取ISR地址(具体方式可参考PL192手册中关于Vectored IRQ interrupt flow sequence using VIC port的描述)。简单说就是可以不读取VICADDRESS寄存器获取ISR地址,从而减少中断延时。
但是S5PV210使用的Cortex-A8内核中并没有实现VE位,这也是使用国嵌代码实现中断会出现异常的原因(详见下文描述)
2.3 PL192 VIC解析
2.3.1 S5PV210的中断控制器的构成部件
4个VIC(Vectored Interrupt Controller),ARM PrimeCell PL192
4个TZIC(TrustZone Interrupt Controller),SP890
4 * VIC + 4 * TZIC共支持93个中断源
当然,我们的示例中仅包含了VIC的使用~~
2.3.2 PL192整体框图分析
说明1:在有中断控制器的系统中,软件必须能识别中断源并获取对应ISR的地址,VIC硬件可以同时完成这2个任务。
而类似2440这种非向量中断,只能通过状态寄存器提供中断源,但无法同时提供ISR地址。
说明2:PL192中每个向量中断的优先级均可以设置,但是本文中不会详细讨论中断优先级相关的主题。
说明3:PL192的输入(VICINTSOURCE[31:0])为高电平有效,而且该信号必须持续到ISR清pend为止。(这点在串口编程中有提及~~)
需要注意的是此处的高电平有效,是指PL192的输入端,而不是说SoC不能处理GPIO的边沿中断。即使GPIO配置的是边沿触发中断,GPIO的中断控制模块发送给PL192的信号仍然是高电平的~~
说明4:读写VICADDRESS寄存器的内部行为
① 读VICADDRESS寄存器
a. 获取当前中断ISR的地址
b. 屏蔽优先级 <= 当前中断的中断请求
② 写VICADDRESS寄存器
a. 标识当前中断已被处理
b. 重新使能优先级 <= 当前中断的中断请求
注:这也就是S5PV210在清pend时必须写4个VICADDRESS寄存器的原因
说明5:FIQ具有最高优先级,而且不需要经过优先级仲裁逻辑
说明6:框图中的daisy-chain(菊花链)接口用于实现多个VIC的级联,手册中介绍了多种级联方式,本文不做分析~~
2.3.3 PL192重点模块分析
① 中断请求逻辑(Interrupt request logic)
a. 输入为各种中断源外设;输出为中断状态寄存器
b. 设置VICSoftInt寄存器,可以通过外设中断触发CPU的软中断(个人感觉该设计的用处在于外设中断发生后,由于触发软中断,CPU进入SWI模式,这与操作系统的运行模式相同,避免了从IRQ模式向SWI模式的切换)
② 非向量FIQ逻辑(Nonvectored FIQ interrupt logic)
a. 根据本VIC和级联VIC的FIQ请求产生FIQ中断
③ 向量IRQ逻辑(Vectored IRQ interrupt logic)
a. 每个VIC有32个Vectored IRQ interrupt logic,每个中断源一个
b. 每个Vectored IRQ interrupt logic向中断优先级逻辑提供向量中断和对应ISR的地址
④ 中断优先级逻辑(Interrupt priority logic)
a. 经过中断优先级逻辑后,会最终向CPU发出IQR中断请求
b. 在发出IRQ中断请求的同时,会通过VICVECTADDROUT提供对应的ISR地址。通过VIC port实现中断处理,就是直接从VICVECTADDROUT获得ISR地址,而不用读取VICADDRESS寄存器
2.3.4 不同种类的中断处理流程
① Vectored interrupt flow sequence using AHB
a. 中断发生
b. ARM核跳转到IRQ异常向量执行
c. 读取VICADDRESS寄存器获得ISR地址
d. 保存中断现场
e. 使能IRQ中断,以使得更高优先级的中断可以触发(ARM核在进入中断处理时,会关闭中断~~)
f. 执行ISR
g. 在中断源端清pend
h. 关闭中断并恢复中断现场
i. 写VICADDRESS寄存器,在VIC端清pend
注:在实际实现中,对该流程有调整(e.g. 在汇编中完成中断现场的保存与恢复,在C语言中完成ISR的获取与调用)
② Nonvectored interrupt flow sequence using AHB
d. dummy read VICADDRESS寄存器,目的是设置内部优先级单元
f. 执行ISR,在ISR开头可以使能IRQ中断,以使得更高优先级的中断可以触发
注1:非向量中断处理方式中通过软件识别中断源后,再去查表执行ISR
注2:此处读写VICADDRESS寄存器只是为了维护内部优先级模块的状态(但是是必须的~~)
注3:这种非向量用法通常在有操作系统时使用。因为中断处理中使用的很多资源是在SVC模式下运行的,一般会通过软件从IRQ模式切换到SVC模式运行,而不是直接在IRQ模式下调用一个函数来处理中断。后文会以nuttx对中断的处理为例进行说明
③ Vectored interrupt flow sequence using VIC port
特别注意:这种方式的最大特点是不通过读取VICADDRESS寄存器获得ISR地址,而是通过VICVECTADDROUT直接获取。但是该方式要求CP15的c1寄存器实现VE位,且该位置1。
由于S5PV210没有详细说明该种方式是否可行,只能通过实验验证,详见后文分析
参考资料:https://download.csdn.net/download/chenjinnanone/13606362
2.4 S5PV210 VIC核心寄存器解析
2.4.1 VICIRQSTATUSn & VICFIQSTATUSn
经过VICINTENABLE和VICINTSELECT之后的中断状态寄存器,当中断发生时,硬件自动将该寄存器的对应位置1,标识中断发生。在使用非向量处理方法时通过该寄存器识别中断源。
2.4.2 VICRAWINTRn
该寄存器记录中断的原始状态,未经过VICINTENABLE和VICINTSELECT处理
2.4.3 VICINTSELECTn
设置中断源对应的中断类型,默认均为IRQ中断
简要说明一下FIQ
① 寄存器资源多
② 异常向量表中最后一个,不用跳转
③ 只有一个FIQ
2.4.4 VICINTENABLEn & VICINTENCLEARn
① VICnINTENABLE寄存器使能相应的中断;VICnINTENCLEAR寄存器禁止相应的中断
② 使用注意点:向VICnINTENCLEAR对应位写1禁止对应中断
③ 注意:这类寄存器的设计一共2种,有些SoC使能和禁止中断是同一个寄存器位(如2440的mask寄存器)。这种寄存器的操作要遵循读-改-写顺序。
210采用2个寄存器,操作时不需读改写,直接把对应位置为1即可,不会影响其他位
2.4.5 VICVECTADDR[0-31]
VICnVECTADDR0 ~ VICnVECTADDR31存放各个中断的ISR地址,共有4 * 32个
2.4.6 VICADDRESS
① VICnADDRESS寄存器的内容由硬件自动设置,当发生了对应中断时,硬件会自动识别中断号,并且自动将该中断的ISR地址从VICnVECTADDRx拷贝到VICnADDR寄存器。这样避免软件查找中断源及ISR,提高了中断响应速度。这也就是所谓向量中断的含义~~
② 手册中提到写VICADDRESS寄存器只能在ISR最后,具体原因见上文对读写VICADDRESS寄存器引发VIC行为的说明。
3. 外部中断程序设计
3.1 中断处理的2个阶段
3.1.1 异常向量表 --> 中断处理函数
① 异常向量表的跳转机制依赖CPU硬件实现
② 为区别于ISR,我们将中断的统一入口称为中断处理函数
3.1.2 中断处理函数 --> ISR
① 在中断处理函数中取出对应的ISR地址,并调用该函数
② ISR中完成中断的实际业务流程
注意:为什么中断处理要先在汇编中进行?
① 中断处理要保护现场和恢复现场(即保护和恢复发生中断时的状态,这里的状态就是各寄存器值,有些寄存器的保护是CPU硬件完成的,恢复则都是由软件完成的。)
② 汇编中需要完成的工作
特别注意:中断处理函数从汇编阶段跳转到C语言阶段只能使用bl,因为执行完C语言阶段后还要回到汇编中恢复异常现场。
但是这也同时对链接带来了要求,因为bl跳转的范围有限所以汇编阶段与C语言阶段的间隔不能超过±32MB
个人:其实一般的bl跳转范围能够满足函数调用的要求,毕竟超过64MB的代码段还是很诡异的~~一般用地址相关指令ldr实现程序执行流的跳转都是跨存储设备的(比如从iRAM跳转到DDR,明显在地址空间上超过了±32MB的范围),而且ldr跳转也是无法返回的。
a. 设置IRQ模式的栈
目前直接将栈设置在DDR中,不再指向iRAM
b. 修正lr并入栈(因为流水线的存在以及ARM在指令末尾检查是否发生异常,所以保存的lr必须先减4)
c. 其他寄存器入栈(r0 ~ r12)
d. 调用C语言阶段的ISR
e. 中断现场恢复
③ C语言阶段完成的工作
C语言阶段最主要的工作就是获取中断对应ISR的地址并调用ISR
需要注意的是,原代码中通过检查4个VICIRQSTATUS的值来确定哪个VIC发生了中断,但是由于S5PV210中的4个VIC通过菊花链方式连接,最终的ISR地址均会保存在VIC0ADDRESS寄存器中(该结论已经过串口中断的验证)
补充:VIC菊花链连接示意图
上图为S5PV210 4个VIC的结构,每个VIC分别管理32个中断源,所有中断源产生的中断最终都由VIC0提交给ARM核。也正是因为这种菊花链结构,在VIC端的清pend流程中,需要写4个VIC的VICADDRESS寄存器
3.2 外部中断解析
3.2.1 什么是外部中断
① SoC支持的中断类型中有一类叫外部中断。内部中断是指中断源来自SoC内部(一般为片内外设,譬如串口、定时器等);外部中断是SoC外部的设备,通过外部中断对应的GPIO引脚产生中断。
② 按键就可以配置为外部中断。具体方法是将按键电路接在外部中断的GPIO上,然后将GPIO配置为外部中断模式。此时通过按键按下 / 弹起改变电压高低,这个电压变化就可以触发外部中断
3.2.2 外部中断寄存器解析
在S5PV210的GPIO中,GPH0 ~ 3可以配置为外部中断管脚,一般连接键盘等设备,可用于实现外部唤醒。
① EXT_INT_n_CON
EXT_INT_n_CON寄存器用于控制外部中断的触发方式,分为电平触发和边沿触发两大类,
电平触发 :GPIO上只要满足电平条件,就会不断触发中断。分为高电平触发和低电平触发
边沿触发:分为上升沿/下降沿/双边沿触发。边沿触发只关心电平变化的瞬间
② EXT_INT_n_MASK
EXT_INT_n_MASK寄存器用于使能对应的外部中断
③ EXT_INT_n_PEND
EXT_INT_n_PEND寄存器为中断请求寄存器,在ISR中需要通过向对应位写1实现清pend
3.3 中断程序设计的3个流程和3个部件
中断程序设计中包括3个流程:中断初始化 --> 中断实际业务 --> 中断返回,其中中断实际业务由ISR负责完成,其余两部分则分散在多个函数中完成。
那么为什么中断初始化和中断返回需要分散在多个函数中完成呢?这是因为整个中断处理涉及3个部件,即CPU + VIC + 中断源,只有通过代码让这3个部件协同工作,才能完成对中断的处理。
3.3.1 CPU
中断初始化流程:
① 设置异常向量表
② CPSR中I位清零(CPU端的中断使能)
③ IRQ栈的设置
④ 中断现场的进一步保存(以区别于CPU自动完成的中断处理流程)
中断返回流程:
① 中断现场返回(同时实现CPU模式的返回)
3.3.2. VIC
中断初始化流程:
① 注册ISR(设置VICnVECTADDRx寄存器)
② VIC端使能中断
中断返回流程:
① VIC端清pend(写4个VICADDRESS寄存器)
3.3.3 中断源
中断初始化流程:
① 设置GPIO模式(主要针对外部中断源,内部中断源无需设置)
② 设置中断源端的中断模式
③ 中断源端使能中断
中断返回流程:
① 中断源端清pend
小结:
① 中断使能需要经过CPU/VIC/中断源三个部件,这条通路上任何一个屏蔽中断,该中断都无法实际触发
② 清pend涉及VIC和中断源两个部件,建议在中断处理函数中清VIC端pend,在ISR中清中断源端pend
3.4 代码分析
3.4.1 按键中断初始化
中断初始化函数完成4项任务:中断配置 + 注册ISR + VIC端使能中断 + 中断源端使能中断
注意:EINT16_31为共享中断
3.4.2 按键中断ISR
中断ISR完成2项任务:中断实际业务 + 中断源端清pend
3.5 验证结果
① 使用iRAM中的异常向量表,中断可以正常工作
② 将异常向量表设置在内存中,中断也可以正常工作。这也是之后中断实验共用的环境。
③ 国嵌课程中使用了另一种中断处理流程,没有设置中断处理函数,直接使用向量中断。根据PL192手册,VIC确实支持这种用法,但是需要CP15 c1寄存器的VE位为1。但是S5PV210中并没有实现VE位。
经过实际验证,中断确实可以正常运行,但是触发2~4次后,心跳打印木有了,目前尚未解决。说明此时函数已经跑飞了,只是仍能响应中断。
当然,如果要使用这种中断处理方式,需要ISR自己处理中断现场的保存与返回。
个人:其实在对CP15不做任何设置的情况下,能够同时让VIC实现AHB中断和VIC port中断在逻辑上就是不合理的。实现不同的模式,总要进行适当的配置~~
4. SWI中断解析
4.1 概述
① 调用SWI指令可以产生一个异常陷阱,进而跳转到SWI异常向量处执行
② 通过SWI机制,运行在用户态的应用程序可以请求操作系统执行一系列特权操作。在ARM-Linux架构中,就是通过SWI机制实现系统调用。
也就是说,只有通过中断或系统调用才能进入内核态,调用内核提供的例程。
4.2 SWI命令解析
语法:
SWI{<cond>} <immed_24>
此处命令中的immed_24可以作为SWI软中断号使用,用于标识用户态应用程序请求的服务类型。虽然ARM核在执行SWI指令时会忽略该立即数,但是可以被SWI handler用于辨识系统调用类型。
其实SWI提供了2种传递系统调用类型的方式:
① 使用SWI指令中的立即数传递
② 忽略SWI指令中的立即数,通过寄存器r0传递
4.3 系统调用简单模拟代码解析
说明1:首先要设置异常向量表中的SWI表项
说明2:注意辨别系统调用号的方法
当发生SWI中断时,lr_swi寄存器中保存的是SWI下一条指令的地址,所以lr_swi - 4即为SWI指令的地址(ARM模式下)。从该地址取出SWI指令后,从中分离出低24位,也就是系统调用号。
说明3:实际libc库中的系统调用都是封装在C语言代码中,通过内嵌汇编调用SWI指令。
说明4:基于switch-case的SWI ISR
在上述范例中,直接在SWI的中断处理函数中解析系统调用号并直接调用对应的系统调用,也可以在中断处理函数中调用统一的SWI ISR,示例如下,
void c_swi_handler(unsigned int number)
{
switch(number) {
case 0:
/* SWI number 0 code */
break;
case 1:
/* SWI number 1 code */
break;
...
default:
/* Unknown SWI number */
break;
}
}
相应的,在汇编阶段做如下调用,
bic r0, r0, #0xff000000
bl c_swi_handler
此外,如果需要传递的参数多于1个,还可以使用栈,将栈指针作为函数参数传递给c_swi_handler,示例如下,
bic r0, r0, #0xff000000
mov r1, sp
bl c_swi_handler
相应的c_swi_handler函数原型为,
void c_swi_handler(unsigned int number, unsigned int *reg);
5. nuttx中的中断处理
说明:选择nuttx作为范例有2个原因,
① 相较于Linux,nuttx的中断子系统更加简单
② 作为一款RTOS,nuttx的中断子系统与硬件的配合更加紧密,中间层更少
5.1 中断注册分析
① 中断注册使用的数据结构
其中handler为nuttx中的ISR,arg为调用ISR时传递的参数。
此处强调nuttx中的ISR是为了与上文中裸机代码的ISR区分,此处的ISR完全由软件维护,是在操作系统层面实现的中断服务函数。
② 中断注册流程
函数原型:
int irq_attach(int irq, xcpt_t isr, FAR void *arg);
调用示例:
irq_attach(INTC_SPI0_INT_NUM, (xcpt_t)pangu_spi0_irq, (void *)priv);
函数流程分析:
说明1:注册ISR时会调用enter_critical_section关闭中断,在实现上enter_critical_section会将CPSR中的I位置1
说明2:注册ISR就是根据中断号设置g_irqvector数组中的成员以便后续调用
说明3:在nuttx中注册中断和使能中断是分开的(Linux中同时完成),在调用irq_attach函数注册中断后,还需要调用up_enable_irq使能该中断。
注意,这个是VIC端的中断使能,中断源端的中断使能需要另外完成,
5.2 中断处理流程
① 异常向量表
所在文件:arch/arm/src/arm/up_vectortab.S
② 汇编阶段中断处理函数up_vectorirq
所在文件:arch/arm/src/arm/up_vectors.S
a. save lr_irq & spsr_irq
说明:执行up_vectorirq时,CPU已处于IRQ模式。代码中先将lr_irq和spsr_irq保存在数据段中,目的是为了在稍后切换到SVC模式后能够正确实现中断返回。
在保存lr_irq时,进行了返回地址的修正。
b. 切换到SVC模式
说明1:开始执行这段代码时,lr_irq寄存器中保存的是spsr_irq中的值,也就是从其他模式因中断进入IRQ模式之前的CPSR值。此处将其模式设置为SVC,并将I位置1,然后实现模式切换。也就是说,进入中断处理流程时是关闭中断的状态。
说明2:在有操作系统的情况下并不在IRQ模式中进行中断处理,因为中断处理所需要的很多资源(e.g. 操作系统提供的功能接口)均在SVC模式下运行。
c. 建立中断处理上下文
首先要理解nuttx中对中断上下文的理解,XCPTCONTEXT_SIZE宏表示中断上下文要保存的寄存器个数,此处设置为17个寄存器,是指r0 ~ r15 + CPSR。
在代码实现中,首先保存r0 ~ r12寄存器的值,这也是传统的保存中断现场。
说明1:此处使用的栈为sp_svc,根据后文可知,此处为SVC模式的用户栈(user stack)
说明2:可以在切换到SVC模式后再保存中断现场,是因为IRQ模式只有sp/lr/spsr这3个私有banked寄存器,r0 ~ r12为共用寄存器。
说明3:此处stmia命令后使用的是sp,而不是sp!,所以数据入栈后不会更新sp寄存器的值
接着保存r13 ~ r15 + CPSR
说明1:r1 ~ r4寄存器值的对应关系
r1:sp_SVC
r2:lr_SVC
r3:lr_IRQ
r4:spsr_IRQ
下面说明一下,保存这4个寄存器的含义。要理解这4个寄存器的含义,就必须抓住一点,即中断返回就是要完全返回中断发生之前的状态。
sp_SVC:保存sp + #XCPTCONTEXT_SIZE的值,即中断发生之前SVC用户栈的栈顶位置;中断返回时赋值给r13(sp)
lr_SVC:中断发生之前SVC模式lr的值,中断返回时赋值给r14(lr)
lr_IRQ:中断返回地址,中断返回时赋值给pc
spsr_IRQ:中断返回状态,中断返回时先从栈中取出并复制给spsr_svc,然后通过^实现中断状态的返回。
说明2:r1 ~ r4入栈的位置就是之前预留出的r13 ~ r15 + CPSR空间
说明3:根据上文分析,nuttx可以区分用户态与内核态,即使在用户态(USR模式)发生中断,也可以正确返回。
假设中断发生在USR模式,然后CPU进入IRQ模式,最终在SVC模式处理中断。这就涉及了两个现场的保护,即USR模式的现场和SVC模式的现场,具体分析如下。
USR模式现场:r0 ~ r12 + lr_IRQ + spsr_IRQ,这组寄存器描述了整个中断现场,保存他们也就是为了实现中断返回。
SVC模式现场:lr_SVC + sp_SVC,这组寄存器描述了从IRQ模式切换到SVC模式进行处理时,SVC模式的状态(即当前的函数返回地址 + SVC用户栈栈顶)。在后续的中断处理中,SVC现场会被破坏(通过调用各种函数),因此需要加以保存。
在构建中断上下文时,我们将这两组现场均保存在SVC用户栈中,在中断返回时,同时实现这两个现场的恢复(后文还有论述)。
d. 调用C语言阶段中断处理函数
说明1:启动SVC模式下的中断栈
当配置CONFIG_ARCH_INTERRUPTSTACK > 3时会用SVC模式下的中断栈,如果不加设置,中断处理过程中将默认使用SVC模式的用户栈。
在编译选项中还可以对SVC模式中断栈的大小进行设置~~
说明2:传递给up_decodeirq的参数为r0,即当前SVC用户栈的栈顶位置。结合up_decodeirq函数的原型,此处传递的就是保存在SVC用户栈上的中断上下文地址。
说明3:如果启用SVC模式中断栈,在调用C语言阶段中断函数处理前后,需要保存/恢复SVC用户栈(实际上并未使用该栈,但要确保在调用up_decodeirq之后,sp_svc中保存的是SVC模式用户栈的栈顶位置)
e. 中断现场返回
说明:首先从保存的中断上下文中取出spsr_IRQ的值并复制给spsr,然后通过ldr + ^实现中断现场 + SVC现场的返回。
注意:此处的现场恢复在通过ldmia加载内存中数据时,其实会修改sp_svc和lr_svc的值
深度分析:nuttx中断处理的"两次返回"
在上文裸机代码的中断返回中,只是从IRQ模式返回到中断发生之前的模式(示例中就是SVC模式),所以只有"一次返回"
所以进一步保存中断现场时,只是使用了r0 ~ r12。同时,由于之前已经分别设置了svc和irq模式的栈,所以恢复时也无需处理sp(r13)寄存器
nuttx中断处理中"两次返回"的原因在于nuttx并不在IRQ模式处理中断,而是切换到SVC模式处理,如下图所示,
由于nuttx中切换到svc模式处理中断,且使用的栈也在svc模式,所以需要恢复2个现场,
a. svc中断处理前后的现场(因为切换到svc模式处理中断,这是问题的根源)
此处借助恢复中断发生前模式现场的时机恢复svc中断处理现场是很巧妙的,核心就是认为切换到目标模式后,需要保存目标模式bank的寄存器
a. 当未发生异常时,当前模式的lr,保存的是函数调用的返回地址
b. 当发生异常时,当前模式的lr,保存的是异常发生前模式的返回地址
参考资料:https://download.csdn.net/download/chenjinnanone/13606383
③ C语言阶段中断处理函数up_decodeirq
所在文件:arch/arm/src/pangu/pangu_decodeirq.c
说明:up_decodeirq主要用于识别中断源,然后调用irq_dispatch函数找到并执行对应的ISR。该函数也是硬件相关的,具体通过哪些寄存器识别中断源,依各SoC的中断控制器决定。
④ 查找&执行ISR函数irq_dispatch
所在文件:sched/irq/irq_dispatch.c
说明:irq_dispatch函数在ISR注册表中查找中断源对应的ISR,并调用执行。
补充:目前nuttx运行在pangu芯片上,该芯片的INTC中没有设计INTC端的清pend操作,而中断源端是否需要清pend,则由各部件规定(e.g. EINT模块是需要清中断的)
6. 通用中断控制器(GIC)简介
6.1 ARM中断控制器简介
6.1.1 中断控制器功能
① ARM内核实际只有2个外部中断输入信号:IRQ & FIQ
② SoC外部中断源数量众多(e.g. S5PV210为93个)
③ 中断控制器就是用于处理众多SoC外部中断源触发的中断信号,通过使能、屏蔽、优先级判断等手段,向ARM核提交当前使能且优先级最高的中断请求
6.1.2 中断处理方式
中断控制器在向ARM核提交中断请求之后,还需要向CPU提供对应中断的ISR地址(异常向量表只是跳转到第1级公共的中断处理函数),根据不同的提供方式,可将中断控制器分为如下2类:
① 中断软件分支处理(e.g. NVIC & GIC)
NVIC & GIC使用软件来处理异常分支,在中断处理函数中,可通过读取中断控制器来获取中断源信息(一般为中断号),然后根据该信息跳转到对应的ISR执行
一种比较好的处理方式是在一片内存中建立中断ISR函数列表(比如构成函数指针数组),然后根据中断号取出对应ISR地址并跳转执行。这种方案的好处是能够很方便地动态改变与某个中断号对应的ISR(比如提供一种注册手段)
② 中断硬件分支处理(e.g. VIC)
VIC使用硬件来处理异常分支,以S5PV210为例,当中断控制器触发中断时,对应的ISR地址已经写入VICADDRESS0寄存器。CPU可以直接读取,并跳转执行
说明:相对而言,硬件分支处理的效率更高(不用查表),但是在操作系统中一般不会在IRQ模式下直接根据VICADDRESS0寄存器的值跳转到ISR处运行,而是涉及模式切换等一系列操作。因此在操作系统中,即使SoC提供了VIC的功能,一般也会使用软件方式处理
6.2 GIC支持的中断类型
① 软中断(Software Generated Interrupt,SGI)
软件生成的中断,通过写入软中断产生寄存器(ICDSGIR)产生。软中断能以所有processor或一组processor为目标,常用于processor间通信
中断号0 ~ 15为SGI预留
② 专用外设中断(Private Peripheral Interrupt, PPI)
由外设产生,但是由特定processor处理。这些中断源对processor是私有的,并且独立于其他processor上的相同中断源(e.g. 每个processor上的定时器中断)
中断号16 ~ 31为PPI预留
③ 共享外设中断(Shared Peripheral Interrupt,SPI)
由外设产生,可以发送给一个或多个processor处理
中断号32 ~ 1020用于SPI中断
6.3 GIC结构概述
GIC在结构上主要分为分配器(Distributor)和CPU接口(CPU interface),他们各持有一组寄存器,CPU通过AMBA总线访问这2组寄存器
根据AMBA总线的类型不同,由可以分为AXI salve接口 & AHB-Lite slave接口
① AXI slave接口
此处注意2点:
a. processor通过AXI interface访问分配器和CPU接口分别持有的寄存器组
b. 此处的2 * ARM processor + 1 * DSP作为AXI master
② AHB-Lite slave接口
6.4 GIC主要组件
6.4.1 分配器
作用:接收SoC外部中断并向对应的CPU接口提交优先级最高的中断
功能:
① 使能 / 禁用中断
② 设定中断优先级
③ 设置中断的目标CPU
④ 设置中断触发方式
⑤ 传递SGI到一个或多个目标CPU
⑥ 查看中断状态
⑦ 提供软件方式设置或清除中断的挂起状态
6.4.2 CPU接口
作用:每个processor配套一个CPU接口,他们接收分配器提交的最高优先级中断,然后根据自身的优先级设定,触发process中断
CPU接口只接收2类中断,
① 优先级 > CPU接口优先级屏蔽寄存器设置值
② 优先级 > 对应CPU当前正在处理的中断
功能:
① 通知processor中断请求
② 应答中断
③ 指示中断处理完成
④ 设置processor的中断优先级屏蔽
⑤ 定义processor中断抢占策略
⑥ 为processor决定最高优先级的挂起中断