在工作需求,需要在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27.macro exception_exit
ldp x2, x0, [sp], #16
switch_el x11, 3f, 2f, 1f
3: msr elr_el3, x2
b 0f
2: msr elr_el2, x2
b 0f
1: msr elr_el1, x2
b 0f
0:
ldp x1, x2, [sp], #16
ldp x3, x4, [sp], #16
ldp x5, x6, [sp], #16
ldp x7, x8, [sp], #16
ldp x9, x10, [sp], #16
ldp x11, x12, [sp], #16
ldp x13, x14, [sp], #16
ldp x15, x16, [sp], #16
ldp x17, x18, [sp], #16
ldp x19, x20, [sp], #16
ldp x21, x22, [sp], #16
ldp x23, x24, [sp], #16
ldp x25, x26, [sp], #16
ldp x27, x28, [sp], #16
ldp x29, x30, [sp], #16
eret
.endm
这里再简单说一下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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void set_physical_timer(int timeout_ms)
{
/*定时器使用细节可查看ARMv8体系结构手册*/
unsigned long value, freq, cnt, cmp;
/*关闭定时器*/
value = 0;
asm volatile("msr CNTP_CTL_EL0, %0" : : "r" (value));
/*计算下次超时时间*/
asm volatile("mrs %0, CNTFRQ_EL0" : "=r" (freq));
asm volatile("mrs %0, CNTPCT_EL0" : "=r" (cnt));
cmp = cnt + (freq/1000)*timeout_ms;
asm volatile("msr CNTP_CVAL_EL0, %0" : :"r" (cmp));
/*打开定时器*/
value = 1;
asm volatile("msr CNTP_CTL_EL0, %0" : : "r" (value));
}
中断服务函数
中断服务函数需要完成几件必要的事情
中断现场保护(前面已经提到)
中断处理
中断现场恢复(前面已经提到)
中断处理分成3个步骤:
从GIC中找出当前的中断编号,对于此次应用,应该是30
重写设置定时器的CNTP_CVAL_EL0寄存器
写GIC的EOI寄存器,指示此次中断处理完毕
具体代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12void do_irq(struct pt_regs *pt_regs, unsigned int esr)
{
int irq;
irq = readl(GICC_BASE + GICC_IAR);
if((irq & 0x3ff) == 30) {
set_physical_timer(TIMER_PERIOD);
printf("%s.%d\n", __FUNCTION__, __LINE__);
}
writel(irq, GICC_BASE + GICC_EOIR);
}
1
2
3
4_do_irq:
exception_entry
bldo_irq
exception_exit