Discovery
Rustでマイクロコントローラの世界を楽しもう!
この本はC/C++ではなく、Rustを使ったマイクロコントローラの組込みシステム入門コースです。
スコープ
以下のトピックを取り上げます(ゆくゆくは、そうしたいです)
- 「組込み」(Rust)の書き方、ビルド方法、フラッシュへの書き込み方法、デバッグ方法。
- マイクロコントローラで一般的な機能(「ペリフェラル」)。デジタル入出力、パルス幅変調(PWM)、アナログデジタル変換(ADC)、シリアル、I2C、SPIのような一般的な通信プロトコル、など。
- マルチタスクの考え方。協調的マルチタスク vs プリエンプティブマルチタスク、割り込み、スケジューラなど。
- 制御システムの概念。センサ、キャリブレーション、デジタルフィルタ、アクチュエータ、開ループ制御、閉ループ制御、など。
進め方
- 初心者に優しく。マイクロコントローラや組込みシステムの開発経験は必要ありません。
- ハンズオン形式で。理論を実践するためにたくさんの演習をします。あなたは多くの演習に取り組むでしょう。
- ツール中心に。開発を容易にするツールをたくさん使用します。GDBを使った「実際の」デバッグとログ出力を早い段階で導入します。デバッグ機能としてLEDを使用するようなことは、ここではやりません。
目標としないこと
この本でスコープ外とすることは、以下の通りです。
- Rustを教えること。このトピックについては、既に多くの教材があります。マイクロコントローラと組込みシステムに集中します。
- 電気回路または電子機器の理論についての包括的なテキストであること。いくつかのデバイスがどのように動くか、を理解するための最低限の情報を提供します。
- リンカスクリプトやブートプロセスといったRustの低レベルな詳細を説明すること。例えば、ボードにプログラムを書き込むために既存のツールを使いますが、それらのツールがどのように動くか、の詳細には踏み込みません。
また、この教材を他の開発ボードに移植するつもりもありません。この本は、micro:bit開発ボード専用のものです。
問題の報告
この本のソースコードはこのレポジトリにあります。誤植やコードに問題を発見した場合は、issueトラッカーに報告して下さい。
訳注:和訳への問題報告は、下記にお願いいたします。
和訳のソースは和訳レポジトリにあります。問題を発見した場合は、和訳issueに報告して下さい。
他の組込みRustの資料
このDiscovery本は、組込みワーキンググループが提供する組込みRust資料の1つに過ぎません。 組込みRustの本棚に、数多くの資料があります。そこには、よくある質問と回答のリストも有ります。
背景
マイクロコントローラとは?
マイクロコントローラは、1チップ上のシステムです。 あなたのコンピュータがプロセッサ、RAM、ストレージ、イーサーネットポートなど、いくつかの個別の部品で構成されている一方、マイクロコントローラはそれらの構成部品を1つの「チップ」またはパッケージに組み込んでいます。 このことにより、より少ない部品数でシステムを構築できます。
マイクロコントローラでできることは?
多くのことができます! マイクロコントローラは「組込みシステム」として知られているものの中心になる部品です。 組込みシステムはどこにでもありますが、通常それらに気づくことはありません。 組込みシステムは、衣服を洗濯したり、書類を印刷したり、食べ物を調理します。 組込みシステムは、生活し働く建物を快適な温度に保ち、自動車を走らせる部品を制御します。
ほとんどの組込みシステムはユーザーの介入なしに動作します。 洗濯機のようにユーザーインタフェースがある場合でさえ、ほとんどの動作は組込みシステムだけで完結します。
組込みシステムは物理的な処理を制御するのに、よく使われます。 そのため、組込みシステムは世界の状態を知るためのデバイス(「センサ」)と物を動かすためのデバイス(「アクチュエータ」)を持ちます。 例えば、建物の気候を制御するシステムは次のデバイスを持つでしょう。
- 複数の場所で温度と湿度を計測するためのセンサ
- ファンの速度を制御するアクチュエータ
- 建物の熱を取り込んだり排出するアクチュエータ
いつマイクロコントローラを使うべきなのでしょうか?
上述したほとんどの組込みシステムはLinuxが動いているコンピュータ(例えば「Raspberry Pi」)で実装することができます。 代わりにマイクロコントローラを使うのはなぜでしょうか? プログラムの開発がより大変に思えます。
いくつかの理由があります。
コスト。マイクロコントローラは汎用コンピュータよりずっと安価です。 マイクロコントローラは安価なだけでなく、動作に必要となる外部電気部品がずっと少ないです。 そのため、プリント基板(PCB)を小さく安価に、設計し製造できます。
消費電力。ほとんどのマイクロコントローラは本格的なプロセッサと比べるとほんの少しの電力しか使いません。 バッテリで動作するアプリケーションにとって、これは大きな違いです。
応答性。組込みシステムの中には、その目的を果たすため、常に限られた時間間隔で応答しなければならないものもあります(例えば車の「アンチロック」ブレーキシステムです)。 もしシステムがこのデッドラインに間に合わないと、悲惨な結末を迎えるでしょう。 このようなデッドラインは「ハードリアルタイム」要求と呼ばれています。 このようなデッドラインの制約がある組込みシステムは「ハードリアルタイムシステム」と呼ばれます。 汎用コンピュータと汎用OSは通常、多くのソフトウェアコンポーネントでコンピュータの処理資源を共有します。 そのため、厳密な時間制約内でのプログラム実行を保証するのが難しいです。
信頼性。より部品が少ないシステム(ハードウェアとソフトウェアの両方)では、間違いが起こりにくくなります!
マイクロコントローラを使うべきでない時はいつでしょうか?
計算量が膨大な場合です。 消費電力を低く抑えるため、マイクロコントローラは非常に限られた計算資源しか持ちません。 例えば、浮動小数点演算を提供するハードウェアすらないマイクロコントローラもあります。 そのようなデバイスでは単精度浮動小数点数の単純な加算ですら、数百CPUサイクルかかります。
CではなくRustを使う理由はなんでしょうか?
あなたが既にRustとCとの違いを知っており、ここで説得する必要がないことを願っています。 あえて1つ強調すると、それはパッケージ管理システムです。 RustにはCargoがある一方、C言語は公式の広く普及しているパッケージ管理システムがありません。 パッケージ管理システムがあることは開発を非常に楽にします。 私の意見としては、パッケージ管理が簡単であることは、コードの再利用を促進します。 なぜなら、ライブラリがアプリケーションに容易に結合できるからです。 このことは、ライブラリがより「実戦で使われる」ことにも良い影響があります。
Rustを使うべきでない理由は何でしょうか?
もしくは、RustよりCを選ぶ理由はなんでしょうか?
C言語のエコシステムはより成熟しています。 多くの問題に対して、既に解決策が存在しています。 時間制約のあるプロセスを制御する必要がある場合、既存の商用リアルタイムOS(RTOS)を選び、問題を解決することができます。 Rustにはまだ、商用で製品レベルのRTOSがないため、自分自身で作るか、開発中のものを試す必要があります。 Awesome Embedded Rustリポジトリにはそれらのリストがあります。
必要な知識とハードウェア環境
この本を読むために必要な知識は、 ある程度のRustに関する知識です。
ある程度のが具体的にどの程度かと聞かれれば難しいところですね。
ジェネリクスを完全に理解している必要はないにしろ、クロージャをどうやって使うかは知っているべきといったところでしょうか。
2018 editionのイディオムにも慣れていた方がよいでしょう。
特に、2018 editionではextern crate
を使わなくてもいいということは理解しておいてほしいです。
それと、この本の内容を実践するにあたり、以下のハードウェアが必要です。
- micro:bit v2ボードが一つ。代わりにmicro:bit v1.5ボードでも大丈夫です。この本の中では、v1.5をv1と表記します。
(ここやここなど、いくつかの電子部品販売店から購入可能です。)
注意 写真はmicro:bit v2のものです。v1の前面はちょっと違う見た目をしています。
- micro-BのUSBケーブルが一本、micro:bitを動かすために必要です。充電しかできないものもあるので、データ転送に対応したケーブルであることを確認してください。
注意 ケーブルはmicro:bitのキットに同梱されている場合もあります。 モバイル機器の充電に使っているようなmicro-Bのケーブルでも、実はデータ転送に対応していて使うことができるという場合もあります。
FAQ: ちょっと待ってください。なぜこの特定のハードウェアが必要なのでしょうか?
その方が私にとっても、あなたにとっても非常に楽だからです。
ハードウェアの違いを気にしなくていいのであれば、この本はとても、とても取り組みやすくなります。 間違いなく、です。
FAQ: 別の開発ボードを使ってこの本の内容に取り組んでも問題ないでしょうか?
おそらく問題ないはず?です。
あなたがマイクロコントローラをこれまでにどれだけ触ったことがあるか、あるいは(および)、あなたが使おうとしている開発ボードに、nrf52-hal
のような高レベルなクレートがすでにあるか次第だと言えるかもしれません。
もし違うマイクロコントローラを使おうとしているのであれば、 Awesome Embedded Rust HAL listでそのようなクレートを探してみるのもいいでしょう。
違う開発ボードを使う場合、この本の持つ、初心者にとっての取り組みやすさが損なわれてしまう、と私は思います
もし違う開発ボードを使おうとしていて、かつ全くの初心者ではないという自信があるのであれば、quickstartプロジェクトのテンプレートに従って始めるほうがいいでしょう。
開発環境のセットアップ
マイクロコントローラを扱うためには、いくつかのツールを導入せねばなりません。 なぜなら、あなたが今使っているそのコンピュータとは異なったアーキテクチャを相手にすることになるからです。 また、プログラムの実行およびデバッグを、「リモート」のデバイス上でおこなう必要もあります。
ドキュメント
もっとも、ツールだけでは不十分です。 ドキュメントなくして、マイクロコントローラを触ることは到底できないなのです。
この本では、全体を通して以下のドキュメントを参照します。
ツール
以下に挙げるツールすべてを使います。 最小バージョンを記載していないものに関しては、最近リリースされたバージョンであれば正常に動くはずです。 ですがここでは、動作確認をしたバージョンを併せて記載しました。
- Rust toolchain 1.53.0以上
gdb-multiarch
バージョン10.2で動作確認済み。 他のバージョンでも同様に正しく動作すると思います。 お使いのOSディストリビューションやプラットフォームに対応したgdb-multiarch
がない場合は、arm-none-eabi-gdb
で代用が可能です。 さらに、通常のgdb
バイナリでも、マルチアーキテクチャに対応しているものがあります。 このことについては、サブチャプターにも詳しく記載しています。
cargo-binutils
バージョン0.3.3以上。
cargo-embed
バージョン0.11.0以上。
- LinuxとmacOSの場合:
minicom
バージョン2.7.1.で動作確認済みですが、他のバージョンでも同様に正しく動作すると思います。
- Windowsの場合:
PuTTY
続いて、OSに共通である以下の手順に従って、いくつかのツールを導入してください。
rustc
& Cargo
https://rustup.rsに従って、rustupをインストールしてください。
もしすでにrustupがインストール済みの場合でも、stableであることと、ツールチェーンが最新版であることを、念のため確認してください。
rustc -V
で得られる実行結果の日付が、以下に示すもの以降になるようにしてください。
$ rustc -V
rustc 1.53.0 (53cb7b09b 2021-06-17)
cargo-binutils
$ rustup component add llvm-tools-preview
$ cargo install cargo-binutils --vers 0.3.3
$ cargo size --version
cargo-size 0.3.3
cargo-embed
$ cargo install cargo-embed --vers 0.11.0
$ cargo embed --version
cargo-embed 0.11.0
git commit: crates.io
この本のリポジトリ
この本では、ちょっとしたRustのコードベースがいろいろなチャプターで使われています。 このため、以下のいずれかの手順に従い、そのソースコードをダウンロードする必要があります。
- この本のリポジトリにアクセスし、緑色の「Code」ボタン、その後「Download Zip」ボタンを続けてクリックします。
訳注:この本の日本語版リポジトリはこちらです
- 上記手順と同じリンク先のリポジトリから、gitでcloneします。(もしあなたがgitをご存じであれば、もうすでにインストール済みであることでしょう。)
OSごとの手順
続いて、お使いのOSに対応した手順に従ってください。
Linux
Here are the installation commands for a few Linux distributions.
Ubuntu 20.04 or newer / Debian 10 or newer
NOTE
gdb-multiarch
is the GDB command you'll use to debug your ARM Cortex-M programs
$ sudo apt-get install \
gdb-multiarch \
minicom
Fedora 32 or newer
NOTE
gdb
is the GDB command you'll use to debug your ARM Cortex-M programs
$ sudo dnf install \
gdb \
minicom
Arch Linux
NOTE
arm-none-eabi-gdb
is the GDB command you'll use to debug your ARM Cortex-M programs
$ sudo pacman -S \
arm-none-eabi-gdb \
minicom
Other distros
NOTE
arm-none-eabi-gdb
is the GDB command you'll use to debug your ARM Cortex-M programs
For distros that don't have packages for ARM's pre-built
toolchain,
download the "Linux 64-bit" file and put its bin
directory on your path.
Here's one way to do it:
$ mkdir -p ~/local && cd ~/local
$ tar xjf /path/to/downloaded/file/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2
Then, use your editor of choice to append to your PATH
in the appropriate
shell init file (e.g. ~/.zshrc
or ~/.bashrc
):
PATH=$PATH:$HOME/local/gcc-arm-none-eabi-9-2020-q2-update/bin
udev rules
These rules let you use USB devices like the micro:bit without root privilege, i.e. sudo
.
Create this file in /etc/udev/rules.d
with the content shown below.
$ cat /etc/udev/rules.d/99-microbit.rules
# CMSIS-DAP for microbit
SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", ATTR{idProduct}=="0204", MODE:="666"
Then reload the udev rules with:
$ sudo udevadm control --reload-rules
If you had any board plugged to your computer, unplug them and then plug them in again.
Now, go to the next section.
Windows
arm-none-eabi-gdb
ARMによって、Windows向けの.exe
形式のインストーラが提供されています。
ここからダウンロードし、説明に従ってインストールをしてください。
インストール完了直前に表示される「Add path to environment variable」項目には、チェックを入れるようにしてください。
ここまで終わったら、ツールのインストール先ディレクトリが、%PATH%
環境変数に登録されていることを確認してください。
$ arm-none-eabi-gcc -v
(..)
gcc version 5.4.1 20160919 (release) (..)
PuTTY
最新版のputty.exe
をこのサイトからダウンロードして、%PATH%
環境変数に登録されるディレクトリのどこかに配置してください。
訳注:WindowsだとTera Termも使えます。(ただし、本書での詳細な手順の解説は省略します。)
macOS
All the tools can be installed using Homebrew:
$ # Arm GCC toolchain
$ brew tap ArmMbed/homebrew-formulae
$ brew install arm-none-eabi-gcc
$ # Minicom
$ brew install minicom
That's all! Go to the next section.
Verify the installation
Let's verify that all the tools were installed correctly.
Linux only
Verify permissions
Connect the micro:bit to your computer using a USB cable.
The micro:bit should now appear as a USB device (file) in /dev/bus/usb
. Let's find out how it got
enumerated:
$ lsusb | grep -i "NXP ARM mbed"
Bus 001 Device 065: ID 0d28:0204 NXP ARM mbed
$ # ^^^ ^^^
In my case, the micro:bit got connected to the bus #1 and got enumerated as the device #65. This means the
file /dev/bus/usb/001/065
is the micro:bit. Let's check its permissions:
$ ls -l /dev/bus/usb/001/065
crw-rw-rw-. 1 root root 189, 64 Sep 5 14:27 /dev/bus/usb/001/065
The permissions should be crw-rw-rw-
. If it's not ... then check your udev
rules and try re-loading them with:
$ sudo udevadm control --reload-rules
All
Verifying cargo-embed
First, connect the micro:bit to your Computer using a USB cable.
At least an orange LED right next to the USB port of the micro:bit should light up. Furthermore, if you have never flashed another program on to your micro:bit, the default program the micro:bit ships with should start blinking the red LEDs on its back, you can ignore them.
Next up you will have to modify Embed.toml
in the src/03-setup
directory of the
book's source code. In the default.general
section you will find two commented out
chip variants:
[default.general]
# chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1
If you are working with the micro:bit v2 board uncomment the first, for the v1 uncomment the second line.
Next run one of these commands:
$ # make sure you are in src/03-setup of the books source code
$ # If you are working with micro:bit v2
$ rustup target add thumbv7em-none-eabihf
$ cargo embed --target thumbv7em-none-eabihf
$ # If you are working with micro:bit v1
$ rustup target add thumbv6m-none-eabi
$ cargo embed --target thumbv6m-none-eabi
If everything works correctly cargo-embed should first compile the small example program in this directory, then flash it and finally open a nice text based user interface that prints Hello World.
(If it does not, check out general troubleshooting instructions.)
This output is coming from the small Rust program you just flashed on to your micro:bit. Everything is working properly and you can continue with the next chapters!
ハードウェアとの出会い
これから使用するハードウェアについて知りましょう。
micro:bit
ボードには多くの部品が搭載されています。
- マイクロコントローラが1つ
- LEDがいくつか。特に裏面にある5x5のLED
- ユーザーボタンが2つとUSBポート横のリセットボタンが1つ
- USBポートが1つ
- 地磁気センサと加速度センサ両方を搭載しているセンサが1つ
訳注: 日本語Wikipediaへのリンク マイクロコントローラ 加速度計 磁気センサ
部品の中で最も重要なものはマイクロコントローラです。マイクロコントローラは「microcontoller unit」を省略して「MCU」と呼ばれることもあります。 ボード上では、USBポートがある面に見える2つの黒い四角の大きい方です。 このMCUはあなたのコードを実行します。 「ボードにプログラムを書く」という文章を目にするかもしれませんが、実際はボードに搭載されているMCUにプログラムを書きます。
このボードのより詳細な説明に興味がある場合、micro:bitのウェブサイトを訪れて見てください。
MCUは重要なので、ボードに搭載されているMCUをより詳しく見てみましょう。 次のセクションはmicro:bit v2を使うかv1を使うかで、どちらか一方を読んでください。
Nordic nRF52833 (「nRF52」, micro:bit v2)
このMCUの下には73個の小さな金属ピンがあります(そのため、aQFN73と呼ばれます)。 これらのピンはトレースと接続しています。トレースとはボード上の部品をつなぐ配線として機能する小さな「道」です。 MCUはピンの電気的特性を動的に変えることができます。 これは照明のスイッチのようなもので、回路上の電流の流れを変えます。 特定のピンに電流を流したり、電流を止めたりすることで、トレース経由でピンと接続しているLEDを点灯したり、消灯したりできます。
各製造メーカーごとに異なる部品番号の付け方をしていますが、多くの場合、部品番号を見るだけで、その部品についての情報を得られます。
今回使用するMCUの部品番号(N52833 QIAAA0 2024AL
)を見てみましょう(肉眼では見えないかもしれませんが、チップに記載されています)。
最初のn
は部品がNordic Semiconductorによって製造された、というヒントになっています。
ウェブサイトで部品番号を調べると、すぐにNordic Semiconductorの製品ページが見つかります。
ここで、このチップの売り込みポイントが「Bluetooth Low Energy and 2.4GHz SoC」であることがわかります(SoCは「System on a Chip」の略です)。
製品名に含まれる「RF」はradio frequency(無線周波数)の省略です。
もし、製品ページからリンクされているチップのドキュメントを少し検索してみると、製品仕様が見つかります。
その10章「Ordering Information(注文情報)」に奇妙なチップの命名規則について説明があり、次のことがわかります。
N52
はMCUシリーズで、他のnRF52
MCUがあることを意味している833
は部品コードQI
はパッケージコードでaQFN73
の略AA
は種別コードで、MCUがもつRAMとフラッシュの容量を示す。今回の場合、512キロバイトのフラッシュと128キロバイトのRAMA0
はビルドコードで、A
はハードウェアバージョン、0
は製品コンフィグレーションを意味する2024AL
はトラッキングコードなので、あなたのチップとは異なるでしょう
もちろん製品仕様には、このチップに関して役立つ情報がたくさん載っています。 例えば、このチップはARM® Cortex™-M4 32ビットプロセッサをベースとしていること、などです。
Arm? Cortex-M4?
使用するチップがNordicによって製造されているとすると、Armとは誰なのでしょう? チップがnRF52833とすると、Cortex-M4とは何なのでしょうか?
「Armベース」のチップは非常に広く使われているので、「Arm」のトレードマークを持つ会社(Armホールディングス)が実際のチップを製造していない、と聞くと驚くかもしれません。 Armの主要なビジネスモデルはチップの一部を設計することなのです。 彼らは、それらの設計を製造メーカーにライセンスします。 製造メーカーは(おそらく独自の変更を加えて)そのデザインを物理的なハードウェアとして実装し、販売できるようにします。 チップの設計と製造とを両方行うインテルのような会社と、Armの戦略とは違っています。
Armはいくつかの異なる設計をライセンスしています。 「Cortex-M」はマクロコントローラのコアとしてよく使われる設計です。 例えば、Cortex-M4(今回使用するチップのベースとなっているコア)は低コストかつ低電力な用途向けに設計されています。 Cortex-M7はより高コストですが、より多くの機能と高い性能を有しています。
幸いなことに、この本を読むために様々なプロセッサやCortexの設計について詳しく知る必要はありません。 しかし、使用するデバイスの専門用語について少し知識が身についたかと思います。 nRF52833はCortex-Mをベースとした設計なので、nRF52833を使って作業を進めると、Cortex-Mベースのチップのドキュメントを読んだり、ツールを使ったりすることがわかるでしょう。
Nordic nRF51822 (「nRF51」、micro:bit v1)
このMCUの下には48個の小さな金属ピンがあります(そのため、QFN48と呼ばれます)。 これらのピンはトレースと接続しています。トレースとはボード上の部品をつなぐ配線として機能する小さな「道」です。 MCUはピンの電気的特性を動的に変えることができます。 これは照明のスイッチのようなもので、回路上の電流の流れを変えます。 特定のピンに電流を流したり、電流を止めたりすることで、トレース経由でピンと接続しているLEDを点灯したり、消灯したりできます。
各製造メーカーごとに異なる部品番号の付け方をしていますが、多くの場合、部品番号を見るだけで、その部品についての情報を得られます。
今回使用するMCUの部品番号(N51822 QFAAH3 1951LN
)を見てみましょう(肉眼では見えないかもしれませんが、チップに記載されています)。
最初のn
は部品がNordic Semiconductorによって製造された、というヒントになっています。
ウェブサイトで部品番号を調べると、すぐにNordic Semiconductorの製品ページが見つかります。
ここで、このチップの売り込みポイントが「Bluetooth Low Energy and 2.4GHz SoC」であることがわかります(SoCは「System on a Chip」の略です)。
製品名に含まれる「RF」はradio frequency(無線周波数)の省略です。
もし、製品ページからリンクされているチップのドキュメントを少し検索してみると、製品仕様が見つかります。
その10章「Ordering Information(注文情報)」に奇妙なチップの命名規則について説明があり、次のことがわかります。
N51
はMCUシリーズで、他のnRF51
MCUがあることを意味している822
は部品コードQF
はパッケージコードでQFN48
の略AA
は種別コードで、MCUがもつRAMとフラッシュの容量を示す。今回の場合、256キロバイトのフラッシュと16キロバイトのRAMH3
はビルドコードで、H
はハードウェアバージョン、3
は製品コンフィグレーションを意味する1951LN
はトラッキングコードなので、あなたのチップとは異なるでしょう
もちろん製品仕様には、このチップに関して役立つ情報がたくさん載っています。 例えば、このチップはARM® Cortex™-M0 32ビットプロセッサをベースとしていること、などです。
Arm? Cortex-M0?
使用するチップがNordicによって製造されているとすると、Armとは誰なのでしょう? チップがnRF51822とすると、Cortex-M0とは何なのでしょうか?
「Armベース」のチップは非常に広く使われているので、「Arm」のトレードマークを持つ会社(Armホールディングス)が実際のチップを製造していない、と聞くと驚くかもしれません。 Armの主要なビジネスモデルはチップの一部を設計することなのです。 彼らは、それらの設計を製造メーカーにライセンスします。 製造メーカーは(おそらく独自の変更を加えて)そのデザインを物理的なハードウェアとして実装し、販売できるようにします。 チップの設計と製造とを両方行うインテルのような会社と、Armの戦略とは違っています。
Armはいくつかの異なる設計をライセンスしています。 「Cortex-M」はマクロコントローラのコアとしてよく使われる設計です。 例えば、Cortex-M0(今回使用するチップのベースとなっているコア)は低コストかつ低電力な用途向けに設計されています。 Cortex-M7はより高コストですが、より多くの機能と高い性能を有しています。
幸いなことに、この本を読むために様々なプロセッサやCortexの設計について詳しく知る必要はありません。 しかし、使用するデバイスの専門用語について少し知識が身についたかと思います。 nRF51822はCortex-Mをベースとした設計なので、nRF51822を使って作業を進めると、Cortex-Mベースのチップのドキュメントを読んだり、ツールを使ったりすることがわかるでしょう。
組込みRustの専門用語
micro:bitでのプログラミングを始める前に、今後の章で重要となるライブラリと専門用語とを一通り確認しておきましょう。
抽象化レイヤー
フルサポートされているマイクロコントローラーあるいはボードに対して、その抽象化レベルに応じて、次の用語を目にするでしょう。
ペリフェラルアクセスクレート (PAC; Peripheral Access Crate)
PACはチップ上のペリフェラルへの安全(と思われる)なインタフェースを提供し、あらゆるビットを好きなように設定できます(もちろん間違った使い方もできます)。 通常、より上位レイヤーでやりたいことができない場合か上位レイヤー自体を開発する時のみ、PACを扱います。 この本で(ひそかに)使うPACはnRF52向けのものかnRF51向けのものです。
ハードウェア抽象化レイヤー (HAL; The Hardware Abstraction Layer)
HALはチップのPACの上位レイヤーを作り上げ、このチップ特有の振る舞いをあまり知らない人にも便利な抽象を提供します。 通常、HALは全てのペリフェラルをそれぞれ1つの構造体で抽象化します。 例えば、ペリフェラル経由でデータを送信するために使える構造体があります。 この本ではnRF52-halもしくはnRF51-halをそれぞれ使います。
ボードサポートクレート (ボードサポートパッケージもしくはBSPと呼ばれます)
BSPは(micro:bitのような)ボード全体を抽象化します。 BSPはマイクロコントローラだけでなくセンサーやLEDなど、ボード上に存在するものを使えるように抽象を提供します。 頻繁に(特にカスタムされたボードでは)そのチップのHALを使ってセンサーのドライバを自分で作成するか、crates.ioを探すことになります。 幸い、micro:bitにはBSPがあるため、HALの上に作られたBSPを使います。
レイヤーを統合する
次に、組込みRustの世界でまさに中心の役割を担うembedded-hal
について見ていきます。
名前の通り、embedded-hal
は2レベル目の抽象化であるHALに関係しています。
embedded-hal
の根底にあるアイデアは、HALに含まれる特定ペリフェラル実装で共通となる振る舞いを定義したトレイト一式を提供することです。
例えば、ボード上のLED追加をオン、オフするために、あるピンの電源をオン、オフできる機能は必ずあるはずです。
共通となる振る舞いをトレイトとして定義することで、embedded-hal
トレイトの実装が存在するチップであれば、embedded-hal
のトレイトにのみ依存するようにドライバを書くだけで、どのチップでも使えるドライバ(例えば温度センサのドライバ)を書くことができます。
このように書かれたドライバをプラットフォーム非依存 (platform agnostic) と呼びます。
嬉しいことに、crates.ioにある多くのドライバが実際にプラットフォーム非依存です。
さらに詳しく
もし紹介した抽象レベルについてより詳しく知りたい場合、Franz Skarman (TheZoq2) がOxidize 2020のAn Overview of the Embedded Rust Ecosystemでこのトピックについて話しています。
LEDルーレット
それでは、次のアプリケーションを作ることから始めましょう。
このアプリを実装するために高機能なAPIを提供しますが、低レベルなことは後々やっていきますのでご心配なく。 この章の主な目的はフラッシュへの書き込みとデバッグに慣れることです。
ではこの本のレポジトリのsrc
ディレクトリにあるコードから始めましょう。
ディレクトリ内に、各章の名前を冠したディレクトリがあります。
それらのディレクトリのほとんどはCargoのスタータープロジェクトです。
早速、src/05-led-roulette
ディレクトリの内容に入っていきましょう。
src/main.rs
ファイルを開いてみてください。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_halt as _; use microbit as _; #[entry] fn main() -> ! { let _y; let x = 42; _y = x; // infinite loop; just so we don't leave this stack frame loop {} }
マイクロコントローラのプログラムは通常のプログラムと比べて2つの点で異なります。
#![no_std]
と#![no_main]
です。
no_std
アトリビュートはこのプログラムがOSの存在を前提としたstd
クレートを使わず、代わりにstd
のサブセットでベアメタルシステム(つまり、ファイルシステムはソケットのようなOSの抽象化がないシステム)でも実行できるcore
クレートを使うことを意味します。
no_main
アトリビュートはこのプログラムが標準のmain
インターフェースを使わないことを意味しています。
標準のmain
インターフェースは引数を受け取るコマンドラインアプリケーション向けに作られています。
エントリーポイントを定義するために、標準のmain
の代わりにcortex-m-rt
クレートからentry
アトリビュートを使います。
このプログラムではエントリーポイントの名前を「main」としますが、それ以外の名前も使えます。
エントリーポイントになる関数はfn() -> !
のシグネチャを持たなければなりません。
このシグネチャは関数が戻れないことを表しており、このプログラムが決して止まらないことを意味します。
ディレクトリ内をよく見ると、Cargoプロジェクトに.cargo
ディレクトリがあることに気がつくと思います。
.cargo
ディレクトリにはCargoの設定ファイル(.cargo/config
)が含まれています。
このファイルはリンク処理を微調整し、ターゲットデバイス用にプログラムのメモリレイアウトを作成します。
このリンク処理の微調整は、cortex-m-rt
クレートを使う要件になっています。
さらに、ディレクトリにはEmbed.toml
ファイルがあります。
[default.general]
# chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1
[default.reset]
halt_afterwards = true
[default.rtt]
enabled = false
[default.gdb]
enabled = true
Embed.toml
はcargo-embed
に次の情報を与えます。
- nrf52833もしくはnrf51822のいずれかを使っていること。第3章でやったのと同じように、使用するチップのコメントアウトを外してください。
- チップのフラッシュに書き込んだあとプログラムの実行を停止したいこと。これはプログラムがすぐにループに到達しないようにするためです。
- RTTを無効化したいこと。RTTはチップがデバッガにテキストを送るためのプロトコルです。実は第3章で「Hello World」を送信するのに使ったプロトコルがRTTだったのです。
- GDBを有効化したいこと。これはデバッグするのに必要です。
それでは、プログラムをビルドするところから始めましょう。
ビルドする
最初のステップは「バイナリ」クレートをビルドすることです。
マイクロコントローラはあなたのコンピュータとはアーキテクチャが異なるため、クロスコンパイルが必要です。
Rustでのクロスコンパイルは単に--target
フラグをrustc
またはCargoに渡すだけです。
難しいところはフラグの引数として渡すターゲット名に目星をつけることです。
micro:bit v2上のマイクロコントローラはCortex-M4Fプロセッサを、v1はCortex-M0プロセッサを搭載していることがわかっています。
rustc
はCortex-Mアーキテクチャへのクロスコンパイル方法を知っています。
いくつかの異なるターゲットを提供しており、Cortex-Mのプロセッサファミリーをカバーしています。
thumbv6m-none-eabi
, Cortex-M0プロセッサとCortex-M1プロセッサ向けthumbv7m-none-eabi
, Cortex-M3プロセッサ向けthumbv7em-none-eabi
, Cortex-M4プロセッサとCortex-M7プロセッサ向けthumbv7em-none-eabihf
, Cortex-M4FプロセッサとCortex-M7Fプロセッサ向けthumbv8m.main-none-eabi
, Cortex-M33プロセッサとCortex-M35Pプロセッサ向けthumbv8m.main-none-eabihf
, Cortex-M33FプロセッサとCortex-M35PFプロセッサ向け
micro:bit2にはthumbv7em-none-eabihf
ターゲットを、v1にはthumbv6m-none-eabi
を使います。
クロスコンパイルをする前に、ターゲット向けに事前コンパイルされた標準ライブラリをダウンロードします(実際は縮小版ですが)。
これはrustup
を使ってやります。
# For micro:bit v2
$ rustup target add thumbv7em-none-eabihf
# For micro:bit v1
$ rustup target add thumbv6m-none-eabi
このステップは一度だければ良いです。ツールチェインをアップデートすると、rustup
は新しい標準ライブラリ(rust-std
)を再インストールします。
そのため、セットアップの検証で既にターゲットを追加していれば、このステップを省略できます。
rust-std
をインストールするとCargoを使ってプログラムをクロスコンパイルすることができます。
# make sure you are in the `src/05-led-roulette` directory
# For micro:bit v2
$ cargo build --features v2 --target thumbv7em-none-eabihf
Compiling semver-parser v0.7.0
Compiling typenum v1.12.0
Compiling cortex-m v0.6.3
(...)
Compiling microbit-v2 v0.10.1
Finished dev [unoptimized + debuginfo] target(s) in 33.67s
# For micro:bit v1
$ cargo build --features v1 --target thumbv6m-none-eabi
Compiling fixed v1.2.0
Compiling syn v1.0.39
Compiling cortex-m v0.6.3
(...)
Compiling microbit v0.10.1
Finished dev [unoptimized + debuginfo] target(s) in 22.73s
注意 このクレートは必ず最適化なしでコンパイルしてください。 提供しているCargo.tomlファイルとビルドコマンドは、最適化がオフになっていることを確認してください。
これで、実行ファイルが作成できました。 この実行ファイルはLEDを点滅しません。この章で後で作る物を単純化したものです。 動作確認として、作成した実行ファイルが本当にARMのバイナリかどうか確かめてみましょう。
# For micro:bit v2
# equivalent to `readelf -h target/thumbv7em-none-eabihf/debug/led-roulette`
$ cargo readobj --features v2 --target thumbv7em-none-eabihf --bin led-roulette -- --file-headers
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x117
Start of program headers: 52 (bytes into file)
Start of section headers: 793112 (bytes into file)
Flags: 0x5000400
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 4
Size of section headers: 40 (bytes)
Number of section headers: 21
Section header string table index: 19
# For micro:bit v1
# equivalent to `readelf -h target/thumbv6m-none-eabi/debug/led-roulette`
$ cargo readobj --features v1 --target thumbv6m-none-eabi --bin led-roulette -- --file-headers
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0xC1
Start of program headers: 52 (bytes into file)
Start of section headers: 693196 (bytes into file)
Flags: 0x5000200
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 4
Size of section headers: 40 (bytes)
Number of section headers: 22
Section header string table index: 20
次にこのプログラムをマイクロコントローラのフラッシュに書き込みます。
フラッシュに書き込む
フラッシュに書き込むということは、マイクロコントローラの(永続)メモリにプログラムを移動するプロセスです。 一度フラッシュに書き込むと、電源がオンになるたびにマイクロコントローラはフラッシュに書かれたプログラムを実行します。
今回の場合、led-roulette
はマイクロコントローラのメモリに存在する唯一のプログラムです。
これはこのマイクロコントローラ上で他に何も動いていないことを意味しています。
OSも「デーモン」も動いていません。
led-roulette
はデバイスの全ての制御を握っています。
cargo embed
のおかげでバイナリをフラッシュに書き込むことは極めて簡単です。
コマンドを実行する前に、このコマンドが何をするのか見てみましょう。 micro:bitのUSBコネクタがある側を見てみると、2つの黒い四角があることに気づくでしょう(micro:bit2では3つ目の一番大きなスピーカーもあります)。 1つは既に話したとおりMCUです。 それではもう1つは何をするものなのでしょうか? このもう1つのチップは主に3つの役割を果たします。
- MCUにUSBコネクタからの電源を供給する
- MCUのシリアルとUSBのブリッジ機能を提供する(後の章で説明します)
- プログラマー/デバッガーになる(これが今のお目当てです)
基本的にこのチップは私たちのコンピュータ(USBで接続されている)とMCU(トレースで接続されていてSWDプロトコルで通信する)とのブリッジです。 このブリッジのおかげでMCUのフラッシュに新しいバイナリを書き込めるし、デバッガーを使ってMCUの状態を調査したりできます。
ファラッシュに書き込んでみましょう!
# For micro:bit v2
$ cargo embed --features v2 --target thumbv7em-none-eabihf
(...)
Erasing sectors ✔ [00:00:00] [####################################################################################################################################################] 2.00KiB/ 2.00KiB @ 4.21KiB/s (eta 0s )
Programming pages ✔ [00:00:00] [####################################################################################################################################################] 2.00KiB/ 2.00KiB @ 2.71KiB/s (eta 0s )
Finished flashing in 0.608s
# For micro:bit v1
$ cargo embed --features v1 --target thumbv6m-none-eabi
(...)
Erasing sectors ✔ [00:00:00] [####################################################################################################################################################] 2.00KiB/ 2.00KiB @ 4.14KiB/s (eta 0s )
Programming pages ✔ [00:00:00] [####################################################################################################################################################] 2.00KiB/ 2.00KiB @ 2.69KiB/s (eta 0s )
Finished flashing in 0.614s
cargo-embed
が最後の1行を出力したあと、(訳注:実行が終了せずにコンソールを)ブロックしていることに気づくでしょう。
これは意図通り動いており、閉じてはいけません。
次のステップでデバッグするためにこのままの状態にしておく必要があります。
さらに、cargo build
とcargo embed
とに同じフラグが渡されていることにも気づくでしょう。
これはcargo embed
がビルドを実行して、そのあとチップにビルド結果のバイナリを書き込んでいるためです。
そのため、新しいコードをすぐにフラッシュに書き込みたいときはcargo build
を省略できます。
デバッグする
どのような仕組みなのか?
小さなプログラムをデバッグする前に、少し寄り道をして何が起こるのか簡単に理解しましょう。 前章でボード上の2つ目のチップの役割とどうやって私たちのコンピュータとやり取りするか、を説明しましたが果たしてどうやって使うのでしょう?
Embed.toml
にdefault.gb.enabled = true
のオプションを追加すると、フラッシュへの書き込み後、cargo-embed
は「GDBスタブ」と呼ばれるものを立ち上げます。
GDBスタブはGDBが接続できるサーバーで、「ブレイクポイントをX番地に設定する」といったコマンドをサーバーに送信します。
その後、サーバーはこのコマンドをどう扱うか決めます。
cargo-embed
の場合、GDBスタブはコマンドをUSB経由でボード上のデバッグプローブに転送し、デバッグプローブが実際にMCUとやり取りします。
デバッグしてみよう!
cargo-embed
が今使っているシェルをブロックしているので、新しいシェルを立ち上げてプロジェクトディレクトリに移動し直します。
まず最初に、プロジェクトディレクトリに居る状態で、次のようにgdbでバイナリを読み込まなければなりません。
# For micro:bit v2
$ gdb target/thumbv7em-none-eabihf/debug/led-roulette
# For micro:bit v1
$ gdb target/thumbv6m-none-eabi/debug/led-roulette
注意 どのGDBをインストールしたかによって、GDB起動のコマンドが違います。 どのGDBをインストールしたか忘れた場合は、第3章を確認してください。
注意 もし
cargo-embed
がたくさん警告を出力しても気にしないでください。cargo-embed
はGDBプロトコルを全て実装しているわけではないので、GDBから送信したコマンドが認識できないことがあります。cargo-embed
が異常終了しない限り、問題ありません。
次にGDBスタブに接続しなければなりません。
GDBスタブはデフォルトではlocalhost:1337
で動いており、これに接続するには次のコマンドを実行します。
(gdb) target remote :1337
Remote debugging using :1337
0x00000116 in nrf52833_pac::{{impl}}::fmt (self=0xd472e165, f=0x3c195ff7) at /home/nix/.cargo/registry/src/github.com-1ecc6299db9ec823/nrf52833-pac-0.9.0/src/lib.rs:157
157 #[derive(Copy, Clone, Debug)]
続いて、プログラムのmain関数に行きたいです。 そのためにまずブレイクポイントをmain関数に設定して、ブレイクポイントに到達するまでプログラムの実行を続けます。
(gdb) break main
Breakpoint 1 at 0x104: file src/05-led-roulette/src/main.rs, line 9.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue
Continuing.
Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:9
9 #[entry]
ブレイクポイントはプログラムのフローを止めるために使えます。
continue
コマンドはブレイクポイントに到達するまで、プログラムを自由に実行します。
この場合、main
関数にブレイクポイントがあるので、そこに到達するまでプログラムを実行します。
GDBが「Breakpoint 1」と出力していることに注意してください。
今使っているプロセッサでは限られた数のブレイクポイントしか使えないことを覚えておいてください。
先程のようなメッセージに注目を払うのは良いアイデアです。
もしブレイクポイントを使い果たしてしまったら、info break
で現在設定しているブレイクポイントの一覧が見れます。
そしてdelete <ブレイクポイント番号>
でお望みのブレイクポイントを削除します。
より良いデバッグを体験するために、GDBのテキストユーザーインターフェース(TUI)を使ってみましょう。 TUIモードに切り替えるために、GDBシェルに次のコマンドを入力します。
(gdb) layout src
注意 Windowsユーザーのみなさんごめんなさい。GNU ARM Embeddedツールチェインで配布されているGDBではTUIモードがサポートされていません
:-(
。
GDBのブレイクコマンドは関数名だけでなく、特定の行番号でも効果を発揮します。 もし13行目でブレイクしたければ、単に次のようにします。
(gdb) break 13
Breakpoint 2 at 0x110: file src/05-led-roulette/src/main.rs, line 13.
(gdb) continue
Continuing.
Breakpoint 2, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:13
(gdb)
次のコマンドでTUIモードをいつでも終了できます。
(gdb) tui disable
現在_y = x
の文の「上」にいます。
この文はまだ実行されていません。
そのため、x
は初期化されていますが、_y
は初期化されていません。
print
コマンドを使って、これらのスタック/ローカル変数を調べてみましょう。
(gdb) print x
$1 = 42
(gdb) print &x
$2 = (*mut i32) 0x20003fe8
(gdb)
期待通り、x
は値42
を格納しています。
print &x
コマンドは変数x
のアドレスを表示します。
ここでちょっとおもしろいところは、GDBがi32*
という型を表示している点です。
これはi32
のポインタ型です。
プログラムの実行を1行ずつ続けたい場合は、next
コマンドを使います。
それでは、loop {}
文まで進んでみましょう。
(gdb) next
16 loop {}
すると、_y
が初期化されています。
(gdb) print _y
$5 = 42
1つずつローカル変数を表示する代わりに、info locals
コマンドを使うことができます。
(gdb) info locals
x = 42
_y = 42
(gdb)
loop {}
文でnext
を再び実行すると、そこから操作できなくなります。
これはloop {}
文を抜けることがないためです。
代わりに、layout asm
コマンドでディスアンセンブル画面に切り替えて、stepi
コマンドを使って1命令ずつ実行を進めます。
layout src
コマンドで、Rustソースコード画面にいつでも戻ってくることができます。
注意: 間違って
next
やcontinue
コマンドを使ってしまいGDBが操作できなくなった場合は、Ctrl+C
で再びGDBを操作できます。
(gdb) layout asm
TUIモードを使わない場合、disassemble /m
コマンドで現在実行している行付近のプログラムをディスアセンブルできます。
(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E:
10 fn main() -> ! {
0x0000010a <+0>: sub sp, #8
0x0000010c <+2>: movs r0, #42 ; 0x2a
11 let _y;
12 let x = 42;
0x0000010e <+4>: str r0, [sp, #0]
13 _y = x;
0x00000110 <+6>: str r0, [sp, #4]
14
15 // infinite loop; just so we don't leave this stack frame
16 loop {}
=> 0x00000112 <+8>: b.n 0x114 <_ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E+10>
0x00000114 <+10>: b.n 0x114 <_ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E+10>
End of assembler dump.
左側に太矢印の=>
があるところを見てください。
これはプロセッサが次に実行する命令です。
TUIモードじゃない場合、stepi
コマンドを実行するたびに、GDBはプロセッサが次に実行する命令の文と行番号を表示します。
(gdb) stepi
16 loop {}
(gdb) stepi
16 loop {}
より楽しい内容に行く前に、最後のトリックを紹介します。 次のコマンドをGDBに入力してください。
(gdb) monitor reset
(gdb) c
Continuing.
Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:9
9 #[entry]
(gdb)
main
の最初に戻っています!
monitor reset
はマイクロコントローラをリセットし、プログラムのエントリーポイントで停止します。
続くcontinue
コマンドでプログラムをmain
に到達するまで実行し、ブレイクポイントのあるmain
で止まります。
このコンボは、プログラムの調査したいところを間違ってスキップしてしまったときに便利です。 簡単にプログラムを最初の状態に戻すことが出来ます。
補足:
reset
コマンドはRAMをクリアしません。 RAMはreset
コマンド実行前と同じ値を保持しています。 プログラムが初期化されていない変数の値に依存していない限り、問題にはなりません。 ただし、そのような状態は未定義動作(Undefined Behavior: UB)と定義付けられています。
デバッグセッションを完了しました。
quit
コマンドでデバッグセッションを終了できます。
(gdb) quit
A debugging session is active.
Inferior 1 [Remote target] will be detached.
Quit anyway? (y or n) y
Detaching from program: $PWD/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]
ノート デフォルトのGDB CLIがお気に召さない場合は、gdb-dashboardをチェックしてみて下さい。 これはPythonを使って、デフォルトのGDB CLIをレジスタやソースコード表示画面、アセンブリ表示画面などをダッシュボード化してくれます。
GDBでできることについてさらに知りたい場合、GDBの使い方を参照して下さい。
さて、お次は? お約束した高レベルのAPIです。
点灯させる
embedded-hal
この章ではmicro:bitの背面にある多くのLEDのうちの1つを光らせます。
これは組込みプログラミングでの「Hello World」です。
このタスクを完了するためにembedded-hal
で提供されるトレイトの1つを使います。
OutputPin
トレイトがピンのオン、オフを可能にします。
micro:bitのLED
micro:bitの背面に5x5のLEDがあります。 これをLEDマトリクスと呼びます。 25本のピンでLEDを1つずつ駆動するのではなく、マトリクス配置を使うことで10(5+5)ピンだけを使って、マトリクスのどの行とどの列を点灯するか、を制御します。
注意 micro:bit v1では少し違う実装になっています。 回路図のページを見ると、実際は3x9マトリクスとして実装されおり、いくつかの行は使っていません。
特定のLEDを点灯するためにどのピンを制御するかは、micro:bit v2回路図やmicro:bit v1回路図を読まなければなりません。 幸運なことに、全てを良い感じに抽象化するmicro:bit BSPを使えます。
実際に点灯する!
マトリクスのLEDを1つ点灯するコードは、とても簡単ですが、少し準備が必要です。 まずコードを見てから、1つずつ見ていきましょう。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_halt as _; use microbit::board::Board; use microbit::hal::prelude::*; #[entry] fn main() -> ! { let mut board = Board::take().unwrap(); board.display_pins.col1.set_low().unwrap(); board.display_pins.row1.set_high().unwrap(); loop {} }
main関数までの最初の数行は、いくつかの基本的なインポートやすでに見たセットアップのコードです。 しかしながら、main関数はこれまでに見たものと全く違います。
最初の行はRustで書かれたほとんどのHALが内部的にどう動くかに関係しています。
先述した通りHALはチップの全ペリフェラルを(Rust的な意味で)所有するPACクレートの上に構築されています。
let mut board = Board::take().unwrap();
はPACから全ペリフェラルを取得し、変数に束縛します。
今回の場合、HALだけではなくBSP全体が対象となります。
これはボード上の他のチップをRustで表現した所有権も取得します。
ノート: なぜここで
unwrap()
を呼ぶ必要があるのかというと、じつはtake()
は2回以上呼ぶことが可能です。 するとペリフェラルは2つの別の変数として存在することになり、同じリソースを2つの変数から変更できるため、数々の混乱を巻き起こすことになります。 この事態を避けるために、ペリフェラルを2回取得しようとするとパニックを起こすようにPACが実装されています。
今から行1
、列1
に接続されているLEDを点灯します。
そのために行1
ピンをhighレベルにします(つまり、オンにします)。
列1
をlowレベルのままにしておく理由は、LEDマトリクス回路の仕組みのためです。
さらに、embedded-hal
は単なるピンのオン、オフも含め、ハードウェアの各操作がエラーを返し得ることを想定して設計されています。
今回の場合、まずありえないので、結果を単にunwrap()
します。
テストする
私たちの小さなプログラムをテストする方法はとても単純です。
まず、プログラムをsrc/main.rs
に書きます。
そのあと、前節でやったように単にcargo embed
コマンドを実行し、前回と同様にフラッシュに書き込みます。
そして、GDBを起動して、GDBスタブに接続します。
$ # 前節と同じGDBデバッグコマンドをここで実行します
(gdb) target remote :1337
Remote debugging using :1337
cortex_m_rt::Reset () at /home/nix/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.12/src/lib.rs:489
489 pub unsafe extern "C" fn Reset() -> ! {
(gdb)
GDBのcontinue
コマンドでプログラムを実行すると、micro:bitの背面にあるLEDが1つ、点灯します。
It blinks
Delaying
Now we're going to take a brief look into delay abstractions provided by embedded-hal
before combining this with the GPIO abstractions from the previous chapter in order to
finally make an LED blink.
embedded-hal
provides us with two abstractions to delay the execution of our program:
DelayUs
and DelayMs
. Both of them essentially work the exact same way except
that they accept different units for their delay function.
Inside our MCU, several so-called "timers" exist. They can do various things regarding time for us, including simply pausing the execution of our program for a fixed amount of time. A very simple delay-based program that prints something every second might for example look like this:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use rtt_target::{rtt_init_print, rprintln};
use panic_rtt_target as _;
use microbit::board::Board;
use microbit::hal::timer::Timer;
use microbit::hal::prelude::*;
#[entry]
fn main() -> ! {
rtt_init_print!();
let mut board = Board::take().unwrap();
let mut timer = Timer::new(board.TIMER0);
loop {
timer.delay_ms(1000u16);
rprintln!("1000 ms passed");
}
}
Note that we changed our panic implementation from panic_halt
to
panic_rtt_target
here. This will require you to uncomment the two
RTT lines from Cargo.toml
and comment the panic-halt
one out,
since Rust only allows one panic implementation at a time.
In order to actually see the prints we have to change Embed.toml
like this:
[default.general]
# chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1
[default.reset]
halt_afterwards = false
[default.rtt]
enabled = true
[default.gdb]
enabled = false
And now after putting the code into src/main.rs
and another quick cargo embed
(again with the same flags you used before)
you should see "1000 ms passed
" being sent to your console every second from your MCU.
Blinking
Now we've arrived at the point where we can combine our new knowledge about GPIO and delay abstractions in order to actually make an LED on the back of the micro:bit blink. The resulting program is really just a mash-up of the one above and the one that turned an LED on in the last section and looks like this:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use rtt_target::{rtt_init_print, rprintln};
use panic_rtt_target as _;
use microbit::board::Board;
use microbit::hal::timer::Timer;
use microbit::hal::prelude::*;
#[entry]
fn main() -> ! {
rtt_init_print!();
let mut board = Board::take().unwrap();
let mut timer = Timer::new(board.TIMER0);
board.display_pins.col1.set_low().unwrap();
let mut row1 = board.display_pins.row1;
loop {
row1.set_low().unwrap();
rprintln!("Dark!");
timer.delay_ms(1_000_u16);
row1.set_high().unwrap();
rprintln!("Light!");
timer.delay_ms(1_000_u16);
}
}
And after putting the code into src/main.rs
and a final cargo embed
(with the proper flags)
you should see the LED we light up before blinking as well as a print, every time the LED changes from off to on and vice versa.
課題
さあ課題に挑戦する準備はできました! この章の最初にお見せしたこのアプリケーションを実装してください。
もしもなにが起きているのかわかりづらければ、動きを遅くしたこちらを見てください。
LEDを駆動するピンをそれぞれ個別に操作するのは(とくに全部のLEDとなると)めんどうですから、BSPが提供するディスプレイAPIを使うといいです。こういうふうになります。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::rtt_init_print; use panic_rtt_target as _; use microbit::{ board::Board, display::blocking::Display, hal::{prelude::*, Timer}, }; #[entry] fn main() -> ! { rtt_init_print!(); let board = Board::take().unwrap(); let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let light_it_all = [ [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], ]; loop { // light_it_allを1000ミリ秒表示 display.show(&mut timer, light_it_all, 1000); // ディスプレイをクリアする display.clear(); timer.delay_ms(1000_u32); } }
このAPIがあれば、あとは適切な画像マトリクスを計算して、それをBSPに渡してやるだけです。
解答例
どんなコードを書きましたか?
筆者のはこれです。おそらくマトリクスを生成するもっとも単純な(もっとも美しいわけではないですが)やり方でしょう。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::rtt_init_print; use panic_rtt_target as _; use microbit::{ board::Board, display::blocking::Display, hal::Timer, }; const PIXELS: [(usize, usize); 16] = [ (0,0), (0,1), (0,2), (0,3), (0,4), (1,4), (2,4), (3,4), (4,4), (4,3), (4,2), (4,1), (4,0), (3,0), (2,0), (1,0) ]; #[entry] fn main() -> ! { rtt_init_print!(); let board = Board::take().unwrap(); let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut leds = [ [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], ]; let mut last_led = (0,0); loop { for current_led in PIXELS.iter() { leds[last_led.0][last_led.1] = 0; leds[current_led.0][current_led.1] = 1; display.show(&mut timer, leds, 30); last_led = *current_led; } } }
もうひとつ! あなたの解答が「release」(リリース)モードでコンパイルされても動作することを確認しましょう。
# For micro:bit v2
$ cargo embed --features v2 --target thumbv7em-none-eabihf --release
(...)
# For micro:bit v1
$ cargo embed --features v1 --target thumbv6m-none-eabi --release
(...)
「release」モードで生成したバイナリをデバッグするには、先に見たものとは違うGDBコマンドを使わなくてはなりません。
# For micro:bit v2
$ gdb target/thumbv7em-none-eabihf/release/led-roulette
# For micro:bit v1
$ gdb target/thumbv6m-none-eabi/release/led-roulette
バイナリのサイズにはいつも注意を払いましょう! あなたの解答はどのくらいのサイズですか? size
コマンドでリリースバイナリのサイズを確認できます。
# For micro:bit v2
$ cargo size --features v2 --target thumbv7em-none-eabihf -- -A
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
led-roulette :
section size addr
.vector_table 256 0x0
.text 26984 0x100
.rodata 2732 0x6a68
.data 0 0x20000000
.bss 1092 0x20000000
.uninit 0 0x20000444
.debug_abbrev 33941 0x0
.debug_info 494113 0x0
.debug_aranges 23528 0x0
.debug_ranges 130824 0x0
.debug_str 498781 0x0
.debug_pubnames 143351 0x0
.debug_pubtypes 124464 0x0
.ARM.attributes 58 0x0
.debug_frame 69128 0x0
.debug_line 290580 0x0
.debug_loc 1449 0x0
.comment 109 0x0
Total 1841390
$ cargo size --features v2 --target thumbv7em-none-eabihf --release -- -A
Finished release [optimized + debuginfo] target(s) in 0.02s
led-roulette :
section size addr
.vector_table 256 0x0
.text 6332 0x100
.rodata 648 0x19bc
.data 0 0x20000000
.bss 1076 0x20000000
.uninit 0 0x20000434
.debug_loc 9036 0x0
.debug_abbrev 2754 0x0
.debug_info 96460 0x0
.debug_aranges 1120 0x0
.debug_ranges 11520 0x0
.debug_str 71325 0x0
.debug_pubnames 32316 0x0
.debug_pubtypes 29294 0x0
.ARM.attributes 58 0x0
.debug_frame 2108 0x0
.debug_line 19303 0x0
.comment 109 0x0
Total 283715
# micro:bit v1
$ cargo size --features v1 --target thumbv6m-none-eabi -- -A
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
led-roulette :
section size addr
.vector_table 168 0x0
.text 28584 0xa8
.rodata 2948 0x7050
.data 0 0x20000000
.bss 1092 0x20000000
.uninit 0 0x20000444
.debug_abbrev 30020 0x0
.debug_info 373392 0x0
.debug_aranges 18344 0x0
.debug_ranges 89656 0x0
.debug_str 375887 0x0
.debug_pubnames 115633 0x0
.debug_pubtypes 86658 0x0
.ARM.attributes 50 0x0
.debug_frame 54144 0x0
.debug_line 237714 0x0
.debug_loc 1499 0x0
.comment 109 0x0
Total 1415898
$ cargo size --features v1 --target thumbv6m-none-eabi --release -- -A
Finished release [optimized + debuginfo] target(s) in 0.02s
led-roulette :
section size addr
.vector_table 168 0x0
.text 4848 0xa8
.rodata 648 0x1398
.data 0 0x20000000
.bss 1076 0x20000000
.uninit 0 0x20000434
.debug_loc 9705 0x0
.debug_abbrev 3235 0x0
.debug_info 61908 0x0
.debug_aranges 1208 0x0
.debug_ranges 5784 0x0
.debug_str 57358 0x0
.debug_pubnames 22959 0x0
.debug_pubtypes 18891 0x0
.ARM.attributes 50 0x0
.debug_frame 2316 0x0
.debug_line 18444 0x0
.comment 19 0x0
Total 208617
注 このCargoプロジェクトは、LTOを使ってリリースバイナリをビルドするように設定されています。
この出力をどう読めばいいかわかりますか? text
セクションは、プログラムのインストラクションを格納しています。一方data
とbss
セクションは、静的にRAMにアロケートされた変数(static
変数)を格納しています。micro:bitが搭載するマイクロコントローラの仕様をおぼえていれば、このバイナリを収めるにはフラッシュメモリが小さすぎるとお気づきでしょう。ではどうやってこれが収まっているのでしょうか? 上のサイズ情報からわかるように、バイナリのほとんどはデバッグに関係したセクションが占めています。これらはコードの実行には関係ないものですから、マイクロコントローラに書き込まれることはないのです。
Serial communication
This is what we'll be using. I hope your computer has one!
Nah, don't worry. This connector, the DE-9, went out of fashion on PCs quite some time ago; it got replaced by the Universal Serial Bus (USB). We won't be dealing with the DE-9 connector itself but with the communication protocol that this cable is/was usually used for.
So what's this serial communication? It's an asynchronous communication protocol where two devices exchange data serially, as in one bit at a time, using two data lines (plus a common ground). The protocol is asynchronous in the sense that neither of the shared lines carries a clock signal. Instead, both parties must agree on how fast data will be sent along the wire before the communication occurs. This protocol allows duplex communication as data can be sent from A to B and from B to A simultaneously.
We'll be using this protocol to exchange data between the microcontroller and your computer. Now you might be asking yourself why exactly we aren't using RTT for this like we did before. RTT is a protocol that is meant to be used solely for debugging. You will most definitely not be able to find a device that actually uses RTT to communicate with some other device in production. However, serial communication is used quite often. For example some GPS receivers send the positioning information they receive via serial communication.
The next practical question you probably want to ask is: How fast can we send data through this protocol?
This protocol works with frames. Each frame has one start bit, 5 to 9 bits of payload (data) and 1 to 2 stop bits. The speed of the protocol is known as baud rate and it's quoted in bits per second (bps). Common baud rates are: 9600, 19200, 38400, 57600 and 115200 bps.
To actually answer the question: With a common configuration of 1 start bit, 8 bits of data, 1 stop bit and a baud rate of 115200 bps one can, in theory, send 11,520 frames per second. Since each one frame carries a byte of data that results in a data rate of 11.52 KB/s. In practice, the data rate will probably be lower because of processing times on the slower side of the communication (the microcontroller).
Today's computers don't support the serial communication protocol. So you can't directly connect your computer to the microcontroller. Luckily for us though, the debug probe on the micro:bit has a so-called USB-to-serial converter. This means that the converter will sit between the two and expose a serial interface to the microcontroller and a USB interface to your computer. The microcontroller will see your computer as another serial device and your computer will see the microcontroller as a virtual serial device.
Now, let's get familiar with the serial module and the serial communication tools that your OS offers. Pick a route:
*nix tooling
Connecting the micro:bit board
If you connect the micro:bit board to your computer you
should see a new TTY device appear in /dev
.
$ # Linux
$ dmesg | tail | grep -i tty
[63712.446286] cdc_acm 1-1.7:1.1: ttyACM0: USB ACM device
This is the USB <-> Serial device. On Linux, it's named tty*
(usually
ttyACM*
or ttyUSB*
).
On Mac OS ls /dev/cu.usbmodem*
will show the serial device.
But what exactly is ttyACM0
? It's a file of course!
Everything is a file in *nix:
$ ls -l /dev/ttyACM0
crw-rw----. 1 root plugdev 166, 0 Jan 21 11:56 /dev/ttyACM0
You can send out data by simply writing to this file:
$ echo 'Hello, world!' > /dev/ttyACM0
You should see the orange LED on the micro:bit, right next to the USB port, blink for a moment, whenever you enter this command.
minicom
We'll use the program minicom
to interact with the serial device using the keyboard.
We must configure minicom
before we use it. There are quite a few ways to do that but we'll use a
.minirc.dfl
file in the home directory. Create a file in ~/.minirc.dfl
with the following
contents:
$ cat ~/.minirc.dfl
pu baudrate 115200
pu bits 8
pu parity N
pu stopbits 1
pu rtscts No
pu xonxoff No
NOTE Make sure this file ends in a newline! Otherwise,
minicom
will fail to read it.
That file should be straightforward to read (except for the last two lines), but nonetheless let's go over it line by line:
pu baudrate 115200
. Sets baud rate to 115200 bps.pu bits 8
. 8 bits per frame.pu parity N
. No parity check.pu stopbits 1
. 1 stop bit.pu rtscts No
. No hardware control flow.pu xonxoff No
. No software control flow.
Once that's in place, we can launch minicom
.
$ # NOTE you may need to use a different device here
$ minicom -D /dev/ttyACM0 -b 115200
This tells minicom
to open the serial device at /dev/ttyACM0
and set its
baud rate to 115200. A text-based user interface (TUI) will pop out.
You can now send data using the keyboard! Go ahead and type something. Note that the text UI will not echo back what you type. If you pay attention to the yellow LED on top of the micro:bit though, you will notice that it blinks whenever you type something.
minicom
commands
minicom
exposes commands via keyboard shortcuts. On Linux, the shortcuts start with Ctrl+A
. On
Mac, the shortcuts start with the Meta
key. Some useful commands below:
Ctrl+A
+Z
. Minicom Command SummaryCtrl+A
+C
. Clear the screenCtrl+A
+X
. Exit and resetCtrl+A
+Q
. Quit with no reset
NOTE Mac users: In the above commands, replace
Ctrl+A
withMeta
.
Windowsのツール
まずmicro:bitをPCから抜きましょう。
micro:bitを差し込む前に、ターミナルで次のコマンドを実行して下さい。
$ mode
このコマンドは、PCに接続されているデバイスの一覧を表示します。COM
から名前が始まるデバイスが、シリアルデバイスです。
このデバイスがこれから使うデバイスの種類です。micro:bitを差し込む前にmode
が出力した全てのCOM
ポートをメモして下さい。
それでは、micro:bitを差し込み、mode
コマンドを再び実行して下さい。新しいCOM
ポートが、リストに現れるはずです。
これが、micro:bitのシリアル通信機能に割り当てられたCOMポートです。
次にputty
を起動します。GUIが現れます。
開始画面では、「Session」カテゴリがあるはずなので、それを開いて「Connection type」として「Serial」を選択して下さい。
「Serial line」フィールドには、先ほどの手順で入手したCOM
デバイスを入力して下さい。例えば、COM3
です。
次に、メニューの左側から、「Connection/Serial」カテゴリを選択します。新しい画面では、 シリアルポートが次の通り設定されていることを確認して下さい。
- "Speed (baud)": 115200
- "Data bits": 8
- "Stop bits": 1
- "Parity": None
- "Flow control": None
最後に、Openボタンをクリックします。コンソールが出現します。
このコンソールでタイピングすると、micro:bit上部にある黄色のLEDが点滅するはずです。 キーストロークごとにLEDは1度点滅します。コンソールは、タイピングしたことをエコーバックしないため、 画面は何も表示されていないままになります。
UART
マイクロコントローラには、UART(Universal Asynchronous Receiver/Transmitter)と呼ばれるペリフェラルがあります。このペリフェラルは、シリアル通信など、いくつかの通信プロトコルで動作するように設定できます。
この章では、シリアル通信を使ってマイクロコントローラとコンピュータ間で情報のやり取りをします。
注 micro:bit v2ではUARTEと呼ばれるペリフェラルを使いますが、普通のUARTと同じように動作します。HALとペリフェラルのやり取りに違いがあるのですが、ここではそれを気にする必要はまったくありません。
セットアップ
これまでのように、Embed.toml
をお使いのmicro:bitのバージョンに合わせて修正してください。
[default.general]
# chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1
[default.reset]
halt_afterwards = false
[default.rtt]
enabled = true
[default.gdb]
enabled = false
1バイト送信する
最初のタスクは、シリアル通信でマイクロコントローラからコンピュータに1バイト送ることです。
そのために、以下のコードを使いましょう。(07-uart/src/main.rs
中にあるものです。)
#![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::rtt_init_print; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::prelude::*, hal::uart, hal::uart::{Baudrate, Parity}, }; #[cfg(feature = "v2")] use microbit::{ hal::prelude::*, hal::uarte, hal::uarte::{Baudrate, Parity}, }; #[cfg(feature = "v2")] mod serial_setup; #[cfg(feature = "v2")] use serial_setup::UartePort; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut serial = { uart::Uart::new( board.UART0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ) }; #[cfg(feature = "v2")] let mut serial = { let serial = uarte::Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); UartePort::new(serial) }; nb::block!(serial.write(b'X')).unwrap(); nb::block!(serial.flush()).unwrap(); loop {} }
まず目新しいものといえば、cfg
ディレクティブでしょう。これは条件によって特定のコードセクションをソースに含めたり除外したりするためのもので、ここではmicro:bit v1用にUART、micro:bit v2用にはUARTEを使用するよう指定しています。
また、ここで初めてライブラリ外のコードを取り込んでいることにもお気づきでしょう。serial_setup
モジュールのことです。UARTEのラッパであるこのモジュールを使うことで、UARTEもUARTとまったく同じようにembedded_hal::serial
トレイト経由で扱うことができます。この章の理解には必要ありませんが、もし興味があればモジュールの設計をのぞいてみてください。
これらの違いを除けば、UARTとUARTEの初期化手続きはよく似ています。ですからここではUARTEの初期化についてだけ解説します。UARTEは以下のコードで初期化します。
uarte::Uarte::new(
board.UARTE0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
);
この関数はRustで表現されたUARTEペリフェラル(board.UARTE0
)、ならびにTX/RXピン(board.uart.into()
)の所有権を取得します。こうすることで、私たちの使っているUARTEとピンを他で使えないようにできます。次に、ふたつの設定オプションをコンストラクタに渡します。ボーレート(Baudrate
)とパリティ(Parity
)です。パリティはシリアル通信ラインに受信したデータが破損していないか確認することを可能にするオプションです。ここでは使いませんので、除外(EXCLUDED
)しておきましょう。最後にUartePort
型で包んでやります。こうすることで、micro:bit v1の serial
と同じように扱うことが可能になります。
初期化できたら、今作ったばかりのUARTインスタンスでX
を送ります。ここに出てくるblock!
マクロは、nb::block!
マクロです。nb
は、「最小限かつ再利用可能なノンブロッキングI/O層」(公式ドキュメントからの引用)です。これによって、バックグラウンドでハードウェア操作を行うあいだに別タスクを処理(ノンブロッキング)することができます。ですが、このケースでは平行して別の処理はしないので、ここではただblock!
を呼び、I/O処理の成功・失敗を待ってからプログラムの実行を続けることにします。
最後に、シリアルポートをflush()
します。なぜかというと、embedded-hal::serial
トレイトの実装によっては、送信するデータが一定のバイト数になるまで、送信バッファに貯めておく実装になっていることがあるからです。(実際にUARTEはそのように実装されています。)flush()
を呼ぶことで、送信データをそれ以上待たずに、送信バッファの内容を強制的に出力させることができるのです。
テストしましょう
プログラムを書き込む前に、忘れずにminicom/PuTTYをスタートしてください。シリアル通信で受信するデータは、どこかに保存されるわけではなく、リアルタイムに観察する必要があるからです。シリアルモニタが立ち上がったら、5章でしたようにプログラムを書き込みましょう。
# For micro:bit v2
$ cargo embed --features v2 --target thumbv7em-none-eabihf
(...)
# For micro:bit v1
$ cargo embed --features v1 --target thumbv6m-none-eabi
書き込みが終わると、X
の文字がminicom/PuTTYのターミナルに出るはずです。おめでとうございます!
文字列を送信する
次のタスクは、マイクロコントローラからコンピュータにひとつの文字列すべてを送信することです。
"The quick brown fox jumps over the lazy dog."
という文字列を送りましょう。
あなたの番です。プログラムを書いてください。
単純な方法とwrite!
単純な方法
おそらく次のようなプログラムを書かれたことと思います。
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use rtt_target::rtt_init_print;
use panic_rtt_target as _;
#[cfg(feature = "v1")]
use microbit::{
hal::prelude::*,
hal::uart,
hal::uart::{Baudrate, Parity},
};
#[cfg(feature = "v2")]
use microbit::{
hal::prelude::*,
hal::uarte,
hal::uarte::{Baudrate, Parity},
};
#[cfg(feature = "v2")]
mod serial_setup;
#[cfg(feature = "v2")]
use serial_setup::UartePort;
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = microbit::Board::take().unwrap();
#[cfg(feature = "v1")]
let mut serial = {
uart::Uart::new(
board.UART0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
)
};
#[cfg(feature = "v2")]
let mut serial = {
let serial = uarte::Uarte::new(
board.UARTE0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
);
UartePort::new(serial)
};
for byte in b"The quick brown fox jumps over the lazy dog.\r\n".iter() {
nb::block!(serial.write(*byte)).unwrap();
}
nb::block!(serial.flush()).unwrap();
loop {}
}
これも立派な実装ですが、print!
のように引数を文字列にフォーマットできたらいいのに、とは思いませんか? そんなことができるのでしょうか。読み進めてください。
write!
とcore::fmt::Write
core::fmt::Write
というトレイトがあります。このトレイトを実装した構造体は、フォーマットされた文字列の出力先として扱うことができ、std
環境におけるprint!
と同じような処理を可能にします。ここでは、nrf
HALのUart
構造体がcore::fmt::Write
を実装しています。これを利用して先ほどのプログラムをリファクタリングすると、こうなります:
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use rtt_target::rtt_init_print;
use panic_rtt_target as _;
use core::fmt::Write;
#[cfg(feature = "v1")]
use microbit::{
hal::prelude::*,
hal::uart,
hal::uart::{Baudrate, Parity},
};
#[cfg(feature = "v2")]
use microbit::{
hal::prelude::*,
hal::uarte,
hal::uarte::{Baudrate, Parity},
};
#[cfg(feature = "v2")]
mod serial_setup;
#[cfg(feature = "v2")]
use serial_setup::UartePort;
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = microbit::Board::take().unwrap();
#[cfg(feature = "v1")]
let mut serial = {
uart::Uart::new(
board.UART0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
)
};
#[cfg(feature = "v2")]
let mut serial = {
let serial = uarte::Uarte::new(
board.UARTE0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
);
UartePort::new(serial)
};
write!(serial, "The quick brown fox jumps over the lazy dog.\r\n").unwrap();
nb::block!(serial.flush()).unwrap();
loop {}
}
このプログラムをmicro:bitに書き込むと、リファクタリング前のイテレータを使ったプログラムと同じ結果を得ることができます。
1バイト受信する
これまでマイクロコントローラからコンピュータにデータを送信してきました。今度はその逆、コンピュータからの受信を試してみましょう。幸いなことに、これもまたembedded-hal
のおかげで簡単です。
#![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::{rtt_init_print, rprintln}; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::prelude::*, hal::uart, hal::uart::{Baudrate, Parity}, }; #[cfg(feature = "v2")] use microbit::{ hal::prelude::*, hal::uarte, hal::uarte::{Baudrate, Parity}, }; #[cfg(feature = "v2")] mod serial_setup; #[cfg(feature = "v2")] use serial_setup::UartePort; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut serial = { uart::Uart::new( board.UART0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ) }; #[cfg(feature = "v2")] let mut serial = { let serial = uarte::Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); UartePort::new(serial) }; loop { let byte = nb::block!(serial.read()).unwrap(); rprintln!("{}", byte); } }
1バイト送信プログラムからの唯一の変更点は、main()
の終わりにあるループです。ここでembedded-hal
が提供するread()
関数を使い、1バイト読み込むのを待っています。次にそのバイトをRTTデバッグコンソールにプリントして、データが本当に来ているか確認しています。
注意点があります。このプログラムを書き込んで、minicom
に文字をタイプするとき、RTTコンソールには数字だけが表示されます。というのも、このプログラムは受信したu8
を実際の文字であるchar
に変換しないからです。u8
からchar
への変換はとても簡単です。どうしてもRTTコンソールで文字を確認したければ、ご自分で実装してみてください。
エコーサーバー
それでは送信と受信をひとつのプログラムにまとめて、エコーサーバーを作ってみましょう。エコーサーバーとは、クライアントから受け取ったテキストをそのまま送り返すものです。このアプリケーションでは、マイクロコントローラがサーバーで、コンピュータがクライアントとなります。
実装は簡単なはずです。(ヒント:1バイトごとに処理しましょう)
文字列を反転させる
では次に、サーバーをもっとおもしろくしてみましょう。クライアントが送るテキストを反転させてから返信してください。サーバーは、ENTERキーが押されるたびに返信することとします。返信はそのつど新しい行となります。
今回はバッファが必要ですので、heapless::Vec
を使ってください。これがスターターコードです:
#![no_main] #![no_std] use cortex_m_rt::entry; use core::fmt::Write; use heapless::Vec; use rtt_target::rtt_init_print; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::prelude::*, hal::uart, hal::uart::{Baudrate, Parity}, }; #[cfg(feature = "v2")] use microbit::{ hal::prelude::*, hal::uarte, hal::uarte::{Baudrate, Parity}, }; #[cfg(feature = "v2")] mod serial_setup; #[cfg(feature = "v2")] use serial_setup::UartePort; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut serial = { uart::Uart::new( board.UART0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ) }; #[cfg(feature = "v2")] let mut serial = { let serial = uarte::Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); UartePort::new(serial) }; // 32バイト容量のバッファ let mut buffer: Vec<u8, 32> = Vec::new(); loop { buffer.clear(); // TODO クライアントからのリクエストを受信してください。それぞれのリクエストはENTERで終わります。 // 注 `buffer.push`は`Result`を返します。エラーメッセージを返信することで、エラーを処理してください。 // TODO 反転させた文字列を返信してください。 } }
解答例
#![no_main] #![no_std] use cortex_m_rt::entry; use core::fmt::Write; use heapless::{Vec, consts}; use rtt_target::rtt_init_print; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::prelude::*, hal::uart, hal::uart::{Baudrate, Parity}, }; #[cfg(feature = "v2")] use microbit::{ hal::prelude::*, hal::uarte, hal::uarte::{Baudrate, Parity}, }; #[cfg(feature = "v2")] mod serial_setup; #[cfg(feature = "v2")] use serial_setup::UartePort; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut serial = { uart::Uart::new( board.UART0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ) }; #[cfg(feature = "v2")] let mut serial = { let serial = uarte::Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); UartePort::new(serial) }; // 32バイト容量のバッファ let mut buffer: Vec<u8, consts::U32> = Vec::new(); loop { buffer.clear(); loop { // 受信は失敗しないものと想定します。 let byte = nb::block!(serial.read()).unwrap(); if buffer.push(byte).is_err() { writeln!(serial, "error: buffer full").unwrap(); break; } if byte == 13 { for byte in buffer.iter().rev().chain(&[b'\n', b'\r']) { nb::block!(serial.write(*byte)).unwrap(); } break; } } nb::block!(serial.flush()).unwrap() } }
I2C
ここまでシリアル通信プロトコルを見てきました。シリアル通信はその単純さから広く使われているプロトコルです。シンプルな設計ゆえに、BluetoothやUSBのような別プロトコルの上に実装することも容易です。
ですが、単純さには欠点もあります。デジタルセンサの読み込みなど、より手の込んだデータのやり取りには別プロトコルが必要となります。
幸か不幸か、組込みの世界には他にもたくさんの通信プロトコルが存在します。中には、デジタルセンサで広く使われているものもあります。
私たちが使っているmicro:bitは、ふたつのモーションセンサを持っています。加速度計と磁力計です。その両方はひとつのコンポーネントとしてパッケージ化されており、I2Cバスでアクセスできます。
I2Cは、Inter-Integrated Circuitを意味し、同期 シリアル通信プロトコルのひとつです。データのやり取りには、データ線(SDA)とクロック線(SCL)の二本の信号線を使います。クロック線で通信を同期させるので、同期プロトコルというわけです。
このプロトコルはマスター・スレーブモデルにのっとっており、マスターが通信を開始し、スレーブとのやり取りをリードします。同じバスに複数のデバイス(マスターでもスレーブでも)を接続することも可能です。特定のデバイスと通信するために、マスターはまず対象となるデバイスのアドレスをバスにブロードキャストします。アドレスは7ビット長、あるいは10ビット長です。一度マスターがスレーブとの通信を開始すると、マスターが通信を停止するまで他のデバイスはバスを使用できません。
クロック線が通信速度を決定します。通常は100 kHz (標準モード) か 400 kHz (ファーストモード)の周波数で動作します。
プロトコルの概要
複数デバイス間の通信をサポートするため、I2Cはシリアル通信よりも複雑なプロトコルとなっています。実際にどう動くのか、例を使って見てみましょう。
Master -> Slave
マスター -> スレーブ
マスターがスレーブにデータを送りたいとき、次のようになります。
- M(マスター):STARTをブロードキャストする
- M:スレーブアドレス(上位7ビット)と、WRITEに設定したR/Wビット(第8ビット)をブロードキャストする
- S(スレーブ):ACK(ACKnowledgement)を返信する
- M:1バイト送信する
- S:ACKを返信する
- 必要なだけ4と5を繰り返す
- M:STOPをブロードキャストする(または、RESTARTをブロードキャストして2へ戻る)
注 スレーブアドレスが7ビット長でなく10ビット長のときもありますが、他はなにも変わりません。
マスター <- スレーブ
マスターがスレーブからデータを読み取りたいときはこうなります。
- M:STARTをブロードキャストする
- M:スレーブアドレス(上位7ビット)と、READに設定したR/Wビット(第8ビット)をブロードキャストする
- S:ACK(ACKnowledgement)を返信する
- S:1バイト送信する
- M:ACKを返答する
- 必要なだけ4と5を繰り返す
- M:STOPをブロードキャストする(または、RESTARTをブロードキャストして2へ戻る)
注 スレーブアドレスが7ビット長でなく10ビット長のときもありますが、他はなにも変わりません。
LSM303AGR
micro:bitが持つ磁力計と加速度計のふたつのセンサは、LSM303AGRという集積回路にひとつにまとめられており、I2Cバスでアクセス可能です。センサはそれぞれ独立したI2Cスレーブとして動作し、別々のアドレスを持ちます。
各センサは測定した環境情報をそれぞれのメモリに保存します。ですからセンサとのやりとりは、主にそのメモリの読み取りとなります。
センサのメモリは、バイト単位でアドレスを指定できるレジスタの集まりとして作られています。レジスタに書き込むことで、センサの設定を変更することもできます。そういう意味では、マイクロコントローラ内のペリフェラルに似ているかもしれません。異なる点は、センサのレジスタはマイクロコントローラのメモリにマッピングされておらず、I2Cバスでアクセスする必要があることです。
LSM303AGRについての一番の情報源はデータシートです。どうやってセンサのレジスタを読むのかを確認してください。ここに書いてあります。
Section 6.1.1 I2C Operation - Page 38 - LSM303AGR Data Sheet
他にこの本に関係のある情報といえば、レジスタについての記述でしょう。このセクションです。
Section 8 Register description - Page 46 - LSM303AGR Data Sheet
レジスタを読む
では理論を実践してみましょう!
まず加速度計(accelerometer)と磁力計(magnetometer)それぞれのスレーブアドレスを知る必要があります。その情報はLSM303AGRのデータシートの39ページにあります。
- 0011001 が加速度計のアドレス
- 0011110 が磁力計のアドレス
注 アドレスは上位7ビットだけで表されることを思い出してください。8番目のビットは読み込みか書き込みかを指定するビットとなります。
次に読み出しのためのレジスタが必要です。多くのI2Cチップはデバイスを識別するためのレジスタを持っています。というのも、何千もの(あるいは何百万もの)I2Cチップが流通しているわけですから、同じアドレスを持つ別製品が製造されていてもおかしくないからです。(結局のところ、アドレスはたったの7ビット幅なのですから。)このデバイスIDレジスタを利用することで、たまたま同じアドレスを持った別のチップではなく、本当にLSM303AGRと通信していることを確認することができるのです。データシート(46と61ページを参照)にあるように、LSM303AGRはアドレス0x0f
にWHO_AM_I_A
(Aは加速度計を指す)、アドレス0x4f
にWHO_AM_I_M
(Mは磁力計を指す)というふたつのレジスタを提供しており、それぞれにはデバイス特有の値が入っています。
ここまできたらあとはソフトウェアです。つまり、microbit
HALのどのAPIを使うか、です。ところがnRFチップのデータシートを読み通してみると、nRFチップはI2Cペリフェラルを持っていないことに気がつくでしょう。ですが幸いなことに、I2Cと互換性のあるTWI (Two Wire Interface)、またはTWIMと呼ばれるペリフェラルがあります。(UARTとUARTEのように、チップによって名称が違うのです。)
ではmicrobit
クレートのtwi(m)
モジュールのドキュメンテーションとこれまでに集めた情報を参考にしながら、ふたつのデバイスIDを読み取ってプリントするコードを書きましょう。このようなものになるはずです。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::{rtt_init_print, rprintln}; use panic_rtt_target as _; use microbit::hal::prelude::*; #[cfg(feature = "v1")] use microbit::{ hal::twi, pac::twi0::frequency::FREQUENCY_A, }; #[cfg(feature = "v2")] use microbit::{ hal::twim, pac::twim0::frequency::FREQUENCY_A, }; const ACCELEROMETER_ADDR: u8 = 0b0011001; const MAGNETOMETER_ADDR: u8 = 0b0011110; const ACCELEROMETER_ID_REG: u8 = 0x0f; const MAGNETOMETER_ID_REG: u8 = 0x4f; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let mut i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut acc = [0]; let mut mag = [0]; // First write the address + register onto the bus, then read the chip's responses i2c.write_read(ACCELEROMETER_ADDR, &[ACCELEROMETER_ID_REG], &mut acc).unwrap(); i2c.write_read(MAGNETOMETER_ADDR, &[MAGNETOMETER_ID_REG], &mut mag).unwrap(); rprintln!("The accelerometer chip's id is: {:#b}", acc[0]); rprintln!("The magnetometer chip's id is: {:#b}", mag[0]); loop {} }
これまでの説明を理解していれば、初期化の仕方を除き、コード自体は単純なはずです。初期化はUARTで見たものと似ています。コンストラクタにペリフェラル、通信に使うピン、バスを駆動する周波数を渡してやります。周波数は、ここでは100kHz(K100
)とします。
Testing it
テストしましょう
いつものようにEmbed.toml
をお使いのMCU向けに修正し、以下を実行してこのプログラムをテストしましょう。
# For micro:bit v2
$ cargo embed --features v2 --target thumbv7em-none-eabihf
# For micro:bit v1
$ cargo embed --features v1 --target thumbv6m-none-eabi
ドライバを使う
5章ですでに触れたとおり、embedded-hal
が提供する抽象化のおかげで、プラットフォームに依存することなくハードウェアとやりとりするコードを書くことができます。実際に、7章と8章でこれまでに使用したメソッドはすべてembedded-hal
が定義するトレイトから来ていました。ここで私たちも初めてembedded-hal
のトレイトを利用してみましょう。
Rustがサポートする(そして将来サポートするであろう)すべての組込みプラットフォームに別々のLSM303AGR用ドライバを開発しなくてはならないとしたら、それはまったくもって無駄なことです。それを避けるために、embedded-hal
トレイトを実装したジェネリック型を消費するコードを書き、プラットフォームに依存しないドライバを作ります。幸運なことに、その作業はすでにlsm303agr
クレート内でなされています。ですから加速度計、磁力計の値を読み取るのは、(少しばかりドキュメンテーションを読むことを除けば)基本的にプラグアンドプレイです。実際に、crates.io
のページを見れば、Raspberry Pi向けにではありますが、加速度計のデータを読み取るのに必要な情報はすべて書いてあります。あとはそれを私たちが使うチップ向けに修正するだけです。
use linux_embedded_hal::I2cdev; use lsm303agr::{AccelOutputDataRate, Lsm303agr}; fn main() { let dev = I2cdev::new("/dev/i2c-1").unwrap(); let mut sensor = Lsm303agr::new_with_i2c(dev); sensor.init().unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz50).unwrap(); loop { if sensor.accel_status().unwrap().xyz_new_data { let data = sensor.accel_data().unwrap(); println!("Acceleration: x {} y {} z {}", data.x, data.y, data.z); } } }
embedded_hal::blocking::i2c
トレイトを実装したオブジェクトのインスタンス生成方法は前のページで確認しましたから、修正は簡単なはずです。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::{rtt_init_print, rprintln}; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::twi, pac::twi0::frequency::FREQUENCY_A, }; #[cfg(feature = "v2")] use microbit::{ hal::twim, pac::twim0::frequency::FREQUENCY_A, }; use lsm303agr::{ AccelOutputDataRate, Lsm303agr, }; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; // ドキュメンテーションからのコード let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz50).unwrap(); loop { if sensor.accel_status().unwrap().xyz_new_data { let data = sensor.accel_data().unwrap(); // printの代わりにRTTを使用します rprintln!("Acceleration: x {} y {} z {}", data.x, data.y, data.z); } } }
前のページでやったように、以下のコマンドでこのプログラムを試すことができます。
# For micro:bit v2
$ cargo embed --features v2 --target thumbv7em-none-eabihf
# For micro:bit v1
$ cargo embed --features v1 --target thumbv6m-none-eabi
さらに、もしmicro:bitを(物理的に)動かしてみれば、画面にプリントされる加速度計の値が変化するのを確認できるでしょう。
課題
この章での課題です。前章で紹介したシリアルインターフェースを利用して、実世界と通信する小さなアプリケーションを作ってください。"magnetometer"(磁力計)、"accelerometer"(加速度計)のふたつのコマンドを受け取って、対応するセンサの値をプリントすることとします。実装に必要な情報はすでにこの章とUARTの章でお渡ししたので、今回はテンプレートはありません。ですが、ヒントは差し上げましょう。
- 文字列を扱うことになるので、
heapless::String
を使うといいかもしれません。 - 磁力計のAPIドキュメンテーションを(もちろん)読まなくてはなりません。ですが、加速度計のそれとほとんど変わりありません。
解答例
#![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::rtt_init_print; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::twi, pac::twi0::frequency::FREQUENCY_A, hal::uart, hal::uart::{Baudrate, Parity}, }; #[cfg(feature = "v2")] use microbit::{ hal::twim, pac::twim0::frequency::FREQUENCY_A, hal::uarte, hal::uarte::{Baudrate, Parity}, }; use microbit::hal::prelude::*; use lsm303agr::{AccelOutputDataRate, MagOutputDataRate, Lsm303agr}; use heapless::{consts, Vec, String}; use nb::block; use core::fmt::Write; #[cfg(feature = "v2")] mod serial_setup; #[cfg(feature = "v2")] use serial_setup::UartePort; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let mut serial = { uart::Uart::new( board.UART0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ) }; #[cfg(feature = "v2")] let mut serial = { let serial = uarte::Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); UartePort::new(serial) }; #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz50).unwrap(); sensor.set_mag_odr(MagOutputDataRate::Hz50).unwrap(); let mut sensor = sensor.into_mag_continuous().ok().unwrap(); loop { let mut buffer: Vec<u8, consts::U32> = Vec::new(); loop { let byte = block!(serial.read()).unwrap(); if buffer.push(byte).is_err() { write!(serial, "error: buffer full\r\n").unwrap(); break; } if byte == 13 { break; } } let command_string = String::from_utf8(buffer).unwrap(); if command_string.as_str().trim() == "accelerometer" { while !sensor.accel_status().unwrap().xyz_new_data { } let data = sensor.accel_data().unwrap(); write!(serial, "Accelerometer: x {} y {} z {}\r\n", data.x, data.y, data.z).unwrap(); } else if command_string.as_str().trim() == "magnetometer" { while !sensor.mag_status().unwrap().xyz_new_data { } let data = sensor.mag_data().unwrap(); write!(serial, "Magnetometer: x {} y {} z {}\r\n", data.x, data.y, data.z).unwrap(); } else { write!(serial, "error: command not detected\r\n").unwrap(); } } }
LEDコンパス
この章では、micro:bitのLEDを利用してコンパスを作ります。本物のコンパスのように、私たちのコンパスも北を指し示さなくてはなりません。そのためにLEDマトリクスを利用しましょう。外縁に配置されたLEDのひとつを点灯させ、そのLEDが北を示すようにします。
磁場は、ガウスやテスラで測定される大きさと、方向を持ちます。micro:bitの磁力計は外部磁場の大きさと方向の両方を測定しますが、その結果はmicro:bitの軸に沿って分解されたものとして提示されます。
磁力計は、みっつの軸を持っています。X軸とY軸は直角に交差して平面に伸びています。Z軸はその交点から「上に」突き出ているイメージです。
I2の章ですでに確認しましたから、磁力計のデータをRTTコンソールに出力し続けるプログラムは書けるはずです。そのプログラムを書き終えたら、あなたの場所での北を確認してください。それからmicro:bitをその方角にぴったりと向けて、センサがどのような値を出力するか観察してください。
次に、ボードを地面に対して平行を保ったまま90度回転させてください。X、Y、Z軸の値はどう変わりましたか? もう一度90度回転させてみましょう。値はどうなりましたか?
キャリブレーション
センサを使ってアプリケーションを作る前に、まずなによりも大切なことがあります。センサの出力が正しいことを確認することです。もし正しくないようなら、センサのキャリブレーションが必要です。(可能性は低いですが、故障しているということもあります。)
筆者がふたつのmicro:bitの磁力計をキャリブレーションせずに試してみたところ、想定される測定値からずいぶん外れていることがわかりました。ですからこの章では、センサはキャリブレーションされるべきものだとします。
キャリブレーションにはかなりの数学(行列)が必要となるので、ここでは詳細は説明しません。もしも処理の仕方に興味があれば、アプリケーションノートに解説があります。
ただ幸いにも、キャリブレーションの仕組みはmicro:bitのソフトウェア開発チームがすでにC++で実装してくれています。これがそのコードです。
このコードのRustへの翻訳は、src/calibration.rs
を見てください。使用例はsrc/main.rs
で確認できます。実際のキャリブレーションの様子は、このビデオを見てください。
要は、LEDマトリクス上のすべてのLEDが点灯するまでmicro:bitを傾ければいいのです。
アプリケーションをリスタートするたびにこの作業をするのは面倒かもしれません。そうであれば、src/main.rs
を修正し、最初のキャリブレーションで得た値をそのまま使い続けてもかまいません。
ではキャリブレーションができたところで、実際にアプリケーションを作りはじめましょう!
課題1
完璧でなくてもいいので、一番簡単にLEDコンパスを実装するとしたらどうするでしょうか?
まず、XとYだけに注目するべきでしょう。コンパスを見るときはいつも水平に持つので、コンパスはXY平面にあるといえるからです。
XとYの値だけに注目すれば、磁場がどの象限に属しているのかわかります。問題は、それぞれの象限がどの方角を指し示しているか、です。それを知るためには、micro:bitを回転させ、違う方角を指したときに象限がどう変化するかを見ればいいです。
しばらく実験すると、micro:bitを北東に向けるとXとYはつねに正だ、といったことがわかるでしょう。この情報を元に、他の象限がどの方角を表しているかわかるはずです。
象限と方角の関係性さえわかれば、下のテンプレートを完成させることができるはずです。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; mod calibration; use crate::calibration::calc_calibration; use crate::calibration::calibrated_measurement; mod led; use led::Direction; use microbit::{display::blocking::Display, hal::Timer}; #[cfg(feature = "v1")] use microbit::{hal::twi, pac::twi0::frequency::FREQUENCY_A}; #[cfg(feature = "v2")] use microbit::{hal::twim, pac::twim0::frequency::FREQUENCY_A}; use lsm303agr::{AccelOutputDataRate, Lsm303agr, MagOutputDataRate}; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_mag_odr(MagOutputDataRate::Hz10).unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz10).unwrap(); let mut sensor = sensor.into_mag_continuous().ok().unwrap(); let calibration = calc_calibration(&mut sensor, &mut display, &mut timer); rprintln!("Calibration: {:?}", calibration); rprintln!("Calibration done, entering busy loop"); loop { while !sensor.mag_status().unwrap().xyz_new_data {} let mut data = sensor.mag_data().unwrap(); data = calibrated_measurement(data, &calibration); let dir = match (data.x > 0, data.y > 0) { // 第I象限 (true, true) => Direction::NorthEast, // 第II象限 ??? (false, true) => panic!("TODO"), // 第III象限 ??? (false, false) => panic!("TODO"), // 第IV象限 ??? (true, false) => panic!("TODO"), }; // ledモジュール(src/led.rs)を利用して、方角をLED矢印に変換してください。 // それから5章で使ったLEDディスプレイ関数を使い、矢印をLEDマトリクス上に表示してください。 } }
解答例1
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; mod calibration; use crate::calibration::calc_calibration; use crate::calibration::calibrated_measurement; mod led; use crate::led::Direction; use crate::led::direction_to_led; use microbit::{display::blocking::Display, hal::Timer}; #[cfg(feature = "v1")] use microbit::{hal::twi, pac::twi0::frequency::FREQUENCY_A}; #[cfg(feature = "v2")] use microbit::{hal::twim, pac::twim0::frequency::FREQUENCY_A}; use lsm303agr::{AccelOutputDataRate, Lsm303agr, MagOutputDataRate}; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_mag_odr(MagOutputDataRate::Hz10).unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz10).unwrap(); let mut sensor = sensor.into_mag_continuous().ok().unwrap(); let calibration = calc_calibration(&mut sensor, &mut display, &mut timer); rprintln!("Calibration: {:?}", calibration); rprintln!("Calibration done, entering busy loop"); loop { while !sensor.mag_status().unwrap().xyz_new_data {} let mut data = sensor.mag_data().unwrap(); data = calibrated_measurement(data, &calibration); let dir = match (data.x > 0, data.y > 0) { // 第I象限 (true, true) => Direction::NorthEast, // 第II象限 (false, true) => Direction::NorthWest, // 第III象限 (false, false) => Direction::SouthWest, // 第IV象限 (true, false) => Direction::SouthEast, }; // ledモジュール(src/led.rs)を利用して、方角をLED矢印に変換。 // それから5章で使ったLEDディスプレイ関数を使い、矢印をLEDマトリクス上に表示。 display.show(&mut timer, direction_to_led(dir), 100); } }
課題2
今回は、磁力計のX、Y軸から、数学を使って磁場の正確な角度を求めます。
atan2
関数を使います。この関数は、-PI
から PI
の範囲で角度を返します。角度がどのように測定されているのかは下図を見てください。
この図では明確に示されていませんが、X軸は右に、Y軸は上に向いています。
これがスターターコードです。ラジアンのtheta
は計算済みです。theta
の値をもとに、どのLEDを点灯するか決めてください。
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
mod calibration;
use crate::calibration::calc_calibration;
use crate::calibration::calibrated_measurement;
mod led;
use crate::led::Direction;
use crate::led::direction_to_led;
// You'll find this useful ;-)
use core::f32::consts::PI;
use libm::atan2f;
use microbit::{display::blocking::Display, hal::Timer};
#[cfg(feature = "v1")]
use microbit::{hal::twi, pac::twi0::frequency::FREQUENCY_A};
#[cfg(feature = "v2")]
use microbit::{hal::twim, pac::twim0::frequency::FREQUENCY_A};
use lsm303agr::{AccelOutputDataRate, Lsm303agr, MagOutputDataRate};
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = microbit::Board::take().unwrap();
#[cfg(feature = "v1")]
let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) };
#[cfg(feature = "v2")]
let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) };
let mut timer = Timer::new(board.TIMER0);
let mut display = Display::new(board.display_pins);
let mut sensor = Lsm303agr::new_with_i2c(i2c);
sensor.init().unwrap();
sensor.set_mag_odr(MagOutputDataRate::Hz10).unwrap();
sensor.set_accel_odr(AccelOutputDataRate::Hz10).unwrap();
let mut sensor = sensor.into_mag_continuous().ok().unwrap();
let calibration = calc_calibration(&mut sensor, &mut display, &mut timer);
rprintln!("Calibration: {:?}", calibration);
rprintln!("Calibration done, entering busy loop");
loop {
while !sensor.mag_status().unwrap().xyz_new_data {}
let mut data = sensor.mag_data().unwrap();
data = calibrated_measurement(data, &calibration);
// atan2はcoreに入っていないので、libmのatan2fを使います。
let theta = atan2f(data.x as f32, data.y as f32);
// thetaの値から、指す方角を決めてください。
let dir = Direction::NorthEast;
display.show(&mut timer, direction_to_led(dir), 100);
}
}
ヒントとアドバイス:
- 円全体の回転角は、360度です。
PI
ラジアンは180度です。theta
がゼロのとき、どの方角を指しますか?theta
がゼロにとても近いとき、どの方角を指しますか?theta
が増え続けるとき、どの値になったら方角を変えますか?
解答例2
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; mod calibration; use crate::calibration::calc_calibration; use crate::calibration::calibrated_measurement; mod led; use crate::led::Direction; use crate::led::direction_to_led; // You'll find this useful ;-) use core::f32::consts::PI; use libm::atan2f; use microbit::{display::blocking::Display, hal::Timer}; #[cfg(feature = "v1")] use microbit::{hal::twi, pac::twi0::frequency::FREQUENCY_A}; #[cfg(feature = "v2")] use microbit::{hal::twim, pac::twim0::frequency::FREQUENCY_A}; use lsm303agr::{AccelOutputDataRate, Lsm303agr, MagOutputDataRate}; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_mag_odr(MagOutputDataRate::Hz10).unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz10).unwrap(); let mut sensor = sensor.into_mag_continuous().ok().unwrap(); let calibration = calc_calibration(&mut sensor, &mut display, &mut timer); rprintln!("Calibration: {:?}", calibration); rprintln!("Calibration done, entering busy loop"); loop { while !sensor.mag_status().unwrap().xyz_new_data {} let mut data = sensor.mag_data().unwrap(); data = calibrated_measurement(data, &calibration); // atan2はcoreに入っていないので、libmのatan2fを使います。 let theta = atan2f(data.x as f32, data.y as f32); // thetaの値から、指す方角を決める。 let dir = if theta < -7. * PI / 8. { Direction::South } else if theta < -5. * PI / 8. { Direction::SouthWest } else if theta < -3. * PI / 8. { Direction::West } else if theta < -PI / 8. { Direction::NorthWest } else if theta < PI / 8. { Direction::North } else if theta < 3. * PI / 8. { Direction::NorthEast } else if theta < 5. * PI / 8. { Direction::East } else if theta < 7. * PI / 8. { Direction::SouthEast } else { Direction::South }; display.show(&mut timer, direction_to_led(dir), 100); } }
磁場の大きさ
これまで磁場の向きについて見てきましたが、実際の大きさはどうでしょうか。ドキュメンテーションによると、mag_data()
関数から得られるx
y
z
の値の単位はナノテスラです。ということは、磁場の大きさをナノテスラで表すにはx
y
z
で表される三次元ベクトルの大きさを計算するだけでいいのです。学校で教わったことをおぼえているかもしれませんね。こう計算します。
#![allow(unused)] fn main() { // この関数もcoreには含まれていないので、atan2fでそうしたようにlibmを使います。 use libm::sqrtf; let magnitude = sqrtf(x * x + y * y + z * z); }
これをプログラムに落とし込みます。
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; mod calibration; use crate::calibration::calc_calibration; use crate::calibration::calibrated_measurement; use libm::sqrtf; use microbit::{display::blocking::Display, hal::Timer}; #[cfg(feature = "v1")] use microbit::{hal::twi, pac::twi0::frequency::FREQUENCY_A}; #[cfg(feature = "v2")] use microbit::{hal::twim, pac::twim0::frequency::FREQUENCY_A}; use lsm303agr::{AccelOutputDataRate, Lsm303agr, MagOutputDataRate}; #[entry] fn main() -> ! { rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_mag_odr(MagOutputDataRate::Hz10).unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz10).unwrap(); let mut sensor = sensor.into_mag_continuous().ok().unwrap(); let calibration = calc_calibration(&mut sensor, &mut display, &mut timer); rprintln!("Calibration: {:?}", calibration); rprintln!("Calibration done, entering busy loop"); loop { while !sensor.mag_status().unwrap().xyz_new_data {} let mut data = sensor.mag_data().unwrap(); data = calibrated_measurement(data, &calibration); let x = data.x as f32; let y = data.y as f32; let z = data.z as f32; let magnitude = sqrtf(x * x + y * y + z * z); rprintln!("{} nT, {} mG", magnitude, magnitude/100.0); } }
このプログラムは磁場の大きさをナノテスラ(nT
)とミリガウス(mG
)で表示します。地球の磁場の大きさは (地理的な場所によって)250 mG
から 650 mG
の範囲に収まります。ですからあなたが測定する値も、その範囲に収まるか、範囲に近いものであるはずです。ちなみに筆者のところでは340 mG
です。
質問:
ボードを動かさないとき、値はどうなっていますか? いつも同じ値ですか?
ボードを回転させるとき、大きさは変化しますか? 変化するべきでしょうか?
パンチングマシン
この章ではボードに載っている加速度計を使って遊んでみます。
なにを作ると思いますか? パンチングマシンです! あなたのジャブの強さを測定します。とは言っても、実際に測定するのは最大加速度です。加速度計が計測するのは加速度ですからね。ですが強さと加速度は比例するので、目安としてはいいでしょう。
これまでの章から、加速度計がLSM303AGRパッケージに内蔵されていることはご存知のことと思います。磁力計と同じように、加速度計にもI2Cバスでアクセスします。また加速度計も、磁力計と同じ座標系を持っています。
重力は上を向いている?
最初にすべきことはなんでしょうか?
センサの基本動作の確認です!
すでにI2Cの章でやりましたから、加速度計の値をRTTコンソールにプリントし続けるのはできるはずです。LEDの側を下に向け、地面に対して水平に持つとき、なにかおもしろい現象を確認できるでしょうか?
XとYの値は両方とも0に近い値で、Zは1000に近い値となっているはずです。ボードが動いていないのに、加速度がゼロではないとは変です。なにが起きているのでしょうか? 重力に関係している、ですよね? そうです、重力の加速度が1 g
(1 g
= 加速度計が報告する1000)だからです。ですが、重力は物体を下に引くものです。そうするとZ軸の加速度は、正ではなく負であるべきではないでしょうか。
プログラムがZ軸を逆に扱っているのでしょうか? 違います。ボードを回転させて、重力がX軸とY軸に添うように持っても、センサで測定された加速度はつねに上を指しています。
なにが起きているかと言うと、加速度計はあなたが観測する加速度ではなく、ボードの固有加速度を測定しているのです。固有加速度とは、自由落下中の観測者から見たボードの加速度です。自由落下中の観察者は地球の中心に向けて1g
の加速度で動いており、この観察者の視点からすると、ボードは1g
の加速度で上へ動いている(地球の中心から離れていっている)ように見えます。これが、固有加速度が上を向いている理由です。これはつまり、もしもボードが自由落下していれば、加速度計は固有加速度をゼロと報告することを意味します。家では試さないようにしてください。
物理は難しいですね。先に進みましょう。
課題
話を簡単にするために、ここではボードを水平に保った状態でX軸の加速度だけを測ることにします。そうすることで、前のページで確認した架空の 1g
を引く必要がなくなります。架空の1g
はボードの向きによってX Y Zのどれにでも現れるので、補正するのはむずかしいです。
パンチングマシンがやるべきことは、次の通りです。
- デフォルトでは、アプリケーションはボードの加速度を「観測」していません。
- X軸に大きな(しきい値を超える)加速度が感知されたとき、アプリケーションは計測をはじめます。
- 計測中、アプリケーションは最大加速度を記録、更新し続けます。
- 計測期間が終わると、アプリケーションは最大加速度を報告します。報告は
rprintln!
マクロを使って行います。
では実装して、あなたのパンチがどのくらい強いか教えてください。;-)
注 まだ紹介していないAPIに、便利なものがふたつあります。 ひとつ目は
set_accel_scale
。大きなg
を測定するときに必要になります。 ふたつ目はembedded_hal
のCountdown
トレイトです。これを計測時間のカウントダウンに使用する場合は、これまでの章でしたようにblock!
マクロを使うのではなく、nb::Result
型をパターンマッチで処理する必要があります。
解答例
#![deny(unsafe_code)] #![no_main] #![no_std] use cortex_m_rt::entry; use rtt_target::{rtt_init_print, rprintln}; use panic_rtt_target as _; #[cfg(feature = "v1")] use microbit::{ hal::twi, pac::twi0::frequency::FREQUENCY_A, }; #[cfg(feature = "v2")] use microbit::{ hal::twim, pac::twim0::frequency::FREQUENCY_A, }; use lsm303agr::{ AccelScale, AccelOutputDataRate, Lsm303agr, }; use microbit::hal::timer::Timer; use microbit::hal::prelude::*; use nb::Error; #[entry] fn main() -> ! { const THRESHOLD: f32 = 0.5; rtt_init_print!(); let board = microbit::Board::take().unwrap(); #[cfg(feature = "v1")] let i2c = { twi::Twi::new(board.TWI0, board.i2c.into(), FREQUENCY_A::K100) }; #[cfg(feature = "v2")] let i2c = { twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100) }; let mut countdown = Timer::new(board.TIMER0); let mut delay = Timer::new(board.TIMER1); let mut sensor = Lsm303agr::new_with_i2c(i2c); sensor.init().unwrap(); sensor.set_accel_odr(AccelOutputDataRate::Hz50).unwrap(); // 16Gまで計測できるようにセンサの設定を変更。 // 人のパンチは案外速いものです。 sensor.set_accel_scale(AccelScale::G16).unwrap(); let mut max_g = 0.; let mut measuring = false; loop { while !sensor.accel_status().unwrap().xyz_new_data {} // X軸の加速度をg単位で取得 let g_x = sensor.accel_data().unwrap().x as f32 / 1000.0; if measuring { // contdownタイマのステータスをチェック match countdown.wait() { // countdownはまだ終わっていない Err(Error::WouldBlock) => { if g_x > max_g { max_g = g_x; } }, // countdown終了 Ok(_) => { // 最大加速度を報告 rprintln!("Max acceleration: {}g", max_g); // リセット max_g = 0.; measuring = false; }, // nrf52とnrf51のHALはエラー型としてVoidを返します。 // VoidはEmpty型なので、ここにたどり着くことはありません。 Err(Error::Other(_)) => { unreachable!() } } } else { // 加速度がしきい値を超えれば測定を開始 if g_x > THRESHOLD { rprintln!("START!"); measuring = true; max_g = g_x; // ドキュメンテーションによると、タイマは1Mhzで動作します。 // よって、1秒待つためには1_000_000ティック必要です。 countdown.start(1_000_000_u32); } } delay.delay_ms(20_u8); } }
もっと楽しむために
組込みRustの入門を終えましたが、旅はまだ始まったばかりです。この先にもっと楽しい世界が広がっています!
注: この本に貢献していただけるならうれしいです! 以下の項目や、ほかにも組込みに関するトピックについて、サンプルコードや課題を追加する作業をお手伝い頂ける方はぜひ手を貸してください。
貢献するにあたってガイドが必要な場合は、イシューを立ててください。ご自分で情報を追加できるようなら、プルリクエストを送ってくださっても結構です。
組込みソフトウェアができること
以下は、組込みソフトウェアを書くにあたっての設計にかかわるトピックです。多くの問題は、いろいろなやり方で解決できます。ここではいくつかのやり方を紹介し、それぞれの向き不向きについて解説します。
マルチタスク
私たちが作ったプログラムはすべてシングルタスクでした。もしもOSもスレッドもない環境でマルチタスクを実現するとしたら、どうするでしょうか。よく使われる方法がふたつあります。プリエンプティブ・マルチタスクと協調的マルチタスクです。
プリエンプティブ・マルチタスクでは、現在実行中のタスクは他のタスクにいつでもプリエンプトされる(強制的に差し替えられる)可能性があります。もともとのタスクは一時停止され、プロセッサは別のタスクを実行します。そしてあとになってから最初のタスクを再開します。マイクロコントローラは割り込みのかたちでプリエンプションをハードウェアサポートしています。
協調的マルチタスクでは、実行中のタスクは中断点に到達するまで実行を続けます。そしてその中断点に到達すると、タスクを一時停止し、別のタスクを実行します。そしてあとになって最初のタスクを再開します。ふたつのマルチタスクの大きな違いは、協調的マルチタスクは実行をいつでも強制的に差し替えるのではなく、あらかじめ知られた中断点において実行制御を譲るということです。
スリープ
この本のすべてのプログラムは、新しい処理が必要かどうかペリフェラルをポーリングしていました。ですが、なにもする必要がないときだってあります。そんなときにはマイクロコントローラは「スリープ」すべきです。
プロセッサがスリープすると、命令の実行が止まり、電力を節約できます。マイクロコントローラはできるかぎりスリープさせるべきです。ですが、どうやっていつ起きればよいと判断するのでしょうか? 「割り込み」(詳細は下をご覧ください)でマイクロコントローラを起こすことができますが、他にも方法があります。また、プロセッサを「スリープ」させる命令はwfi
とwfe
となります。
マイクロコントローラができること
nRF52やnRF51などのマイクロコントローラにはできることがたくさんあります。そのなかでも、さまざまな問題を解決するためによく使われる機能があります。
以下では、そういった機能を組込み開発でいかに効果的に使うかを解説します。
ダイレクトメモリアクセス (DMA)
このペリフェラルは非同期の memcpy
と言っていいでしょう。もしもmicro:bit v2をお使いでしたら、すでにDMAを使ったことになります。なぜならHALがUARTEとTWIMペリフェラルにこの機能を利用しているからです。DMAペリフェラルは、まとまったデータの転送に使われます。RAMからRAMへも、UARTEのようなペリフェラルからRAMへも、RAMからペリフェラルへも使えます。たとえば256バイトをUARTEからバッファに読み込むとしましょう。DMA転送をスケジュールすれば、バックグラウンドで処理を行うことができます。処理の終了はレジスタをポーリングすることで確認し、転送作業中はその終了を待たずに他のタスクを実行できるのです。これがどうやって実装されているのか興味があれば、UART の章で見たserial_setup
モジュールをのぞいてみてください。それでも満足できなければ、nrf52-hal
のコードをどうぞ。
割り込み
マイクロコントローラが実世界とやり取りするとき、なにかイベントが起きたときに即座に反応しなくてはならないことがよくあります。
マイクロコントローラには割り込み機能があります。つまりなにかイベントが起きたときに、実行中の処理を中断し、そのイベントに反応するのです。たとえば、ボタンが押されると即座にモーターを止めたいとか、タイマのカウントダウンが終わったらセンサに測定させたいとかいったときに便利です。
割り込みはとても便利なものですが、適切に扱うのは簡単ではありません。イベントにすばやく反応はしたいのですが、他の処理も続けたいところです。
Rustでは、デスクトップアプリケーションにおけるスレッドの概念に似せて割り込みを設計しています。それはつまり、割り込み処理を実行するコードとメインアプリケーションとでデータを共有するとき、Send
とSync
についてよく考えなくてはならないということも意味します。
パルス幅変調 (PWM)
簡単に言うと、PWMとは「オンの時間」と「オフの時間」を一定の割合(デューティー比)で保ちながら、周期的にオン、オフを繰り返すことです。十分高い周波数でこれをLEDに使うと、調光することもできます。低いデューティー比、たとえばオンの時間10%、オフの時間90%ではLEDはとても暗く光ります。それに対し、オンの時間90%、オフの時間10%といった高いデューティー比ではLEDはずっと明るく(ほとんど全灯に思えるほどに)なります。
一般的に、PWMはどれだけの電力を電子機器に与えるかを制御するのに使われます。適切な駆動回路を挟めば、PWMを利用してマイクロコントローラでモーターの操作もできます。どれだけの電力を供給するかを調整することで、モーターのトルクとスピードを制御できるのです。さらに角度センサを加えれば、モーターを自動制御するシステムを作ることもできます。
PWMはすでにembedded-hal
Pwm
トレイトによって抽象化されています。その実装はnrf52-hal
をご覧ください。
デジタル入力
この本では、LEDを制御するためにマイクロコントローラのピンをデジタル出力として扱ってきました。ですが、ピンはデジタル入力として設定することもできます。デジタル入力ピンは、スイッチ(オン/オフ)やボタン(押された/押されていない)の二値状態を読むことができます。
デジタル入力もまたembedded-hal
InputPin
トレイトで抽象化されています。もちろんnrf52-hal
がそれを実装しています。
(ネタバレ スイッチやボタンの二値状態を読むのは案外簡単ではありません。;-) )
アナログデジタルコンバータ (ADC)
世の中にはたくさんのデジタルセンサがあり、I2CやSPIプロトコルを使ってセンサからデータを読み取ります。ですが、アナログセンサもあります! これらのセンサは検知するものの大きさに比例した電圧を出力します。
ADCペリフェラルは、たとえば1.25
ボルトといった「アナログな」電圧レベルを、プロセッサが計算に使える[0, 65535]
に収まる「デジタルな」数値に置き換えます。
ここでもまた、embedded-hal
adc
モジュールとnrf52-hal
が抽象化と実装をしてくれています。
デジタルアナログコンバータ (DAC)
お気づきかもしれませんが、DACはADCの反対です。デジタルな値をレジスタに書き込むことで、[0, 3.3V]
(3.3V
電源と想定)の範囲に収まる電圧を「アナログ」ピンから出力することができます。このアナログピンを適切な回路に接続し、レジスタに一定の高い周波数で適当な値を書き込めば、音を発生させることも音楽を奏でることだってできます!
リアルタイムクロック (RTC)
このペリフェラルは、「人にわかりやすいかたちで」時間を追ってくれます。「ティック」を秒、分、時、日、月、年といった単位に変換します。うるう年や夏時間にも対応できます!
その他の通信プロトコル
- SPI:
embedded-hal
spi
モジュールによって抽象化、nrf52-hal
にて実装されてます。 - I2S:現時点では
embedded-hal
による抽象化はされていませんが、nrf52-hal
が実装しています。 - Ethernet:
smoltcp
という軽量のTCP/IPスタックがあり、いくつかのチップでは実装されています。ですが、micro:bitのチップにはEthernetペリフェラルがありません。 - USB:
usb-device
クレートなど、実験的な試みはあります。 - Bluetooth:開発途中のものですが、
rubble
というBLEスタックがあり、nrfチップもサポートしています。 - SMBUS:現時点では、
embedded-hal
による抽象化もnrf52-hal
による実装もされていません。 - CAN:現時点では、
embedded-hal
による抽象化もnrf52-hal
による実装もされていません。 - IrDA:現時点では、
embedded-hal
による抽象化もnrf52-hal
による実装もされていません。
アプリケーションによって使用する通信プロトコルは変わってきます。ユーザーとのインターフェースになるアプリケーションは通常USBコネクターを持っています。USBがPCやスマートフォンのありとあらゆるところで使われているプロトコルだからです。それに対し、車のなかはCAN「バス」であふれていることでしょう。デジタルセンサによってSPIを使ったり、I2Cを使ったり、SMBUSを使うものもあります。
もしもembedded-hal
での抽象化作業や、ペリフェラルの実装に興味があれば、遠慮せずにHALのリポジトリでイシューを立ててください。また、Rust Embedded matrix channelに参加してもらってもいいです。上記のモジュールの開発者のほとんどとやり取りできます。
組込みシステム一般についてのトピック
以下は、micro:bitやその上に載っているハードウェアについてではなく、組込みシステム開発における便利なテクニックを紹介します。
ジャイロスコープ
パンチングマシンの課題では、加速度計を使って三軸における加速度の変化を計測しました。しかし、モーションセンサは他にもあります。ジャイロスコープはそのひとつで、「回転」を三次元で計測することができます。
この機能は、たとえばロボットの転倒を防ぎたいときなどにはとても便利です。さらに、ジャイロスコープからのデータと加速度計からのデータとを合わせて、センサフュージョンと呼ばれるテクニックを使うこともできます。(詳細は下を参照してください)
サーボモーターとステッピングモーター
たとえばリモコンカーを進めたりバックさせたりと、モーターはただ一方向に回ればいいというときもありますが、ときにはモーターを決まった角度だけ正確に動かしたいこともあります。
より正確な操作が可能なサーボモーターとステッピングモーターというものがあります。マイクロコントローラを使い、決まった方向に決まった角度だけ動かしたり、あるポジションで止めることができます。これを利用して、たとえば時計の針を動かすとかいったことができます。
センサフュージョン
micro:bitはふたつのモーションセンサを搭載しています。加速度計と磁力計です。それぞれは(固有)加速度と、(地球の)磁場を計測しています。ですが、これらの大きさを「まとめて」もっと有用なデータとすることができます。つまり、ボードの向きについて「信頼性の高い」計測をすることができます。ここでの「信頼性を高める」とは、ひとつのセンサでは防ぐことのできないエラーを減らすという意味です。
このように異なるセンサからのデータを集めてより信頼性の高いデータを取得することを、センサフュージョンと呼びます。
さて、次はなにに取り組みましょうか? いくつか提案です。
microbit
ボードサポートクレートについてくるサンプルコードを試してみるといいかもしれません。コードはすべてお持ちのmicro:bitで動作します。
- Rust Embedded matrix channelに参加するのもいいかもしれません。組込みソフトウェアに貢献している開発者たちが集まっています。
microbit
のBSP、nrf52-hal
、embedded-hal
などを書いた人たちです。
- 組込みRustで今すぐ使えるものの一覧をお探しなら、Awesome Rust Embeddedリストがおすすめです。
- Real-Time Interrupt-driven Concurrencyを試してみてもいいでしょう。非常に効率的なプリエンプティブ・マルチタスク・フレームワークです。タスクの優先順位づけとデッドロックのない実行をサポートしてくれます。
embedded-hal
による抽象化をさらに深く学ぶのもいいかもしれません。また、それを利用してプラットフォームに依存しないドライバをあなた自身で書いてみるのもいいでしょう。
- Rustを別の開発ボードで走らせてもいいでしょう。一番簡単な始め方は、
cortex-m-quickstart
というプロジェクトテンプレートを使うことです。
- このモーションセンサのデモを試すこともできます。実装の詳細とソースコードはこのブログ記事にあります。
- Rustの型システムがいかにI/O設定におけるバグを防ぐかについて、このブログ記事が解説してくれています。
- japaric氏のブログは組込みRustについて幅広いトピックを扱っています。
- Weekly driver initiativeに参加するのもいいでしょう。
embedded-hal
トレイトを利用して、多くのプラットフォーム(ARM Cortex-M, AVR, MSP430, RISCVなど)で動作する汎用性の高いドライバを開発してみませんか。
トラブルシューティング
cargo-embed
の問題
cargo-embed
に関した問題のほとんどは、Embed.toml
で間違ったチップを選択しているか、(Linuxの場合)udev
ルールを適切にインストールしていないかが原因です。ですから、その両方が正しく設定されていることを確認してください。
それでもまだうまくいかないときは、discovery
issue trackerでイシューを立ててください。またRust Embedded matrix channelやprobe-rs matrix channelを訪れて、そこで質問していただいてもよいです。
Cargoの問題
"can't find crate for core
"
症状
Compiling volatile-register v0.1.2
Compiling rlibc v1.0.0
Compiling r0 v0.1.0
error[E0463]: can't find crate for `core`
error: aborting due to previous error
error[E0463]: can't find crate for `core`
error: aborting due to previous error
error[E0463]: can't find crate for `core`
error: aborting due to previous error
Build failed, waiting for other jobs to finish...
Build failed, waiting for other jobs to finish...
error: Could not compile `r0`.
To learn more, run the command again with --verbose.
原因
お使いのマイクロコントローラ向けのターゲットがインストールされていません。(v2にはthumbv7em-none-eabihf
、v1にはthumbv6m-none-eabi
)
解決策
適切なターゲットをインストールします。
# micro:bit v2
$ rustup target add thumbv7em-none-eabihf
# micro:bit v1
$ rustup target add thumbv6m-none-eabi
How to use GDB
Below are some useful GDB commands that can help us debug our programs. This assumes you have flashed a program onto your microcontroller and attached GDB to a cargo-embed
session.
General Debugging
NOTE: Many of the commands you see below can be executed using a short form. For example,
continue
can simply be used asc
, orbreak $location
can be used asb $location
. Once you have experience with the commands below, try to see how short you can get the commands to go before GDB doesn't recognize them!
Dealing with Breakpoints
break $location
: Set a breakpoint at a place in your code. The value of$location
can include:break *main
- Break on the exact address of the functionmain
break *0x080012f2
- Break on the exact memory location0x080012f2
break 123
- Break on line 123 of the currently displayed filebreak main.rs:123
- Break on line 123 of the filemain.rs
info break
: Display current breakpointsdelete
: Delete all breakpointsdelete $n
: Delete breakpoint$n
(n
being a number. For example:delete $2
)
clear
: Delete breakpoint at next instructionclear main.rs:$function
: Delete breakpoint at entry of$function
inmain.rs
clear main.rs:123
: Delete breakpoint on line 123 ofmain.rs
enable
: Enable all set breakpointsenable $n
: Enable breakpoint$n
disable
: Disable all set breakpointsdisable $n
: Disable breakpoint$n
Controlling Execution
continue
: Begin or continue execution of your programnext
: Execute the next line of your programnext $n
: Repeatnext
$n
number times
nexti
: Same asnext
but with machine instructions insteadstep
: Execute the next line, if the next line includes a call to another function, step into that codestep $n
: Repeatstep
$n
number times
stepi
: Same asstep
but with machine instructions insteadjump $location
: Resume execution at specified location:jump 123
: Resume execution at line 123jump 0x080012f2
: Resume execution at address 0x080012f2
Printing Information
print /$f $data
- Print the value contained by the variable$data
. Optionally format the output with$f
, which can include:x: hexadecimal d: signed decimal u: unsigned decimal o: octal t: binary a: address c: character f: floating point
print /t 0xA
: Prints the hexadecimal value0xA
as binary (0b1010)
x /$n$u$f $address
: Examine memory at$address
. Optionally,$n
define the number of units to display,$u
unit size (bytes, halfwords, words, etc.),$f
anyprint
format defined abovex /5i 0x080012c4
: Print 5 machine instructions staring at address0x080012c4
x/4xb $pc
: Print 4 bytes of memory starting where$pc
currently is pointing
disassemble $location
disassemble /r main
: Disassemble the functionmain
, using/r
to show the bytes that make up each instruction
Looking at the Symbol Table
info functions $regex
: Print the names and data types of functions matched by$regex
, omit$regex
to print all functionsinfo functions main
: Print names and types of defined functions that contain the wordmain
info address $symbol
: Print where$symbol
is stored in memoryinfo address GPIOC
: Print the memory address of the variableGPIOC
info variables $regex
: Print names and types of global variables matched by$regex
, omit$regex
to print all global variablesptype $data
: Print more detailed information about$data
ptype cp
: Print detailed type information about the variablecp
Poking around the Program Stack
backtrace $n
: Print trace of$n
frames, or omit$n
to print all framesbacktrace 2
: Print trace of first 2 frames
frame $n
: Select frame with number or address$n
, omit$n
to display current frameup $n
: Select frame$n
frames updown $n
: Select frame$n
frames downinfo frame $address
: Describe frame at$address
, omit$address
for currently selected frameinfo args
: Print arguments of selected frameinfo registers $r
: Print the value of register$r
in selected frame, omit$r
for all registersinfo registers $sp
: Print the value of the stack pointer register$sp
in the current frame
Controlling cargo-embed
Remotely
monitor reset
: Reset the CPU, starting execution over again