CS140e 的坑主要原因还是想学习一下 Rust,正好想起来年初有这么一个非常有意思的课,不如跟着做吧。

准备硬件

Check that your Raspberry Pi kit includes all of the following materials:

  • 1 Raspberry Pi 3
  • 1 1⁄2-sized breadboard
  • 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

看上去好像需要准备很多东西,但是仔细一看这是上这门课发的 Raspberry Pi kit 里面的东西。我往后翻了一翻,好像并不需要这么多东西呀。第一次作业是点灯,让你熟悉一下树莓派硬件,有一种以前刚开始玩单片机时候的感觉。搞电子玩嘛,感觉精髓就在于“捡垃圾”,除了从淘宝上买了个树莓派,剩下的都从家里捡到了:

先翻出来以前玩单片机时候的开发版,获得了若干杜邦线,其实上面还有个面包板,不过感觉用不到,毕竟这个作业只要点个灯就好了。

以前刷路由器的时候把路由器刷成了砖,当时买了一个USB转TTL线回来,看了一下上面的字,芯片是 PL-2303HX,网上搜了一下,竟然还有最新的 macOS 驱动,美滋滋。

接下来从老爸一大堆的“宝贝”里面捡了一个LED灯。

感觉要从刚刚的抽屉里面找到一个合适大小的电阻还是很蛋疼的,一是已经忘了色环电阻的颜色排序了,二是色环电阻这么小看久了眼睛真的瞎啊,不如拿新的。找到一包51Ω的,拿两个,102Ω在3.3V的输出下差不多可以用。

把两个电阻绑在LED灯脚上,非常完美!正好可以插到杜邦线里面。

好吧,老爸觉得这还不够完美,要加上一个绝缘壳。感觉这个操作我学不来。

准备好了东西以后,还是得确定一下刚才捡的LED灯能不能亮。把LED灯插到 Pin 1 3.3V PWR 和 Pin 6 GND 上面,插上电源,亮了。

至此,硬件一切就绪。

运行样例程序

当时买树莓派3B+的时候就在担心会不会启动不了课程给的程序,果然最不幸的事情就发生了。还好这个问题好解决,用官方固件里面新的 start.elf 替换就能运行了。

不过我还蛮好奇这个启动过程是怎样的,为什么往TF卡里面拷贝几个文件就行了?查到了这么个资料,感觉树莓派启动方式还是很魔性的:

  • SoC竟然是让 VideoCore 4 GPU 而不是 Cortex-A53 CPU 负责启动,真的是迷
  • 第一阶段的 bootloader 是写死在SoC里面的,来读取TF卡里面的 bootcode.bin
  • 第二阶段是 bootcode.bin,依然是GPU执行,引导进 start.elf
  • 第三阶段是 start.elf,根据 config.txt 初始化CPU,然后载入 kernel8.img
  • 最后终于轮到了CPU开始运行内核 kernel8.img 了,我猜这个8的意思是 ARMv8
  • config.txt 就相当于以前电脑上的 CMOS 保存着 BIOS 的设置信息

感觉还是一种非常便利的启动方式的,只需要拷贝文件就行。

用 C 语言点灯

首先是要安装交叉编译的工具链,但是这里只给了个二进制包。搜索了一下怎么自己编译,似乎还是挺复杂的,以后用到了再研究研究吧。

写程序前要先看 datasheet,其实作业页面上都告诉你看哪一页了,还是非常轻松的。非常有意思的是,这份 datasheet 是从 BCM2835 魔改而来的,我试着查了下 BCM2837 和 BCM2837B0,发现都查不到对应的 datasheet。看来 Broadcom 还是把资料藏得很严啊。

BCM2837的GPIO和以前用的PIC16F877A单片机的GPIO还是有点类似的,可能嵌入式开发都是这样的吧,就是有一个GPIO功能选择的寄存器,比方说这里叫做 GPFSELx 而PIC16叫做 TRISx,先选好引脚是用来输入的还是输出的。

和以前PIC16不太一样的是,这里把输出0和输出1分开成了两个寄存器 GPCLRxGPSETx。往相应的位写0没有效果,往 GPCLRx 写1会输出0,往 GPSETx 写1会输出1。datasheet 上面是这么解释这个事情的:

Separating the set and clear functions removes the need for read-modify-write operations.

可惜我没有体会出来这个点。

用 Rust 语言点灯

照道理来说只要把几行 C 翻译成 Rust 就好了,然而因为我秉承了作死的传统,使用了最新版的编译器 rustc 1.28.0-nightly (cd494c1f0 2018-06-27),编译起来就出了两个错误

一个是 error[E0522]: definition of an unknown language item: `panic_fmt`。找到了这个帖子以及RFC2070,新版本里面把原先的

#![feature(lang_items)]

use core::intrinsics;

#[lang = "panic_fmt"]
unsafe extern "C" fn panic_fmt(
    _args: core::fmt::Arguments,
    _file: &'static str,
    _line: u32,
    _col: u32,
) -> ! {
    intrinsics::abort()
}

替换成

#![feature(panic_implementation)]

use core::intrinsics;
use core::panic::PanicInfo;

#[panic_implementation]
fn panic(_info: &PanicInfo) -> ! {
    unsafe { intrinsics::abort() }
}

另外一个错误是 error[E0259]: the name `compiler_builtins` is defined multiple times。这个错误我倒是没有找到相关的 issue,而且 The Unstable Book 上面的写法也跟目前的代码一致的。

我试着把 extern crate compiler_builtins; 去掉,就能成功编译了,另外我试着去掉 #![feature(compiler_builtins_lib)] 也能正常编译。有点不明所以。

更新:经 @codeworm96 大神提醒,现在已经不需要这两句话了,一句 #![no_std] 就行了,见这个讨论

另外还有一个小问题,我编译成功之后发现LED灯常亮,不会闪。这让我折腾了好久。

不过我隐约觉得LED灯有微微的频闪而且亮度似乎下降了,拿手机摄像头一看更加明显。把 Rust 代码和 C 代码拿来对比了一下,果然给的代码里面出现了不一致:

static void spin_sleep_us(unsigned int us) {
  for (unsigned int i = 0; i < us * 6; i++) {
    asm volatile("nop");
  }
}

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

仔细看,Rust 里面 sleep 的时间是 C 里面的十分之一!于是乎本来一个闪烁效果愣是变成了低频PWM调光……

600 后面加了个 0 就解决问题了。

总结

对于我这样不搞硬件的人,每次折腾硬件总是非常的兴奋。这次作业让我回想起了小时候折腾单片机的时光,还是非常好玩的。第0次作业基本上跟 Rust 还没太关系,不过我根据 Syllabus 上面的安排把 The Rust Programming Language 的第1~11章又读了一遍,这次是倒着读的,感觉又学习/回想起了好多 Rust 的语法,还是非常有效果的。