移植树莓派驱动框架 Circle 到自制操作系统

目录:

0. 前言

什么是 Circle?

Circle 是一个叫 rsta2 的大佬用 C++ 写的 bare-metal 的树莓派驱动框架,同时支持现存的几乎所有版本树莓派,能够驱动树莓派上的大部分设备,包括 SD 卡控制器、有线和无线网卡、GPIO、USB 控制器及一些常用 USB 设备等。

这些设备驱动中有一些是 rsta2 参考 Linux 或其它 bare-metal 的实现自己写的,还有一些是他直接从其它系统移植过来的。各驱动对外封装成了 C++ 类的形式,可以按需对其实例化和使用。对于在树莓派上编写自制操作系统并且希望尽快驱动一些设备的场景,将 Circle 整个移植过来是非常值得考虑的选项。当然,需要注意的是,Circle 的开源协议是 GPLv3,具有传染性,应该把它作为系统的非必要组件来使用。

本文干了什么?

本文将简要记述将 Circle 驱动框架移植到一个自制微内核操作系统(实际上是实验室项目),作为用户态驱动进程的过程。

由于只是简要地记录移植过程和经验总结,本文不会深入到每一个具体的细节,如果你恰好有相似的需求,除了参考本文,还需要具体地去研究 Circle 的代码,并根据具体情况具体分析。

1. 确定需求

首先一开始不要盲目移植,先确定一下自己需要 Circle 中的哪些设备驱动,比如说,如果你只是需要 USB 相关驱动,那么在移植过程中可以不关心 WLAN 那块是否有不兼容。

然后跑一些 Circle 提供的 sample,确定 Circle 确实可以满足需求。

2. 移植基本部分

这部分包括一些非常必要的组件,比如 mailbox 访问,许多驱动都需要通过 mailbox 获取信息或申请资源等。这部分移植完成后,将可以在启动时获取机器信息,并点亮 LED 灯。

具体地,主要需要做下面这些事情:

  1. 修改 Rules.mkMakefile,去掉任何 boot 相关的代码
  2. 重新实现一些模块,去除需要特权指令的地方,并使用用户态 lib 提供的功能代替:
    • assert:assert 失败后不要真的关机或重启,可以 exit
    • interrupt:一开始可简单打印点内容,不用真的实现
    • logger:改为 printf 输出
    • new:使用 malloc 分配
    • sysinit:提供假实现
    • timer:使用 nanosleep 实现 SimpleusDelay
    • memory:CMemorySystem 类可以直接去掉,一开始会有地方用到 CMemorySystem::GetCoherentPage,改用系统提供的分配物理上连续的 non-cacheable 内存的接口
  3. Circle 进程启动时将物理地址的外设区域直接对等映射到当前进程的虚拟地址空间,这样将不需要改动 Circle 中通过 MMIO 访问外设时使用的地址

经过一些调试后,可以点亮 LED 灯,输出日志到 stdout,然后退出(需要编写适当的 kernel.cpp,可参考 sample),这意味着简单的 MMIO 已经可以了。

3. 驱动屏幕

这一步同样需要重新实现一些模块:

  • synchronize:主要是刷 cache 相关操作
  • bcmframebuffer:向 GPU 申请 frame buffer 后需要将其映射到当前进程的虚拟地址

经过一些调试后,可以通过 HDMI 输出内容到屏幕。

4. 模拟实现 Timer

对于一些稍复杂的驱动,例如 USB 和 WLAN,会依赖 timer 获取当前 tick,因此需要重新实现 timer 模块。

具体地,可以在 CTimer::Initialize 中创建一个新的线程,每隔 10 ms(利用 nanosleep 等函数)调用一次 CTimer::InterruptHandler,其它代码几乎不用改动。此外,还需要实现 CTimer::GetClockTicks 以获得当前 tick 数。

经过一些调试后,输出的日志中能够包含当前时间,此时说明 timer 基本实现对了。

5. 解决内存相关的一系列问题

许多驱动在运行的过程中需要分配内存以供外设进行 DMA,同时又需要在进程内访问这块内存以读写跟外设交互的数据。因此,这块内存既需要能通过虚拟地址访问,又需要能获取到物理地址,同时在物理地址上连续且 non-cacheable。malloc 是不能满足这个需求的,因为 malloc 只保证分配出的内存虚拟地址连续,不能保证物理地址连续,也无法配置成 non-cacheable。

要解决这个问题,需要内核提供分配物理上连续且 non-cacheable 的内存并映射到虚拟地址空间的相关系统调用,然后再在 Circle 中利用这些系统调用重新实现内存相关模块。其实在前面已经粗略地实现了,但在这一步需要确保实现的正确性。对 Circle 的修改主要涉及 CMemorySystem::GetCoherentPageDMA_BUFFERBUS_ADDRESSnew (HEAP_DMA) 的定义及使用它们的地方。

6. 用户态处理中断

对于像 USB 和 WLAN 这些需要利用中断通知操作系统发生了特定事件的设备,还需要把之前虚假实现的 interrupt 模块实现对。

首先要求内核提供让特定用户态进程处理特定 IRQ 的能力,具体来说就是驱动进程要能够通过系统调用注册一个函数作为特定编号的 IRQ 的用户态处理函数,然后内核在收到 IRQ 后调用此函数来处理。

接着重新实现 interrupt 模块,把 CInterruptSystem::ConnectIRQ 改为使用上述注册 IRQ 处理函数的系统调用,暂时用不到的函数可以不实现,比如与 FIQ 相关的。

7. 驱动 USB

Timer、DMA buffer、中断这几个重要的部分移植完成后,比较容易就可以驱动 USB 控制器,进而可以检测并驱动 USB 键盘、鼠标、存储、串口转换器等设备,对于树莓派 3,还可以驱动有线网卡(LAN7800)。

8. 其它驱动

到目前为止已经移植了大部分驱动所需的运行环境,之后的移植工作主要看具体的需求了,比如如果需要网络协议栈,还要重新实现 sched 模块,里面包括线程抽象、调度、线程同步机制等。

评论