一、 实验目的
-
理解中断的原理和机制,掌握CPU访问设备控制器的方法
-
掌握Arm体系结构的中断机制和规范,实现时钟中断服务
二、 实验过程
1、理解Arm架构的中断系统
arm的中断分为两类IRQ(普通中断),与FIQ(快速中断)。
Arm采用的中断控制器叫做GIC,即general interrupt controller。gic包括多个版本,如GICv1(已弃用),GICv2,GICv3,GICv4。简单起见,我们实验将选用GICv2版本。
-
GICv2 最多支持8个核的中断管理。
-
GIC包括两大主要部分(由图中蓝色虚竖线分隔,Distributor和CPU Interface由蓝色虚矩形框标示),分别是:
-
Distributor,其通过GICD_开头的寄存器进行控制(蓝色实矩形框标示)
-
CPU Interface,其通过GICC_开头的寄存器进行控制(蓝色实矩形框标示)
-
中断类型分为以下几类(由图中红色虚线椭圆标示):
-
SPI:(shared peripheral interrupt),共享外设中断。该中断来源于外设,通过Distributor分发给特定的core,其中断编号为32-1019。从图中可以看到所有核共享SPI。
-
PPI:(private peripheral interrupt),私有外设中断。该中断来源于外设,但只对指定的core有效,中断信号只会发送给指定的core,其中断编号为16-31。从图中可以看到每个core都有自己的PPI。
-
SGI:(software-generated interrupt),软中断。软件产生的中断,用于给其他的core发送中断信号,其中断编号为0-15。
-
virtual interrupt,虚拟中断,用于支持虚拟机。图中也可以看到,因为我们暂时不关心,所以没有标注。
2.分析virt.dts中intc和timer的部分
分析可得:intc中的 reg 指明GICD寄存器映射到内存的位置为0x8000000,长度为0x10000, GICC寄存器映射到内存的位置为0x8010000,长度为0x10000。
intc中的 #interrupt-cells 指明 interrupts 包括3个cells。
第一个cell为中断类型,0表示SPI,1表示PPI;
第二个cell为中断号,SPI范围为[0-987],PPI为[0-15];
第三个cell为flags,其中[3:0]位表示触发类型,4表示高电平触发,[15:8]为PPI的cpu中断掩码,每1位对应一个cpu,为1表示该中断会连接到对应的cpu。
timer设备,包括4个中断。以第二个中断的参数 0x01 0x0e 0x104 为例,其指明该中断为PPI类型的中断,中断号14, 路由到第一个cpu,且高电平触发。PPI的起始中断号为16,所以实际上该中断在GICv2中的中断号应为16 + 14 = 30。
3.在bsp目录下新建文件hwi_init.c,初始化GIC
#include "prt_typedef.h"
#include "os_attr_armv8_external.h"
#define OS_GIC_VER 2
#define GIC_DIST_BASE 0x08000000
#define GIC_CPU_BASE 0x08010000
#define GICD_CTLR (GIC_DIST_BASE + 0x0000U)
#define GICD_TYPER (GIC_DIST_BASE + 0x0004U)
#define GICD_IIDR (GIC_DIST_BASE + 0x0008U)
#define GICD_IGROUPRn (GIC_DIST_BASE + 0x0080U)
#define GICD_ISENABLERn (GIC_DIST_BASE + 0x0100U)
#define GICD_ICENABLERn (GIC_DIST_BASE + 0x0180U)
#define GICD_ISPENDRn (GIC_DIST_BASE + 0x0200U)
#define GICD_ICPENDRn (GIC_DIST_BASE + 0x0280U)
#define GICD_ISACTIVERn (GIC_DIST_BASE + 0x0300U)
#define GICD_ICACTIVERn (GIC_DIST_BASE + 0x0380U)
#define GICD_IPRIORITYn (GIC_DIST_BASE + 0x0400U)
#define GICD_ICFGR (GIC_DIST_BASE + 0x0C00U)
#define GICD_CTLR_ENABLE 1 /* Enable GICD */
#define GICD_CTLR_DISABLE 0 /* Disable GICD */
#define GICD_ISENABLER_SIZE 32
#define GICD_ICENABLER_SIZE 32
#define GICD_ICPENDR_SIZE 32
#define GICD_IPRIORITY_SIZE 4
#define GICD_IPRIORITY_BITS 8
#define GICD_ICFGR_SIZE 16
#define GICD_ICFGR_BITS 2
#define GICC_CTLR (GIC_CPU_BASE + 0x0000U)
#define GICC_PMR (GIC_CPU_BASE + 0x0004U)
#define GICC_BPR (GIC_CPU_BASE + 0x0008U)
#define IAR_MASK 0x3FFU
#define GICC_IAR (GIC_CPU_BASE + 0xc)
#define GICC_EOIR (GIC_CPU_BASE + 0x10)
#define GICD_SGIR (GIC_DIST_BASE + 0xf00)
#define BIT(n) (1 << (n))
#define GICC_CTLR_ENABLEGRP0 BIT(0)
#define GICC_CTLR_ENABLEGRP1 BIT(1)
#define GICC_CTLR_FIQBYPDISGRP0 BIT(5)
#define GICC_CTLR_IRQBYPDISGRP0 BIT(6)
#define GICC_CTLR_FIQBYPDISGRP1 BIT(7)
#define GICC_CTLR_IRQBYPDISGRP1 BIT(8)
#define GICC_CTLR_ENABLE_MASK (GICC_CTLR_ENABLEGRP0 | \
GICC_CTLR_ENABLEGRP1)
#define GICC_CTLR_BYPASS_MASK (GICC_CTLR_FIQBYPDISGRP0 | \
GICC_CTLR_IRQBYPDISGRP0 | \
GICC_CTLR_FIQBYPDISGRP1 | \
GICC_CTLR_IRQBYPDISGRP1)
#define GICC_CTLR_ENABLE 1
#define GICC_CTLR_DISABLE 0
// Priority Mask Register. interrupt priority filter, Higher priority corresponds to a lower Priority field value.
#define GICC_PMR_PRIO_LOW 0xff
// The register defines the point at which the priority value fields split into two parts,
// the group priority field and the subpriority field. The group priority field is used to
// determine interrupt preemption. NO GROUP.
#define GICC_BPR_NO_GROUP 0x00
#define GIC_REG_READ(addr) (*(volatile U32 *)((uintptr_t)(addr)))
#define GIC_REG_WRITE(addr, data) (*(volatile U32 *)((uintptr_t)(addr)) = (U32)(data))
void OsGicInitCpuInterface(void)
{
// 初始化Gicv2的distributor和cpu interface
// 禁用distributor和cpu interface后进行相应配置
GIC_REG_WRITE(GICD_CTLR, GICD_CTLR_DISABLE);
GIC_REG_WRITE(GICC_CTLR, GICC_CTLR_DISABLE);
GIC_REG_WRITE(GICC_PMR, GICC_PMR_PRIO_LOW);
GIC_REG_WRITE(GICC_BPR, GICC_BPR_NO_GROUP);
// 启用distributor和cpu interface
GIC_REG_WRITE(GICD_CTLR, GICD_CTLR_ENABLE);
GIC_REG_WRITE(GICC_CTLR, GICC_CTLR_ENABLE);
}
// src/arch/drv/gic/prt_gic_init.c
/*
* 描述: 去使能(禁用)指定中断
*/
OS_SEC_L4_TEXT void OsGicDisableInt(U32 intId)
{
// Interrupt Clear-Enable Registers
GIC_REG_WRITE(GICD_ICENABLERn + (intId / GICD_ICENABLER_SIZE)*sizeof(U32), 1<<(intId % GICD_ICENABLER_SIZE));
}
/*
* 描述: 使能指定中断
*/
OS_SEC_L4_TEXT void OsGicEnableInt(U32 intId)
{
// Interrupt Set-Enable Registers
GIC_REG_WRITE(GICD_ISENABLERn + (intId / GICD_ISENABLER_SIZE)*sizeof(U32), 1<<(intId % GICD_ISENABLER_SIZE));
}
OS_SEC_L4_TEXT void OsGicClearInt(uint32_t interrupt)
{
GIC_REG_WRITE(GICD_ICPENDRn + (interrupt / GICD_ICPENDR_SIZE)*sizeof(U32), 1 << (interrupt % GICD_ICPENDR_SIZE));
}
// 设置中断号为interrupt的中断的优先级为priority
OS_SEC_L4_TEXT void OsGicIntSetPriority(uint32_t interrupt, uint32_t priority) {
uint32_t shift = (interrupt % GICD_IPRIORITY_SIZE) * GICD_IPRIORITY_BITS;
volatile uint32_t* addr = ((volatile U32 *)(uintptr_t)(GICD_IPRIORITYn + (interrupt / GICD_IPRIORITY_SIZE) * sizeof(U32))) ;
uint32_t value = GIC_REG_READ(addr);
value &= ~(0xff << shift); // 每个中断占8位,所以掩码为 0xFF
value |= priority << shift;
GIC_REG_WRITE(addr, value);
}
// 设置中断号为interrupt的中断的属性为config
OS_SEC_L4_TEXT void OsGicIntSetConfig(uint32_t interrupt, uint32_t config) {
uint32_t shift = (interrupt % GICD_ICFGR_SIZE) * GICD_ICFGR_BITS;
volatile uint32_t* addr = ((volatile U32 *)(uintptr_t)(GICD_ICFGR + (interrupt / GICD_ICFGR_SIZE)*sizeof(U32)));
uint32_t value = GIC_REG_READ(addr);
value &= ~(0x03 << shift);
value |= config << shift;
GIC_REG_WRITE(addr, value);
}
/*
* 描述: 中断确认
*/
OS_SEC_L4_TEXT U32 OsGicIntAcknowledge(void)
{
// reads this register to obtain the interrupt ID of the signaled interrupt.
// This read acts as an acknowledge for the interrupt.
U32 value = GIC_REG_READ(GICC_IAR);
return value;
}
/*
* 描述: 标记中断完成,清除相应中断位
*/
OS_SEC_L4_TEXT void OsGicIntClear(U32 value)
{
// A processor writes to this register to inform the CPU interface either:
// • that it has completed the processing of the specified interrupt
// • in a GICv2 implementation, when the appropriate GICC_CTLR.EOImode bit is set to 1, to indicate that the interface should perform priority drop for the specified interrupt.
GIC_REG_WRITE(GICC_EOIR, value);
}
U32 OsHwiInit(void)
{
OsGicInitCpuInterface();
return OS_OK;
}
这段代码定义了一系列寄存器的地址、各种使能信号以及一系列相关函数;
①查阅Arm用户手册中的寄存器表,分析各寄存器的功能:
GICD:
GICC:
GICD_*寄存器:
-
GICD_CTLR:控制Distributor是否向CPU Interface转发中断。
-
GICD_ISENABLER:对于SPI和PPI类型的中断,每一位控制对应中断的转发行为。中断的使能可以通过GICD_ISENABLER寄存器设置。
-
GICD_ICENABLER:对于SPI和PPI类型的中断,每一位控制对应中断的转发行为。中断的失能可以通过GICD_ICENABLER寄存器设置。
-
GICD_ISPENDR:设置一个或一组中断为pending状态,若该中断当前为inactive状态,则转换为pending状态。若当前为pending状态,则转换为pending and active状态。该寄存器的每一个bit代表一个中断号。
-
GICD_ICPENDR:清除一个或一组中断为pending状态,该寄存器的每一个bit代表一个中断号。
-
GICD_IPRIORITYR:用于设置中断的优先级。这个寄存器是字节有效的,也就是一个字节,对应一个中断的优先级。优先级数值越小,那么这个中断的优先级越高。
-
GICD_ICFGR:中断的触发方式可分为边沿触发(上升沿、下降沿)和电平触发(高电平、低电平),两种触发方式的行为有所不同。用于设置SPI的中断触发方式。
GICC_*寄存器:
-
GICC_CTLR:此寄存器用来控制CPU interface传给CPU的中断信号。
-
GICC_PMR:提供优先级过滤功能,优先级高于某值的中断,才会发送给CPU。
-
GICC_BPR:此寄存器用来把8位的优先级字段拆分为组优先级和子优先级,组优先级用来决定中断抢占。
-
GICC_IAR:CPU读此寄存器,获得当前中断的interrtup ID。
-
GICC_EOIR:写此寄存器,表示某中断已经处理完毕。GICC_IAR的值表示当前在处理的中断,把GICC_IAR的值写入GICC_EOIR就表示中断处理完了。
②初始化GIC中定义的函数的功能
OsGicInitCpuInterface:首先通过向GICD_CTLR(分发器控制寄存器)和GICC_CTLR(CPU接口控制寄存器)写入禁用值(GICD_CTLR_DISABLE和GICC_CTLR_DISABLE),然后设置CPU接口的优先级掩码寄存器(GICC_PMR)和二进制点寄存器(GICC_BPR)。GICC_PMR用于设置接收中断的优先级阈值,只有高于这个阈值的中断才会被处理。GICC_BPR用于分割优先级字段,确定哪些中断有资格打断当前正在处理的中断。最后,启用GIC的分发器和CPU接口,这样GIC正常工作;
OsGicDisableInt:通过向GICD的ICENABLERn寄存器写入数据以禁用特定的中断(对应intID),由于每一个ICENABLERn寄存器可以处理GICD_ICENABLER_SIZE(32)个终端信号,所以用intID除以GICD_ICENABLER_SIZE得到寄存器号,intID模GICD_ICENABLER_SIZE得到寄存器内偏移,所以GIC_REG_WRITE(GICD_ICENABLERn + (intId / GICD_ICENABLER_SIZE)*sizeof(U32), 1<<(intId % GICD_ICENABLER_SIZE))将相应的禁用位设置为1;
OsGicEnsableInt:通过向GICD的ISENABLERn寄存器写入数据以启用特定的中断(对应intID),具体实现方式与禁用函数完全一致;
OsGicClearInt:通过向GICD的ICPENDRn寄存器写入数据以清除特定的中断(对应interrupt),具体实现方式与禁用函数完全一致;
OsGicIntSetPriority:与上文相似的思路,每一个ICPENDRn寄存器可以处理GICD_IPRIORITY_SIZE(4)个中断优先级信号,所以用interrupt除以GICD_ICPENDRn_SIZE得到寄存器号,interrupt模GICD_IPRIORITY_SIZE再乘上每个信号的大小得到寄存器内偏移,准备工作完成之后,根据寄存器号读出寄存器的值至value,根据偏移将寄存器相应位置设置为优先级priority,最后再写回寄存器;
OsGicIntSetConfig:与设置中断优先级的函数准备工作完全一致,根据寄存器号读出寄存器的值至value,根据偏移将寄存器相应位置设置为中断属性config,最后再写回寄存器;
最后两个函数OsGicIntAcknowledge与OsGicIntClear是共同使用的,前者读出当前中断的中断号,后者将中断号写入相应寄存器,表示中断处理结束。
4.在include目录下创建文件prt_config.h,设置中断时间间隔,tick处理时间不能超过1/OS_TICK_PER_SECOND(s)
5.在include目录下创建os_cpu_armv8.h文件
这个文件定义了异常处理等级,同时定义了Debug、Abort、IRQ和FIQ四个异常的屏蔽位,分别代表调试异常、终止异常、IRQ中断和FIQ中断,以及一些ARMv8架构中的内存屏障汇编指令,用来保证特定的内存访问顺序。
6.在bsp目录下创建time.c文件对定时器和相应的中断进行配置
7.构建时钟中断处理
① prt_vector.S 中的 EXC_HANDLE 5 OsExcDispatch 改为 EXC_HANDLE 5 OsHwiDispatcher,设置IRQ类型的异常处理函数
② 在 prt_vector.S 中加入 OsHwiDispatcher 处理代码,整体上下文的恢复与保存的结构与系统调用完全一致,C语言处理函数的入口变为OsExcDispatch
③ 在prt_exc.c中引用头文件 os_attr_armv8_external.h,os_cpu_armv8.h,同时定义C语言处理函数OsHwiDispatch,用于IRQ 类型的中断。
在中断处理函数中,先调用之前定义的函数OsGicIntAcknowledge获取当前的中断号,然后分析Arm用户手册可知
中断号的低10位表示interruptID,存储于irq_num,表示要访问的中断,高3位表示CPUID,存储于core_num,然后调用HandleActive函数,根据要访问的中断调用相应函数,最后清除中断。
④ 在src/kernel/tick目录下新建文件prt_tick.c文件,实现 OsTickDispatcher 时钟中断处理函数。
在函数 OsTickDispatcher中,首先将全局可屏蔽中断关闭(相关函数在后文介绍),并保存关闭前的状态(开启),令时钟计数器加一,再恢复为原来的状态,接着利用内联汇编代码设置中断周期,确定下一次产生时钟中断的时间,最后定义了一个函数可以获取当前的tick计数
⑤将新创建的文件纳入构建系统
src/kernel目录
src/kernel/tick目录
src目录,修改增加include目录、包含子目录和编译目标
src/bsp目录
⑥ 在prt_exc.c文件中实现OsIntLock 和 OsIntRestore 函数
这三个函数采用内联汇编,直接对寄存器进行操作,对于开启全局可屏蔽中断函数,首先将DAIF寄存器设置为输出对象,将DAIFClr设置为输入对象,用C语言变量state作为输出对象的接受变量,DAIF_IRQ_BIT作为立即数进行输入,从而清除DAIF寄存器的IRQ位,开启中断,第二个函数则是通过设置DAIF的IRQ位从而禁用中断,最后一个函数则是根据输入的状态对DAIF进行设置,相当于指定状态设置
⑦ 在src/bsp目录下创建头文件os_cpu_armv8_external.h,用于相关宏的定义
⑧ 在include目录下新建 prt_tick.h,声明 Tick 相关的接口函数.
8.最后修改main函数,完成时钟中断的构建
三、测试及分析
运行内核程序,发现正常引发时钟中断
四、lab5作业
实现 hwi_init.c 中缺失的 OsGicEnableInt 和 OsGicClearInt 函数。
与上文的分析一致,去使能指定中断通过向GICD的ICENABLERn寄存器写入数据以禁用特定的中断(对应intID),由于每一个ICENABLERn寄存器可以处理GICD_ICENABLER_SIZE(32)个终端信号,所以用intID除以GICD_ICENABLER_SIZE得到寄存器号,intID模GICD_ICENABLER_SIZE得到寄存器内偏移,所以GIC_REG_WRITE(GICD_ICENABLERn + (intId / GICD_ICENABLER_SIZE)*sizeof(U32), 1<<(intId % GICD_ICENABLER_SIZE))将相应的禁用位设置为1;
使能指定中断函数则是通过向GICD的ISENABLERn寄存器写入数据以启用特定的中断(对应intID),具体实现方式与禁用函数完全一致;
五、心得体会
-
深刻理解了中断的原理和机制,深刻理解CPU访问设备控制器的方法。
-
掌握Arm体系结构的中断机制和规范,实现时钟中断服务。
-
实现了时钟中断,并完成了一个定时器进行时钟中断处理。