一、中断体系介绍
1、什么是中断
- 中断的发明是用来解决宏观上的并行需要的。宏观就是从整体上来看,多件事情都完成了。
- 微观上的并行,就是指的真正的并行,就是精确到每一秒甚至每一刻,多个事情都是在同时进行的。宏观上面的并行并不等于微观的并行,有时候宏观上是并行的,微观上是串行的。
- 例子,一个人在看电影,快递来了暂停电影跑去收快递,收完快递继续回来看电影,这个例子就是宏观上的并行和微观上的串行。例子中一个人等同于 SoC 中 1 个 CPU(也就是单核 CPU),这个 CPU 看电影就不能收快递,收快递就不能看电影(也就是说不能真正的并行)。单核心 CPU 在微观角度是串行的,但是因为 CPU 很快,所以在宏观看来可以并行。
- 上例中大部分时间在看电影,中间少量时间去收快递,那么类比于 CPU 来说,看电影就应该是 CPU 的常规任务,而收快递则应该是中断例程。也就是说 CPU 平时一直在进行看电影任务,等快递来了(中断发生了)快递员(类似于中断源)会打电话叫人去收快递(中断源会触发中断通知CPU去处理中断),人收到电话(CPU收到中断信号)后会暂定电影(CPU保存常规任务的现场)跑去收快递(CPU去执行中断处理程序ISR处理中断),收完快递(执行完ISR)回来继续看电影(CPU恢复常规任务的现场,继续执行常规任务)
- 为什么需要中断?因为单核 CPU 实际无法并行的,但是通过中断机制,可以实现假并行(宏观上的并行,微观上实际还是串行的)。
二、SoC 对中断的实现机制:异常向量表
- 异常向量表是 CPU 中某些特定地址的特定定义。当中断发生的时候,中断要想办法通知 CPU 去处理中断,怎么做到?这就要靠异常向量表。
- 在 CPU 设计时,就事先定义了 CPU 中一些特定地址作为特定异常的入口地址(譬如定义0x00000000 地址为复位异常向量地址,则发生复位异常时,CPU 会自动跳转到 0x00000000 地址去执行指令。又譬如外部中断对应的异常向量地址为 0x30000008,则发生外部中断后,CPU 会硬件自动跳转到 0x30000008地址去执行指令。)
- 以上讲的是 CPU 硬件设计时对异常向量表的支持,下来就需要软件支持了。硬件已经决定了发生什么异常 CPU 自动跳转 PC 到哪个地址去执行,软件需要做的就是把处理这个异常的代码的首地址填入这个异常向量地址。
三、S5PV210 的异常向量表
-
异常向量表在1.2.14节讲过,可以返回去看一下。
-
异常向量表中各个向量的相对位置是固定的,但是他们的起始地址是不固定的,各种 SoC 可以不一样,而且复杂 ARM 中还可以让用户来软件设置这个异常向量表的基地址。
-
扩展到所有架构的 CPU 中:所有架构(譬如51单片机、PIC单片机)的 CPU 实现中断都是通过异常向量表实现的,这个机制是不变的;但是不同 CPU 异常向量表的构造和位置是不同的。
四、异常和中断的区别和联系
- 针对 SoC 来说,发生复位、软中断、中断、快速中断、取指令异常、数据异常等,我们都统一叫异常。所以说:中断其实是异常的一种。
- 异常的定义就是突发事件,打断了 CPU 的正常常规业务,CPU 不得不跳转到异常向量表中去执行异常处理程序;中断是异常的一种,一般特指 SoC 内的内部外设产生的打断 SoC 常规业务,或者外部中断(SoC的GPIO引脚传回来的中断)。
五、异常向量表的编程处理
1、像内存一样去访问异常向量表
- S5PV210 的异常向量表可以改变(在 CP15 协处理器中),以适应操作系统的需求。但是目前系统刚启动时,此时 DRAM 尚未初始化,程序都在 SRAM 中运行。210 在 iRAM 中设置了异常向量表,供暂时性使用。
- 查 210 的 iROM application note 文档中 iRAM 的地址分配,可知,iRAM 中的异常向量表起始地址为 0xD003,7400。知道了异常向量表的起始地址后,各个异常对应的入口就很好知道了。
/*------------------------ int.h ----------------------*/
#define exception_vector_table_base 0xD0037400
#define exception_reset (exception_vector_table_base + 0x00)
#define exception_undef (exception_vector_table_base + 0x04)
#define exception_sotf_int (exception_vector_table_base + 0x08)
#define exception_prefetch (exception_vector_table_base + 0x0C)
#define exception_data (exception_vector_table_base + 0x10)
#define exception_irq (exception_vector_table_base + 0x18)
#define exception_fiq (exception_vector_table_base + 0x1C)
#define r_exception_reset (*(volatile unsigned int *)exception_reset)
#define r_exception_undef (*(volatile unsigned int *)exception_undef)
#define r_exception_sotf_int (*(volatile unsigned int *)exception_sotf_int)
#define r_exception_prefetch (*(volatile unsigned int *)exception_prefetch)
#define r_exception_data (*(volatile unsigned int *)exception_data)
#define r_exception_irq (*(volatile unsigned int *)exception_irq)
#define r_exception_fiq (*(volatile unsigned int *)exception_fiq)
/*------------------------ int.c ----------------------*/
#include "int.h"
#include "stdio.h"
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;
}
void reset_exception(void)
{
printf("reset_exception.\n");
}
void undef_exception(void)
{
printf("undef_exception.\n");
}
void sotf_int_exception(void)
{
printf("sotf_int_exception.\n");
}
void prefetch_exception(void)
{
printf("prefetch_exception.\n");
}
void data_exception(void)
{
printf("data_exception.\n");
}
// 真正的中断处理程序。意思就是说这里只考虑中断处理,不考虑保护/恢复现场
void irq_handler(void)
{
//printf("irq_handler.\n");
// SoC支持很多个(在低端CPU例如2440中有30多个,在210中有100多个)中断
// 这么多中断irq在第一个阶段走的是一条路,都会进入到irq_handler来
// 我们在irq_handler中要去区分究竟是哪个中断发生了,然后再去调用该中断
// 对应的isr。
}
2、函数名的实质就是函数的首地址
- 函数名在C语言中的理解方法和变量名其实没区别。编译器会把这个函数的函数体对应的代码段和这个函数的函数名(实质是符号)对应起来,等我们在使用这个函数名符号时,编译器会将函数的函数体实际上做替换。因为函数体都不止4字节,而函数名这个符号只能对应1个地址,所以实际对应的是函数体那一个代码段的首地址。
- 拿 C 语言中的语法来讲,函数名就是这个函数的函数指针。
总结:当我们将异常处理程序的首地址和异常向量表绑定起来后,异常处理初步阶段就完成了。到目前可以保证相应异常发生后,硬件自动跳转到对应异常向量表入口去执行时,可以执行到我们事先绑定的函数。
3、为什么中断处理要先在汇编中进行
- 中断处理要注意保护现场(中断从 SVC 模式来,则保存 SVC 模式下的必要寄存器的值)和恢复现场(中断处理完成后,准备返回 SVC 模式前,要将保存的 SVC 模式下的必要寄存器的值恢复回去,不然到了 SVC 模式后寄存器的值乱了,SVC 模式下原来正在进行的常规任务就被你搞坏了)。
- 保存现场包括:第一:设置 IRQ 栈;第二,保存 LR;第三,保存 R0~R12。
- 为什么要保存 LR 寄存器?要考虑中断返回的问题。中断 ISR 执行完后,如何返回 SVC 模式下,去接着执行原来的代码。中断返回其实取决于我们进入中断时如何保存现场。中断返回时关键的 2 个寄存器就是 PC 和 CPSR。所以我们在进入 IRQ 模式时,应该将 SVC 模式下的下一句指令的地址(中断返回地址)和 CPSR 保存起来,将来恢复时才可以将中断返回地址给 PC,将保存的 CPSR 给 CPSR。
- 中断返回地址就保存在 LR 中,而 CPSR(自动)保存在(IRQ模式下的)SPSR中。
/******************************* start.S ****************************************/
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
#define IRQ_STACK 0xd0037f80
.global _start
.global IRQ_handle
// 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:初始化时钟
bl clock_init
// 第3步:设置SVC栈
ldr sp, =SVC_STACK
// 第4步:开/关icache
mrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中
//bic r0, r0, #(1<<12) // bit12 置0 关icache
orr r0, r0, #(1<<12) // bit12 置1 开icache
mcr p15,0,r0,c1,c0,0;
bl main
// 从这里之后就可以开始调用C程序了
//bl led_blink // led_blink是C语言实现的一个函数
// 汇编最后的这个死循环不能丢
b .
// 在这个汇编函数中,用来做中断模式下的现场保护和恢复,并且调用真正的中断处理程序
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}^
4、汇编保存现场和恢复现场
(1) 保护现场关键是保存:中断处理程序的返回地址,r0-r12(cpsr是自动保存的)
(2) 恢复现场主要是恢复:r0-r12,pc,cpsr。
六、S5PV210 的向量中断控制器
1、异常处理的 2 个阶段
- 可以将异常处理分为 2 个阶段来理解。第一个阶段是异常向量表跳转;第二个阶段就是进入了真正的异常处理程序
irq_handler
之后的部分。
2、回顾:中断处理的第一阶段(异常向量表阶段)处理。
- 第一个阶段之所以能够进行,主要依赖于 CPU 设计时提供的异常向量表机制。第一个阶段的主要任务是从异常发生到响应异常并且保存/恢复现场、跳转到真正的异常处理程序处。
- 第二个阶段的目的是识别多个中断源中究竟哪一个发生了中断,然后调用相应的中断处理程序来处理这个中断。
3、S3C2440 的第二阶段处理过程
- 第一个问题,怎么找到具体是哪个中断:S3C2440 的中断控制器中有一个寄存器(32位的),寄存器的每一个位对应一个中断源(为了解决支持更多中断源,2440 又设计了一个子中断机制。在一级中断寄存器中有一些中断是共用的一个 bit 位,譬如 AC97 和 WDT。对于共用中断,用子中断来区分究竟是哪一个发生了中断)。
- 第二个问题,怎么找到对应的 isr 的问题:首先给每个中断做了个编号,进入 isr_handler 之后先通过查阅中断源寄存器和子中断寄存器(中哪一位为1)确定中断的编号,然后用这个编号去 isr 数组(isr 数组是中断初始化时事先设定好的,就是把各个中断的 isr 的函数名组成一个数组,用中断对应的编号作为索引来查询这个数组)中查阅得到 isr 地址。
评价:2440 的中断处理设计不是特别优秀:第一个过程中使用子中断搞成 2 级的很麻烦;第二个过程中计算中断编号是个麻烦事,很耗费时间。而中断处理的时间是很宝贵的(系统有一个性能指标,叫实时性。实时性就是中断发生到响应的时间,这个时间越短越好。)
4、S5PV210 的第二阶段处理过程
- 第一个问题,怎么找到具体是哪个中断:S5PV210 中因为支持的中断源很多,所以直接设计了4个中断寄存器,每个32位,每位对应一个中断源。(理论上 210 最多支持128个中断,实际支持不足128个,有些位是空的);210 没有子中断寄存器,每个中断源都是并列的。当中断发生时,在 irq_handler 中依次去查询 4 个中断源寄存器,看哪一个的哪一位被置 1,则这个位对应的寄存器就发生了中断,即找到了中断编号。
- 第二个问题,怎么找到对应的 isr 的问题:210 中支持的中断源多了很多,如果还使用2440的那一套来寻找isr地址就太慢了,太影响实时性了。于是210开拓了一种全新的寻找 isr 的机制。210 提供了很多寄存器来解决每个中断源对应isr的寻找问题,具体寻找过程和建立过程见下节,实现的效果是当发生相应中断时,硬件会自动的将相应 isr 推入一定的寄存器中,我们软件只要去这个寄存器中执行函数就行了。
5、总结:第一阶段都相同,第二阶段各不同
- 第一阶段(异常向量表阶段)2440和210几乎是完全相同的。实际上几乎所有的CPU在第一阶段都是相同的。
- 第二阶段就彼此不同了。各个SoC根据自己对实时性的要求,和支持的中断源的多少,各自发明了各自处理中断,找到中断编号,进一步找到对应isr地址的方式。
七、S5PV210 中断处理的主要寄存器
1、VICnINTENABLE 和 VICnINTENCLEAR
- VICnINTENABLE 对应 interrupt enable,INTENCLEAR 对应 interrupt enable clear。
- INTENABLE 寄存器负责相应的中断的使能,INTENCLEAR 寄存器负责相应的中断的禁止。
- 当我们想使能(意思就是启用这个中断,意思就是当硬件产生中断时 CPU 能接收的到)某个中断时,只要在这个中断编号对应的 VICnINTENABLE 的相应 bit 位写 1 即可(注意这个位写 1 其他位写 0 对其他位没有影响);如果我们想禁止某个中断源时,只要向 VICnINTENCLEAR 中相应的 bit 位写 1 即可。
注意:这里的设计一共有 2 种:有些 CPU 是中断使能和禁止是一个寄存器位,写1就使能,写 0就禁止(或者反过来写 1 就禁止,写 0 就使能),这样的中断使能设计就要非常小心,要使用我们之前说过的读改写三部曲来操作;另一种就是使能和禁止分开为 2 个寄存器,要使能就写使能寄存器,要禁止就写禁止寄存器。这样的好处是我们使能/禁止操作时不需要读改写,直接写即可。
2、VICnINTSELECT
- 设置各个中断的模式为 irq 还是 fiq。一般都设置成 irq。
- IRQ 和 FIQ 究竟有何区别。210中支持 2 种中断,irq 和 fiq。irq 是普通中断,fiq 是快速中断。快速中断提供一种更快响应处理的中断通道,用于对实时性要求很高的中断源。fiq 在 CPU 设计时预先提供了一些机制保证 fiq 可以被快速处理,从而保证实时性。fiq 的限制就是只能有一个中断源被设置为 fiq,其他都是 irq。
- CPU 如何保证 fiq 比 irq 快?有 2 个原因:第一,fiq 模式有专用的 r8~r12,因此在 fiq 的 isr 中可以直接使用 r8-r12 而不用保存,这就能节省时间;第二,异常向量表中 fiq 是最后一个异常向量入口。因此 fiq 的 isr 不需要跳转,可以直接写在原地,这样就比其他异常少跳转一次,省了些时间。
3、VICnIRQSTATUS 和 VICnFIQSTATUS
- 中断状态寄存器,是只读的。当发生了中断时,硬件会自动将该寄存器的对应位置为 1,表示中断发生了。软件在处理中断第二阶段的第一阶段,就是靠查询这个寄存器来得到中断编号的。
4、VICnVECTPRIORITY0~VICnVECTPRIORITY31
中断优先级设置寄存器,设置多个中断同时发生时先处理谁后处理谁的问题。一般来说高优先级的中断可以打断低优先级的中断,从而嵌套处理中断。当然了,有些硬件/软件可以设置不支持中断嵌套。
5、VICnVECTADDR0~VICnVECTADDR31、VICnADDR
- 这三个寄存器和 210 中断处理第二阶段的第二阶段有关。
- VICnVECTADDR0 到 31 这 32 个寄存器分别用来存放真正的各个中断对应的 isr 的函数地址。相当于每一个中断源都有一个 VECTADDR 寄存器,程序员在设置中断的时候,把这个中断的 isr 地址直接放入这个中断对应的 VECTADDR 寄存器即可。
- VICnADDR 这个寄存器是只需要读的,它里面的内容是由硬件自动设置的。当发生了相应中断时,硬件会自动识别中断编号,并且会自动找到这个中断的 VECTADDR 寄存器,然后将其读出复制到 VICnADDR 中,供我们使用。这样的设计避免了软件查找中断源和 isr,节省了时间,提高了 210 的中断响应速度。
八、 S5PV210 中断处理的编程实践
1、中断控制器初始化
主要工作有:第一阶段绑定异常向量表到异常处理程序;禁止所有中断源;选择所有中断类型为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_clearvectaddr(void)
{
// VICxADDR:当前正在处理的中断的中断处理函数的地址
VIC0ADDR = 0;
VIC1ADDR = 0;
VIC2ADDR = 0;
VIC3ADDR = 0;
}
// 初始化中断控制器
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();
}
2、中断的使能与禁止
思路是先根据中断号判断这个中断属于 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)
{
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;
}
3、绑定自己实现的 isr 到 VICnVECTADDR
(1)搞清楚 2 个寄存器的区别:VICnVECTADDR 和 VICnADDR.
(2) VICVECTADDR 寄存器一共有4×32个,每个中断源都有一个 VECTADDR 寄存器,我们应该将自己为这个中断源写的 isr 地址丢到这个中断源对应的 VECTADDR 寄存器中即可。
// 绑定我们写的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;
}
4、真正的中断处理程序如何获取 isr
(1) 当发生中断时,硬件会自动把相应中断源的 isr 地址从 VICnVECTADDR寄存器中推入VICnADDR 寄存器中,所以我们第二阶段的第二阶段 isr_handler 中,只需要到相应的VICnADDR 中去拿出 isr 地址,调用执行即可。
// 通过读取VICnIRQSTATUS寄存器,判断其中哪个有一位为1,来得知哪个VIC发生中断了
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;
}
// 真正的中断处理程序。意思就是说这里只考虑中断处理,不考虑保护/恢复现场
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)(); // 通过函数指针来调用函数
}
总结:第3步绑定 isr 地址到 VICnVECTADDR 和 第4步中断发生时第二阶段的第二阶段如何获取isr地址,这两步是相关的。这两个的结合技术,就是我们一直在说的 210 的硬件自动寻找 isr 的机制。
整个中断的流程梳理:
整个中断的工作分为2部分:
第一部分是我们为中断响应而做的预备工作:
1. 初始化中断控制器
2. 绑定写好的isr到中断控制器
3. 相应中断的所有条件使能
第二部分是当硬件产生中断后如何自动执行isr:
1. 第一步,经过异常向量表跳转入IRQ/FIQ的入口
2. 第二步,做中断现场保护(在start.S中),然后跳入isr_handler
3. 第三步,在isr_handler中先去搞清楚是哪个VIC中断了,然后直接去这个VIC的ADDR
寄存器中取isr来执行即可。
4. 第四步,isr执行完,中断现场恢复,直接返回继续做常规任务。
九、外部中断
1、什么是外部中断?数据手册在哪里?
- SoC 支持的中断类型中,有一类叫外部中断。内部中断就是指的中断源来自于 SoC 内部(一般是内部外设),譬如串口、定时器等部件产生的中断;外部中断是 SoC 外部的设备,通过外部中断对应的 GPIO 引脚产生的中断。
- 按键在 SoC 中就使用外部中断来实现。具体实现方法是:将按键电路接在外部中断的 GPIO 上,然后将 GPIO 配置为外部中断模式。此时人通过按按键改变按键电路的电压高低,这个电压高低会触发 GPIO 对应的外部中断,通过引脚传进去给 CPU 处理。
- 外部中断相关的介绍和寄存器都在 2.2.6 章节(属于 GPIO 部分)
2、电平触发和边沿触发
- 外部中断的触发模式主要有 2 种:电平触发和边沿触发。
(1) 电平触发就是说,GPIO 上的电平只要满足条件,就会不停触发中断。电平触发分为高电平触发和低电平触发。电平触发的特点是,只要电平满足条件就会不停触发中断。
(2) 边沿触发分为上升沿触发、下降沿触发和双边沿触发三种。边沿触发不关心电平的常规状态,只关心电平变化的瞬间(边沿触发不关心电平本身是高还是低,只关心变化是从高到低还是从低到高的这个过程)。
分析按键的工作:如果我们关注的是按键按下和弹起这两个事件本身,那么应该用边沿触发来处理按键;如果我们关心的是按键按下/弹起的那一段时间,那么应该用电平触发。
3、关键寄存器:CON、PEND、MASK
- 外部中断的主要配置寄存器有 3 个:EXT_CON、EXT_PEND、EXT_MASK。
- EXT_CON 配置外部中断的触发方式。触发方式就是说外部电平怎么变化就能触发中断,也就是说这个外部中断产生的条件是什么。
-
EXT_PEND 寄存器是中断挂起寄存器。这个寄存器中每一 bit 对应一个外部中断,平时没有中断时值为0。当发生了中断后,硬件会自动将这个寄存器中该中断对应的 bit 置1,我们去处理完这个中断后应该手工将该位置0。这个 PEND 寄存器的位就相当于是一个标志,如果发生了中断但是我们暂时忙来不及去处理时,这个位一直是1(这就是挂起),直到我有空了去处理了这个中断才会手工清除(写代码清除)这个挂起位表示这个中断被我处理了。
-
EXT_MASK 寄存器就是各个外部中断的使能/禁止开关。
分析 X210 开发板的按键对应的 EINT 编号:
EINT2、EINT3、EINT16、EINT17、EINT18、EINT19。
十、中断方式处理按键编程实践
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_eint3(void)
{
// 真正的isr应该做2件事情。
// 第一,中断处理代码,就是真正干活的代码
printf("isr_eint3_DOWN.\n");
// 第二,清除中断挂起
rEXT_INT_0_PEND |= (1<<3);
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();
}
/************************************************************************/
#include "stdio.h"
#include "int.h"
#include "main.h"
void uart_init(void);
#define KEY_EINT2 NUM_EINT2 // left
#define KEY_EINT3 NUM_EINT3 // down
#define KEY_EINT16_19 NUM_EINT16_31 // 其余4个共用的
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
uart_init();
//key_init();
key_init_interrupt();
// 如果程序中要使用中断,就要调用中断初始化来初步初始化中断控制器
system_init_exception();
printf("-------------key interrypt test--------------");
// 绑定isr到中断控制器硬件
intc_setvectaddr(KEY_EINT2, isr_eint2);
intc_setvectaddr(KEY_EINT3, isr_eint3);
intc_setvectaddr(KEY_EINT16_19, isr_eint16171819);
// 使能中断
intc_enable(KEY_EINT2);
intc_enable(KEY_EINT3);
intc_enable(KEY_EINT16_19);
// 在这里加个心跳
while (1)
{
printf("A ");
delay(10000);
}
return 0;
}
源自朱有鹏老师.