9.3 设备驱动概述
Last updated
Last updated
通常来说,管理设备的代码称为驱动,所有的驱动都在内核中。我们今天要看的是UART设备的驱动,代码在uart.c文件中。如果我们查看代码的结构,我们可以发现大部分驱动都分为两个部分,bottom/top。
bottom部分通常是Interrupt handler。当一个中断送到了CPU,并且CPU设置接收这个中断,CPU会调用相应的Interrupt handler。Interrupt handler并不运行在任何特定进程的context中,它只是处理中断。
top部分,是用户进程,或者内核的其他部分调用的接口。对于UART来说,这里有read/write接口,这些接口可以被更高层级的代码调用。
通常情况下,驱动中会有一些队列(或者说buffer),top部分的代码会从队列中读写数据,而Interrupt handler(bottom部分)同时也会向队列中读写数据。这里的队列可以将并行运行的设备和CPU解耦开来。
通常对于Interrupt handler来说存在一些限制,因为它并没有运行在任何进程的context中,所以进程的page table并不知道该从哪个地址读写数据,也就无法直接从Interrupt handler读写数据。驱动的top部分通常与用户的进程交互,并进行数据的读写。我们后面会看更多的细节,这里是一个驱动的典型架构。
在很多操作系统中,驱动代码加起来可能会比内核还要大,主要是因为,对于每个设备,你都需要一个驱动,而设备又很多。
接下来我们看一下如何对设备进行编程。通常来说,编程是通过memory mapped I/O完成的。在SiFive的手册中,设备地址出现在物理地址的特定区间内,这个区间由主板制造商决定。操作系统需要知道这些设备位于物理地址空间的具体位置,然后再通过普通的load/store指令对这些地址进行编程。load/store指令实际上的工作就是读写设备的控制寄存器。例如,对网卡执行store指令时,CPU会修改网卡的某个控制寄存器,进而导致网卡发送一个packet。所以这里的load/store指令不会读写内存,而是会操作设备。并且你需要阅读设备的文档来弄清楚设备的寄存器和相应的行为,有的时候文档很清晰,有的时候文档不是那么清晰。
下图中是SiFive主板中的对应设备的物理地址,
例如,0x200_0000对应CLINT,0xC000000对应的是PLIC。在这个图中UART0对应的是0x1001_0000,但是在QEMU中,我们的UART0的地址略有不同,因为在QEMU中我们并不是完全的模拟SiFive主板,而是模拟与SiFive主板非常类似的东西。
以上就是Memory-mapped IO。
下图是UART的文档。16550是QEMU模拟的UART设备,QEMU用这个模拟的设备来与键盘和Console进行交互。
这是一个很简单的芯片,图中表明了芯片拥有的寄存器。例如对于控制寄存器000,如果写它会将数据写入到寄存器中并被传输到其他地方,如果读它可以读出存储在寄存器中的内容。UART可以让你能够通过串口发送数据bit,在线路的另一侧会有另一个UART芯片,能够将数据bit组合成一个个Byte。
这里还有一些其他可以控制的地方,例如控制寄存器001,可以通过它来控制UART是否产生中断。实际上对于一个寄存器,其中的每个bit都有不同的作用。例如对于寄存器001,也就是IER寄存器,bit0-bit3分别控制了不同的中断。这个文档还有很多内容,但是对于我们这节课来说,上图就足够了。不过即使是这么简单的一个设备,它的文档也有很多页。
学生提问:如果你写入数据到Transmit Holding Register,然后再次写入,那么前一个数据不会被覆盖掉吗?
Frans教授:这是我们需要注意的一件事情。我们通过load将数据写入到这个寄存器中,之后UART芯片会通过串口线将这个Byte送出。当完成了发送,UART会生成一个中断给内核,这个时候才能再次写入下一个数据。所以内核和设备之间需要遵守一些协议才能确保一切工作正常。上图中的UART芯片会有一个容量是16的FIFO,但是你还是要小心,因为如果阻塞了16个Byte之后再次写入还是会造成数据覆盖。