uboot下开启ARMv8的定时器中断
在工作需求,需要在uboot下实现一个定时器来完成一些业务,使用的是Cortex A53 CPU(基于ARM v8-A架构),因此对ARMv8-A的定时器以及中断作了一些研究,这篇文章主要描述如在uboot下开启ARM v8的定时器中断。
文章内容会涉及到GIC及ARM v8-A(后面简写成ARMv8)架构相关知识,可查看下面2篇文章
、ARM v8架构注:本文中所使用和参考的uboot版本:U-Boot 2015.07
总体思路
一般情况下uboot都是关闭中断的,所以要让定时器正常工作,需要完成以下几点:
- 打开ARM核心中断,这里主要是指IRQ中断
- 正确配置中断向量表,中断现场保存与恢复
- 正确配置GIC
- 正确配置定时器参数
- 编写好正确的中断服务函数
打开ARMv8中断
这一节描述如何打开ARMv8的IRQ中断,有兴趣的可以自行阅读ARMv8体系结构手册,可以从这里下载。
首先ARMv8运行在64位模式时与ARMv7有很大的区别,不能简单的通过修改CPSR寄存器来完成。
设置DAIF寄存器,打开IRQ中断
64位模式下DAIF寄存器定义如下,其实DAIF寄存器只是将ARMv7中CPSR寄存器的A、I、F单独拿出来使用只需要将I位清空成0即可打开 IRQ 中断
针对DAIF寄存器,体系结构中提供了2个位操作寄存器DAIFSet、DAIFClr
打开中断代码1
2
3
4
5
6void enable_interrupts(void)
{
//开启IRQ
asm volatile("msr daifclr, #2");
return;
}相应的关闭中断代码
1
2
3
4
5
6int disable_interrupts(void)
{
//关闭IRQ
asm volatile("msr daifset, #2");
return 0;
}配置中断路由
中断部分,ARMv8与ARMv7最大的不同可能是中断路由了,因为ARMv8中取消了工作模式改用异常级别,因为中断可以通过配置被路由到不同的级别,比如一个中断发生后,可以进入EL1级别处理,也可以进入EL2级别处理。
目前工作使用的软硬件方向中,uboot启动后CPU运行中EL2级别(不确定其他硬件方向运行的级别),使用最小化修改代码框架原则,不修改uboot的运行级别,通过配置将相应的中断路由到EL2级别处理。
下图是我这次选择的中断路由配置,目前使用的硬件方案(A53)实现了EL2和EL3
其中HCR_EL2寄存器的默认值与上图的配置不一致,IMO默认是0,需要修改成1
当然其他方案的寄存器可能不同,需要根据实际情况修改
修改代码如下1
2
3
4
5
6
7
8
9
10
11
12
13int interrupt_init(void)
{
unsigned long value, cur_el;
/*如果当前处于EL2级别,需要设置HCR_EL2寄存器,将IRQ路由到EL2*/
asm volatile("mrs %0, CurrentEL" : "=r" (cur_el));
if(cur_el == 0x8) {
asm volatile("mrs %0, HCR_EL2" : "=r" (value));
value |= (1<<4);
asm volatile("msr HCR_EL2, %0" : : "r" (value));
}
return 0;
}
中断向量表
U-Boot 2015.07官方代码中已经正确配置了中断向量表,可以不用太关注。
ARMv8的中断向量表也ARMv7也有很大的区别,v7的中断向量表只有一个并且在固定位置。v8的不同异常级别对应不同的中断向量表,当然可以只实现一个异常级别的中断,比如linux kernel只使用EL1。
ARMv8的中断向量表由VBAR_ELx(x=1,2,3)决定,比如我此次使用的EL2级别,需要配置vbar_el2
因为官方代码已经实现,可以直接查看uboot源码,这里不再详解。
中断现场保护与恢复
与中断现场相关的寄存器
ELR_ELx(x=1,2,3,下同):进入ELx异常时保存需要返回的地址,功能ARMv7的LR寄存器类似
SP、SP_ELx:各级别所使用的栈指针,其中SP是当前模式下的栈指针,可以通过SPSel来选择(所有级别都使用SP_EL0、不同级别使用相应的SP_ELx)
SPSR_ELx:进入ELx异常时保存处理器的状态,异常返回时会自动恢复到相应的状态寄存器中,正常的中断程序可以不用处理
中断现场保护
将所有通用寄存器(x0-x30)和程序返回寄存器(ELR_ELx)入栈,入栈完成后即可进入中断处理流程
因为官方代码已经实现了入栈功能,可以直接查看uboot源码,这里不再详解。
中断现场恢复
相应的需要将通用寄存器(x0-x30)和程序返回寄存器(ELR_ELx)出栈,然后使用eret指令返回
代码如下
1 | .macro exception_exit |
这里再简单说一下eret指令
eret指令的大概作用是:使用当前的SPSR和ELR寄存器,将异常返回,SPSR寄存器的内容会恢复到PSTATE寄存器中,程序从ELR指向的地址继续执行
配置GIC
GIC是ARM CPU里的中断控制器,对于没有了解过的人来说还是有一点小小的复杂,不过如果只是将ARM核心的定时器中断打开,配置起来还是非常简单的,只需要几个操作:
打开 GIC Distributor总中断
打开CPU核心定时器中断(选择 physical timer,对应的中断是30)
打开GIC CPU interface总中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void timer_gic_init(void)
{
uint32_t value;
/*打开GIC Distributor总中断*/
value = readl(GICD_BASE+GICD_CTLR);
value |= 1;
writel(value, GICD_BASE+GICD_CTLR);
/*打开Non-secure physical timer中断,具体可以看GIC-400手册*/
value = readl(GICD_BASE+GICD_ISENABLERn);
value |= (1<<30);
writel(value, GICD_BASE+GICD_ISENABLERn);
/*打开GIC CPU interface总中断*/
value = readl(GICC_BASE+GICC_CTLR);
value |= 1;
writel(value, GICC_BASE+GICC_CTLR);
}
配置定时器参数
参考了linux内核,选择physical timer定时器,详细的配置可以查看ARMv8体系结构手册
与定时器相关的寄存器:
CNTFRQ_EL0:系统定时器的频率,由硬件决定,软件被始化时需要填写正确的值,目前我使用的是24Mhz
CNTP_CTL_EL0, :定时器使能(包括中断使能)控制
CNTPCT_EL0:定时器计数,只要CPU在运行(没有休眠)就会一直累加,累加的频为CNTFRQ_EL0,不可关闭
CNTP_CVAL_EL0:比较寄存器,如果定时器使能且中断已经打开(由CNTP_CTL_EL0控制),当CNTPCT_EL0计数达到CNTP_CVAL_EL0时,就会产生中断
根据以上几个寄存器的描述可以,只要在CNTP_CVAL_EL0里写入合适的值即可产生想要的中断,具体代码如下:
1 |
|
中断服务函数
中断服务函数需要完成几件必要的事情
- 中断现场保护(前面已经提到)
- 中断处理
- 中断现场恢复(前面已经提到)
中断处理分成3个步骤:
- 从GIC中找出当前的中断编号,对于此次应用,应该是30
- 重写设置定时器的CNTP_CVAL_EL0寄存器
- 写GIC的EOI寄存器,指示此次中断处理完毕
具体代码如下 :
1 | void do_irq(struct pt_regs *pt_regs, unsigned int esr) |
1 | _do_irq: |