6.2 驱动程序

驱动程序是允许对硬件设备进行抽象访问的一段代码。在 Loko Scheme 中,它们被收集在 drivers 目录中。

6.2.1 驱动程序抽象

编写驱动程序的很大一部分工作是将硬件适配到系统其余部分使用的抽象。有时这意味着硬件的全部功能被隐藏。由 UART 支持的串行端口可以表现为一个 Scheme 输入端口和一个输出端口,这将允许你将其连接到任何使用端口的 Scheme 代码。但 UART 可以做的事情远不止 Scheme 端口所能做的。输出端口通常不能控制波特率或发送中断信号。

Scheme 缺乏对硬件的抽象。在 RnRS 标准中,文件和端口是唯一可以作为硬件抽象的东西。这并不太糟糕,因为 Unix 系统已经证明,许多硬件可以表示为 /dev 中的特殊文件以及打开这些特殊文件时获得的文件描述符。

然而,这些文件描述符只是允许用户空间与驱动程序通信的句柄。有时通信通过抽象层进行,有时几乎直接到达驱动程序。用户空间需要使用特殊的系统调用(例如 ioctl)来实际执行除读/写操作之外的任何有趣操作。

Loko 的驱动程序不应被锁定在任何特定的用户空间接口设计方式上。这类决策是在不同的层面上做出的。这将允许开发人员尝试不同的用户空间设计。像 Barrelfish 这样的设计,其中硬件虚拟化设备直接分发给用户空间,应该是可以表达的。

Loko 中的驱动程序应编写为提供便捷 API 的库。这些 API 应以同类相似硬件通用的方式提供基本功能。接口需要随时间发展。

当需要并发时(现代设备最常见的情况),驱动程序应使用通道与系统其余部分通信。并发驱动程序可以启动任意数量的纤程。在这些通道上发送的消息最好是简单对象,如向量、符号、对和 fixnums。这些消息成为驱动程序 API 的一部分。

6.2.2 硬件访问

驱动程序需要访问其设备。这取决于设备所连接的总线类型。

现代 PC 具有可以探测的总线,如 PCI 和 USB。此过程提供足够的信息,使你可以轻松检测总线上的设备类型以及如何访问它们。硬件往往呈现为树状结构,因此很自然地,总线驱动程序会将总线引用传递到调用栈中。

PC 上的旧设备不出现在 PCI 总线上,应该通过知道它们应该存在来检测和启动,因为这是一台 PC。大多数 ARM 系统没有 PCI 总线,其所有设备都位于需要提前知道的地址上,但不同平台之间有所不同。一个流行的解决方案是使用 DeviceTree 来编码这些信息,这当然值得为 Loko 探索。

主要的硬件交互点有:

  1. 扫描和配置总线;检测新的和已移除的设备。
  2. 设置对设备的访问。
  3. 通过设备的寄存器、通道等与设备交互。
  4. 允许设备写入系统内存。
  5. 等待来自设备的中断。

这些事情的做法取决于总线。需要更多的文档。目前,请参考源代码或询问。

6.2.3 驱动程序的未来方向

eval 使用在线编译时,可以对 PCI 设备驱动程序做一件有趣的事情。PCI 设备可以出现在内存中的任何位置,有时甚至可以出现在 I/O 空间的任何位置。寄存器访问可以像这样:

(define (driver·pci·uhci dev controller)
  ;; UHCI 寄存器映射到 BAR4 中的位置
  (let ((bar (vector-ref (pcidev-BARs dev) 4)))
    ;; 禁用键盘和鼠标传统支持
    (pci-put-u16 dev #xC0 #x0000)
    (driver·uhci (if (pcibar-i/o? bar) 'i/o 'mem)
                 (pcibar-base bar)
                 (pcibar-size bar)
                 (pcidev-irq dev)
                 controller)))

(define (driver·uhci reg-type reg-base reg-size irq controller)
  ;; 设备寄存器访问(独立于 i/o 与 mem)
  (define (reg-u8-ref offset)
    (assert (fx<? -1 offset reg-size))
    (case reg-type
      ((i/o) (get-i/o-u8 (fx+ reg-base offset)))
      ((mem) (get-mem-u8 (fx+ reg-base offset)))
      (else (assert #f))))
  ...)

如果 driver·pci·uhci 使用 eval 来编译驱动程序的专用版本,其中 reg-u8-ref(等)已被 cp0 内联,那将非常有趣。编译后,每个 reg-u8-ref 调用将只是一条指令。专用化、编译和启动驱动程序可以如此简单:

(let ((driver·uhci
       (eval `(lambda (controller)
                (driver-source·uhci ,reg-type ,reg-base
                                    ,reg-size ,irq
                                    controller))
             (apply environment driver-environment·uhci))))
  (driver·uhci controller))

原则上,这种代码即使在今天也可以工作,但驱动程序会因为 eval 速度慢而变慢。

相同的原理可以应用于使用 DeviceTree 的嵌入式系统。如果使用静态 DeviceTree,这甚至可以作为构建过程的一部分完成。如果使用动态 DeviceTree(允许同一内核在不同的 ARM 平台上运行),则启动时间可能会成为问题。但驱动程序可以设计为最初使用非专用驱动程序,异步调用 eval,并在 eval 返回后告诉运行中的驱动程序切换到专用驱动程序。