Knurling Sessions 2020Q4 をやる (4) 〜ディスプレイ〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!

現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。

2020 Q4 のレンダリングされているドキュメントはこちら。

session20q4.ferrous-systems.com

前回、CO2 センサドライバを作成して、defmt で出力しました。

tomo-wait-for-it-yuki.hatenablog.com

今回は、CO2センサから読み取った値をディスプレイに表示します。 Knurling Sessions では Waveshare 4.2 inch ePaper ディスプレイを使っていますが、とりあえず手元にあった SSD1331 でやってみることにしました。

f:id:tomo-wait-for-it-yuki:20210501204734p:plain
Waveshare 4.2 inch b&w ePaper Display

f:id:tomo-wait-for-it-yuki:20210501204919p:plain
SSD1331

embedded-graphics を使っているので、なんとかなるだろう、と踏みました。 ePaper ディスプレイが 300x200 のサイズに対して、SSD1331 は 96x64 なので、サイズは調整が必要です。 また、ePaper ディスプレイは白黒なのに対して、SSD1331 は RGB フルカラーなので色の使い方も調整します。

Hello, e-Paper Display!

ピンアサインだけ一応ドキュメントに合わせた以外はほぼ独自で書きました。

Cargo.toml に ssd1331 crate と embedded-graphics crate を追加します。

ssd1331 = "0.2.3"
embedded-graphics = "0.6.2"

初期化はこのような感じです。

use ssd1331::{DisplayRotation::Rotate0, Ssd1331};

#[cortex_m_rt::entry]
fn main() -> ! {
    // ...
    let din = pins_1.p1_01.into_push_pull_output(Level::Low).degrade();
    let clk = pins_1.p1_02.into_push_pull_output(Level::Low).degrade();
    let _cs = pins_1.p1_03.into_push_pull_output(Level::Low);
    let dc = pins_1.p1_04.into_push_pull_output(Level::Low);
    let mut rst = pins_1.p1_05.into_push_pull_output(Level::Low);

    let spi_pins = spim::Pins {
        sck: clk,
        miso: None,
        mosi: Some(din),
    };
    let spi = Spim::new(board.SPIM3, spi_pins, spim::Frequency::K500, spim::MODE_0, 0);

    let mut display = Ssd1331::new(spi, dc, Rotate0);
    display.reset(&mut rst, &mut timer).unwrap();
    display.init().unwrap();

    // ...
}

とりあえず 1 pixel 書いてみます。 ssd1331 は flush() メソッドを呼び出さないとディスプレイに描画が始まらないので注意です。

    display.set_pixel(10, 20, 0xf00);
    display.flush().unwrap();

Display Sensor Data

さて、いよいよ大詰めです。 ここでは、動的に文字列をディスプレイに表示する方法が説明されています。

arrayvec::ArrayString を使います。ArrayString は長さが固定の文字列として使えます。 次のような感じで、SCD30 から読み取った浮動小数点数を文字に変換します。

let mut buf = ArrayString::<[_; 12]>::new();
write!(&mut buf, "{:.2} {}", value, unit).expect("Failed to write to buffer");

文字サイズと文字の表示位置を調整して…

f:id:tomo-wait-for-it-yuki:20210501202831p:plain
SCD30 で読み取った値を SSD1331 に表示

f:id:tomo-wait-for-it-yuki:20210501202934p:plain
ちょっと小さいけど良い感じに表示できている

さいごに

少々機材が高いですが、基礎から学ぶ組込みRust を読んだ方なら、すんなりできるのではないでしょうか。 probe-run や defmt といったツールのできがよく、良いディベロッパー経験を得られます。

Knurling Sessions 2020Q4 をやる (3) 〜CO2センサドライバ〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!

現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。

2020 Q4 のレンダリングされているドキュメントはこちら。

session20q4.ferrous-systems.com

前回、Lチカ終わらせたので、いよいよ CO2 センサ (SCD30) ドライバを書いていきます。

tomo-wait-for-it-yuki.hatenablog.com

Hello, Sensor!

まずは、CO2 センサの配線です。スルーホール用のテストワイヤ使ってみました。

f:id:tomo-wait-for-it-yuki:20210501171508p:plain
CO2センサの接続

SCD30 は、I2C インタフェースで制御します。 I2C ドライバのオブジェクトを作成して、SCD30 ドライバのオブジェクトを初期化します。

    let scl = pins.p0_30.degrade().into_floating_input();
    let sda = pins.p0_31.degrade().into_floating_input();
    let twi_pins = twim::Pins{ scl, sda };
    let i2c = Twim::new(board.TWIM0, twi_pins, twim::Frequency::K100);
    let mut scd30 = SCD30::init(i2c);

SCD30 ドライバも自作します。 データシートを見て、firmware version を取得するコードを書きます。

pub struct SCD30<T: Instance>(Twim<T>);

impl<T> SCD30<T> where T: Instance {
    pub fn init(i2c: Twim<T>) -> Self {
        SCD30(i2c)
    }

    pub fn get_firmware_version(&mut self) -> Result<[u8; 2], Error> {
        let command: [u8; 2] = [0xd1, 0x00];
        let mut rd_buffer = [0u8; 2];

        self.0.write(DEFAULT_ADDRESS, &command)?;
        self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

        let major = u8::from_be(rd_buffer[0]);
        let minor = u8::from_be(rd_buffer[1]);

        Ok([major, minor])
    }
}

これで firmware version が取得できるようになります。

       0 INFO  Firmware Version: 3.66

Start Measuring

データシートを見ながら、SCD30 ドライバにメソッドを追加していきます。 今回作成する SCD30 ドライバは、70 行くらいで、それほど複雑ではありません。

impl<T> SCD30<T> where T: Instance {
    pub fn start_continuous_measurement(&mut self, pressure: u16) -> Result<(), Error> {
        let mut command: [u8; 5] = [0x00, 0x10, 0x00, 0x00, 0x00];
        let argument_bytes = &pressure.to_be_bytes();
        command[2] = argument_bytes[0];
        command[3] = argument_bytes[1];

        let mut crc = Crc::<u8>::new(0x31, 8, 0xff, 0x00, false);
        crc.update(&pressure.to_be_bytes());
        command[4] = crc.finish();

        self.0.write(DEFAULT_ADDRESS, &command)?;

        Ok(())
    }

    pub fn data_ready(&mut self) -> Result<bool, Error> {
        let command: [u8; 2] = [0x02, 0x02];
        let mut rd_buffer = [0u8; 3];

        self.0.write(DEFAULT_ADDRESS, &command)?;
        self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

        Ok(u16::from_be_bytes([rd_buffer[0], rd_buffer[1]]) == 1)
    }

    pub fn read_measurement(&mut self) -> Result<SensorData, Error> {
        let command: [u8; 2] = [0x03, 0x00];
        let mut rd_buffer = [0u8; 18];

        self.0.write(DEFAULT_ADDRESS, &command)?;
        self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?;

        let co2 = f32::from_bits(u32::from_be_bytes([rd_buffer[0], rd_buffer[1], rd_buffer[3], rd_buffer[4]]));
        let temperature = f32::from_bits(u32::from_be_bytes([rd_buffer[6], rd_buffer[7], rd_buffer[9], rd_buffer[10]]));
        let humidity = f32::from_bits(u32::from_be_bytes([rd_buffer[12], rd_buffer[13], rd_buffer[15], rd_buffer[16]]));

        let data = SensorData{ co2, temperature, humidity };
        Ok(data)
    }

ここまでできると、defmt 経由で計測結果を出力します。

f:id:tomo-wait-for-it-yuki:20210501171053p:plain
CO2

Red Alert!

CO2 センサ、RGB LED、ブザーを組み合わせてプログラムを作成します。

Knurling Sessions 2020Q4 をやる (2) 〜基本編〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!

現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。

2020 Q4 のレンダリングされているドキュメントはこちら。

session20q4.ferrous-systems.com

前回、環境構築したので、LED やボタン使う基本からやっていきます。

tomo-wait-for-it-yuki.hatenablog.com

Hello World

nRF52840-DK ボード上の L チカからです。

コードは1_hello_extended.rs のような感じで、どこかで見たのと同じような感じだなぁ、となるかと思います。

External RGB LED

お次は、RGB LED を使って、様々な色で LED を点灯します。 3つ口のある LED というだけなので、それほど通常の L チカと違う感じではありません。

https://akizukidenshi.com/catalog/g/gI-02476/

が、サクッと家のどこかに埋まっている RGB LED を発掘できなかったので、nRF52840-DK の LED を3つ使って代替しました。

Internal Temperature Sensor

nRF52840 に搭載されている内蔵温度計の温度を defmt を使って出力します。 別途 UART がいらないので、ここは非常に体験が良いです。

3_temperature.rs

More about Methods

ここまでで作ったものをリファクタリングします。 この、どのピン選んでも動くようにジェネリックな型にするの良いですね。 (組込み Rust 本でもやりたかったのですが、お蔵入りになってます)

- struct LEDState {
-     r: P0_03<Output<PushPull>>,
-     b: P0_04<Output<PushPull>>,
-     g: P0_28<Output<PushPull>>,
- }
+ struct LEDColor {
+     r: Pin<Output<PushPull>>,
+     b: Pin<Output<PushPull>>,
+     g: Pin<Output<PushPull>>,
+ }

初期化も次のよな感じで、GPIO ピンのモードをジェネリックパラメータにして、どんなモードのピンでも関数の引数に指定できるようになっています。

pub fn init<Mode>(led_red: Pin<Mode>, led_blue: Pin<Mode>, led_green: Pin<Mode>) -> LEDColor {

    LEDColor {
        r: led_red.into_push_pull_output(Level::High),
        b: led_blue.into_push_pull_output(Level::High),
        g: led_green.into_push_pull_output(Level::High),
    }
}

Adding User - Input

ボタン入力を受け付けるやつです。 ボタンを押している間、LED が点灯するプログラムを作成します。 組込み Rust 本執筆中は読んでなかったのですが、やっぱ同じような過程を経るものですねぇ。

Adding User Input - Advanced

もう少し真面目にボタンが押された瞬間を受け付けます。 次のような型を定義して、立ち上がりエッジを検出します。

struct Button {
    pin: Pin<Input<PullUp>>,
    was_pressed: bool,
}

ボタンが押されたら、温度を表示する単位を変換するコードを作成します。

Bringing it all Together

RGB LED と、温度とボタンを組み合わせたプログラムを作成します。 組み合わせだけなので、新しいことはないです。

次回とうとうお高い CO2 センサが登場します。

Knurling Sessions 2020Q4 をやる (1) 〜環境構築〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!

現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。

2020 Q4 のレンダリングされているドキュメントはこちら。

session20q4.ferrous-systems.com

ハードウェア

ferrous-systems.com

主要なもの - nRF52840-DK (¥6,000くらい) - Mouser とか Digikey とかマルツで購入できます - SCD30 CO2 センサ (¥6,000くらい) - Digi-Key

あと、ePaper ディスプレイが挙げられていますが、いくつか SPI / I2C 接続可能なディスプレイデバイスを持っているので、それで代用することにします (embedded-graphics 使っているので大丈夫なはず) 。

ケーブル類など - RGB LED / 220Ω 抵抗器3つ - なくても nRF52840-DK の LED で代用可能 - ジャンパワイヤ - ピンヘッダ

ツール

cargo install でインストールします。

cargo install cargo-generate
cargo install probe-run
cargo install flip-link

Linux

USB ドライバと udev の設定をします。

sudo apt-get install libudev-dev libusb-1.0-0-dev

udev は knurling-session の手順では、JLink の Product ID が異なる場合があります。

$ cat /etc/udev/rules.d/50-knurling.rules
# udev rules to allow access to USB devices as a non-root user

# nRF52840 Development Kit
ATTRS{idVendor}=="1366", ATTRS{idProduct}=="1015", TAG+="uaccess"

nRF52840-DK を接続して、lsusb で Product ID を確認して、udev rule を書き換えるとうまく動きます。 私の場合は、0105 でした。

$ lsusb
Bus 001 Device 012: ID 1366:0105 SEGGER J-Link
sudo udevadm control --reload-rules

これで準備完了です。

『基礎から学ぶ 組込みRust』を書きました

はじめに

2021年4月20日、『基礎から学ぶ 組込みRust』(C&R研究所) を出版します。Rust の文法から組込み Rust でファームウェアを作成するところまでを、片手で持てる (多分!まだ持ったことないけど!) 1 冊の本にまとめた、喉から手が出るほど (私が) 求めていた書籍です。 先日、無事入稿を果たしたので、今の気持ちを徒然なるまま綴ったのが、このエントリです。

基礎から学ぶ 組込みRust

基礎から学ぶ 組込みRust

一番始めに言いたいこととしては、今現在、「組込み Rust がプロダクションレディか?」、と問われると、「ほとんどのプロジェクトに対してそうではない」というのが私個人の見解です。セーフティクリティカルな分野では、機能安全をはじめとする認証の問題があり、そんなにおいそれとプログラミング言語変更できないことも承知しています。

でも、その上で言いたい!
プログラミング言語は進化していて、Rust は本当に良いプログラミング言語です!私は組込み製品開発で Rust を使いたいと強く望みます。 C/C++ の他にもう1つくらい、選択肢があっても良いと思いませんか?思いますね!思いましょう!
Rust を有力な選択肢とするためにはエコシステムの発展が欠かせません。そのためには関わる人口を増やさねば!と考えたわけです。そんなおり、今回の書籍執筆のお話をいただいて、本当に価値ある書籍を書けるかどうか不安に思いながらも、本書を執筆するにいたりました。

普段から Rust を書いている方は、これを機に組込み Rust にも興味持って頂けると嬉しいです。 本業が組込みの方は、とりあえず仕事で使うかどうかは置いておいて、最近話題になってるけどどんなもんかな、という気持ちで触ってもらえると幸いです。 けっこう既存のドキュメントにはない、コードリーディングとトライ&エラー繰り返して習得したこと、いっぱい書きました。

あと、冒頭でお知らせしておきたいのは、非常に残念なことながら、本書のカバー内容ではプロユースできるレベルには到達できません。そういう期待を持って本書に興味を持ってくださった方は、ごめんなさい!今後にご期待下さい。

私と Rust

私が Rust を学び始めたのは、3年前からで、比較的新参者と言えるでしょう。当初から C/C++ 以外で組込みで使える言語ないかなぁ、というモチベーションでした。 Rust の入門を済ませて、組込み Rust のドキュメント3本 (The Embedded Rust Book, Discovery, Embedonomicon) を夢中で読み、「これはイケる!」という結論になりました。

当時は、今よりさらに組込み Rust やっている人が少なくて、まず仲間を増やすことが第一歩だな、と考えたことを覚えています。 先述の組込み Rust ドキュメントの和訳が存在していなかったため、まずリーチできる日本語の情報源を増やそう、ということで、2018年の年末からドキュメントの和訳を開始しました。おおよそ半年かけて3本のドキュメントを和訳しました。

この頃、ブログでも積極的に組込み Rust のエントリを書き、Interface のイベントやったり、第1回技書博で組込み/ベアメタルRustクックブック出したりしていました。

そして記念すべき、Interface 2020年5月号 に組込み Rust の特集が掲載され、少しずつ活動の芽が出てきている感じがしていました。

その後、私事ですが、次男が産まれたことで少し活動の時間が取りにくくなり、なんやかんやしている間に本書の執筆をすることになりました (雑。

これは内緒なのですが、実践Rustプログラミング入門に組込みの章があって、組込み Rust の認知度が上がるの良いぞぉ、と思う半面、先を越された!と本気で悔しがっておりました。

書籍執筆の経緯

まさかこんなに早く組込み Rust を題材にした書籍を出版する機会が訪れるとは思ってもみませんでした。 現在担当してくださっている方から、Rust の基礎を解説する書籍執筆の依頼を受けました。

この時点で内心ウキウキでしたが、1つものすごい不安がありました。それは、「Rust の基礎を解説する書籍」の執筆依頼だったことです。 正直、組込み Rust という特定分野ですらあやしいのに、Rust の基礎を解説するのに私という人選が良いとは思えなかったわけです。 なので、組込み Rust に関してだったら書けると思うけど、本当に Rust の基礎が必要なら不適任です、という返信をしました。 この時点で、「多分お断りだろうなぁ…」と8割方諦めていました。

先方から、「組込み Rust でええよ」(関西弁ではなかったですが) 、という予想外の返答が返ってきたので、心底驚きました。 この機会を逃すと次がいつになるかわかったものではないな、という打算もあり、Rust の文法から組込み Rust を扱う、という企画で執筆依頼を受けました。

Wio Terminal を選んだ理由

企画の当初から、Rust の文法解説から始めて、最終的にターゲットボードでアプリケーションを作る、という流れで話を進めていました。 そこで、ネックになったのが、ターゲットボード何にするか?です。
Discovery book だと STM32F3Discovery を採用しているのですが、現在だと少し入手性が悪くなっており、ドキュメントの内容終わった後も何して遊んで良いものか、という感想を持っていました。 (Discovery book は micro:bit 使うバージョンへの書き換えが進行中です)

そこで、当面は入手性が良さそうで、遊びがいがあって、安くて (3,000円くらい)、GDB デバッグできるもの、という条件で探していました。 遊びがい、という意味では、画面欲しいよね、と思ってました。やっぱわかりやすく遊べるので。embedded-graphics クレートの存在も知っていたので、対応するドライバがあるやつで探していると、意外と STM32 の評価ボードは高かったりで難航していました…。

当時、まだ共著じゃなかった井田さんに良いボードない?って質問したところ、Wio Terminal を紹介されました。 Wio Terminal は色々デバイスが載っているわりに、4,000円弱で手に入って、ちょっと改造すれば GDB デバッグもできます。 本の内容一通りやった後でも、色々遊べそうということもあり、Wio Terminal をターゲットにすることにしました。

Wio Terminal と Rust でいくなら、井田さんも書こうよ、という話をして、このあたりで引きずり込みました。

f:id:tomo-wait-for-it-yuki:20210329203923p:plain
かにさんを Wio Terminal に描写して遊びます

基礎から学ぶ組込みRust の良いところ

Rust の (最低限の) 文法から、組込み Rust のアプリケーション開発まで1冊の本にまとまっていることです。 この範囲を1冊でカバーしている本は、いまのところないはずです。 特に、embedded-hal トレイトの解説や BSP クレートを使ったアプリケーション作成方法は、あまりまとまった情報がないので、それが書かれているのは1つの特徴です。

ただ、Rust に入門する目的では、本書に掲載している文法の章を読むより、The Rust Programming Language 読んだり、他の入門書読む方が良いです。 またページ数の都合上、ドライバ書いたり、ブートストラップ書いたり、組込み Rust のリアルタイムフレームワーク RTIC を使ったりする内容は書けませんでした (無念じゃー!) 。

もう1つ重要な良いところがあります。 それは、私がしばらくは embedded-hal を取り扱った入門書を書かなくて済むことです! とにかく組込み Rust の認知度をあげるための活動を第一に行っていたわけですが、そのために自分が Rust を書く時間が少なくなっていました。 書籍発売で1つの区切りもでき、今後は自分が何か作りながら、その過程をまとめて公表できればなぁ、と思います。

書籍の想定読者と内容

組込みも Rust もあまり知らない状態からでも読めるようにしたつもりです。 しかし、どちらかの入門が済んでいる方が読みやすいと思います。どちらかというと、Rust の入門が済んでいる方が良いと思います。

C/C++/Rust 未経験者が本書を読んでどう感じるか、は未知数ですが、ちょっと色々話がつながらないところが出てくるのか、と思ってます。

回路図も参照しながら、こうデバイスがつながっているから、実装がこうなっているんだよ、という解説も書きました。 本書を読み終わった後でも、Wio Terminal 上の本書で取り扱っていないデバイス動かしたり、別のボード使うときに雰囲気だけでも読めると捗るはずです。 回路について、詳細な解説はしていません。なので回路図を見たことない方も気負わず読めると思います。

本書内で扱うデバイスは次の通りです。

  • LEDとボタン / GPIO
  • シリアル通信 / UART
  • タイマ / 割り込み
  • ブザー / PWM
  • 光センサ / ADC
  • 加速度センサ / I2C
  • 液晶ディスプレイ / SPI

無線はまだ動かないので、今後にご期待下さい!

サポートサイト

本書のサポートサイトです。本書について質問があれば、Issue にて受け付けています。また、本書で使うハードウェア一覧を掲載していますので、ぜひ発売日までにハードウェアを入手して、本の購入とともに Wio Terminal で遊んでいただければ幸いです。

目次を節レベルまで掲載していますので、ご購入の参考にして下さい。

GitHub - tomoyuki-nakabayashi/Embedded-Rust-from-Basics: 中林智之、井田健太が執筆した『基礎から学ぶ 組込みRust』 (C&R研究所) のサポートサイトです。

Rust serialport お試し①

はじめに

Rust でシリアル通信したくなったので、serialport クレートを使ってみます。

docs.rs

serialport クレート

serialport クレートはクロスプラットフォームで使えるシリアルポートライブラリです。 各プラットフォーム固有の API も提供しています。

using blocking I/O with serial ports

とあるので、async には対応していなさそうです。

環境

open

シリアルポートの open はビルダーで行います。デバイスパスとボーレートを指定して SerialPortBuilder オブジェクトを取得し、シリアルポートの設定を行います。

    let mut port = serialport::new("/dev/ttyUSB0", 115200)
        .stop_bits(serialport::StopBits::One)
        .data_bits(serialport::DataBits::Eight)
        .timeout(Duration::from_millis(10))
        .open()
        .unwrap_or_else(|e| {
            eprintln!("Failed to open \"{}\". Error: {}", "/dev/ttyUSB", e);
            ::std::process::exit(1);
    });

new() は下の通りなので、ストップビットとデータビットはデフォルト値で良いのですが、お試しのために設定しています。

serialport/src/lib.rs
pub fn new<'a>(path: impl Into<std::borrow::Cow<'a, str>>, baud_rate: u32) -> SerialPortBuilder {
    SerialPortBuilder {
        path: path.into().into_owned(),
        baud_rate,
        data_bits: DataBits::Eight,
        flow_control: FlowControl::None,
        parity: Parity::None,
        stop_bits: StopBits::One,
        timeout: Duration::from_millis(0),
    }
}

大体普通のシリアルポート設定が SerialPortBuilder でできます。受信時のタイムアウトtimeout() で設定できます。

serialport/src/lib.rs
    /// Set the amount of time to wait to receive data before timing out
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

open() の代わりに open_native() すると、プラットフォーム固有のシリアルポートオブジェクトが取得できます。

serialport/src/lib.rs
    /// Open a platform-specific interface to the port with the specified settings
    #[cfg(unix)]
    pub fn open_native(self) -> Result<TTYPort> {
        posix::TTYPort::open(&self)
    }

    /// Open a platform-specific interface to the port with the specified settings
    #[cfg(windows)]
    pub fn open_native(self) -> Result<COMPort> {
        windows::COMPort::open(&self)
    }

read

手元のマイコンボードから送信された Hello World を受信してみます (とりあえずエラーは無視しています) 。

    let mut serial_buf: Vec<u8> = vec![0; 1000];
    loop {
        match port.read(serial_buf.as_mut_slice()) {
            Ok(t) => io::stdout().write_all(&serial_buf[..t]).unwrap(),
            Err(_e) => {},
        }
    }
$ cargo run
# マイコンボードをリセットします
hello world

お、ええやん。

knurling-rs のツールお試し Get a grip on bare-metal Rust!

はじめに

この記事は Rust Advent Calendar 2020 12日目の記事です。

github.com

Our mission is to improve the embedded Rust experience. To achieve this, we build and improve tools and create learning materials.

knurling-rs は Ferrous Systems による組込み Rust の開発経験を向上するためのツールや教材を作成するプロジェクトです。 本記事では、knurling-rs で提供されているツールを紹介します。

ターゲットボードにはアプリケーションテンプレートの手順で説明されている nRF52840-DK を使います。

プロジェクト成果物一覧 (2020/12時点)

次の3つのツールが公開されています。

  • probe-run : 組込み Rust のプログラムをネイティブと同じように実行します
  • defmt : 効率的な文字列フォーマットを提供します
  • flip-link : ゼロコストなスタックオーバーフロー保護機能を提供します

probe-run

github.com

Cargo のカスタムランナーで、RTT 経由でプログラムの出力を表示します。 ブレイクポイントでスタックバックトレースを表示してファームウェア実行を終了することができます。

UART を接続して他ターミナルを立ち上げなくても、ファームウェアからの出力を見ることができて、とっても便利です。 バックトレース表示してくれるのも Good です。

defmt

github.com

defmt ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers.

ということで、可能な文字列フォーマットはホスト側で行う仕組みになっており、マイコン側の負担が軽減されています。

flip-link

github.com

スタックオーバーフローを検出してくれます。組込み開発では気づきにくいバグなので、ありがたいですね。 現在は ARM Cortex-M でのテストが実施されています。

GitHub Sponsor でスポンサーになると、公開前のツールや教材をいち早く試すことができます。 現在で言うと、組込み Rust の教材 knurling-books はスポンサーのみが見ることができます。

サンプルアプリケーションを動かしてみる

セットアップ

Linux では libusb と libudev が必要です。

Knurling のツールをセットアップします。

$ cargo install flip-link
$ cargo install probe-run
$ cargo install cargo-generate

sudo なしでデバイスにアクセスできるように、udev ルールを設定しておきます。

/etc/udev/rules.d/99-nrf.rules
# udev rules to allow access to USB devices as a non-root user

# nRF52840 Development Kit
ATTRS{idVendor}=="1366", ATTRS{idProduct}=="1015", TAG+="uaccess"

アプリケーションテンプレートの作成

$ cargo generate \
    --git https://github.com/knurling-rs/app-template \
    --branch main \
    --name my-app

アプリケーションテンプレートの修正

nRF52840-DK 用に修正します。 これも手順通りです。

.cargo/config.toml
-runner = "probe-run --chip $CHIP --defmt"
+runner = "probe-run --chip nRF52840_xxAA --defmt"

デフォルトだと Cortex-M0 のターゲットトリプルが指定されているので、Cortex-M4F をターゲットにする。

.cargo/config.toml
-target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
+# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
 # target = "thumbv7m-none-eabi"    # Cortex-M3
 # target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
-# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
+target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
Cargo.toml
-# some-hal = "1.2.3"
+nrf52840-hal = "0.11.0"
src/lib.rs
-// use some_hal as _; // memory layout
+use nrf52840_hal as _;

実行

nRF52840-DK を USB ケーブルでホスト PC に接続して、実行します。

$ cargo rb hello
    Finished dev [optimized + debuginfo] target(s) in 0.02s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/hello`
  (HOST) INFO  flashing program (8.14 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  Hello, world!
└─ hello::__cortex_m_rt_main @ src/bin/hello.rs:8
stack backtrace:
   0: __bkpt
   1: my_app::exit
        at src/lib.rs:29
   2: hello::__cortex_m_rt_main
        at src/bin/hello.rs:10
   3: main
        at src/bin/hello.rs:6
   4: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   5: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

Hello, world! が出力されてスタックトレースが表示されました。

サンプルコードを拝見

サンプルコードの中身を少し見てみます。

まず、src/lib.rs です。 panic-probedefmt で使うグローバルオブジェクトや関数が実装されているようです。

src/lib.rs
#![no_std]

use core::sync::atomic::{AtomicUsize, Ordering};

use defmt_rtt as _; // global logger
use nrf52840_hal as _;

use panic_probe as _;

// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
    cortex_m::asm::udf()
}

#[defmt::timestamp]
fn timestamp() -> u64 {
    static COUNT: AtomicUsize = AtomicUsize::new(0);
    // NOTE(no-CAS) `timestamps` runs with interrupts disabled
    let n = COUNT.load(Ordering::Relaxed);
    COUNT.store(n + 1, Ordering::Relaxed);
    n as u64
}

/// Terminates the application and makes `probe-run` exit with exit-code = 0
pub fn exit() -> ! {
    loop {
        cortex_m::asm::bkpt();
    }
}

hello.rs ではログレベル INFO で Hello, world を出力したあとに、my_app::exit() を呼んでいます。 cortex_m::asm::bkpt() の実行でバックトレースを吐き出して、プログラムが停止するようになっているみたいです。

src/bin/hello.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("Hello, world!");

    my_app::exit()
}

src/bin 下には他にも5つのサンプルがあります。

$  ls
bitfield.rs  format.rs  hello.rs  levels.rs  overflow.rs  panic.rs

panic.rs は、panic 発生時にバックトレースが出力されることが確認できます。

src/bin/panic.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("main");

    defmt::panic!()
}

実行すると次のようになります。

$ cargo rb panic
   Compiling my-app v0.1.0 (/home/tomoyuki/rust/my-app)
    Finished dev [optimized + debuginfo] target(s) in 0.25s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/panic`
  (HOST) INFO  flashing program (8.22 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  main
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:8
0.000001 ERROR panicked at 'explicit panic'
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:10
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: _defmt_panic
        at src/lib.rs:15
   4: defmt::export::panic
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/defmt-0.1.3/src/export.rs:204
   5: panic::__cortex_m_rt_main
        at src/bin/panic.rs:10
   6: main
        at src/bin/panic.rs:6
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

overflow.rsは、スタックオーバーフローを検出する flip-link crate の動作を確認するものです。 わざと大きなデータを ack 関数の入り口で確保して、ひたすら再帰呼び出しします。

src/bin/overflow.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    ack(10, 10);
    my_app::exit()
}

fn ack(m: u32, n: u32) -> u32 {
    defmt::info!("ack(m={:u32}, n={:u32})", m, n);
    let mut big = [2; 512];
    if m == 0 {
        n + 1
    } else {
        big[100] += 1;
        if n == 0 {
            ack(m - 1, 1)
        } else {
            ack(m - 1, ack(m, n - 1))
        }
    }
}

実行すると次のようになります。

$ cargo rb overflow
    Finished dev [optimized + debuginfo] target(s) in 0.76s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/overflow`
  (HOST) INFO  flashing program (8.67 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  ack(m=10, n=10)
└─ overflow::ack @ src/bin/overflow.rs:13
0.000001 INFO  ack(m=10, n=9)
└─ overflow::ack @ src/bin/overflow.rs:13
// ...
0.004313 INFO  ack(m=1, n=1)
└─ overflow::ack @ src/bin/overflow.rs:13
0.004314 INFO  ack(m=1, n=0)
└─ overflow::ack @ src/bin/overflow.rs:13
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: {"package":"panic-probe","tag":"defmt_error","data":"{:str}{:str}","disambiguator":"18392481814486563737"}
error: the stack appears to be corrupted beyond this point
  (HOST) ERROR the program has overflowed its stack

スタックオーバーフローしたことを最後に教えてくれていますね。

テスト

testsuite 下にテストコードがあります。defmt の assert と assert_eq を使っていることと、#[defmt_test::tests]アトリビュートを tests モジュールに付与していること以外、普通のテストを同じような感じですね。

testsuite/tests/test.rs
#![no_std]
#![no_main]

use my_app as _; // memory layout + panic handler
use defmt::{assert, assert_eq};

// See https://crates.io/crates/defmt-test/0.1.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
    #[test]
    fn assert_true() {
        assert!(true)
    }

    #[test]
    fn assert_eq() {
        assert_eq!(24, 42, "TODO: write actual tests")
    }
}

次のコマンドで実行できます。ちゃんとアサーションが失敗したときにはバックトレースを出力されています。

$ cargo test -p testsuite
0.000000 INFO  running assert_true ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000001 INFO  .. assert_true ok
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000002 INFO  running assert_eq ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000003 ERROR panicked at 'assertion failed: `(left == right)`
  left: `24`,
 right: `42`: TODO: write actual tests', testsuite/tests/test.rs:…
└─ panic_probe::print_defmt::print @ /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: rust_begin_unwind
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libcore/panicking.rs:85
   5: test::tests::assert_eq
        at tests/test.rs:18
   6: main
        at tests/test.rs:9
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
error: test failed, to rerun pass '-p testsuite --test test'

素晴らしい。お手軽にベアメタルでもテストが書けますね!

knurling って何?

www.weblio.jp

ハンドルの握り棒や検査具の丸棒など使用中に手や指が滑らないようにするため操作部分に付ける横、または斜めの凹凸をローレットといい、旋盤などで凹凸を付けることをローレット加工、その加工を行うときに使用する工具をローレット工具(またはローレット)という。また、転造法の一種でダイヤモンド形、七子目などの凹凸のあるロール(こま型エ具)を、回転しながら棒材の表面に押し付けてギザギザを付ける加工をいう。

へー!