提示
以下为 PDF 渲染预览版本,没得复制(打印生成的PDF是这样的qwq) 原件戳这里
在技术的大陆上空展翅飞翔
提示
以下为 PDF 渲染预览版本,没得复制(打印生成的PDF是这样的qwq) 原件戳这里
好久不见!自从上一篇博客,上板成功,到现在已经过去了 3 个月。这几个月,主要是忙活一些学业上的东西,4月跟着老师探索了一点 IC design with LLM 的内容,5月忙活各种大作业、小组作业,6月忙活备考。终于考完试了,开完香槟就开始填坑啦!
由于 Unix 的 “文件即一切” 思想实在很好,所以我们采用这样的思路。
虚拟文件系统,即 Virtual FileSystem, VFS, 是整个文件系统的抽象,向上层提供统一的访问接口。考虑简单的设计,VFS 只需实现真实文件系统的挂载、卸载,以及各种操作的代理即可。
写了那么久,也快一个月了,都是在 QEMU 调试,多没意思。电子人,就是干,火速弄了块 Sipeed 的 LicheeRV Nano,基于 C906 和 A53的 SG2002,不到一够水就有百兆网口和 WiFi。(深圳到广州居然发了两天,真是慢啊,还有这排线还要我自己焊,好傻)
拿到板子,插锭,开机,屏幕没有当然点不亮。电脑上不停的叮咚响,看了下是模拟出了一个串口。那就很简单,直接找到镜像,烧录!再把卡插进去,启动!好了,过了一会,多了一个网卡,ssh一下成功,测试功能正常。那么就开始我们的上板之旅吧!
我们知道,嵌入式 Linux 的启动流程通常是,FSBL -> 固件 -> U-Boot -> 内核,这里的固件就是 OpenSBI。根据 Sipeed 官网的描述,这个片子的启动流程如下:
搞了这么多基础设施,终于到中断的实现了。首先看下 RV 的中断和异常,
Interrupt | Exception Code | Description |
---|---|---|
1 | 0 | Reserved |
1 | 1 | Supervisor software interrupt |
1 | 2 | Reserved |
1 | 3 | Machine software interrupt |
1 | 4 | Reserved |
1 | 5 | Supervisor timer interrupt |
1 | 6 | Reserved |
1 | 7 | Machine timer interrupt |
1 | 8 | Reserved |
1 | 9 | Supervisor external interrupt |
1 | 10 | Reserved |
1 | 11 | Machine external interrupt |
1 | 12–15 | Reserved |
1 | ≥16 | Designated for platform use |
0 | 0 | Instruction address misaligned |
0 | 1 | Instruction access fault |
0 | 2 | Illegal instruction |
0 | 3 | Breakpoint |
0 | 4 | Load address misaligned |
0 | 5 | Load access fault |
0 | 6 | Store/AMO address misaligned |
0 | 7 | Store/AMO access fault |
0 | 8 | Environment call from U-mode |
0 | 9 | Environment call from S-mode |
0 | 10 | Reserved |
0 | 11 | Environment call from M-mode |
0 | 12 | Instruction page fault |
0 | 13 | Load page fault |
0 | 14 | Reserved |
0 | 15 | Store/AMO page fault |
0 | 16–23 | Reserved |
0 | 24–31 | Designated for custom use |
0 | 32–47 | Reserved |
0 | 48–63 | Designated for custom use |
0 | ≥64 | Reserved |
一个 CPU 上可能不止一个核心 (Hart),这些核心在物理上分开,独立运行,但是共享总线和外设。也就是说,它们的物理内存空间是一致的,只不过有自己单独的寄存器和 Cache、MMU。而多线程非常类似,只不过是通过调度的方式实现,将多个线程的寄存器等资源分时复用。在裸机的角度上看,我们可以把多核看成硬件的多线程,并利用类似的技术实现。
在 RISCV 手册中,并没有规定 Hart 的启动顺序。比较通常的做法是,核心全部(近乎)同时从 M-mode 启动,随后 SBI 会通过原子量确定一个启动核心,并且执行初始化。至于 S-mode 中是否同时启动,我并没有明确看到文档指出,所以应该也是 SBI 实现自定义。通过实验,OpenSBI 只会将启动核心带入到 S-mode,其他核心会留在 M-mode 等待。但是,我们仍然有必要通过一个原子量保证 S-mode 初期只有一个核心运行,具体看,就是下面的汇编代码:
由于大部分的 RV64 SoC 都将 DRAM 放置在 0x80000000 以上,其下的空间保留给 IO,如 QEMU 的 virt,其布局如下:
static const MemMapEntry virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 },
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_PLATFORM_BUS] = { 0x4000000, 0x2000000 },
[VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
[VIRT_APLIC_M] = { 0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_APLIC_S] = { 0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_UART0] = { 0x10000000, 0x100 },
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FW_CFG] = { 0x10100000, 0x18 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_IMSIC_M] = { 0x24000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_IMSIC_S] = { 0x28000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 },
};
内存,即 Memory,是存储计算机运行过程数据的主要部件。不过在操作系统中,内存并不单单是主存,还有各种外设内存等。本文所说的内存布局,主要是指在可寻址空间中的布局,这关系到系统和后面应用程序的运行,是及其重要的。在有内存管理单元 (MMU) 的处理器上,寻址空间可以分为两种类型,即 物理地址空间 (Physical Memory Area, PMA) 和 虚拟地址空间 (Virtual Memory Area, VMA)。通过 MMU 的控制与转换,可以实现有效的隔离和保护,防止非法的内存访问。
本文仅浅读标准手册,未尽之处,敬请参阅标准手册。
上一篇我们已经搭建好了驱动框架,现在我们来实现三个基础驱动:根设备、内存、串口。
根设备并不是一个具体设备,而是设备树的根节点,包含了模组信息和兼容信息,可以根据它区别平台。为了方便起见,我们让根设备驱动一并处理 /chosen
节点,因为平台相关的一些额外参数可以直接这样传入。
首先定义一个基类 (顺便定义一个属性导出宏):
#define __K_PROP_EXPORT__(name, pri) \
const auto &name() const \
{ \
return pri; \
}
class SysRoot : public DriverBase
{
public:
virtual dev_type_t getDeviceType() override
{
return DEV_TYPE_SYS;
}
__K_PROP_EXPORT__(compatible, _compatible)
__K_PROP_EXPORT__(model, _model)
__K_PROP_EXPORT__(stdout_path, _stdout_path)
protected:
std::string _compatible;
std::string _model;
std::string _stdout_path;
};
所有连接到系统上的部件,都可以视为设备,从而并入设备驱动框架统一管理。
但是在现代操作系统中,如果没有 ACPI,如何发现设备呢?主线 Linux 已经给出了答案,利用设备树 (Device Tree) 描述。
在设备树中,我们可以描述平台的特性、外设的接入情况、CPU 和 Memory 的数量,还可以携带配置参数,如命令行参数。
由于树状结构不好存储,所以 Linux 推出了一个工具 DTC (Device Tree Compiler),用于将设备树源码 (DTS) 编译成扁平化的设备树 (Flattened Device Tree,FDT),并且提供了一个 libfdt 库进行操作,可以进行增删改查。
上回说到,通过一点手段,我们成功启用了 C++ 的支持。但是,这样的支持并不完善,我们的内核到目前为止,还没有一个 C 运行库(libc),更别提 C++ 标准库(libc++)了。这就意味着,所有需要用的函数和类都要手写一份,非常折磨人。所以,引入 C 和 C++ 基础库是接下来要做的事。
嵌入式里边,最常用的 libc 是 newlib, 而 C++ 的实现则可以使用 libsupc++。
之前的编译里边,为了最小化库的引入,我加入了一个编译参数:-nostdlib
。这个参数指示 gcc 不要链接标准库。所以要引入标准库,第一步就是把这个参数去掉,但是又不能引入默认的 crt 初始文件,所以还得加上 -nostartfiles
,然后测试编译器的支持。很不幸地,我只写了一句: