在工作需求,需要在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都是关闭中断的,所以要让定时器正常工作,需要完成以下几点:

  1. 打开ARM核心中断,这里主要是指IRQ中断
  2. 正确配置中断向量表,中断现场保存与恢复
  3. 正确配置GIC
  4. 正确配置定时器参数
  5. 编写好正确的中断服务函数

打开ARMv8中断

这一节描述如何打开ARMv8的IRQ中断,有兴趣的可以自行阅读ARMv8体系结构手册,可以从这里下载。

首先ARMv8运行在64位模式时与ARMv7有很大的区别,不能简单的通过修改CPSR寄存器来完成。

  1. 设置DAIF寄存器,打开IRQ中断
    64位模式下DAIF寄存器定义如下,其实DAIF寄存器只是将ARMv7中CPSR寄存器的A、I、F单独拿出来使用

    只需要将I位清空成0即可打开 IRQ 中断
    针对DAIF寄存器,体系结构中提供了2个位操作寄存器DAIFSetDAIFClr
    打开中断代码

    1
    2
    3
    4
    5
    6
    void enable_interrupts(void)
    {
    //开启IRQ
    asm volatile("msr daifclr, #2");
    return;
    }

    相应的关闭中断代码

    1
    2
    3
    4
    5
    6
    int disable_interrupts(void)
    {
    //关闭IRQ
    asm volatile("msr daifset, #2");
    return 0;
    }
  2. 配置中断路由

    中断部分,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
    13
    int 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核心的定时器中断打开,配置起来还是非常简单的,只需要几个操作:

  1. 打开 GIC Distributor总中断

  2. 打开CPU核心定时器中断(选择 physical timer,对应的中断是30)

  3. 打开GIC CPU interface总中断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void 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
21


void 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));
}

中断服务函数

中断服务函数需要完成几件必要的事情

  1. 中断现场保护(前面已经提到)
  2. 中断处理
  3. 中断现场恢复(前面已经提到)

中断处理分成3个步骤:

  1. 从GIC中找出当前的中断编号,对于此次应用,应该是30
  2. 重写设置定时器的CNTP_CVAL_EL0寄存器
  3. 写GIC的EOI寄存器,指示此次中断处理完毕

具体代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
void 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
bl do_irq
exception_exit