在上一章我们大概讲了中断原理,并且在放出来汇编的中断向量表和预留的中断服务函数,下面我们就要结合前面学过的知识完善这些中断服务函数。
复位中断函数
I.MX6U在上电开始或复位的时候就会调用这个复位中断,在这个中断要做的工作有:
- 关闭全局终端
- 关闭I Cache、D Cache、MMU
- 设置中断向量表偏移量(非必要,可以放在C环境下执行)
- 设置各个模式下的栈指针:要注意点是I.MX6UL的堆栈是向下增长的,在设置堆栈指针的时候一定要注意4字节对齐,不要设置个类似0x00000003种不是4的倍数这种的地址,注意DDR的地址范围是0x80000000到0x9FFFFFFF(512MB)或0x8FFFFFFF(256MB)
- 使能全局中断
上面的流程在后面讲UBoot的时候会分析,这里只放结论,下面我们一步步来说明
关闭中断
全局终端的关闭只需要通过指令即可完成
cpsid i
关闭I/DCache、MMU
关闭I Cache、D Cache和MMU的操作要借助CP15协处理器的c1寄存器来完成,而c1寄存器应反射为SCTLR寄存器,《Cortex-A7 Technical ReferenceManua.pdf》4.3.27里还直接给出了这个寄存器的读写指令
我们采用读——改——写的方式,将SCTLR寄存器里的值复制到R0寄存器里,只休改需要修改的bit,完成后再写入SCTLR即可。
设置各个模式
中断向量偏移
中断向量偏移的设置要通过CP15的c12对应的VBVR寄存器来操作
对应的参数手册里也都给出来了,把偏移的地址给他就行了。这里加个附加指令:DSB和ISB用来同步数据清洗流水线,保证前面的指令都执行完成后才执行后面的指令。
设置各个模式下的栈指针
从下面的图可以看出来,除了User模式和Sys模式,每个工作模式都是用自己的sp指针,所以我们需要进入每个工作模式设置对应的sp指针。
我们这一章主要用IRQ,这个sp_IRQ是必须要设置的。做的时候就是修改CPSR寄存器的bit[4:0],进入对应的模式然后给指针赋值就可以了
使能中断
中断等使能也是直接一个指令就可以了,也可以像前面讲原理的时候说的修改CPSR对应的bit。
代码结构
下面的代码就是整个中断复位函数
/*复位中断服务函数 */ Reset_Handler: CPSID i @禁止全局中断 /*操作CP15协处理器,关闭ICache DCache,MMU*/ MRC p15,0,r0,c1,c0,0 @读取SCTLR寄存器的数据到R0寄存器中 BIC r0,r0,#(1<<12) @清零bit12,关闭I Cache BIC r0,r0,#(1<<11) @清零bit11,关闭分支预测 BIC r0,r0,#(1<<2) @清零bit2,关闭D Cache BIC r0,r0,#(1<<1) @清零bit1,关闭对齐控制 BIC r0,r0,#(1<<0) @清零bit0,关闭MMU MCR p15,0,r0,c1,c0,0 @将R0里的数据写入到SCTLR寄存器中 #if 0 /*设置中断向量偏移(此步骤可以在C语言里实现)修改P15协处理器VBVR寄存器 */ LDR r0,=0x87800000 DSB ISB @同步指令,清洗流水线 MCR p15,0,r0,c12,c0,0 @设置VBVR寄存器=0x87800000 DSB ISB #endif /*设置各个模式下sp指针 */ /*进入IRQ模式 */ MRS r0,CPSR BIC r0,r0,#0x1f ORR r0,r0,#0x12 MSR CPSR,r0 LDR sp,=0x80600000 /*进入SYS模式 */ MRS r0,CPSR BIC r0,r0,#0x1f ORR r0,r0,#0x1f MSR CPSR,r0 LDR sp,=0x80400000 /*进入SVC模式 */ MRS r0,CPSR BIC r0,r0,#0x1f ORR r0,r0,#0x13 MSR CPSR,r0 LDR sp,=0x80200000 CPSIE i @使能全局中断 b main
上面的代码我们只设置了IRQ模式、SYS模式和SVC模式的sp指针,每种模式的栈大小都是2MB,作为裸机开发已经够用了。当所有初始化都完成后就可以跳转到最后的main函数。
IRQ中断服务函数
IRQ中断服务函数是重点,因为所有的外部中断都会触发这个IRQ中断。这个中断函数分两部分,一部分是IRQ模式下中断响应,在start.s里的;还有一部分是C语言构成的中断服务。流程为在汇编环境响应中断、切换至IRQ模式下获取中断相关参数(主要就是ID)、保护现场、调用C函数运行中断服务、
- 响应外部中断并进入IRQ对应函数
- 保护现场、包括r0~r3、r12以及SPSR(CPSR的备份寄存器)
- 获取中断相关参数:主要就是中断ID,用来判定是哪个外设发出的中断请求。并保存参数
- 切换至SVC模式,可以接受其他的中断请求。
- 保护SVC模式现场(LR寄存器),跳转至C函数执行中断服务。在C语言中根据中断ID进行相关程序(这里有个疑问,使用BLX指令跳转时是会保存用到LR寄存器的)。
- 执行完C程序后,要通过软件设置返回IRQ模式,将中断ID写回给EOIR,告诉中断管理器这个中断任务已经结束了
- 恢复现场,调整pc指针
这里有几个点要注意一下:
获取中断相关参数
这里主要就是获取中断ID,中断ID是在GICC_IAR[9:0]里存放的,而GICC_IAR是在GIC的CPU Interface上偏移量0xC个字节,CPU Interface的地址又是在GIC第基地址上偏移量0x2000和字节,而GIC的基地址是通过CP15的c15获取
中断服务函数调用(C函数)
由于我们的这段汇编指令主要是操作处理器运行状态,实际的外设操作是通过调用的C语言来完成的。C函数需要针对不同的中断ID来调取不同的子函数,所以在调用C函数时需要传递参数,这个参数就是中断ID。根据 ATPCS(ARM-Thumb Procedure Call Standard)定义的函数参数传递规则,在不超过4个形参时,形参是通过R0~R3这几个寄存器传递的,如果形参超过4个,那么要借助堆栈进行参数传递了。所以,在前面的操作中,一定要保证最后的中断ID是放在R0中,就可以被后面调用的C函数获取。
恢复现场
最后恢复现场的时候要注意,不能直接把LR寄存器里的值传给PC指针,因为ARM的指令是三级流水线:取指、译指和执行。也就是PC=当前执行指令地址+8比如下面的代码以及对应的地址
0x2000 MOV R1,R0 ; 执行 0x2004 MOV R2,R3 ; 译指 0x2008 MOV R4,R5 ; 取指,当前PC指针
比如说当前PC指针指向地址为0x2008,但是实际执行的指令是0x2000地址的;此刻如果发生中断,处理器在执行完0x2000地址内指令后去执行中断任务,此刻LR保存的是PC的值也就是0x2008,在中断完成后,如果直接跳回LR保存的地址2008,则2004的指令会不被执行,所以要把LR的值减4传给PC,从2004那条指令重新开始执行。
代码结构
还是只放IRQ中断处理函数,后面备比较详细,可以参考。暂时有个地方不理解:为什么中间要转到SVC模式处理中断?留着疑问以后看看能不能解决吧!
IRQ_Handler: PUSH {lr} /* 保存lr寄存器内容 */ PUSH {r0-r3, r12} /* 保存r0-r3,r12寄存器 */ MRS r0, spsr /* 读取spsr寄存器 */ PUSH {r0} /* 保存spsr寄存器 */ MRC p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器CBAR内的值到R1寄存器中,获取GIC基地址 * 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49 * Cortex-A7 Technical ReferenceManua.pdf P68 P138*/ ADD r1, r1, #0x2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 *至此R1保存内容为GIC里CPUInterface基地址 */ LDR r0, [r1, #0xC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,从bit[9:0]获取中断ID *至此,R0保存为GICC_IAR,对应中断ID */ push {r0, r1} /* 保存r0,r1 */ CPS #0x13 /* 进入SVC模式,允许其他中断再次进去 注意R0和R1内为有用数据,后面使用时应注意*/ PUSH {lr} /* 保存SVC模式的lr寄存器 */ LDR r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中 该函数需要传参 * 跳转至定义的C语言中断处理函数,中断ID作为参数保存在R0中 */ BLX r2 /* r2是指向函数的地址,所以用BX,加BXL是保存跳转处地址至LR寄存器,共返回时使用*/ POP {lr} /* 执行完C语言中断服务函数,lr出栈 */ CPS #0x12 /* 进入IRQ模式 */ POP {r0, r1} STR r0, [r1, #0X10] /* 中断执行完成,写EOIR ,此步骤进行后,R0和R1的值就不重要了,可以被覆盖*/ POP {r0} MSR spsr_cxsf, r0 /* 恢复spsr */ POP {r0-r3, r12} /* r0-r3,r12出栈 */ POP {lr} /* lr出栈 */ SUBS pc, lr, #4 /* 将lr-4赋给pc */
基本上所有的语句都加了备注,哪个CPS是个类似于语法糖的用法,在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》B9.3.2里介绍过,可以直接赋值跳转CPU模式
这个CPS开始让我晕了半天,查了手册才发现这种用法!
通用中断程序
上面的程序完成了,下面主要就是处理中断了,也就是指向的system_irqhandler函数。这里使用了教程提供的一个基于恩智浦官方提供的SDK修改以后中断相关的头文件(core_ca7.h),头文件里提供了一系列接口,我们可以直接调用。在这个中断驱动文件中,核心函数就是被调用的system_irqhandler。它将根据不同的中断ID去调用不同的函数。I.MX6U一共提供了160个中断源,所以就对应了160个中断函数,我们可以把这160个函数放在一个数组中,函数的索引值就是对应其中断ID。当中断发生以后,函数system_irqhandler会根据传递过来的中断ID找到对应的函数并处理就可以了。我们在bsp下建立新的文件夹int,里面新建对应的文件
声明函数属组
定义这个属组,我们需要在头文件里声明几个函数和数据类型
/*声明终端处理函数*/ typedef void(*system_irq_handler_t)(unsigned int gicciar,void *param); /*创建中断函数结构体*/ typedef struct _sys_irq_handle { system_irq_handler_t irqHander; //中断处理函数 void *userParam; //中断处理函数的参数 }sys_irq_handle_t;
这里我们声明的是函数指针:*system_irq_handler_t,这个函数我们在C文件里需要使用,还有后面的结构体包含了函数本身以及函数需要对参数。
在声明好了函数以及数据类型以后,我们就可以在.c文件里编写对应功能的代码了
/*定义中断处理函数表*/ static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS]; //NUMBER_OF_INT_VECTORS为中断ID个数 /*默认中断处理函数*/ void default_irqhandler(unsigned int gcciar,void *userparam) { while(1){} }
先定义了一个表,这个表的数据类型就是我们在头文件里定义的结构体sys_irq_handle_t,表的大小就是中断等个数160。然后,再定义一个默认的函数。这个函数是在对函数数组初始化时候要要用到的,是一个空函数。函数两个变量,一个是整形的中断ID,另一个是指针。
初始化函数数组
前面的工作完成了函数数组的准备工作,然后我们要对这个数组进行初始化
/*初始化中断处理函数表*/ void system_irqtable_init(void) { unsigned int i = 0; irqNesting = 0; for(i=0;i<NUMBER_OF_INT_VECTORS;i++) { irqTable[i].irqHander = default_irqhandler; //初始化为默认函数 irqTable[i].userParam = NULL; //参数为指针,指向空 } }
初始化的过程中做了个循环,在循环体里将函数指向我们定义的空函数,参数也指向一个空值。
注册中断处理函数
到上面为止,已经可以实现中断功能了,但问题是,并没有实际的函数去执行中断对应请求,而我们写的中断处理函数要运行必须要和那个中断函数数组关联起来,这就需要写一个函数,用来对中断服务函数进行注册
/*注册中断处理函数*/ void system_register_irqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam) { irqTable[irq].irqHander = handler; irqTable[irq].userParam = userParam; }
这个函数里面有3个参数,参数一对应的是中断id,第二个就是我们的中断服务函数,第三个就是中断函数需要传递的参数。中断id用来对数组进行索引,直接赋值就可以。这个函数需要在外部使用,记得在头文件中声明。
中断服务函数
这个函数主要用于和汇编IRQ模式对接,就是核心的那个函数
void system_irqhandler(unsigned int gicciar) { uint32_t intNum = gicciar &0x3FF; //bit[9:0]共10位,转换过来就是0x3FF与运算就可获取中断ID /* 检查中断ID是否为异常*/ if(intNum>=NUMBER_OF_INT_VECTORS) { return; //中断ID异常,直接返回 } irqNesting++; //有新中断,计数器+1 irqTable[intNum].irqHander(intNum,irqTable[intNum].userParam); //执行具体中断处理 irqNesting--; //中断处理完成,嵌套计数器-1 }
在函数中调用了变量irqNesting,是用作嵌套中断的记录,如果在中断执行时有高优先级的中断进来,当前中断会被打断,嵌套计数器自增1,中断处理完成后计数器自减1,如果计数器为0说明当前中断无嵌套。
函数构成
下面分别是头文件和c文件
#ifndef __BSP_INT_H #define __BSP_INT_H #include "imx6ul.h" /*声明终端处理函数*/ typedef void(*system_irq_handler_t)(unsigned int gicciar,void *param); /*创建中断函数结构体*/ typedef struct _sys_irq_handle { system_irq_handler_t irqHander; //中断处理函数 void *userParam; //中断处理函数的参数 }sys_irq_handle_t; void int_init(void); void system_irqtable_init(void); void default_irqhandler(unsigned int gcciar,void *userParam); void system_irqhandler(unsigned int gicciar); void system_register_irqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam); #endifbsp_int.h
#include "bsp_int.h" static unsigned int irqNesting; //记录中断嵌套计数器 /*定义中断处理函数表*/ static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS]; //NUMBER_OF_INT_VECTORS为中断ID个数 /* * @description : 默认中断处理函数 * @param-gcciar : 中断ID * @param-userParam : 中断服务处理函数参数 * @return : 无 */ void default_irqhandler(unsigned int gcciar,void *userParam) { while(1){} } /*初始化中断处理函数表*/ void system_irqtable_init(void) { unsigned int i = 0; irqNesting = 0; for(i=0;i<NUMBER_OF_INT_VECTORS;i++) { irqTable[i].irqHander = default_irqhandler; //初始化为默认函数 irqTable[i].userParam = NULL; //参数为指针,指向空 } } /*中断初始化函数*/ void int_init(void) { GIC_Init(); //GIC初始化 system_irqtable_init(); //初始化中断函数表 __set_VBAR(0x87800000); //中断向量表偏执 } /* * @description : 注册中断处理函数 * @param-irq : 中断id * @param-handler : 中断处理服务函数 * @param-userParam : 中断服务处理函数参数 * @return : 无 */ void system_register_irqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam) { irqTable[irq].irqHander = handler; //函数 irqTable[irq].userParam = userParam; //参数 } /* *@desciption : C语言中断服务,IRQ中断调用函数 *@param-gicciar : 中断ID,在汇编处通过R0传入 *@return : 无 */ void system_irqhandler(unsigned int gicciar) { uint32_t intNum = gicciar &0x3FF; //bit[9:0]共10位,转换过来就是0x3FF与运算就可获取中断ID /* 检查中断ID是否为异常*/ if(intNum>=NUMBER_OF_INT_VECTORS) { return; //中断ID异常,直接返回 } irqNesting++; //有新中断,计数器+1 irqTable[intNum].irqHander(intNum,irqTable[intNum].userParam); //执行具体中断处理 irqNesting--; //中断处理完成,嵌套计数器-1 }bsp_int.c
现在就完成了通用的中断服务程序,现在需要对就是使用外设中断、调用中断程序。