7

rust写个操作系统——课程实验blogos移至armV8深度解析:实验六 GPIO关机

 1 year ago
source link: https://noionion.top/ec262d5c.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

rust写个操作系统——课程实验blogos移至armV8深度解析:实验六 GPIO关机

发表于2022-04-26|更新于2022-05-21|BlogOS_armv8
字数总计:2.8k|阅读时长:10分钟|阅读量:13

你将在每个实验对应分支上都看到这句话,确保作者实验代码在被下载后,能在正确的环境中运行。

运行环境请参考: lab1 环境搭建

cargo build
qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/blogos_armv8 -semihosting

实验六 GPIO关机

我们不能一直到qemu的暴力退出来关机(就是不用系统的关机而暴力断电)。所幸,virt机器为我们提供了GPIO来实现关机功能。这节我们将编写GPIO相关的驱动来实现关机功能。


实验指导书中这节也没有写实验目的。我大致把目的划分如下:

  1. 编写pl061(GPIO)通用输入输出模块的驱动

  2. 实现关机功能


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机器关机原理

查看设备树:

gpio-keys {
#address-cells = <0x01>;
#size-cells = <0x00>;
compatible = "gpio-keys";

poweroff {
gpios = <0x8003 0x03 0x00>;
linux,code = <0x74>;
label = "GPIO Key Poweroff";
};
};

virt机器关机原理
virt机器关机原理

可以看到,关机键接入到了GPIO处理芯片的三号输入口(设备树上的反映在gpio-keyspoweroff["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的设备描述如下:

pl061@9030000 {
phandle = <0x8003>;
clock-names = "apb_pclk";
clocks = <0x8000>;
interrupts = <0x00 0x07 0x04>;
gpio-controller;
#gpio-cells = <0x02>;
compatible = "arm,pl061\0arm,primecell";
reg = <0x00 0x9030000 0x00 0x1000>;
};

可以看到GPIO设备的内存映射起始地址是0x09030000

由于GPIORIS是一个只读寄存器,而我们知道一旦关机该寄存器的值将变为0b00001000(三号线产生的中断),在此我们并不需要将其在代码中体现。因此,我们在本节实验中,只需要定义GPIOIEGPIOIC两个寄存器。

现在我们开始动手写驱动了,新建src/pl061.rs,先写入一个基本的模板。

use tock_registers::{registers::{ReadWrite, WriteOnly, ReadOnly}, register_bitfields, register_structs};

// 寄存器基址定义
pub const PL061REGS: *mut PL061Regs = (0x0903_0000) as *mut PL061Regs;

// 寄存器位级描述
register_bitfields! [
u32,
];

// 寄存器结构定义和映射描述
register_structs! {
pub PL061Regs {
}
}

我们自顶向下,从寄存器结构定义和映射开始,在到寄存器位级细节进行对应的定义:

寄存器基本结构描述

首先是两个寄存器的定义,我们查看GPIO的寄存器表:Summary of PrimeCell GPIO registers,找到我们需要的两个寄存器信息:

GPIOIE

GPIOIE

GPIOIC

GPIOIC

需要记下的是两个寄存器的基址和读写类型,我们可以作如下基本的定义:

// 寄存器结构定义和映射描述
register_structs! {
pub PL061Regs {
(0x410 => pub ie: ReadWrite<u32>),
(0x41c => pub ic: WriteOnly<u32>),
}
}

tock-registers对寄存器结构定义有如下的要求,我用加粗标识出我们需要注意的部分:

寄存器的定义是通过register_structs宏完成的,该宏要求每个寄存器有一个偏移量、一个字段名和一个类型。寄存器必须按偏移量的递增顺序连续顺序声明。定义寄存器时,必须使用偏移量和间隙标识符(按照惯例,使用名为_reservedN的字段)显式注释间隙,但不使用类型。然后,宏将自动计算间隙大小并插入合适的填充结构。结构的末尾用大小和@end关键字标记,有效地指向寄存器列表后面的偏移量。

寄存器基址从0x000开始,故我们填入空缺,并在最后一个寄存器的下一个地址填入@end标记:

// 寄存器结构定义和映射描述
register_structs! {
pub PL061Regs {
(0x000 => __reserved_0),
(0x410 => pub ie: ReadWrite<u32>),
(0x414 => __reserved_1),
(0x41c => pub ic: WriteOnly<u32>),
(0x420 => @END),
}
}
寄存器位级细节

首先是GPIOIE寄存器的细节定义,我们查看该寄存器的细节:Interrupt mask register, GPIOIE

可以知道每位的值即为对应输入输出线的中断掩码。例如第3号位(0开始)的中断启用,则应设置第三位的值为1。我们在register_bitfields!宏中写入我们需要的第三号位具体描述:

// 寄存器位级描述
register_bitfields![
u32,

// PrimeCell GPIO interrupt mask
pub GPIOIE [
IO3 OFFSET(3) NUMBITS(1) [
Disabled = 0,
Enabled = 1
]
],
];

这里的IO3只是对三号位的一个命名,OFFSET偏移参数指明该位为第三号位,NUMBITS指明该位功能共有1位。你在其它的定义中可能会见到以两位甚至更多来存储对应信息。

IO3下的键值更像是一种标识,定义后变可以以更方便的方式对寄存器进行读写。左边的命名是对右边赋值的解释。我们之后在解释时,不需要去记忆某一个位中赋值多少是什么功能,而可以通过命名去做精准的调用。例如官方文档示例中:

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一行:

-         (0x410 => pub ie: ReadWrite<u32>),
+ (0x410 => pub ie: ReadWrite<u32, GPIOIE::Register>),

发生中断时,回调处理中GPIOIC寄存器的值我们可以直接写入GPIOIE来描述,这里对不对其进行细节描述并不重要。当然对其具体定义也不会有太大的问题。

而在模板开头我们引入了类型READONLY,而我们定义完寄存器后并没有使用它,因此删除这个引用。

- use tock_registers::{registers::{ReadWrite, WriteOnly, ReadOnly}, register_bitfields, register_structs};
+ use tock_registers::{registers::{ReadWrite, WriteOnly}, register_bitfields, register_structs};

最后记得向src/main.rs中引入驱动:mod pl061;,最终的pl061模块驱动如下:

use tock_registers::{registers::{ReadWrite, WriteOnly}, register_bitfields, register_structs};

// 寄存器结构定义和映射描述
pub const PL061REGS: *mut PL061Regs = (0x0903_0000) as *mut PL061Regs;

// 寄存器位级描述
register_bitfields![
u32,

// PrimeCell GPIO interrupt mask
pub GPIOIE [
IO3 OFFSET(3) NUMBITS(1) [
Disabled = 0,
Enabled = 1
]
],
];

// 寄存器结构定义和映射描述
register_structs! {
pub PL061Regs {
(0x000 => __reserved_0),
(0x410 => pub ie: ReadWrite<u32, GPIOIE::Register>),
(0x414 => __reserved_1),
(0x41c => pub ic: WriteOnly<u32>),
(0x420 => @END),
}
}

个人也写了个较为完整的驱动(gpiodata那个描述可能有些问题),可以查看https://github.com/2X-ercha/blogOS-armV8/blob/lab6/src/pl061_all.rs

实现关机中断及其处理回调函数

关机中断仍然是el1_irq级别的中断,经过了上两个实验的回调函数编写,这部分可以说是熟门熟路了。

关机中断初始化

同前两个中断一样,我们还是需要对输入中断进行启用和配置。同时不一样的是,我们还要为GPIOGPIOIE中断掩码寄存器作初始化。修改src/interrupts.rs,新增如下内容:

// GPIO中断号39
const GPIO_IRQ: u32 = 39;

use tock_registers::interfaces::{Readable, Writeable};

pub fn init_gicv2() {
// ...

// 初始化GPIO中断
set_config(GPIO_IRQ, ICFGR_LEVEL); //电平触发
set_priority(GPIO_IRQ, 0); //优先级设定
clear(GPIO_IRQ); //清除中断请求
enable(GPIO_IRQ); //使能中断

// 使能GPIO的poweroff key中断
use crate::pl061::*;
unsafe{
let pl061r: &PL061Regs = &*PL061REGS;

// 启用pl061 gpio中的3号线中断
pl061r.ie.write(GPIOIE::IO3::Enabled);
}
}

关机中断处理回调

然后对关机中断进行处理:修改我们的中断实际处理函数handle_irq_lines为如下,并新增输入中断处理函数handle_gpio_irq

fn handle_irq_lines(ctx: &mut ExceptionCtx, _core_num: u32, irq_num: u32) {
if irq_num == TIMER_IRQ {
handle_timer_irq(ctx);
} else if irq_num == UART0_IRQ {
handle_uart0_rx_irq(ctx);
} else if irq_num == GPIO_IRQ {
handle_gpio_irq(ctx);
} else{
catch(ctx, EL1_IRQ);
}
}

fn handle_gpio_irq(_ctx: &mut ExceptionCtx){
use crate::pl061::*;
crate::println!("power off!\n");
unsafe {
let pl061r: &PL061Regs = &*PL061REGS;

// 清除中断信号 此时get到的应该是0x8
pl061r.ic.set(pl061r.ie.get());
// 关机
asm!("mov w0, #0x18");
asm!("hlt #0xF000");
}
}

我们尝试关机,这里用到了ArmSemihosting功能。

Semihosting 的作用

Semihosting 能够让 bare-metal 的 ARM 设备通过拦截指定的 SVC 指令,在连操作系统都没有的环境中实现 POSIX 中的许多标准函数,比如 printf、scanf、open、read、write 等等。这些 IO 操作将被 Semihosting 协议转发到 Host 主机上,然后由主机代为执行。

构建并运行内核。为了启用semihosting功能,在QEMU执行时需要加入-semihosting参数

cargo build
qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel target/aarch64-unknown-none-softfloat/debug/blogos_armv8 -semihosting

在系统执行过程中,在窗口按键ctrl + a, c,后输入system_powerdown关机。(这里为了实验更加直观,我注释掉了时间中断的打点输出)

关机!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK