「基礎から学ぶ組み込みRust」を読んで、Rustのコードを書いてみた

Seeed K.K.の中井です。

4月20日に発売された「基礎から学ぶ組込みRust」ですが、動作確認用のマイコンに「Wio Terminal」が採用されているということで、さっそく購入して実践してみました。

実は密かにRustに興味をもっていたので、仕事でRustに触れるいい機会だと思いポチっていました。 本書中にも記載がありましたが、Linuxのシステム記述言語として採用する動きがあるという記事を見てそろそろやらないとと思っていたのです。 Wio Terminalを採用していただいた中林さん、井田さんに感謝です! また、本書と一緒にWio Terminalをご購入いただいている読者も多くいるようで嬉しい限りです!

f:id:mnakai:20210428084002j:plain

Rustの特徴 (本書中より一部抜粋)

  • C/C++と同等に動作速度が速く、メモリ使用量が少なく、バイナリサイズが小さい
  • 所有権システムと型システムでコンパイル時に不正なメモリアクセスをチェックできる
  • パッケージマネージャやビルドシステム、コード補完やリファレンスを表示してくれる言語サーバーなどで快適な開発が可能

他の言語のいいとこどりで且つメモリアクセスに対するセキュリティが向上している言語?組込みシステムでは、放置しているとメモリリークでお亡くなりになるというのがあるので、コンパイルタイムでメモリリークを抑制できるのは非常にメリットになります。メモリリークを見つけるためのデバッグ時間の節約にもなりますし。 またメモリ使用量が小さく、少ししかメモリを積んでいないマイコンなどでも使え、クロスプラットフォームであるというところに魅力を感じます。

読み終えての感想

本書は非常に丁寧に解説されていて、特にハードウェアの解説部分には驚きました。 動作の仕組み、回路図、関連するクレートとトレイトまで、理解を深めるためのリファレンスとして利用することができました。 (アプリ開発中には大変お世話になりましたw)

Rustについては、 最大の特徴であり有益な「所有権システムと型システム」が難しい。慣れれば平気なのかもしれませんが、ちょっとディレイさせたい場合にもディレイオブジェクトを貸し出してあげなくてはならず、少し使いづらいなぁと思うところもあります。グローバルなオブジェクトにすればよいのでしょうが、それが標準的な実装方法なのかが(まだ)判断できず悩んでしまいがちです。

本書で学んだことを実践!Groveモジュールを動かしてみた

手を動かしてみないと理解したのかわからない!ということで、Wio TerminalにGrove - AHT20をつないでアプリケーションを書いてみました。

www.seeedstudio.com

f:id:mnakai:20210430113435j:plain

Grove - AHT20はI2C接続のモジュールなので、Wio TerminalのGrove I2Cポートに接続すればサクッとうまくいくのだろうと考えていましたが、、甘かったです。 というのも本書中のサンプルではPins構造体のサブセットであるSets構造体を使ってLCDやシリアルなどの設定を行っているのですが、Sets構造体にはGrove I2Cポートの定義がなかったのです。Pins構造体には該当ピンの定義があるので何とか使えないかと試行錯誤してみましたがダメでした。逆にPins構造体を使ってLCDなどの制御ができないかも検討しましたが、できそうではあるが手間がかかりそう。。結局、Sets構造体にGrove I2Cポートの定義を加えてアプリケーションを作成しました。(これが正当な方法なのかはわかりません)

Sets構造体にGrove I2Cポートの定義を追加

定義を追加すること自体はそこまで難しくありません。 下記のようなdiffで実現できます。

diff --git a/src/pins.rs b/src/pins.rs
index 1e6297b..b47e5a7 100644
--- a/src/pins.rs
+++ b/src/pins.rs
@@ -171,6 +171,9 @@ pub struct Sets {
     /// QSPI Flash pins
     pub flash: QSPIFlash,
 
+    /// Grove Port pins
+    pub grove_i2c: GroveI2C,
+
     /// Analog Light Sensor pins
     pub light_sensor: LightSensor,
 
@@ -231,6 +234,11 @@ impl Pins {
             d3: self.mcu_flash_qspi_io3,
         };
 
+        let grove_i2c = GroveI2C {
+            sda: self.i2c1_sda,
+            scl: self.i2c1_scl,
+        };
+
         let light_sensor = LightSensor {
             pd1: self.fpc_d13_a13,
         };
@@ -300,6 +308,7 @@ impl Pins {
             buzzer,
             display,
             flash,
+            grove_i2c,
             light_sensor,
             microphone,
             port,
@@ -326,3 +335,8 @@ pub struct HeaderPins {
     pub a7_d7: Pb7<Input<Floating>>,
     pub a8_d8: Pa6<Input<Floating>>,
 }
+
+pub struct GroveI2C {
+    pub sda: Pa17<Input<Floating>>,
+    pub scl: Pa16<Input<Floating>>,
+}

追加したGrove I2CポートでI2CMasterを初期化

上記を追加すると、"sets."と入力すると補完候補にgrove_i2cが!よかったw f:id:mnakai:20210430115517p:plain

ということで、このgrove_i2c.sda / .sclを使ってI2CMasterを初期化すればI2Cデバイスを制御することができます。

    // I2Cドライバオブジェクトを初期化する
    let gclk0 = &clocks.gclk0();

    let i2c: I2CMaster3<
        Sercom3Pad0<Pa17<PfD>>,
        Sercom3Pad1<Pa16<PfD>>
    > = I2CMaster3::new(
        &clocks.sercom3_core(&gclk0).unwrap(),
        400.khz(),
        peripherals.SERCOM3,
        &mut peripherals.MCLK,
        sets.grove_i2c.sda.into_pad(&mut sets.port),
        sets.grove_i2c.scl.into_pad(&mut sets.port),
    );

作成したアプリケーションの全体のコードはこの記事の末尾に貼り付けました。

やってみたが失敗した (Grove - Ultrasonic Ranger)

実はGrove - AHT20の前には、Grove - Ultrasonic Rangerを動かそうと試していました。理由は本書中に1-Wire的な制御方法のサンプルがなかったためです。結果的に投げ出してしまったのは、1つの物理ピンでINPUTとOUTPUTを切り替えて制御することができなかったためです。(調べ方が足りなかったというのもあります)

同じような制御となるDHT11/22のサンプルがあったので参考にできないかを試していたのですが、この時は出来ませんでした。いま改めてライブラリのコードをみると、、できそうな気がするw

色々なコードを見て実力をつけないといけませんね。

まとめ

熟成しきっていない感がある組込みRustですが、数年先には組込み業界でもRustを標準的に採用する企業が多くなりそうと予感させてくれるくらいに魅力を感じました。 今回は本書でさわりの部分を学習してみましたが、もっと踏み込んで学習する必要があると実感しました。

興味を持った方は是非実践を!

www.seeedstudio.com

github.com

  • 本書以外のサンプルコード

とにかくたくさんのコードを動かしてみて学びたい人は、こちらにもサンプルコードがあります。本書中では触れられていないSDカードやUSBデバイスに関わるサンプルがあります。

https://github.com/atsamd-rs/atsamd/tree/master/boards/wio_terminal/examples

usb_serial_display.rsをビルドして動かしてみました。

f:id:mnakai:20210430113406j:plain

作成したアプリケーションのコード全体

ベースにさせていただいたのは、本書中の「7-4-splash.rs」です。

#![no_std]
#![no_main]

use embedded_graphics as eg;
use panic_halt as _;
use wio_terminal as wio;

use eg::{fonts::*, pixelcolor::*, prelude::*, primitives::Rectangle, style::*};
use wio::hal::{clock::GenericClockController, delay::Delay, gpio::*, sercom::*};
use wio::hal::hal::blocking::i2c::{
    Read as I2CRead,
    Write as I2CWrite,
};
use wio::pac::{CorePeripherals, Peripherals};
use wio::prelude::*;
use wio::{entry, Pins};
use heapless::consts::*;
use heapless::String;
use core::fmt::Write;

#[entry]
fn main() -> ! {
    let mut peripherals = Peripherals::take().unwrap();
    let core = CorePeripherals::take().unwrap();
    let mut clocks = GenericClockController::with_external_32kosc(
        peripherals.GCLK,
        &mut peripherals.MCLK,
        &mut peripherals.OSC32KCTRL,
        &mut peripherals.OSCCTRL,
        &mut peripherals.NVMCTRL,
    );
    let mut delay = Delay::new(core.SYST, &mut clocks);
    let mut sets = Pins::new(peripherals.PORT).split();

    // ディスプレイドライバを初期化する
    let (mut display, _backlight) = sets
        .display
        .init(
            &mut clocks,
            peripherals.SERCOM7,
            &mut peripherals.MCLK,
            &mut sets.port,
            58.mhz(),
            &mut delay,
        )
        .unwrap();

    // LCDのクリア(全体を黒で塗りつぶす)
    let style = PrimitiveStyleBuilder::new()
        .fill_color(Rgb565::BLACK)
        .build();
    Rectangle::new(Point::new(0, 0), Point::new(319, 239))
        .into_styled(style)
        .draw(&mut display).unwrap();

    // I2Cドライバオブジェクトを初期化する
    let gclk0 = &clocks.gclk0();

    let i2c: I2CMaster3<
        Sercom3Pad0<Pa17<PfD>>,
        Sercom3Pad1<Pa16<PfD>>
    > = I2CMaster3::new(
        &clocks.sercom3_core(&gclk0).unwrap(),
        400.khz(),
        peripherals.SERCOM3,
        &mut peripherals.MCLK,
        sets.grove_i2c.sda.into_pad(&mut sets.port),
        sets.grove_i2c.scl.into_pad(&mut sets.port),
    );

    // AHT20を初期化
    let mut grove_aht20 = GroveAHT20::new(i2c, 0x38);
    grove_aht20.init(&mut delay);

    Text::new("Temperature", Point::new(30, 30))
        .into_styled(TextStyle::new(Font12x16, Rgb565::WHITE))
        .draw(&mut display).unwrap();

    Text::new("C", Point::new(220, 60))
        .into_styled(TextStyle::new(Font24x32, Rgb565::WHITE))
        .draw(&mut display).unwrap();

    Text::new("Humidity", Point::new(30, 120))
        .into_styled(TextStyle::new(Font12x16, Rgb565::WHITE))
        .draw(&mut display).unwrap();

    Text::new("%", Point::new(220, 150))
        .into_styled(TextStyle::new(Font24x32, Rgb565::WHITE))
        .draw(&mut display).unwrap();

    loop {
        let (temperature, humidity) = grove_aht20.measurement(&mut delay).unwrap();

        let style = TextStyleBuilder::new(Font24x32)
            .text_color(Rgb565::GREEN)
            .background_color(Rgb565::BLACK)
            .build();
        let mut label = String::<U256>::new();
        write!(&mut label, "{:-2.1}", temperature).unwrap();
        Text::new(label.as_str(), Point::new(100, 60))
            .into_styled(style)
            .draw(&mut display).unwrap();

        label.clear();
        write!(&mut label, "{:-2.1}", humidity).unwrap();
        Text::new(label.as_str(), Point::new(100, 150))
            .into_styled(style)
            .draw(&mut display).unwrap();

        delay.delay_ms(1000u16);
    }
}

struct GroveAHT20<I2C> {
    i2c: I2C,
    slave_addr: u8,
}

impl<I2C> GroveAHT20<I2C>
where
    I2C: I2CRead + I2CWrite,
{
    fn new(i2c: I2C, slave_addr: u8) -> Self {
        GroveAHT20 {
            i2c,
            slave_addr: slave_addr,
        }
    }

    fn init(&mut self, delay: &mut Delay) {
        delay.delay_ms(40u16);

        let init_cmd = [0xBE, 0x08, 0x00];
        self.i2c.write(self.slave_addr, &init_cmd).ok();

        delay.delay_ms(10u16);
    }

    fn measurement(&mut self, delay: &mut Delay) -> Result<(f32, f32), ()> {
        let measurement_cmd = [0xAC, 0x33, 0x00];
        self.i2c.write(self.slave_addr, &measurement_cmd).ok();

        delay.delay_ms(80u16);
        loop {
            let mut status = [0x80];
            self.i2c.read(self.slave_addr, &mut status).ok();
            if (status[0] & 0x80) == 0x00 {
                break;
            }
        }

        let mut data: [u8; 6] = [0x00; 6];
        self.i2c.read(self.slave_addr, &mut data).ok();

        let _temperature: u32 = (((data[3] & 0x0F) as u32) << 16) |
            ((data[4] as u32) << 8) |
            (data[5] as u32);
        let temperature = _temperature as f32 / 1048576.0 * 200.0 - 50.0;

        let _humidity = ((data[1] as u32) << 12) |
            ((data[2] as u32) << 4) |
            (data[3] as u32) >> 4;
        let humidity = _humidity as f32 / 1048576.0 * 100.0;

        Ok((temperature, humidity))
    }
}

変更履歴

日付 変更者 変更内容
2021/4/30 mnakai 作成