rust写个操作系统——课程实验blogos移至armV8深度解析:实验六 GPIO关机
本文最后更新于:14 小时前
你将在每个实验对应分支上都看到这句话,确保作者实验代码在被下载后,能在正确的环境中运行。
运行环境请参考: lab1 环境搭建
1 |
|
实验六 GPIO关机
我们不能一直到qemu的暴力退出来关机(就是不用系统的关机而暴力断电)。所幸,virt机器为我们提供了GPIO来实现关机功能。这节我们将编写GPIO相关的驱动来实现关机功能。
实验目的
实验指导书中这节也没有写实验目的。我大致把目的划分如下:
编写pl061(GPIO)通用输入输出模块的驱动
实现关机功能
pl061(GPIO)模块驱动编写
上一实验我们已经对tock-registers
有了基础的了解,恰好实验六终于是有点意思,开始让我们自己写驱动了。
所以在这节我们将做一个示例,讲述我们该如何去描述一个硬件的驱动。
pl061(GPIO)基本知识
GPIO(General-purpose input/output)
,通用型之输入输出的简称,功能类似8051的P0—P3
,其接脚可以供使用者由程控自由使用,PIN脚依现实考量可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO),如当clk generator
, chip select
等。
既然一个引脚可以用于输入、输出或其他特殊功能,那么一定有寄存器用来选择这些功能。对于输入,一定可以通过读取某个寄存器来确定引脚电位的高低;对于输出,一定可以通过写入某个寄存器来让这个引脚输出高电位或者低电位;对于其他特殊功能,则有另外的寄存器来控制它们。
而在此实验中,我们用的arm
架构的GPIO文档在此:ARM PrimeCell General Purpose Input/Output (PL061) Technical Reference Manual
virt机器关机原理
查看设备树:
1 |
|
可以看到,关机键接入到了GPIO处理芯片的三号输入口(设备树上的反映在gpio-keys
的poweroff["gpios"]
第二个参数反映。当外部输入关机指令时,三号线将产生一次信号并发生一次中断。让我们记住这一点,这是实现关机功能的关键。
驱动编写实例
由于我们只需要实现关机功能,所以这里我们也并不定义额外的寄存器。之所以我在这里称之为一个示例,是因为我们并没有完整的实现它。
当我们向GPIO
中输入关机指令时,GPIORIS
(中断状态寄存器PrimeCell GPIO raw interrupt status
)中的第三位将从0
跳变到1
。而当GPIOIE
(中断掩码寄存器PrimeCell GPIO interrupt mask
)中的第三位为1
时,GPIO处理芯片将向GIC中断控制器发送一次中断,中断号为39
。而我们受到中断后,需要丢此次GPIO中断进行清除,将GPIOIC
(中断清除寄存器PrimeCell GPIO interrupt clear
)的对应位置为1
,然后进行关机操作。
另外在设备树文件中,关于GPIO的设备描述如下:
1 |
|
可以看到GPIO设备的内存映射起始地址是0x09030000
。
由于GPIORIS
是一个只读寄存器,而我们知道一旦关机该寄存器的值将变为0b00001000
(三号线产生的中断),在此我们并不需要将其在代码中体现。因此,我们在本节实验中,只需要定义GPIOIE
和GPIOIC
两个寄存器。
现在我们开始动手写驱动了,新建src/pl061.rs
,先写入一个基本的模板。
1 |
|
我们自顶向下,从寄存器结构定义和映射开始,在到寄存器位级细节进行对应的定义:
寄存器基本结构描述
首先是两个寄存器的定义,我们查看GPIO的寄存器表:Summary of PrimeCell GPIO registers,找到我们需要的两个寄存器信息:
需要记下的是两个寄存器的基址和读写类型,我们可以作如下基本的定义:
1 |
|
而tock-registers
对寄存器结构定义有如下的要求,我用加粗标识出我们需要注意的部分:
寄存器的定义是通过
register_structs
宏完成的,该宏要求每个寄存器有一个偏移量、一个字段名和一个类型。寄存器必须按偏移量的递增顺序和连续顺序声明。定义寄存器时,必须使用偏移量和间隙标识符(按照惯例,使用名为_reservedN的字段)显式注释间隙,但不使用类型。然后,宏将自动计算间隙大小并插入合适的填充结构。结构的末尾用大小和@end关键字标记,有效地指向寄存器列表后面的偏移量。
寄存器基址从0x000
开始,故我们填入空缺,并在最后一个寄存器的下一个地址填入@end
标记:
1 |
|
寄存器位级细节
首先是GPIOIE
寄存器的细节定义,我们查看该寄存器的细节:Interrupt mask register, GPIOIE
可以知道每位的值即为对应输入输出线的中断掩码。例如第3号位(0开始)的中断启用,则应设置第三位的值为1
。我们在register_bitfields!
宏中写入我们需要的第三号位具体描述:
1 |
|
这里的
IO3
只是对三号位的一个命名,OFFSET
偏移参数指明该位为第三号位,NUMBITS
指明该位功能共有1位。你在其它的定义中可能会见到以两位甚至更多来存储对应信息。
IO3
下的键值更像是一种标识,定义后变可以以更方便的方式对寄存器进行读写。左边的命名是对右边赋值的解释。我们之后在解释时,不需要去记忆某一个位中赋值多少是什么功能,而可以通过命名去做精准的调用。例如官方文档示例中:
1
2
3
4
5
6
7
8
9
Control [
RANGE OFFSET(4) NUMBITS(2) [
// Each of these defines a name for a value that the bitfield can be
// written with or matched against. Note that this set is not exclusive--
// the field can still be written with arbitrary constants.
VeryHigh = 0,
High = 1,
Low = 2
]]我们在对
Control
寄存器的[4:6]号位赋值低电平时,只需要使用xx::Control.write(xx::Control::Low)
,而无需记忆低电平是0
还是2
然后我们需要将寄存器结构描述中的寄存器与其细节联系起来,修改register_structs!
宏中的0X410
一行:
1 |
|
发生中断时,回调处理中GPIOIC
寄存器的值我们可以直接写入GPIOIE
来描述,这里对不对其进行细节描述并不重要。当然对其具体定义也不会有太大的问题。
而在模板开头我们引入了类型READONLY
,而我们定义完寄存器后并没有使用它,因此删除这个引用。
1 |
|
最后记得向src/main.rs
中引入驱动:mod pl061;
,最终的pl061
模块驱动如下:
1 |
|
个人也写了个较为完整的驱动(gpiodata那个描述可能有些问题),可以查看https://github.com/2X-ercha/blogOS-armV8/blob/lab6/src/pl061_all.rs
实现关机中断及其处理回调函数
关机中断仍然是el1_irq
级别的中断,经过了上两个实验的回调函数编写,这部分可以说是熟门熟路了。
关机中断初始化
同前两个中断一样,我们还是需要对输入中断进行启用和配置。同时不一样的是,我们还要为GPIO
的GPIOIE
中断掩码寄存器作初始化。修改src/interrupts.rs
,新增如下内容:
1 |
|
关机中断处理回调
然后对关机中断进行处理:修改我们的中断实际处理函数handle_irq_lines
为如下,并新增输入中断处理函数handle_gpio_irq
:
1 |
|
我们尝试关机,这里用到了Arm
的Semihosting
功能。
Semihosting 的作用
Semihosting 能够让 bare-metal 的 ARM 设备通过拦截指定的 SVC 指令,在连操作系统都没有的环境中实现 POSIX 中的许多标准函数,比如 printf、scanf、open、read、write 等等。这些 IO 操作将被 Semihosting 协议转发到 Host 主机上,然后由主机代为执行。
构建并运行内核。为了启用semihosting
功能,在QEMU执行时需要加入-semihosting
参数
1 |
|
在系统执行过程中,在窗口按键ctrl + a, c
,后输入system_powerdown
关机。(这里为了实验更加直观,我注释掉了时间中断的打点输出)