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バイナリでも、マルチアーキテクチャに対応しているものがあります。 このことについては、サブチャプターにも詳しく記載しています。
  • 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

ボードには多くの部品が搭載されています。

訳注: 日本語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キロバイトのRAM
  • A0はビルドコードで、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キロバイトのRAM
  • H3はビルドコードで、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.tomlcargo-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つの役割を果たします。

  1. MCUにUSBコネクタからの電源を供給する
  2. MCUのシリアルとUSBのブリッジ機能を提供する(後の章で説明します)
  3. プログラマー/デバッガーになる(これが今のお目当てです)

基本的にこのチップは私たちのコンピュータ(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 buildcargo embedとに同じフラグが渡されていることにも気づくでしょう。 これはcargo embedがビルドを実行して、そのあとチップにビルド結果のバイナリを書き込んでいるためです。 そのため、新しいコードをすぐにフラッシュに書き込みたいときはcargo buildを省略できます。

デバッグする

どのような仕組みなのか?

小さなプログラムをデバッグする前に、少し寄り道をして何が起こるのか簡単に理解しましょう。 前章でボード上の2つ目のチップの役割とどうやって私たちのコンピュータとやり取りするか、を説明しましたが果たしてどうやって使うのでしょう?

Embed.tomldefault.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セッション

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ソースコード画面にいつでも戻ってくることができます。

注意: 間違ってnextcontinueコマンドを使ってしまいGDBが操作できなくなった場合は、Ctrl+Cで再びGDBを操作できます。

(gdb) layout asm

GDB session

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セクションは、プログラムのインストラクションを格納しています。一方databssセクションは、静的に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 Summary
  • Ctrl+A + C. Clear the screen
  • Ctrl+A + X. Exit and reset
  • Ctrl+A + Q. Quit with no reset

NOTE Mac users: In the above commands, replace Ctrl+A with Meta.

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

マスター -> スレーブ

マスターがスレーブにデータを送りたいとき、次のようになります。

  1. M(マスター):STARTをブロードキャストする
  2. M:スレーブアドレス(上位7ビット)と、WRITEに設定したR/Wビット(第8ビット)をブロードキャストする
  3. S(スレーブ):ACK(ACKnowledgement)を返信する
  4. M:1バイト送信する
  5. S:ACKを返信する
  6. 必要なだけ4と5を繰り返す
  7. M:STOPをブロードキャストする(または、RESTARTをブロードキャストして2へ戻る)

スレーブアドレスが7ビット長でなく10ビット長のときもありますが、他はなにも変わりません。

マスター <- スレーブ

マスターがスレーブからデータを読み取りたいときはこうなります。

  1. M:STARTをブロードキャストする
  2. M:スレーブアドレス(上位7ビット)と、READに設定したR/Wビット(第8ビット)をブロードキャストする
  3. S:ACK(ACKnowledgement)を返信する
  4. S:1バイト送信する
  5. M:ACKを返答する
  6. 必要なだけ4と5を繰り返す
  7. 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はアドレス0x0fWHO_AM_I_A(Aは加速度計を指す)、アドレス0x4fWHO_AM_I_M(Mは磁力計を指す)というふたつのレジスタを提供しており、それぞれにはデバイス特有の値が入っています。

ここまできたらあとはソフトウェアです。つまり、microbitHALのどの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 g1 g= 加速度計が報告する1000)だからです。ですが、重力は物体を下に引くものです。そうするとZ軸の加速度は、正ではなく負であるべきではないでしょうか。

プログラムがZ軸を逆に扱っているのでしょうか? 違います。ボードを回転させて、重力がX軸とY軸に添うように持っても、センサで測定された加速度はつねに上を指しています。

なにが起きているかと言うと、加速度計はあなたが観測する加速度ではなく、ボードの固有加速度を測定しているのです。固有加速度とは、自由落下中の観測者から見たボードの加速度です。自由落下中の観察者は地球の中心に向けて1gの加速度で動いており、この観察者の視点からすると、ボードは1gの加速度で上へ動いている(地球の中心から離れていっている)ように見えます。これが、固有加速度が上を向いている理由です。これはつまり、もしもボードが自由落下していれば、加速度計は固有加速度をゼロと報告することを意味します。家では試さないようにしてください。

物理は難しいですね。先に進みましょう。

課題

話を簡単にするために、ここではボードを水平に保った状態でX軸の加速度だけを測ることにします。そうすることで、前のページで確認した架空の 1gを引く必要がなくなります。架空の1gはボードの向きによってX Y Zのどれにでも現れるので、補正するのはむずかしいです。

パンチングマシンがやるべきことは、次の通りです。

  • デフォルトでは、アプリケーションはボードの加速度を「観測」していません。
  • X軸に大きな(しきい値を超える)加速度が感知されたとき、アプリケーションは計測をはじめます。
  • 計測中、アプリケーションは最大加速度を記録、更新し続けます。
  • 計測期間が終わると、アプリケーションは最大加速度を報告します。報告はrprintln!マクロを使って行います。

では実装して、あなたのパンチがどのくらい強いか教えてください。;-)

まだ紹介していないAPIに、便利なものがふたつあります。 ひとつ目はset_accel_scale。大きなgを測定するときに必要になります。 ふたつ目はembedded_halCountdownトレイトです。これを計測時間のカウントダウンに使用する場合は、これまでの章でしたように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もスレッドもない環境でマルチタスクを実現するとしたら、どうするでしょうか。よく使われる方法がふたつあります。プリエンプティブ・マルチタスクと協調的マルチタスクです。

プリエンプティブ・マルチタスクでは、現在実行中のタスクは他のタスクにいつでもプリエンプトされる(強制的に差し替えられる)可能性があります。もともとのタスクは一時停止され、プロセッサは別のタスクを実行します。そしてあとになってから最初のタスクを再開します。マイクロコントローラは割り込みのかたちでプリエンプションをハードウェアサポートしています。

協調的マルチタスクでは、実行中のタスクは中断点に到達するまで実行を続けます。そしてその中断点に到達すると、タスクを一時停止し、別のタスクを実行します。そしてあとになって最初のタスクを再開します。ふたつのマルチタスクの大きな違いは、協調的マルチタスクは実行をいつでも強制的に差し替えるのではなく、あらかじめ知られた中断点において実行制御を譲るということです。

スリープ

この本のすべてのプログラムは、新しい処理が必要かどうかペリフェラルをポーリングしていました。ですが、なにもする必要がないときだってあります。そんなときにはマイクロコントローラは「スリープ」すべきです。

プロセッサがスリープすると、命令の実行が止まり、電力を節約できます。マイクロコントローラはできるかぎりスリープさせるべきです。ですが、どうやっていつ起きればよいと判断するのでしょうか? 「割り込み」(詳細は下をご覧ください)でマイクロコントローラを起こすことができますが、他にも方法があります。また、プロセッサを「スリープ」させる命令はwfiwfeとなります。

マイクロコントローラができること

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では、デスクトップアプリケーションにおけるスレッドの概念に似せて割り込みを設計しています。それはつまり、割り込み処理を実行するコードとメインアプリケーションとでデータを共有するとき、SendSyncについてよく考えなくてはならないということも意味します。

パルス幅変調 (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-halembedded-halなどを書いた人たちです。
  • 組込みRustで今すぐ使えるものの一覧をお探しなら、Awesome Rust Embeddedリストがおすすめです。
  • Real-Time Interrupt-driven Concurrencyを試してみてもいいでしょう。非常に効率的なプリエンプティブ・マルチタスク・フレームワークです。タスクの優先順位づけとデッドロックのない実行をサポートしてくれます。
  • embedded-halによる抽象化をさらに深く学ぶのもいいかもしれません。また、それを利用してプラットフォームに依存しないドライバをあなた自身で書いてみるのもいいでしょう。
  • Rustを別の開発ボードで走らせてもいいでしょう。一番簡単な始め方は、cortex-m-quickstartというプロジェクトテンプレートを使うことです。
  • Rustの型システムがいかにI/O設定におけるバグを防ぐかについて、このブログ記事が解説してくれています。
  • 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 channelprobe-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 as c, or break $location can be used as b $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 function main
    • break *0x080012f2 - Break on the exact memory location 0x080012f2
    • break 123 - Break on line 123 of the currently displayed file
    • break main.rs:123 - Break on line 123 of the file main.rs
  • info break: Display current breakpoints
  • delete: Delete all breakpoints
    • delete $n: Delete breakpoint $n (n being a number. For example: delete $2)
  • clear: Delete breakpoint at next instruction
    • clear main.rs:$function: Delete breakpoint at entry of $function in main.rs
    • clear main.rs:123: Delete breakpoint on line 123 of main.rs
  • enable: Enable all set breakpoints
    • enable $n: Enable breakpoint $n
  • disable: Disable all set breakpoints
    • disable $n: Disable breakpoint $n

Controlling Execution

  • continue: Begin or continue execution of your program
  • next: Execute the next line of your program
    • next $n: Repeat next $n number times
  • nexti: Same as next but with machine instructions instead
  • step: Execute the next line, if the next line includes a call to another function, step into that code
    • step $n: Repeat step $n number times
  • stepi: Same as step but with machine instructions instead
  • jump $location: Resume execution at specified location:
    • jump 123: Resume execution at line 123
    • jump 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 value 0xA 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 any print format defined above
    • x /5i 0x080012c4: Print 5 machine instructions staring at address 0x080012c4
    • x/4xb $pc: Print 4 bytes of memory starting where $pc currently is pointing
  • disassemble $location
    • disassemble /r main: Disassemble the function main, 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 functions
    • info functions main: Print names and types of defined functions that contain the word main
  • info address $symbol: Print where $symbol is stored in memory
    • info address GPIOC: Print the memory address of the variable GPIOC
  • info variables $regex: Print names and types of global variables matched by $regex, omit $regex to print all global variables
  • ptype $data: Print more detailed information about $data
    • ptype cp: Print detailed type information about the variable cp

Poking around the Program Stack

  • backtrace $n: Print trace of $n frames, or omit $n to print all frames
    • backtrace 2: Print trace of first 2 frames
  • frame $n: Select frame with number or address $n, omit $n to display current frame
  • up $n: Select frame $n frames up
  • down $n: Select frame $n frames down
  • info frame $address: Describe frame at $address, omit $address for currently selected frame
  • info args: Print arguments of selected frame
  • info registers $r: Print the value of register $r in selected frame, omit $r for all registers
    • info 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