本文节选自《实验指导手册》第二版第16.6章
实验指导手册是奔跑吧Linux内核入门篇第二版配套实验书,pdf版本已经release,可以免费下载和自由打印!
下载方法:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。

本文是《奔跑吧Linux内核 入门篇》第二版中第16章的实验16-4:中断实验。在实现操作系统基本功能之前,我们先实现中断处理功能,我们以树莓派上ARM Core里面的generic timer为中断源,把中断这条路径先打通,以后在做进程创建和进程切换的时候也是需要用的,可以作为时钟中断来使用。

20220318_154205_024.jpg
20220318_154205_025.jpg

1.实验目的

1)了解和熟悉ARM64汇编语言。
2)了解和熟悉ARM64的异常等级处理。
3)了解和熟悉ARM64的中断处理流程。
4)了解和熟悉树莓派中系统定时器(system timer)的用法。

2.实验要求

1)在boot.s中实现对ARM64异常向量表的支持。
2)将树莓派中的系统定时器作为中断源,编写中断处理程序,每当有定时器中断到来时输出“Timer interrupt occured”。

3.实验详解

本实验对初学者有一定难度,读者需要熟悉如下几个方面。

ARMv8体系结构中的中断处理过程。

树莓派3B以及树莓派4B上的中断控制器。

树莓派上的定时器外设

ARMv8上的generic timer

中断上下文保存与恢复

3.1 中断处理过程
ARM64处理器核心一般都有两个和中断相关的管脚:nIRQ 和 nFIQ,并且是每个处理器核心都有一对这样的管脚。

640 (2).png

另外,PSTATE状态中有两个比特位和中断相关:

I:用来屏蔽IRQ中断

F:用来屏蔽FIQ中断

下图是ARM64处理器发生中断的处理流程。

640 (3).png

3.2 树莓派上的中断控制器
ARM公司提供了标准的GIC控制器,例如树莓派4b上支持GIC-400,树莓派3b上支持传统的中断方式(legacy interrupt)。

640 (4).png

树莓派4b上提供和支持两种中断控制器,而树莓派3b只支持传统的中断控制器。

GIC-400 (默认)

传统的中断控制器(legacy interrupt controller)

树莓派3b和4b支持多种中断源,如图所示。

ARM Core N:ARM Core本身的中断源,例如Core里面的generic timer

ARMC:可以被VPU和CPU访问的中断源,例如mailbox等

ARM_LOCAL:只能被CPU访问的中断源,例如本地timer等

20220318_154205_026.jpg

下面这个图是树莓派传统中断控制器的路由情况。
640 (5).png

从图上可以看到,ARM core N和ARM_LOCAL的中断都会路由到ARM_LOCAL routing的硬件单元,而ARMC中断会被路由到ARMC routing的硬件单元。
下面这个图是中断状态的路由情况。

640 (6).png

中断状态寄存器 路由过程:

先读SOURCEn中断状态寄存器

SOURCEn寄存器 bit 8 是否置位,若置位,读取PENDING2寄存器

若PENDING2的bit24置位,则读PENDING0寄存器

若PENDING2的bit25置位,则读PENDING1寄存器

树莓派4b上还支持GIC-400的中断控制器,这个留给读者自己去学习。

3.3 ARM Core的generic timer
我们以ARM core上的generic timer为例来说明,如何在树莓派上使用timer中断。
Cortex-A72支持4个ARM Core的generic timer:

CNT_PS_IRQ:Secure EL1 Physical Timer Event interrupt

CNT_PNS_IRQ:Nonsecure EL 1 Physical Timer Event interrupt

CNT_HP_IRQ:Hypervisor Physical Timer Event interrupt, EL2

CNT_V_IRQ:Virtual Timer Event interrupt EL3
其中,P是physical的意思,S是secure,指的是是不是安全世界还是非安全世界,armv8支持 trustzone,在trustzone的软件跑在安全世界里,trustzone外面的软件跑在非安全世界。第二个是,PNS,NS是non-secure的意思,非安全世界的timer,第三个HP timer,HP指的是hypervisor的意思,也就是A72手册里说的ANon-secure EL2 physical timer,第4个是v timer,指的是virtual timer。
这4个generic timer的中断相关设置是在ARM LOCAL中断组的寄存器里。它有4个IRQ_SOURCE寄存器,每个CPU一个,表示中断源的状态,4个FIQ_SOURCE寄存器,并且每个CPU一个。4个TIMER_CNTRL寄存器,每个CPU一个,用来enable对应的中断源。

640 (8).png

我们这个实验以Nonsecure EL 1 Physical Timer为例,也就是树莓派手册上说的PNS timer,相关初始化的寄存器是在ARM Core中,见armv8.6手册。和他相关的寄存器也就2~3个,所以timer是非常简单的设备。

第一个寄存器:CNTP_CTL_EL0, Counter-timer Physical Timer Control register. (第D13.8.16章)。比较相关的是第bit0,enable bit,表示打开这个timer。Imask表示中断是mask。ISTATES是一个中断状态位。

640 (9).png

第二寄存器:CNTP_TVAL_EL0, Counter-timer Physical Timer TimerValue register (第D13.8.18章)
这里是timervalue,timervalue的方式,就是初始化一个值,当递减到0的时候,timer中断就触发。

640 (10).png

64位的 comparevalue的方式
最简单的方式就是timervalue的方式,就是你初始化一个值,让它递减,然后递减到0,timer中断就触发了,我们实验也用这种方式。

3.4 例子
下面是EL1的Nonsecure generic timer的中断处理流程:
a) 初始化timer,设置cntp_ctl_el0寄存器的enable域为1
b) 给timer的TimeValue一个初值,设置cntp_tval_el0寄存器
c) 打开树莓派中断控制器中和timer相关的中断,设置TIMER_CNTRL0寄存器中的CNT_PNS_IRQ为1.
d) 打开PSTATE寄存器中的IRQ中断总开关

e) Timer中断发生
f) 跳转到el1_irq汇编函数
g) 保存中断上下文(使用kernel_entry宏)
h) 跳转到中断处理函数
i) 读取ARM_LOCAL中中断状态寄存器IRQ_SOURCE0
j) 判断是否CNT_PNS_IRQ中断发生
k) 如果是,重新设置TimeValue

l) 返回到el1_irq汇编函数
m) 恢复中断上下文
n) 返回中断现场

3.5 中断上下文
中断现场是发生中断时候那个瞬间的现场,包括CPU处理器的一些状态,对于ARM64处理器来说,中断上下文包括如下内容:

PSTATE寄存器

PC值

SP值

X0~x30寄存器

使用一个栈框数据结构来描述需要保存的中断现场(struct pt_regs)。

640 (11).png

中断发生时,中断上下文保存到当前进程的内核栈里,使用栈框来协助保存中断上下文的内容。

640 (12).png

本实验的保存中断上下文的参考代码是在arch/arm64/kernel/entry.S文件里。

/*
保存异常发生时候的上下文

保存x0~x29,x30(lr),sp, elr, spsr保存到 栈中
*/

.macro kernel_entry el
/*
   SP指向了栈底, S_FRAME_SIZE表示一个栈框的大小.
   定义一个struct pt_regs来描述一个栈框,
   用在异常发生时保存上下文.
 */
sub sp, sp, #S_FRAME_SIZE

/*
   保存通用寄存器x0~x29到栈框里pt_regs->x0~x29
 */
stp x0, x1, [sp, #16 *0]
stp x2, x3, [sp, #16 *1]
stp x4, x5, [sp, #16 *2]
stp x6, x7, [sp, #16 *3]
stp x8, x9, [sp, #16 *4]
stp x10, x11, [sp, #16 *5]
stp x12, x13, [sp, #16 *6]
stp x14, x15, [sp, #16 *7]
stp x16, x17, [sp, #16 *8]
stp x18, x19, [sp, #16 *9]
stp x20, x21, [sp, #16 *10]
stp x22, x23, [sp, #16 *11]
stp x24, x25, [sp, #16 *12]
stp x26, x27, [sp, #16 *13]
stp x28, x29, [sp, #16 *14]

/* x21: 栈顶 的位置*/
add     x21, sp, #S_FRAME_SIZE

mrs     x22, elr_el1
mrs     x23, spsr_el1

/* 把lr保存到pt_regs->lr, 把sp保存到pt_regs->sp位置*/
stp     lr, x21, [sp, #S_LR]
/* 把elr_el1保存到pt_regs->pc中
   把spsr_elr保存到pt_regs->pstate中*/
stp     x22, x23, [sp, #S_PC]
.endm

中断返回时,从当前进程的内核栈中恢复中断现场。

640 (13).png

本实验的恢复中断上下文的参考代码是在arch/arm64/kernel/entry.S文件里。

/*
恢复异常发生时保存下来的上下文
*/

.macro kernel_exit el
/* 从pt_regs->pc中恢复elr_el1,
   从pt_regs->pstate中恢复spsr_el1
 */
ldp     x21, x22, [sp, #S_PC]           // load ELR, SPSR
msr     elr_el1, x21                    // set up the return data
msr     spsr_el1, x22

ldp     x0, x1, [sp, #16 * 0]
ldp     x2, x3, [sp, #16 * 1]
ldp     x4, x5, [sp, #16 * 2]
ldp     x6, x7, [sp, #16 * 3]
ldp     x8, x9, [sp, #16 * 4]
ldp     x10, x11, [sp, #16 * 5]
ldp     x12, x13, [sp, #16 * 6]
ldp     x14, x15, [sp, #16 * 7]
ldp     x16, x17, [sp, #16 * 8]
ldp     x18, x19, [sp, #16 * 9]
ldp     x20, x21, [sp, #16 * 10]
ldp     x22, x23, [sp, #16 * 11]
ldp     x24, x25, [sp, #16 * 12]
ldp     x26, x27, [sp, #16 * 13]
ldp     x28, x29, [sp, #16 * 14]

/* 从pt_regs->lr中恢复lr*/
ldr     lr, [sp, #S_LR]
add     sp, sp, #S_FRAME_SIZE           // restore sp
eret                                    // return to kernel
.endm

4.实验步骤

本实验的参考代码可以在QEMU以及树莓派3b上运行。
除了ARM core里面的generic timer之后,树莓派3b和4b还额外支持System Timer and ARM side Timer这两个外设,不过由于QEMU模拟器并不支持这两个外设,所以本实验采用ARM core里面的generic timer。

4.1 QEMU上模拟

我们先在QEMU上模拟。由于本实验的参考代码还没有实现对GIC-400中断控制器的支持,因此,在QEMU里我们只能模拟树莓派3b了。
在Ubuntu Linux主机中,进入参考实验代码目录。

rlk@rlk :$ cd /home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer/
进入make menuconfig菜单。

rlk@master:lab04_generic_timer $ make menuconfig
其中:

Board selection (Raspberry 3B) -> 选树莓派3b
Uart for Pi (pl_uart) -> 选PL串口
640 (14).png

编译并运行。

rlk@master:lab04_generic_timer $ make
rlk@master:lab04_generic_timer $ make run
640 (1).png

4.2 在树莓派3b上运行
把4.1章编译的benos.bin文件拷贝到MicroSD里(可以参考第16.1章介绍的VMWare Player和Windows主机共享的方法来拷贝到主机,然后在拷贝到MicroSD卡里),并且修改名字为benos3.bin。
把MicroSD插回树莓派3b上,然后上电运行。在串口中可以看到如下日志信息。

640 (15).png

5. 在树莓派4b上运行
我们想在树莓派4b上运行本实验的参考代码。
如果刚才已经编译过的话,首先make clean一遍。

rlk@master:lab04_generic_timer $ make clean
进入make menuconfig菜单。

rlk@master:lab04_generic_timer $ make menuconfig
其中:

Board selection (Raspberry 4B) -> 选树莓派4b
Uart for Pi (pl_uart) -> 选PL串口
640 (16).png

然后重新编译。

rlk@master:lab04_generic_timer $ make
按照之前的方法,把编译的benos.bin文件拷贝到MicroSD里。然后,把MicroSD卡插回树莓派4B上。启动树莓派。
我们从串口里发现打印了启动信息,但是没有打印“Core0 Timer interrupt received”。

640 (17).png

那是什么原因导致的?为什么树莓派3b能跑,而树莓派4b就不能跑呢?

我们在前面介绍过,树莓派3b和树莓派4b的中断控制器有一点不一样。树莓派3b默认只支持传统的中断控制器,而树莓派4b支持两种:传统的中断控制器和GIC-400中断控制器。而且树莓派4b默认支持GIC-400。我们的实验参考代码只是对传统中断控制器进行了支持,暂时还没有支持GIC-400。
因此,这里有两种解决方法。

方案一:修改配置,让树莓派4b默认使用传统的中控制器。
方案二:修改代码,让其支持GIC-400。
下面给大家一个方案一的hack做法。
1.需要修改MicroSD卡boot分区里的bcm2711-rpi-4-b-nogic.dtb文件。使用hexedit工具来修改,把里面的“arm,gic-400”字符串暴力修改为“not,gic-400”。其实,只要把这个字符串修改了就行。这样,树莓派4b的固件就不能找到设备树上关于GIC的描述,它就会自动切换到传统的中断控制器中。
2.修改MicroSD卡boot分区的config.txt文件,新增一个选项“enable_gic=0”。

[pi4]

kernel=benos4.bin
enable_gic=0

上述两个文件在:
/home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer/hack_rpi4目录

把MicroSD卡重新插入到树莓派4b上,重新上电,就会看到树莓派4b板子上能打印“Core0 Timer interrupt received”。

640 (18).png

关于方案二,我们留给学有余力的读者自行完成。

实验代码

实验参考代码runninglinuxkenrel_5.0的git repo。

国内访问:
https://benshushu.coding.net/...

github:
https://github.com/figozhang/...
本文对应的参考代码在如下目录:kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab04_generic_timer

我们提供配置好的实验环境,基于ubuntu 20.04的VMware/Vbox镜像,可以通过如下方式获取下载地址:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。

20220318_154205_032.jpg


奔跑吧Linux社区
4 声望4 粉丝

奔跑吧Linux社区,为广大小伙伴布道Linux开源!


引用和评论

0 条评论