CS140e (0) 点亮树莓派
开 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分开成了两个寄存器 GPCLRx
和 GPSETx
。往相应的位写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 的语法,还是非常有效果的。