文章目录
说在前面
中断这东西,你说它难,看着别人的源码听着课程分析还是能够理解的,但是单靠目前我这样的水平自己对着手册写出来应该还是不怎么行。
那怎么办呢?
目前就看懂别人的代码,然后学会怎么配置,最后直接整个拷贝过来,移植过来后自己修改中断服务程序isr即可。
中断配置的顺序
根据开发板手册进行中断初始化,初始化各种中断的寄存器。
绑定异常向量表
这个S5PV210开发板的ARM芯片的异常向量表,左边的地址是每一个异常(中断)的偏移量。
怎么理解这个绑定异常向量表呢?
可以这样理解,通过函数的方式去跟它进行绑定。
函数名可以看做是一个指针,可以通过(函数名(指针))来访问这个函数。
有了这个前提条件,就可以把对应的Vector Interrupt 的地址与中断服务程序(IRQ)进行绑定。
#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.h头文件,采用了基地址+变址的方式,把对应的异常向量的地址先通过宏定义改成比较好看好读的对应的异常向量的名字。
然后呢使用了一下C语言的技巧,通过解引用的方式变成可以访问内存的方式去访问这个寄存器。
与之相对应,在int.c函数里面
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();
}
把我们的相对应的函数名(指针)放到这些内存地址里面,以后访问这个内存地址就是去执行相关的中断函数了。
有点特别的地方,这里的IRQ和FIQ的异常向量表都绑在了IRQ_handle里面,因为是没有设置FIQ。
FIQ和IRQ的区别就是FIQ是fast模式的IRQ。
那么这个IRQ_handle里面写的什么呢?
在start.S启动文件里面
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}^
简单解析
首先,发生了中断之后就会从之前的SVC模式跳到IRQ模式,每种模式下都有对应的r0-r12,还有lr
复习: 什么是SP LR PC 为什么只有R0-R12 R131415去哪了.
看完之后了解了。
发生中断的时候呢,会自动把执行到目前的地址放到lr里面,但是这时候的pc的地址是指向下一条指令的fetch那里的(ARM流水线.),所以要减4回到当前命令的位置。
然后通过stmfd这个指令,把r0-r12,还有lr都通过栈指针sp保存到栈里面,实现了中断模式下的现场保护,保护了SVC模式下的r0-r12,还有当前程序执行到的那一行的位置(lr=lr-4)
紧接着调用中断服务程序irq来处理中断,通过bl指令跳转
最后通过ldmfd指令,把在栈里面的存好的之前的现场给恢复好,这时候就把之前保存好的lr里面的地址赋给pc,然后pc接着执行即可。
跳转到的bl irq_handler在后面中断处理程序部分讲
初始化中断控制器
三步:
1.禁止所有的中断;
// 禁止所有中断
// 为什么在中断初始化之初要禁止所有中断?
// 因为中断一旦打开,因为外部或者硬件自己的原因产生中断后一定就会寻找isr
// 而我们可能认为自己用不到这个中断就没有提供isr,这时它自动拿到的就是乱码
// 则程序很可能跑飞,所以不用的中断一定要关掉。
// 一般的做法是先全部关掉,然后再逐一打开自己感兴趣的中断。一旦打开就必须
// 给这个中断提供相应的isr并绑定好。
2.设置中断类型;
设置是IRQ或者啥的。。
3清除需要处理的中断的中断处理函数的地址。
第三步有点绕。
下面是相应的代码
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;
}
根据数据手册,写0就清掉。
为什么要清掉?
这里要注意区分VICADDR和VICVECTADDR
VICVECTADDR和VICADDR里面都放的是irq的程序地址,有什么不一样呢?
VICVECTADDR是一个寄存器,里面有32位的,每个位都放一个地址,这些地址将来是跟irq绑定的。
VICADDR是当发生中断之后,自动会把相对应的VICVECTADDR里面的isr地址放到VICADDR里面,然后供我们读出来的。
可以理解为前者是快递站,放了很多快递,后者是快递员,快递站会给快递员分发快递这样,快递员发完快递就会标识好这条信息,告知公司快递发完了,公司就会清理了这条完成的信息,相当于这里的清中断。
头文件里面那么多个中断源编号,我怎么把这些编号和我的中断服务程序isr进行绑定呢?
针对这个问题,在int.c里面
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;
}
S5PV210这款开发板有4个VICVECTADDR,一个32位。
//VIC0
if(intnum<32)
{
*( (volatile unsigned long *)(VIC0VECTADDR + 4*(intnum-0)) ) = (unsigned)handler;
}
采用了基地址+变址的方式,把你传进去的intnum和isr
为什么要乘4?
这里节选VIC0VECTADDR为例子,一共有0-31的,一共32位。
假设现在的intnum是第九个,那么它相对应的地址应该是(9-0)*4+base地址,这个乘4就是因为每个寄存器的地址间隔都是4。
初始化之后可以使能中断了(禁止也类似这样的操作)
这里用到了VICINTENABLE寄存器
void intc_enable(unsigned long intnum)
{
unsigned long temp;
// 确定intnum在哪个寄存器的哪一位
// <32就是0~31,必然在VIC0
if(intnum<32)
{
temp = VIC0INTENABLE;
temp |= (1<<intnum); // 如果是第一种设计则必须位操作,第二种设计可以
// 直接写。
VIC0INTENABLE = temp;
}
截取一小段,也很好理解,把intnum传进来,然后把intnum对应的在VICINTENABLE对应的位置置1,那么当前位置就使能了
中断禁止
也类似这样的操作,不过操作的是VICINTENCLEAR寄存器而已。
中断处理程序部分
要跟start.S里面 bl irq_handler联系起来。
之前只是讲了要跳转到这里去执行中断处理程序,那么这部分又是怎么实现的呢?
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; //把isr指向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)(); // 通过函数指针来调用函数
}
之前说了VICADDR是硬件会置位的,所以这里用一个vicaddr[4]数组,里面放着这4个寄存器,然后通过for循环去看哪个vidaddr里面有东西。
for循环里面怎么实现的呢?
调用了一个叫intc_getvicirqstatus的函数
intc_getvicirqstatus()
通过读取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;
}
首先传参的ucontroller是你for循环的i,通过这个函数,就能够返回VICIRQSTATUS状态寄存器的值。
回看到for循环里面的if判断语句
if(intc_getvicirqstatus(i) != 0) 如果返回的状态位的值不等于0为真的话,那么就证明当前的VICADDR里面有中断的isr了
回看for循环上面的指针(什么指针,函数指针。)
再结合if语句里面执行的内容,就是把当前的VICADDR里面存放的isr的地址赋值给这个函数指针,最后完成了之后就解引用这个函数指针,也就是执行这个中断服务程序,到这里这一步才是真真执行了中断服务程序了。
其实一步一步link回去,这里的isr就是到时候你自己要写的isr程序。
上述的部分都只是目前搭建好的框架,还要自己写用到的isr以及在main函数里面调用初始化函数,使能中断才行。
按键的外部中断
初始化
之前轮询的时候按键对应GPIO口也进行了初始化,这里也一样。
初始化之后就可以写中断服务程序了。
中断服务程序之后要记得清中断,除了自己isr要清,整个中断架构的VICADDR也要清除
main函数里面
要添加
1.按键的初始化
2.中断的初始化
3.把中断源编号号和中断服务程序绑定起来
4.使能中断(通过中断源编号来使能)