Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Discovery

Rust を通じてマイクロコントローラーの世界を発見しましょう!

この本は、通常の C/C++ ではなく Rust を学習言語として使用する、マイクロコントローラーベースの組み込みシステムに関する入門コースです。

範囲

次のトピックを扱う予定です(いずれは、という希望ですが):

  • 「組み込み」(Rust) プログラムの書き方、ビルド、書き込み、デバッグの方法。

  • マイクロコントローラーで一般的に見られる機能(「ペリフェラル」): デジタル入出力、パルス幅変調 (PWM)、アナログ-デジタルコンバーター (ADC)、Serial、I2C、SPI などの一般的な通信プロトコル、など。

  • マルチタスクの概念: 協調型とプリエンプティブ型のマルチタスク、割り込み、スケジューラー、など。

  • 制御システムの概念: センサー、キャリブレーション、デジタルフィルター、アクチュエーター、開ループ制御、閉ループ制御、など。

アプローチ

  • 初心者向けです。マイクロコントローラーや組み込みシステムの事前経験は必要ありません。

  • 実践重視です。理論を実践に移すための演習を豊富に用意しています。ここでは あなた が作業の大半を行います。

  • ツール中心です。開発を容易にするために、さまざまなツールを積極的に活用します。GDB を使った「本物の」デバッグやロギングを早い段階で導入します。デバッグ手段として LED を使うようなやり方は、ここでは出番がありません。

非目標

この本で扱わないものは次のとおりです:

  • Rust を教えること。この話題については、すでに豊富な教材があります。ここではマイクロコントローラーと組み込みシステムに焦点を当てます。

  • 電気回路理論や電子工学についての包括的なテキストであること。いくつかのデバイスがどのように動作するかを理解するために必要な最小限だけを扱います。

  • リンカスクリプトやブートプロセスのような詳細を扱うこと。たとえば、コードをボードに書き込むのを助ける既存のツールは使用しますが、それらのツールがどのように動作するかの詳細には立ち入りません。

また、この教材を他の開発ボード向けに移植するつもりもありません。この本では micro:bit 開発ボードのみを使用します。

問題の報告

この本のソースは this repository にあります。誤字やコードの問題を見つけた場合は、issue tracker で報告してください。

その他の組み込み Rust リソース

この Discovery 本は、Embedded Working Group が提供する複数の組み込み Rust リソースの 1 つにすぎません。全体の一覧は The Embedded Rust Bookshelf で確認できます。これには Frequently Asked Questions の一覧も含まれます。

背景

マイクロコントローラーとは?

マイクロコントローラーは、1つのチップ上に載ったシステムです。みなさんのコンピューターは、プロセッサー、RAM、ストレージ、Ethernet ポートなど、複数の個別部品で構成されています。一方、マイクロコントローラーは、そうした種類の部品をすべて単一の「チップ」またはパッケージに内蔵しています。これにより、より少ない部品でシステムを構築できます。

マイクロコントローラーで何ができますか?

いろいろなことができます! マイクロコントローラーは、いわゆる「組み込みシステム」の中核を成します。組み込みシステムは至るところにありますが、普段は意識しないことがほとんどです。衣類を洗う機械、文書を印刷する機械、食べ物を調理する機械は、組み込みシステムによって制御されています。組み込みシステムは、みなさんが住み、働く建物を快適な温度に保ち、みなさんが移動に使う乗り物を走らせたり止めたりする部品も制御しています。

ほとんどの組み込みシステムは、ユーザーの介入なしに動作します。洗濯機のようにユーザーインターフェースを備えている場合でも、その動作の大部分は自律的に行われます。

組み込みシステムは、物理的なプロセスを制御するためによく使われます。これを可能にするために、組み込みシステムには、世界の状態を知らせる1つ以上のデバイス(「センサー」)と、何かを変化させることを可能にする1つ以上のデバイス(「アクチュエーター」)があります。たとえば、建物の空調制御システムには次のようなものがあるでしょう。

  • さまざまな場所の温度と湿度を測定するセンサー。
  • ファンの速度を制御するアクチュエーター。
  • 建物に熱を加えたり、建物から熱を取り除いたりするアクチュエーター。

どのような場合にマイクロコントローラーを使うべきですか?

上に挙げた組み込みシステムの多くは、Linux を実行するコンピューター(たとえば「Raspberry Pi」)でも実装できます。それなのに、なぜ代わりにマイクロコントローラーを使うのでしょうか。プログラムを開発するのは、むしろ難しそうに思えます。

理由としては、次のようなものがあります。

コスト。 マイクロコントローラーは、汎用コンピューターよりもはるかに安価です。マイクロコントローラー自体が安いだけでなく、動作に必要な外部の電気部品もずっと少なくて済みます。そのため、プリント回路基板(PCB)はより小さくなり、設計や製造のコストも下がります。

消費電力。 ほとんどのマイクロコントローラーの消費電力は、本格的なプロセッサーのごく一部です。バッテリーで動作するアプリケーションでは、これは非常に大きな違いになります。

応答性。 その目的を果たすために、ある種の組み込みシステムは、常に限られた時間内に反応しなければなりません(たとえば、自動車の「アンチロック」ブレーキシステム)。システムがこの種のデッドラインを守れないと、致命的な障害が発生する可能性があります。そのようなデッドラインは「ハードリアルタイム」要件と呼ばれます。このようなデッドラインに縛られる組み込みシステムは、「ハードリアルタイムシステム」と呼ばれます。汎用コンピューターや OS には通常、コンピューターの処理資源を共有する多くのソフトウェアコンポーネントがあります。そのため、厳しい時間制約の中でプログラムの実行を保証することが難しくなります。

信頼性。 構成要素(ハードウェアとソフトウェアの両方)が少ないシステムでは、不具合の原因になるものも少なくなります!

どのような場合にマイクロコントローラーを使うべきではないですか?

重い計算処理が必要な場合です。消費電力を低く抑えるため、マイクロコントローラーで利用できる計算資源は非常に限られています。たとえば、一部のマイクロコントローラーは浮動小数点演算をハードウェアでサポートしていません。そのようなデバイスでは、単精度浮動小数点数同士の単純な加算でさえ、数百 CPU サイクルかかることがあります。

なぜ C ではなく Rust を使うのですか?

おそらくここで説得する必要はないでしょう。みなさんはおそらく Rust と C の言語としての違いをすでによくご存じだからです。ぜひ触れておきたい点が1つあります。それはパッケージ管理です。C には公式で広く受け入れられたパッケージ管理ソリューションがありませんが、Rust には Cargo があります。これにより、開発ははるかに容易になります。そして、IMO、簡単なパッケージ管理はコードの再利用を促します。ライブラリをアプリケーションに簡単に統合できるようになるからです。これは良いことでもあります。ライブラリがより多くの「実戦的な検証」を受けることになるからです。

なぜ Rust を使うべきではないのですか?

あるいは、なぜ Rust より C を選ぶべきなのでしょうか?

C のエコシステムのほうが、はるかに成熟しています。いくつかの問題に対しては、すでに既製のソリューションが存在します。時間に敏感なプロセスを制御する必要があるなら、既存の商用 Real Time Operating Systems (RTOS) のいずれかを使って問題を解決できます。Rust には、商用で本番運用に耐える RTOS はまだありません。そのため、自分で1つ作るか、開発中のものを試す必要があります。それらの一覧は Awesome Embedded Rust リポジトリで見つけることができます。

ハードウェア/知識の要件

この本を読むために主に必要な知識は、Rust を ある程度 知っていることです。それを ある程度 と定量化するのは難しいのですが、少なくとも、ジェネリクスを完全に 理解している必要はない一方で、クロージャを 使う 方法は知っている必要があります。 また、2018 edition のイディオムにも慣れている必要があります。特に、 2018 edition では extern crate が不要であることは知っておく必要があります。

また、この教材を進めるには、次のハードウェアが必要です:

  • micro:bit v2 ボード。代わりに micro:bit v1.5 ボードでも構いません。この本では v1.5 を単に v1 と表記します。

(このボードは複数の electronics suppliers から購入できます)

注記 これは micro:bit v2 の画像です。v1 の前面は少し異なって見えます

  • micro-B USB ケーブル 1 本。これは micro:bit ボードを動作させるために必要です。 一部のケーブルはデバイスの充電にしか対応していないため、データ転送をサポートしていることを確認してください。

注記 このようなケーブルは、micro:bit のキットに同梱されている場合があるため、すでに持っているかもしれません。 モバイル機器の充電に使われる USB ケーブルの中にも、micro-B で、データを 転送できるものであれば使えるものがあります。

FAQ: ちょっと待って、なぜこの特定のハードウェアが必要なのですか?

そのほうが、私にとってもあなたにとっても、ずっと楽になるからです。

ハードウェアの違いを気にしなくてよければ、この教材はずっと、ずっと取り組みやすく なります。この点は信じてください。

FAQ: 別の開発ボードでもこの教材を進められますか?

たぶん可能です。それは主に 2 つの点に左右されます。1 つは、あなたがこれまでに マイクロコントローラを扱った経験があるかどうか、もう 1 つは、あなたの開発ボード 向けに nrf52-hal のような高水準の crate が、すでにどこかに存在するかどうかです。 別のものを使うつもりなら、対象のマイクロコントローラについて Awesome Embedded Rust HAL list を調べてみてください。

私の考えでは、別の開発ボードを使うと、この文章は初心者にやさしいという長所や 「追いやすさ」の大半、場合によってはそのすべてを失ってしまいます。

別の開発ボードを持っていて、自分をまったくの初心者だとは思わないのであれば、 quickstart プロジェクトテンプレートから始めるほうがよいでしょう。

開発環境のセットアップ

マイクロコントローラーを扱うにはいくつかのツールが必要です。というのも、コンピューターとは異なるアーキテクチャを扱うことになり、さらに「リモート」デバイス上でプログラムを実行してデバッグする必要があるからです。

ドキュメント

とはいえ、ツールだけですべてが済むわけではありません。ドキュメントがなければ、マイクロコントローラーを扱うことはほとんど不可能です。

この本を通して、以下のドキュメントを参照していきます。

ツール

以下に挙げるすべてのツールを使用します。最小バージョンが明記されていないものについては、最近のバージョンであればどれでも動作するはずですが、ここでは私たちがテストしたバージョンを記載しています。

  • Rust 1.57.0 以降のツールチェーン。

  • gdb-multiarch。テストしたバージョン: 10.2。ほかのバージョンでもおそらく動作するはずです。 お使いのディストリビューション/プラットフォームで gdb-multiarch が利用できない場合は、arm-none-eabi-gdb でも問題ありません。さらに、通常の gdb バイナリの中にもマルチアーキテクチャ機能付きでビルドされているものがあり、 これについての詳しい情報は以降の節で確認できます。

  • cargo-binutils。バージョン 0.3.3 以降。

  • Linux および macOS では minicom。テストしたバージョン: 2.7.1。ほかのバージョンでもおそらく動作するはずです

  • Windows では PuTTY

次に、いくつかのツールについて、OS に依存しないインストール手順に従ってください。

rustc & Cargo

https://rustup.rs の手順に従って rustup をインストールしてください。

すでに rustup をインストールしている場合は、stable チャネルを使用していること、および stable ツールチェーンが最新であることを再確認してください。rustc -V は、以下に示す日付より新しい日付を返すはずです。

$ rustc -V
rustc 1.53.0 (53cb7b09b 2021-06-17)

cargo-binutils

$ rustup component add llvm-tools

$ cargo install cargo-binutils --vers 0.3.3

$ cargo size --version
cargo-size 0.3.3

cargo-embed

cargo-embed をインストールするには、まずその前提条件をインストールしてください(注: これらの手順は、より汎用的な組み込みデバッグツールキット probe-rs の一部です)。その後、cargo を使ってインストールします。

$ cargo install --locked probe-rs-tools --vers '^0.24'

注記 probe-rs は頻繁に変更されるため、これは失敗することがあります。その場合は、https://probe.rs にアクセスし、現在のインストール手順に従ってください。

最後に、以下を実行して cargo-embed が正しくインストールされていることを確認してください。

$ cargo embed --version
cargo-embed 0.24.0 (git commit: crates.io)

このリポジトリ

この本には各章で使用する小さな Rust コードベースもいくつか含まれているため、そのソースコードもダウンロードする必要があります。これは次のいずれかの方法で行えます。

  • リポジトリ にアクセスし、緑色の “Code” ボタンをクリックしてから “Download Zip” をクリックする
  • git を使ってクローンする(git を知っているなら、おそらくすでにインストール済みでしょう)。リポジトリは zip を使う方法でリンクされているものと同じです

OS ごとの手順

次に、お使いの OS に対応する手順に従ってください。

Linux

以下は、いくつかの Linux ディストリビューション向けのインストールコマンドです。

Ubuntu 20.04 以降 / Debian 10 以降

NOTE gdb-multiarch は、ARM Cortex-M プログラムをデバッグする際に使用する GDB コマンドです

$ sudo apt-get install \
  gdb-multiarch \
  minicom

Fedora 32 以降

NOTE gdb は、ARM Cortex-M プログラムをデバッグする際に使用する GDB コマンドです

$ sudo dnf install \
  gdb \
  minicom

Arch Linux

NOTE arm-none-eabi-gdb は、ARM Cortex-M プログラムをデバッグする際に使用する GDB コマンドです

$ sudo pacman -S \
  arm-none-eabi-gdb \
  minicom

その他のディストリビューション

NOTE arm-none-eabi-gdb は、ARM Cortex-M プログラムをデバッグする際に使用する GDB コマンドです

ARM’s pre-built toolchain のパッケージがないディストリビューションでは、 「Linux 64-bit」ファイルをダウンロードし、その bin ディレクトリをパスに追加してください。 以下はその一例です。

$ mkdir -p ~/local && cd ~/local
$ tar xjf /path/to/downloaded/file/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2

次に、使用しているエディタで適切なシェルの初期化ファイル(例: ~/.zshrc または ~/.bashrc)を開き、 PATH に次の内容を追記します。

PATH=$PATH:$HOME/local/gcc-arm-none-eabi-9-2020-q2-update/bin

udev ルール

これらのルールにより、micro:bit のような USB デバイスを root 権限、つまり sudo なしで使用できます。

以下の内容で、このファイルを /etc/udev/rules.d に作成してください。

$ cat /etc/udev/rules.d/69-microbit.rules
# microbit 用 CMSIS-DAP

ACTION!="add|change", GOTO="microbit_rules_end"

SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", ATTR{idProduct}=="0204", TAG+="uaccess"

LABEL="microbit_rules_end"

次に、以下のコマンドで udev ルールを再読み込みします。

$ sudo udevadm control --reload

ボードがコンピューターに接続されている場合は、いったん取り外してから再度接続するか、次のコマンドを実行してください。

$ sudo udevadm trigger

次に、next section に進んでください。

Windows

arm-none-eabi-gdb

ARM は Windows 向けの .exe インストーラーを提供しています。here から入手し、指示に従ってください。 インストール処理が完了する直前に、“Add path to environment variable” オプションにチェックを入れるか選択してください。次に、ツールが %PATH% に含まれていることを確認します。

$ arm-none-eabi-gcc -v
(..)
gcc version 5.4.1 20160919 (release) (..)

PuTTY

最新の putty.exethis site からダウンロードし、%PATH% 内のどこかに配置してください。

次に、next section に進んでください。

macOS

すべてのツールはHomebrewを使用してインストールできます。

$ # ARM GCC デバッガー
$ brew install arm-none-eabi-gdb

$ # Minicom
$ brew install minicom

$ # lsusb (接続されている USB デバイスを一覧表示)
$ brew install lsusb

以上です!next sectionに進んでください。

インストールを確認する

すべてのツールが正しくインストールされたことを確認しましょう。

Linux のみ

パーミッションを確認する

USB ケーブルを使って micro:bit をコンピューターに接続します。

micro:bit は、これで /dev/bus/usb に USB デバイス(ファイル)として表示されるはずです。どのように 列挙されたかを確認してみましょう:

$ lsusb | grep -i "NXP ARM mbed"
Bus 001 Device 065: ID 0d28:0204 NXP ARM mbed
$ # ^^^        ^^^

私の場合、micro:bit はバス #1 に接続され、デバイス #65 として列挙されました。つまり、 ファイル /dev/bus/usb/001/065 そのもの が micro:bit です。ファイルのパーミッションを確認してみましょう:

$ ls -l /dev/bus/usb/001/065
crw-rw-r--+ 1 nobody nobody 189, 64 Sep  5 14:27 /dev/bus/usb/001/065

パーミッションは crw-rw-r--+ になっているはずです。末尾の + に注意し、次のコマンドを実行してアクセス権を確認してください。

$ getfacl /dev/bus/usb/001/065
getfacl: Removing leadin '/' from absolute path names
# file: dev/bus/usb/001/065
# owner: nobody
# group: nobody
user::rw-
user:<YOUR-USER-NAME>:rw-
group::rw-
mask::rw-
other::r-

上の一覧に、あなたのユーザー名が rw- パーミッション付きで表示されるはずです。表示されない場合は … udev rules を確認し、次のコマンドで再読み込みしてみてください:

$ sudo udevadm control --reload
$ sudo udevadm trigger

共通

cargo-embed を確認する

まず、USB ケーブルを使って micro:bit をコンピューターに接続します。

少なくとも、micro:bit の USB ポートのすぐ横にあるオレンジ色の LED が点灯するはずです。 さらに、まだ別のプログラムを micro:bit に書き込んだことがない場合は、micro:bit に最初から入っている デフォルトプログラムが背面の赤い LED を点滅させ始めるはずですが、それらは 無視してかまいません。

それでは、probe-rs、そしてその拡張である cargo-embed が micro:bit を認識できるか確認しましょう。これは次のコマンドを実行することで行えます。

$ probe-rs list
The following debug probes were found:
[0]: BBC micro:bit CMSIS-DAP -- 0d28:0204:990636020005282030f57fa14252d446000000006e052820 (CMSIS-DAP)

あるいは、micro:bit のデバッグ機能についてさらに詳しい情報が必要であれば、次を実行できます:

$ probe-rs info
Probing target via JTAG

Error identifying target using protocol JTAG: The probe does not support the JTAG protocol.

Probing target via SWD

ARM Chip with debug port Default:
Debug Port: DPv1, DP Designer: ARM Ltd
├── 0 MemoryAP
│   └── ROM Table (Class 1), Designer: Nordic VLSI ASA
│       ├── Cortex-M4 SCS   (Generic IP component)
│       │   └── CPUID
│       │       ├── IMPLEMENTER: ARM Ltd
│       │       ├── VARIANT: 0
│       │       ├── PARTNO: Cortex-M4
│       │       └── REVISION: 1
│       ├── Cortex-M3 DWT   (Generic IP component)
│       ├── Cortex-M3 FBP   (Generic IP component)
│       ├── Cortex-M3 ITM   (Generic IP component)
│       ├── Cortex-M4 TPIU  (Coresight Component)
│       └── Cortex-M4 ETM   (Coresight Component)
└── 1 Unknown AP (Designer: Nordic VLSI ASA, Class: Undefined, Type: 0x0, Variant: 0x0, Revision: 0x0)


Debugging RISC-V targets over SWD is not supported. For these targets, JTAG is the only supported protocol. RISC-V specific information cannot be printed.
Debugging Xtensa targets over SWD is not supported. For these targets, JTAG is the only supported protocol. Xtensa specific information cannot be printed.

次に、本書のソースコードの src/03-setup ディレクトリにある Embed.toml を 変更する必要があります。default.general セクションには、コメントアウトされた 2 つのチップバリアントがあります:

[default.general]
# chip = "nrf52833_xxAA" # micro:bit V2 の場合はこの行のコメントを外します
# chip = "nrf51822_xxAA" # micro:bit V1 の場合はこの行のコメントを外します

micro:bit v2 ボードを使っている場合は 1 行目のコメントを外し、v1 の場合は 2 行目のコメントを外してください。

次に、以下のいずれかのコマンドを実行します:

$ # 本書のソースコードの src/03-setup にいることを確認してください
$ # micro:bit v2 を使っている場合
$ rustup target add thumbv7em-none-eabihf
$ cargo embed --target thumbv7em-none-eabihf

$ # micro:bit v1 を使っている場合
$ rustup target add thumbv6m-none-eabi
$ cargo embed --target thumbv6m-none-eabi

すべて正しく動作すれば、cargo-embed はまずこのディレクトリ内の小さなサンプルプログラムを コンパイルし、それをフラッシュして、最後に Hello World を表示する見やすいテキストベースのユーザーインターフェイスを 開くはずです。

(うまくいかない場合は、general troubleshooting の手順を確認してください。)

この出力は、たった今 micro:bit に書き込んだ小さな Rust プログラムから送られてきています。 すべて正常に動作しており、次の章に進めます!

IDE を最大限活用する

この本のすべてのコードは、シンプルなターミナルを使ってコードをビルドし、 実行し、それと対話することを前提としています。テキストエディタについても、 特に何かを前提にしてはいません。

ただし、自動補完や型注釈、 好みのショートカットなど、さまざまな機能を備えたお気に入りの IDE があるかもしれません。このセクションでは、 この本のリポジトリから取得したコードを使って、IDE を最大限活用する方法を説明します。

自動補完、型注釈、その他

一部の IDE は、ある用語が microbit と microbit-v2 のどちらのコードベースで定義されているのかを 判別できないため、コードを正しく理解できないことがあります。自動補完がうまく動作しない場合は、 本書で出てくる Cargo.toml ファイルを編集して、 使用していないバージョンの microbit への参照をすべて削除してみるとよいでしょう。つまり: Cargo.toml ファイルでは、使用しない依存関係と feature を削除する必要があります(#[cfg(feature = "vI")] でガードされた部分と、そのガード自体)

IDE の設定

以下では、この本を最大限活用できるように IDE を設定する方法を説明します。 お使いの IDE が以下に載っていない場合は、次の 読者が最良の体験を得られるように、セクションを追加してこの本を改善してください。

IntelliJ でビルドする方法

IntelliJ のビルド設定を編集する際は、いくつか既定値以外の設定があります:

  • コマンドを編集してください。本書で cargo embed FLAGS を実行するよう指示された場合は、 既定値の run をコマンド embed FLAGS に置き換える必要があります
  • "Emulate terminal in output console" を有効にしてください。そうしないと、プログラムはターミナルにテキストを出力できません
  • 作業ディレクトリが microbit/src/N-name であることを確認してください。ここで N-name は、あなたが 読んでいる章のディレクトリです。src ディレクトリには cargo ファイルがないため、そこから実行することはできません。

ハードウェアを知ろう

これから扱うハードウェアに慣れていきましょう。

micro:bit

ボード上には多くのコンポーネントがありますが、その一部を挙げると次のとおりです:

これらのコンポーネントの中で最も重要なのはマイクロコントローラーです(“microcontroller unit” を 略して “MCU” と呼ぶこともあります)。これは、USB ポートがある側のボード上にある 2 つの 黒い四角のうち大きいほうです。MCU は、あなたのコードを実行するものです。ときどき 「ボードをプログラミングする」という表現を目にするかもしれませんが、実際に行っているのは ボードに搭載されている MCU をプログラミングすることです。

ボードのより詳しい説明に興味があれば、 micro:bit の Web サイト を参照してください。

MCU は非常に重要なので、ボード上にあるものをもう少し詳しく見てみましょう。 以下の 2 つのセクションのうち、あなたのボードに当てはまるのは 1 つだけである点に注意してください。これは、 micro:bit v2 と v1 のどちらを使っているかによって異なります。

Nordic nRF52833(「nRF52」、micro:bit v2)

この MCU には、その直下に 73 本の小さな金属製の ピン があります(いわゆる aQFN73 パッケージのチップです)。 これらのピンは トレース に接続されています。トレースとは、基板上で部品同士を つなぐ配線の役割を果たす、小さな「道路」のようなものです。MCU は、これらのピンの電気的特性を 動的に変更できます。これは、回路内を 流れる電流の流れ方をライトスイッチが変えるのに似ています。特定のピンに 電流を流すことを有効または無効にすることで、そのピンに接続された LED(トレース経由)を 点灯したり消灯したりできます。

メーカーごとに部品番号の付番方式は異なりますが、多くの場合、 部品番号を見るだけでその部品に関する情報を判断できます。 この MCU の部品番号(N52833 QIAAA0 2024AL。裸眼ではおそらく見えませんが、 チップ上に記されています)を見ると、先頭の n から これが Nordic Semiconductor 製の部品であることがわかります。 この部品番号を同社の Web サイトで調べると、すぐに product page が見つかります。 そこから、このチップの主な訴求点が 「Bluetooth Low Energy and 2.4 GHz SoC」(SoC は「System on a Chip」の略)であることがわかります。 これで、製品名に RF が入っている理由も説明できます。RF は radio frequency の略です。 さらに、product page からリンクされているこのチップのドキュメントを 少し調べると、product specification が見つかります。そこには、この一見奇妙なチップ名の付け方を説明する 第 10 章「Ordering Information」があります。 ここから次のことがわかります。

  • N52 は MCU のシリーズであり、ほかにも nRF52 MCU があることを示しています
  • 833 は部品コードです
  • QI はパッケージコードで、aQFN73 の略です
  • AA はバリアントコードで、MCU が搭載する RAM とフラッシュメモリの容量を示します。 この場合はフラッシュ 512 キロバイト、RAM 128 キロバイトです
  • A0 はビルドコードで、ハードウェアのバージョン(A)と製品構成(0)を示します
  • 2024AL はトラッキングコードなので、あなたのチップでは異なる可能性があります

もちろん、product specification にはこのチップに関する有用な情報が他にもたくさんあり、 たとえば ARM® Cortex™-M4 32 ビットプロセッサをベースにしていることなどがわかります。

Arm? Cortex-M4?

このチップは Nordic が製造しているのに、では Arm とは何なのでしょうか? そして、このチップが nRF52833 だとすると、Cortex-M4 とは何でしょうか?

「Arm ベース」のチップはかなり 広く使われていますが、「Arm」という商標の企業である Arm Holdings は 実際には販売用のチップを製造していない、と聞くと驚くかもしれません。代わりに、その主なビジネス モデルは、チップの一部を 設計 することです。その後、その設計を メーカーにライセンスし、メーカーはその設計を(独自の 調整をいくらか加えつつ)実際に販売可能な物理ハードウェアとして実装します。 この点での Arm の戦略は、Intel のように自社のチップを 設計し かつ 製造もする企業とは異なります。

Arm はさまざまな設計をライセンスしています。その「Cortex-M」ファミリーの設計は 主にマイクロコントローラのコアとして使われます。たとえば、Cortex-M4 (このチップがベースにしているコア)は、低コストかつ低消費電力向けに設計されています。 Cortex-M7 はより高コストですが、そのぶん機能と性能が高くなっています。

幸い、この本を読むうえで、さまざまな種類のプロセッサや Cortex の設計について深く知っておく必要はありません。ただし、これで このデバイスに関する用語への理解は少し深まったはずです。 扱っているのは具体的には nRF52833 ですが、nRF52833 は Cortex-M の設計をベースにしているため、Cortex-M ベースのチップ向けの ドキュメントを読んだりツールを使ったりすることがあるでしょう。

Nordic nRF51822(「nRF51」、micro:bit v1)

この MCU の真下には、48 本の小さな金属製のピンがあります(いわゆる QFN48 チップです)。 これらのピンは traces に接続されています。これは基板上で部品同士を つなぐ配線として機能する、小さな「道路」のようなものです。MCU は ピンの電気的特性を動的に変化させることができます。これは、照明の スイッチが回路内の電流の流れ方を変えるのと似ています。特定のピンを 電流が流れるようにしたり、流れないようにしたりすることで、そのピンに (traces を介して)接続された LED をオン・オフできます。

各メーカーは異なる部品番号体系を使っていますが、多くの場合、 部品番号を見るだけでコンポーネントに関する情報を判断できます。 この MCU の部品番号(N51822 QFAAH3 1951LN。肉眼ではおそらく見えませんが、 チップ上にあります)を見ると、先頭の n から、これは Nordic Semiconductor が製造した部品であることが推測できます。同社の Web サイトで部品番号を調べると、 すぐに product page が見つかります。そこでは、このチップの主な マーケティング上の訴求点が「Bluetooth Low Energy and 2.4 GHz SoC」 であることがわかります(SoC は「System on a Chip」の略です)。 これは、RF が radio frequency の略であることから、製品名に RF が含まれている 理由も説明しています。product page からリンクされている このチップのドキュメントを少し調べると、product specification が見つかり、 その中の第 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 Holdings)は、実は購入可能なチップを製造していません。 その代わり、同社の主なビジネスモデルは、チップの一部を設計することです。 そして、その設計をメーカーにライセンスし、メーカーはそれを (おそらく独自の調整をいくつか加えつつ)物理的なハードウェアとして実装し、 販売できるようにします。ここでの Arm の戦略は、チップの設計も製造も 行う Intel のような企業とは異なります。

Arm はさまざまな設計をライセンスしています。その「Cortex-M」ファミリーの設計は、 主にマイクロコントローラのコアとして使われます。たとえば Cortex-M0 (このチップのベースになっているコア)は、低コストかつ低消費電力向けに 設計されています。Cortex-M7 はより高コストですが、機能と性能がより高くなっています。

ありがたいことに、この本を読み進めるうえで、さまざまな種類のプロセッサや Cortex の設計についてそれほど多くを知っている必要はありません。ただし、 これでこのデバイスの用語について、少しは理解が深まったはずです。 あなたが具体的に扱っているのは nRF51822 ですが、nRF51822 は Cortex-M ベースの設計に基づいているため、Cortex-M ベースのチップ向けの ドキュメントを読んだり、ツールを使ったりすることがあるかもしれません。

Rust Embedded の用語

micro:bit のプログラミングに入る前に、今後のすべての章で重要になるライブラリと用語を手短に見ておきましょう。

抽象化レイヤー

十分にサポートされているマイクロコントローラ、またはマイクロコントローラを搭載したボードについては、通常、その抽象化レベルを表す用語として次のものを耳にするでしょう。

Peripheral Access Crate (PAC)

PAC の役割は、チップのペリフェラルに対する(完全ではないにせよ)安全な直接インターフェースを提供し、設定したいとおりに最後の 1 ビットまですべて構成できるようにすることです(もちろん、誤った方法で設定することもできます)。通常、PAC を扱う必要があるのは、上位のレイヤーでは要件を満たせない場合か、それらのレイヤー自体を開発している場合だけです。ここで私たちが(暗黙的に)使用する PAC は、nRF52 用のものか nRF51 用のものです。

Hardware Abstraction Layer (HAL)

HAL の役割は、チップの PAC の上に構築され、そのチップ特有のあらゆる挙動を知らない人でも実際に使える抽象化を提供することです。通常は、ペリフェラル全体を単一の struct に抽象化し、たとえばそのペリフェラル経由でデータをやり取りできるようにします。私たちはそれぞれ nRF52-hal または nRF51-hal を使用します。

Board Support Crate(歴史的には Board Support Package、略して BSP と呼ばれていました)

BSP の役割は、ボード全体(micro:bit のようなもの)を一度に抽象化することです。つまり、マイクロコントローラだけでなく、そのボード上に搭載されている可能性のあるセンサーや LED なども扱うための抽象化を提供する必要があります。かなり多くの場合(特に自作ボードでは)、チップ用の HAL を使い、センサー用のドライバは自分で作るか crates.io で探すことになります。ですが幸いなことに、micro:bit には実際に BSP があるので、私たちは HAL の上にそれも使っていきます。

レイヤーの統一

次に、Rust Embedded の世界で非常に中心的なソフトウェアである embedded-hal を見ていきます。その名前が示すとおり、これは先ほど学んだ第 2 の抽象化レベル、つまり HAL に関係しています。embedded-hal の背後にある考え方は、すべての HAL における特定のペリフェラルの実装に通常共通している振る舞いを記述する trait のセットを提供することです。たとえば、ピンの電源をオンまたはオフにできる関数は、常に存在すると期待されます。たとえば、ボード上の LED をオン・オフするときです。これにより、たとえば温度センサー用のドライバを、embedded-hal の trait 実装が存在するあらゆるチップで使えるように書けます。つまり、そのドライバを embedded-hal の trait にのみ依存するように書けばよいのです。このように書かれたドライバはプラットフォーム非依存と呼ばれ、幸いなことに 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 の代わりに、cortex-m-rt クレートの entry 属性を使ってカスタムのエントリポイントを定義します。このプログラムではエントリポイントに “main” という名前を付けていますが、ほかのどんな名前でも使えました。エントリポイント関数は fn() -> ! というシグネチャでなければなりません。この型は、その関数がリターンできないことを示します。つまり、このプログラムは決して終了しません。

注意深く見ているなら、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

このファイルは、cargo-embed に次のことを伝えています:

  • nrf52833 または nrf51822 のどちらかを使っていること。ここでも 3 章で行ったのと同じように、使用しているチップに対応するコメントを外す必要があります。
  • 書き込み後にチップを停止させたいこと。これにより、プログラムが即座にループへ飛ばないようにします。
  • RTT を無効にしたいこと。RTT は、チップがデバッガにテキストを送信できるようにするプロトコルです。 実際、RTT が動作しているところはすでに見ています。3 章で “Hello World” を送っていたのがそのプロトコルでした。
  • GDB を有効にしたいこと。これはデバッグ手順で必要になります

それでは、このプログラムをビルドするところから始めましょう。

ビルドしてみましょう

最初のステップは、“binary” クレートをビルドすることです。マイクロコントローラーはあなたのコンピューターとは異なる アーキテクチャを持っているため、クロスコンパイルする必要があります。Rust の世界では、クロスコンパイルは rustc または Cargo に追加の --target フラグを渡すだけで簡単に行えます。複雑なのは、その フラグに渡す引数、つまりターゲットの名前を見つけることです。

すでに知っているように、micro:bit v2 のマイクロコントローラーには Cortex-M4F プロセッサーが搭載されており、v1 には Cortex-M0 が搭載されています。 rustc は 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:bit v2 では thumbv7em-none-eabihf ターゲットを使い、v1 では thumbv6m-none-eabi を使います。 クロスコンパイルする前に、ターゲット向けの標準ライブラリのプリコンパイル済みバージョン (実際にはその縮小版)をダウンロードする必要があります。これは rustup を使って行います。

# micro:bit v2 の場合
$ rustup target add thumbv7em-none-eabihf
# micro:bit v1 の場合
$ rustup target add thumbv6m-none-eabi

上記の手順は一度だけ実行すれば十分です。rustup はツールチェーンを更新するたびに、新しい標準ライブラリ (rust-std コンポーネント)を再インストールします。したがって、verifying your setup の際に必要なターゲットをすでに追加している場合は、 この手順をスキップできます。

rust-std コンポーネントが用意できたので、今度は Cargo を使ってプログラムをクロスコンパイルできます。

# `src/05-led-roulette` ディレクトリにいることを確認してください

# 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

# 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 ファイルとビルドコマンドにより、最適化が無効になることが保証されます。

OK、これで実行可能ファイルが生成されました。この実行可能ファイルはまだどの LED も点滅させません。 これは、この章の後半で土台として使う簡略化されたバージョンにすぎません。 念のため、生成された実行可能ファイルが本当に ARM バイナリであることを確認してみましょう。

# micro:bit v2 の場合
# `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

# micro:bit v1 の場合
# `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 のおかげでとても簡単です。

ただし、そのコマンドを実行する前に、それが実際に何をしているのかを見てみましょう。USB コネクターが上を向くように micro:bit の側面を見ると、実際にはそこに黒い四角が 2 つあることに気づくはずです (micro:bit v2 には 3 つ目の、そして最も大きいものがありますが、それはスピーカーです)。1 つはすでに説明した MCU ですが、 もう 1 つは何のためにあるのでしょうか? そのもう 1 つのチップには、主に 3 つの役割があります。

  1. USB コネクターから MCU に電力を供給する
  2. MCU 用のシリアル-USB ブリッジを提供する(これは後の章で見ていきます)
  3. プログラマ/デバッガとして動作する(今のところ重要なのはこの役割です)

基本的に、このチップはコンピューター(USB 経由で接続されている)と MCU(配線パターンを介して接続され、SWD プロトコルで通信している)の間の ブリッジとして動作します。このブリッジによって、MCU に新しいバイナリをフラッシュしたり、 デバッガを通してその状態を調べたり、その他いろいろなことができるようになります。

では、フラッシュしてみましょう!

# 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

# 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 がそのまま待機し続けることに気づくでしょう。これは意図された挙動なので、 閉じないでください。次のステップ、つまりデバッグでこの状態のまま必要になるからです! さらに、 cargo buildcargo embed には実際には同じフラグが渡されていることにも気づいたでしょう。これは、 cargo embed が実際にはビルドを実行し、その結果できたバイナリをチップにフラッシュしているためです。したがって、 今後すぐにコードをフラッシュしたい場合は、cargo build のステップを省いてもかまいません。

デバッグしてみよう

いったいこれはどう動いているのでしょう?

この小さなプログラムをデバッグする前に、ここで実際に何が 起きているのかを手短に理解しておきましょう。前の章では、ボード上の 2 つ目のチップの役割と、 それがどのようにコンピューターと通信するのかについてはすでに説明しましたが、では実際にそれをどう使えばよいのでしょうか?

Embed.toml にある小さなオプション default.gdb.enabled = true によって、cargo-embed は書き込み後に いわゆる「GDB stub」を開きます。これは GDB が接続できるサーバーで、そこに 「アドレス X にブレークポイントを設定する」といったコマンドを送れます。このサーバーは、そのコマンドをどう処理するかを 自分で判断できます。cargo-embed の GDB stub の場合は、このコマンドを USB 経由でボード上のデバッグプローブへ転送し、その後はそのプローブが実際に MCU と通信する役割を担ってくれます。

デバッグしてみましょう!

cargo-embed は現在のシェルを占有しているので、新しいシェルを開いて プロジェクトディレクトリに戻れば大丈夫です。そこに移動したら、まずは次のようにして gdb でバイナリを開く必要があります。

# micro:bit v2 の場合
$ gdb target/thumbv7em-none-eabihf/debug/led-roulette

# micro:bit v1 の場合
$ gdb target/thumbv6m-none-eabi/debug/led-roulette

NOTE: インストールした GDB によって、起動に使うコマンドが異なります。 どれだったか忘れた場合は、chapter 3 を確認してください。

NOTE: target/thumbv7em-none-eabihf/debug/led-roulette: No such file or directory というエラーが出る場合は、ファイルパスの先頭に ../../ を付けてみてください。たとえば次のようになります。

$ gdb ../../target/thumbv7em-none-eabihf/debug/led-roulette

これは、各サンプルプロジェクトが書籍全体を含む workspace の中にあり、workspace では target ディレクトリが 1 つだけ共有されるためです。詳しくは Workspaces chapter in Rust Book を参照してください。

NOTE: ここで cargo-embed が大量の警告を出しても心配しないでください。現時点では GDB プロトコルを完全には実装していないため、GDB から送られてくるすべてのコマンドを認識できないことがあります。 クラッシュしない限りは問題ありません。

次に、GDB stub に接続する必要があります。これはデフォルトで 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 関数まで進むことです。 そのためには、まずそこにブレークポイントを設定し、その後 ブレークポイントに到達するまでプログラムの実行を続けます。

(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 <breakpoint-num> で不要なものを削除できます。

より快適にデバッグするために、ここでは GDB のテキストユーザーインターフェイス(TUI)を使います。 このモードに入るには、GDB シェルで次のコマンドを入力します。

(gdb) layout src

NOTE: Windows ユーザーのみなさんには申し訳ありません。GNU ARM Embedded Toolchain に 同梱されている GDB は、この TUI モードをサポートしていません :-(

GDB セッション

GDB の break コマンドは、関数名に対してだけでなく特定の行番号でも使えます。 たとえば 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 を実行すると、その文をプログラムが 決して通過しないため、そこで止まってしまいます。そこで代わりに layout asm コマンドで逆アセンブル表示に切り替え、stepi を使って 1 命令ずつ進めます。あとで 언제でも layout src コマンドを再度実行すれば、Rust のソースコード表示に戻せます。

NOTE: もし誤って nextcontinue コマンドを使って GDB が止まってしまった場合は、Ctrl+C を押せば抜け出せます。

(gdb) layout asm

GDB セッション

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          // 無限ループ。これはこのスタックフレームを抜けないようにするためです
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 {}

もう少し面白いものに進む前に、最後にもう 1 つ便利なテクニックを紹介します。次のコマンドを 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 関数に到達するまで、プログラムを自由に実行できます。

この組み合わせは、誤って詳しく調べたかったプログラムの一部を 通り過ぎてしまったときに便利です。プログラムの状態を簡単に いちばん最初の時点まで巻き戻せます。

細かい注意点: この reset コマンドは RAM を消去したり変更したりしません。そのメモリには 前回の実行時の値がそのまま残ります。ただし、プログラムの挙動が 未初期化 変数の値に依存していない限り、これは問題にならないはずです。もっとも、それは 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]

NOTE: デフォルトの 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 マトリクスと呼ばれます。このようなマトリクス配置を使うことで、それぞれの LED を駆動するために 25 本の別々のピンを使わなくても、どの列とどの行を点灯させるかを制御するために 10 本(5+5)のピンだけで済みます。

NOTE micro:bit v1 のチームはこれを少し異なる形で実装していました。schematic page によると、 実際には 3x9 のマトリクスとして実装されていますが、いくつかの列は単に未使用のままになっています。

通常であれば、特定の LED を点灯させるためにどのピンを制御すべきかを判断するには、 ここで micro:bit v2 schematic または micro:bit v1 schematic を それぞれ読む必要があります。 しかし幸運なことに、先ほど述べた micro:bit BSP を使うことで、 こうした詳細はすべてきれいに抽象化されています。

実際に点灯してみよう!

LED マトリクス内の LED を点灯させるのに必要なコードは、実際にはとてもシンプルですが、少し準備が必要です。まずコードを見て、そのあとで順を追って確認していきましょう。

#![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 が内部でどのように動作しているかに関係しています。 前に説明したように、これらは PAC クレートの上に構築されており、PAC クレートは チップ上のすべてのペリフェラルを(Rust 的な意味で)所有しています。 let mut board = Board::take().unwrap(); は基本的に、 これらのペリフェラルを PAC から取得し、変数に束縛します。この特定のケースでは、 HAL だけでなく BSP 全体を扱っているため、これによって ボード上のほかのチップの Rust 表現の所有権も取得します。

NOTE: ここでなぜ unwrap() を呼ぶ必要があるのか疑問に思っているなら、理論上は take() が 複数回呼び出される可能性があるからです。そうなると、ペリフェラルが 2 つの別々の変数で表現されることになり、 2 つの変数が同じリソースを変更してしまうため、多くの分かりにくい挙動が起こりえます。これを避けるため、 PAC は、ペリフェラルを 2 回取得しようとすると panic するように実装されています。

これで、row1col1 に接続された LED を、row1 ピンを high に設定することで点灯できます (つまりオンにします)。 col1 を low のままにしておける理由は、LED マトリクス回路の仕組みにあります。 さらに、embedded-hal は、ハードウェアに対するあらゆる操作が、 ピンのオン・オフ切り替えのような単純なものであっても、エラーを返す可能性があるように設計されています。 このケースではそれが起こる可能性は非常に低いので、結果に対して単に unwrap() を使えます。

テストする

この小さなプログラムをテストするのはとても簡単です。まずこれを src/main.rs に入れます。 そのあと、前のセクションと同じように cargo embed コマンドを実行し、書き込みを行います。 そして前と同様に GDB を開いて、GDB stub に接続します。

$ # 前のセクションの 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 つが点灯するはずです。

点滅します

遅延

ここでは、embedded-hal が提供する遅延抽象化について簡単に見ていきます。 その後、これを前の章で扱った GPIO 抽象化と組み合わせて、 最終的に LED を点滅させます。

embedded-hal は、プログラムの実行を遅らせるための 2 つの抽象化を提供しています: DelayUsDelayMs です。これらは本質的にはまったく同じように動作しますが、 遅延関数が受け取る単位が異なります。

MCU の内部には、「タイマー」と呼ばれるものがいくつか存在します。これらは時間に関するさまざまな処理を行えますが、 そのひとつとして、プログラムの実行を一定時間だけ単純に停止させることができます。たとえば、 1 秒ごとに何かを出力する非常にシンプルな遅延ベースのプログラムは、次のようになります:

#![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");
    }
}

ここでは panic 実装を panic_halt から panic_rtt_target に変更していることに注意してください。これにより、 Cargo.toml の RTT に関する 2 行のコメントを外し、 panic-halt の行をコメントアウトする必要があります。 これは、Rust では同時に 1 つの panic 実装しか使えないためです。

実際に出力を確認するには、Embed.toml を次のように変更する必要があります:

[default.general]
# micro:bit V2 の場合はこの行のコメントを外します
# chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# micro:bit V1 の場合はこの行のコメントを外します
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1

[default.reset]
halt_afterwards = false

[default.rtt]
enabled = true

[default.gdb]
enabled = false

そして、このコードを src/main.rs に書き込み、もう一度すばやく cargo embed を実行すると (前と同じフラグを再び使ってください)、 MCU から毎秒 “1000 ms passed” がコンソールに送られてくるはずです。

点滅

ここまでで、GPIO と遅延抽象化について新たに学んだ知識を組み合わせて、 micro:bit の背面にある LED を実際に点滅させる準備が整いました。できあがるプログラムは、 実際には上のものと、前のセクションで LED を点灯させたものを 組み合わせたものにすぎず、次のようになります:

#![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);
    }
}

このコードを src/main.rs に書き込み、最後に cargo embed を実行すると (適切なフラグを付けてください)、 点滅させる前に点灯した LED に加えて、LED が消灯から点灯へ、またはその逆へ切り替わるたびに 出力も表示されるはずです。

課題

これで、課題に立ち向かうための準備は万端です!あなたのタスクは、この章の冒頭でお見せした アプリケーションを実装することです。

ここで何が起きているのかよくわからない場合は、かなり遅くしたバージョンがあります。

LED ピンを個別に扱うのはかなり面倒なので (特に今回のように実質的にそのほとんどすべてを使わなければならない場合は) BSP が提供する display 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 を 1000ms 表示する
        display.show(&mut timer, light_it_all, 1000);
        // ディスプレイを再びクリアする
        display.clear();
        timer.delay_ms(1000_u32);
    }
}

この API を使えば、あなたのタスクは基本的に、適切な 画像マトリクスを計算してそれを BSP に渡すだけです。

私の解答

どのような解答を思いつきましたか?

これが私のものです。これは、必要なマトリクスを生成する方法としては、おそらく最もシンプルなものの 1 つです(もちろん、最も 美しいものではありませんが):

#![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;
        }
    }
}

もう 1 つあります! あなたの解答が “release” モードでコンパイルした場合にも動作することを確認してください:

# micro:bit v2 の場合
$ cargo embed --features v2 --target thumbv7em-none-eabihf --release
  (...)

# micro:bit v1 の場合
$ cargo embed --features v1 --target thumbv6m-none-eabi --release
  (...)

“release” モードのバイナリをデバッグしたい場合は、別の GDB コマンドを使う必要があります:

# micro:bit v2 の場合
$ gdb target/thumbv7em-none-eabihf/release/led-roulette

# micro:bit v1 の場合
$ gdb target/thumbv6m-none-eabi/release/led-roulette

バイナリサイズは、私たちが常に注意しておくべきものです! あなたの解答はどれくらいの大きさですか? それは、 release バイナリに対して size コマンドを使って確認できます:

# 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 を使用して release バイナリをビルドするよう、すでに設定されています。

この出力の読み方はわかりますか? text セクションにはプログラムの命令が含まれています。一方で、 data および bss セクションには、RAM に静的に割り当てられた変数(static 変数)が含まれています。 micro:bit 上のマイクロコントローラーの仕様を思い出せば、 そのフラッシュメモリは実際にはこのバイナリを格納するには小さすぎることに気づくはずです。では、これはなぜ可能なのでしょうか? サイズの統計からわかるように、バイナリの大部分は実際にはデバッグ関連の セクションで構成されています。しかし、それらがマイクロコントローラーに書き込まれることはありません。結局のところ、実行には 関係ないからです。

シリアル通信

これがこれから使うものです。あなたのコンピューターにも付いているといいですね!

いえ、心配しないでください。このコネクタである DE-9 は、かなり前に PC では廃れ、Universal Serial Bus(USB)に置き換えられました。私たちが扱うのは DE-9 コネクタそのものではなく、このケーブルで通常使われている、あるいは使われていた通信プロトコルです。

では、このシリアル通信とは何でしょうか? これは 非同期 通信プロトコルで、2 つのデバイスが 2 本のデータ線(に加えて共通のグラウンド)を使って、データを シリアルに、つまり一度に 1 ビットずつやり取りします。このプロトコルが非同期であるというのは、共有されるどちらの線にもクロック信号が流れないという意味です。その代わり、通信が行われる 前に、両者がワイヤ上でどの速さでデータを送るかについて合意しておく必要があります。このプロトコルでは、データを A から B にも、B から A にも同時に送れるため、全二重 通信が可能です。

このプロトコルを使って、マイクロコントローラとあなたのコンピューターの間でデータをやり取りします。ここで、なぜ以前のように RTT を使わないのかと思うかもしれません。RTT は、デバッグ専用に使うことを意図したプロトコルです。本番環境で、実際に RTT を使って別のデバイスと通信するデバイスを見つけることは、まず間違いなくできないでしょう。一方で、シリアル通信はかなりよく使われています。たとえば、一部の GPS 受信機は、受信した測位情報をシリアル通信で送信します。

次に実際的な疑問としておそらく浮かぶのは、このプロトコルでどれくらいの速さでデータを送れるのか、ということです。

このプロトコルはフレーム単位で動作します。各フレームには 1 つの スタート ビット、5〜9 ビットのペイロード(データ)、そして 1〜2 個の ストップ ビットが含まれます。このプロトコルの速度は ボーレート と呼ばれ、ビット毎秒(bps)で表されます。一般的なボーレートは 9600、19200、38400、57600、115200 bps です。

実際にこの問いに答えると、1 スタートビット、8 データビット、1 ストップビット、ボーレート 115200 bps という一般的な構成では、理論上、1 秒あたり 11,520 フレームを送信できます。各フレームは 1 バイトのデータを運ぶため、データレートは 11.52 KB/s になります。実際には、通信の遅い側(マイクロコントローラ)での処理時間のため、データレートはこれより低くなる可能性があります。

今日のコンピューターは、シリアル通信プロトコルを直接サポートしていません。そのため、コンピューターをマイクロコントローラに直接接続することはできません。しかし幸いなことに、micro:bit 上のデバッグプローブには、いわゆる USB-to-serial コンバーターがあります。これは、このコンバーターが両者の間に入り、マイクロコントローラにはシリアルインターフェースを、コンピューターには USB インターフェースを提供することを意味します。マイクロコントローラからは、あなたのコンピューターは別のシリアルデバイスとして見え、あなたのコンピューターからは、マイクロコントローラは仮想シリアルデバイスとして見えます。

それでは、serial モジュールと、OS が提供するシリアル通信ツールに慣れていきましょう。進むルートを選んでください:

*nix ツール

micro:bit ボードを接続する

micro:bit ボードをコンピューターに接続すると、 /dev に新しい TTY デバイスが現れるはずです。

$ # Linux
$ dmesg | grep -i tty
[63712.446286] cdc_acm 1-1.7:1.1: ttyACM0: USB ACM device

これが USB <-> シリアルデバイスです。Linux では、これは tty*(通常は ttyACM* または ttyUSB*)という名前になります。 Mac OS では ls /dev/cu.usbmodem* でシリアルデバイスが表示されます。

では、ttyACM0 は正確には何でしょうか?もちろん、これはファイルです! *nix では、すべてがファイルです:

$ ls -l /dev/ttyACM0
crw-rw----. 1 root plugdev 166, 0 Jan 21 11:56 /dev/ttyACM0

このファイルに単に書き込むだけで、データを送信できます:

$ echo 'Hello, world!' > /dev/ttyACM0

このコマンドを入力するたびに、USB ポートのすぐ横にある micro:bit のオレンジ色の LED が 一瞬点滅するはずです。

minicom

キーボードを使ってシリアルデバイスとやり取りするために、minicom というプログラムを使います。

minicom は使う前に設定しなければなりません。その方法はいくつもありますが、ここではホームディレクトリにある .minirc.dfl ファイルを使います。~/.minirc.dfl に次の内容のファイルを作成してください:

$ cat ~/.minirc.dfl
pu baudrate 115200
pu bits 8
pu parity N
pu stopbits 1
pu rtscts No
pu xonxoff No

このファイルの末尾が改行で終わっていることを確認してください!そうしないと、minicom はこれを読み取れません。

このファイルは(最後の 2 行を除けば)簡単に読めるはずですが、それでも 1 行ずつ見ていきましょう:

  • pu baudrate 115200. ボーレートを 115200 bps に設定します。
  • pu bits 8. 1 フレームあたり 8 ビットです。
  • pu parity N. パリティチェックなし。
  • pu stopbits 1. ストップビット 1。
  • pu rtscts No. ハードウェアフロー制御なし。
  • pu xonxoff No. ソフトウェアフロー制御なし。

これで準備ができたので、minicom を起動できます。

$ # 注 ここでは別のデバイスを使う必要があるかもしれません
$ minicom -D /dev/ttyACM0 -b 115200

これは、minicom/dev/ttyACM0 のシリアルデバイスを開かせ、 ボーレートを 115200 に設定させるものです。テキストベースのユーザーインターフェース(TUI)が表示されます。

これで、キーボードを使ってデータを送信できるようになりました!何か入力してみてください。なお、 テキスト UI は、入力した内容を 表示しません。ただし、micro:bit の上部にある黄色の LED を 見ていると、何か入力するたびに点滅することに気づくでしょう。

minicom のコマンド

minicom には、キーボードショートカットで使えるコマンドがあります。Linux では、ショートカットは Ctrl+A で始まります。Mac では ショートカットは Meta キーで始まります。以下は便利なコマンドです:

  • Ctrl+A + Z. Minicom コマンド概要
  • Ctrl+A + C. 画面をクリア
  • Ctrl+A + X. 終了してリセット
  • Ctrl+A + Q. リセットせずに終了

Mac ユーザー: 上記のコマンドでは、Ctrl+AMeta に置き換えてください。

Meta キーが見つかりませんか?

Windows ツール

まず、micro:bit を取り外してください。

micro:bit を接続する前に、ターミナルで次のコマンドを実行してください。

$ mode

これにより、コンピューターに接続されているデバイスの一覧が表示されます。名前が COM で始まるものはシリアルデバイスです。これが今回扱う種類のデバイスです。シリアルモジュールを接続する前に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 が点滅します。キーを 1 回押すごとに、LED が 1 回点滅するはずです。なお、コンソールは入力した内容をエコーバックしないため、画面は空白のままです。

macOS での Minicom

コマンドキーを確認する

$ # minicom をセットアップモードで起動
$ minicom -s
  1. Keyboard and Misc Functions を選択します
  2. 最初のオプション A - Command key is を確認します

UART

マイクロコントローラには UART というペリフェラルがあり、これは Universal Asynchronous Receiver/Transmitter の略です。このペリフェラルは、 シリアル通信プロトコルのようないくつかの通信プロトコルで動作するように設定できます。

この章全体を通して、マイクロコントローラとコンピューターの間で情報をやり取りするために、 シリアル通信を使用します。

注意 micro:bit v2 では、いわゆる UARTE ペリフェラルを使用します。これは 通常の UART と同じように振る舞いますが、HAL はこれを異なる方法で扱う必要があります。 ただし、これはもちろん私たちが気にする必要のあることではありません。

セットアップ

これまでと同様に、以後は使用している micro:bit のバージョンに合わせて 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 = 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 panic_rtt_target as _;
use rtt_target::rtt_init_print;

#[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 = "v1")]
use embedded_io::Write;

#[cfg(feature = "v2")]
use embedded_hal_nb::serial::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 = {
        // Set up UART for microbit v1
        let serial = uart::Uart::new(
            board.UART0,
            board.uart.into(),
            Parity::EXCLUDED,
            Baudrate::BAUD115200,
        );
        serial
    };

    #[cfg(feature = "v2")]
    let mut serial = {
        // Set up UARTE for microbit v2 using UartePort wrapper
        let serial = uarte::Uarte::new(
            board.UARTE0,
            board.uart.into(),
            Parity::EXCLUDED,
            Baudrate::BAUD115200,
        );
        UartePort::new(serial)
    };

    // Write a byte and flush
    #[cfg(feature = "v1")]
    serial.write(&[b'X']).unwrap(); // Adjusted for UART on v1, no need for nb::block!

    #[cfg(feature = "v2")]
    {
        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 をうまくラップして、embedded_hal::serial トレイトを通じて UART とまったく同じように使えるようにすることです。必要であればこのモジュールが具体的に何をしているか確認してもかまいませんが、この章を理解するうえでは必須ではありません。

それらの違いを除けば、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 ペリフェラルやピンを勝手に触ることができなくなります。その後、2 つの設定オプションをコンストラクタに渡します。1 つは baudrate(これはもうおなじみでしょう)、もう 1 つは「parity」と呼ばれるオプションです。Parity は、シリアル通信線が受信したデータが伝送中に破損していないか確認できるようにする仕組みです。ここではそれを使いたくないので、単に除外します。次に、それを 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 章と同じようにプログラムを書き込めます:

# micro:bit v2 の場合
$ cargo embed --features v2 --target thumbv7em-none-eabihf
  (...)

# micro:bit v1 の場合
$ cargo embed --features v1 --target thumbv6m-none-eabi

書き込みが完了したら、minicom/PuTTY のターミナルに文字 X が表示されるはずです。おめでとうございます!

文字列を送信する

次の課題は、マイクロコントローラーからコンピューターに文字列全体を送信することです。

マイクロコントローラーからコンピューターに文字列 "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);
    }
}

バイト送信プログラムと比べて変更された箇所は、main() の最後にあるループだけです。ここでは embedded-hal が提供する read() 関数を使って、1バイトが利用可能になるまで待機し、それを読み取ります。続いて、そのバイトを RTT デバッグコンソールに表示し、実際にデータが届いているかどうかを確認します。

なお、このプログラムを書き込んで minicom 内で文字を入力し、それをマイクロコントローラーに送信すると、RTT コンソールには数値しか表示されません。これは、受信した u8 を実際の char に変換していないためです。u8 から char への変換はとても簡単なので、RTT コンソール上で文字を表示したい場合は、この作業は皆さんにお任せします。

エコーサーバー

送信と受信を1つのプログラムにまとめて、エコーサーバーを書いてみましょう。エコー サーバーは、受信したものと同じテキストをクライアントに送り返します。このアプリケーションでは、マイクロコントローラー がサーバーになり、あなたとあなたのコンピューターがクライアントになります。

これは簡単に実装できるはずです。(ヒント: 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;
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();

        loop {
            // 受信は失敗しないものと仮定する
            let byte = nb::block!(serial.read()).unwrap();

            if buffer.push(byte).is_err() {
                write!(serial, "error: buffer full\r\n").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 ボードには、2 つのモーションセンサーが搭載されています。1 つは加速度計、 もう 1 つは磁力計です。これら 2 つのセンサーは 1 つのコンポーネントにまとめられており、 I2C バス経由でアクセスできます。

I2C は Inter-Integrated Circuit の略で、同期式シリアル通信プロトコルです。データのやり取りには 2 本の線を使います。データ線(SDA)とクロック線(SCL)です。通信の同期にクロック線が 使われるため、これは 同期式 プロトコルです。

このプロトコルは、コントローラ ターゲット モデルを使用します。コントローラは、ターゲットデバイスとの通信を 開始し、その通信を進める側のデバイスです。コントローラとターゲットの両方を含む複数のデバイスを、 同時に同じバスへ接続できます。コントローラデバイスは、まず対象のアドレスをバスにブロードキャストすることで、特定のターゲット デバイスと通信できます。このアドレスは 7 ビットまたは 10 ビット長です。 いったんコントローラがターゲットとの通信を開始すると、コントローラがその通信を停止するまで、 ほかのどのデバイスもそのバスを利用できません。

クロック線は、データをどれだけ高速にやり取りできるかを決定し、通常は 100 kHz(標準モード)または 400 kHz(高速モード)の周波数で動作します。

一般的なプロトコル

I2C プロトコルは、複数のデバイス間の通信をサポートする必要があるため、シリアル通信プロトコルよりも複雑です。例を使って、その仕組みを見ていきましょう。

Controller -> Target

Controller が Target にデータを送信したい場合:

  1. Controller: START をブロードキャストする
  2. C: ターゲットアドレス(7 ビット)+ R/W(8 ビット目)ビットを WRITE に設定してブロードキャストする
  3. Target: ACK(確認応答)を返す
  4. C: 1 バイト送信する
  5. T: ACK を返す
  6. 手順 4 と 5 を 0 回以上繰り返す
  7. C: STOP をブロードキャストする OR(RESTART をブロードキャストして (2) に戻る)

ターゲットアドレスは 7 ビット長ではなく 10 ビット長であることもあります。それ以外は何も変わりません。

Controller <- Target

controller が target からデータを読み取りたい場合:

  1. C: START をブロードキャストする
  2. C: ターゲットアドレス(7 ビット)+ R/W(8 ビット目)ビットを READ に設定してブロードキャストする
  3. T: ACK を返す
  4. T: バイトを送信する
  5. C: ACK を返す
  6. 手順 4 と 5 を 0 回以上繰り返す
  7. C: STOP をブロードキャストする OR(RESTART をブロードキャストして (2) に戻る)

ターゲットアドレスは 7 ビット長ではなく 10 ビット長であることもあります。それ以外は何も変わりません。

LSM303AGR

micro:bit 上の 2 つのモーションセンサー、つまり磁力計と加速度計は、1 つの コンポーネントにまとめて実装されています。それが LSM303AGR 集積回路です。これら 2 つの センサーには I2C バス経由でアクセスできます。それぞれの センサーは I2C ターゲットのように振る舞い、異なる アドレスを持ちます。

各センサーは、それぞれ周囲の環境をセンシングした結果を保存する独自のメモリを持っています。私たちが これらのセンサーとやり取りするときは、主にそのメモリを読み取ることになります。

これらのセンサーのメモリは、バイト単位でアドレス指定可能なレジスタとしてモデル化されています。これらのセンサーは 設定することもできますが、それはレジスタへの書き込みによって行います。したがって、ある意味では、これらのセンサーは マイクロコントローラー 内部 の周辺機器と非常によく似ています。違いは、それらのレジスタが マイクロコントローラーのメモリにマップされていないことです。その代わりに、それらのレジスタには I2C バス経由でアクセスしなければなりません。

LSM303AGR に関する主な情報源は、その Data Sheet です。目を通して、 センサーのレジスタをどのように読み取れるかを確認してください。その部分は次にあります:

セクション 6.1.1 I2C の動作 - 38 ページ - LSM303AGR データシート

この本に関連するドキュメントのもう 1 つの部分は、レジスタの説明です。その 部分は次にあります:

セクション 8 レジスタの説明 - 46 ページ - LSM303AGR データシート

単一のレジスタを読み取る

これまでの理論をすべて実践に移してみましょう!

まず最初に、チップ内の加速度計と磁力計の両方の対象アドレスを知る必要があります。 これらは LSM303AGR のデータシートの 39 ページに記載されており、次のとおりです:

  • 加速度計は 0011001
  • 磁力計は 0011110

これらはアドレスの先頭 7 ビットにすぎないことを忘れないでください。 8 ビット目は、読み取りを行うのか書き込みを行うのかを 決定するビットになります。

次に、読み取るレジスタが必要です。世の中にある多くの I2C チップには、 コントローラーが読み取るための何らかのデバイス識別レジスタが用意されています。 これは、世の中に何千(あるいは何百万)もの I2C チップが存在することを考えると、 いつか同じアドレスを持つ 2 つのチップが作られる可能性が非常に高いためです (結局のところ、アドレス幅は「たった」7 ビットしかありません)。この デバイス ID レジスタがあれば、ドライバーは実際に LSM303AGR と通信しているのであって、 たまたま同じアドレスを持つ別のチップではないことを確認できます。 LSM303AGR のデータシート(具体的には 46 ページと 61 ページ)を読むとわかるように、 このデバイスには 0x0f 番地の WHO_AM_I_A0x4f 番地の WHO_AM_I_M という 2 つのレジスタがあり、そこにはデバイス固有のビットパターンが含まれています (A は accelerometer、M は magnetometer を表します)。

ここで不足しているのはソフトウェア側、つまりこのために microbit / HAL クレートのどの API を使うべきか、という点だけです。しかし、使用している nRF チップの データシートを読むと、実際には I2C ペリフェラルを持っていないことがすぐにわかります。 とはいえ幸いなことに、UART と UARTE の場合と同じく、使用するチップに応じて TWI (Two Wire Interface) と TWIM という I2C 互換のものが用意されています。

ここまでに集めたほかのすべての情報と microbit クレートの twi(m) module の ドキュメントを組み合わせると、2 つのデバイス 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 {}
}

初期化を除けば、前述の I2C プロトコルを理解していれば、このコードはわかりやすいはずです。 ここでの初期化は UART の章のものと同様に機能します。 コンストラクタには、ペリフェラルに加えてチップとの通信に使用するピンを渡し、 その後でバスを動作させたい周波数を指定します。この場合は 100 kHz (K100) です。

テストする

いつものように、Embed.toml を自分の MCU に合うように変更し、その後で次を使用できます:

# micro:bit v2 の場合
$ cargo embed --features v2 --target thumbv7em-none-eabihf

# micro:bit v1 の場合
$ cargo embed --features v1 --target thumbv6m-none-eabi

これで、この小さなサンプルプログラムをテストできます。

ドライバーを使う

すでに第 5 章で説明したように、embedded-hal はハードウェアとやり取りできるプラットフォーム非依存のコードを書くために使える抽象化を提供します。実際、第 7 章でハードウェアとやり取りするために使ったメソッド、そしてここまでの第 8 章で使ってきたメソッドは、すべて embedded-hal で定義されたトレイトに由来するものでした。ここで初めて、embedded-hal が提供するトレイトを実際に活用します。

embedded 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);
        }
    }
}

前のスニペットと同じように、これは次のようにそのまま試せるはずです。

# micro:bit v2 の場合
$ cargo embed --features v2 --target thumbv7em-none-eabihf

# micro:bit v1 の場合
$ cargo embed --features v1 --target thumbv6m-none-eabi

さらに、micro:bit を少し(物理的に)動かしてみると、表示される加速度の数値が変化するのが分かるはずです。

チャレンジ

この章の課題は、前の章で導入したシリアルインターフェースを介して外部と通信する小さなアプリケーションを作成することです。このアプリケーションは、"magnetometer" および "accelerometer" というコマンドを受信でき、それに応じて対応するセンサーデータを出力できる必要があります。今回は必要なものがすでに UART とこの章で提供されているため、テンプレートコードは用意されていません。ただし、いくつかヒントを挙げます。

  • バッファ内のバイト列を &str に変換するために core::str::from_utf8 が役に立つかもしれません。これは、"magnetometer" および "accelerometer" と比較する必要があるためです。
  • (当然ながら)magnetometer API のドキュメントを読む必要がありますが、 これは accelerometer のものとだいたい同等です

私の解答

#![no_main]
#![no_std]

use core::str;

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::Vec;
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, 32> = Vec::new();

        loop {
            let byte = block!(serial.read()).unwrap();

            if byte == 13 {
                break;
            }

            if buffer.push(byte).is_err() {
                write!(serial, "error: buffer full\r\n").unwrap();
                break;
            }
        }

        if str::from_utf8(&buffer).unwrap().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 str::from_utf8(&buffer).unwrap().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 の 1 つを点灯させます。点灯する LED は北の方向を向いている必要があります。

磁場には、ガウスまたはテスラで測定される大きさと、方向の両方があります。micro:bit 上の 磁力計は、外部磁場の大きさと方向の両方を測定しますが、返してくるのは、その磁場を その軸 に沿って 分解 した値です。

磁力計には、対応する 3 つの軸があります。X 軸と Y 軸は、基本的には床である平面を張っています。 Z 軸は床から「外」へ、つまり上向きに伸びています。

I2C の章で、RTT コンソールに磁力計のデータを継続的に表示するプログラムを すでに書けるはずです。そのプログラムを書いたら、今いる場所で北がどこにあるかを確認してください。その後、micro:bit を その方向に合わせて、センサーの測定値がどのように見えるか観察してください。

次に、基板を地面と平行に保ったまま 90 度回転させてください。このとき、X、Y、Z の値は どう見えますか? さらにもう一度 90 度回転させると、どのような値が見えますか?

キャリブレーション

センサーを使用し、それを使ったアプリケーションを開発しようとする前に行うべき非常に重要なことの 1 つは、その出力が実際に正しいことを検証することです。 そうでない場合は、センサーをキャリブレーションする必要があります (あるいは壊れている可能性もありますが、この場合はその可能性はかなり低いです)。

私の場合、2 台の異なる micro:bit で、キャリブレーションを行わない状態の magnetometer は、 本来測定すべき値からかなりずれていました。そのため、この章では センサーはキャリブレーションしなければならないものと仮定します。

キャリブレーションにはかなり多くの数学(行列)が関わるため、ここでは扱いませんが、 興味があれば Design Note に手順が説明されています。

ありがたいことに、micro:bit 向けの元のソフトウェアを作成したグループが、 すでに here で C++ によるキャリブレーション機構を実装しています。

その Rust への移植版は src/calibration.rs にあります。使い方は デフォルトの src/main.rs ファイルで示されています。キャリブレーションの 動作はこの動画で説明されています。

基本的には、LED マトリクス上のすべての LED が点灯するまで micro:bit を傾ける必要があります。

開発中にアプリケーションを再起動するたびに毎回このゲームをやりたくない場合は、 最初の 1 回目が済んだら、同じ静的なキャリブレーションを使うように src/main.rs テンプレートを自由に変更してください。

これでセンサーのキャリブレーションは片付いたので、 次は実際にこのアプリケーションを作っていきましょう!

第1案

LED コンパスを、完璧ではなくても、もっとも単純な方法で実装するにはどうすればよいでしょうか?

まずは、磁場の X 成分と Y 成分だけを気にすれば十分です。というのも、コンパスを見るときは常に水平に持つため、コンパスは XY 平面内にあるからです。

X 成分と Y 成分の符号だけを見れば、磁場がどの象限に属しているかを判定できます。ここで当然問題になるのは、4 つの象限がそれぞれどの方角(北、北東など)を表すのか、ということです。これを確かめるには、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) {
            // 象限 ???
            (true, true) => Direction::NorthEast,
            // 象限 ???
            (false, true) => panic!("TODO"),
            // 象限 ???
            (false, false) => panic!("TODO"),
            // 象限 ???
            (true, false) => panic!("TODO"),
        };

        // led モジュールを使って方角を LED の矢印に変換し
        // それを第 5 章の 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) {
            // 第1象限
            (true, true) => Direction::NorthEast,
            // 第2象限
            (false, true) => Direction::NorthWest,
            // 第3象限
            (false, false) => Direction::SouthWest,
            // 第4象限
            (true, false) => Direction::SouthEast,
        };

        // led モジュールを使用して方向を LED の矢印に変換し、
        // 第 5 章の 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;

// これは便利です ;-)
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);

        // これはまだ core にないので libm の atan2f を使います
        let theta = atan2f(data.y as f32, data.x as f32);

        // theta に基づいて方角を判定する
        let dir = Direction::NorthEast;

        display.show(&mut timer, direction_to_led(dir), 100);
    }
}

ヒント:

  • 1 回転は 360 度です。
  • PI ラジアンは 180 度に相当します。
  • theta が 0 なら、どの方向を指していますか?
  • 逆に theta が 0 に非常に近い値なら、どの方向を指していますか?
  • 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;

// これは役に立ちます ;-)
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);

        // これはまだ core にないため、libm の atan2f を使用する
        let theta = atan2f(data.y as f32, data.x as f32);

        // theta に基づいて方向を判定する
        let dir = if theta < -7. * PI / 8. {
            Direction::West
        } else if theta < -5. * PI / 8. {
            Direction::SouthWest
        } else if theta < -3. * PI / 8. {
            Direction::South
        } else if theta < -PI / 8. {
            Direction::SouthEast
        } else if theta < PI / 8. {
            Direction::East
        } else if theta < 3. * PI / 8. {
            Direction::NorthEast
        } else if theta < 5. * PI / 8. {
            Direction::North
        } else if theta < 7. * PI / 8. {
            Direction::NorthWest
        } else {
            Direction::West
        };

        display.show(&mut timer, direction_to_led(dir), 100);
    }
}

大きさ

これまでは磁場の方向を扱ってきましたが、その実際の大きさはどれくらいなのでしょうか? mag_data() 関数のドキュメントによると、取得している x y z の値は ナノテスラ単位です。つまり、磁場の大きさをナノテスラで求めるために計算しなければならないのは、 x y z の値が表している 3D ベクトルの大きさだけです。学校で習ったように、これは単純に次のようになります。

#![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 chapter で、加速度センサーのデータを RTT コンソールに継続的に表示するプログラムは、すでに書けるようになっているはずです。LED 側を下にしてボードを床と平行に持ったときでも、何か興味深いことが観察できますか?

このように見えるはずなのは、X と Y の値はどちらも 0 にかなり近い一方で、Z の値はおよそ 1000 になっていることです。ボードは動いていないのに加速度が 0 ではないので、これは奇妙です。何が起きているのでしょうか?これは重力に関係しているに違いありませんよね?重力加速度は 1 g です(なるほど、1 g = 加速度センサーの 1000)。しかし、重力は物体を下向きに引っ張るので、Z 軸方向の加速度は正ではなく負であるはずです。

プログラムが Z 軸を逆に扱ってしまったのでしょうか?いいえ。ボードを回転させて重力を X 軸や Y 軸に揃えて試すことはできますが、加速度センサーが測定する加速度は常に上向きです。

ここで起きているのは、加速度センサーが測定しているのは proper acceleration、つまりボードの固有加速度であって、あなた が観測している加速度ではない、ということです。この固有加速度は、自由落下している観測者から見たボードの加速度です。自由落下している観測者は、1g の加速度で地球の中心へ向かって移動しています。その観点から見ると、ボードは実際には 1g の加速度で上向き(地球の中心から遠ざかる向き)に動いています。だからこそ、固有加速度は上向きになるのです。これはまた、もしボードが自由落下していたなら、加速度センサーは固有加速度 0 を報告することも意味します。どうか、家では試さないでください。

はい、物理は難しいです。先へ進みましょう。

課題

話を簡単にするため、ボードを水平に保ったまま、加速度は X 軸方向だけを測定します。そうすれば、先ほど観測した 見かけの 1g を差し引く必要がありません。ボードの向きによって、その 1g は X、Y、Z の成分を持ちうるため、差し引くのは難しいからです。

punch-o-meter が満たすべき要件は次のとおりです。

  • デフォルトでは、アプリはボードの加速度を「観測」していません。
  • 大きな X 軸加速度が検出されたとき(つまり、加速度があるしきい値を超えたとき)、アプリは新しい測定を開始する必要があります。
  • その測定区間のあいだ、アプリは観測された最大加速度を追跡し続ける必要があります
  • 測定区間が終了したら、アプリは観測された最大加速度を報告しなければなりません。この値は rprintln! マクロを使って報告できます。

試してみて、どれくらい強くパンチできるか教えてください ;-)

この課題で役立つ、まだ説明していない API が 2 つあります。 1 つ目は、高い g 値を測定するのに必要な set_accel_scale です。 2 つ目は、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();
    // 人のパンチは実際かなり速くなり得るため、センサーが最大 16 G まで
    // 測定できるようにする
    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 {
            // カウントダウンの状態を確認する
            match countdown.wait() {
                // カウントダウンはまだ完了していない
                Err(Error::WouldBlock) => {
                    if g_x > max_g {
                        max_g = g_x;
                    }
                },
                // カウントダウンが完了した
                Ok(_) => {
                    // 最大値を報告する
                    rprintln!("最大加速度: {}g", max_g);

                    // リセット
                    max_g = 0.;
                    measuring = false;
                },
                // nrf52 と nrf51 の HAL ではエラー型として Void を使用しているため
                // Void は空型なので、この経路は発生しない
                Err(Error::Other(_)) => {
                    unreachable!()
                }
            }
        } else {
            // 加速度がしきい値を超えたら、測定を開始する
            if g_x > THRESHOLD {
                rprintln!("開始!");

                measuring = true;
                max_g = g_x;
                // ドキュメントによると、このタイマーは 1 MHz の周波数で動作するため、
                // 1 秒待機するには
                // 1_000_000 ティックに設定する必要がある。
                countdown.start(1_000_000_u32);
            }
        }
        delay.delay_ms(20_u8);
    }
}

Snake game

これから、micro:bit v2 の 5x5 LED マトリクスをディスプレイとして使い、2 つのボタンを操作に使って遊べる、基本的な snake ゲームを実装します。これにより、この本の前の 章で扱ったいくつかの概念を基にしつつ、新しいペリフェラルや概念についても学びます。

特に、ハードウェア割り込みの概念を使って、プログラムが複数の ペリフェラルを同時に扱えるようにします。割り込みは、組み込みの文脈で並行性を実装する一般的な方法です。組み込みの文脈における並行性については、Embedded Rust Book に良い 入門があるので、先にそれを読んでおくことをお勧めします。

NOTE この章は micro:bit v2 のみを対象に作成されており、v1 には対応していません。コードを v1 に移植するためのコントリビューションは歓迎します。

NOTE この章では、前の 章で使用したものより新しいバージョンの特定のライブラリを使用します。microbit ライブラリはバージョン 0.13.0 を使用します(前の章では 0.12.0 を使用していました)。 バージョン 0.13.0 では、これから使用するノンブロッキングのディスプレイコードに関するいくつかのバグが修正されています。また、heapless ライブラリはバージョン 0.8.0 を使用します(前の章ではバージョン 0.7.10 を使用していました)。これにより、Rust の core::Hash トレイトを実装した構造体で、その データ構造の一部を使用できるようになります。

ゲームロジック

まず、ゲームロジックを説明します。おそらくスネークゲームはご存じだと思いますが、そうでない場合は、基本的な考え方として プレイヤーが 2D グリッド上でヘビを動かします。任意の時点で、グリッド上のランダムな場所にいくらかの「餌」があり、 ゲームの目的はヘビにできるだけ多くの餌を「食べ」させることです。ヘビが餌を食べるたびに、 その長さは伸びます。ヘビが自分自身のしっぽに衝突すると、プレイヤーの負けです。ゲームのバリアントによっては、ヘビが グリッドの端に衝突した場合にもプレイヤーの負けになりますが、今回のグリッドは小さいため、 「ラップアラウンド」ルールを実装します。これは、ヘビがグリッドの一方の端から外に出た場合、 反対側の端から続けて現れる、というものです。

game モジュール

このセクションのコードは、src ディレクトリ内の別ファイル game.rs に記述してください。

#![allow(unused)]
fn main() {
use heapless::FnvIndexSet;

/// グリッド上の 1 点。
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct Coords {
   // 負の値を扱えるように符号付き整数を使用する(グリッドの上端または
   // 左端から外に出たかどうかを確認するときに便利)
   row: i8,
   col: i8
}

impl Coords {
   /// グリッド内のランダムな座標を取得する。`exclude` は、
   /// 出力から除外すべき座標の省略可能な集合。
   fn random(
      rng: &mut Prng,  // `Prng` 構造体はこの後で定義する
      exclude: Option<&FnvIndexSet<Coords, 32>>
   ) -> Self {
      let mut coords = Coords {
         row: ((rng.random_u32() as usize) % 5) as i8,
         col: ((rng.random_u32() as usize) % 5) as i8
      };
      while exclude.is_some_and(|exc| exc.contains(&coords)) {
         coords = Coords {
            row: ((rng.random_u32() as usize) % 5) as i8,
            col: ((rng.random_u32() as usize) % 5) as i8
         }
      }
      coords
   }

   /// その点がグリッドの範囲外にあるかどうか。
   fn is_out_of_bounds(&self) -> bool {
      self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5
   }
}
}

グリッド上の位置を表すために Coords 構造体を使います。Coords には 2 つの整数しか含まれていないため、 コンパイラに対して Copy トレイトの実装を derive するよう指定し、所有権を気にすることなく Coords 構造体を受け渡しできるようにします。

関連関数 Coords::random を定義すると、グリッド上のランダムな位置を取得できます。これは後で、 ヘビの餌をどこに配置するかを決めるために使用します。そのためには、乱数の供給源が必要です。nRF52833 には 乱数生成器(RNG)ペリフェラルがあり、仕様書 の第 6.19 節に記載されています。HAL は microbit::hal::rng::Rng 構造体を介して RNG へのシンプルなインターフェイスを提供しています。しかし、これは ブロッキングなインターフェイスであり、1 バイトの乱数データを生成するのに必要な時間は可変で予測できません。 そのため、xorshift アルゴリズムを使って疑似乱数の u32 値を生成する 疑似乱数 生成器(PRNG)を定義します。これを使って、餌をどこに配置するかを決定できます。 このアルゴリズムは基本的なもので、暗号学的に安全ではありませんが、効率的で実装が簡単であり、 私たちのささやかなスネークゲームには十分です。Prng 構造体には初期シード値が必要で、これは RNG ペリフェラルから取得します。

#![allow(unused)]
fn main() {
/// 基本的な疑似乱数生成器。
struct Prng {
    value: u32
}

impl Prng {
    fn new(seed: u32) -> Self {
        Self {value: seed}
    }

    /// 基本的な xorshift PRNG 関数: https://en.wikipedia.org/wiki/Xorshift を参照
    fn xorshift32(mut input: u32) -> u32 {
        input ^= input << 13;
        input ^= input >> 17;
        input ^= input << 5;
        input
    }

    /// 疑似乱数の `u32` を返す。
    fn random_u32(&mut self) -> u32 {
        self.value = Self::xorshift32(self.value);
        self.value
    }
}
}

ゲームの状態管理に役立ついくつかの enum も定義する必要があります。移動方向、曲がる方向、 現在のゲーム状態、そしてゲーム内の特定の「ステップ」(つまり、ヘビの 1 回の移動)の結果です。

#![allow(unused)]
fn main() {
/// ヘビが移動できる方向を定義する。
enum Direction {
    Up,
    Down,
    Left,
    Right
}

/// ヘビがどちらの方向に曲がるべきか。
#[derive(Debug, Copy, Clone)]
pub enum Turn {
    Left,
    Right,
    None
}

/// 現在のゲーム状態。
pub enum GameStatus {
    Won,
    Lost,
    Ongoing
}

/// 1 回の移動/ステップの結果。
enum StepOutcome {
    /// グリッドが満杯(プレイヤーの勝利)
    Full(Coords),
    /// ヘビが自分自身に衝突した(プレイヤーの敗北)
    Collision(Coords),
    /// ヘビが餌を食べた
    Eat(Coords),
    /// ヘビが移動した(ほかには何も起きていない)
    Move(Coords)
}
}

次に Snake 構造体を定義します。これは、ヘビが占有している座標と進行方向を追跡します。 座標の順序を追跡するためにキュー(heapless::spsc::Queue)を使い、高速な衝突判定を可能にするために ハッシュセット(heapless::FnvIndexSet)を使います。Snake には、移動を行うためのメソッドがあります。

#![allow(unused)]
fn main() {
use heapless::spsc::Queue;

// ...

struct Snake {
    /// ヘビの頭の座標。
    head: Coords,
    /// ヘビの残りの胴体の座標を保持するキュー。尾の端は
    /// 先頭にあります。
    tail: Queue<Coords, 32>,
    /// 現在ヘビが占有しているすべての座標を含むセット(高速な
    /// 衝突判定用)。
    coord_set: FnvIndexSet<Coords, 32>,
    /// ヘビが現在移動している方向。
    direction: Direction
}

impl Snake {
    fn new() -> Self {
        let head = Coords { row: 2, col: 2 };
        let initial_tail = Coords { row: 2, col: 1 };
        let mut tail = Queue::new();
        tail.enqueue(initial_tail).unwrap();
        let mut coord_set: FnvIndexSet<Coords, 32> = FnvIndexSet::new();
        coord_set.insert(head).unwrap();
        coord_set.insert(initial_tail).unwrap();
        Self {
            head,
            tail,
            coord_set,
            direction: Direction::Right,
        }
    }

    /// ヘビを指定した座標のタイルへ移動します。`extend` が false の場合、
    /// ヘビの尾は最後尾のタイルを空けます。
    fn move_snake(&mut self, coords: Coords, extend: bool) {
        // 頭の位置が尾の先頭になる
        self.tail.enqueue(self.head).unwrap();
        // 頭が新しい座標へ移動する
        self.head = coords;
        self.coord_set.insert(coords).unwrap();
        if !extend {
            let back = self.tail.dequeue().unwrap();
            self.coord_set.remove(&back);
        }
    }

    fn turn_right(&mut self) {
        self.direction = match self.direction {
            Direction::Up => Direction::Right,
            Direction::Down => Direction::Left,
            Direction::Left => Direction::Up,
            Direction::Right => Direction::Down
        }
    }

    fn turn_left(&mut self) {
        self.direction = match self.direction {
            Direction::Up => Direction::Left,
            Direction::Down => Direction::Right,
            Direction::Left => Direction::Down,
            Direction::Right => Direction::Up
        }
    }

    fn turn(&mut self, direction: Turn) {
        match direction {
            Turn::Left => self.turn_left(),
            Turn::Right => self.turn_right(),
            Turn::None => ()
        }
    }
}
}

Game 構造体はゲームの状態を追跡します。これには Snake オブジェクト、現在のエサの座標、ゲームの速度(ヘビの各移動の間に経過する時間を決定するために使用されます)、ゲームの状態(ゲームが進行中か、プレイヤーが勝利または敗北したか)、およびプレイヤーのスコアが含まれます。

この構造体には、ゲームの各ステップを処理し、ヘビの次の動きを決定して、それに応じてゲームの状態を更新するメソッドが含まれています。また、game_matrixscore_matrix という 2 つのメソッドも含まれており、ゲームの状態やプレイヤーのスコアを LED マトリクスに表示するために使用できる値の 2D 配列を出力します(これについては後で見ていきます)。

#![allow(unused)]
fn main() {
/// ゲームの状態と関連する振る舞いを保持する構造体
pub(crate) struct Game {
    rng: Prng,
    snake: Snake,
    food_coords: Coords,
    speed: u8,
    pub(crate) status: GameStatus,
    score: u8
}

impl Game {
    pub(crate) fn new(rng_seed: u32) -> Self {
        let mut rng = Prng::new(rng_seed);
        let mut tail: FnvIndexSet<Coords, 32> = FnvIndexSet::new();
        tail.insert(Coords { row: 2, col: 1 }).unwrap();
        let snake = Snake::new();
        let food_coords = Coords::random(&mut rng, Some(&snake.coord_set));
        Self {
            rng,
            snake,
            food_coords,
            speed: 1,
            status: GameStatus::Ongoing,
            score: 0
        }
    }

    /// 新しいゲームを開始するためにゲームの状態をリセットする。
    pub(crate) fn reset(&mut self) {
        self.snake = Snake::new();
        self.place_food();
        self.speed = 1;
        self.status = GameStatus::Ongoing;
        self.score = 0;
    }

    /// グリッド上にランダムに餌を配置する。
    fn place_food(&mut self) -> Coords {
        let coords = Coords::random(&mut self.rng, Some(&self.snake.coord_set));
        self.food_coords = coords;
        coords
    }

    /// 範囲外の座標を「折り返す」(例: グリッドの左側にはみ出した
    /// 座標は、最も右の列に現れる)。座標が範囲外になるのは
    /// 1 次元のみであると仮定する。
    fn wraparound(&self, coords: Coords) -> Coords {
        if coords.row < 0 {
            Coords { row: 4, ..coords }
        } else if coords.row >= 5 {
            Coords { row: 0, ..coords }
        } else if coords.col < 0 {
            Coords { col: 4, ..coords }
        } else {
            Coords { col: 0, ..coords }
        }
    }

    /// ヘビが次に移動するマスを決定する(実際には
    /// ヘビを移動させない)。
    fn get_next_move(&self) -> Coords {
        let head = &self.snake.head;
        let next_move = match self.snake.direction {
            Direction::Up => Coords { row: head.row - 1, col: head.col },
            Direction::Down => Coords { row: head.row + 1, col: head.col },
            Direction::Left => Coords { row: head.row, col: head.col - 1 },
            Direction::Right => Coords { row: head.row, col: head.col + 1 },
        };
        if next_move.is_out_of_bounds() {
            self.wraparound(next_move)
        } else {
            next_move
        }
    }

    /// ヘビの次の移動を評価し、その結果を返す。実際には
    /// ゲームの状態を更新しない。
    fn get_step_outcome(&self) -> StepOutcome {
        let next_move = self.get_next_move();
        if self.snake.coord_set.contains(&next_move) {
            // まだヘビを移動させていないので、次の移動先が
            // 尻尾の末端であれば、実際には衝突は発生しない
            // (頭がそのマスに移動するまでに尻尾が動いているため)
            if next_move != *self.snake.tail.peek().unwrap() {
                StepOutcome::Collision(next_move)
            } else {
                StepOutcome::Move(next_move)
            }
        } else if next_move == self.food_coords {
            if self.snake.tail.len() == 23 {
                StepOutcome::Full(next_move)
            } else {
                StepOutcome::Eat(next_move)
            }
        } else {
            StepOutcome::Move(next_move)
        }
    }

    /// 1 ステップの結果を処理し、ゲームの内部状態を更新する。
    fn handle_step_outcome(&mut self, outcome: StepOutcome) {
        self.status = match outcome {
            StepOutcome::Collision(_) => GameStatus::Lost,
            StepOutcome::Full(_) => GameStatus::Won,
            StepOutcome::Eat(c) => {
                self.snake.move_snake(c, true);
                self.place_food();
                self.score += 1;
                if self.score % 5 == 0 {
                    self.speed += 1
                }
                GameStatus::Ongoing
            },
            StepOutcome::Move(c) => {
                self.snake.move_snake(c, false);
                GameStatus::Ongoing
            }
        }
    }

    pub(crate) fn step(&mut self, turn: Turn) {
        self.snake.turn(turn);
        let outcome = self.get_step_outcome();
        self.handle_step_outcome(outcome);
    }

    /// ゲームの各ステップ間で待機する時間をミリ秒単位で計算する。
    /// 一般に、プレイヤーのスコアが増えるほどこの値は小さくなるが、
    /// 0 未満の値にならないよう注意する必要がある。
    pub(crate) fn step_len_ms(&self) -> u32 {
        let result = 1000 - (200 * ((self.speed as i32) - 1));
        if result < 200 {
            200u32
        } else {
            result as u32
        }
    }

    /// ゲームの状態を表す配列を返す。これは
    /// microbit の LED マトリクスに状態を表示するために使用できる。各 `_brightness`
    /// パラメータは 0 から 9 までの値である必要がある。
    pub(crate) fn game_matrix(
        &self,
        head_brightness: u8,
        tail_brightness: u8,
        food_brightness: u8
    ) -> [[u8; 5]; 5] {
        let mut values = [[0u8; 5]; 5];
        values[self.snake.head.row as usize][self.snake.head.col as usize] = head_brightness;
        for t in &self.snake.tail {
            values[t.row as usize][t.col as usize] = tail_brightness
        }
        values[self.food_coords.row as usize][self.food_coords.col as usize] = food_brightness;
        values
    }

    /// ゲームのスコアを表す配列を返す。これは
    /// microbit の LED マトリクスにスコアを表示するために使用できる(左->右、
    /// 上->下の順に、対応する数の LED を点灯する)。
    pub(crate) fn score_matrix(&self) -> [[u8; 5]; 5] {
        let mut values = [[0u8; 5]; 5];
        let full_rows = (self.score as usize) / 5;
        for r in 0..full_rows {
            values[r] = [1; 5];
        }
        for c in 0..(self.score as usize) % 5 {
            values[full_rows][c] = 1;
        }
        values
    }
}
}

main ファイル

次のコードを main.rs ファイルに配置してください。

#![no_main]
#![no_std]

mod game;

use cortex_m_rt::entry;
use microbit::{
   Board,
   hal::{prelude::*, Rng, Timer},
   display::blocking::Display
};
use rtt_target::rtt_init_print;
use panic_rtt_target as _;
use crate::game::{Game, GameStatus, Turn};

#[entry]
fn main() -> ! {
   rtt_init_print!();
   let mut board = Board::take().unwrap();
   let mut timer = Timer::new(board.TIMER0);
   let mut rng = Rng::new(board.RNG);
   let mut game = Game::new(rng.random_u32());
   let mut display = Display::new(board.display_pins);

   loop {
      loop {  // ゲームループ
         let image = game.game_matrix(9, 9, 9);
         // 現時点では明るさの値に意味はありません。まだ
         // 異なる明るさを表示できるディスプレイを
         // 実装していないためです
         display.show(&mut timer, image, game.step_len_ms());
         match game.status {
            GameStatus::Ongoing => game.step(Turn::None), // プレースホルダーです。まだ
                                                          // 操作を実装して
                                                          // いないためです
            _ => {
               for _ in 0..3 {
                  display.clear();
                  timer.delay_ms(200u32);
                  display.show(&mut timer, image, 200);
               }
               display.clear();
               display.show(&mut timer, game.score_matrix(), 1000);
               break
            }
         }
      }
      game.reset();
   }
}

ボードとそのタイマーおよび RNG ペリフェラルを初期化した後、Game 構造体と、microbit::display::blocking モジュールの Display を初期化します。

「ゲームループ」(main 関数内に配置した「メインループ」の内側で実行されるループ)では、以下の手順を繰り返し実行します。

  1. グリッドを表す 5x5 のバイト配列を取得します。Game::get_matrix メソッドは 3 つの整数引数を取ります(最終的には 0 から 9 までの値を両端を含めて指定する想定です)。これらは、頭、尾、食べ物をどれくらい明るく表示するかを表します。この時点で使用している基本的な Display は可変の明るさをサポートしていないため、この段階ではそれぞれに 9 を渡しています(ただし、0 以外の値であればどれでも動作します)。
  2. Game::step_len_ms メソッドで決まる時間だけ、その行列を表示します。現在の実装では、基本的にステップ間隔は 1 秒で、プレイヤーが 5 点獲得するごとに 200ms ずつ短くなります(食べ物 1 つを食べると 1 点)。ただし下限は 200ms です。
  3. ゲームの状態を確認します。これが Ongoing(初期値)であれば、ゲームを 1 ステップ進めてゲームの状態(status プロパティを含む)を更新します。そうでなければゲームオーバーなので、現在の画像を 3 回点滅させた後、プレイヤーのスコアを表示します(スコアに対応する数の LED を点灯させた形で表現されます)。その後、ゲームループを終了します。

メインループでは、各反復の後にゲームの状態をリセットしながら、ゲームループを繰り返し実行します。

これを実行すると、ディスプレイの中央の高さに 2 つの LED が点灯しているはずです(中央にヘビの頭、その左に尾があります)。また、ボード上のどこかに別の LED が点灯しているのも見えるはずで、これはヘビの食べ物を表しています。およそ 1 秒ごとに、ヘビは右に 1 マス移動します。

次に、ヘビの動きを操作できるようにします。

操作

主人公は micro:bit の前面にある 2 つのボタンで操作します。ボタン A は(ヘビを) 左に曲げ、ボタン B は(ヘビを)右に曲げます。

ボタンの押下を並行的に処理するために、microbit::pac::interrupt マクロを使用します。割り込みは micro:bit の GPIOTE(General Purpose Input/Output Tasks and Events)ペリフェラルによって生成されます。

controls モジュール

このセクションのコードは、src ディレクトリ内の別ファイル controls.rs に配置してください。

2 つの独立したグローバルな可変状態を追跡する必要があります。1 つは GPIOTE ペリフェラルへの参照、もう 1 つは 次に曲がる向きの選択結果を記録したものです。

#![allow(unused)]
fn main() {
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use microbit::hal::gpiote::Gpiote;
use crate::game::Turn;

// ...

static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None));
static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None));
}

内部可変性を可能にするため、データは RefCell でラップされています。RefCell の詳細については、 そのドキュメントThe Rust Book の該当章を読んでください。 さらに RefCell は、安全にアクセスできるよう cortex_m::interrupt::Mutex でラップされています。 cortex_m クレートが提供する Mutex は、クリティカルセクション の概念を使用します。 Mutex 内のデータには、cortex_m::interrupt:free に渡された関数またはクロージャの内部からしかアクセスできず、これにより その関数またはクロージャ内のコード自体が割り込まれないことが保証されます。

まず、ボタンを初期化します。

#![allow(unused)]
fn main() {
use cortex_m::interrupt::free;
use microbit::{
    board::Buttons,
    pac::{self, GPIOTE}
};

// ...

/// ボタンを初期化し、割り込みを有効化します。
pub(crate) fn init_buttons(board_gpiote: GPIOTE, board_buttons: Buttons) {
    let gpiote = Gpiote::new(board_gpiote);

    let channel0 = gpiote.channel0();
    channel0
        .input_pin(&board_buttons.button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel0.reset_events();

    let channel1 = gpiote.channel1();
    channel1
        .input_pin(&board_buttons.button_b.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel1.reset_events();

    free(move |cs| {
        *GPIO.borrow(cs).borrow_mut() = Some(gpiote);

        unsafe {
            pac::NVIC::unmask(pac::Interrupt::GPIOTE);
        }
        pac::NVIC::unpend(pac::Interrupt::GPIOTE);
    });
}
}

nRF52 上の GPIOTE ペリフェラルには 8 つの「チャネル」があり、それぞれを GPIO ピンに接続して、 立ち上がりエッジ(信号が low から high に遷移)や立ち下がりエッジ(high から low への 遷移)などの特定のイベントに応答するよう設定できます。ボタンは、押されていないときには high 信号で、 押されると low 信号になる GPIO ピンです。したがって、ボタンの押下は立ち下がりエッジです。

channel0button_a に、channel1button_b に接続し、それぞれについて 立ち下がりエッジ(hi_to_lo)でイベントを生成するよう設定します。GPIOTE ペリフェラルへの参照は GPIO Mutex に保存します。 その後、GPIOTE 割り込みを unmask してハードウェアが伝播できるようにし、さらに unpend を呼び出して pending 状態の 割り込みをすべてクリアします(これらは割り込みが unmask される前に生成されていた可能性があります)。

次に、割り込みを処理するコードを書きます。microbit::pac が提供する interrupt マクロを使用します(v2 の 場合は、nrf52833_hal クレートから再エクスポートされています)。処理したい割り込みと同じ名前の関数を 定義し(一覧は こちら で確認できます)、#[interrupt] を付けます。

#![allow(unused)]
fn main() {
use microbit::pac::interrupt;

// ...

#[interrupt]
fn GPIOTE() {
    free(|cs| {
        if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() {
            let a_pressed = gpiote.channel0().is_event_triggered();
            let b_pressed = gpiote.channel1().is_event_triggered();

            let turn = match (a_pressed, b_pressed) {
                (true, false) => Turn::Left,
                (false, true) => Turn::Right,
                _ => Turn::None
            };

            gpiote.channel0().reset_events();
            gpiote.channel1().reset_events();

            *TURN.borrow(cs).borrow_mut() = turn;
        }
    });
}
}

GPIOTE 割り込みが生成されると、各ボタンが押されたかどうかを確認します。ボタン A だけが押されていれば、 ヘビは左に曲がるべきだと記録します。ボタン B だけが押されていれば、ヘビは右に曲がるべきだと記録します。 それ以外の場合は、ヘビは曲がらないものとして記録します。該当する曲がる方向は TURN Mutex に保存されます。 これらはすべて free ブロック内で行われ、これによりこの割り込みの処理中に再度割り込まれないことが保証されます。

最後に、次の曲がる方向を取得するためのシンプルな関数を公開します。

#![allow(unused)]
fn main() {
/// 次の曲がる方向(つまり、直前に押されたボタンに対応する方向)を取得します。
pub fn get_turn(reset: bool) -> Turn {
    free(|cs| {
        let turn = *TURN.borrow(cs).borrow();
        if reset {
            *TURN.borrow(cs).borrow_mut() = Turn::None
        }
        turn
    })
}
}

この関数は、TURN Mutex の現在の値を返すだけです。引数として 1 つの真偽値 reset を取ります。resettrue の場合、TURN の値はリセットされ、つまり Turn::None に設定されます。

main ファイルの更新

main 関数に戻ると、メインループの前に init_buttons の呼び出しを追加し、ゲームループでは、 game.step メソッドに渡していたプレースホルダーの Turn::None 引数を、get_turn が返す値に置き換える必要があります。

#![no_main]
#![no_std]

mod game;
mod control;

use cortex_m_rt::entry;
use microbit::{
    Board,
    hal::{prelude::*, Rng, Timer},
    display::blocking::Display
};
use rtt_target::rtt_init_print;
use panic_rtt_target as _;

use crate::game::{Game, GameStatus};
use crate::control::{init_buttons, get_turn};

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let mut board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut rng = Rng::new(board.RNG);
    let mut game = Game::new(rng.random_u32());

    let mut display = Display::new(board.display_pins);

    init_buttons(board.GPIOTE, board.buttons);

    loop {  // メインループ
        loop {  // ゲームループ
            let image = game.game_matrix(9, 9, 9);
            // 明るさの値は、異なる明るさを表示できるディスプレイをまだ
            // 実装していないため、現時点では意味がありません
            display.show(&mut timer, image, game.step_len_ms());
            match game.status {
                GameStatus::Ongoing => game.step(get_turn(true)),
                _ => {
                    for _ in 0..3 {
                        display.clear();
                        timer.delay_ms(200u32);
                        display.show(&mut timer, image, 200);
                    }
                    display.clear();
                    display.show(&mut timer, game.score_matrix(), 1000);
                    break
                }
            }
        }
        game.reset();
    }
}

これで、micro:bit のボタンを使ってヘビを操作できるようになりました!

ノンブロッキングディスプレイを使う

これで、基本的に動作するスネークゲームができました。しかし、ヘビが少し長くなると、 ヘビとエサを見分けたり、ヘビがどちらの方向に進んでいるのかを判断したりするのが難しくなるかもしれません。すべての LED が 同じ明るさだからです。これを直しましょう。

microbit ライブラリは LED マトリクスに対する 2 種類の異なるインターフェースを提供しています。これまで使ってきた基本的な ブロッキングインターフェースと、各 LED の明るさをカスタマイズできるノンブロッキングインターフェースです。ハードウェアレベルでは各 LED は「オン」か「オフ」かのどちらかですが、microbit::display::nonblocking モジュールは LED を高速にオン・オフ することで、各 LED に対して 10 段階の明るさをシミュレートします。

ノンブロッキングインターフェースとやり取りするコードはかなりシンプルで、ボタンとやり取りするために使ったコードと似た構成に なります。

#![allow(unused)]
fn main() {
use core::cell::RefCell;
use cortex_m::interrupt::{free, Mutex};
use microbit::display::nonblocking::Display;
use microbit::gpio::DisplayPins;
use microbit::pac;
use microbit::pac::TIMER1;

static DISPLAY: Mutex<RefCell<Option<Display<TIMER1>>>> = Mutex::new(RefCell::new(None));

pub(crate) fn init_display(board_timer: TIMER1, board_display: DisplayPins) {
    let display = Display::new(board_timer, board_display);

    free(move |cs| {
        *DISPLAY.borrow(cs).borrow_mut() = Some(display);
    });
    unsafe {
        pac::NVIC::unmask(pac::Interrupt::TIMER1)
    }
}
}

まず、LED ディスプレイを表す microbit::display::nonblocking::Display 構造体を初期化し、そこに board の TIMER1DisplayPins ペリフェラルを渡します。次にディスプレイを Mutex に格納します。最後に、TIMER1 割り込みをアンマスクします。

次に、表示する画像を簡単に設定(または解除)できるようにする便利関数を 2 つ定義します。

#![allow(unused)]
fn main() {
use tiny_led_matrix::Render;

// ...

/// 画像を表示する。
pub(crate) fn display_image(image: &impl Render) {
    free(|cs| {
        if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
            display.show(image);
        }
    })
}

/// ディスプレイをクリアする(すべての LED を消灯する)。
pub(crate) fn clear_display() {
    free(|cs| {
        if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
            display.clear();
        }
    })
}
}

display_image は画像を受け取り、それを表示するようディスプレイに指示します。これが呼び出す Display::show メソッドと同様に、この 関数は tiny_led_matrix::Render トレイトを実装した構造体を受け取ります。そのトレイトは、その構造体が LED マトリクス上に Display がそれを描画するために必要なデータとメソッドを備えていることを保証します。microbit::display::nonblocking モジュールが提供する Render の 2 つの実装は、BitImageGreyscaleImage です。BitImage では、各 「ピクセル」(つまり LED)は点灯しているかしていないかのどちらかであり(ブロッキングインターフェースを使ったときと同様です)、一方で GreyscaleImage では各「ピクセル」に異なる明るさを持たせることができます。

clear_display は名前のとおりの動作をします。

最後に、interrupt マクロを使って TIMER1 割り込みのハンドラを定義します。この割り込みは 1 秒間に何度も発生し、これによって Display は異なる LED を高速にオン・オフしながら切り替え、明るさが変化しているような錯覚を生み出せます。このハンドラのコードが していることは、これを処理する Display::handle_display_event メソッドを呼び出すことだけです。

#![allow(unused)]
fn main() {
use microbit::pac::interrupt;

// ...

#[interrupt]
fn TIMER1() {
    free(|cs| {
        if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
            display.handle_display_event();
        }
    })
}
}

あとは、main 関数を更新して init_display を呼び出し、定義した新しい関数を使ってこの新しい高機能なディスプレイと やり取りするだけです。

#![no_main]
#![no_std]

mod game;
mod control;
mod display;

use cortex_m_rt::entry;
use microbit::{
    Board,
    hal::{prelude::*, Rng, Timer},
    display::nonblocking::{BitImage, GreyscaleImage}
};
use rtt_target::rtt_init_print;
use panic_rtt_target as _;

use crate::control::{get_turn, init_buttons};
use crate::display::{clear_display, display_image, init_display};
use crate::game::{Game, GameStatus};


#[entry]
fn main() -> ! {
    rtt_init_print!();
    let mut board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0).into_periodic();
    let mut rng = Rng::new(board.RNG);
    let mut game = Game::new(rng.random_u32());

    init_buttons(board.GPIOTE, board.buttons);
    init_display(board.TIMER1, board.display_pins);


    loop {
        loop {  // ゲームループ
            let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9));
            display_image(&image);
            timer.delay_ms(game.step_len_ms());
            match game.status {
                GameStatus::Ongoing => game.step(get_turn(true)),
                _ => {
                    for _ in 0..3 {
                        clear_display();
                        timer.delay_ms(200u32);
                        display_image(&image);
                        timer.delay_ms(200u32);
                    }
                    clear_display();
                    display_image(&BitImage::new(&game.score_matrix()));
                    timer.delay_ms(2000u32);
                    break
                }
            }
        }
        game.reset();
    }
}

あなたがまだ探求できること

まだほんの表面をなぞったにすぎません!あなたが探求できることは、まだたくさん 残っています。

注記: これを読んでいて、以下の項目やその他の関連する組み込みトピックについて、 Discovery book に例や演習を追加するのを手伝いたいと思ってくださるなら、 ぜひ力を貸してください!

本書への貢献方法について支援やメンタリングが必要な場合は、open an issue を 作成してください。あるいは、情報を追加する Pull Request を送ってください!

組み込みソフトウェアに関するトピック

これらのトピックでは、組み込みソフトウェアを書くための戦略を扱います。多くの 問題はさまざまな方法で解決できますが、これらの節ではいくつかの 戦略と、それらを使うことに意味がある場合(または意味がない場合)について説明します。

マルチタスク

これまでのプログラムのほとんどは単一のタスクを実行していました。OS がなく、 したがってスレッドもないシステムで、どうすればマルチタスクを実現できるでしょうか。 マルチタスクには主に 2 つのアプローチがあります。プリエンプティブマルチタスクと 協調的マルチタスクです。

プリエンプティブマルチタスクでは、現在実行中のタスクは、任意の時点で 別のタスクによって プリエンプト(割り込まれ)される可能性があります。プリエンプションが 発生すると、最初のタスクは中断され、代わりにプロセッサは 2 番目のタスクを実行します。 ある時点で最初のタスクは再開されます。マイクロコントローラは、割り込み という形で プリエンプションをハードウェア的にサポートしています。第 11 章でスネークゲームを作ったときに、 私たちは割り込みに触れました。

協調的マルチタスクでは、実行中のタスクは 中断点 に到達するまで実行されます。 プロセッサがその中断点に到達すると、現在のタスクの実行を停止し、 代わりに別のタスクを実行します。ある時点で最初のタスクは再開されます。これら 2 つのマルチタスクのアプローチの主な違いは、協調的マルチタスクでは、実行中の任意の 時点で強制的にプリエンプトされるのではなく、既知の 中断点で実行制御を 譲る ことです。

スリープ

これまでのプログラムはすべて、何かする必要があるかどうかを確かめるために、 周辺機器を継続的にポーリングしてきました。しかし、ときには何もすることがありません! そのようなとき、マイクロコントローラは「スリープ」すべきです。

プロセッサがスリープすると、命令の実行を停止するため、電力を節約できます。 電力を節約するのはほとんど常に良い考えなので、マイクロコントローラはできるだけ 多くの時間をスリープ状態で過ごすべきです。しかし、何らかの動作を行うためにいつ 起きなければならないかを、どうやって知るのでしょうか。「割り込み」(それが正確に何かは 下を参照してください)は、マイクロコントローラを起こすイベントの 1 つですが、ほかにも あり、wfiwfe はプロセッサを「スリープ」させる命令です。

マイクロコントローラの機能に関連するトピック

マイクロコントローラ(nRF52/nRF51 のようなもの)には多くの機能があります。しかし、 多くのマイクロコントローラは、さまざまな種類の問題を解決するために使える、共通した 機能を備えています。

これらのトピックでは、そのような機能のいくつかと、それらを効果的に 組み込み開発で活用する方法について説明します。

ダイレクトメモリアクセス (DMA).

この周辺機器は、非同期の memcpy の一種です。micro:bit v2 を使っているなら、 実はすでにこれを使っています。HAL が UARTE と TWIM 周辺機器でこれを 代わりに行ってくれているからです。DMA 周辺機器は、データの一括 転送を行うために使えます。RAM から RAM へ、UARTE のような周辺機器から RAM へ、 あるいは RAM から周辺機器へ、といった転送です。たとえば「UARTE からこのバッファに 256 バイト読み込む」といった DMA 転送をスケジュールし、それをバックグラウンドで 走らせたまま、完了したかどうかをレジスタをポーリングして確認できます。そうすれば 転送の進行中に別の作業を行えます。これがどのように実装されているかをもっと知るには、 UART 章の serial_setup モジュールを確認できます。それでもまだ足りなければ、 nrf52-hal のコードを読み込んでみることさえできます。

割り込み

現実世界とやり取りするためには、何らかのイベントが発生したときに、 マイクロコントローラが 即座に 応答する必要があることがよくあります。

マイクロコントローラには割り込みを受ける機能があり、つまり特定のイベントが 発生すると、その時点で行っている処理をいったん止めて、代わりにその イベントに応答します。これは、ボタンが押されたときにモーターを止めたい場合や、 タイマーのカウントダウンが終了したときにセンサーを測定したい場合に、非常に役立ちます。

これらの割り込みは非常に便利ですが、正しく扱うのは少し難しいこともあります。 私たちはイベントに素早く応答できるようにしたい一方で、 ほかの処理も継続できるようにしておきたいのです。

Rust では、割り込みをデスクトップ Rust プログラムにおけるスレッドの概念に近いものとして モデル化します。これは、メインアプリケーションと、割り込みイベントの処理 の一部として実行されるコードのあいだでデータを共有するときに、 Rust の SendSync の概念についても考えなければならないことを意味します。

パルス幅変調 (PWM)

ひとことで言えば、PWM とは、何かをオンにしてから周期的にオフにすることを、 「オンの時間」と「オフの時間」のあいだのある比率(「デューティサイクル」)を保ちながら 繰り返すことです。十分に高い周波数で LED に対して使うと、これを使って LED を暗くできます。たとえば、オンの時間が 10%、オフの時間が 90% のような低い デューティサイクルでは LED はかなり暗くなりますが、オンの時間が 90%、オフの時間が 10% のような高いデューティサイクルでは、LED ははるかに明るくなります(ほとんど フルパワーで駆動されているかのようです)。

一般に、PWM は、ある電気機器にどれだけの 電力 を与えるかを制御するために使えます。 マイクロコントローラと電動モーターの間に適切な(電力用の)電子回路があれば、 PWM を使ってモーターに与える電力の大きさを制御できるため、そのトルクや速度を 制御するのに使えます。さらに、角度位置センサーを追加すれば、さまざまな負荷において モーターの位置を制御できる閉ループコントローラを作れます。

PWM はすでに embedded-hal Pwm trait の中で抽象化されており、nrf52-hal に もこの実装があります。

デジタル入力

私たちは LED を駆動するために、マイクロコントローラのピンをデジタル出力として使ってきました。 スネークゲームを作るときには、これらのピンをデジタル入力として 設定する方法も少し見ました。デジタル入力として使うと、これらのピンは スイッチ(オン/オフ)やボタン(押されている/押されていない)の二値の状態を読み取れます。

デジタル入力もまた embedded-hal InputPin trait の中で抽象化されており、 もちろん nrf52-hal にはその実装があります。

(ネタバレ スイッチ / ボタンの二値状態を読むのは、見た目ほど 簡単ではありません ;-) )

アナログ-デジタル変換器 (ADC)

世の中には多くのデジタルセンサーがあります。I2C や SPI のようなプロトコルを使って それらを読み取れます。しかし、アナログセンサーも存在します!これらのセンサーは、測定している量に 比例した電圧レベルを出力するだけです。 ADC ペリフェラルは、その「アナログ」の電圧レベル、たとえば 1.25 ボルトを、たとえば [0, 65535] の範囲にある「デジタル」の数値へと変換でき、プロセッサはそれを 計算に利用できます。

ここでも、embedded-hal adc modulenrf52-hal がこの用途に対応しています。

デジタル-アナログ変換器 (DAC)

ご想像のとおり、DAC は ADC のちょうど反対です。ある レジスタにデジタル値を書き込むことで、ある「アナログ」ピンに [0, 3.3V] の範囲の 電圧を出力できます(3.3V の電源を仮定)。このアナログピンが 適切な電子回路に接続され、正しい値が一定の高速なレート(周波数)でレジスタに 書き込まれると、音や さらには音楽まで生成できます!

リアルタイムクロック (RTC)

このペリフェラルは、「人間向けの形式」で時刻を追跡するために使用できます。秒、分、 時、日、月、年です。このペリフェラルは、 「tick」をこれらの人にとって扱いやすい時間単位へ変換してくれます。さらに、うるう年や 夏時間まで処理してくれます!

その他の通信プロトコル

  • SPI。embedded-hal spi module で抽象化されており、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 リポジトリで issue を立ててください。あるいは Rust Embedded matrix channel に参加して、 上記のものを作った人たちの大半と連絡を取ることもできます。

一般的な組み込み関連トピック

これらのトピックでは、私たちのデバイスや、その上のハードウェアに 固有ではない項目を扱います。その代わりに、組み込み システムで使用できる有用な技法について説明します。

ジャイロスコープ

Punch-o-meter の演習の一環として、私たちは加速度計を使って 3 次元での加速度の変化を測定しました。しかし、ジャイロスコープのような他のモーション センサーもあり、これを使うと 3 次元での「回転」の変化を測定できます。

これは、たとえば転倒を避けたいロボットのような特定のシステムを構築しようとするときに 非常に有用です。さらに、ジャイロスコープのようなセンサーからのデータは Sensor Fusion と呼ばれる技法を使って加速度計のデータと組み合わせることもできます (詳細は後述します)。

サーボモーターとステッピングモーター

一部のモーターは主に一方向または反対方向に回転させるためだけに使われます。 たとえば、ラジコンカーを前進または後退させる場合です。しかし、モーターがどのように回転するかを より正確に測定したいこともあります。

私たちのマイクロコントローラは、サーボモーターやステッピングモーターを駆動する ために使用でき、これによりモーターが何回転するかをより正確に制御でき、あるいは モーターを特定の位置に置くことさえできます。たとえば、時計の針を特定の方向へ 動かしたい場合です。

センサーフュージョン

micro:bit には 2 つのモーションセンサー、加速度計と磁力計が搭載されています。 それぞれ単独では、(固有)加速度と(地球の)磁場を測定します。 しかし、これらの物理量を「融合」することで、より有用なもの、すなわち「ロバスト」な測定、 つまりボードの姿勢の測定を得られます。ここでロバストとは、 単一のセンサーで可能な場合よりも測定誤差が小さいことを意味します。

異なるソースからより信頼性の高いデータを導き出すこの考え方は センサーフュージョンとして知られています。


では、次はどこへ進みましょうか。いくつかの選択肢があります:

  • microbit ボードサポートクレートにあるサンプルを見てみることができます。これらのサンプルはすべて 手元の micro:bit ボードで動作します。
  • Rust Embedded matrix channel に参加することもできます。組み込みソフトウェアに貢献している人や取り組んでいる人の多くが そこに集まっています。たとえば、microbit BSP、nrf52-halembedded-hal などを書いた人たちもいます。
  • 今の Rust Embedded で利用できるものの全体像を知りたいなら、Awesome Rust Embedded リストを見てみてください
  • Real-Time Interrupt-driven Concurrency を見てみることもできます。非常に効率的なプリエンプティブ・マルチタスキングフレームワークで、 タスクの優先順位付けとデッドロックのない実行をサポートしています。
  • embedded-hal プロジェクトのさらに多くの抽象化を見てみて、それを基に独自の プラットフォーム非依存ドライバを書いてみるのもよいでしょう。
  • 別の開発ボードで Rust を動かしてみることもできます。始めるための最も簡単な方法は、 cortex-m-quickstart Cargo プロジェクトテンプレートを使うことです。
  • Rust の型システムがどのように I/O 設定のバグを防げるかを説明している このブログ記事 を見てみることもできます。
  • Rust による組み込み開発に関するさまざまなトピックについては、japaric’s blog を見てみることもできます。
  • Weekly driver initiative に参加して、 embedded-hal traits の上に構築され、あらゆる種類のプラットフォーム(ARM Cortex-M, AVR, MSP430, RISCV, など)で動作する汎用ドライバを書くのを手伝うこともできます。

一般的なトラブルシューティング

cargo-embed の問題

ほとんどの cargo-embed の問題は、udev ルールが正しくインストールされていないこと (Linux の場合)か、Embed.toml で誤ったチップ設定を選択していることに関連しているため、 その両方が正しいことを確認してください。

上記で解決しない場合は、discovery issue tracker に issue を作成できます。 あるいは、Rust Embedded matrix channel または probe-rs matrix channel にアクセスして、 そこで助けを求めることもできます。

Cargo の問題

“can’t find crate for core

症状

   Compiling volatile-register v0.1.2
   Compiling rlibc v1.0.0
   Compiling r0 v0.1.0
error[E0463]: can't find crate for `core`

error: aborting due to previous error

error[E0463]: can't find crate for `core`

error: aborting due to previous error

error[E0463]: can't find crate for `core`

error: aborting due to previous error

Build failed, waiting for other jobs to finish...
Build failed, waiting for other jobs to finish...
error: Could not compile `r0`.

To learn more, run the command again with --verbose.

原因

マイクロコントローラーに対応する適切なターゲット(v2 では thumbv7em-none-eabihf、 v1 では thumbv6m-none-eabi)のインストールを忘れています。

対処法

適切なターゲットをインストールしてください。

# micro:bit v2
$ rustup target add thumbv7em-none-eabihf

# micro:bit v1
$ rustup target add thumbv6m-none-eabi

GDBの使い方

以下は、プログラムのデバッグに役立つGDBコマンドです。ここでは、マイクロコントローラーにプログラムを書き込み、GDBを cargo-embed セッションに接続していることを前提としています。

一般的なデバッグ

注: 以下に示すコマンドの多くは短縮形で実行できます。たとえば、continue は単に cbreak $locationb $location として使えます。以下のコマンドに慣れてきたら、GDBが認識しなくなる直前まで、どこまで短くできるか試してみてください!

ブレークポイントの扱い

  • break $location: コード内の場所にブレークポイントを設定します。$location には次のような値を指定できます:
    • break *main - 関数 main の正確なアドレスで停止します
    • break *0x080012f2 - 正確なメモリ位置 0x080012f2 で停止します
    • break 123 - 現在表示されているファイルの123行目で停止します
    • break main.rs:123 - ファイル main.rs の123行目で停止します
  • info break: 現在のブレークポイントを表示します
  • delete: すべてのブレークポイントを削除します
    • delete $n: ブレークポイント $n を削除します(n は数値。例: delete $2
  • clear: 次の命令位置にあるブレークポイントを削除します
    • clear main.rs:$function: main.rs 内の $function の入口にあるブレークポイントを削除します
    • clear main.rs:123: main.rs の123行目にあるブレークポイントを削除します
  • enable: 設定済みのすべてのブレークポイントを有効化します
    • enable $n: ブレークポイント $n を有効化します
  • disable: 設定済みのすべてのブレークポイントを無効化します
    • disable $n: ブレークポイント $n を無効化します

実行の制御

  • continue: プログラムの実行を開始または再開します
  • next: プログラムの次の行を実行します
    • next $n: next$n 回繰り返します
  • nexti: next と同じですが、代わりに機械命令単位で実行します
  • step: 次の行を実行します。次の行に別の関数呼び出しが含まれている場合は、そのコードの中に入ります
    • step $n: step$n 回繰り返します
  • stepi: step と同じですが、代わりに機械命令単位で実行します
  • jump $location: 指定した場所から実行を再開します:
    • jump 123: 123行目から実行を再開します
    • jump 0x080012f2: アドレス 0x080012f2 から実行を再開します

情報の表示

  • print /$f $data - 変数 $data が保持する値を表示します。必要に応じて $f で出力形式を指定でき、次の値を使用できます:
    x: hexadecimal
    d: signed decimal
    u: unsigned decimal
    o: octal
    t: binary
    a: address
    c: character
    f: floating point
    
    • print /t 0xA: 16進数の値 0xA を2進数として表示します(0b1010)
  • x /$n$u$f $address: $address のメモリを調べます。必要に応じて、$n で表示するユニット数、 $u でユニットサイズ(バイト、ハーフワード、ワードなど)、$f で上で定義した print 形式を指定できます
    • x /5i 0x080012c4: アドレス 0x080012c4 から始まる5つの機械命令を表示します
    • x/4xb $pc: $pc が現在指している位置から始まる4バイトのメモリを表示します
  • disassemble $location
    • disassemble /r main: 関数 main を逆アセンブルします。/r を使うと、各命令を構成するバイトも表示します

シンボルテーブルの確認

  • info functions $regex: $regex に一致する関数の名前とデータ型を表示します。$regex を省略すると、すべての関数を表示します
    • info functions main: main という単語を含む、定義済み関数の名前と型を表示します
  • info address $symbol: $symbol がメモリのどこに格納されているかを表示します
    • info address GPIOC: 変数 GPIOC のメモリアドレスを表示します
  • info variables $regex: $regex に一致するグローバル変数の名前と型を表示します。$regex を省略すると、すべてのグローバル変数を表示します
  • ptype $data: $data に関するより詳細な情報を表示します
    • ptype cp: 変数 cp の詳細な型情報を表示します

プログラムスタックの調査

  • backtrace $n: $n 個のフレームのトレースを表示します。$n を省略すると、すべてのフレームを表示します
    • backtrace 2: 最初の2つのフレームのトレースを表示します
  • frame $n: 番号またはアドレス $n のフレームを選択します。$n を省略すると、現在のフレームを表示します
  • up $n: $n フレーム上のフレームを選択します
  • down $n: $n フレーム下のフレームを選択します
  • info frame $address: $address にあるフレームの情報を表示します。$address を省略すると、現在選択されているフレームの情報を表示します
  • info args: 選択されているフレームの引数を表示します
  • info registers $r: 選択されているフレームのレジスタ $r の値を表示します。$r を省略すると、すべてのレジスタを表示します
    • info registers $sp: 現在のフレームで、スタックポインタレジスタ $sp の値を表示します

cargo-embed のリモート制御

  • monitor reset: CPUをリセットし、実行を最初からやり直します