見出し画像

Raspberry Pi PicoとRustを使って組み込み開発に入門してみました

今年はアドベントカレンダーに参加します!

12/17日分の記事になります。

こちらは初めてRaspberry Pi Picoに触れ、初めてRustを書いてみた感想です。すみません、うっかり長文になりました。

PicoとRustを使ってプログラムをデバッグしたり、LEDアレイを光らせてみたり、それをADCと組み合わせて光り方を制御してみたよという内容になります。

ざっくり感想をまとめるとRaspberry Pi Picoは今でも1000円以下と安価な上に、潤沢に流通していて入手性が良いのがめちゃめちゃえらいです。各種ドキュメントやサンプルとなるソースコードが山盛りでベアメタル入門に最適なボードじゃないかなって思います。
一方組み込みRustはハードウェアが抽象化されていてこんなに楽できていいのって感じで、ドキュメントの品質がめちゃめちゃ良かったり、出来合いのパッケージを検索することであっという間にやりたいことが実現できたりと嬉しさ詰まってます!
逆にムズいなってところはいろんなライブラリが発展途上で手探りで作業を進めていかなければならないところでした。
ひとまずの成果物はこちらに置いてあります。

追記:すみません、肝心のリポジトリがPrivateになっていたのをPublicに変更しました!

状況

今年はいろんなものがひっくり返って散々でしたね。組み込み分野に限っても半導体やパーツの不足、輸送料金の値上がりなどで今までの倍以上のコストがかかってしまっているものも多いのではないかと。どのパーツが不足するかも見えない中で製品開発に二の足を踏んでいる企業もあることでしょう。

自分は音楽が好きです。もっぱら作るほう。でもここ2年ほど半導体不足で音楽関連機材が値上がりして在庫すら安定しなかったところに今年に入ってからの円安が直撃して機材を買うのがかなり厳しい状況になってしまいました。
それなら自分で機材を作れるようになろうと回路などを学び始めた次第です。早くモジュラーシンセのモジュールを作れるようになりたいな。ちなみにこの分野ではハギヲさんがめっちゃ頑張っていてすごいので、どのようなものが作られているのか気になるかたはぜひハギヲさんの記事を読んでみてください。

Raspberry Pi Pico

実はモジュラーシンセではSoM(System On Module)を使った製品も多く出回っています。SoMって何かというとマイコンボードなんかを主基板(?)に接続するやつです。コントローラ周りの基板設計も必要ないし、ある程度ライブラリも提供されていたりで少量生産の製品ではコスト削減に寄与してくれそう。
有名どころだとNoise Engineeringというメーカーのモジュールもそうで、最近ではElectro SmithのDaisyというマイコンボードを載せている製品が多いです。それ以前に開発された製品でもXMOSというメーカーのXS1というプロセッサを乗せたドーターボードを使っていたりします。

Noise Engineering Basimilus Iteritas Alter のドーターボード

ほかには最近のKorg製品がラズパイのコンピューティングモジュールを使っているよっていうのも。

で、こういうのやりたい!と。

ところが調べてみると今はマイコンボードも手に入れづらくなってるではないですか。シンセなどでよく使われているSTM32系のマイコンボードはこの半導体不足の影響をモロに受けているようでなかなか在庫しない様子。
今回は一番入手性良く安価(この円安状況下でも1000円しない)というのに惹かれてRaspberry Pi Picoをチョイス。重たい処理をさせても100mAほどの電力消費というのも気に入りました。拡張ボード類などもけっこうあるみたいです。

とはいえ自分でこういうマイコンを触るのってほぼ初めて。以前NTS-1というKorg社製の小さなシンセにプログラムを書き込むっていうのはやってたりしたんですが・・・。

あと勢い余ってDaisyやTeensyといったボードも取り寄せてはあるんですけどそちらはもうちょっと実力がついてから。信号処理をゴリゴリするならCortex-M33やM4以上になると浮動小数点ユニットやDSPが使えるらしいのでそちらでやったほうが楽そうです。PicoでもqfplibというCortex-M0用の浮動小数点ライブラリが使えるみたいです。
https://www.quinapalus.com/qfplib-m0-full.html

Rustを始める

組み込みでは一般的にCやC++を使うことが多いですよね。今回Rustを選んだのはC以外に挑戦してみたかったのとRustとPicoの利用例が多かったから、コードがシンプルに見えたというのもあります。

Rustで使われるCargoというビルドシステム&パッケージマネージャはシンプルでとても使いやすいです。CMakeに比べて断然とっつきやすいしパッケージの依存解決もやってくれます。Node.jsのnpmみたい。

パッケージマネージャとしての機能は実際npmとよく似ているように見えます。パッケージ(クレート)はcrates.ioのリポジトリに格納されており、`cargo install crate名`とすると必要なcrateをインストール、`cargo add crate名`とするとプロジェクトのdependencyにcrateを追加してくれたり、`cargo search`でパッケージを検索することもお手のものです。中央集権的に管理されたリポジトリがあるのはやはり素敵です。セキュリティ上の懸念はあるにしてもやはりこの一覧性に起因する利便性がないと開発者コミュニティの伸びが鈍化してしまうと思っています。

さて、実際に使ってみると公式ドキュメントの良さにも感動しました。たとえばこれは標準ライブラリの`std::collections`のドキュメントで説明は機能のとどまらず、どのような場合に使えばいいかまで踏み込んだ解説があり、果ては計算量にまで言及されています。
Rustコミュニティではドキュメントも重視されており、この際きちんとRust的にわかりやすいドキュメントを書けるようになっておきたいです。
クレートのドキュメントはソースコード内に記述してrustdocで生成するものが多く、そのためにソースコードがめちゃめちゃ読みやすくなっているのもありがたいですね。

また、コンパイラが出力するエラーもそれがどういうエラーか、修正するのに何をすればいいかもわかりやすくて素敵すぎます。

さらっとRustを触ってみて独特だなーと思ったのはシャドウイングにターボフィッシュ、match、所有権(ライフタイムとか)、トレイトや…たくさんあるっちゃあります。
自分はある程度Rustを覚えるのにRustlingというチュートリアルを使いました。Rust力をつけるためのリングフィットアドベンチャーみたいな。ハンズオン形式で進めていけるのでRustへの理解をより深められます。
Hintを参照してThe Bookを読みつつ、またYouTube上の解説も参考にしてRustを手に馴れさせていきました。

ちなみにRustって名前はCrustacean(甲殻類)から来ているらしく、カニっぽいキャラが随所に出てきます。このキャラクターはFerrisという名前でRustの非公式マスコットらしいです。で、FerrisはFerrousという第一鉄からきているらしく例えばferrous oxideは酸化第一鉄で錆び(rust)を意味してるとのことでカニでも鉄でもいいよ(?)ってことなのでしょうか?

Rustと組み込み

今回のアドベントカレンダーのタイトルにもある「ベアメタル」、最初なんのことだかわかりませんでした。これはおおよそ2つの使われ方をしていて、片方は「OSなし」、もう一方は「アプリケーションがインストールされていない」まっさらの状態という使われ方をしているようです。
使われるコンテクストはだいたい2つあってクラウドと組み込み。クラウドのコンテナやデータセンターのサーバでアプリケーションやOSがインストールされていないものを指す場合と、組み込み開発でOSを載せないターゲットを指す場合があるみたいですね。

ということで今回のはRustで組み込みベアメタル編です。

Rustの組み込みで気をつけたいのはOSがないとシステムコールを必要とする標準ライブラリが利用できないところ。そのため`#![no_std]`というおまじないを書いて標準ライブラリを使わずにプログラミングしていきます。
標準ライブラリのサブセットである`libcore`というクレートがあって`no_std`環境でも利用できますが、こちらはかなり基本的なAPIのみのようです。
組み込み用途でも利用可能なFreeRTOSやZephyrといったOSを使えば標準ライブラリを使えるものかもしれません。ただOSを動かすということはそれだけ消費電力が上がったり気を配らなければいけないことも増えるのでちょっとしたものを作るのであればやはりベアメタルが良さそう。

と、このような制限があるためCrates.ioで公開されているクレートをなんでも使えるというわけではなく、`no_std`に対応したクレートのみがターゲットとなるマイクロコントローラ上で使えます。もしくはstd使ってないCrateも使えるのかな?

さて、組み込みとRust、実はとても美味しい部分があります。
うまいことハードウェア操作を抽象化してあってソフトウェアの再利用性を高めているんです。主にHAL(Hardware Abstraction Layer)というレイヤでハードウェアを抽象化し、マイコンボードごとの差異をその上のレイヤでうまく吸収しています。
`embedded-hal`では入出力ピンやシリアル通信、I2CやSPI、タイマ、ADC/DACなどのインターフェイスが定義され、それがマイクロコントローラごとに実装されているといった感じ。
マイクロコントローラのレジスタなどはHALの下にあるレイヤであるPeripheral Access Crateで抽象化されています。

Rustは組織がしっかりしていて、このHALのような標準化に気を配られているのがかなり嬉しいです。異なるマイコンボードへの移植性が段違いに高くなりそう。組み込み分野ではEmbedded Rust Working Groupが組織され、その中のチーム内でライブラリ仕様などの標準化が行われています。

4-Step Primer on Navigating Embedded Rust HAL Documentation - DEV Community 👩‍💻👨‍💻

PicoとRustで組み込み開発

Raspberry Pi PicoはRaspberry Pi 財団が開発したRP2040というプロセッサのリファレンス実装という面もあるようで、PicoのほかにもRP2040を搭載したマイコンボードはいろいろとあります。PicoではADCが3つしか使えませんがRP2040自体は4つ扱えるのでADCを4つ使えるようにした実装もあったり、フラッシュメモリを多く搭載していたりと様々なバリエーションがあります。

Seeed XIAO RP2040
Adafruit Feather RP2040

このような実装の差分は先ほど述べたHALの上のレイヤーにあるBSPで吸収されています。綺麗ですよね。
RP2040載せた対応ボード一覧はこちら。
https://github.com/rp-rs/rp-hal-boards

開発環境について

ハードウェアは実機とブレッドボードにジャンパワイヤが基本セットでしょうか。とりあえず実機さえあればUSBケーブルを繋ぐだけでソフトウェアの書き込みができるようになります。自分はさらにGroveシールドというのを使ってM5StackというIoTデバイス(?)用に作られたモジュール(センサーやカメラなどの入力デバイスやモーターやリレーなどの制御デバイスなど)をPicoで使えるようにしました。ブレッドボードに配線をしていくのに比べてケーブル一本で繋げられるのは便利です。気になるのは電圧で、GroveデバイスはVCCに5Vを流すものが多いのにPicoのロジック電圧(?)は3.3Vなんです。動かないものもあるかもと思いつつできるところまでやっていきます。

IDEはVisual Studio Codeが良いのかなと。JetBrainsのCLionも好きなんですが、IDE側のリンターにバグがあるのか、コンパイルはできるけれどエラー表示が出るっていうのに悩ませられたというのもあり、Visual Studio Codeを使っていたりもします。Issueもあるのでそのうち治るかも・・・。
ちなみにCargo workspaceを利用する場合にはCLionに軍配が上がります。詳しくは後ほど。
Visual Studio Codeのextensionはrust-analyzerとCortex-Debug、それにEmbedded Toolというのを入れておくと良さげです。あと組み込み以外のRustプログラムもMacでデバッグできるようにCodeLLDBというのもインストールしておきました。

デバッグ環境について

PicoをIDE上でデバッグできるまでの環境を作るやり方にはいくつかあってopenocdとgdbサーバを組み合わせる方法、またCMSIS-DAPプローブを使う方法などがあります。組み込み開発でよく使われるロギングライブラリーにdefmtというものがあり、相性の良いのはRust製のライブラリとCMSIS-DAPプローブを組み合わせて利用する方法です。openocdでもdefmtの出力を利用できますが、若干手間が増えます。デバッグ環境については次の節で詳しく説明します。

PicoをGroveシールドにのせてデバッグ結線した様子

実際にRustプログラムをPicoで走らせてデバッグ

Picoで使えるRustのクレートなどが`rp-rs`というリポジトリにまとまっているので、まずはそこからテンプレートプロジェクトを使ってLチカプログラムを作ってみます(というかリポジトリクローンしてくるだけです)

CMSIS-DAPを使ってデバッグ

コチラのReadmeの内容に従って必要なライブラリをインストールします。VS Code用のprobe-rs-debuggerというextensionもインストールしておきます。

probe-rs-debuggerもインストール

デバッグプローブ用のイメージにはrust-dapのイメージを使ってみました。ブートセレクタボタンを押しながらPicoをコンピュータとUSB接続して、マウントされたディスクにイメージを放り込むだけ。
で、一点気を付けておくべきなのはVS Code用の`launch.json`がそのままでは動作しない点。preLaunchTaskに"rust: cargo build"と設定されているのに、このままデバッグしようとするとタスクが見つからないためにデバッグできないというダイアログが出てしまいます。ちょっと前までできてた気がするんですけど・・・。`.vscode/tasks.json`を作成してそちらにcargo buildを記述して設定しておきました。preLaunchTaskに${defaultBuildTask}と記述してプログラムを走らせようとするとデフォルトビルドタスクが設定されていない場合のタスク候補として"rust: cargo build"が出てくるのに謎な挙動です・・・。

タスク候補にrust: cargo buildが出てくるのですが

ということでこんな感じのtasks.jsonを定義しておきました。

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Cargo Build (debug)",
            "type": "process",
            "command": "cargo",
            "args": [
                "build"
            ],
            "problemMatcher": [
                "$rustc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

実際にPicoを2台結線し、USBケーブルを接続した状態でVSCodeからプログラムを走らせてみます。

変数の値が見えない・・・

一応breakpointで止めたりといったことはできるようになりました。2台用意したPicoを使って一台はデバッガ、一台はターゲットとして使えるようになったし、プログラムの書き込みはいちいちUSBディスクとしてマウントしなくても良くなりました。

が、残念なことにデバッガで変数の値が見えません・・・。Cargo.tomlでopt-level=0、debug=2のように設定してみてもダメ。なぜなのでしょう。どうしても変数の値を見たい。ということでopenocdを使ってみます。

openocdとgdbを使ってデバッグ

こちらはまずMacにデバッグサーバとなるOpenocdというソフトウェアをインストールします。openocdはpicoのリポジトリにあるものを使います。それほど活発に開発されているソフトウェアではないのか、本家のopenocdはpicoに対応してないためです。

で、こちらのopenocdのコンパイルの際にApple Silicon搭載のMacだとコンパイル時にヘッダファイルが見つからないと怒られるのでこちらのIssueを参考にコンパイルしました。

自分の場合、openocdはそのままmake installしておきました。で、Getting Started GuideのAppendixのリンクを辿ってPicoprobeのイメージをダウンロードしてPico一台にインストールします。Picoprobe自体もいろいろと改良されているのは良いのですが、最新のリポジトリをcloneしてきてビルドしたものだと自分の環境では動きませんでした。

では実際にRustでプログラムを書いてopenocd経由でデバッグしてみましょう。先ほどのrp2040-project-templateをベースにしつつ、Digi-Keyのブログも参考にしました。`.cargo/config.toml`のrunnerには"probe-run --chip RP2040"を指定してあります。ようやくデバッグできた時はめちゃめちゃホッとしましたよ。

変数の内容も見える!

ということでVS Code本体やextension、リポジトリなどのアップデートも相まってかなり大変でした。こないだまで動いてた設定が動かないとかホントに勘弁してほしいですね。参考までに現状のlaunch.jsonを載せておきます。あとrp2040.svdはpico-sdkから引っ張ってきて.vscodeディレクトリに置いておきました。なおopenocdはmake installしたので/usr/local以下にインストールされている点にご留意ください。それとプロジェクトの名前はblinkyという名前にしてあります。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Pico Debug",
            "executable": "./target/thumbv6m-none-eabi/debug/blinky",
            "preLaunchTask": "Cargo build",
            "type": "cortex-debug",
            "request": "launch",
            "cwd": "${workspaceRoot}",
            "servertype": "openocd",
            // This may need to be arm-none-eabi-gdb depending on your system
            "gdbPath": "/opt/homebrew/bin/arm-none-eabi-gdb",
            "device": "RP2040",
            "configFiles": [
                "interface/picoprobe.cfg",
                "target/rp2040.cfg"
            ],
            "svdFile": "${workspaceRoot}/.vscode/rp2040.svd",
            "runToEntryPoint": "main",
            "showDevDebugOutput": "raw",
            // Work around for stopping at main on restart
            "postRestartCommands": [
                "break main",
                "continue"
            ],
            "searchDir": [
                "/usr/local/share/openocd/scripts"
            ],
        }
    ]
}

それとtasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Cargo build",
            "type": "shell",
            "command": "cargo",
            "args": [
                "build"
            ],
            "problemMatcher": [
                "$rustc"
            ],
            "group": "build"
        }
    ]
}

あとsettings.json

{
    "rust-analyzer.cargo.target": "thumbv6m-none-eabi",
    "rust-analyzer.checkOnSave.allTargets": false,
    "editor.formatOnSave": true,
    "cortex-debug.openocdPath": "/usr/local/bin/openocd"
}

さてもうひと息。これで終わりじゃありません、defmtの出力をなんとか引き出します。
先ほどリンクを貼っておきましたが、こちらの記事を参考にします。ferrous systemsのferrousってやっぱり鉄を意味してるのでしょうか、鉄システム。
で、この記事の内容を全てやる必要はなく、実際にすべきことはdefmt-printというクレートをインストールして、launch.jsonにpostLaunchCommandsを設定してSeggerのRTT経由でdefmtの出力をモニターできるようにしたらターミナルからncコマンドでその内容を受け取り、defmt-printコマンドでフォーマットして表示するということでした。

今回はpostLaunchCommandに

            "postLaunchCommands": [
                "monitor rtt server start 8765 0",
                // should span the *right* part of RAM
                "monitor rtt setup 0x2003fbc0 0x30 \"SEGGER RTT\"",
                "monitor rtt start",
            ],

このように設定してあります。0x2003fbc0の部分はバイナリによって変わる(?)ようなので適宜rust-nmコマンドを利用して取得します。

% rust-nm -S target/thumbv6m-none-eabi/debug/embedded_target | grep RTT
2003fbc0 00000030 D _SEGGER_RTT

これでようやく準備が整いました。netcatコマンドでターミナルに出力します。

%  nc localhost 8765 | defmt-print -e target/thumbv6m-none-eabi/debug/embedded_target
defmt-print

プログラムからのdefmt出力を無事表示することができてますね。いや長かった・・・。でもこれで終わりじゃなくてプログラムを組んでいくことが本題なんですよ。

Rustを使ったPico用プログラム開発の準備

ようやく開発環境が整備できたので実際にPico用プログラムを書いていきます。

その前にPicoにつなぐ機器類、まずは治具(?)となるGroveシールド。

こちらにPinoutやその他情報があります。

なんかPythonのコードが見え隠れしてて、しかも簡単にプログラム書けそうでうっかりPython書きたくなっちゃいますね。でもここは辛抱してRust修行です。
このGroveシールドではアナログ、デジタル、UART、I2Cを使ったGroveデバイスを利用できます。SPIの端子もあるのにGrove用ポートの形ではなく普通のピンヘッダということはSPIはM5Stackではマイナーなのでしょうか?

NeoPixel互換LEDアレイの制御(実装昼の部)

さて、デバッグ環境作るのにPicoのオンボードのLEDをチカチカさせたので次はもうちょっとたくさんLEDをチカチカさせましょうってことでこちらのモジュールです。

もっと大きくてもいいんです。でもLEDって結構えげつない電力が必要なんですよね。プログラム制御できればいいのでこちらを使っていきます。ドキュメントはこちら。

はい、こちらのドキュメントでもすごい楽できそうなGUIプログラミング環境が登場していますね。でもやはりここは辛抱してRust修行です。
仕様を見るとLEDチップはSK6812って書いてあるのでとりあえずデータシートを参照します。Adafruitのものを見てれば良いのでしょうか。

https://cdn-shop.adafruit.com/product-files/1138/SK6812+LED+datasheet+.pdf

近縁にws2812があるようです

https://cdn-shop.adafruit.com/datasheets/WS2812.pdf

ということでcargo searchですでにモジュールがないか調べてみます。

@studiocase blinky % cargo search sk6812 --limit 40
sk6812_rpi = "0.1.2"                 # SK6812RGBW library for RaspberryPi
ws2812-esp32-rmt-driver = "0.4.0"    # WS2812 driver using ESP32 RMT
@studiocase blinky % cargo search ws2812 --limit 40
ws2812-esp32-rmt-driver = "0.4.0"       # WS2812 driver using ESP32 RMT
ws2812-async = "0.1.0"                  # Async SPI driver for ws2812 leds
ws2812-blocking-spi = "0.2.0"           # Driver based on embedded_hal::blocking::spi::Write for WS2812
ws2812-spi = "0.4.0"                    # SPI-based driver for ws2812 leds
ws2812-timer-delay = "0.3.0"            # Timer-based driver for ws2812 leds
ws2812-nop-samd21 = "0.0.0"             # Nop-based bitbanger for 48MHz SAMD21 devices
ws2812-nop-stm32f0 = "0.0.0"            # Nop-based bitbanger for 48MHz stm32f0 devices
ws2812-uart = "0.1.0"                   # UART-based driver for WS2812 and similar smart LEDs
ws2812-nop-imxrt1062 = "0.2.0"          # Nop-based bitbanger for 600MHz (customizeable) IMXRT1062 devices
ws2812-pio = "0.5.0"                    # Driver implementation for the WS2812 smart LED using the RP2040's PIO peripheral.
ws2812-spi-write-constants = "0.2.0"    # ProcMacros for generating SPI constants for use for WS2812 control.
ws2818-rgb-led-spi-driver = "2.0.0"     # Simple, stripped down, educational, no_std-compatible driver for WS28XX (WS2811/12) RGB…
smart-leds-matrix = "0.1.0"             # DrawTarget implementation for smart led based matrixes. It allows the usage of the embe…
smart-leds = "0.3.0"                    # A crate to use smart-leds device drivers
trenchcoat = "0.5.1"                    # JavaScript-ish virtual machine for embedded, LEDs, hot code reload, etc.
beat-detector = "0.1.2"                 # Audio beat detection library, that supports different audio input devices as source. Yo…
new-home-ws281x = "1.0.4"               # This application will control all kinds of LED strips based on the ws281x chip.
nrf-smartled = "0.5.0"                  # A Smart LED (WS2812) driver using hardware acceleration
pimoroni-plasma-2040 = "0.4.0"          # Board Support Package for the Pimoroni Plasma 2040
ledcat = "0.2.0"                        # Control lots of LED's over lots of protocols
sk6812_rpi = "0.1.2"                    # SK6812RGBW library for RaspberryPi
vcc-gnd-yd-rp2040 = "0.3.0"             # Board Support Package for the VCC-GND Studio YD-RP2040

さて、このうちRaspberry Pi Picoで使えるものはどれでしょうか?とりあえずの正解は`ws2812-pio`です。PIOはPicoで使えるProgrammable Input/Outputのことですね。

この、組み込み用途で使えるライブラリを見つけるのってみなさんどうしてるんでしょう?やっぱりひとつひとつリポジトリのコードにno_stdあるかを手探りしていくしかないのでしょうか???一応lib.rsにNo standard libraryの項目はあるんですけど・・・

それはさておき実装を進めて行きます。まずはドキュメント

ちょこっとサンプルコードらしきものがありますね。crates.ioのほうでこのライブラリを使ったクレートがないかも探してみたらずらっと出てきてrp_picoでも使われているのを発見しました!
そのものズバリのサンプルソースがここに。

ちょっと面倒なのはPin配置です。HexのドキュメントにGPIOとかいろいろ書いてますがこっちのPinは気にしちゃいけません。

Groveコネクタは4pin。でも使うピンは3本のみということです。Groveシールド側のデジタル入出力ブロックのピンアウトを見て、うまく合わせます。例えば下の写真の右端のポートを使うならD21(PicoのGP21)に出力するってことですね。

ということでソースコードを引っこ抜いてきて使えるかどうか試してみたら見事動作しましたよ。

で、このソースコードの見どころです

// Import the `sin` function for a smooth hue animation from the
// Pico rp2040 ROM:
let sin = hal::rom_data::float_funcs::fsin::ptr();

このようにROMからsin関数を引っ張り出しています。便利。PicoはFPU積んでないのでこうやって使うんですね。

// PIOExt for the split() method that is needed to bring
// PIO0 into useable form for Ws2812:
use rp_pico::hal::pio::PIOExt;

// Split the PIO state machine 0 into individual objects, so that
// Ws2812 can use it:
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);

それに`pac.RESETS`を渡してPIO0をsplitするなんてこともできると。splitの定義を見てみると

fn split(
    self,
    resets: &mut pac::RESETS,
) -> (
    PIO<Self>,
    UninitStateMachine<(Self, SM0)>,
    UninitStateMachine<(Self, SM1)>,
    UninitStateMachine<(Self, SM2)>,
    UninitStateMachine<(Self, SM3)>,
) {...}

となっていてステートマシンを返してくれるようです。タプルで省略されているのはsm1, sm2, sm3ってことなんですね。これはPIOを使うときのイディオムとして覚えた方が良さそう。

LEDへの信号出力はws2812のクレートでPIOっぽいところが隠蔽されていて、PIOのアセンブラ部分は見えないようになっています。

let mut ws = Ws2812::new(...);
ws.write(brightness(led, brightness)).unwrap();

ポテンショメータとLEDアレイを組み合わせる(実装夜の部)

次にポテンショメータを使ってみます。使うのはこちらのGroveモジュール。Rust的に夜の部です。まだRustを使いこなしてもいませんがNightlyにチャレンジです。

で、このユニット、ポテンショメータと言いながらLEDアレイもついてるんです。ゲーミングポテンショメータ。ドキュメントはこちら。

これもSK6812というLEDを使っていてさっきの六角形のと一緒です。ちゃちゃっと組み合わせるだけで仕上がってしまいそうですね。なのでもうちょっとチャレンジしてみます。

Cargo workspaceの導入

Cargoにはworkspace機能があって、ワークスペース内ににライブラリクレートやバイナリクレートを作ることができます。この機能を利用して組み込みの開発と同時にデスクトップでもLEDアレイの光り方をシミュレートできるようにしてみたいですね。とはいえ今回はno_stdの関数をいくつかライブラリクレートに移すだけにします。

実際のcargo workspaceを設定していきましょう。適当にdesktop、embedded_targetという名前でバイナリパッケージを、sharedlibという名前でライブラリパッケージをワークスペース内に作成しました。

Cargo Workspacesについては公式ドキュメントに詳しく書かれています。

で、今回のようにコンパイルターゲットが異なる場合にはper-package-targetというcargoのunstableフィーチャーを使います。いざnightlyデビュー!

現状の組み込みRustのサンプルコードなどでは`.cargo/config.toml`の中でbuildのtargetが組み込み対象のターゲットになっていることと思います。例えばpicoであれば

[build]
target = "thumbv6m-none-eabi"

こんな感じになってる部分ですね。これをcargoのper-package-targetを使って組み込みターゲットのワークスペース内のCargo.tomlに移します。

cargo-features = ["per-package-target"]

[package]
name = "embedded_target"
version = "0.1.0"
edition = "2021"
forced-target = "thumbv6m-none-eabi"

Cargo.tomlにこんな感じでcargo-featuresにper-package-targetをセットしてpackageセクションのforce-targetにターゲットトリプルをセットするだけです。Nightlyこわくない。

Cargo workspace per-package-target用のVS Code設定

targetを複数扱う際に面倒なのがVS Code側でも設定を工夫しなければいけない点です。そのままだとrust-analyzerがどのターゲットを基準にしていいのかわからずにlintエラー吐いたりlaunch.jsonの設定をうまく読めなかったりしたので。対処にはVS Codeのmulti root workspace機能を使います。

この、Add Folder to Workspaceというメニューアイテムを使ってcargoのworkspaceを追加していきます。終わったら一旦セーブすると$PROJECT_NAME.code-workspaceというファイルが生成されるのでその中のsetingsプロパティにいらない設定がないか確認しておきましょう。

VS Codeでワークスペースを追加するとそれぞれのワークスペース直下に.vscodeディレクトリを作れるのでそこにsettings.jsonを作成し、rust-analyzerの設定をしておきます。以下は組み込みターゲット用のsettings.jsonです。

{
    "rust-analyzer.cargo.target": "thumbv6m-none-eabi",
    "rust-analyzer.checkOnSave.allTargets": false,
    "editor.formatOnSave": true,
    "cortex-debug.openocdPath": "/usr/local/bin/openocd"
}

launch.jsonやtasks.jsonも設定します。

終わったらcargo build --package バイナリパッケージ名 --verboseでターゲット用のバイナリが生成されているか確認します。

@studiocase m5fader % cargo build --package embedded_target --verbose
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package:   /Users/euskace/rpi/m5fader/desktop/Cargo.toml
workspace: /Users/euskace/rpi/m5fader/Cargo.toml
       Fresh unicode-ident v1.0.5
       Fresh semver-parser v0.7.0
       Fresh version_check v0.9.4
----- snip ------
   Compiling embedded_target v0.1.0 (/Users/euskace/rpi/m5fader/embedded_target)
     Running `rustc --crate-name embedded_target --edition=2021 embedded_target/src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C metadata=ce31c800af460e96 -C extra-filename=-ce31c800af460e96 --out-dir /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps --target thumbv6m-none-eabi -C incremental=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/incremental -L dependency=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps -L dependency=/Users/euskace/rpi/m5fader/target/debug/deps --extern cortex_m=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libcortex_m-9ff2d4b88f0103f2.rlib --extern cortex_m_rt=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libcortex_m_rt-fb9e01d0d0df2ceb.rlib --extern defmt=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libdefmt-34d6edc1b8bec03e.rlib --extern defmt_rtt=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libdefmt_rtt-e5ff8bd0dbddcad9.rlib --extern embedded_hal=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libembedded_hal-8b3346c54fb8e663.rlib --extern panic_halt=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libpanic_halt-d4fd0c9b4e4e057d.rlib --extern panic_probe=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libpanic_probe-10f37ac90cf1c237.rlib --extern rp_pico=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/librp_pico-ca90634f315d2530.rlib --extern sharedlib=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libsharedlib-5f3ff755a27fc8a7.rlib --extern smart_leds=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libsmart_leds-9669de8c8179238c.rlib --extern ws2812_pio=/Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/deps/libws2812_pio-68bef8c37a753731.rlib -L /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/build/embedded_target-0da2df8c73f52ddd/out -L /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/build/cortex-m-0f6b99baea0df933/out -L /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/build/cortex-m-rt-285dde3732cd2ec1/out -L /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/build/defmt-6beff97b231ca50e/out -L /Users/euskace/rpi/m5fader/target/thumbv6m-none-eabi/debug/build/rp2040-pac-ba047f7f47fb6426/out`
    Finished dev [unoptimized + debuginfo] target(s) in 4.55s
@studiocase m5fader % file target/thumbv6m-none-eabi/debug/embedded_target
target/thumbv6m-none-eabi/debug/embedded_target: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

無事生成できていますね。ターゲットのpicoに書き込みができるか、デバッグできるかも試しておきましょう。特にソースを変更していなければすんなりとデバッグできるかなと。現状、VS Codeからデバッグ実行するとPicoへの書き込みができます。しかしcargo runでpicoに書き込む方法がわかりません。Cargo workspace使うと.cargo/config.tomlで指定したrunnerが動いてないような・・・。

で、ここまでのディレクトリ構成です。exa便利!色を扱えるターミナルなら色もついて表示されるはず。

@studiocase m5fader % exa -aT -L3 -I".git*" --git-ignore
.
├── .vscode
│  └── settings.json
├── Cargo.lock
├── Cargo.toml
├── desktop
│  ├── Cargo.toml
│  └── src
│     └── main.rs
├── embedded_target
│  ├── .cargo
│  │  └── config.toml
│  ├── .vscode
│  │  ├── .cortex-debug.peripherals.state.json
│  │  ├── .cortex-debug.registers.state.json
│  │  ├── launch.json
│  │  ├── settings.json
│  │  └── tasks.json
│  ├── build.rs
│  ├── Cargo.toml
│  ├── memory.x
│  └── src
│     └── main.rs
├── m5fader.code-workspace
└── sharedlib
   ├── .vscode
   │  └── settings.json
   ├── Cargo.toml
   └── src
      └── lib.rs

デスクトップアプリケーションの作成

それではデスクトップで確認できるように開発を進めます。イメージとしてはフェーダーユニットを模したもの。LEDが7x2で並んでいて、フェーダーをいじれてアニメーション速度を調整できるようにします。

こんな感じのができると嬉しいです。一応ライブラリを見て回ったもののなかなかドンピシャなのが見つからず、結局ゲームエンジンのbevyを使うことにしました。レンダリングにwgpuというクレートが使われていてかなり筋がいいです。WebGPUやWebGL2のほかにVulkanやMetal,Direct Xでもネイティブ動作します。もちろん今回作るのはそれを活かし切るようなアプリではないですが、やはり将来性がありそうで期待してしまいます。

bevyの最新バージョンは現時点で0.9.1、バージョンアップに伴って結構ガラガラと変わっていて、現行バージョンを使って以前のバージョンのチュートリアルを進めていくとハマります。公式サイトにマイグレーションガイドがあるのでハマったかもと思ったらまず確認しましょう。

デスクトップアプリケーションはdesktopディレクトリ以下で作成していきます。bevyではcargo workspaceを使う場合にプロジェクトルートのCargo.tomlのworkspaceセクションに`resolver="2"`を設定しなければビルドできないようなので設定しておきます。
一応Cargo.tomlのpackageセクションでeditionを2021にしておくとresolverは2として設定されるようなのですが、プロジェクトルートのCargo.tomlにはpackageセクションがないためworkspaceセクションに明示的に設定する必要があるようです。

ということでBevyのガイド本を読んでいきます。本とはいえども分量はないのでさらっと。

実はチートブックっていうのもあってそちらのほうが詳細に書かれています。

さて、BevyではEntity Component Systemというものが採用されており、ECSと称されます。AWSのじゃなくって最近のUnityについてるやつです。systemというループに差し込めるフックみたいな仕組みでコンポーネントごとにエンティティを処理していくみたいな。なんと説明すればいいのでしょう。Unity JapanさんがCEDECで発表していた資料に良いのがあるので紹介しておきます。

ということでゲームにはとても向いてそうな作り方です。今回みたいなどんなライブラリを使っても実現できそうなものに使うものではないかもしれませんが使っちゃいます。

ECSに関してはbevyのecs_guideというサンプルがGUIを使わずコマンドライン完結、かつ詳細なコメント付きなのでわかりやすいです。こちらでECSが何かを把握したら2Dのサンプルを中心に見ていって、あとはマウスのイベントを拾うサンプルを見れば良いくらいでしょうか。

ということで初期化処理から。まずLEDを7列2行に描画します。

動きとしてはLED1つずつが時間経過とともにhueが変わっていくという、先ほど拝借してきたrp_picoのサンプルとほぼ一緒です。フェーダーの値はアニメーションスピードを変えるのに使うことにしました。

bevyは最初こそとっつきにくいものの、ある程度の作法を覚えてしまえば楽に実装できる印象です。main関数は今回は

fn main() {
    App::new()
        .insert_resource(ClearColor(CLEAR))
        .insert_resource(ScheduleRunnerSettings::run_loop(std::time::Duration::from_millis(DELAY as u64)))
        .add_plugin(ScheduleRunnerPlugin::default())
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            window: WindowDescriptor {
                width: 1600.0,
                height: 900.0,
                title: "DesktopSim".to_string(),
                resizable: false,
                ..default()
            },
            ..default()
        }))
        .add_startup_system(setup)
        .add_system(update_color_system)
        .add_system(handle_mouse_events_system)
        .run();
}

こんな感じでそれほど大したことはしてなくて、肝となるのはadd_startup_systemとadd_systemに渡している中身です。startup_systemで画面やComponentの導入と初期化を行い、add_systemでLEDの色を変更したりマウスイベントを拾ってフェーダーを操作できるようといったことをしています。ゲームループでadd_systemに登録した関数が毎回呼ばれるイメージ。

かなり適当な作りですが無事動作させることができました。もちろんこれくらいであればデスクトップで動作させる必要もありません。それでもこれから先にシンセを作るとなった時に少しでも組み込みターゲット側での試行錯誤を減らすため、どうしてもデスクトップで動くようにしたかったので今回このようなものを作ってみた次第です。
できればターゲット側ではノイズ対策などといった詰めの(?)作業に集中できるようになると嬉しいですね。

組み込み用のターゲットにもフェーダーを追加する

さてさて、フェーダーの値をADCで読み込んで組み込みターゲット側でもアニメーションのスピードを変えられるようにしていきましょう。

ピン配置だけまたもちょっと面倒でした。まず今回のFaderユニットのピンアウト。

見方がちょっとややこしいです。G36がフェーダーからの値の入力、G26がLEDへの信号入力、電源、グラウンドという並びになっています。で、G36やG26というのは気にせず、並び順だけ気にかけます。

上の写真がGroveシールドです。右端のA2というブロックからA1とA2というのが見えると思います。これはPicoのGP27とGP28に接続されています。

ということでフェーダー側のピンアウトを参考にするとGP28(A2)をADCの入力に設定し、GP27(A1)はLEDへの出力に使用するということですね。

ADCのRustサンプルはこちらにあります。

難しいことはそれほどなく、サンプル通りにPinを設定してループの中で値を読み込み、フェーダーを操作して最小値と最大値がどのくらいか読み取ります。で、その最大値で値を割ることで0から1の間に値を落ち着かせれば後はなんとでもという感じでした。

終わりに

Rustは覚えることが他の言語に比べてたくさんありますね。それでも慣れてくると出来(設計?)の良いクレートが多いこともあってなかなか使いやすい言語という印象です。今回はベタべたな書き方でしたがもっとロジックをうまく抽象化できるようになって自分も読みやすいコードを書けるようになれたらと思いつつ精進中です。
あと組み込み開発ってデータシートとの戦いだと思いました。もう何度同じドキュメントを読む羽目になったか・・・。今後はDACなども取り混ぜて電圧をブワッと出力してシンセサイザーとして動作するものを作っていきたいです。

ひとまずフェーダーユニットの章で作ったソースをアップしておきます。

https://gitlab.com/euskace/m5fader

まだやり方がわからなくてcargo runでpicoへのダウンロードができないうえにVS Codeだとrust-analyzerが多重起動してめちゃめちゃ重いです。多分親ワークスペースと子ワークスペース3つぶんの計4つが同時起動してます。

とりあえずIssueは立ってるんですけどこれFixされるのかな・・・

などなど解決すべき問題はいろいろありますがとりあえず。自分は結局このプロジェクトではIDEをCLionに戻しました!笑

あと今回RustのNightlyバージョンを使っていて、Cargo Installでコンパイルに失敗するというのも体験しました。堅牢なイメージのあるRustであってもさすがにNightlyだとお茶目な一面が顔を覗かせますね。もちろんバージョンを一つ戻すことで解決できました。

ちなみにGroveモジュール使う以外だとAD9833というProgrammable Waveform Generatorのモジュールを使ってサイン波を出してたりしました。このAD9833もクレートが存在していてあっという間にプログラムができてびっくりしました。Picoを購入するときに一緒に買ったオシロスコープが大活躍。

ということで。テストが書けていないなどの課題についてはまた次回。

この記事が参加している募集

やってみた