ARM 裸机之中断

 

这里记录着 ARM 中断的处理流程、中断控制器和引入外部中断的相关设置

这里记录着 ARM 中断的处理流程、中断控制器和引入外部中断的相关设置

中断程序处理流程

与中断有关的硬件器件有中断控制器、iROM中的异常向量表GPIO引脚

iROM 中的异常向量表设置:

r_exception_irq = (unsigned int)IRQ_handle;

把异常处理的地址直接放到对应的地址中即可

初始化系统的中断控制器

  • 首先要禁止所有中断,防止程序进入未初始化的 ISR 而跑飞 VIC0INTENCLEAR
  • 设置中断的类型为 IRQ 模式:VIC0INTSELECT
  • 清除各个中断向量地址寄存器内容 VIC0ADDR = 0

对于外部中断的引入,需要把 GPIO 引脚设置成外部中断模式:

  • 设置外部中断模式:GPH0CON
  • 外部中断触发模式:EXT_INT_0_CON:上升沿触发
  • 外部中断屏蔽失效:EXT_INT_0_MASK:0
  • 清除外部中断悬挂状态:EXT_INT_0_PEND[n] = 1

绑定 ISR 到中断控制器硬件

就是把对应中断源 n 的 ISR 函数地址放到中断控制器的 VIC0VECTADDR[n] 寄存器中,这样当相应的中断源有信号时,中断控制器会把这个 VIC0VECTADDR[n] 放到 VIC0ADDR 中。

中断源编号是从 0 ~ 127,每个编号对应着不同的中断类型,比如中断源 0 对应 EINT0 中断、中断源 40 对应 NFC 中断。

我们把中断源 EINT2 和 isr_eint2() 中断处理程序绑定到一起,也就是把 isr_eint2() 放到 VIC0VECTADDR[EINT2] 寄存器中。这样,当 EINT2 这个中断源有效时,isr_eint2() 就会放入 VIC0ADDR 中

最后在使能相应中断

  • 我们通过 VIC0INTENABLE[n] = 1 来使能相应中断的响应;
  • 通过 VIC0INTENCLEAR[n] = 1 来关闭相应中断响应

异常处理的两个阶段

  • 异常向量表的跳转
    • 硬件完成
    • 注意要先设置好异常向量表现场保护/恢复程序
  • 真正的异常处理程序
    • 找到中断源区间
      • 通过轮询中断控制器的 VIC0IRQSTATUS 状态,如果某个 VICxIRQSTATUS 的状态非零,说明这个区间有中断源有效
    • 找到对应的 ISR
      • 从 VICxADDR 中取出对应的中断处理函数地址
      • VICxADDR 中的值是硬件自动放到其中的
    • 进行对应的中断处理 (*isr)()
      • 先执行处理程序
      • 处理完后,清除 GPIO 引脚的外部中断悬挂位 rEXT_INT_0_PEND |= (1<<2)
      • 最后清除 VICxADDR 寄存器 VIC0ADDR = 0

ARM 裸机之 CPU 的中断系统

按键

按键的电路连接

key

按键 GPIO 引脚
SW5 GPH0_2
SW6 GPH0_3
SW78910 GPH2_0123

按键的电路连接分析:

平时按钮没有按下时,按钮内部断开,GPIO引脚处电压为高电平;当有人按下按钮时,按钮内部导通,外部VDD经过电阻和按钮连接到地,形成回路,此时GPIO引脚处电压就变成了低电平。此时VDD电压全部分压在了电阻上(这个电阻就叫分压电阻,这个电阻不能太小,因为电阻的功率是U*U/R)

按键的工作方法

按键的按下与弹开,分别对应GPIO的两种电平状态(按下则GPIO为低电平,弹开则GPIO为高电平)。此时SoC内部可以通过检测这个GPIO的电平高低来判断按键有没有被按下,这个判断结果即可作为SoC的输入信号。按下时是低电平,弹起时是高电平

按键属于输入类设备

  • 按键一般用来做输入设备(由人向SoC发送信息的设备,叫输入设备),由人向SoC发送按键信号(按键信号有2种:按下信号和弹开信号)。
  • 有些设备就是单纯的输入设备,譬如按键、触摸屏等;有些设备就是单纯的输出设备,譬如LCD;还有一些设备是既能输入又能输出的,叫输入输出设备(IO),譬如串口。

按键对应的GPIO模式设置

  • 按键接到GPIO上,按键按下还是弹起,决定外部电路的接通与否,从而决定这个GPIO引脚的电压是高还是低;这个电压可以作为这个GPIO引脚的输入信号,此时GPIO配置为输入模式,即可从SoC内部读取该引脚的电平为1还是0(1对应高电平,0对应低电平)。
  • GPH0CON(0xE0200C00) GPH2DAT(0xE0200C04) GPH2CON(0xE0200C40) GPH2DAT(0xE0200C44)
  • 应该在CON寄存器中将GPIO设置为input模式,然后去读取DAT寄存器(读取到的相应位的值为1表示外部是高电平(对应按键弹起),读取到的位的值为0表明外部是低电平(按键按下))

什么是按键消抖

  • 按键这种物理器件本身会有抖动信号,抖动信号指的是在电平由高到低(也就是按键按下时)或者电平由低到高(也就是按键弹起时)过程中,电平的变化不是立刻变化,而是经过了一段时间的不稳定期才完成变化,在这个不稳定期间电平可能会时高时低反复变化,这个不稳定期就叫抖动(抖动期内获取按键信息是不可靠的,要想办法消抖)。
  • 什么叫消抖?消抖就是用硬件或者软件方法来尽量减小抖动期对按键获取的影响。
  • 消抖常用2种思路:
    • 第一是硬件消抖,消抖思路就是尽量减小抖动时间,方法是通过硬件添加电容等元件来减小抖动;
    • 第二是软件消抖,消抖思路是发现一次按键按下/弹起事件后,不立即处理按键,而是延时一段时间(一般10~20ms,这就是消抖时间)后再次获取按键键值,如果此次获取和上次一样是按下/弹起,那就认为真的按下/弹起了。

S5PV210的中断体系介绍

什么是中断

  • 中断的发明是用来解决宏观上的并行需要的。宏观就是从整体上来看,并行就是多件事情都完成了。
  • 微观上的并行,就是指的真正的并行,就是精确到每一秒甚至每一刻,多个事情都是在同时进行的。宏观上面的并行并不等于围观的并行,有时候宏观上是并行的,微观上是串行的。
  • 为什么需要中断?因为单核CPU实际无法并行的,但是通过中断机制,可以实现假并行(宏观上的并行,微观上实际还是串行的)。

SoC对中断的实现机制:异常向量表

vtable

  1. 异常向量表是CPU中某些特定地址的特定定义。当中断发生的时候,中断要想办法通知CPU去处理中断,怎么做到?这就要靠异常向量表。
  2. 在CPU设计时,就事先定义了CPU中一些特定地址作为特定异常的入口地址(譬如定义0x00000000地址为复位异常向量地址,则发生复位异常时CPU会自动跳转到0x00000000地址去执行指令。又譬如外部中断对应的异常向量地址为0x30000008,则发生外部中断后,CPU会硬件自动跳转到0x30000008地址去执行指令。)
  3. 以上讲的是CPU硬件设计时对异常向量表的支持,下来就需要软件支持了。硬件已经决定了发生什么异常CPU自动跳转PC到哪个地址去执行,软件需要做的就是把处理这个异常的代码的首地址填入这个异常向量地址。

S5PV210的异常向量表

(1)异常向量表中各个向量的相对位置是固定的,但是他们的起始地址是不固定的,各种 SoC 可以不一样,而且复杂 ARM 中还可以让用户来软件设置这个异常向量表的基地址。

(2)扩展到所有架构的 CPU 中:所有架构(譬如 51 单片机、PIC 单片机)的 CPU实现中断都是通过异常向量表实现的,这个机制是不变的;但是不同 CPU 异常向量表的构造和位置是不同的。

异常和中断的区别和联系

  • 针对SoC来说,发生复位、软中断、中断、快速中断、取指令异常、数据异常等,我们都统一叫异常。所以说:中断其实是异常的一种。
  • 异常的定义就是突发事件,打断了CPU的正常常规业务,CPU不得不跳转到异常向量表中去执行异常处理程序;中断是异常的一种,一般特指SoC内的内部外设产生的打断SoC常规业务,或者外部中断(SoC的GPIO引脚传回来的中断)。

异常向量表的编程处理

像内存一样去访问异常向量表

  1. S5PV210的异常向量表可以改变(在CP15协处理器中),以适应操作系统的需求。但是目前系统刚启动时,此时DRAM尚未初始化,程序都在SRAM中运行。210在iRAM中设置了异常向量表,供暂时性使用。

  2. 查210的《iROM application note》文档中iRAM的地址分配,可知,iRAM中的异常向量表起始地址为0xD0037400。知道了异常向量表的起始地址后,各个异常对应的入口就很好知道了。

    addr

函数名的实质就是函数的首地址

函数名在C语言中的理解方法和变量名其实没区别。编译器会把这个函数的函数体对应的代码段和这个函数的函数名(实质是符号)对应起来,等我们在使用这个函数名符号时,编译器会将函数的函数体实际上做替换。因为函数体都不止4字节,而函数名这个符号只能对应1个地址,所以实际对应的是函数体那一个代码段的首地址。

总结:当我们将异常处理程序的首地址和异常向量表绑定起来后,异常处理初步阶段就完成了。到目前可以保证相应异常发生后,硬件自动跳转到对应异常向量表入口去执行时,可以执行到我们事先绑定的函数。

为什么中断处理要先在汇编中进行

(1) 中断处理要注意保护现场(中断从SVC模式来,则保存SVC模式下的必要寄存器的值)和恢复现场(中断处理完成后,准备返回SVC模式前,要将保存的SVC模式下的必要寄存器的值恢复回去,不然到了SVC模式后寄存器的值乱了,SVC模式下原来正在进行的常规任务就被你搞坏了)

(2) 保存现场包括:第一:设置IRQ栈;第二,保存LR;第三,保存R0~R12

(3) 为什么要保存LR寄存器?要考虑中断返回的问题。中断ISR执行完后如何返回SVC模式下去接着执行原来的代码。中断返回其实取决于我们进入中断时如何保存现场。中断返回时关键的2个寄存器就是PC和CPSR。所以我们在进入IRQ模式时,应该将SVC模式下的下一句指令的地址(中断返回地址)和CPSR保存起来,将来恢复时才可以将中断返回地址给PC,将保存的CPSR给CPSR。

(4) 中断返回地址就保存在LR中,而CPSR(自动)保存在(IRQ模式下的)SPSR中

汇编保存现场和恢复现场

  • 保护现场关键是保存:中断处理程序的返回地址,r0-r12(cpsr是自动保存的)
  • 恢复现场主要是恢复:r0-r12,pc,cpsr
IRQ_handle:
	// 设置IRQ模式下的栈
	ldr sp, =IRQ_STACK
	// 保存LR
	// 因为ARM有流水线,所以PC的值会比真正执行的代码+8,
	sub lr, lr, #4
	// 保存r0-r12和lr到irq模式下的栈上面
	stmfd sp!, {r0-r12, lr}
	// 在此调用真正的isr来处理中断
	bl irq_handler
	// 处理完成开始恢复现场,其实就是做中断返回,关键是将r0-r12,pc,cpsr一起回复
	ldmfd sp!, {r0-r12, pc}^

S5PV210的向量中断控制器

异常处理的2个阶段

可以将异常处理分为2个阶段来理解:

  • 第一个阶段是异常向量表跳转
    • 第一个阶段之所以能够进行,主要依赖于CPU设计时提供的异常向量表机制。第一个阶段的主要任务是从异常发生到响应异常并且保存/恢复现场、跳转到真正的异常处理程序处。
  • 第二个阶段就是进入了真正的异常处理程序irq_handler之后的部分
    • 第二个阶段的目的是识别多个中断源中究竟哪一个发生了中断,然后调用相应的中断处理程序来处理这个中断

S5PV210 的第二阶段处理过程

  • 怎么找到具体是哪个中断
    • S5PV210中因为支持的中断源很多,所以直接设计了4个中断寄存器,每个32位,每位对应一个中断源。
    • 理论上210最多支持128个中断,实际支持不足128个,有些位是空的
    • 210没有子中断寄存器,每个中断源都是并列的。当中断发生时,在irq_handler中依次去查询4个中断源寄存器,看哪一个的哪一位被置1,则这个位对应的寄存器就发生了中断,即找到了中断编号。
  • 怎么找到对应的isr的问题
    • 当发生相应中断时,硬件会自动的将相应isr推入一定的寄存器中,我们软件只要去这个寄存器中执行函数就行了

S5PV210中断处理的主要寄存器

VICnINTENABLE和VICnINTENCLEAR

  • VICnINTENABLE 对应interrupt enable,INTENCLEAR对应interrupt enable clear
  • INTENABLE寄存器负责相应的中断的使能,INTENCLEAR寄存器负责相应的中断的禁止。
  • 当我们想使能(意思就是启用这个中断,意思就是当硬件产生中断时CPU能接收的到)某个中断时,只要在这个中断编号对应的VICnINTENABLE的相应bit位写1即可(注意这个位写1其他位写0对其他位没有影响);
  • 如果我们想禁止某个中断源时,只要向VICnINTENCLEAR中相应的位写1即可。

注意:这里的设计一共有2种:

  • 有些CPU是中断使能和禁止是一个寄存器位,写1就使能写0就进制(或者反过来写1就进制写0就使能),这样的中断使能设计就要非常小心,要使用我们之前说过的读改写三部曲来操作;
  • 另一种就是使能和禁止分开为2个寄存器,要使能就写使能寄存器,要禁止就写禁止寄存器。这样的好处是我们使能/禁止操作时不需要读改写,直接写即可。

VICnINTSELECT

  • 设置各个中断的模式为irq还是fiq。一般都设置成irq
  • IRQ和FIQ究竟有何区别。
    • 210中支持2种中断,irq和fiq。
    • irq是普通中断,fiq是快速中断。
      • 快速中断提供一种更快响应处理的中断通道,用于对实时性要求很高的中断源。
      • fiq在CPU设计时预先提供了一些机制保证fiq可以被快速处理,从而保证实时性。fiq的限制就是只能有一个中断源被设置为fiq,其他都是irq。
  • CPU如何保证fiq比irq快?
    • 第一,fiq模式有专用的r8~r12,因此在fiq的isr中可以直接使用r8-r12而不用保存,这就能节省时间;
    • 第二,异常向量表中fiq是最后一个异常向量入口。因此fiq的isr不需要跳转,可以直接写在原地,这样就比其他异常少跳转一次,省了些时间。

VICnIRQSTATUS和VICnFIQSTATUS

中断状态寄存器,是只读的。当发生了中断时,硬件会自动将该寄存器的对应位置为1,表示中断发生了。软件在处理中断第二阶段的第一步,就是靠查询这个寄存器来得到中断编号的。

VICnVECTPRIORITY0~VICnVECTPRIORITY31

中断优先级设置寄存器,设置多个中断同时发生时先处理谁后处理谁的问题。一般来说高优先级的中断可以打断低优先级的中断,从而嵌套处理中断。当然了有些硬件/软件可以设置不支持中断嵌套。

VICnVECTADDR0~VICnVECTADDR31、VICnADDR

  • VICnVECTADDR0到31这32个寄存器分别用来存放真正的各个中断对应的isr的函数地址。相当于每一个中断源都有一个VECTADDR寄存器,程序员在设置中断的时候,把这个中断的isr地址直接放入这个中断对应的VECTADDR寄存器即可。
  • VICnADDR这个寄存器是只需要读的,它里面的内容是由硬件自动设置的。当发生了相应中断时,硬件会自动识别中断编号,并且会自动找到这个中断的VECTADDR寄存器,然后将其读出复制到VICnADDR中,供我们使用。这样的设计避免了软件查找中断源和isr,节省了时间,提高了210的中断响应速度。

中断向量控制器概述

S5PV210 的中断控制器是由 4 个中断向量控制器、ARM PrimeCell 中断控制器 PL192 和 4 个 TrustZone 中断控制器(TZIC)SP890 组成。

4 个 TZIC 和 4 个 VIC 通过菊花链方式连接以支持 93 个中断源。TZIC在TrustZone设计中提供了安全中断系统的软件接口。它提供了对nFIQ中断的安全控制,并屏蔽了从系统的非安全侧的中断控制器(VIC)中产生的中断源。使用后者产生nIRQ信号。

为了从非安全中断源中产生nFIQ,TZIC0 从非安全中断控制器中获取 nNSFIQIN 信号。

S5PV210 的中断源编号在 P560 页标出。

当用户清除中断挂起时,用户必须将0写入所有的VICADDRESS寄存器(VIC0ADDRESS、VIC1ADDRESS、VIC2ADDRESS和VIC3ADDRESS)。

寄存器地址

寄存器 地址 R/W 说明 初始值
VIC0IRQSTATUS 0xF200_0000 R IRQ 状态寄存器 0x0
VIC0FIQSTATUS 0xF200_0004 R FIQ 状态寄存器 0x0
VIC0RAWINTR 0xF200_0008 R 原始中断状态寄存器 -
VIC0INTSELECT 0xF200_000C R/W 中断选择寄存器 0x0
VIC0INTENABLE 0xF200_0010 R/W 中断使能寄存器 0x0
VIC0INTENCLEAR 0xF200_0014 W 中断使能清除寄存器 -
VIC0SOFTINT 0xF200_0018 R/W 软中断寄存器 0x0
VIC0SOFTINTCLEAR 0xF200_001C W 软中断清除寄存器 -
VIC0PROTECTION 0xF200_0020 R/W 保护使能寄存器 0x0
VIC0SWPRIORITYMASK 0xF200_0024 R/W 软件优先屏蔽寄存器 0xFFFF
VIC0PRIORITYDAISY 0xF200_0028 R/W 菊花链的向量优先寄存器 0xF
VIC0VECTADDR0 0xF200_0100 R/W 向量0地址寄存器 0x0
VIC0VECTADDR31 0xF200_017C R/W 向量31地址0寄存器 0x0
VIC0VECPRIORITY0 0xF200_0200 R/W 向量0优先级寄存器 0xF
VIC0VECTPRIORITY31 0xF200_027C R/W 向量31优先级寄存器 0xF
VIC0ADDRESS 0xF200_0F00 R/W VIC0向量地址 0x0

中断控制相关寄存器

寄存器 名称 说明 初始值
IRQ 状态寄存器 IRQStatus [31:0] 1 - 中断处于活动状态,每一位一个中断源
VICINTENABLE 和 VICINTSELECT 屏蔽之后的状态
0x0
FIQ 状态寄存器 FIQStatus [31:0] 1 - 中断处于活动状态,每一位一个中断源
VICINTENABLE 和 VICINTSELECT 屏蔽之后的状态
0x0
原始中断状态寄存器 RawInterrupt [31:0] 1 - 中断处于活动状态,每一位一个中断源
VICINTENABLE 和 VICINTSELECT 屏蔽之前的状态
-
中断选择寄存器 IntSelect [31:0] 0 = IRQ interrupt
1 = FIQ interrupt
0x0
中断使能寄存器 IntEnable [31:0] 写入:1 - 使能中断;0 - 无效果
读取:1 - 中断使能;0 - 中断禁用
必须用 VICINTENCLEAR 禁止中断
0x0
中断使能清除寄存器 IntEnable Clear [31:0] 清除相应的 VICINTENABLE 位
写入: 0 - 无效果;1 - 禁用 VICINTENABLE 寄存器中的中断。
-
向量地址寄存器 VectAddr [31:0] 当前有效的 ISR 的地址。向该寄存器写入任何值都会清除当前中断,只有在中断服务例程结束时才能写入。 0x0
各向量地址寄存器 VICVECTADDR[0-31] [31:0] 包含 ISR 向量的地址 0x0
向量优先级寄存器 VICVECTPRIORITY[0-31] [31:0] 选择向量中断优先级。您可以用所需的优先级的十六进制值对寄存器进行编程,从 0-15 之间选择 16 个矢量中断优先级中的任何一个。 0xF

S5PV210中断处理的编程实践

中断控制器初始化

  • 第一阶段绑定异常向量表到异常处理程序;
  • 禁止所有中断源;
  • 选择所有中断类型为IRQ;
  • 清理VICnADDR寄存器为0.
// 主要功能:绑定第一阶段异常向量表;禁止所有中断;选择所有中断类型为IRQ;
// 清除VICnADDR为0
void system_init_exception(void)
{
	// 第一阶段处理,绑定异常向量表
	r_exception_reset = (unsigned int)reset_exception;
	r_exception_undef = (unsigned int)undef_exception;
	r_exception_sotf_int = (unsigned int)sotf_int_exception;
	r_exception_prefetch = (unsigned int)prefetch_exception;
	r_exception_data = (unsigned int)data_exception;
	r_exception_irq = (unsigned int)IRQ_handle;
	r_exception_fiq = (unsigned int)IRQ_handle;
	
	// 初始化中断控制器的基本寄存器
	intc_init();
}

// 初始化中断控制器
void intc_init(void)
{
    // 禁止所有中断
	// 为什么在中断初始化之初要禁止所有中断?
	// 因为中断一旦打开,因为外部或者硬件自己的原因产生中断后一定就会寻找isr
	// 而我们可能认为自己用不到这个中断就没有提供isr,这时它自动拿到的就是乱码
	// 则程序很可能跑飞,所以不用的中断一定要关掉。
	// 一般的做法是先全部关掉,然后再逐一打开自己感兴趣的中断。一旦打开就必须
	// 给这个中断提供相应的isr并绑定好。
    VIC0INTENCLEAR = 0xffffffff;
    VIC1INTENCLEAR = 0xffffffff;
    VIC2INTENCLEAR = 0xffffffff;
    VIC3INTENCLEAR = 0xffffffff;

    // 选择中断类型为IRQ
    VIC0INTSELECT = 0x0;
    VIC1INTSELECT = 0x0;
    VIC2INTSELECT = 0x0;
    VIC3INTSELECT = 0x0;

    // 清VICxADDR
    intc_clearvectaddr();
}

// 清除需要处理的中断的中断处理函数的地址
void intc_clearvectaddr(void)
{
    // VICxADDR:当前正在处理的中断的中断处理函数的地址
    VIC0ADDR = 0;
    VIC1ADDR = 0;
    VIC2ADDR = 0;
    VIC3ADDR = 0;
}

中断的使能与禁止

先根据中断号判断这个中断属于VIC几,然后在用中断源减去这个VIC的偏移量,得到这个中断号在本VIC中的偏移量,然后1«x位,写入相应的VIC的INTENABLE/INTENCLEAR寄存器即可。

// 使能中断
// 通过传参的intnum来使能某个具体的中断源,中断号在int.h中定义,是物理中断号
void intc_enable(unsigned long intnum)
{
    unsigned long temp;
	// 确定intnum在哪个寄存器的哪一位
	// <32就是0~31,必然在VIC0
    if(intnum<32)
    {
        // 读改写流程,实际可以直接给相应地址赋0
    	// VIC0INTENABLE = 1<<intnum
        temp = VIC0INTENABLE;
        temp |= (1<<intnum);		// 如果是第一种设计则必须位操作,第二种设计可以直接写。
        VIC0INTENABLE = temp;
    }
    else if(intnum<64)
    {
        temp = VIC1INTENABLE;
        temp |= (1<<(intnum-32));
        VIC1INTENABLE = temp;
    }
    else if(intnum<96)
    {
        temp = VIC2INTENABLE;
        temp |= (1<<(intnum-64));
        VIC2INTENABLE = temp;
    }
    else if(intnum<NUM_ALL)
    {
        temp = VIC3INTENABLE;
        temp |= (1<<(intnum-96));
        VIC3INTENABLE = temp;
    }
    // NUM_ALL : enable all interrupt
    else
    {
        VIC0INTENABLE = 0xFFFFFFFF;
        VIC1INTENABLE = 0xFFFFFFFF;
        VIC2INTENABLE = 0xFFFFFFFF;
        VIC3INTENABLE = 0xFFFFFFFF;
    }

}

// 禁止中断
// 通过传参的intnum来禁止某个具体的中断源,中断号在int.h中定义,是物理中断号
void intc_disable(unsigned long intnum)
{
    unsigned long temp;

    if(intnum<32)
    {
        temp = VIC0INTENCLEAR;
        temp |= (1<<intnum);
        VIC0INTENCLEAR = temp;
    }
    else if(intnum<64)
    {
        temp = VIC1INTENCLEAR;
        temp |= (1<<(intnum-32));
        VIC1INTENCLEAR = temp;
    }
    else if(intnum<96)
    {
        temp = VIC2INTENCLEAR;
        temp |= (1<<(intnum-64));
        VIC2INTENCLEAR = temp;
    }
    else if(intnum<NUM_ALL)
    {
        temp = VIC3INTENCLEAR;
        temp |= (1<<(intnum-96));
        VIC3INTENCLEAR = temp;
    }
    // NUM_ALL : disable all interrupt
    else
    {
        VIC0INTENCLEAR = 0xFFFFFFFF;
        VIC1INTENCLEAR = 0xFFFFFFFF;
        VIC2INTENCLEAR = 0xFFFFFFFF;
        VIC3INTENCLEAR = 0xFFFFFFFF;
    }

    return;
}

绑定自己实现的isr到VICnVECTADDR

VICVECTADDR寄存器一共有4×32个,每个中断源都有一个VECTADDR寄存器,我们应该将自己为这个中断源写的isr地址丢到这个中断源对应的VECTADDR寄存器中即可

// 绑定isr到中断控制器硬件
intc_setvectaddr(KEY_EINT2, isr_eint2);
intc_setvectaddr(KEY_EINT3, isr_eint3);
intc_setvectaddr(KEY_EINT16_19, isr_eint16171819);

// 绑定我们写的isr到VICnVECTADDR寄存器
// 绑定过之后我们就把isr地址交给硬件了,剩下的我们不用管了,硬件自己会处理
// 等发生相应中断的时候,我们直接到相应的VICnADDR中去取isr地址即可。
// 参数:intnum是int.h定义的物理中断号,handler是函数指针,就是我们写的isr
// VIC0VECTADDR定义为VIC0VECTADDR0寄存器的地址,就相当于是VIC0VECTADDR0~31这个
// 数组(这个数组就是一个函数指针数组)的首地址,然后具体计算每一个中断的时候
// 只需要首地址+偏移量即可。
void intc_setvectaddr(unsigned long intnum, void (*handler)(void))
{
    //VIC0
    if(intnum<32)
    {
        *( (volatile unsigned long *)(VIC0VECTADDR + 4*(intnum-0)) ) = (unsigned)handler;
    }
    //VIC1
    else if(intnum<64)
    {
        *( (volatile unsigned long *)(VIC1VECTADDR + 4*(intnum-32)) ) = (unsigned)handler;
    }
    //VIC2
    else if(intnum<96)
    {
        *( (volatile unsigned long *)(VIC2VECTADDR + 4*(intnum-64)) ) = (unsigned)handler;
    }
    //VIC3
    else
    {
        *( (volatile unsigned long *)(VIC3VECTADDR + 4*(intnum-96)) ) = (unsigned)handler;
    }
    return;
}

真正的中断处理程序如何获取isr

当发生中断时,硬件会自动把相应中断源的isr地址从VICnVECTADDR寄存器中推入VICnADDR寄存器中,所以我们第二阶段的第二阶段isr_handler中,只需要到相应的VICnADDR中去拿出isr地址,调用执行即可。

// 真正的中断处理程序。意思就是说这里只考虑中断处理,不考虑保护/恢复现场
void irq_handler(void)
{
	//printf("irq_handler.\n");
	// SoC支持很多个(在低端CPU例如2440中有30多个,在210中有100多个)中断
	// 这么多中断irq在第一个阶段走的是一条路,都会进入到irq_handler来
	// 我们在irq_handler中要去区分究竟是哪个中断发生了,然后再去调用该中断
	// 对应的isr。
	
	
	// 虽然硬件已经自动帮我们把isr放入了VICnADDR中,但是因为有4个,所以我们必须
	// 先去软件的检查出来到底哪个VIC中断了,也就是说isr到底在哪个VICADDR寄存器中
	unsigned long vicaddr[4] = {VIC0ADDR,VIC1ADDR,VIC2ADDR,VIC3ADDR};
    int i=0;
    void (*isr)(void) = NULL;

    for(i=0; i<4; i++)
    {
		// 发生一个中断时,4个VIC中有3个是全0,1个的其中一位不是0
        if(intc_getvicirqstatus(i) != 0)
        {
            isr = (void (*)(void)) vicaddr[i];
            break;
        }
    }
    (*isr)();		// 通过函数指针来调用函数
}

unsigned long intc_getvicirqstatus(unsigned long ucontroller)
{
    if(ucontroller == 0)
        return	VIC0IRQSTATUS;
    else if(ucontroller == 1)
        return 	VIC1IRQSTATUS;
    else if(ucontroller == 2)
        return 	VIC2IRQSTATUS;
    else if(ucontroller == 3)
        return 	VIC3IRQSTATUS;
    else
    {}
    return 0;
}

外部中断

什么是外部中断

SoC支持的中断类型中有一类叫外部中断。内部中断就是指的中断源来自于SoC内部(一般是内部外设),譬如串口、定时器等部件产生的中断;外部中断是SoC外部的设备,通过外部中断对应的GPIO引脚产生的中断。

按键在SoC中就使用外部中断来实现。具体实现方法是:将按键电路接在外部中断的GPIO上,然后将GPIO配置为外部中断模式。此时人通过按按键改变按键电路的电压高低,这个电压高低会触发GPIO对应的外部中断,通过引脚传进去给CPU处理。

电平触发和边沿触发

  • 电平触发就是说GPIO上的电平只要满足条件,就会不停触发中断。电平触发分为高电平触发和低电平触发。电平触发的特点是,只要电平满足条件就会不停触发中断。
  • 边沿触发分为上升沿触发、下降沿触发和双边沿触发三种。边沿触发不关心电平常规状态,只关心电平变化的瞬间(边沿触发不关心电平本身是高还是低,只关心变化是从高到低还是从低到高的这个过程)

如果我们关注的是按键按下和弹起这两个事件本身,那么应该用边沿触发来处理按键;如果我们关心的是按键按下/弹起的那一段时间,那么应该用电平触发。

关键寄存器:CON、PEND、MASK

  • 外部中断的主要配置寄存器有3个:EXT_CON、EXT_PEND、EXT_MASK
  • EXT_CON配置外部中断的触发方式。触发方式就是说这个外部中断产生的条件是什么
  • EXT_PEND寄存器是中断挂起寄存器。
    • 这个寄存器中每一位对应一个外部中断,平时没有中断时值为0。
    • 当发生了中断后,硬件会自动将这个寄存器中该中断对应的位置1,我们去处理完这个中断后应该手动将该位置0
    • 这个PEND寄存器的位就相当于是一个标志,如果发生了中断但是我们暂时忙来不及去处理时,这个位一直是1(这就是挂起),直到我有空了去处理了这个中断才会手工清除(写代码清除)这个挂起位表示这个中断被我处理了。
  • EXT_MASK寄存器就是各个外部中断的使能/禁止开关

分析X210开发板的按键对应的EINT编号:

EINT2、EINT3、EINT16、EINT17、EINT18、EINT19

x210 的外部中断是通过 GPIO 引入的,它的设置在 GPIO 章节的 2.2.55 节。

外部中断的关键寄存器是 CON、PEND 和 MASK 三个

x210 开发板的按键对应的 EINT 编号是:EINT2、EINT3、EINT16、EINT17、EINT18、EINT19

在 P103 页可以查到这几个中断对应的 GPIO 引脚

Pin Name GPIO Func0 Func1 Default PUD
XEINT[2] GPH0[2]     GPI PD
XEINT[3] GPH0[3]     GPI PD
XEINT[16] GPH2[0]   KP_COL[0] GPI PD
XEINT[17] GPH2[1]   KP_COL[1] GPI PD
XEINT[18] GPH2[2]   KP_COL[2] GPI PD
XEINT[19] GPH2[3]   KP_COL[3] GPI PD

GPH0 和 GPH2 的寄存器地址

寄存器 地址 R/W 描述 初始值
GPH0CON 0xE020_0C00 R/W GPH0 的配置寄存器 0x0
GPH0DAT 0xE020_0C04 R/W GPH0 的数据寄存器 0x0
GPH0PUD 0xE020_0C08 R/W GPH0 上拉/下拉寄存器 0x5555
GPH0DRV 0xE020_0C0C R/W GPH0 的驱动强度寄存器 0x0
GPH2CON 0xE020_0C40 R/W GPH2 的配置寄存器 0x0
GPH2DAT 0xE020_0C44 R/W GPH2 的数据寄存器 0x0
GPH2PUD 0xE020_0C48 R/W GPH2 上拉/下拉寄存器 0x5555
GPH2DRV 0xE020_0C4C R/W GPH2 的驱动强度寄存器 0x0

外部中断配置寄存器

寄存器 地址 R/W 描述 初始值
EXT_INT_0_CON 0xE020_0E00 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的配置寄存器 0x0
EXT_INT_2_CON 0xE020_0E08 R/W 外部中断 EXT_INT[16] ~ EXT_INT[23] 的配置寄存器 0x0
EXT_INT_0_FLTCON0 0xE020_0E80 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的滤波配置寄存器1 0x80808080
EXT_INT_0_FLTCON1 0xE020_0E84 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的滤波配置寄存器2 0x80808080
EXT_INT_2_FLTCON0 0xE020_0E90 R/W 外部中断 EXT_INT[16] ~ EXT_INT[23] 的滤波配置寄存器1 0x80808080
EXT_INT_2_FLTCON1 0xE020_0E94 R/W 外部中断 EXT_INT[16] ~ EXT_INT[23] 的滤波配置寄存器2 0x80808080
EXT_INT_0_MASK 0xE020_0F00 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的屏蔽寄存器 0xFF
EXT_INT_2_MASK 0xE020_0F08 R/W 外部中断 EXT_INT[16] ~ EXT_INT[23] 的屏蔽寄存器 0xFF
EXT_INT_0_PEND 0xE020_0F40 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的悬挂寄存器 0x0
EXT_INT_2_PEND 0xE020_0F48 R/W 外部中断 EXT_INT[0] ~ EXT_INT[7] 的悬挂寄存器 0x0

GPH0 配置寄存器

[31:16] [15:12] [11:8] [7:0]
GPH0CON[7:4] GPH0CON[3] GPH0CON[2] GPH0CON[1:0]
0xF = EXT_INT[7:4] 0xF = EXT_INT[3] 0xF = EXT_INT[2] 0xF = EXT_INT[1:0]

GPH2 配置寄存器

[23:16] [15:12] [11:8] [7:4] [3:0]
GPH2CON[7:4] GPH2CON[3] GPH2CON[2] GPH2CON[1] GPH2CON[0]
0xF = EXT_INT[21:20] 1111 = EXT_INT[19] 1111 = EXT_INT[18] 1111 = EXT_INT[17] 1111 = EXT_INT[16]

外部中断控制寄存器 EXT_INT_0_CON

Address =0xE020_0E00

[31:16] [15] [14:12] [11] [10:8]
EXT_INT_0_CON[7:4] Reserved EXT_INT_0_CON[3] Reserved EXT_INT_0_CON[2]
设置 EXT_INT[7:4] 的信号 Reserved 设置 EXT_INT[3] 的信号 Reserved 设置 EXT_INT[2] 的信号
000 = Low level
001 = High level
010 = Falling edge triggered
011 = Rising edge triggered
100 = Both edge triggered
101 ~ 111 = Reserved
  000 = Low level
001 = High level
010 = Falling edge triggered
011 = Rising edge triggered
100 = Both edge triggered
101 ~ 111 = Reserved
  000 = Low level
001 = High level
010 = Falling edge triggered
011 = Rising edge triggered
100 = Both edge triggered
101 ~ 111 = Reserved

外部中断控制寄存器 EXT_INT_2_CON

[31:16] [15] [14:12] [6:4] [2:0]
EXT_INT_2_CON[7:4] EXT_INT_2_CON[3] EXT_INT_2_CON[2] EXT_INT_2_CON[1] EXT_INT_2_CON[0]
EXT_INT[23:20] EXT_INT[19] EXT_INT[18] EXT_INT[17] 设置 EXT_INT[16] 的信号

外部中断滤波控制寄存器

[31] [30] [29:24] [23] [22] [21:16]
FLTEN_0[3] FLTSEL_0[3] FLTWIDTH_0[3] FLTEN_0[2] FLTSEL_0[2] FLTWIDTH_0[2]
EXT_INT[3] 滤波使能 = 1 EXT_INT[3] 滤波选择 EXT_INT[3] 滤波宽度 EXT_INT[2] 滤波使能 = 1 EXT_INT[2] 滤波选择 EXT_INT[3] 滤波宽度
0 = Disables
1 = Enables
0 = Delay filter
1 = Digital filter (clock count)
当 FLTSEL30 =1 时,有效 - - -
[31] [30] [29:24] [7] [6] [5:0]
FLTEN_0[3] FLTSEL_0[3] FLTWIDTH_0[3] FLTEN_0[2] FLTSEL_0[2] FLTWIDTH_0[2]
EXT_INT[19]滤波使能 = 1 EXT_INT[19] 滤波选择 EXT_INT[19] 滤波宽度 EXT_INT[16] 滤波使能 = 1 EXT_INT[16] 滤波选择 EXT_INT[16] 滤波宽度

外部中断屏蔽寄存器 EXT_INT_0_MASK

从外部中断控制寄存器 EXT_INT_2_CON 可以知道,EXT_INT_0_CON 控制 EXT_INT[7:0],EXT_INT_2_CON 控制 EXT_INT[23:16],所以这些屏蔽寄存器的控制范围也就确定了。

addr = 0xE020_0F00

[3] [2]
EXT_INT_0_MASK[3] EXT_INT_0_MASK[2]
控制 EXT_INT[3] 的屏蔽 控制 EXT_INT[2] 的屏蔽
1 = 屏蔽;0 - 打开 1 = 屏蔽;0 - 打开

外部中断屏蔽寄存器 EXT_INT_2_MASK

addr = 0xE020_0F08

[3] [2] [1] [0]
EXT_INT_2_MASK[3] EXT_INT_2_MASK[2] EXT_INT_2_MASK[1] EXT_INT_2_MASK[0]
EXT_INT[19] EXT_INT[18] EXT_INT[17] EXT_INT[16]

外部中断悬挂寄存器 EXT_INT_0_PEND

addr = 0xE020_0F40

[3] [2]
EXT_INT_0_PEND[3] EXT_INT_0_PEND[2]
1 = EXT_INT[3] 发生中断 1 = EXT_INT[2] 发生中断

外部中断悬挂寄存器 EXT_INT_2_PEND

addr = 0xE020_0F48

[3] [2] [1] [0]
EXT_INT_2_PEND[3] EXT_INT_2_PEND[2] EXT_INT_2_PEND[1] EXT_INT_2_PEND[0]
EXT_INT[19] EXT_INT[18] EXT_INT[17] EXT_INT[16]

中断方式处理按键编程实践

  1. 外部中断对应的GPIO模式设置

    // 以中断方式来处理按键的初始化
    void key_init_interrupt(void)
    {
    	// 1. 外部中断对应的GPIO模式设置
    	rGPH0CON |= 0xFF<<8;		// GPH0_2 GPH0_3设置为外部中断模式
    	rGPH2CON |= 0xFFFF<<0;		// GPH2_0123共4个引脚设置为外部中断模式
       	
    	// 2. 中断触发模式设置
    	rEXT_INT_0_CON &= ~(0xFF<<8);	// bit8~bit15全部清零
    	rEXT_INT_0_CON |= ((2<<8)|(2<<12));	// EXT_INT2和EXT_INT3设置为下降沿触发
    	rEXT_INT_2_CON &= ~(0xFFFF<<0);
    	rEXT_INT_2_CON |= ((2<<0)|(2<<4)|(2<<8)|(2<<12));	
       	
    	// 3. 中断允许
    	rEXT_INT_0_MASK &= ~(3<<2);	// 外部中断允许
    	rEXT_INT_2_MASK &= ~(0x0f<<0);
       	
    	// 4. 清挂起,清除是写1,不是写0
    	rEXT_INT_0_PEND |= (3<<2);
    	rEXT_INT_2_PEND |= (0x0F<<0);
    }
    
  2. 中断处理程序isr编写

    // EINT2通道对应的按键,就是GPH0_2引脚对应的按键,就是开发板上标了LEFT的那个按键
    void isr_eint2(void)
    {
    	// 真正的isr应该做2件事情。
    	// 第一,中断处理代码,就是真正干活的代码
    	printf("isr_eint2_LEFT.\n");
    	// 第二,清除中断挂起
    	rEXT_INT_0_PEND |= (1<<2);
    	intc_clearvectaddr();
    }
       
    void isr_eint16171819(void)
    {
    	// 真正的isr应该做2件事情。
    	// 第一,中断处理代码,就是真正干活的代码
    	// 因为EINT16~31是共享中断,所以要在这里再次去区分具体是哪个子中断
    	if (rEXT_INT_2_PEND & (1<<0)) {
    		printf("eint16\n");
    	}
    	if (rEXT_INT_2_PEND & (1<<1)) {
    		printf("eint17\n");
    	}
    	if (rEXT_INT_2_PEND & (1<<2)) {
    		printf("eint18\n");
    	}
    	if (rEXT_INT_2_PEND & (1<<3)) {
    		printf("eint19\n");
    	}
       
    	// 第二,清除中断挂起
    	rEXT_INT_2_PEND |= (0x0f<<0);
    	intc_clearvectaddr();
    }
       
       
    // 清除需要处理的中断的中断处理函数的地址
    void intc_clearvectaddr(void)
    {
        // VICxADDR:当前正在处理的中断的中断处理函数的地址
        VIC0ADDR = 0;
        VIC1ADDR = 0;
        VIC2ADDR = 0;
        VIC3ADDR = 0;
    }
    

    在 《S5PV210_UM_REV1.1》CPU 手册的 P563 页,说明了 16 ~ 31 是共享中断

VIC 端口号 中断号 中断请求 备注
16 16 EINT 16_31 EXT_INT[16] ~ [31]
0 0 EINT0 EXT_INT[0]