I.MX6U 裸机开发14.IRQ中断服务函数

  • 一、IRQ中断服务启用与禁用
  • 1. 打开全局中断
  • 2. GIC控制器
  • (1)Distributor 分配器
  • (2)CPU接口
  • 二、汇编中断处理函数
  • 1. 汇编代码
  • 2. 汇编指令解析
  • (1)`push {r0-r3, r12}`
  • (2)`mrc p15, 4, r1, c15, c0, 0 `
  • (3)`CBAR`寄存器
  • (4)`GICC_IAR` 寄存器
  • (5)进入 SVC 模式`cps #0x13`
  • (6)运行C语言中断函数
  • (7)返回 IRQ 模式,写`EDIR`
  • (8)返回
  • 三、通用中断驱动编写
  • 1. 移植 `core_ca7.h`
  • 2. 向量表大小
  • 3. 编写通用中断处理函数
  • bsp_irq.h
  • bsq_irq.c


一、IRQ中断服务启用与禁用

1. 打开全局中断

在上一章的程序中,启动文件中调用了 CPSIE i 。该指令用于将CPSR的寄存器的 I 位清0,从而打开全局中断。 打开全局中断后,处理器可以响应 IRQ 信号,并跳转到相应的中断处理程序。

2. GIC控制器

前一篇文章介绍了GIC控制,我们了解到GIC是通用中断控制器,GIC控制器的结构如下图所示:

相对于PERIPHBASE[39:15]的偏移量

GIC块

0x0000 - 0x0FFF

保留(Reserved)

0x1000 - 0x1FFF

分配器(Distributor)

0x2000 - 0x3FFF

CPU接口(CPU interface)

0x4000 - 0x4FFF

虚拟接口控制,公共基地址(Virtual interface control, common base address)

0x5000 - 0x5FFF

虚拟接口控制,特定处理器基地址(Virtual interface control, processor - specific base address)

0x6000 - 0x7FFF

虚拟CPU接口(Virtual CPU interface)

下面重点分析分配器和CPU接口。

(1)Distributor 分配器

分配器地址偏移量是: 0x1000~0x1FFF。
分配器负责管理和分发中断请求到各个 CPU 接口,它有以下功能:

  1. 中断优先级管理,分配器能够为不同的中断源设置优先级。
  2. 中断屏蔽和使能,它可以选择性地屏蔽或使能特定的中断源。
  3. 中断分发,当一个中断发生时,分配器会根据系统配置将中断请求发送到相应的CPU接口。在多核系统中,它可以根据负载均衡策略或特定的绑定规则来决定将中断发送到哪个CPU。
(2)CPU接口

在通用中断控制器(GIC)架构中,CPU 接口是连接 GIC 和 CPU 的关键部分。CPU接口端有以下功能:

  1. 中断接收与处理
  2. 中断确认
  3. 中断优先级处理(部分功能)
  4. 中断结束处理

在后面的中断处理函数中,会对GIC进行操作。

二、汇编中断处理函数

1. 汇编代码

下面是从正点原子示例中复制的 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 */

2. 汇编指令解析

(1)push {r0-r3, r12}

push 指令可以一次性将多个寄存器的值压入堆栈,这是因为 ARM 处理器支持多寄存器操作指令,这些指令可以同时对多个寄存器进行操作,从而提高效率。 具体来说,push {r0-r3, r12} 指令会将寄存器 r0 到 r3 和 r12 的值按顺序压入堆栈。

(2)mrc p15, 4, r1, c15, c0, 0

mrc p15, 4, r1, c15, c0, 0 用于从 CP15 协处理器的寄存器中读取值,并将其存储到通用寄存器 r1 中。具体读取的是 CP15 协处理器的 CRn=4, CRm=15, opc1=0, opc2=0 的寄存器值。

参考文档《Cortex-A7 Technical ReferenceManua》第68页的表格:

I.MX6U 裸机开发14.IRQ中断服务函数_嵌入式硬件

(3)CBAR寄存器

I.MX6U 裸机开发14.IRQ中断服务函数_嵌入式硬件_02


CBAR寄存器保存了GIC控制器的寄存器组首地址。

再次详细分析上面的汇编指令:
mrc p15, 4, r1, c15, c0, 0 这里将CP15的C0寄存器的值加载到了R1寄存器,即GIC的基地址加载到了 R1 寄存器。

后面的 add r1, r1, #0x2000,将GIC的基地址加0x2000,从前面GIC控制器结构可知,这时R1保存了CPU接口端的基地址。

(4)GICC_IAR 寄存器

GICC_IAR(Interrupt Acknowledge Register)在 GIC 中用于标识当前正在处理的中断。它的主要作用是读取当前发生的中断号,以便处理器可以跳转到相应的中断服务程序。

I.MX6U 裸机开发14.IRQ中断服务函数_中断处理_03


从 GICC_IAR 中,bit9~bit0是中断号。

ldr r0, [r1, #0xC] ,偏移 0x0C 是 GICC_IAR 寄存器,即读取 CPU 接口端 GICC_IAR 寄存器值保存到 R0 寄存器。通过其中的中断号,就可以得到对应的中断处理函数。

(5)进入 SVC 模式cps #0x13

IRQ中断模式主要用于处理中断。 后面要进入到SVC特权模式,主要以下几个原因:

  1. 特权级别:SVC 模式具有更高的特权级别,可以访问更多的系统资源和执行特权指令,这对于某些中断处理程序是必要的。
  2. 中断嵌套:在 SVC 模式下,可以允许其他中断嵌套进入,从而提高系统的响应能力和实时性。
  3. 安全性:在 SVC 模式下,可以更好地控制中断处理程序的执行环境,确保系统的安全性和稳定性。

(6)运行C语言中断函数

push {lr}					/* 保存SVC模式的lr寄存器 */
	ldr r2, =system_irqhandler	/* 加载C语言中断处理函数到r2寄存器中*/
	blx r2						/* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

其中 system_irqhandler 有一个参数:

void ssytem_irqhandler(unsigned int giccIar){
   uint32_t intNum = giccIar & 0x3FFUL;
   
}

参数 giccIar 保存了中断ID。

(7)返回 IRQ 模式,写EDIR

cps #0x12
pop {r0, r1}
str r0, [r1, #0x10]

这里的 str r0, [r1, #0x10],意思将R0的值写入到 [R1+0x10] 地址,由于 R0 保存的是 GICC_IAR 寄存器的值,R1 保存的是 GIC的CPU接口端基地址,这个位置保存的是 GIC_EDIR寄存器。

I.MX6U 裸机开发14.IRQ中断服务函数_嵌入式硬件_04


EDIR(End of Interrupt Register)寄存器用于通知 GIC 当前中断处理已经完成,可以接受新的中断请求,其寄存器结构:

  • Bits [31:13]:保留,写入时应为 0。
  • Bits [12:10]:CPUID,表示发起中断的 CPU ID。
  • Bits [9:0]:Interrupt ID,表示当前处理的中断号。

(8)返回

subs pc, lr, #4LR(Link Register)是一个专用寄存器,用于存储子程序调用的返回地址。当执行子程序调用(如 BL 或 BLX 指令)时,处理器会将返回地址存储在 LR 寄存器中,以便在子程序执行完毕后能够返回到调用点。
subs pc, lr, #4 指令将 LR 寄存器的值减去 4,并将结果赋值给 PC(程序计数器)寄存器,从而实现从中断处理程序返回到中断发生时的指令地址。

由于 ARM 是三级流水线构架:取指、译指、执行,PC指向的是正在取值的地址,如下例:

0x2000 MOV R1, R0
0x2004 MOV R2, R3
0x2008 MOV R4, R5

当正在执行 0x2000时,PC里保存的是0x2008地址,如果这时发生了中断,LR保存的是0x2008。所以返回时,需要返回到 LR - 4处的地址,即0x2004。

三、通用中断驱动编写

1. 移植 core_ca7.h

从 SDK 包中找到文件 core_ca7.h进行修改,留下与GIC相关的内容 。
这里直接使用了正点原子改后的头文件。
该文件主要有以下功能:

  • 实现了一些寄存器访问,如 CP15 协处理器寄存器 __get_SCTLR, __set_SCTLR, __get_ACTLR, __set_ACTLR 等;
  • 定义一些寄存器的结构体类型,如 CPSR_Type, SCTLR_Type, ACTLR_Type, CPACR_Type, DFSR_Type, IFSR_Type, ISR_Type等;

2. 向量表大小

如前面章节所提到, GIC 管理128个中断源(SPI)和32个CPU接口私有中断源(SGI+PPI),共160个中断源。
中断向量表的结构如下图所示:

|------------------------------------|
| 中断向量表项1(对应中断源1)     |
|------------------------------------|
| 中断向量表项2(对应中断源2)     |
|------------------------------------|
|       ...                        |
|------------------------------------|
| 中断向量表项N(对应中断源N)     |
|------------------------------------|

当中断发生的时候,IRQ调用 system_irqhandler , 函数根据中断ID值到表中找到对应中断函数地址。

3. 编写通用中断处理函数

执行编译命令,提示:

I.MX6U 裸机开发14.IRQ中断服务函数_嵌入式Linux_05


下面创建文件: bsp_irq.c 和 bsp_irq.h:

I.MX6U 裸机开发14.IRQ中断服务函数_寄存器_06

bsp_irq.h
//
// Created by Xundh on 2024/11/18.
//

#ifndef LEARN_I_MX6U_BSP_ISR_H
#define LEARN_I_MX6U_BSP_ISR_H
#include "imx6u.h"

/**
* @brief 中断处理函数, 这里定义的是一个名为 *system_irq_handler_t 函数指针类型, 用于注册中断服务函数
* @param gicc_iar 中断号
* @param param 参数
*/
typedef void (*system_irq_handler_t)(unsigned int gicc_iar, void *param);
/**
* @brief 中断处理函数结构体
 */
typedef struct _sys_irq_handle{
    system_irq_handler_t irqhandler;    /* 中断处理函数 */
    void *user_param;   /* 中断处理函数参数 */
}sys_irq_handle_t;

/**
* @brief 注册中断处理函数
*/
void sys_irq_handle_register(IRQn_Type irq, system_irq_handler_t handler, void *param);

/**
* @brief 中断初始化函数
*/
void bsp_int_init(void);
/**
* @brief 具体的中断处理函数, IRQ_Handler函数会调用此函数
 */
void system_irqhandler(unsigned int gicc_iar);
/**
* @brief 默认中断处理函数
*/
void default_irq_handler(unsigned int gicc_iar, void *param);

#endif //LEARN_I_MX6U_BSP_ISR_H
bsq_irq.c
//
// Created by Xundh on 2024/11/18.
//
#include "bsp_int.h"

// 记录中断嵌套次数
static unsigned int irq_nesting;

/**
* @brief 定义中断处理函数结构体
*/
static sys_irq_handle_t sys_irq_handle_table[NUMBER_OF_INT_VECTORS];

/**
* @brief 中断处理函数表初始化
 */
void sys_irq_table_init(void){
    unsigned  int i;
    irq_nesting = 0;

    for(i = 0; i < NUMBER_OF_INT_VECTORS; i++){
        sys_irq_handle_table[i].irqhandler = default_irq_handler;
        sys_irq_handle_table[i].user_param = NULL;
    }
}

/**
* @brief 注册中断处理函数
*/
void sys_irq_handle_register(IRQn_Type irq, system_irq_handler_t handler, void *param){
    sys_irq_handle_table[irq].irqhandler = handler;
    sys_irq_handle_table[irq].user_param = param;
}

/**
* @brief 中断初始化函数
*/
void bsp_int_init(void){
    // 初始化GIC
    GIC_Init();
    // 初始化中断处理函数表
    sys_irq_table_init();

    // 中断向量偏移设置
    __set_VBAR(0x87800000);
}
/**
* @brief 具体的中断处理函数, IRQ_Handler函数会调用此函数
 */
void system_irqhandler(unsigned int gicc_iar){
    // 取出中断号
    unsigned int int_num = gicc_iar & 0x3FF;
    // 检查中断号是否合法
    if(int_num >= NUMBER_OF_INT_VECTORS){
        return;
    }
    irq_nesting++;
    // 根据中断号取出中断处理函数
    sys_irq_handle_table[int_num].irqhandler(int_num, sys_irq_handle_table[int_num].user_param);
    irq_nesting--;
}

/**
*@brief  默认中断服务函数
*/
void default_irq_handler(unsigned int gicc_iar, void *param){
    while (1)
    {
        
    }
}