rust写个操作系统——课程实验blogos移至armV8深度解析:实验五 输入
你将在每个实验对应分支上都看到这句话,确保作者实验代码在被下载后,能在正确的环境中运行。
运行环境请参考: lab1 环境搭建
cargo build |
实验五 输入
QEMU的virt机器默认没有键盘作为输入设备,但当我们执行QEMU使用 -nographic 参数(disable graphical output and redirect serial I/Os to console)时QEMU会将串口重定向到控制台,因此我们可以使用UART作为输入设备。
同时这次实验也将顺便完成上一节没有完成的异常回调处理,我们将作对时钟中断和硬件中断的不同处理。
实验目的
实验指导书中这节就没有写实验目的了。我大致把目的划分如下:
完成实验四未完成的时钟中断处理回调
完成pl011(UART)异步串行接口的驱动编写
完成串口输入中断
时钟中断回调函数实现
在上一个实验中,我们实现了时间中断,但没有对引发的时间中断做处理回调。我们先扫尾,然后再来处理输入中断。
我们知道,时间中断后引发的异常是el1_irq
类中断,所以我们所需修改的是src/interrupts.rs
文件中关于el1_irq
的回调函数。原函数如下:
#[no_mangle] |
我们需要的是实现对时钟的中断进行准确的分辨,所以我们需要在该异常被处发后,读取中断号并作相应处理。
当定时器触发时间中断后,中断控制器的GICC_IAR
寄存器将被写入中断号30
。结合上节的GICC寄存器表,我们在GICC寄存器处新增两个需要调用的寄存器地址映射,定义如下:
//GICC寄存器基址 |
GICC_IAR
寄存器中存放的是当前的中断号。例如当时间中断发生时,寄存器中将写入中断号30
(前5位)和对应的内核编号(后三位),我们可以通过读取该寄存器中的值来做中断号识别GICC_EOIR
寄存器则用于标记某一中断被完成,即中断处理结束的信号。这个信号告诉控制器:中断已经被处理,并且系统已经准备好接收下一个中断。
基于以上,我们可以根据GIC手册修改el1_irq
处理回调函数,修改如下:
#[no_mangle] |
并编写中断处理函数handle_irq_lines
:
fn handle_irq_lines(ctx: &mut ExceptionCtx, _core_num: u32, irq_num: u32) { |
大致的流程还是很好理解的,我们编译运行后看看效果:
cargo build |
效果如下(每两秒将会有一次打点):
循环打点一方面是定时的功劳,另一方面是主函数中循环将系统置于低电平状态后的结果。每一次的中断处理后,系统将重新回到高电平运行状态。如果我们不采用loop
轮询,将只会发生一次打点,此后及时重新到达定时器时间并发送了时钟中断,GIC也不会进行处理(因为设置的是低电平触发)。
pl011(UART)异步串行接口驱动编写
QEMU的virt机器默认没有键盘作为输入设备,但当我们执行QEMU使用-nographic
参数(disable graphical output and redirect serial I/Os to console)时QEMU会将串口重定向到控制台,因此我们可以使用UART作为输入设备。
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART通常被集成于其他通讯接口的连结上。
UART作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。我们在控制台中的输入,也会被它传输到qemu中。
tock-registers
在实验四中,针对GICD,GICC,TIMER等硬件我们定义了大量的常量和寄存器值,这在使用时过于繁琐也容易出错。于是我们决定使用tock-registers
库。
tock-registers
提供了一些接口用于更好的定义寄存器。官方说明如下:
The crate provides three types for working with memory mapped registers:
ReadWrite
,ReadOnly
, andWriteOnly
, providing read-write, read-only, and write-only functionality, respectively. These types implement theReadable
,Writeable
andReadWriteable
traits.Defining the registers is done with the
register_structs
macro, which expects for each register an offset, a field name, and a type. Registers must be declared in increasing order of offsets and contiguously. Gaps when defining the registers must be explicitly annotated with an offset and gap identifier (by convention using a field named_reservedN
), but without a type. The macro will then automatically take care of calculating the gap size and inserting a suitable filler struct. The end of the struct is marked with its size and the@END
keyword, effectively pointing to the offset immediately past the list of registers.
翻译如下:
tock-registers 提供了三种类型的内存映射寄存器:ReadWrite、ReadOnly和WriteOnly,分别提供读写、只读和只读功能。这些类型实现了可读、可写和可读写特性。
寄存器的定义是通过
register_structs
宏完成的,该宏要求每个寄存器有一个偏移量、一个字段名和一个类型。寄存器必须按偏移量的递增顺序和连续顺序声明。定义寄存器时,必须使用偏移量和间隙标识符(按照惯例,使用名为_reservedN的字段)显式注释间隙,但不使用类型。然后,宏将自动计算间隙大小并插入合适的填充结构。结构的末尾用大小和@end关键字标记,有效地指向寄存器列表后面的偏移量。
根据官方的说明tock_registers作为一个示例,我们来实现pl011
串口驱动。
阅读设备树关于pl011
部分内容(实验二):
pl011@9000000 { |
由上可以看出,virt机器包含有pl011的设备,该设备的寄存器在0x9000000
开始处。pl011实际上是一个UART设备,即串口。可以看到virt选择使用pl011作为标准输出,这是因为与PC不同,大部分嵌入式系统默认情况下并不包含VGA设备。
而uart寄存器表也列出了UART相关的寄存器如下图所示:
我们可以开始定义pl011
驱动文件了。原则上来讲这部分内容应当定义在src/uart_console.rs
中。但为了避免代码过长,我们选择重构uart_console.rs
。
首先创建src/uart_console
目录,并将原uart_console.rs
更名为mod.rs
,且置于src/uart_console
目录下, 最后新建src/uart_console/pl011.rs
文件。目录结构看起来像这样:
. |
我们先需要在Cargo.toml
中的[dependencies]
节中加入依赖(这里实验指导书有误):
[dependencies] |
驱动编写
根据上述tock_registers
官方说明和寄存器表,我们修改src/uart_console/pl011.rs
如下:
use tock_registers::{registers::{ReadOnly, ReadWrite, WriteOnly}, register_bitfields, register_structs}; |
这里对以上读写内容也不再细讲。只需要知道的是pl011
的设备基址位于0x0900_0000
(第二行代码),然后根据寄存器表定义我们需要的寄存器:
register_structs! { |
这看起来好像比实验四中对应的寄存器描述部分要复杂,但如果你熟悉了之后,基本上可以依据技术参考手册中的寄存器描述无脑写了。(很多部分可以无脑抄)
然后我们在src/uart_console/mod.rs
中引入pl011.rs
,并修改write_byte
。
我们在前面对输出是直接定义寄存器常量的
pub fn write_byte(&mut self, byte: u8) { |
而现在我们已经定义好了UART
的寄存器表,可以选择直接调用pl011.rs
中定义的寄存器:
use tock_registers::{interfaces::Writeable}; |
由于我们较为完整的定义好了pl011
寄存器组,每次调用都需要一次初始化行为。故我们还需要为Writer
结构实现构造函数,并修改WRITER
宏的定义:
//往串口寄存器写入字节和字符串进行输出 |
最后是将无用的ptr
引用去除
- use core::{fmt, ptr}; |
至此,我们完成了所有关于pl011(uart)串口驱动的编写。
串口输入中断处理回调
第一节我们讲到了如何去实现timer中断的处理回调。而输入中断也是el1_irq
一类的中断。回到我们修改/新增的几个函数,我们将中断实际处理部分针对输入中断做一些判断和处理即可。
输入中断初始化
同时钟中断一样,我们还是需要对输入中断进行启用和配置。修改src/interrupts.rs
,新增如下内容:
// 串口输入中断号33 |
输入中断处理回调
然后对UART的数据接收中断进行处理:修改我们的中断实际处理函数handle_irq_lines
为如下,并新增输入中断处理函数handle_uart0_rx_irq
:
fn handle_irq_lines(ctx: &mut ExceptionCtx, _core_num: u32, irq_num: u32) { |
当我们输入一个字符后,uart产生一次输入中断,而输入中断处理函数则将我们输入的字符从寄存器中取出并调用print!
宏打印出来。
由此我们完成了输入中断的处理。我们进行代码的构建并运行:
cargo build |
当我们随意的在控制台中敲击字符,除去时钟中断的打点输出,我们将看到我们输入的字符。此时说明我们的输入中断是成功运作的。