ARM S5PV210 中断体系与外部中断实战

news2024/9/26 3:30:34

一、中断体系介绍

1、什么是中断

  1. 中断的发明是用来解决宏观上的并行需要的。宏观就是从整体上来看,多件事情都完成了。
  2. 微观上的并行,就是指的真正的并行,就是精确到每一秒甚至每一刻,多个事情都是在同时进行的。宏观上面的并行并不等于微观的并行,有时候宏观上是并行的,微观上是串行的。
  3. 例子,一个人在看电影,快递来了暂停电影跑去收快递,收完快递继续回来看电影,这个例子就是宏观上的并行和微观上的串行。例子中一个人等同于 SoC 中 1 个 CPU(也就是单核 CPU),这个 CPU 看电影就不能收快递,收快递就不能看电影(也就是说不能真正的并行)。单核心 CPU 在微观角度是串行的,但是因为 CPU 很快,所以在宏观看来可以并行。
  4. 上例中大部分时间在看电影,中间少量时间去收快递,那么类比于 CPU 来说,看电影就应该是 CPU 的常规任务,而收快递则应该是中断例程。也就是说 CPU 平时一直在进行看电影任务,等快递来了(中断发生了)快递员(类似于中断源)会打电话叫人去收快递(中断源会触发中断通知CPU去处理中断),人收到电话(CPU收到中断信号)后会暂定电影(CPU保存常规任务的现场)跑去收快递(CPU去执行中断处理程序ISR处理中断),收完快递(执行完ISR)回来继续看电影(CPU恢复常规任务的现场,继续执行常规任务)
  5. 为什么需要中断?因为单核 CPU 实际无法并行的,但是通过中断机制,可以实现假并行(宏观上的并行,微观上实际还是串行的)。

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

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

三、S5PV210 的异常向量表

  1. 异常向量表在1.2.14节讲过,可以返回去看一下。
    在这里插入图片描述

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

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


四、异常和中断的区别和联系

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

五、异常向量表的编程处理

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

  1. S5PV210 的异常向量表可以改变(在 CP15 协处理器中),以适应操作系统的需求。但是目前系统刚启动时,此时 DRAM 尚未初始化,程序都在 SRAM 中运行。210 在 iRAM 中设置了异常向量表,供暂时性使用。
  2. 查 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、函数名的实质就是函数的首地址

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

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


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

  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中。

/******************************* 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 个阶段

  1. 可以将异常处理分为 2 个阶段来理解。第一个阶段是异常向量表跳转;第二个阶段就是进入了真正的异常处理程序 irq_handler 之后的部分。

2、回顾:中断处理的第一阶段(异常向量表阶段)处理。

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

3、S3C2440 的第二阶段处理过程

  1. 第一个问题,怎么找到具体是哪个中断:S3C2440 的中断控制器中有一个寄存器(32位的),寄存器的每一个位对应一个中断源(为了解决支持更多中断源,2440 又设计了一个子中断机制。在一级中断寄存器中有一些中断是共用的一个 bit 位,譬如 AC97 和 WDT。对于共用中断,用子中断来区分究竟是哪一个发生了中断)。
  2. 第二个问题,怎么找到对应的 isr 的问题:首先给每个中断做了个编号,进入 isr_handler 之后先通过查阅中断源寄存器和子中断寄存器(中哪一位为1)确定中断的编号,然后用这个编号去 isr 数组(isr 数组是中断初始化时事先设定好的,就是把各个中断的 isr 的函数名组成一个数组,用中断对应的编号作为索引来查询这个数组)中查阅得到 isr 地址。

评价:2440 的中断处理设计不是特别优秀:第一个过程中使用子中断搞成 2 级的很麻烦;第二个过程中计算中断编号是个麻烦事,很耗费时间。而中断处理的时间是很宝贵的(系统有一个性能指标,叫实时性。实时性就是中断发生到响应的时间,这个时间越短越好。)


4、S5PV210 的第二阶段处理过程

  1. 第一个问题,怎么找到具体是哪个中断:S5PV210 中因为支持的中断源很多,所以直接设计了4个中断寄存器,每个32位,每位对应一个中断源。(理论上 210 最多支持128个中断,实际支持不足128个,有些位是空的);210 没有子中断寄存器,每个中断源都是并列的。当中断发生时,在 irq_handler 中依次去查询 4 个中断源寄存器,看哪一个的哪一位被置 1,则这个位对应的寄存器就发生了中断,即找到了中断编号。
  2. 第二个问题,怎么找到对应的 isr 的问题:210 中支持的中断源多了很多,如果还使用2440的那一套来寻找isr地址就太慢了,太影响实时性了。于是210开拓了一种全新的寻找 isr 的机制。210 提供了很多寄存器来解决每个中断源对应isr的寻找问题,具体寻找过程和建立过程见下节,实现的效果是当发生相应中断时,硬件会自动的将相应 isr 推入一定的寄存器中,我们软件只要去这个寄存器中执行函数就行了。

5、总结:第一阶段都相同,第二阶段各不同

  1. 第一阶段(异常向量表阶段)2440和210几乎是完全相同的。实际上几乎所有的CPU在第一阶段都是相同的。
  2. 第二阶段就彼此不同了。各个SoC根据自己对实时性的要求,和支持的中断源的多少,各自发明了各自处理中断,找到中断编号,进一步找到对应isr地址的方式。

七、S5PV210 中断处理的主要寄存器

1、VICnINTENABLE 和 VICnINTENCLEAR

  1. VICnINTENABLE 对应 interrupt enable,INTENCLEAR 对应 interrupt enable clear。
  2. INTENABLE 寄存器负责相应的中断的使能,INTENCLEAR 寄存器负责相应的中断的禁止。
  3. 当我们想使能(意思就是启用这个中断,意思就是当硬件产生中断时 CPU 能接收的到)某个中断时,只要在这个中断编号对应的 VICnINTENABLE 的相应 bit 位写 1 即可(注意这个位写 1 其他位写 0 对其他位没有影响);如果我们想禁止某个中断源时,只要向 VICnINTENCLEAR 中相应的 bit 位写 1 即可。
    注意:这里的设计一共有 2 种:有些 CPU 是中断使能和禁止是一个寄存器位,写1就使能,写 0就禁止(或者反过来写 1 就禁止,写 0 就使能),这样的中断使能设计就要非常小心,要使用我们之前说过的读改写三部曲来操作;另一种就是使能和禁止分开为 2 个寄存器,要使能就写使能寄存器,要禁止就写禁止寄存器。这样的好处是我们使能/禁止操作时不需要读改写,直接写即可。

2、VICnINTSELECT

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

在这里插入图片描述


3、VICnIRQSTATUS 和 VICnFIQSTATUS

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

4、VICnVECTPRIORITY0~VICnVECTPRIORITY31

在这里插入图片描述

在这里插入图片描述

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


5、VICnVECTADDR0~VICnVECTADDR31、VICnADDR

在这里插入图片描述

在这里插入图片描述

  1. 这三个寄存器和 210 中断处理第二阶段的第二阶段有关。
  2. VICnVECTADDR0 到 31 这 32 个寄存器分别用来存放真正的各个中断对应的 isr 的函数地址。相当于每一个中断源都有一个 VECTADDR 寄存器,程序员在设置中断的时候,把这个中断的 isr 地址直接放入这个中断对应的 VECTADDR 寄存器即可。
  3. 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、什么是外部中断?数据手册在哪里?

  1. SoC 支持的中断类型中,有一类叫外部中断。内部中断就是指的中断源来自于 SoC 内部(一般是内部外设),譬如串口、定时器等部件产生的中断;外部中断是 SoC 外部的设备,通过外部中断对应的 GPIO 引脚产生的中断。
  2. 按键在 SoC 中就使用外部中断来实现。具体实现方法是:将按键电路接在外部中断的 GPIO 上,然后将 GPIO 配置为外部中断模式。此时人通过按按键改变按键电路的电压高低,这个电压高低会触发 GPIO 对应的外部中断,通过引脚传进去给 CPU 处理。
  3. 外部中断相关的介绍和寄存器都在 2.2.6 章节(属于 GPIO 部分)
    在这里插入图片描述

2、电平触发和边沿触发

  1. 外部中断的触发模式主要有 2 种:电平触发和边沿触发。
    在这里插入图片描述

(1) 电平触发就是说,GPIO 上的电平只要满足条件,就会不停触发中断。电平触发分为高电平触发和低电平触发。电平触发的特点是,只要电平满足条件就会不停触发中断。
(2) 边沿触发分为上升沿触发、下降沿触发和双边沿触发三种。边沿触发不关心电平的常规状态,只关心电平变化的瞬间(边沿触发不关心电平本身是高还是低,只关心变化是从高到低还是从低到高的这个过程)。
分析按键的工作:如果我们关注的是按键按下和弹起这两个事件本身,那么应该用边沿触发来处理按键;如果我们关心的是按键按下/弹起的那一段时间,那么应该用电平触发。


3、关键寄存器:CON、PEND、MASK

  1. 外部中断的主要配置寄存器有 3 个:EXT_CON、EXT_PEND、EXT_MASK。
  2. EXT_CON 配置外部中断的触发方式。触发方式就是说外部电平怎么变化就能触发中断,也就是说这个外部中断产生的条件是什么。

在这里插入图片描述

  1. EXT_PEND 寄存器是中断挂起寄存器。这个寄存器中每一 bit 对应一个外部中断,平时没有中断时值为0。当发生了中断后,硬件会自动将这个寄存器中该中断对应的 bit 置1,我们去处理完这个中断后应该手工将该位置0。这个 PEND 寄存器的位就相当于是一个标志,如果发生了中断但是我们暂时忙来不及去处理时,这个位一直是1(这就是挂起),直到我有空了去处理了这个中断才会手工清除(写代码清除)这个挂起位表示这个中断被我处理了。在这里插入图片描述

  2. 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;
}

源自朱有鹏老师.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/140106.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OQC与Cyxtera将量子计算应用于数据中心

牛津量子电路&#xff08;OQC&#xff09;正与云服务公司Cyxtera合作&#xff0c;在数据中心安装量子计算机&#xff0c;方便用户在其本地数据集上运行量子算法。 在葡萄牙里本举行的网络峰会上&#xff0c;牛津量子电路&#xff08;OQC&#xff09;首席执行官ILana Wisby谈到了…

【力扣经典题目】环形链表,判断链表是否有环

题目描述&#xff1a; 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链…

【Unity】新手初学Animation实现人物移动

【Unity】新手初学Animation实现人物移动 需求&#xff1a;开发影院系统&#xff0c;希望加入Avatar人物&#xff0c;在其中行走和坐下 环境&#xff1a;Unity2021.3 新手初学Animation教程&#xff1a;BV1BW41187fL Avatar人物模型资源&#xff1a;学长网盘链接分享 Animation…

【电动汽车充电站有序充电调度的分散式优化】基于蒙特卡诺和拉格朗日的电动汽车优化调度(分时电价调度)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

实习------SpringMVC 框架学习

Spring MVC 是什么 &#xff08;了解&#xff09;Spring MVC&#xff08;全称 Spring Web MVC&#xff09;是 Spring 框架提供的一款基于 MVC 模式的轻量级 Web 开发框架&#xff0c;是 Spring 为表示层&#xff08;UI&#xff09;开发提供的一整套完备的解决方案。注&#xff…

《Linux运维总结:使用旧版redis-shake2.x进行redis集群间的数据同步【方案二】》

一、redis-shake简介 redis-shake是阿里云开源的用于Redis数据迁移和过滤的工具。 Github参考&#xff1a;redis-shake简介 1.1、迁移工具对比 redis-migrate-tool redis-migrate-tool是唯品会开源的一款Redis异构集群之间的数据实时迁移工具&#xff0c;不过已经有两年没有更…

如何对【javaSE】语法阶段复习

下面是我对学习java阶段的总复习&#xff0c;我愿称之为【复习宝典】 如果你对java的部分语法阶段的知识有所困惑&#xff0c;进来看看吧&#xff01; 文章目录 目录 文章目录 一、初始java 1.1java 的由来 1.2JDK安装 1.3main方法的介绍 二、数据类型和变量 2.1数据类型 三、运…

xxljob 里面 InheritableThreadLocal详解,XxlJobContext类的详解

目录 1 需求2 XxlJobContext类3 InheritableThreadLocal 类可以做什么1 需求 在xxljob 里面,有一个地方使用到了InheritableThreadLocal类 我们先说XxlJobContext类是干什么的,里面有什么东西 2 XxlJobContext类 这个类就是一个实体类,可以理解为实体类,里面有属性,有g…

Spring Boot读取配置文件内容的三种方式

系列文章目录 Spring Boot[概述、功能、快速入门]_心态还需努力呀的博客-CSDN博客 该系列文章持续更新中~ 目录 系列文章目录 前言 一、Value 二、Environment 2.1 注入对象 2.2 调用获取属性的方法 2.3 上述两种方法对比 三、ConfigurationProperties 3.1 创建一个实…

前端工程师leetcode算法面试必备-简单的二叉树

一、前言 本难度的题目主要考察二叉树的基本概念和操作。 1、基本概念 树是计算机科学中经常用到的一种非线性数据结构&#xff0c;以分层的形式存储数据。二叉树是一种特殊的树结构&#xff0c;每个节点最多有两个子树&#xff0c;通常子树被称作“左子树”和“右子树”。 …

在结构异质的云杉林中使用机载激光扫描仪数据进行单树分割

论文题目&#xff1a;Single Tree Segmentation Using Airborne Laser Scanner Data in a Structurally Heterogeneous Spruce Forest Abstract 在这项研究中&#xff0c;我们提出了一种基于机载激光扫描的树冠表面模型 (CSM) 及其相应点云的单树分割和表征的新方法。该方法包…

AI算法工程师 | 09机器学习-概率图模型(一)概率图模型概述

目录机器学习 - 概率图模型 之 概率图模型概述1、概率图模型学习的意义2、有向图和无向图3、生成式模型与判别式模型机器学习 - 概率图模型 之 概率图模型概述 本阶段将开启 概率图模型系列 的旅程。 1、概率图模型学习的意义 从自然语言处理 NLP 的角度看&#xff1a; 在自…

前端性能优化(四):资源优化

目录 一&#xff1a;资源的压缩与合并 1.1.为什么要压缩&合并 1.2.HTML压缩&#xff1a; 1.3.CSS压缩&#xff1a; 1.4.JS 压缩与混淆&#xff1a; 1.5.CSS JS 文件合并&#xff1a; 二&#xff1a;图片格式优化 2.1.JPEG/JPG&#xff1a; 2.2.PNG&#xff1a; 2…

linux之终端里sqlite数据库的使用

一. linux下安装数据库和创建一个数据库 1.安装命令&#xff1a; (1)sudo apt-get install sqlite (2) sudo apt-get install libsqlite3-dev 2.安装完后,创建一个数据库&#xff0c;终端下输入命令 【sqlite3 数据库名字 】数据库名字以.db 结尾格式 创建数据库student.db 【 …

node.js--vm沙箱逃逸初探

前言 前几天遇到一个考察vm沙箱逃逸的题目&#xff0c;由于这个点是第一次听说&#xff0c;所以就花时间了解了解什么是沙箱逃逸。此篇文章是对于自己初学vm沙箱逃逸的学习记录&#xff0c;若记录知识有误&#xff0c;欢迎师傅们指正。 什么是沙箱 就只针对于node.js而言&am…

有关于huggingface tokenizer的text_pair

tokenizer有一个名为text pair的参数&#xff0c;这个参数在做NLI任务的时候很有用&#xff0c;因为输入不是一个single sentence&#xff0c;而是sentence pair。 但是这个参数的类型让人非常confused&#xff0c;而且同时还有一个text参数&#xff0c;让人不知道传入的sente…

Java-集合(2)

List集合 List接口是Collection接口的子接口 List集合类的元素是有序的&#xff08;添加顺序和取出顺序是一致的&#xff09;且可重复List集合类的每个元素都有对应的索引(和数组索引是一样的) List集合的常用方法 add(int index Object ele)&#xff1a;在index索引位置插…

软件供应链安全中:攻击面远超想象

| 软件供应链攻击3年飙升742% | 引人注目的软件供应链攻击正在上升。欧盟网络安全机构ENISA报告称&#xff0c;自2020年初以来&#xff0c;有组织的软件供应链攻击数量增加了4倍。Gartner认为&#xff0c;这一趋势将持续下去。在《软件工程领导者如何降低软件供应链安全风险》…

dubbo学习笔记1(小d课堂)

常见的dubbo项目分层&#xff1a; 搭建springbootdubbo环境 我们首先用idea创建一个maven项目&#xff1a; 我们把src删除&#xff0c;在里面创建module&#xff1a; 然后接下来我们就要去用springboot去整合dubbo。 我们可以去github上去搜索dubbo&#xff0c;会找到dubbo-s…

心理应激微反应刑事侦查应用,社交行为、情绪行为、生物行为,说谎掩饰,单向表达不分析,情绪反应管理机制,惊讶,厌恶,愤怒,恐惧,悲伤,愉悦

心理应激微反应刑事侦查应用&#xff0c;社交行为、情绪行为、生物行为&#xff0c;说谎掩饰&#xff0c;单向表达不分析&#xff0c;情绪反应管理机制&#xff0c;惊讶&#xff0c;厌恶&#xff0c;愤怒&#xff0c;恐惧&#xff0c;悲伤&#xff0c;愉悦 提示&#xff1a;系列…