系列文章目录
文章目录
- 系列文章目录
- 前言
- STM32 中断系统
- IMX6U中断控制
- 8个中断
- GIC中断控制器
- GIC介绍
- 中断ID
- GIC逻辑分块
- GIC协处理器
- 中断使能
- 中断优先级
- 重点代码分析
- 官方SDK函数
- start.S文件
- 自行编写中断驱动文件
前言
最近在学习中发现,学Linux嵌入式不仅是对Linux的学习熟悉,而且还是对Cortex-A内核的学习掌握
没怎么看懂,内容太多了,我觉得暂时先搞清楚一些原理概念以及简单的外部函数接口就行,内部可能在后续的学习中进行钻研
STM32 中断系统
STM32 的中断系统主要有以下几个关键点:
①、 中断向量表。
②、 NVIC(内嵌向量中断控制器)。
③、 中断使能。
④、 中断服务函数。
中断向量表是一个表,这个表里面存放的是中断向量。中断向量表是一系列中断服务程序入口地址组成的表。
由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。
中断向量表在整个程序的最前面,如 STM32F103 :
__Vectors DCD __initial_sp ; Top of StackDCD Reset_Handler ; Reset HandlerDCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault HandlerDCD BusFault_Handler ; Bus Fault HandlerDCD UsageFault_Handler ; Usage Fault HandlerDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD SVC_Handler ; SVCall HandlerDCD DebugMon_Handler ; Debug Monitor HandlerDCD 0 ; ReservedDCD PendSV_Handler ; PendSV HandlerDCD SysTick_Handler ; SysTick Handler; External InterruptsDCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; TamperDCD RTC_IRQHandler ; RTCDCD FLASH_IRQHandler ; FlashDCD RCC_IRQHandler ; RCCDCD EXTI0_IRQHandler ; EXTI Line 0DCD EXTI1_IRQHandler ; EXTI Line 1DCD EXTI2_IRQHandler ; EXTI Line 2DCD EXTI3_IRQHandler ; EXTI Line 3DCD EXTI4_IRQHandler ; EXTI Line 4DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7DCD ADC1_2_IRQHandler ; ADC1_2DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TXDCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0DCD CAN1_RX1_IRQHandler ; CAN1 RX1DCD CAN1_SCE_IRQHandler ; CAN1 SCEDCD EXTI9_5_IRQHandler ; EXTI Line 9..5DCD TIM1_BRK_IRQHandler ; TIM1 BreakDCD TIM1_UP_IRQHandler ; TIM1 UpdateDCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and CommutationDCD TIM1_CC_IRQHandler ; TIM1 Capture CompareDCD TIM2_IRQHandler ; TIM2DCD TIM3_IRQHandler ; TIM3DCD 0 ; ReservedDCD I2C1_EV_IRQHandler ; I2C1 EventDCD I2C1_ER_IRQHandler ; I2C1 ErrorDCD 0 ; ReservedDCD 0 ; ReservedDCD SPI1_IRQHandler ; SPI1DCD 0 ; ReservedDCD USART1_IRQHandler ; USART1DCD USART2_IRQHandler ; USART2DCD 0 ; ReservedDCD EXTI15_10_IRQHandler ; EXTI Line 15..10DCD RTC_Alarm_IRQHandler ; RTC Alarm through EXTI LineDCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
__Vectors_End
中断向量表都是链接到代码的最前面,比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的,那么中断向量表就是从 0X00000000 开始存放的。
“__initial_sp”就是第一条中断向量,存放的是栈顶指针
复位中断复位函数 Reset_Handler 的入口地址
ARM 处理器都是从地址 0X00000000 开始运行,但是我们是下载到 0X8000000 开始的存储区域中,Cortex-M 架构引入了中断向量表偏移,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可。
void SystemInit (void)
{RCC->CR |= (uint32_t)0x00000001;/* 省略其它代码 */#ifdef VECT_TAB_SRAMSCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; //将中断向量表设置到 RAM 中#elseSCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //将中断向量表设置到 ROM 中#endif
}#define FLASH_BASE ((uint32_t)0x08000000)
对于Cortex-M来说:
中断使能:
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
中断服务函数:
要处理的工作就可以放到中断服务函数中去完成
/* 外部中断 2 服务程序 */
void EXTI2_IRQHandler(void)
{
/* 中断处理代码 */
}
IMX6U中断控制
Cortex-M 内核有个中断系统的管理机构—NVIC(Nested Vectored Interrupt Controller)
Cortex-A7 内核有个中断系统管理机构——GIC(general interrupt controller)
8个中断
Cortex-A内核有8个异常中断:
其中还包括一个未使用中断,实际只有七个中断。
对于 Cortex-M 内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。
Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理。
①、复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、 DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断, Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、 IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中断的发生。
⑦、 FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
常用复位中断和IRQ中断,实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断本暂时没用到,所以都是死循环。
GIC中断控制器
GIC介绍
GIC目前有V1-V4,V1太老淘汰了,GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、 Cortex-A9、 Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。ARM 会根据 GIC 版本的不同研发出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可,ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ。
VFIQ:虚拟快速 FIQ。VIRQ:虚拟外部 IRQ。FIQ:快速中断 IRQ。IRQ:外部中断 IRQ。
VFIQ 和 VIRQ 是针对虚拟化的,不管,只看IRQ
①、 SPI(Shared Peripheral Interrupt),共享中断,所有 Core 共享的中断,那些外部中断都属于 SPI 中断。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、 PPI(Private Peripheral Interrupt),私有中断, GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理。
③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
中断ID
每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI。
ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断
I.MX6U 的总共使用了 128 个SPI中断,以及 PPI 和 SGI 的 32 个 ID,一共160个
中断ID全部都在参考手册的第三章:
移植的官方SDK(Software Development Kit)中就有所有中断ID的枚举
GIC逻辑分块
GIC 架构分为了两个逻辑块: Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端
Distributor(分发器端):中断事件应该发送到哪个 CPU 接口,将优先级最高的中断事件发送到 CPU 接口端
全局中断使能控制。控制每一个中断的使能或者关闭。设置每个中断的优先级。设置每个中断的目标处理器列表。设置每个外部中断的触发模式:电平触发或边沿触发。设置每个中断属于组 0 还是组 1。
CPU Interface(CPU 接口端):每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface
使能或者关闭发送到 CPU Core 的中断请求信号。应答中断。通知中断处理完成。设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。定义抢占策略。当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。
在我们移植的 core_ca7.h 文件里面有定义结构体 GIC_Type,分发器端和 CPU 接口端:
GIC协处理器
通过 c0 寄存器可以获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、 I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。(暂时没太理解,后续补充)
中断使能
使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断
寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ; F=1 禁止 FIQ, F=0 使能 FIQ。
还可以使用更简单的指令:
GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止,Cortex-A7 内核中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么就需要 512/32=16 个 GICD_ISENABLER 寄存器和16个GICD_ICENABLER来完成中断的使能与禁止。
GICD_ISENABLER0 的 bit[15:0]对应ID15 ~ 0 的 SGI 中断, GICD_ISENABLER0 的 bit[31:16]对应 ID31 ~ 16 的 PPI 中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
中断优先级
Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置的。 GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高! Cortex-A7 选择了 32 个优先级。
在使用中断的时候需要初始化 GICC_PMR 寄存器:(设置优先级数)
Cortex-A7内核支持 32 个优先级。
抢占优先级和子优先级位数设置由寄存器 GICC_BPR决定:
寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同:
有32个抢占优先级,Cortex-A7 使用了 512 个中断 ID
如要设置ID40 中断的优先级为 5:
GICD_IPRIORITYR[40] = 5 << 3
重点代码分析
官方SDK函数
先了解官方SDK包的API函数:
GIC_Init //初始化 GIC。
GIC_EnableIRQ //使能指定的外设中断。
GIC_DisableIRQ //关闭指定的外设中断。
GIC_AcknowledgeIRQ //返回中断号。
GIC_DeactivateIRQ //无效化指定中断。
GIC_GetRunningPriority //获取当前正在运行的中断优先级。
GIC_SetPriorityGrouping //设置抢占优先级位数。
GIC_GetPriorityGrouping //获取抢占优先级位数。
GIC_SetPriority //设置指定中断的优先级。
GIC_GetPriority //获取指定中断的优先级。
这些函数都在文件末尾:
start.S文件
.global _start /* 全局标号 *//** 描述: _start函数,首先是中断向量表的创建* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)*/
_start:ldr pc, =Reset_Handler /* 复位中断 */ ldr pc, =Undefined_Handler /* 未定义中断 */ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */ldr pc, =PrefAbort_Handler /* 预取终止中断 */ldr pc, =DataAbort_Handler /* 数据终止中断 */ldr pc, =NotUsed_Handler /* 未使用中断 */ldr pc, =IRQ_Handler /* IRQ中断 */ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 *//* 复位中断 */
Reset_Handler:cpsid i /* 关闭全局中断 *//* 关闭I,DCache和MMU 关闭 I/D Cache、 MMU、对齐检测和分支预测* 采取读-改-写的方式。*/mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */#if 0/* 汇编版本设置中断向量表偏移 */ldr r0, =0X87800000dsbisbmcr p15, 0, r0, c12, c0, 0dsbisb
#endif/* 设置各个模式下的栈指针,* 注意:IMX6UL的堆栈是向下增长的!* 堆栈指针地址一定要是4字节地址对齐的!!!* DDR范围:0X80000000~0X9FFFFFFF*//* 进入IRQ模式 */mrs r0, cpsrbic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB *//* 进入SYS模式 */mrs r0, cpsrbic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB *//* 进入SVC模式 */mrs r0, cpsrbic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */cpsie i /* 打开全局中断 */
#if 0/* 使能IRQ中断 */mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endifb main /* 跳转到main函数 *//* 未定义中断 */
Undefined_Handler:ldr r0, =Undefined_Handlerbx r0/* SVC中断 */
SVC_Handler:ldr r0, =SVC_Handlerbx r0/* 预取终止中断 */
PrefAbort_Handler:ldr r0, =PrefAbort_Handler bx r0/* 数据终止中断 */
DataAbort_Handler:ldr r0, =DataAbort_Handlerbx r0/* 未使用的中断 */
NotUsed_Handler:ldr r0, =NotUsed_Handlerbx r0/* IRQ中断!重点!!!!! */
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寄存器内的值到R1寄存器中* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49* Cortex-A7 Technical ReferenceManua.pdf P68 P138*/ add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据* 这个中断号来绝对调用哪个中断服务函数*/push {r0, r1} /* 保存r0,r1 */cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */push {lr} /* 保存SVC模式的lr寄存器 */ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */pop {lr} /* 执行完C语言中断服务函数,lr出栈 */cps #0x12 /* 进入IRQ模式 */pop {r0, r1} str r0, [r1, #0X10] /* 中断执行完成,写EOIR */pop {r0} msr spsr_cxsf, r0 /* 恢复spsr */pop {r0-r3, r12} /* r0-r3,r12出栈 */pop {lr} /* lr出栈 */subs pc, lr, #4 /* 将lr-4赋给pc *//* FIQ中断 */
FIQ_Handler:ldr r0, =FIQ_Handler bx r0
重点在复位中断和IRQ中断(目前学的比较浅,还不太理解),但是关于下图:
之前学习过ARM的三级流水过程:(类似下图)
中断发生保存的是0x2008,所以要跳回到0x2004处执行这条指令,不然漏了这条指令。
自行编写中断驱动文件
irqNesting:中断嵌套计数器
irqTable:中断服务函数数组,大小为 I.MX6U 的中断源个数:160 个
int_init:中断初始化函数,初始化了 GIC,然后初始化了中断服务函数表,最终设置了中断向量表偏移
system_irqtable_init:中断服务函数表初始化函数,初始化 irqTable,给其赋初值
start.S 中调用的 system_irqhandler 函数:根据中断号在中断处理函数表 irqTable 中取出对应的中断处理函数并执行
default_irqhandler: 默认中断处理函数
#include "bsp_int.h"/* 中断嵌套计数器 */
static unsigned int irqNesting;/* 中断服务函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];/** @description : 中断初始化函数* @param : 无* @return : 无*/
void int_init(void)
{GIC_Init(); /* 初始化GIC */system_irqtable_init(); /* 初始化中断表 */__set_VBAR((uint32_t)0x87800000); /* 中断向量表偏移,偏移到起始地址 */
}/** @description : 初始化中断服务函数表 * @param : 无* @return : 无*/
void system_irqtable_init(void)
{unsigned int i = 0;irqNesting = 0;/* 先将所有的中断服务函数设置为默认值 */for(i = 0; i < NUMBER_OF_INT_VECTORS; i++){system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);}
}/** @description : 给指定的中断号注册中断服务函数 * @param - irq : 要注册的中断号* @param - handler : 要注册的中断处理函数* @param - usrParam : 中断服务处理函数参数* @return : 无*/
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{irqTable[irq].irqHandler = handler;irqTable[irq].userParam = userParam;
}/** @description : C语言中断服务函数,irq汇编中断服务函数会调用此函数,此函数通过在中断服务列表中查找指定中断号所对应的中断处理函数并执行。* @param - giccIar : 中断号* @return : 无*/
void system_irqhandler(unsigned int giccIar)
{uint32_t intNum = giccIar & 0x3FFUL;/* 检查中断号是否符合要求 */if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS)){return;}irqNesting++; /* 中断嵌套计数器加一 *//* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);irqNesting--; /* 中断执行完成,中断嵌套寄存器减一 */}/** @description : 默认中断服务函数* @param - giccIar : 中断号* @param - usrParam : 中断服务处理函数参数* @return : 无*/
void default_irqhandler(unsigned int giccIar, void *userParam)
{while(1) {}
}
还是没怎么看懂,内容太多了,我觉得暂时先搞清楚一些原理概念以及简单的外部函数接口就行,内部可能在后续的学习中进行钻研
gpio内部有中断的设置:
中断函数写在这里:
主函数进行初始化: