Stanford CS140e 学习笔记 (2):驱动、bootloader、shell
在 CS140e Assignment 0 中,主要通过点亮 LED,来熟悉 Rust 和 Raspberry Pi 的开发环境。Assignment 1 正式开始操作系统的编写,主要包括驱动、bootloader 和 shell。
本文记录了 Assignment 1: Shell 的学习过程。
(html comment removed: more)
文章目录:
- Stanford CS140e 学习笔记 (1):Rust 基础、LED 闪烁
- Stanford CS140e 学习笔记 (2):驱动、bootloader、shell
- Stanford CS140e 学习笔记 (3):文件系统
- Stanford CS140e 学习笔记 (4):用户态程序的运行
Rust 语法学习
在正式编写代码之前,Assignment 1 提供了一系列任务来熟悉 Rust 语法。
首先通过阅读两篇文章,对 Rust 的语法,以及 Rust 内存安全的特点有了更多的了解:
接下来,通过对 25 个未完成的 Rust 文件进行修改,使程序能够按照不同的要求编译成功、编译失败、或者通过测试用例。由于刚接触 Rust,还是有不少地方一时半会儿没想明白怎么修改,需要查阅文档、参考示例代码来慢慢完成。
基础数据结构
StackVec: 保存在栈上的 Vector
Rust 本身提供了 Vector, Box, String 等数据结构,但这些结构都需要依赖操作系统提供的 malloc()
来实现。现在的程序需要直接在 Raspberry Pi 裸机上运行,无法使用这些结构。
所以,这部分的任务是自己实现一个 Vector。为了更简单地实现,将 Vector 的数据保存在栈上,并提供与 std::Vec
类似的接口。
为了保证正确实现,代码中提供了测试用例,实现完毕后,通过 cargo test
运行测试,修改错误,并最终测试通过。
volatile: 忽略编译器的优化,直接读写内存
与 C 语言中,尤其是在单片机编程中,经常使用 volatile 关键字。在 Rust 中,也会有类似的用法,使编译器不对内存读写进行优化,直接在指定的内存地址读取或写入数据。
对于内存映射寄存器、内存映射 I/O 等,不仅用户编写的程序需要对其进行读写,硬件状态等各种外部因素,也会影响其中的值。所以需要保证每次读取的数据都为最新值,每次写入都能正确、及时进行。在这种情况下,使用 volatile 就比较有意义了。
代码中实现了四种 Volatile 相关类型,并实现了常用的一些位操作函数,可以对方便地对 raw 指针进行 volatile 读写。
示例程序中使用了 Unique,通过如下文章了解了 Unique
的作用:
- https://doc.rust-lang.org/std/ptr/struct.Unique.html
- https://github.com/ScottHuangZL/Rust-Articles-Translation/blob/master/r4cpp%20-%20Unique%20Pointers.md
- https://aminb.gitbooks.io/rust-for-c/content/unique/index.html
这部分任务不需要自己实现代码。阅读已有代码并回答问题即可。
Raspberry Pi 驱动
完成基础数据结构和 XMODEM 工具(在下文中介绍)之后,就开始了正式的操作系统编写。这时候需要创建一个新的 OS
目录,用来保存操作系统代码。目录结构如下:
os
├── Makefile
├── bootloader
├── kernel
├── pi
├── std
└── volatile
首先通过参考 datasheet,完成定时器、GPIO、UART 驱动程序。并在主函数中调用这些驱动,实现 LED 闪烁、串口数据收发等,来验证并确定驱动功能是否正常。
尤其是 GPIO 驱动的部分,在课程提供的程序框架中,PhantomData 与状态机的结合比较巧妙:能够在编译过程中检查并确保 IO 口处于读状态时,只能调用读数据相关的接口;处于写状态时,只能调用写数据相关的接口。这样一来,降低了误操作的概率,使程序能够安全运行。这种编程思路值得学习。
UART 与 IO 驱动类似,也是寄存器操作,根据 datasheet 就可以完成。
Shell
Shell 是 Assignment 1 中需要重点完成的部分。这部分需要实现一个带有 echo
命令的简单 Shell,可以通过该命令,将用户输入的内容回显在屏幕上。同时注意代码的结构,方便以后添加新的命令。
Shell 用到了 CONSOLE
全局资源。 CONSOLE
主要使用 UART 驱动,实现串口读写数据的基础功能。为了保证对 CONSOLE
的安全访问,需要通过 Mutex
对其进行加锁。标准库中的 Mutex
依赖操作系统,所以使用了代码框架中提供的不依赖操作系统的 Mutex
。Mutex
相关的代码标准库中已实现,暂时可以先不关注其具体实现,等待以后进一步研究。
完成 Shell 之后,通过在内核入口函数调用 Shell,即可通过串口,访问并测试 Shell 的功能。
Bootloader 与 XMODEM 协议
由于内核代码在后续的课程和实验中,会不停地变化。每次编译内核后,将 microSD 卡从 Raspberry Pi 中拔出、插入电脑更新文件、再插回 Raspberry Pi 并重新上电...... 这一过程过于繁琐。所以可以通过实现一个带有数据下载功能的 Bootloader,通过串口直接内核程序传输至 Raspberry Pi 并启动,来避免反复插拔 microSD 卡。
XMODEM 协议
通过串口传输数据,最简单的方式是直接传输原始数据,但这样稳定性较差。所以通过 XMODEM 协议,增加了数据校验等功能,确保数据能够正确传输。
首先实现了一个不依赖底层的 XMODEM 协议模块,在这个过程中练习了 std::io::Read
和 std::io::Write
的用法。实现完毕后,就可以通过与串口驱动、以及后续可能会添加的其他驱动相结合,实现数据的传输。
(另外,我也在考虑为我的 AirTerminal App 添加 XMODEM 协议,方便在 iOS 设备上实现与嵌入式设备之间的文件交换。)
ttywrite
接下来基于刚刚完成的 XMODEM
模块,实现了在电脑上运行的 ttywrite
命令行工具,将电脑中的程序,通过串口发送至 Raspberry Pi.
同时课程代码中提供了 test.sh
脚本,用于对完成后的工具进程测试。通过该脚本的代码,了解了 socat
工具及其用法。
Bootloader
有了前面的 XMODEM 协议模块,Bootloader 代码的编写就容易了很多。Bootloader 与内核位于内存的不同地址,通过 XMODEM 将内核传入 Raspberry Pi 内存中的指定地址,加载完毕后,跳转到该地址,即可启动内核。
小结
Assignment 1 开始进入操作系统的正式编写,任务量比 Assignment 0 多了不少。由于中间经历了出差结束等一些事情,花了三个多星期才完成。经过 Assignment 1,自己才算刚刚入门 Rust,能够独立地写一些程序。
接下来开始进行 Assignment 2 的学习,关于 SD 卡驱动和文件系统的。这部分在之前的单片机编程中也有一定的接触,准备看一看与之前自己接触的,有哪些不同之处。
Posted from my blog with SteemPress : https://blanboom.org/2018/cs140e-learning-notes-2/
@blanboom, 我好欣赏你滴~~~![img](https://steemitimages.com/640x0/https://i.imgur.com/AWr77TF.png)
谢谢☺️