最近看到了Stanford新开的一节操作系统课CS140e。这门课的主要特点在于

  • 真实:不使用虚拟机,所有程序在真机(树莓派微型电脑)上运行
  • 自下而上:从最底层开始构建操作系统的核心模块
  • 现代:使用现代化的硬件(64-bit ARMv8多核平台)和现代化的编程语言(Rust)及工具

之前我在NYU的操作系统课上只在虚拟环境中模拟过操作系统的运行原理(且仅限单核环境),加之我本来就对Rust和树莓派有着浓厚的兴趣,我想便不妨借此契机学习一下Rust与树莓派,并进一步加深对操作系统原理的理解。

我打算写一系列文章来记录我的学习历程,并在最后总结我的学习心得并对NYU和Stanford的操作系统课程加以比较。这篇文章是该系列的第一篇,记录我准备相关软硬件环境和完成Assignment 0 Blinky点亮树莓派的过程。

准备环境

正如前文所述,在这门课上学生需要面向真实的硬件环境搭建系统,那么硬件的准备自然不缺少了。由于不是正式的注册学生,所有的硬件设备需要自行购买。指导中给出的清单是:

  • 1 Raspberry Pi 3
  • 1 1⁄2-sized breadboard (5.5 cm x 8.5 cm,尺寸更大的也可以)
  • 1 4GiB microSD card
  • 1 microSD card USB adapter
  • 1 CP2102 USB TTL adapter w/4 jumper cables
  • 10 multicolored LEDs (其实单色的也行)
  • 4 100 ohm resistors
  • 4 1k ohm resistors
  • 10 male-male DuPont jumper cables (公对公杜邦线)
  • 10 female-male DuPont jumper cables (公对母杜邦线)

这些淘宝上都能买到,全套下来300元就够了。接下来就是根据Assignment指导把灯点亮就行了。这一块指导讲解很详细,在此不再赘述。

值得注意的是如果像我一样采购了最新款Raspberry Pi 3 Model B+的话,使用提供的文件树莓派将无法正常启动,使用screen命令也不会有任何显示。连接电源后,PWR灯闪一下之后就熄灭,而绿色的ACT灯先慢闪4下再快闪4下,循环往复,查阅故障排查指南知道是 start.elf 无法加载,使用官方固件里面新的 start.elf 替换就能运行了。这应该是课程使用的树莓派版本较低,配套的文件无法用于启动新版本的硬件。

点灯

前面都在做准备工作,这块才是真正的开味菜(大雾。。。

这一块的主要知识点在于如何通过 Memory-mapped I/O (MMIO) 和硬件通信。所谓内存映射输入输出 (MMIO),指的是一种特定的CPU和外部设备通信的方法,其使用相同的地址总线来寻址内存和输入输出设备以实现CPU和外部设备通信。为了实现这一功能,输出输出设备的设备内存和寄存器会被映射到内存空间的某个地址,而相关规范会被用来指定当那些特定内存地址上的值被改变时硬件的行为。在这次作业中,我们就会通过修改特定内存地址的值的方式来修改树莓派上的特定 General Purpose I/O (GPIO) 控制寄存器的值,进而指定特定的GPIO接口的行为来实现让LED灯闪烁的功能。具体需要修改哪些内存地址的值,修改成什么值就要查阅Broadcom的芯片手册了。

值得一提的是,在直接操作输入输出设备的寄存器的值的时候,我们需要使用C的 volatile 关键字来避免编译器优化,保证程序的正确性。考察下面一个例子:

1
2
3
4
5
unsigned *IO_INIT = 0x7E000000;
for (int i = 0; i < 8; i++) {
*IO_INIT = i;
}
*IO_INIT = 8;

对于编译器来说,循环内的内容并不影响程序结束时的取值,因此可以直接优化成

1
2
unsigned *IO_INIT = 0x7E000000;
*IO_INIT = 8;

但是如果说为了硬件的正常初始化,必须按顺序向硬件的寄存器这样赋值呢?这个时候编译器的优化反而导致了一个错误的结果,而我们就需要使用 volatile 关键字声明一个变量来告诉编译器我们需要系统总是重从它所在的内存读写数据来避免此类问题。事实上,volatile本义是易变的,我们用它声明一个变量时也是在告诉编译器这个变量的值是易变的,并且我们关心它的变化,而不仅仅是它在程序结束运行时的最终值。

Phase 4要求使用Rust改写C的程序,非常类似(Rust里的write_volatile起到了C里把变量声明为volatile一样的作用)。不过值得注意的是Rust和相关工具的版本需要和Assignment指导所列(用的是nightly release的开发版本)严格符合,否则无法编译通过,这也从一个侧面说明了当前Rust开发迭代的速度之快。

此外,Rust版本的代码里sleep的时间是C版本的里的十分之一,会导致闪烁间隔时间太短。在600后面再加个0改成6000即可。

C:

1
2
3
4

static void spin_sleep_ms(unsigned int ms) {
spin_sleep_us(ms * 1000);
}

Rust:

1
2
3
4
5
6
7

#[inline(never)]
fn spin_sleep_ms(ms: usize) {
for _ in 0..(ms * 600) {
unsafe { asm!("nop" :::: "volatile"); }
}
}