『基礎から学ぶ 組込みRust』の読書メモ
『基礎から学ぶ 組込みRust』をやっていきます。本書の内容すべてについて手を動かしてやっているわけじゃないので、ところどころとびます。
環境
- macOS Big Sur
- 2.3 GHz クアッドコアIntel Core i7(M1ではない)
Rustのインストール
はじめに - Rustプログラミング言語にある通り。
開発用ツールのインストール
OSのパッケージを入れる。
$ brew install git minicom sdl2 openssl
minicomというのは、シリアル通信するターミナルエミュレータとのこと。これまではGNU screenを使ってやったりしていたが、本書の記載通りminicomを使ってみよう。
SDL2といのは、マルチメディアデバイスへの接続を抽象化するライブラリとのこと。この本のゴールをまだ確認していないのでなんのために必要なのかはわからないが。ディスプレイにグラフィック描いたりするのに使うのかな。
cargoサブコマンドのインストール
$ cargo install cargo-generate
$ cargo install hf2-cli
$ cargo install cargo-hf2
hf2というのはデバイスとデータをやり取りするときのプロトコルとメッセージフォーマットを規定しているもののようだ。
Lチカ
サンプルプロジェクトを試す。tomoyuki-nakabayashi/wio-terminal-blink-rs: Wio Terminal で L チカするをgit clone
してくる。
以下のコマンドでビルドする。
$ cargo hf2
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Searching for a connected device with known vid/pid pair.
Trying Ok(Some("Seeed Studio")) Ok(Some("Wio Terminal"))
Flashing "/Users/antipop/src/github.com/tomoyuki-nakabayashi/wio-terminal-blink-rs/target/thumbv7em-none-eabihf/debug/wio-terminal-blink-rs"
Finished in 0.403s
うまく行ったら上記のような感じになる。
Wio Terminal付属のケーブルは短いので、適当に両端がUSB TypeCのケーブルを使ったら、以下のようにエラーになった。
$ cargo hf2
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Searching for a connected device with known vid/pid pair.
thread 'main' panicked at 'Are you sure device is plugged in and in bootloader mode?', /Users/antipop/.cargo/registry/src/github.com-1ecc6299db9ec823/cargo-hf2-0.3.1/src/main.rs:98:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
しかし、USB Type-AとUSB Type-Cのケーブルを使ったらうまくいった。
Lチカを自分で書いてみる
cargo generate
でテンプレからパッケージを作れるようだ。
$ cargo generate --git https://github.com/tomoyuki-nakabayashi/wio-blink-template.git
🤷 Project Name : my-blink
🔧 Creating project called `my-blink`...
✨ Done! New project created /Users/antipop/src/github.com/kentaro/my-blink
main.rs
のloop
の箇所を付け足して、cargo run
するか、main
関数の上部に表示されているRun
を押したらビルドしてデバイスへのデプロイもされる(ブートローダーモードにするのを忘れずに)。
#![no_std]
#![no_main]
use panic_halt as _;
use support::*;
#[entry]
fn main() -> ! {
let (mut user_led, mut delay) = support::init();
loop {
delay.delay_ms(500u16);
user_led.toggle();
}
}
no_std
, no_main
, entry
上記したサンプルコードには#![no_std]
というアトリビュートがつている。これによって、Rustはstdクレートではなくcoreクレートを使ってビルドするとのこと。coreクレートはstdクレートのサブセットで、プラットフォーム依存のないライブラリのみを含む。
Wio TerminalではOSは動作しないので、coreクレートを使うとのこと。また、OSの例外ハンドラが使えないので、panic-haltクレートを使ってハンドラを実装する必要がある。
#![no_main]
は、エントリポイントとしてのmain
関数を使わないよう指示している(OSがないので)。#![entry]
アトリビュートは、その関数がエントリポイントであることを示す(cortex-m-rtクレートが提供する機能。そのあたりはsupport/Cargo.toml
で依存が定義されている)。
また、main
関数の返り値の型が!
になっているが、「発散する関数(divreging function)」と呼ばれ、その関数から処理が戻らないことを示す(なんかいろいろ出てくるなあ!)。ファームウェアなので、戻る必要がない(戻り先がない)のでこうしている。
ビルドの内側
Wio Terminalの積んでいるプロセッサはCortex-M4Fなので、ターゲットトリプル(プロセッサアーキテクチャーOSーABI)はthumbv7em-none-eabihf
となる。ターゲット用のビルド設定は、.cargo/config.toml
に書く。
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "hf2 elf"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
Rustコンパイラにわたしてるフラグとか細かいことはThe Embedonomiconを見てくれとのこと。
ファームウェアの書き込み方法は、デバッガを使ったりシリアル通信をしたりとかいろいろあるけど、本書ではHF2(USB HID Flashing Format)を使う。
embedded-hal
HALとは、Hardware Abstraction Layerのこと。embedded-halは、組み込みデバイス開発に便利なクレートをあつめたもの。
rust-embedded/embedded-hal: A Hardware Abstraction Layer (HAL) for embedded systems
こんなレイヤリング。
wio-examplesの準備
6〜8章では、こちらのテンプレートを使っていく。
tomoyuki-nakabayashi/wio-examples-template: Wio Terminal でデバイスを試すための組込み Rust プロジェクトテンプレートです。
$ cargo generate --git https://github.com/tomoyuki-nakabayashi/wio-examples-template.git
🤷 Project Name : wio-examples
🔧 Creating project called `wio-examples`...
Error: liquid: Cannot read file
with:
path=/Users/antipop/src/github.com/kentaro/wio-examples/examples/assets/ferris.raw
エラーが出た。けど、そのファイルはあるけどなあ。
$ ls /Users/antipop/src/github.com/kentaro/wio-examples/examples/assets/ferris.raw
/Users/antipop/src/github.com/kentaro/wio-examples/examples/assets/ferris.raw
LEDを押している間、ユーザLEDが点灯するアプリケーション
いよいよアプリケーションを作っていく。GPIOを使う。
テンプレートだと書かれてない部分があるので、本を見ながら書き込んでいく。
#![no_std]
#![no_main]
use panic_halt as _;
use wio_terminal as wio;
use wio::entry;
use wio::pac::Peripherals;
use wio::prelude::*; // 主要な構造体やトレイトをインポートする
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let mut pins = wio::Pins::new(peripherals.PORT);
// ユーザLEDを出力状態に設定する
let mut led = pins.user_led.into_push_pull_output(&mut pins.port);
// ボタン1を入力状態に設定する
let button1 = pins.button1.into_floating_input(&mut pins.port);
loop {
if button1.is_low().unwrap() {
// ボタンが押されていればユーザLEDを点灯する
led.set_high().unwrap();
} else {
// ボタンが押されていなければユーザLEDを消灯する
led.set_low().unwrap();
}
}
}
以下のあたり、なんで&mut pins.port
を渡すようなインタフェイスになってるんだろうか。&mut
としてわたしてるから、ポートの中に含まれるユーザLEDの状態を管理するフラグかなんかを変更するみたいなことをしてるのかな。
let mut led = pins.user_led.into_push_pull_output(&mut pins.port);
上述のコードだと、ピンの状態を直接触ってしまっているので、もっと抽象化してやる。
#![no_std]
#![no_main]
#![allow(dead_code)] // 使用しないメソッドでコンパイラが警告を出さないようにする
use panic_halt as _;
use wio_terminal as wio;
use wio::entry;
use wio::hal::gpio::*; // GPIOの構造体やトレイトをインポートする
use wio::pac::Peripherals;
use wio::prelude::*; // 主要な構造体やトレイトをインポートする
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let mut pins = wio::Pins::new(peripherals.PORT);
let mut led = Led::new(pins.user_led, &mut pins.port);
let button1 = Button1::new(pins.button1, &mut pins.port);
loop {
if button1.is_pressed() {
led.turn_on();
} else {
led.turn_off();
}
}
}
// Wio Terminalのボタン1ドライバ
struct Button1 {
pin: Pc26<Input<Floating>>,
}
impl Button1 {
fn new(pin: Pc26<Input<Floating>>, port: &mut Port) -> Button1 {
Button1 {
pin: pin.into_floating_input(port),
}
}
fn is_pressed(&self) -> bool {
self.pin.is_low().unwrap()
}
fn is_released(&self) -> bool {
self.pin.is_high().unwrap()
}
}
// Wio TerminalのユーザーLEDドライバ
struct Led {
pin: Pa15<Output<PushPull>>,
}
impl Led {
fn new(pin: Pa15<Input<Floating>>, port: &mut Port) -> Led {
Led {
pin: pin.into_push_pull_output(port)
}
}
fn turn_on(&mut self) {
self.pin.set_high().unwrap();
}
fn turn_off(&mut self) {
self.pin.set_low().unwrap();
}
fn toggle(&mut self) {
self.pin.toggle();
}
}
UARTを使ったシリアル入出力
まずは本を見ながら、サンプルコードの6-3-uarts.rs
に書き込む。
#![no_std]
#![no_main]
use panic_halt as _;
use wio_terminal as wio;
use core::fmt::Write;
use wio::hal::clock::GenericClockController;
use wio::pac::Peripherals;
use wio::prelude::*;
use wio::{entry, Pins, Sets};
#[entry]
fn main() -> ! {
let mut peripherals = Peripherals::take().unwrap();
// クロックを初期化する
let mut clocks = GenericClockController::with_external_32kosc(
peripherals.GCLK,
&mut peripherals.MCLK,
&mut peripherals.OSC32KCTRL,
&mut peripherals.OSCCTRL,
&mut peripherals.NVMCTRL,
);
// TODO: UARTドライバオブジェクトを初期化する
let mut sets: Sets = Pins::new(peripherals.PORT).split();
let mut serial = sets.uart.init(
&mut clocks,
115200.hz(),
peripherals.SERCOM2,
&mut peripherals.MCLK,
&mut sets.port,
);
// TODO: 「hello world」と出力する
for c in b"hello world\n".iter() {
nb::block!(serial.write(*c)).unwrap();
}
// TODO: 「this is UART example!」と出力する
writeln!(&mut serial, "this is {} example!", "UART").unwrap();
loop {}
}
Wio Terminal背面のピンをそれぞれ以下の通りUSBシリアル変換モジュールとつなぐ(デバイス側とホスト側とでは、TXDとRXDがそれぞれ逆につなぐことに注意)。
- 6番(GND)→ GND
- 8番(TXD)→ RXD
- 10番(RXD)→ TXD
ちゃんとデバイスがつながっているかどうか確認する。
$ ls /dev | grep tty.usb
tty.usbserial-AI045BJ1
minicomの設定を$HOME/minirc.dfl
に以下の通り書く。
pu baudrate 1153200
pu bits 8
pu parity N
pu stopbits 1
pu rtscts No
pu xonxoff No
pu addlinefeed Yes
pu add carreturn Yes
上記のデバイスに対して、minicomを起動する。
$ minicom -D /dev/tty.usbserial-AI045BJ1
そんでもって、上記のコードを書き込めばOKというところなのだが、使っているMacBook ProにつないでいるハブのUSB Type-Aポートがひとつしかあいておらず、ファームウェア書き込み用のケーブルを指した状態だとUSBシリアル変換モジュールをつなぐポートがあまってない。
なので、いったん書き込んでみてからケーブルをはずして、USBシリアル変換モジュールを指してみたが、そのときにはすでに文字列の書き込み送信が終わっているからか特に何も起こらず。実験できなかった。
それなら、延々送信し続ければいいのでは?と思ってループのところをいかのように変更してみて、上記の手順でファームウェアの更新とUARTでの接続をしてみたが、デバイスからデータが送られてないように見える。
let mut i = 0;
loop {
// 「hello world」と出力する
write!(&mut serial, "{}: ", i).unwrap();
for c in b"hello world\n".iter() {
nb::block!(serial.write(*c)).unwrap();
}
i += 1;
}
よくわからない。
ブザーと加速度のところは読むだけ読んだ。
LCDに直線を表示する
embedded-graphics/embedded-graphics: A no_std graphics library for embedded applicationsを使っていく。
(というか、Rustライブラリのリポジトリって言語を示すプレフィクス/サフィックスをつける慣習はあんまないのかな)
実験用プロジェクトを作る。
$ cargo new eg-playground
Cargo.toml
の依存を追加。
[dependencies]
embedded-graphics = "0.6.2"
embedded-graphics-simulator = "0.2.1"
本を見ながらコードをexamples/line.rs
としてコードを書いて実行する。
use embedded_graphics::prelude::*;
use embedded_graphics::{pixelcolor::Rgb565, primitives::*, style::*};
use embedded_graphics_simulator::*;
fn main() {
let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
let output_settings = OutputSettingsBuilder::new().build();
let mut window = Window::new("draw a line", &output_settings);
let start = Point::new(50, 20);
let end = Point::new(270, 220);
let style = PrimitiveStyle::with_stroke(Rgb565::GREEN, 1);
Line::new(start, end)
.into_styled(style)
.draw(&mut display)
.unwrap();
window.show_static(&display);
}
シミュレータが起動して、斜めの直線が表示された!
main
関数におけるtips
上記のコードではmain
関数は()
を返すが、以降はResult<(), core::convert::Infallible>
を返すことにする。こうすると、draw()
メソッドのあとにunwrap()
を呼び出す代わりにdraw()?;
と書くことができる。この型は決してエラーを返さないことを意味するとのこと。
これの何がうれしいのかはよくわからない。
もっと図形を書いてみる
丸・三角・四角を描いていく。
use embedded_graphics::prelude::*;
use embedded_graphics::{pixelcolor::Rgb565, primitives::*, style::*};
use embedded_graphics_simulator::*;
fn main() -> Result<(), core::convert::Infallible> {
let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
let output_settings = OutputSettingsBuilder::new().build();
let mut window = Window::new("draw primitives", &output_settings);
Line::new(Point::new(50, 20), Point::new(270, 220))
.into_styled(PrimitiveStyle::with_stroke(Rgb565::GREEN, 1))
.draw(&mut display)?;
Circle::new(Point::new(50, 200), 20)
.into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 5))
.draw(&mut display)?;
Triangle::new(
Point::new(200, 20),
Point::new(170, 45),
Point::new(300, 150),
)
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLUE))
.draw(&mut display)?;
let style = PrimitiveStyleBuilder::new()
.stroke_width(10)
.stroke_color(Rgb565::CYAN)
.fill_color(Rgb565::YELLOW)
.build();
Rectangle::new(Point::new(100, 100), Point::new(220, 140))
.into_styled(style)
.draw(&mut display)?;
window.show_static(&display);
Ok(())
}
出た〜。
文字を描画する
図形のかわりにText
を使うだけ。
use embedded_graphics::{fonts::*, pixelcolor::Rgb565, prelude::*, style::*};
use embedded_graphics_simulator::*;
fn main() -> Result<(), core::convert::Infallible>{
let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
let output_settings = OutputSettingsBuilder::new().build();
let mut window = Window::new("hello, world!", &output_settings);
Text::new("hello, world!", Point::new(0,0))
.into_styled(TextStyle::new(Font12x16, Rgb565::GREEN))
.draw(&mut display)?;
window.show_static(&display);
Ok(())
}
テキストが表示された。
画像を表示する
https://github.com/tomoyuki-nakabayashi/wio-examples-template/tree/master/examples/assets ディレクトリをexamples
以下のコピーする。
use embedded_graphics::{image::{Image, ImageRawLE}, pixelcolor::Rgb565, prelude::*};
use embedded_graphics_simulator::*;
fn main() -> Result<(), core::convert::Infallible>{
let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
let output_settings = OutputSettingsBuilder::new().build();
let mut window = Window::new("ferris", &output_settings);
let raw = ImageRawLE::new(include_bytes!("./assets/ferris.raw"), 86, 64);
let image = Image::new(&raw, Point::new(32, 32));
image.draw(&mut display)?;
window.show_static(&display);
Ok(())
}
画像が表示される。Rustの非公式マスコットキャラクタらしい。
WebAssembly Simulator
RustはWebAssembly形式でバイナリをはくこともできる。rustwasm/wasm-pack: 📦✨ your favorite rust -> wasm workflow tool!を使ってビルドする。
wasm-packにある通りのコマンドでインストールする。
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
wasm-packを使うと、npmパッケージとして成果物ができるそうな(なのでnodeが必要)。
rahul-thakoor/embedded-graphics-web-simulator: A web simulator for the embedded-graphics libraryにあるサンプルリポジトリを試す。
$ cd examples/basic
$ npm install
$ npm start
なんか表示された!
その後、WebシミュレータでもWioSplashのコードが動くよという例が出てくる。少し改変する必要はあるようだ。
Rustで書けばパソコン、Webブラウザ、マイコンとさまざまな環境で同じプログラムを動かせる可能性があります。
一般にどんぐらい変更がいるのかというのと、それらのターゲット向けのコードベースを共通化しつつビルドしわけるみたいなのはできるのかなというところが気になる。
疲れてきたので、アプリケーション編は読んだだけ。
この本に出てくる「型状態プログラミング」について、著者がブログに書いていたのを読んだ。