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 などの一般的な通信プロトコル、 など。

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

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

アプローチ

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

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

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

対象外

本書の対象外となるもの:

  • Rust を教えること。そのテーマに関する資料はすでに豊富にあります。ここではマイクロコントローラー と組み込みシステムに焦点を当てます。

  • 電気回路理論や電子工学についての包括的な教科書であること。ここでは、一部のデバイスがどのように 動作するかを理解するために必要な最低限だけを扱います。

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

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

問題の報告

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

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

この Discovery ブックは、Embedded Working Group が提供する複数の組み込み Rust リソースの うちの 1 つにすぎません。全体の一覧は The Embedded Rust Bookshelf にあります。ここには Frequently Asked Questions の一覧も含まれています。

スポンサー

この本に取り組むためのスポンサーになってくれた integer 32 に心から感謝します! ぜひたくさん 仕事を依頼してください(Rust コンサルティングをしています!)。そうすれば、より多くの Rustaceans を雇う以外に選択肢がなくなるはずです <3。

背景

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

マイクロコントローラーは、1つのチップ上のシステムです。一方、あなたのコンピューターは複数の独立した コンポーネント、つまりプロセッサー、RAMモジュール、ハードドライブ、イーサネットポートなどで構成されますが、 マイクロコントローラーでは、それらすべてのコンポーネントが単一の「チップ」またはパッケージに組み込まれています。これにより、 部品点数を最小限に抑えたシステムを構築できます。

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

いろいろなことができます! マイクロコントローラーは、組み込みシステムとして知られるシステムの中核です。 こうしたシステムは至る所にありますが、普段はあまり意識されません。これらのシステムは、自動車のブレーキを制御し、 洗濯を行い、文書を印刷し、あなたを暖かく保ち、涼しく保ち、 自動車の燃費を最適化するなどしています。

これらのシステムの主な特徴は、洗濯機のようにユーザーインターフェースを備えている場合でも、 ユーザーの介入なしに動作することです。その動作の大半は自律的に行われます。

もう1つの共通した特徴は、これらのシステムがプロセスを制御することです。そのため、これらのシステムには通常、 1つ以上のセンサーと1つ以上のアクチュエーターがあります。たとえば、HVACシステムには、いくつかの領域に配置された 複数のセンサー、温度計、湿度センサーがあり、さらに複数のアクチュエーターとして、 ダクトに接続された発熱体やファンもあります。

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

先ほど挙げたこれらの用途は、Linuxが動作するコンピューターであるRaspberry Piでもおそらく 実装できます。なぜわざわざ、OSなしで動作するマイクロコントローラーを使うべきなのでしょうか。なんだか プログラムの開発がより難しくなりそうです。

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

もう1つの大きな理由は消費電力です。マイクロコントローラーは、本格的なプロセッサーと比べて桁違いに少ない電力しか消費しません。 アプリケーションがバッテリーで動作するなら、この差は非常に大きなものになります。

そして最後になりますが、重要なのが(ハード)リアルタイム制約です。プロセスによっては、そのコントローラーが 一定時間内に何らかのイベントへ応答することを求められます(例: 突風を受けたクアッドコプター/ドローン)。 このデッドラインを守れなければ、そのプロセスは致命的な失敗に終わる可能性があります(例: ドローンが地面に 墜落する)。汎用OS上で動作する汎用コンピューターでは、多くのサービスが バックグラウンドで動いています。そのため、厳しい時間制約の下でプログラムの実行を保証するのは困難です。

どのような場合にマイクロコントローラーを使うべきではありませんか?

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

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

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

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

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

Cのエコシステムは、はるかに成熟しています。いくつかの問題については、既製のソリューションがすでに存在します。もし 時間に厳しいプロセスを制御する必要があるなら、既存の商用リアルタイムオペレーティングシステム (RTOS)のいずれかを使って問題を解決できます。Rustには、まだ商用で本番運用に耐えるRTOSが ないため、自分で作るか、現在開発中のもののいずれかを 試すしかありません。

ハードウェア/知識要件

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

組み込みプログラミングの性質上、値の2進数表現や16進数表現の仕組み、さらに いくつかのビット演算子の使い方を理解していることも、非常に役に立ちます。 たとえば、次のプログラムがどのようにその出力を生成するのかを 理解しているとよいでしょう。

fn main() {
    let a = 0x4000_0000 + 0xa2;

    // ビットシフト "<<" 演算の使用。
    let b = 1 << 5;

    // {:X} は値を16進数として整形する
    println!("{:X}: {:X}", a, b);
}

また、この内容を進めるには、次のハードウェアが必要です。

(いくつかの部品は任意ですが、あると便利です)

(このボードは、“大手” の電子部品 販売業者、または 電子商取引 サイト から購入できます)

  • 任意。3.3V USB <-> Serial モジュール。詳しく言うと、新しいほうの Discovery ボードのリビジョンを持っているなら(通常はそうです。というのも、 最初のリビジョンが出たのは何年も前だからです)、このモジュールは 不要 です。 ボード自体にこの機能がオンボードで含まれているからです。古いリビジョンの ボードを持っている場合は、第10章と第11章でこのモジュールが必要になります。 完全を期すため、Serial モジュールの使い方についての説明も含めます。本書では この特定のモデル を使いますが、3.3V で動作するならほかのモデル でもかまいません。電子商取引 サイトで買える CH340G モジュールも使えますし、 おそらくこちらのほうが安く手に入るでしょう。

  • 任意。HC-05 Bluetooth モジュール(ヘッダ付きのもの!)。HC-06 でも動作します。

(ほかの中国製部品と同様に、これらはほぼ 電子商取引 サイト でしか見つかりません。 (米国の)電子部品販売業者は、なぜかたいていこれを扱っていません)

  • mini-B USB ケーブルを2本。1本は STM32F3DISCOVERY ボードを動かすために必要です。もう1本は Serial <-> USB モジュールがある場合にのみ必要です。ケーブルによっては充電専用 のものもあるので、両方ともデータ転送に対応していることを確認してください。

これらは、ほとんどすべての Android 端末に付属している USB ケーブルでは ありません。 それらは micro USB ケーブルです。正しいものを用意してください!

  • ほぼ任意。メス-メス 5本、オス-メス 4本、オス-オス 1本の ジャンパー(別名 Dupont) ワイヤ。ITM を動かすには、ほぼ確実にメス-メスが1本必要になります。ほかのワイヤは、 USB <-> Serial モジュールと Bluetooth モジュールを使う場合にのみ必要です。

(これらは電子部品の販売業者電子商取引 サイト から入手できます)

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

そうすることで、私にとってもあなたにとってもずっと楽になります。

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

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

たぶん可能です。主に2つのこと次第です。1つは、これまでのマイクロコントローラの経験、 そしてもう1つは、あなたの開発ボード向けに f3 のような高レベルクレートが どこかにすでに存在するかどうかです。

私見では、別の開発ボードを使うと、このテキストは初心者にやさしい点や「追いやすさ」の大半、 場合によってはそのすべてを失ってしまいます。

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

開発環境のセットアップ

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

ドキュメント

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

本書では、以下の文書を通して参照していきます:

注意 これらのリンクはすべて PDF ファイルを指しており、その一部は数百ページにおよび、 サイズも数 MB あります。

* 注意: 新しい(2020/09 ごろ以降の)Discovery ボードでは、異なる電子コンパスとジャイロスコープが 搭載されている場合があります(ユーザーマニュアルを参照)。 そのため、14〜16 章の内容の多くはそのままでは動作しません。 こちら のような GitHub issue も確認してください。

ツール

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

  • Rust 1.31 またはそれ以降のツールチェーン。第 USART 章では 1.51 以降が必要です。

  • itmdump >=0.3.1 (cargo install itm)。テストしたバージョン: 0.3.1。

  • OpenOCD >=0.8。テストしたバージョン: v0.9.0 と v0.10.0

  • arm-none-eabi-gdb。バージョン 7.12 以降を強く推奨します。テストしたバージョン: 7.10, 7.11, 7.12 および 8.1

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

  • Linux および macOS では minicom。テストしたバージョン: 2.7。読者からは picocom でも動作すると 報告されていますが、本書では minicom を使います。

  • Windows では PuTTY

お使いのコンピューターに Bluetooth 機能があり、Bluetooth モジュールを持っている場合は、Bluetooth モジュールを試すために追加で 以下のツールをインストールできます。これらはすべて任意です:

  • Linux の場合、Blueman のような Bluetooth マネージャーアプリケーションがないときにのみ必要です。
    • bluez
    • hcitool
    • rfcomm
    • rfkill

macOS / OSX / Windows のユーザーは、OS に標準搭載されているデフォルトの Bluetooth マネージャーだけで十分です。

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

rustc & Cargo

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

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

$ rustc -V
rustc 1.31.0 (abe02cefd 2018-12-04)

itmdump

cargo install itm

バージョンが >=0.3.1 であることを確認してください

$ itmdump -V
itmdump 0.3.1

cargo-binutils

llvm-tools をインストールしてください

rustup component add llvm-tools

cargo-binutils をインストールしてください

cargo install cargo-binutils

ツールがインストールされていることを確認する

端末で次のコマンドを実行してください

cargo new test-size
cd test-size
cargo run
cargo size -- --version

結果は次のようになります:

~
$ cargo new test-size
     Created binary (application) `test-size` package

~
$ cd test-size

~/test-size (main)
$ cargo run
   Compiling test-size v0.1.0 (~/test-size)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/test-size`
Hello, world!

~/test-size (main)
$ cargo size -- --version
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
LLVM (http://llvm.org/):
  LLVM version 11.0.0-rust-1.50.0-stable
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver2

OS 別の手順

次に、使用している OS に対応する手順に従ってください:

Linux

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

必須パッケージ

Ubuntu 18.04 以降 / Debian stretch 以降

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

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

Ubuntu 14.04 と 16.04

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

sudo apt-get install \
  gdb-arm-none-eabi \
  minicom \
  openocd

Fedora 23 以降

sudo dnf install \
  minicom \
  openocd \
  gdb

Arch Linux

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

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

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

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

ARM のビルド済み ツールチェーン のパッケージがないディストリビューションでは、“Linux 64-bit” ファイルをダウンロードし、 その bin ディレクトリをパスに追加してください。 その方法の一例を以下に示します。

mkdir -p ~/local && cd ~/local
tar xjf /path/to/downloaded/file/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2

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

PATH=$PATH:$HOME/local/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux/bin

オプションのパッケージ

Ubuntu / Debian

sudo apt-get install \
  bluez \
  rfkill

Fedora

sudo dnf install \
  bluez \
  rfkill

Arch Linux

sudo pacman -S \
  bluez \
  bluez-utils \
  rfkill

udev ルール

これらのルールにより、F3 やシリアルモジュールのような USB デバイスを root 権限、つまり sudo なしで使用できます。

lsusb の出力にある idVendoridProduct を使って、/etc/udev/rules.d99-openocd.rules を作成します。

たとえば、USB ケーブルを使って STM32F3DISCOVERY をコンピューターに接続します。 ケーブルは必ず “USB ST-LINK” ポート、つまり基板の縁の中央にある USB ポートに接続してください。

lsusb を実行します:

lsusb | grep ST-LINK

次のような結果になるはずです:

$ lsusb | grep ST-LINK
Bus 003 Device 003: ID 0483:374b STMicroelectronics ST-LINK/V2.1

したがって、idVendor0483 で、idProduct374b です。

/etc/udev/rules.d/99-openocd.rules を作成する:

sudo vi /etc/udev/rules.d/99-openocd.rules

内容は次のとおりです:

# STM32F3DISCOVERY - ST-LINK/V2.1
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE:="0666"

古いデバイスでオプションの USB <-> FT232 ベースのシリアルモジュールを使う場合

/etc/udev/rules.d/99-ftdi.rules を作成します:

sudo vi /etc/udev/rules.d/99-openocd.rules

内容は次のとおりです:

# FT232 - USB <-> シリアルコンバーター
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE:="0666"

次のコマンドで udev ルールを再読み込みします:

sudo udevadm control --reload-rules

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

次に、next section に進みます。

Windows

arm-none-eabi-gdb

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

gcc がインストールされていることを確認します:

arm-none-eabi-gcc -v

結果は次のようになります:

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

OpenOCD

Windows 向けの OpenOCD には公式のバイナリーリリースはありませんが、非公式のリリースが こちら で入手できます。0.10.x の zip ファイルを入手し、ドライブ上のどこかに展開してください( C:\OpenOCD をおすすめしますが、ドライブ文字は適切なものにしてください)。その後、次のパスが含まれるように %PATH% 環境変数を更新します: C:\OpenOCD\bin(または先ほど使用したパス)。

次のコマンドで、OpenOCD がインストールされており %PATH% に含まれていることを確認します:

openocd -v

結果は次のようになります:

$ openocd -v
Open On-Chip Debugger 0.10.0
(..)

PuTTY

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

さらに、この USB ドライバー もインストールする必要があります。そうしないと OpenOCD は動作しません。インストーラーの 指示に従い、正しい(32 ビット版または 64 ビット版)ドライバーをインストールしてください。

以上です! 次のセクション に進んでください。

macOS

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

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

$ # Minicom と OpenOCD
$ brew install minicom openocd

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

インストールを検証する

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

Linux のみ

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

USB ケーブルを使って STM32F3DISCOVERY をコンピューターに接続してください。ケーブルは必ず “USB ST-LINK” ポート、つまりボードの縁の中央にある USB ポートに接続してください。

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

lsusb | grep -i stm

次のような結果になるはずです:

$ lsusb | grep -i stm
Bus 003 Device 004: ID 0483:374b STMicroelectronics ST-LINK/V2.1
$ # ^^^        ^^^

私の環境では、STM32F3DISCOVERY はバス #3 に接続され、デバイス #4 として列挙されました。これは、ファイル /dev/bus/usb/003/004 STM32F3DISCOVERY であることを意味します。そのパーミッションを確認してみましょう:

$ ls -la /dev/bus/usb/003/004
crw-rw-rw-+ 1 root root 189, 259 Feb 28 13:32 /dev/bus/usb/003/00

パーミッションは crw-rw-rw- であるはずです。そうでない場合は、udev rules を確認し、次のコマンドで再読み込みしてみてください:

sudo udevadm control --reload-rules

オプションの USB <-> FT232 ベースのシリアルモジュールを使う古いデバイスの場合

STM32F3DISCOVERY を取り外し、シリアルモジュールを接続してください。次に、対応するファイルを確認します:

$ lsusb | grep -i ft232
Bus 003 Device 005: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC

私の環境では /dev/bus/usb/003/005 でした。では、そのパーミッションを確認します:

$ ls -l /dev/bus/usb/003/005
crw-rw-rw- 1 root root 189, 21 Sep 13 00:00 /dev/bus/usb/003/005

前と同様に、パーミッションは crw-rw-rw- であるはずです。

OpenOCD 接続を確認する

USB ケーブルを使って STM32F3DISCOVERY を、ボードの縁の中央にある USB ポート、つまり “USB ST-LINK” と 表示されたポートに接続してください。

USB ケーブルをボードに接続すると、すぐに 2 つの 赤い LED が点灯するはずです。

重要 STM32F3DISCOVERY ボードには複数のハードウェアリビジョンがあります。古い リビジョンでは、“interface” 引数を -f interface/stlink-v2.cfg に変更する必要があります(注: 末尾に -1 は付きません)。あるいは、古いリビジョンでは -f board/stm32f3discovery.cfg-f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg の代わりに使用できます。

注記 OpenOCD v0.11.0 では interface/stlink-v2.cfg は非推奨になり、代わりに ST-LINK/V1、ST-LINK/V2、ST-LINK/V2-1、および ST-LINK/V3 をサポートする interface/stlink.cfg が使われます。

*Nix

参考: interface ディレクトリは通常 /usr/share/openocd/scripts/ にあり、 これは OpenOCD がこれらのファイルを想定しているデフォルトの場所です。別の場所に インストールしている場合は、-s /path/to/scripts/ オプションを使ってインストールディレクトリを指定してください。

openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

または

openocd -f interface/stlink.cfg -f target/stm32f3x.cfg

Windows

以下で C:\OpenOCD と書かれている箇所は、OpenOCD がインストールされているディレクトリです。

openocd -s C:\OpenOCD\share\scripts -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

注記 cygwin ユーザーからは -s フラグに関する問題が報告されています。その問題に遭遇した 場合は、パラメーターに C:\OpenOCD\share\scripts\ ディレクトリを追加できます。

cygwin ユーザー:

openocd -f C:\OpenOCD\share\scripts\interface\stlink-v2-1.cfg -f C:\OpenOCD\share\scripts\target\stm32f3x.cfg

共通

OpenOCD は ITM チャネルからのデバッグ情報をファイル itm.txt に転送するサービスです。そのため、 これは終了せずに動作し続け、ターミナルのプロンプトには 戻りません

OpenOCD の起動直後の出力は次のようになります:

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.915608
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

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

また、赤い LED のうち USB ポートに最も近いものが、赤色と緑色の間で交互に点灯し始めるはずです。

以上です。正常に動作しています。これで Ctrl-c を使って OpenOCD を停止するか、ターミナルを閉じる/終了できます。

ハードウェアを知る

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

STM32F3DISCOVERY(「F3」)

この本では、このボードを一貫して「F3」と呼びます。以下は、この ボード上にある多くのコンポーネントの一部です:

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

STM32F303VCT6(「STM32F3」)

MCU は非常に重要なので、私たちのボードに載っているものをもう少し詳しく見てみましょう。

私たちの MCU は、100 本の小さな金属製の ピン に囲まれています。これらのピンは トレース に接続されています。トレースとは、ボード上でコンポーネント同士をつなぐ 配線として機能する小さな「道路」です。MCU は、ピンの電気的特性を動的に 変化させることができます。これは、回路を流れる電流の流れ方をスイッチが変える のと似ています。特定のピンに電流が流れるようにしたり、流れないようにしたりする ことで、そのピンに接続された LED(トレース経由)を オンとオフにできます。

各メーカーは異なる型番体系を使っていますが、多くの場合、型番を見るだけで コンポーネントに関する情報を判断できます。私たちの MCU の型番 (STM32F303VCT6) を 見ると、先頭の ST から、これが ST Microelectronics 製の部品であることが分かります。 ST’s marketing materials を調べると、次のことも分かります:

  • M32 は、これが Arm® ベースの 32 ビットマイクロコントローラーであることを表します。
  • F3 は、この MCU が ST の「STM32F3」シリーズに属していることを表します。これは Cortex®-M4 プロセッサ設計に基づく MCU のシリーズです。
  • 型番の残りの部分は、追加機能や RAM サイズのようなものについて、さらに詳しい情報を 表していますが、この時点ではそれほど気にする必要はありません。

Arm? Cortex-M4?

私たちのチップを製造しているのが ST だとすると、Arm とは何でしょうか? そして、私たちの チップが STM32F3 だとすると、Cortex-M4 とは何でしょうか?

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

Arm はさまざまな設計をライセンス提供しています。その「Cortex-M」ファミリーの設計は 主にマイクロコントローラーのコアとして使われています。たとえば、Cortex-M0 は低コストで低消費電力になるよう設計されています。Cortex-M7 はより高価ですが、 より多くの機能と性能を備えています。私たちの STM32F3 のコアは Cortex-M4 に基づいており、これはその中間に位置します。つまり、Cortex-M0 よりも 機能と性能が高い一方で、Cortex-M7 ほど高価ではありません。

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

シリアルモジュール

古いリビジョンの discovery ボードを持っている場合は、このモジュールを使って F3 上のマイクロコントローラーとコンピューターの間でデータをやり取りできます。このモジュールは USB ケーブルを使ってコンピューターに接続します。ここではこれ以上は 説明しません。

新しいリリースのボードを持っている場合は、このモジュールは必要ありません。代わりに ST-LINK が、ピン PC4 と PC5 にあるマイクロコントローラーの USART1 に接続された USB<->シリアル コンバーターも兼ねます。

Bluetooth モジュール

このモジュールの目的はシリアルモジュールとまったく同じですが、データは Bluetooth 経由で 送信され、USB 経由ではありません。

LED ルーレット

それでは、次のアプリケーションを作ることから始めましょう。

このアプリを実装するための高水準 API をこれから示しますが、心配はいりません。低レベルなことは後で扱います。この章の主な目標は、フラッシュ書き込み とデバッグのプロセスに慣れることです。

このテキスト全体を通して、discovery リポジトリにあるスターターコードを使用します。この Web サイトはそのブランチを追跡しているので、常に master ブランチの最新バージョンを使うようにしてください。

スターターコードはそのリポジトリの src ディレクトリにあります。そのディレクトリの中には、この本の各章に対応する名前のディレクトリがさらにあります。それらのディレクトリのほとんどは、Cargo のスタータープロジェクトです。

では、src/05-led-roulette ディレクトリに移動しましょう。src/main.rs ファイルを確認してください。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::entry;

#[entry]
fn main() -> ! {
    let _y;
    let x = 42;
    _y = x;

    // infinite loop; just so we don't leave this stack frame
    loop {}
}

マイクロコントローラ向けのプログラムは、#![no_std]#![no_main] という 2 つの点で通常のプログラムと異なります。

no_std 属性は、このプログラムが基盤となる OS を前提とする std クレートを使わないことを示します。代わりに、このプログラムは core クレートを使います。これは std のサブセットで、ベアメタルシステム(つまり、ファイルやソケットのような OS の抽象化がないシステム)上で動作できます。

no_main 属性は、このプログラムが標準の main インターフェースを使わないことを示します。標準の main は、引数を受け取るコマンドラインアプリケーション向けに作られているためです。標準の main の代わりに、cortex-m-rt クレートの entry 属性を使ってカスタムのエントリポイントを定義します。このプログラムではエントリポイントに “main” という名前を付けていますが、ほかのどんな名前でも使えます。エントリポイント関数は fn() -> ! というシグネチャでなければなりません。この型は、その関数がリターンできないことを示します。つまり、このプログラムは決して終了しません。

注意深く見れば、この Cargo プロジェクトには .cargo ディレクトリもあることに気付くでしょう。このディレクトリには Cargo の設定ファイル(.cargo/config)が含まれており、リンクプロセスを調整して、プログラムのメモリレイアウトをターゲットデバイスの要件に合わせています。この変更されたリンクプロセスは、cortex-m-rt クレートの要件です。さらに、後の節では、ビルドとデバッグをしやすくするために .cargo/config に追加の調整を加えていきます。

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

ビルドする

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

F3 のマイクロコントローラーには Cortex-M4F プロセッサーが搭載されています。rustc は Cortex-M アーキテクチャ向けにクロスコンパイルする方法を理解しており、 そのアーキテクチャ内のさまざまなプロセッサーファミリーをカバーする 4 つの異なるターゲットを提供しています:

  • 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 プロセッサー向け

F3 では、thumbv7em-none-eabihf ターゲットを使用します。クロスコンパイルする前に、 ターゲット向けの標準ライブラリのプリコンパイル済みバージョン(実際にはその縮小版)を ダウンロードしておく必要があります。これは rustup を使って行います:

rustup target add thumbv7em-none-eabihf

上記の手順は一度だけ実行すれば十分です。ツールチェーンを更新するたびに、rustup が新しい標準ライブラリ (rust-std コンポーネント)を再インストールします。

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

注記 src/05-led-roulette ディレクトリにいることを確認し、 実行可能ファイルを作成するために、以下の cargo build コマンドを実行してください:

cargo build --target thumbv7em-none-eabihf

コンソールには次のような出力が表示されるはずです:

$ cargo build --target thumbv7em-none-eabihf
   Compiling typenum v1.12.0
   Compiling semver-parser v0.7.0
   Compiling version_check v0.9.2
   Compiling nb v1.0.0
   Compiling void v1.0.2
   Compiling autocfg v1.0.1
   Compiling cortex-m v0.7.1
   Compiling proc-macro2 v1.0.24
   Compiling vcell v0.1.3
   Compiling unicode-xid v0.2.1
   Compiling stable_deref_trait v1.2.0
   Compiling syn v1.0.60
   Compiling bitfield v0.13.2
   Compiling cortex-m v0.6.7
   Compiling cortex-m-rt v0.6.13
   Compiling r0 v0.2.2
   Compiling stm32-usbd v0.5.1
   Compiling stm32f3 v0.12.1
   Compiling usb-device v0.2.7
   Compiling cfg-if v1.0.0
   Compiling paste v1.0.4
   Compiling stm32f3-discovery v0.6.0
   Compiling embedded-dma v0.1.2
   Compiling volatile-register v0.2.0
   Compiling nb v0.1.3
   Compiling embedded-hal v0.2.4
   Compiling semver v0.9.0
   Compiling generic-array v0.14.4
   Compiling switch-hal v0.3.2
   Compiling num-traits v0.2.14
   Compiling num-integer v0.1.44
   Compiling rustc_version v0.2.3
   Compiling bare-metal v0.2.5
   Compiling cast v0.2.3
   Compiling quote v1.0.9
   Compiling generic-array v0.13.2
   Compiling generic-array v0.12.3
   Compiling generic-array v0.11.1
   Compiling panic-itm v0.4.2
   Compiling lsm303dlhc v0.2.0
   Compiling as-slice v0.1.4
   Compiling micromath v1.1.0
   Compiling accelerometer v0.12.0
   Compiling chrono v0.4.19
   Compiling aligned v0.3.4
   Compiling rtcc v0.2.0
   Compiling cortex-m-rt-macros v0.1.8
   Compiling stm32f3xx-hal v0.6.1
   Compiling aux5 v0.2.0 (~/embedded-discovery/src/05-led-roulette/auxiliary)
   Compiling led-roulette v0.2.0 (~/embedded-discovery/src/05-led-roulette)
    Finished dev [unoptimized + debuginfo] target(s) in 17.91s

注記 このクレートは必ず最適化を 行わずに コンパイルしてください。付属の Cargo.toml ファイルと上記のビルドコマンドにより、最適化が無効になるようになっています。

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

cargo readobj --target thumbv7em-none-eabihf --bin led-roulette -- --file-header

上の cargo readobj ..readelf -h target/thumbv7em-none-eabihf/debug/led-roulette と等価であり、次のような出力になるはずです:

$ cargo readobj --target thumbv7em-none-eabihf --bin led-roulette -- --file-header
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
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:               0x8000195
  Start of program headers:          52 (bytes into file)
  Start of section headers:          818328 (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:         22
  Section header string table index: 20

次に、このプログラムをマイクロコントローラーにフラッシュ書き込みします。

書き込んでみよう

フラッシュとは、プログラムをマイクロコントローラーの(永続的な)メモリに書き込む処理のことです。いったんフラッシュされると、マイクロコントローラーは電源が入るたびに、その書き込まれたプログラムを実行します。

この場合、led-roulette プログラムがマイクロコントローラーのメモリ内で 唯一の プログラムになります。 つまり、マイクロコントローラー上ではほかに何も動いていません。OS も「daemon」も何もありません。led-roulette がデバイスを完全に制御します。

では、実際にフラッシュしていきましょう。最初に行う必要があるのは OpenOCD の起動です。これは前のセクションでも行いましたが、今回は一時ディレクトリ(*nix では /tmp、Windows では %TEMP%)の中でコマンドを実行します。

F3 がコンピューターに接続されていることを確認し、新しいターミナルで次のコマンドを実行してください。

*nix & MacOS の場合:

cd /tmp
openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

Windows の場合 : 実際の OpenOCD のパスに合わせて C: を置き換えてください:

cd %TEMP%
openocd -s C:\share\scripts -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

NOTE ボードの古いリビジョンでは、openocd に少し異なる引数を渡す必要があります。詳細は this section を確認してください。

このプログラムはそこで待機状態になるので、そのターミナルは開いたままにしておいてください。

ここで、openocd コマンドが実際には何をしているのかを説明するのにちょうどよいタイミングです。

前にも触れたように、STM32F3DISCOVERY(別名 F3)には実際には 2 つのマイクロコントローラーがあります。そのうちの 1 つはプログラマ/デバッガとして使われます。プログラマとして使われるボード上の部分は ST-LINK と呼ばれます(STMicroelectronics がそう名付けました)。この ST-LINK は、Serial Wire Debug(SWD)インターフェースを使ってターゲットのマイクロコントローラーに接続されています(このインターフェースは ARM の標準なので、ほかの Cortex-M ベースのマイクロコントローラーを扱うときにも目にすることになります)。この SWD インターフェースは、マイクロコントローラーのフラッシュやデバッグに利用できます。ST-LINK は「USB ST-LINK」ポートに接続されており、F3 をコンピューターに接続すると USB デバイスとして認識されます。

OpenOCD はというと、SWD や JTAG のようなデバッグプロトコルを公開する USB デバイスの上で、GDB サーバー などのサービスを提供するソフトウェアです。

では、実際のコマンドを見てみましょう。使用しているこれらの .cfg ファイルは、ST-LINK の USB デバイス(interface/stlink-v2-1.cfg)を探し、その ST-LINK に STM32F3XX マイクロコントローラー(target/stm32f3x.cfg)が接続されていることを想定するよう OpenOCD に指示しています。

OpenOCD の出力は次のようになります。

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v37 API v2 SWIM v26 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.888183
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

「6 breakpoints, 4 watchpoints」という部分は、そのプロセッサーが利用可能なデバッグ機能を示しています。

その openocd プロセスは動かしたままにし、先ほどのターミナル、または新しいターミナルで、 プロジェクトの src/05-led-roulette/ ディレクトリの中にいることを確認してください

OpenOCD は GDB サーバーを提供すると説明しましたので、さっそくそこに接続してみましょう。

GDB を実行する

まず、ARM バイナリをデバッグできる gdb のどのバージョンが手元にあるかを確認する必要があります。

以下のいずれかのコマンドが使えるはずなので、順に試してみてください。

arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
gdb-multiarch -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette

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

$ gdb -q -ex "target remote :3333" ../../target/thumbv7em-none-eabihf/debug/led-roulette

これは、各サンプルプロジェクトが本全体を含む workspace 内にあり、workspace では target ディレクトリが 1 つだけだからです。詳しくは [Workspaces chapter in Rust Book] を参照してください。

失敗するケース

Remote debugging using :3333 の行のあとに warning または error が表示される場合は、失敗しているケースだと判断できます。

$ gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
warning: Architecture rejected target-supplied description
Truncated register 16 in remote 'g' packet
(gdb)

成功するケース

成功例 1:

$ arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
cortex_m_rt::Reset () at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:497
497     pub unsafe extern "C" fn Reset() -> ! {
(gdb)

成功例 2:

~/embedded-discovery/src/05-led-roulette (master)
$ arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
0x00000000 in ?? ()
(gdb)

失敗するケースでも成功するケースでも、OpenOCD のターミナルに次のような新しい出力が表示されるはずです。

 Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints
+Info : accepting 'gdb' connection on tcp/3333
+Info : device id = 0x10036422
+Info : flash size = 256kbytes

NOTE undefined debug reason 7 - target needs reset のようなエラーが出る場合は、こちら にある説明のように monitor reset halt を実行してみてください。

デフォルトでは、OpenOCD の GDB サーバーは TCP ポート 3333(localhost)で待ち受けます。このコマンドはそのポートに接続しています。

../.cargo/config.toml を更新する

使用する必要のあるデバッガを無事に特定できたので、次は cargo run コマンドが成功するように ../.cargo/config.toml を変更する必要があります。

NOTE cargo は Rust のパッケージマネージャーで、こちら で読むことができます。

ターミナルプロンプトに戻って、`../.cargo/config.toml` を確認してください:
``` console
~/embedded-discovery/src/05-led-roulette
$ cat ../.cargo/config.toml
# デフォルトの runner は GDB セッションを開始します。これには OpenOCD が
# 実行されている必要があります。たとえば:
## openocd -f interface/stlink.cfg -f target/stm32f3x.cfg
# ローカルの GDB に応じて、次のいずれかを選んでください
[target.thumbv7em-none-eabihf]
runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
# runner = "gdb-multiarch -q -x ../openocd.gdb"
# runner = "gdb -q -x ../openocd.gdb"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

好みのエディターを使って ../.cargo/config.toml を編集し、 runner の行にそのデバッガーの正しい名前が入るようにしてください:

nano ../.cargo/config.toml

たとえば、デバッガーが gdb-multiarch だった場合、編集後の git diff は 次のようになるはずです:

$ git diff ../.cargo/config.toml
diff --git a/f3discovery/src/.cargo/config.toml b/f3discovery/src/.cargo/config.toml
index 2f38f6b..95860a0 100644
--- a/f3discovery/src/.cargo/config.toml
+++ b/f3discovery/src/.cargo/config.toml
@@ -3,8 +3,8 @@
 ## openocd -f interface/stlink.cfg -f target/stm32f3x.cfg
 # ローカルの GDB に応じて、次のいずれかを選んでください
 [target.thumbv7em-none-eabihf]
-runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
-# runner = "gdb-multiarch -q -x ../openocd.gdb"
+# runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
+runner = "gdb-multiarch -q -x ../openocd.gdb"
 # runner = "gdb -q -x ../openocd.gdb"
 rustflags = [
   "-C", "link-arg=-Tlink.x",

これで ../.cargo/config.toml の設定ができたので、cargo run を使って デバッグセッションを開始し、動作確認してみましょう。

NOTE --target thumbv7em-none-eabihf は、どのアーキテクチャ向けに ビルドして実行するかを定義します。../.cargo/config.toml ファイルには target = "thumbv7em-none-eabihf" があるため、実際には --target を 指定する必要はありません。ここで指定しているのは、コマンドライン上の パラメーターも利用でき、それらが config.toml ファイル内の設定を 上書きすることを知ってもらうためです。

cargo run --target thumbv7em-none-eabihf

結果は次のようになります:

~/embedded-discovery/src/05-led-roulette
$ cargo run --target thumbv7em-none-eabihf
    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `gdb-multiarch -q -x ../openocd.gdb /home/adam/vc/rust-training/discovery/f3discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from /home/adam/vc/rust-training/discovery/f3discovery/target/thumbv7em-none-eabihf/debug/led-roulette...
0x08000230 in core::fmt::Arguments::new_v1 (pieces=..., args=...)
    at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/fmt/mod.rs:394
394	/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/fmt/mod.rs: No such file or directory.
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x1ad8 lma 0x8000194
Loading section .rodata, size 0x5a4 lma 0x8001c6c
Start address 0x08000194, load size 8720
Transfer rate: 12 KB/sec, 2906 bytes/write.
Breakpoint 1 at 0x80001e8: file src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x800020a: file src/lib.rs, line 570.
Breakpoint 3 at 0x8001c5a: file src/lib.rs, line 560.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7
7	#[entry]
halted: PC: 0x080001ee
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:10
10	    let x = 42;

すばらしいです。今後も ../.cargo/config.toml を変更していきます。ただし、 このファイルはすべての章で共有されているため、その点を踏まえて変更を 行う必要があります。特定の章だけに関係する変更をしたい、あるいは必要な 場合は、その章のディレクトリにローカルな .cargo/config.toml を作成して ください。

デバイスにフラッシュする

GDB が実行中であることを前提とします。もし実行していなければ、前の セクションで説明したとおりに起動してください。

NOTE gdb に対する -x ../openocd.gdb 引数は、デバイスへの フラッシュが行われるようにすでに設定されています。そのため、通常は 単に cargo run を実行するだけで、プロジェクトのコードをデバイスに 明示的に書き込む処理まで行われます。openocd の設定スクリプトについては、 次のセクションで扱います。

それでは、gdbload コマンドを使って、実際にプログラムをデバイスへ フラッシュしましょう:

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x20ec lma 0x8000194
Loading section .rodata, size 0x514 lma 0x8002280
Start address 0x08000194, load size 10132
Transfer rate: 17 KB/sec, 3377 bytes/write.

OpenOCD のターミナルにも、新しい出力が表示されます。たとえば次のような ものです:

 Info : flash size = 256kbytes
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+adapter speed: 4000 kHz
+target halted due to breakpoint, current mode: Thread
+xPSR: 0x61000000 pc: 0x2000003a msp: 0x2000a000
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

プログラムがロードされました。さっそくデバッグしてみましょう!

デバッグしてみましょう

すでにデバッグセッションに入っているので、プログラムをデバッグしていきましょう。

load コマンドの後、プログラムはその entry point で停止しています。これは GDB の出力にある Start address 0x8000XXX の部分が示しています。エントリポイントとは、プロセッサ / CPU が 最初に実行するプログラムの部分です。

こちらで用意したスタータープロジェクトには、main 関数の に実行される追加コードが含まれています。 今はその「pre-main」部分には関心がないので、main 関数の先頭まで一気に進みましょう。 そのためにブレークポイントを使います。(gdb) プロンプトで break main を実行してください。

NOTE これらの GDB コマンドについては、通常はコピー可能なコードブロックを示しません。 どれも短く、自分で入力したほうが速いからです。さらに、その多くは短縮できます。 たとえば breakbsteps のように省略できます。詳しくは GDB Quick Reference を参照するか、そのほかのコマンドは Google で調べてください。加えて、タブ補完も使えます。 最初の数文字を入力してから Tab を 1 回押すと補完され、Tab を 2 回押すと 利用可能なコマンドをすべて表示できます。

最後に、xxxx の部分にコマンド名を入れた help xxxx を使うと、短縮名やそのほかの情報を確認できます:

(gdb) help s
step, s
Step program until it reaches a different source line.
Usage: step [N]
Argument N means step N times (or till program stops for another reason).
(gdb) break main
Breakpoint 1 at 0x80001f0: file src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

次に continue コマンドを実行します:

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7
7       #[entry]

ブレークポイントは、プログラムの通常の流れを止めるために使えます。continue コマンドを使うと、 プログラムはブレークポイントに到達する まで 自由に実行されます。この場合は #[entry] に到達するまでです。ここは main 関数へのトランポリンであり、break main によって ブレークポイントが設定される場所です。

Note GDB の出力に “Breakpoint 1” と表示されていることに注目してください。私たちの プロセッサではこの種のブレークポイントを 6 個しか使えないので、こうしたメッセージに 注意を払うのは良い考えです。

OK。#[entry] で停止しているので、disassemble /m を使うと entry のコードが見えます。 これは main へのトランポリンです。つまり、スタックをセットアップしてから、 ARM の branch and link 命令 bl を使って main 関数をサブルーチン呼び出ししている ということです。

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <_ZN12led_roulette18__cortex_m_rt_main17he61ef18c060014a5E>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

次に、step という GDB コマンドを実行する必要があります。これは関数やプロシージャの中に入りながら、 プログラムを文ごとに進めるコマンドです。したがって、この最初の step コマンドの後には main の中に入り、最初の実行可能な rust 文である 10 行目の位置にいますが、 まだ 実行はされていません:

(gdb) step
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:10
10          let x = 42;

次に 2 回目の step を実行すると、10 行目が実行され、11 _y = x; の行で止まります。 ここでも 11 行目は まだ実行されていません

NOTE 2 回目の (gdb) プロンプトでは Enter を押せば、 直前のコマンド step を再実行できましたが、わかりやすさのために このチュートリアルでは基本的にコマンドを毎回入力し直します。

(gdb) step
11          _y = x;

このように、このモードでは step コマンドを実行するたびに、GDB は現在の文を 行番号付きで表示します。後で見る TUI モードでは、コマンド領域にこの文は表示されません。

今は _y = x 文の「上」にいます。この文はまだ実行されていません。つまり、x は 初期化されていますが、_y はまだです。print コマンド(短縮形は p)を使って、 これらのスタック / ローカル変数を確認してみましょう。

(gdb) print x
$1 = 42
(gdb) p &x
$2 = (*mut i32) 0x20009fe0
(gdb) p _y
$3 = 536870912
(gdb) p &_y
$4 = (*mut i32) 0x20009fe4

予想どおり、x には値 42 が入っています。一方で _y には 536870912 (?) という値が 入っています。これは _y がまだ初期化されておらず、ゴミ値が入っているためです。

print &x コマンドは変数 x のアドレスを表示します。ここで興味深いのは、GDB の出力に 参照の型が *mut i32、つまり i32 値への mutable pointer として表示されている点です。 もう 1 つ興味深いのは、x_y のアドレスが互いに非常に近いことです。両者のアドレスは わずか 4 バイトしか離れていません。

ローカル変数を 1 つずつ表示する代わりに、info locals コマンドを使うこともできます。

(gdb) info locals
x = 42
_y = 536870912

OK。さらに step を 1 回実行すると、loop {} 文の位置に来ます。

(gdb) step
14          loop {}

そして、この時点で _y は初期化されているはずです。

(gdb) print _y
$5 = 42

loop {} 文の上で再び step を使うと、プログラムはその文を決して通過しないため、 そこで固まってしまいます。

NOTE 間違って step やほかのコマンドを使って GDB が固まってしまった場合は、 Ctrl+C を押せば復帰できます。

前に紹介したように、disassemble /m コマンドを使うと、現在いる行の周辺のプログラムを 逆アセンブルできます。さらに set print asm-demangle on を設定して、 名前をデマングル表示するようにしておくとよいでしょう。これはデバッグセッションごとに 1 回だけ行えば十分です。後で、これやほかのコマンドは初期化ファイルに入れることになり、 デバッグセッションの開始が簡単になります。

(gdb) set print asm-demangle on
(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h51e7c3daad2af251E:
8       fn main() -> ! {
   0x080001f6 <+0>:     sub     sp, #8
   0x080001f8 <+2>:     movs    r0, #42 ; 0x2a

9           let _y;
10          let x = 42;
   0x080001fa <+4>:     str     r0, [sp, #0]

11          _y = x;
   0x080001fc <+6>:     str     r0, [sp, #4]

12
13          // 無限ループ。このスタックフレームを抜けないようにするため
14          loop {}
=> 0x080001fe <+8>:     b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>
   0x08000200 <+10>:    b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>

End of assembler dump.

左側の太い矢印 => が見えますか? これは、次にプロセッサが実行する命令を示しています。

また、先ほど触れたように step コマンドを実行すると、GDB は自分自身への分岐命令を 実行してその先に進めなくなるため、固まってしまいます。そのため、制御を取り戻すには Ctrl+C を使う必要があります。代わりに stepisi)という GDB コマンドを使う方法もあります。 これは asm 命令を 1 つだけ進めるコマンドで、GDB は次にプロセッサが実行する文のアドレス 行番号を表示し、しかも固まりません。

(gdb) stepi
0x08000194      14          loop {}

(gdb) si
0x08000194      14          loop {}

もう少し面白い内容に進む前に、最後の小技を 1 つ紹介します。以下のコマンドを GDB に入力してください:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <led_roulette::__cortex_m_rt_main>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

これで #[entry] の先頭に戻ってきました!

monitor reset halt はマイクロコントローラーをリセットし、プログラムのまさに先頭で停止させます。 その後 continue コマンドを使うと、ブレークポイントに到達するまでプログラムを自由に実行できます。この場合は #[entry] にあるブレークポイントです。

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

細かい注意点: この 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.

より快適にデバッグしたい場合は、GDB の Text User Interface(TUI)を使えます。このモードに入るには、GDB シェルで次のいずれかのコマンドを入力してください。

(gdb) layout src
(gdb) layout asm
(gdb) layout split

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

以下は、layout split を使うための準備の例です。以下のコマンドを実行します。 ご覧のとおり、--target パラメータを渡すのはやめています。

$ cargo run
(gdb) target remote :3333
(gdb) load
(gdb) set print asm-demangle on
(gdb) set style sources off
(gdb) break main
(gdb) continue

以下は、上記のコマンドを -ex パラメータとしてまとめたコマンドラインで、入力の手間を少し省けます。 まもなく、初期コマンド群をもっと簡単に実行する方法を提供します。

cargo run -- -q -ex 'target remote :3333' -ex 'load' -ex 'set print asm-demangle on' -ex 'set style sources off' -ex 'b main' -ex 'c' target/thumbv7em-none-eabihf/debug/led-roulette

そして以下がその結果です。

GDB セッション layout split

次に、上側のソースウィンドウを下へスクロールしてファイル全体が見えるようにし、layout split を実行してから step を実行します。

GDB セッション layout split

その後、info localsstep を数回実行します。

(gdb) info locals
(gdb) step
(gdb) info locals
(gdb) step
(gdb) info locals

GDB セッション layout split

どの時点でも、次のコマンドで TUI モードを終了できます。

(gdb) tui disable

GDB セッション layout split

NOTE デフォルトの GDB CLI が好みに合わない場合は、gdb-dashboard を試してみてください。これは Python を使って、デフォルトの GDB CLI を、レジスタ、ソースビュー、アセンブリビュー、そのほかさまざまな情報を表示するダッシュボードに変えてくれます。

ただし、OpenOCD は閉じないでください! これから先も何度も使います。 そのまま動かし続けておくほうがよいです。GDB でできることについてもっと知りたい場合は、GDB の使い方 のセクションを参照してください。

次は何でしょう? 約束していた高レベル API です。

LedDelay の抽象化

これから、LEDルーレットアプリケーションを実装するために使用する、2つの高水準の抽象化を紹介します。

補助クレート aux5 は、init という初期化関数を公開しています。この関数を呼び出すと、タプルにまとめられた2つの値、Delay 値と LedArray 値が返されます。

Delay は、指定したミリ秒数の間プログラムをブロックするために使用できます。

LedArray は、8個の Led からなる配列です。各 Led は F3 ボード上の1つのLEDを表し、2つのメソッド onoff を公開しています。これらを使うと、それぞれLEDをオンまたはオフにできます。

では、スターターコードを次のように変更して、この2つの抽象化を試してみましょう。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::{entry, Delay, DelayMs, LedArray, OutputSwitch};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let half_period = 500_u16;

    loop {
        leds[0].on().ok();
        delay.delay_ms(half_period);

        leds[0].off().ok();
        delay.delay_ms(half_period);
    }
}

ではビルドします:

cargo build

NOTE: GDB セッションを開始する 前に プログラムを再ビルドするのを忘れてしまうことがあります。この見落としは、非常に混乱しやすいデバッグセッションにつながる可能性があります。この問題を避けるために、cargo build の代わりに cargo run を実行するだけでかまいません。cargo run コマンドはビルドを行い、さらに デバッグセッションも開始するため、プログラムの再コンパイルを忘れずに済みます。

では、新しいプログラムで前の節と同じように実行し、フラッシュ手順を繰り返しましょう。cargo run は自分で入力してみてください。これもすぐに楽になります。:)

NOTE: 別のターミナルで openocd(デバッガー)を起動するのを忘れないでください。
そうしないと target remote :3333 は動作しません!

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...

(gdb) target remote :3333
Remote debugging using :3333
led_roulette::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x52c0 lma 0x8000194
Loading section .rodata, size 0xb50 lma 0x8005454
Start address 0x08000194, load size 24484
Transfer rate: 21 KB/sec, 6121 bytes/write.

(gdb) break main
Breakpoint 1 at 0x8000202: file ~/embedded-discovery/src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline ()
    at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) step
led_roulette::__cortex_m_rt_main () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:9
9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

(gdb)

OK。コードをステップ実行していきましょう。今回は step ではなく next コマンドを使います。違いは、next コマンドは関数呼び出しの中に入るのではなく、それを 飛び越えて ステップ実行することです。

(gdb) next
11          let half_period = 500_u16;

(gdb) next
13          loop {

(gdb) next
14              leds[0].on().ok();

(gdb) next
15              delay.delay_ms(half_period);

leds[0].on().ok() 文を実行した後、北を向いた赤色LEDが点灯するはずです。

そのままプログラムをステップオーバーしていきましょう。

(gdb) next
17              leds[0].off().ok();

(gdb) next
18              delay.delay_ms(half_period);

delay_ms の呼び出しはプログラムを0.5秒間ブロックしますが、next コマンド自体の実行にも少し時間がかかるため、気づかないかもしれません。ただし、leds[0].off() 文をステップオーバーした後には、赤色LEDが消灯するはずです。

このプログラムが何をするかは、もう想像できるでしょう。continue コマンドを使って、中断せずに実行させてみましょう。

(gdb) continue
Continuing.

では、もっと面白いことをしてみましょう。GDB を使って、プログラムの挙動を変更します。

まず、Ctrl+C を押して無限ループを止めましょう。おそらく Led::onLed::offdelay_ms のどこかで停止します。

^C
Program received signal SIGINT, Interrupt.
0x08003434 in core::ptr::read_volatile<u32> (src=0xe000e010)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053

私の場合、プログラムは read_volatile 関数の中で実行を停止しました。GDB の出力には、そのことに関する興味深い情報が表示されています: core::ptr::read_volatile (src=0xe000e010)。これは、その関数が core クレート由来であり、引数 src = 0xe000e010 を伴って呼び出されたことを意味します。

補足すると、関数の引数をより明示的に表示するには info args コマンドを使います。

(gdb) info args
src = 0xe000e010

プログラムがどこで停止していても、backtrace コマンド(短縮形は bt)の出力を見れば、そこに至るまでの経路をいつでも確認できます。

(gdb) backtrace
#0  0x08003434 in core::ptr::read_volatile<u32> (src=0xe000e010)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053
#1  0x08002d66 in vcell::VolatileCell<u32>::get<u32> (self=0xe000e010) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/vcell-0.1.3/src/lib.rs:33
#2  volatile_register::RW<u32>::read<u32> (self=0xe000e010) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/volatile-register-0.2.0/src/lib.rs:75
#3  cortex_m::peripheral::SYST::has_wrapped (self=0x20009fa4)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/peripheral/syst.rs:136
#4  0x08003004 in stm32f3xx_hal::delay::{{impl}}::delay_us (self=0x20009fa4, us=500000)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:58
#5  0x08002f3e in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:32
#6  0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38
#7  0x0800024c in led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:15
#8  0x08000206 in led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7

backtrace は、現在の関数から main までの関数呼び出しのトレースを表示します。

本題に戻りましょう。やりたいことを行うには、まず main 関数に戻る必要があります。それには finish コマンドを使えます。このコマンドはプログラムの実行を再開し、現在の関数からプログラムが戻った直後に再び停止します。これを数回呼び出す必要があります。

(gdb) finish Run till exit from #0 0x08003434 in core::ptr::read_volatile (src=0xe000e010) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053 cortex_m::peripheral::SYST::has_wrapped (self=0x20009fa4) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/peripheral/syst.rs:136 136 self.csr.read() & SYST_CSR_COUNTFLAG != 0 Value returned is $1 = 5

(..)

(gdb) finish Run till exit from #0 0x08002f3e in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:32 0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38 38 self.delay_ms(u32(ms));

(gdb) finish Run till exit from #0 0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38 0x0800024c in led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:15 15 delay.delay_ms(half_period);


`main` に戻ってきました。ここにはローカル変数 `half_period` があります。

(gdb) print half_period $3 = 500


では、`set` コマンドを使ってこの変数を変更してみましょう。

(gdb) set half_period = 100

(gdb) print half_period $5 = 100


`continue` コマンドを使ってプログラムを再びそのまま実行させると、LED が今度はずっと速く
点滅するのが **見えるかもしれません** が、おそらく点滅速度は変わっていません。**何が起きたのでしょうか?**

`Ctrl+C` でプログラムを停止し、その後 `main:14` にブレークポイントを設定しましょう。
``` console
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
core::cell::UnsafeCell<u32>::get<u32> (self=0x20009fa4)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:1711
1711        pub const fn get(&self) -> *mut T {

次に、main.rs:14 にブレークポイントを設定して、continue を実行します。

(gdb) break main.rs:14
Breakpoint 2 at 0x8000236: file src/05-led-roulette/src/main.rs, line 14.
(gdb) continue
Continuing.

Breakpoint 2, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:14
14              leds[0].on().ok();

次に、可能であれば端末ウィンドウを縦約 80 行、横約 170 文字になるように開いてください。

: そこまで大きく端末を開けなくても問題ありません。その場合は --Type <RET> for more, q to quit, c to continue without paging-- と表示されるので、 (gdb) プロンプトが見えるまで return を入力してください。その後、端末ウィンドウを スクロールして結果を確認してください。

(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h51e7c3daad2af251E:
8       fn main() -> ! {
   0x08000208 <+0>:     push    {r7, lr}
   0x0800020a <+2>:     mov     r7, sp
   0x0800020c <+4>:     sub     sp, #64 ; 0x40
   0x0800020e <+6>:     add     r0, sp, #32

9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();
   0x08000210 <+8>:     bl      0x8000302 <aux5::init>
   0x08000214 <+12>:    b.n     0x8000216 <led_roulette::__cortex_m_rt_main+14>
   0x08000216 <+14>:    add     r0, sp, #32
   0x08000218 <+16>:    add     r1, sp, #4
   0x0800021a <+18>:    ldmia.w r0, {r2, r3, r4, r12, lr}
   0x0800021e <+22>:    stmia.w r1, {r2, r3, r4, r12, lr}
   0x08000222 <+26>:    ldr     r0, [sp, #52]   ; 0x34
   0x08000224 <+28>:    ldr     r1, [sp, #56]   ; 0x38
   0x08000226 <+30>:    str     r1, [sp, #28]
   0x08000228 <+32>:    str     r0, [sp, #24]
   0x0800022a <+34>:    mov.w   r0, #500        ; 0x1f4

10
11          let half_period = 500_u16;
   0x0800022e <+38>:    strh.w  r0, [r7, #-2]

12
13          loop {
   0x08000232 <+42>:    b.n     0x8000234 <led_roulette::__cortex_m_rt_main+44>
   0x08000234 <+44>:    add     r0, sp, #24
   0x08000268 <+96>:    b.n     0x8000234 <led_roulette::__cortex_m_rt_main+44>

14              leds[0].on().ok();
=> 0x08000236 <+46>:    bl      0x80001ec <switch_hal::output::{{impl}}::on<stm32f3xx_hal::gpio::gpioe::PEx<stm32f3xx_hal::gpio::Output<stm32f3xx_hal::gpio::PushPull>>>>
   0x0800023a <+50>:    b.n     0x800023c <led_roulette::__cortex_m_rt_main+52>
   0x0800023c <+52>:    bl      0x8000594 <core::result::Result<(), core::convert::Infallible>::ok<(),core::convert::Infallible>>
   0x08000240 <+56>:    b.n     0x8000242 <led_roulette::__cortex_m_rt_main+58>
   0x08000242 <+58>:    add     r0, sp, #4
   0x08000244 <+60>:    mov.w   r1, #500        ; 0x1f4

15              delay.delay_ms(half_period);
   0x08000248 <+64>:    bl      0x8002f5c <stm32f3xx_hal::delay::{{impl}}::delay_ms>
   0x0800024c <+68>:    b.n     0x800024e <led_roulette::__cortex_m_rt_main+70>
   0x0800024e <+70>:    add     r0, sp, #24

16
17              leds[0].off().ok();
   0x08000250 <+72>:    bl      0x800081a <switch_hal::output::{{impl}}::off<stm32f3xx_hal::gpio::gpioe::PEx<stm32f3xx_hal::gpio::Output<stm32f3xx_hal::gpio::PushPull>>>>
   0x08000254 <+76>:    b.n     0x8000256 <led_roulette::__cortex_m_rt_main+78>
   0x08000256 <+78>:    bl      0x8000594 <core::result::Result<(), core::convert::Infallible>::ok<(),core::convert::Infallible>>
   0x0800025a <+82>:    b.n     0x800025c <led_roulette::__cortex_m_rt_main+84>
   0x0800025c <+84>:    add     r0, sp, #4
   0x0800025e <+86>:    mov.w   r1, #500        ; 0x1f4

18              delay.delay_ms(half_period);
   0x08000262 <+90>:    bl      0x8002f5c <stm32f3xx_hal::delay::{{impl}}::delay_ms>
   0x08000266 <+94>:    b.n     0x8000268 <led_roulette::__cortex_m_rt_main+96>

End of assembler dump.

上のダンプで遅延が変わらなかった理由は、コンパイラが half_period は変化しないと 認識し、その結果 delay.delay_ms(half_period); が呼ばれている 2 か所で mov.w r1, #500 が見えているためです。つまり、half_period の値を変更しても 何も起こりません。

   0x08000244 <+60>:    mov.w   r1, #500        ; 0x1f4

15              delay.delay_ms(half_period);
   0x08000248 <+64>:    bl      0x8002f5c <stm32f3xx_hal::delay::{{impl}}::delay_ms>

(..)

   0x0800025e <+86>:    mov.w   r1, #500        ; 0x1f4

18              delay.delay_ms(half_period);
   0x08000262 <+90>:    bl      0x8002f5c <stm32f3xx_hal::delay::{{impl}}::delay_ms>

この問題に対する 1 つの解決策は、以下に示すように half_periodVolatile でラップすることです。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use volatile::Volatile;
use aux5::{Delay, DelayMs, LedArray, OutputSwitch, entry};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let mut half_period = 500_u16;
    let v_half_period = Volatile::new(&mut half_period);

    loop {
        leds[0].on().ok();
        delay.delay_ms(v_half_period.read());

        leds[0].off().ok();
        delay.delay_ms(v_half_period.read());
    }
}

Cargo.toml[dependencies] セクションに volatile = "0.4.3" を追加してください。

[dependencies]
aux5 = { path = "auxiliary" }
volatile = "0.4.3"

上記のコードで Volatile を使うと、half_period を変更できるようになり、 さまざまな値を試せるようになります。以下にコマンドの一覧を示し、その後で 説明します。# xxxx は説明のためのものです。

$ cargo run --target thumbv7em-none-eabihf   # プログラムをコンパイルして gdb に読み込む
(gdb) target remote :3333           # PC から STM32F3DISCOVERY ボードに接続する
(gdb) load                          # プログラムを書き込む
(gdb) break main.rs:16              # ループの先頭にブレークポイント 1 を設定する
(gdb) continue                      # 続行。main.rs:16 で停止する
(gdb) disable 1                     # ブレークポイント 1 を無効にする
(gdb) set print asm-demangle on     # asm-demangle を有効にする
(gdb) disassemble /m                # main 関数を逆アセンブルする
(gdb) continue                      # LED が 1/2 秒点灯し、その後 1/2 秒消灯する
^C                                  # Ctrl+C で停止する
(gdb) enable 1                      # ブレークポイント 1 を有効にする
(gdb) continue                      # 続行。main.rs:16 で停止する
(gdb) print half_period             # half_period を表示する。結果は 500
(gdb) set half_period = 2000        # half_period を 2000ms に設定する
(gdb) print half_period             # half_period を表示する。結果は 2000
(gdb) disable 1                     # ブレークポイント 1 を無効にする
(gdb) continue                      # LED が 2 秒点灯し、その後 2 秒消灯する
^C                                  # Ctrl+C で停止する
(gdb) quit                          # gdb を終了する

重要な変更点はソースコードの 13、17、20 行目にあり、これは 逆アセンブル結果で確認できます。13 行目では v_half_period を作成し、その後 17 行目と 20 行目でその値を read() しています。これは、set half_period = 2000 を実行すると、LED が 2 秒間点灯し、その後 2 秒間消灯するようになることを意味します。

$ cargo run --target thumbv7em-none-eabihf
   Compiling led-roulette v0.2.0 (~/embedded-discovery/src/05-led-roulette)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `arm-none-eabi-gdb -q ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...

(gdb) target remote :3333
Remote debugging using :3333
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x5258 lma 0x8000194
Loading section .rodata, size 0xbd8 lma 0x80053ec
Start address 0x08000194, load size 24516
Transfer rate: 21 KB/sec, 6129 bytes/write.

(gdb) break main.rs:16
Breakpoint 1 at 0x8000246: file src/05-led-roulette/src/main.rs, line 16.
Note: automatically using hardware breakpoints for read-only addresses.

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) disable 1

(gdb) set print asm-demangle on

(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17he1f2bc7990b13731E:
9       fn main() -> ! {
   0x0800020e <+0>:     push    {r7, lr}
   0x08000210 <+2>:     mov     r7, sp
   0x08000212 <+4>:     sub     sp, #72 ; 0x48
   0x08000214 <+6>:     add     r0, sp, #36     ; 0x24

10          let (mut delay, mut leds): (Delay, LedArray) = aux5::init();
   0x08000216 <+8>:     bl      0x800036a <aux5::init>
   0x0800021a <+12>:    b.n     0x800021c <led_roulette::__cortex_m_rt_main+14>
   0x0800021c <+14>:    add     r0, sp, #36     ; 0x24
   0x0800021e <+16>:    add     r1, sp, #8
   0x08000220 <+18>:    ldmia.w r0, {r2, r3, r4, r12, lr}
   0x08000224 <+22>:    stmia.w r1, {r2, r3, r4, r12, lr}
   0x08000228 <+26>:    ldr     r0, [sp, #56]   ; 0x38
   0x0800022a <+28>:    ldr     r1, [sp, #60]   ; 0x3c
   0x0800022c <+30>:    str     r1, [sp, #32]
   0x0800022e <+32>:    str     r0, [sp, #28]
   0x08000230 <+34>:    mov.w   r0, #500        ; 0x1f4

11
12          let mut half_period = 500_u16;
   0x08000234 <+38>:    strh.w  r0, [r7, #-6]
   0x08000238 <+42>:    subs    r0, r7, #6

13          let v_half_period = Volatile::new(&mut half_period);
   0x0800023a <+44>:    bl      0x800033e <volatile::Volatile<&mut u16, volatile::access::ReadWrite>::new<&mut u16>>
   0x0800023e <+48>:    str     r0, [sp, #68]   ; 0x44
   0x08000240 <+50>:    b.n     0x8000242 <led_roulette::__cortex_m_rt_main+52>

14
15          loop {
   0x08000242 <+52>:    b.n     0x8000244 <led_roulette::__cortex_m_rt_main+54>
   0x08000244 <+54>:    add     r0, sp, #28
   0x08000288 <+122>:   b.n     0x8000244 <led_roulette::__cortex_m_rt_main+54>

16              leds[0].on().ok();
=> 0x08000246 <+56>:    bl      0x800032c <switch_hal::output::{{impl}}::on<stm32f3xx_hal::gpio::gpioe::PEx<stm32f3xx_hal::gpio::Output<stm32f3xx_hal::gpio::PushPull>>>>
   0x0800024a <+60>:    b.n     0x800024c <led_roulette::__cortex_m_rt_main+62>
   0x0800024c <+62>:    bl      0x80005fc <core::result::Result<(), core::convert::Infallible>::ok<(),core::convert::Infallible>>
   0x08000250 <+66>:    b.n     0x8000252 <led_roulette::__cortex_m_rt_main+68>
   0x08000252 <+68>:    add     r0, sp, #68     ; 0x44

17              delay.delay_ms(v_half_period.read());
   0x08000254 <+70>:    bl      0x800034a <volatile::Volatile<&mut u16, volatile::access::ReadWrite>::read<&mut u16,u16,volatile::access::ReadWrite>>
   0x08000258 <+74>:    str     r0, [sp, #4]
   0x0800025a <+76>:    b.n     0x800025c <led_roulette::__cortex_m_rt_main+78>
   0x0800025c <+78>:    add     r0, sp, #8
   0x0800025e <+80>:    ldr     r1, [sp, #4]
   0x08000260 <+82>:    bl      0x8002fc4 <stm32f3xx_hal::delay::{{impl}}::delay_ms>
   0x08000264 <+86>:    b.n     0x8000266 <led_roulette::__cortex_m_rt_main+88>
   0x08000266 <+88>:    add     r0, sp, #28

18
19              leds[0].off().ok();
   0x08000268 <+90>:    bl      0x8000882 <switch_hal::output::{{impl}}::off<stm32f3xx_hal::gpio::gpioe::PEx<stm32f3xx_hal::gpio::Output<stm32f3xx_hal::gpio::PushPull>>>>
   0x0800026c <+94>:    b.n     0x800026e <led_roulette::__cortex_m_rt_main+96>
   0x0800026e <+96>:    bl      0x80005fc <core::result::Result<(), core::convert::Infallible>::ok<(),core::convert::Infallible>>
   0x08000272 <+100>:   b.n     0x8000274 <led_roulette::__cortex_m_rt_main+102>
   0x08000274 <+102>:   add     r0, sp, #68     ; 0x44

20              delay.delay_ms(v_half_period.read());
   0x08000276 <+104>:   bl      0x800034a <volatile::Volatile<&mut u16, volatile::access::ReadWrite>::read<&mut u16,u16,volatile::access::ReadWrite>>
   0x0800027a <+108>:   str     r0, [sp, #0]
   0x0800027c <+110>:   b.n     0x800027e <led_roulette::__cortex_m_rt_main+112>
   0x0800027e <+112>:   add     r0, sp, #8
   0x08000280 <+114>:   ldr     r1, [sp, #0]
   0x08000282 <+116>:   bl      0x8002fc4 <stm32f3xx_hal::delay::{{impl}}::delay_ms>
   0x08000286 <+120>:   b.n     0x8000288 <led_roulette::__cortex_m_rt_main+122>

End of assembler dump.

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x080037b2 in core::cell::UnsafeCell<u32>::get<u32> (self=0x20009fa0) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:1716
1716        }

(gdb) enable 1

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) print half_period
$2 = 500

(gdb) disable 1

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x08003498 in core::ptr::read_volatile<u32> (src=0xe000e010) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1052
1052        unsafe { intrinsics::volatile_load(src) }

(gdb) enable 1

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) print half_period
$3 = 500

(gdb) set half_period = 2000

(gdb) print half_period
$4 = 2000

(gdb) disable 1

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x0800348e in core::ptr::read_volatile<u32> (src=0xe000e010) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1046
1046    pub unsafe fn read_volatile<T>(src: *const T) -> T {

(gdb) q
Detaching from program: ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

質問です!half_period の値を下げ始めると何が起こりますか?half_period が いくつになると、LED の点滅が見えなくなりますか?

では、今度はあなたがプログラムを書く番です。

課題

もう課題に挑む準備は万端です!この章の冒頭でお見せしたアプリケーションを、 今度は自分で実装するのがあなたの課題です。

もう一度 GIF を載せておきます:

また、これも参考になるかもしれません:

これはタイミング図です。任意の時点でどの LED が点灯しているか、また各 LED がどれくらいの時間 点灯しているべきかを示しています。X 軸はミリ秒単位の時間です。このタイミング図が示しているのは 1 周期分です。このパターンは 800 ms ごとに繰り返されます。Y 軸では各 LED が方位でラベル付け されています。North、East などです。課題の一環として、Leds 配列の各要素がこれらの方位の どれに対応しているかを突き止める必要があります(ヒント: cargo doc --open ;-))。

この課題に取り組む前に、追加でもう 1 つヒントを挙げておきます。GDB セッションでは、最初に毎回 同じコマンドを入力することになります。GDB の起動直後にいくつかのコマンドを実行するために .gdb ファイルを使えます。こうしておけば、GDB セッションのたびにそれらを手作業で入力する手間を 省けます。

実は、../openocd.gdb はすでに作成済みで、前のセクションで行ったことに加えて いくつかのコマンドも実行しているのがわかります。詳しくは、 コメントを見てください:

$ cat ../openocd.gdb
# gdb リモートサーバーに接続する
target remote :3333

# load でコードを書き込む
load

# 逆アセンブル時に asm 名のデマングリングを有効にする
set print asm-demangle on

# pretty printing を有効にする
set print pretty on

# デフォルトの色は読みにくいことがあるので、sources のスタイルを無効にする
set style sources off

# モニタリングを初期化し、iprintln! マクロの出力が
# itm ポートから itm.txt に送られるようにする
monitor tpiu config internal itm.txt uart off 8000000

# itm ポートを有効にする
monitor itm port 0 on

# main(つまり entry)にブレークポイントを設定する
break main

# DefaultHandler にブレークポイントを設定する
break DefaultHandler

# HardFault にブレークポイントを設定する
break HardFault

# 実行を継続し、main のブレークポイントに到達するまで進める
continue

# entry のトランポリンコードから main へステップ実行する
step

次に、../.cargo/config.toml ファイルを変更して ../openocd.gdb を実行するようにします

nano ../.cargo/config.toml

runner コマンドに -x ../openocd.gdb を追加してください。 arm-none-eabi-gdb を使っているとすると、差分は次のようになります:

~/embedded-discovery/src/05-led-roulette
$ git diff ../.cargo/config.toml
diff --git a/src/.cargo/config.toml b/src/.cargo/config.toml
index ddff17f..02ac952 100644
--- a/src/.cargo/config.toml
+++ b/src/.cargo/config.toml
@@ -1,5 +1,5 @@
 [target.thumbv7em-none-eabihf]
-runner = "arm-none-eabi-gdb -q"
+runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
 # runner = "gdb-multiarch -q"
 # runner = "gdb -q"
 rustflags = [

また、arm-none-eabi-gdb を使っている前提での ../.cargo/config.toml の内容全体は、 次のとおりです:

[target.thumbv7em-none-eabihf]
runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
# runner = "gdb-multiarch -q"
# runner = "gdb -q"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

これで、シンプルに cargo run コマンドを使うだけで、ARM 向けのコードをビルドして gdb セッションを実行できるようになります。gdb セッションはプログラムを自動的に フラッシュし、entry のトランポリンを step でたどりながら main の先頭へジャンプします:

cargo run
~/embedded-discovery/src/05-led-roulette (Update-05-led-roulette-WIP)
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q -x openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...
led_roulette::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x52c0 lma 0x8000194
Loading section .rodata, size 0xb50 lma 0x8005454
Start address 0x08000194, load size 24484
Transfer rate: 21 KB/sec, 6121 bytes/write.
Breakpoint 1 at 0x8000202: file ~/embedded-discovery/src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline ()
    at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]
led_roulette::__cortex_m_rt_main () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:9
9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

discovery book をフォークする

まだであれば、変更を自分の fork 上の自分のブランチに保存できるように、 embedded discovery book をフォークしておくのがよいでしょう。 自分用のブランチを作成し、master ブランチはそのままにしておくことをおすすめします。 そうすれば、fork 側の master ブランチを upstream リポジトリと同期したままに できます。また、PR をより簡単に作成してこの本を改善しやすくなります。よろしくお願いします

私の解答

どのような解答になりましたか?

私のものは次のとおりです:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::{Delay, DelayMs, LedArray, OutputSwitch, entry};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let ms = 50_u8;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().ok();
            delay.delay_ms(ms);
            leds[curr].off().ok();
            delay.delay_ms(ms);
        }
    }
}

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

$ cargo build --target thumbv7em-none-eabihf --release

次の gdb コマンドでテストできます:

$ # または、単に cargo run --target thumbv7em-none-eabihf --release を実行してもかまいません
$ arm-none-eabi-gdb target/thumbv7em-none-eabihf/release/led-roulette
$ #                                              ~~~~~~~

バイナリサイズは常に気にかけておくべきものです!あなたの解答はどれくらいの大きさですか? それは release バイナリに対して size コマンドを実行することで確認できます:

$ # size target/thumbv7em-none-eabihf/debug/led-roulette と同等
$ cargo size --target thumbv7em-none-eabihf --bin led-roulette -- -A
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
led-roulette  :
section               size        addr
.vector_table          404   0x8000000
.text                21144   0x8000194
.rodata               3144   0x800542c
.data                    0  0x20000000
.bss                     4  0x20000000
.uninit                  0  0x20000004
.debug_abbrev        19160         0x0
.debug_info         471239         0x0
.debug_aranges       18376         0x0
.debug_ranges       102536         0x0
.debug_str          508618         0x0
.debug_pubnames      76975         0x0
.debug_pubtypes     112797         0x0
.ARM.attributes         58         0x0
.debug_frame         55848         0x0
.debug_line         282067         0x0
.debug_loc             845         0x0
.comment               147         0x0
Total              1673362


$ cargo size --target thumbv7em-none-eabihf --bin led-roulette --release -- -A
    Finished release [optimized + debuginfo] target(s) in 0.03s
led-roulette  :
section              size        addr
.vector_table         404   0x8000000
.text                5380   0x8000194
.rodata               564   0x8001698
.data                   0  0x20000000
.bss                    4  0x20000000
.uninit                 0  0x20000004
.debug_loc           9994         0x0
.debug_abbrev        1821         0x0
.debug_info         74974         0x0
.debug_aranges        600         0x0
.debug_ranges        6848         0x0
.debug_str          52828         0x0
.debug_pubnames     20821         0x0
.debug_pubtypes     18891         0x0
.ARM.attributes        58         0x0
.debug_frame         1088         0x0
.debug_line         15307         0x0
.comment               19         0x0
Total              209601

Cargo プロジェクトは、LTO を使って release バイナリをビルドするように、すでに設定されています。

この出力の読み方はわかりますか? text セクションにはプログラムの命令が含まれています。私の 場合は約 5.25KB です。一方、data セクションと bss セクションには、RAM に静的に割り当てられる 変数(static 変数)が含まれています。aux5::init では static 変数が使われているため、bss が 4 バイト表示されています。

最後にもう 1 つ!これまでプログラムは GDB の中から実行してきましたが、プログラム自体は GDB にまったく依存していません。これを確認するには、GDB と OpenOCD の両方を終了してから、 ボード上の黒いボタンを押してボードをリセットしてください。LED ルーレットアプリケーションは GDB の介入 なしで動作します。

Hello, world!

注意 STM32F3DISCOVERY 上の「ソルダーブリッジ」SB10(ボード裏面を参照)は、 以下に示す ITM と iprint! マクロを使用するために必要ですが、デフォルトでははんだ付けされていませんUser Manual の 21 ページを参照)。 (より正確には、これは実際にはボードのリビジョンに依存します。ボードが 旧版の User Manual に 書かれている古いバージョンであれば、SB10 ははんだ付けされています。修正が必要かどうかを判断するために、 自分のボードを確認してください。)

要点 これを修正するには 2 つの方法があります。SB10 のソルダーブリッジをはんだ付けするか、 以下の図のように SWO と PB3 の間をメス-メスのジャンパーワイヤで接続してください。


低レベルなことを始める前に、もう少しだけ便利な魔法を使いましょう。

LED を点滅させることは、組み込みの世界における “Hello, world” のようなものです。

しかし、この節では、コンピューターのコンソールに出力する、ちゃんとした “Hello, world” プログラムを実行します。

06-hello-world ディレクトリに移動してください。そこにはスターターコードがいくつかあります:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux6::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let mut itm = aux6::init();

    iprintln!(&mut itm.stim[0], "Hello, world!");

    loop {}
}

iprintln マクロはメッセージを整形し、マイクロコントローラーの ITM に出力します。ITM は Instrumentation Trace Macrocell の略で、SWD (Serial Wire Debug) の上に成り立つ通信プロトコルです。これは、マイクロコントローラーからデバッグホストへ メッセージを送るために使用できます。この通信は 一方向 です。つまり、デバッグホストから マイクロコントローラーへデータを送ることはできません。

デバッグセッションを管理している OpenOCD は、この ITM チャネル を通して送られたデータを受信し、 それをファイルにリダイレクトできます。

ITM プロトコルは フレーム で動作します(Ethernet フレームのようなものだと考えてください)。 各フレームはヘッダーと可変長のペイロードを持ちます。OpenOCD はこれらのフレームを受信し、解析せずに そのままファイルへ書き込みます。したがって、マイクロコントローラーが iprintln マクロを使って 文字列 “Hello, world!” を送信しても、OpenOCD の出力ファイルにはその文字列がそのまま 入るわけではありません。

元の文字列を取り出すには、OpenOCD の出力ファイルを解析する必要があります。新しいデータが到着するたびに この解析を行うために、itmdump プログラムを使います。

installation chapter で、すでに itmdump プログラムをインストールしているはずです。

新しいターミナルで、*nix OS を使っている場合は /tmp ディレクトリ内で、Windows を使っている場合は %TEMP% ディレクトリ内で、このコマンドを実行してください。これは OpenOCD を実行している ディレクトリと同じである必要があります。

注記 itmdumpopenocd の両方が 同じディレクトリから実行されていることが非常に重要です!

$ # itmdump 用ターミナル

$ # *nix
$ cd /tmp && touch itm.txt

$ # Windows
$ cd %TEMP% && type nul >> itm.txt

$ # 共通
$ itmdump -F -f itm.txt

itmdump は現在 itm.txt ファイルを監視しているため、このコマンドはブロックします。このターミナルは 開いたままにしておいてください。

STM32F3DISCOVERY ボードがコンピューターに接続されていることを確認してください。OpenOCD を起動するために、 /tmp ディレクトリ(Windows では %TEMP%)から別のターミナルを開き、chapter 3 で説明したのと 同様に進めます。

では、スターターコードをビルドしてマイクロコントローラーに書き込みましょう。

これからアプリケーションをビルドして実行します。cargo run を使います。そして next を使って ステップ実行します。openocd.gdb には monitor コマンドが含まれているので、OpenOCD は ITM の出力を itm.txt にリダイレクトし、itmdump はそれを自身のターミナルウィンドウに書き出します。 また、ブレークポイントを設定してトランポリンをステップ実行するので、fn main() の最初の実行可能な 文で停止します:

~/embedded-discovery/src/06-hello-world
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q -x ../openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world...
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:14
14          loop {}
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x2828 lma 0x8000194
Loading section .rodata, size 0x638 lma 0x80029bc
Start address 0x08000194, load size 12276
Transfer rate: 18 KB/sec, 4092 bytes/write.
Breakpoint 1 at 0x80001f0: file ~/embedded-discovery/src/06-hello-world/src/main.rs, line 8.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x800092a: file /home/wink/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 570.
Breakpoint 3 at 0x80029a8: file /home/wink/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 560.

Breakpoint 1, hello_world::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/06-hello-world/src/main.rs:8
8       #[entry]
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          let mut itm = aux6::init();

(gdb)

ここで next コマンドを実行すると、aux6::init() が実行され、main.rs の次の実行可能な文で 停止します。つまり 12 行目に移動します:

(gdb) next
12	    iprintln!(&mut itm.stim[0], "Hello, world!");

次にもう一度 next コマンドを実行すると、12 行目、つまり iprintln が実行され、14 行目で停止します:

(gdb) next
14	    loop {}

これで iprintln が実行されたので、itmdump のターミナルウィンドウには Hello, world! という文字列が表示されるはずです:

$ itmdump -F -f itm.txt
(...)
Hello, world!

すばらしいでしょう? 以降の節では、iprintln をロギングツールとして自由に使ってください。

次へ: それだけではありません! ITM を使うのは iprint! マクロだけではありません。:-)

panic!

panic! マクロも、その出力を ITM に送ります!

main 関数を次のように変更してください:

#[entry]
fn main() -> ! {
    panic!("Hello, world!");
}

実行する前に、もう 1 つ提案があります。gdb を終了するたびに確認を 求められるのは不便です。ホームディレクトリに次のファイル ~/.gdbinit を追加して、すぐに終了するようにしましょう:

$ cat ~/.gdbinit
define hook-quit
  set confirm off
end

では、cargo run を使うと fn main() の最初の行で停止します:

$ cargo run
   Compiling hello-world v0.2.0 (~/embedded-discovery/src/06-hello-world)
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `arm-none-eabi-gdb -q -x ../openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world...
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          panic!("Hello, world!");
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x20fc lma 0x8000194
Loading section .rodata, size 0x554 lma 0x8002290
Start address 0x08000194, load size 10212
Transfer rate: 17 KB/sec, 3404 bytes/write.
Breakpoint 1 at 0x80001f0: file ~/embedded-discovery/src/06-hello-world/src/main.rs, line 8.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x8000222: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 570.
Breakpoint 3 at 0x800227a: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 560.

Breakpoint 1, hello_world::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/06-hello-world/src/main.rs:8
8       #[entry]
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          panic!("Hello, world!");
(gdb)

入力を減らすために短いコマンド名を使います。c を入力してから Enter または Return キーを押してください:

(gdb) c
Continuing.

すべてうまくいけば、itmdump ターミナルに新しい出力が表示されます。

$ # itmdump ターミナル
(..)
panicked at 'Hello, world!', src/06-hello-world/src/main.rs:10:5

次に Ctrl-c を入力します。これでランタイム内のループを抜けます:

^C
Program received signal SIGINT, Interrupt.
0x0800115c in panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:57
57	        atomic::compiler_fence(Ordering::SeqCst);

最終的に、panic! は単なる別の関数呼び出しにすぎないので、 関数呼び出しの痕跡が残ることがわかります。これにより、backtrace または単に bt を使って、パニックを引き起こしたコールスタックを確認できます:

(gdb) bt
#0  panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47
#1  0x080005c2 in core::panicking::panic_fmt () at library/core/src/panicking.rs:92
#2  0x0800055a in core::panicking::panic () at library/core/src/panicking.rs:50
#3  0x08000210 in hello_world::__cortex_m_rt_main () at src/06-hello-world/src/main.rs:10
#4  0x080001f4 in hello_world::__cortex_m_rt_main_trampoline () at src/06-hello-world/src/main.rs:8

もう 1 つできることは、ログ出力を行う にパニックを捕捉することです。 そのためにいくつかのことを行います。先頭の状態にリセットし、ブレークポイント 1 を 無効化し、rust_begin_unwind に新しいブレークポイントを設定し、ブレークポイントを一覧表示してから続行します:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

(gdb) disable 1

(gdb) break rust_begin_unwind 
Breakpoint 4 at 0x800106c: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs, line 47.

(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x080001f0 in hello_world::__cortex_m_rt_main_trampoline 
                                           at ~/prgs/rust/tutorial/embedded-discovery/src/06-hello-world/src/main.rs:8
        breakpoint already hit 1 time
2       breakpoint     keep y   0x08000222 in cortex_m_rt::DefaultHandler_ 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:570
3       breakpoint     keep y   0x0800227a in cortex_m_rt::HardFault_ 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:560
4       breakpoint     keep y   0x0800106c in panic_itm::panic 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47

(gdb) c
Continuing.

Breakpoint 4, panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47
47          interrupt::disable();

今回は itmdump コンソールに何も表示されなかったことに気付くでしょう。もし continue を使ってプログラムを再開すると、新しい行が出力されます。

後のセクションでは、別のより単純な通信プロトコルを見ていきます。

最後に、q コマンドを入力して終了します。確認を求められることなく、 すぐに終了します:

(gdb) q
Detaching from program: ~/prgs/rust/tutorial/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

さらに短いやり方として、Ctrl-d を入力することもできます。これで 1 回キーを打つ手間が省けます!

注記 この場合、(gdb) プロンプトは quit) で上書きされます

quit)
Detaching from program: ~/prgs/rust/tutorial/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

レジスタ

Led API が内部で何をしているのかを探っていく時間です。

一言でいえば、これはいくつかの特別なメモリ領域に書き込んでいるだけです。07-registers ディレクトリに移動して、スターターコードを文ごとに実行していきましょう。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux7::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        // A magic address!
        const GPIOE_BSRR: u32 = 0x48001018;

        // Turn on the "North" LED (red)
        *(GPIOE_BSRR as *mut u32) = 1 << 9;

        // Turn on the "East" LED (green)
        *(GPIOE_BSRR as *mut u32) = 1 << 11;

        // Turn off the "North" LED
        *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);

        // Turn off the "East" LED
        *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
    }

    loop {}
}

このマジックは何でしょうか?

アドレス 0x48001018レジスタ を指しています。レジスタとは、周辺機能 を制御する特別なメモリ領域のことです。周辺機能とは、マイクロコントローラのパッケージ内でプロセッサのすぐ隣にある電子回路であり、プロセッサに追加の機能を提供します。というのも、プロセッサはそれ単体では計算と論理処理しかできないからです。

この特定のレジスタは汎用入出力(GPIO)ピン を制御します(GPIO 自体が 周辺機能です)。そして、それらの各ピンを low または high駆動 するために使用できます。

余談: LED、デジタル出力、電圧レベル

駆動? ピン? Low? High?

ピンは電気的な接点です。私たちのマイクロコントローラにはそのような接点がいくつもあり、そのうちのいくつかは LED に接続されています。LED、すなわち発光ダイオードは、ある極性で電圧が加えられたときにのみ発光します。

幸いなことに、マイクロコントローラのピンは正しい極性で LED に接続されています。したがって、LED を点灯させるために必要なのは、そのピンから 0 でない電圧を 出力 することだけです。LED に接続されたピンは デジタル出力 として設定されており、出力できる電圧レベルは 2 種類だけです。すなわち、「low」の 0 ボルトか、「high」の 3 ボルトです。「high」の(電圧)レベルは LED を点灯させ、「low」の(電圧)レベルは消灯させます。

これらの「low」と「high」の状態は、デジタル論理の概念に直接対応しています。「low」は 0 または false であり、「high」は 1 または true です。これが、このピン構成がデジタル出力として知られている理由です。


なるほど。では、このレジスタが何をするのかはどうやって調べればよいのでしょうか? RTRM(リファレンスマニュアルを読む)するときです!

RTRM: リファレンスマニュアルを読む

先ほど、マイクロコントローラには複数のピンがあると述べました。便宜上、これらのピンは 16 本ずつの ポート にまとめられています。各ポートには Port A、Port B などのように文字で 名前が付けられており、各ポート内のピンには 0 から 15 までの番号が付いています。

まず最初に調べるべきなのは、どのピンがどの LED に接続されているかです。この情報は STM32F3DISCOVERY の User Manual にあります(コピーをダウンロードしましたよね?)。この場合は、 次のセクションです。

Section 6.4 LEDs - Page 18

マニュアルには次のように書かれています。

  • LD3(北側の LED)は、ピン PE9 に接続されています。PE9 は Port E の Pin 9 の短縮形です。
  • LD7(東側の LED)は、ピン PE11 に接続されています。

ここまでで、北側/東側の LED をオン/オフするには、PE9 と PE11 の状態を変更すればよいことが わかりました。これらのピンは Port E の一部なので、GPIOE ペリフェラルを扱う必要があります。

各ペリフェラルには、それに対応するレジスタ ブロック があります。レジスタブロックとは、 連続したメモリ上に配置されたレジスタの集まりです。レジスタブロックが始まるアドレスは、その ベースアドレスとして知られています。GPIOE ペリフェラルのベースアドレスが何かを調べる必要があります。その 情報は、マイクロコントローラの Reference Manual の次のセクションにあります。

Section 3.2.2 Memory map and register boundary addresses - Page 51

表によると、GPIOE レジスタブロックのベースアドレスは 0x4800_1000 です。

また、各ペリフェラルにはドキュメント内にそれぞれ専用のセクションがあります。これらのセクションはどれも、 そのペリフェラルのレジスタブロックに含まれるレジスタの表で終わります。GPIO ペリフェラルファミリについては、その表は次にあります。

Section 11.4.12 GPIO register map - Page 243

BSRR は、セット/リセットに使用するレジスタです。GPIOE のベースアドレスからの オフセット値は 0x18 です。リファレンスマニュアルで BSRR を調べることができます。 GPIO Registers -> GPIO port bit set/reset register (GPIOx_BSRR)。

次に、その特定のレジスタの説明箇所へ移動する必要があります。それは数ページ前の次の場所にあります。

Section 11.4.7 GPIO port bit set/reset register (GPIOx_BSRR) - Page 240

ついに!

これが、先ほど書き込んでいたレジスタです。ドキュメントには興味深いことがいくつか書かれています。まず、この レジスタは書き込み専用です … では、その値を読んでみましょう :-)

GDB の examine コマンド x を使います。

(gdb) next
16              *(GPIOE_BSRR as *mut u32) = 1 << 9;

(gdb) x 0x48001018
0x48001018:     0x00000000

(gdb) # 次のコマンドで北側の LED がオンになる
(gdb) next
19              *(GPIOE_BSRR as *mut u32) = 1 << 11;

(gdb) x 0x48001018
0x48001018:     0x00000000

レジスタを読むと 0 が返ってきます。これはドキュメントの記述と一致しています。

ドキュメントがもうひとつ述べているのは、ビット 0 から 15 を使って対応するピンを セット できるということです。 つまり、ビット 0 はピン 0 をセットします。ここで セット とは、そのピンに ハイ の値を 出力することを意味します。

また、ドキュメントには、ビット 16 から 31 を使って対応するピンを リセット できるとも書かれています。この 場合、ビット 16 はピン番号 0 をリセットします。想像できると思いますが、リセット とは、そのピンに ロー の値を 出力することを意味します。

その情報をプログラムと照らし合わせると、すべてつじつまが合っています。

  • 1 << 9 (BS9 = 1) を BSRR に書き込むと、PE9ハイ に設定されます。これにより北側の LED が オン になります。

  • 1 << 11 (BS11 = 1) を BSRR に書き込むと、PE11ハイ に設定されます。これにより東側の LED が オン になります。

  • 1 << 25 (BR9 = 1) を BSRR に書き込むと、PE9ロー に設定されます。これにより北側の LED が オフ になります。

  • 最後に、1 << 27 (BR11 = 1) を BSRR に書き込むと、PE11ロー に設定されます。これにより東側の LED が オフ になります。

(誤)最適化

レジスタの読み書きはかなり特殊です。副作用を体現したものだと言ってしまってもよいくらいです。前の例では、同じレジスタに 4 つの異なる値を書き込みました。そのアドレスがレジスタだと知らなければ、ロジックを単純化して最後の値 1 << (11 + 16) だけを書き込むようにしてしまうかもしれません。

実際には、コンパイラのバックエンド / オプティマイザである LLVM は、こちらがレジスタを扱っていることを知りません。そのため、これらの書き込みをまとめてしまい、結果としてプログラムの動作を変えてしまいます。手早く確認してみましょう。

$ cargo run --release
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:9
9           aux7::init();

(gdb) next
25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);

(gdb) disassemble /m
Dump of assembler code for function _ZN9registers18__cortex_m_rt_main17h45b1ef53e18aa8d0E:
8       fn main() -> ! {
   0x08000248 <+0>:     push    {r7, lr}
   0x0800024a <+2>:     mov     r7, sp

9           aux7::init();
   0x0800024c <+4>:     bl      0x8000260 <aux7::init>
   0x08000250 <+8>:     movw    r0, #4120       ; 0x1018
   0x08000254 <+12>:    mov.w   r1, #134217728  ; 0x8000000
   0x08000258 <+16>:    movt    r0, #18432      ; 0x4800

10
11          unsafe {
12              // マジックアドレス!
13              const GPIOE_BSRR: u32 = 0x48001018;
14
15              // "North" LED(赤)を点灯
16              *(GPIOE_BSRR as *mut u32) = 1 << 9;
17
18              // "East" LED(緑)を点灯
19              *(GPIOE_BSRR as *mut u32) = 1 << 11;
20
21              // "North" LED を消灯
22              *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
23
24              // "East" LED を消灯
25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
=> 0x0800025c <+20>:    str     r1, [r0, #0]
   0x0800025e <+22>:    b.n     0x800025e <registers::__cortex_m_rt_main+22>

End of assembler dump.

今回は LED の状態が変化しませんでした! str 命令は、レジスタに値を書き込む命令です。debug(非最適化)プログラムにはそれが 4 つあり、レジスタへの各書き込みごとに 1 つずつ対応していましたが、release(最適化)プログラムには 1 つしかありません。

これは objdump を使って確認でき、その出力を out.asm に保存できます。

# cargo objdump -- -d --no-show-raw-insn --print-imm-hex --source target/thumbv7em-none-eabihf/debug/registers と同じ
cargo objdump --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > debug.txt

次に、debug.txt の中で main を探すと、4 つの str 命令があることが分かります。

080001ec <main>:
; #[entry]
 80001ec:       push    {r7, lr}
 80001ee:       mov     r7, sp
 80001f0:       bl      #0x2
 80001f4:       trap

080001f6 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2>:
; fn main() -> ! {
 80001f6:       push    {r7, lr}
 80001f8:       mov     r7, sp
;     aux7::init();
 80001fa:       bl      #0x3e
 80001fe:       b       #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0xa>
;         *(GPIOE_BSRR as *mut u32) = 1 << 9;
 8000200:       movw    r0, #0x2640
 8000204:       movt    r0, #0x800
 8000208:       ldr     r0, [r0]
 800020a:       movw    r1, #0x1018
 800020e:       movt    r1, #0x4800
 8000212:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << 11;
 8000214:       movw    r0, #0x2648
 8000218:       movt    r0, #0x800
 800021c:       ldr     r0, [r0]
 800021e:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
 8000220:       movw    r0, #0x2650
 8000224:       movt    r0, #0x800
 8000228:       ldr     r0, [r0]
 800022a:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
 800022c:       movw    r0, #0x2638
 8000230:       movt    r0, #0x800
 8000234:       ldr     r0, [r0]
 8000236:       str     r0, [r1]
;     loop {}
 8000238:       b       #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
 800023a:       b       #-0x4 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
 (..)

LLVM がプログラムを誤って最適化しないようにするにはどうすればよいのでしょうか? 通常の読み書きの代わりに volatile 操作を使います。

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::entry;

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        // マジックアドレス!
        const GPIOE_BSRR: u32 = 0x48001018;

        // "North" LED(赤)を点灯
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);

        // "East" LED(緑)を点灯
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);

        // "North" LED を消灯
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));

        // "East" LED を消灯
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
    }

    loop {}
}

--release モードで release.txt を生成します。

cargo objdump --release --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > release.txt

次に release.txt の中で main ルーチンを探すと、4 つの str 命令があることが分かります。

0800023e <main>:
; #[entry]
 800023e:       push    {r7, lr}
 8000240:       mov     r7, sp
 8000242:       bl      #0x2
 8000246:       trap

08000248 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0>:
; fn main() -> ! {
 8000248:       push    {r7, lr}
 800024a:       mov     r7, sp
;     aux7::init();
 800024c:       bl      #0x22
 8000250:       movw    r0, #0x1018
 8000254:       mov.w   r1, #0x200
 8000258:       movt    r0, #0x4800
;         intrinsics::volatile_store(dst, src);
 800025c:       str     r1, [r0]
 800025e:       mov.w   r1, #0x800
 8000262:       str     r1, [r0]
 8000264:       mov.w   r1, #0x2000000
 8000268:       str     r1, [r0]
 800026a:       mov.w   r1, #0x8000000
 800026e:       str     r1, [r0]
 8000270:       b       #-0x4 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0+0x28>
 (..)

4 回の書き込み(str 命令)が保持されていることが分かります。gdb を使って実行すると、期待どおりの動作になることも確認できます。

注: 最後の nextloop {} を延々と実行するので、Ctrl-c を使って (gdb) プロンプトに戻ってください。

$ cargo run --release
(..)

Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:9
9       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:11
11          aux7::init();

(gdb) next
18              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);

(gdb) next
21              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);

(gdb) next
24              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));

(gdb) next
27              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));

(gdb) next
^C
Program received signal SIGINT, Interrupt.
0x08000270 in registers::__cortex_m_rt_main ()
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1124
1124            intrinsics::volatile_store(dst, src);
(gdb) 

0xBAAAAAAD アドレス

すべてのペリフェラルメモリにアクセスできるわけではありません。次のプログラムを見てください。

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        ptr::read_volatile(0x4800_1800 as *const u32);
    }

    loop {}
}

このアドレスは、以前に使った GPIOE_BSRR のアドレスに近いですが、このアドレスは無効です。 無効というのは、このアドレスにはレジスタが存在しないという意味です。

では、試してみましょう。

$ cargo run
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:9
9       #[entry]

(gdb) continue
Continuing.

Breakpoint 3, cortex_m_rt::HardFault_ (ef=0x20009fb0)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:560
560         loop {

(gdb)

存在しないメモリを読み取るという無効な操作を実行しようとしたため、プロセッサは 例外、つまりハードウェア例外を発生させました。

ほとんどの場合、例外はプロセッサが無効な操作を実行しようとしたときに発生します。 例外はプログラムの通常の流れを中断し、プロセッサに例外ハンドラを実行させます。 これは単なる関数 / サブルーチンです。

例外にはさまざまな種類があります。各種類の例外は異なる条件で発生し、 それぞれ異なる例外ハンドラによって処理されます。

aux7 クレートは cortex-m-rt クレートに依存しており、後者は 「無効なメモリアドレス」例外を処理する、HardFault という名前のデフォルトの ハードフォールトハンドラを定義しています。openocd.gdbHardFault に ブレークポイントを置いていました。そのため、デバッガは例外ハンドラを実行している 最中にプログラムを停止させたのです。 デバッガから、この例外についてさらに詳しい情報を取得できます。見てみましょう。

(gdb) list
555     #[allow(unused_variables)]
556     #[doc(hidden)]
557     #[link_section = ".HardFault.default"]
558     #[no_mangle]
559     pub unsafe extern "C" fn HardFault_(ef: &ExceptionFrame) -> ! {
560         loop {
561             // UDF 命令に変換されるのを防ぐために何らかの副作用を追加する
562             // 詳細は rust-lang/rust#28728 を参照
563             atomic::compiler_fence(Ordering::SeqCst);
564         }

ef は、例外が発生する直前のプログラム状態のスナップショットです。調べてみましょう。

(gdb) print/x *ef
$1 = cortex_m_rt::ExceptionFrame {
  r0: 0x48001800,
  r1: 0x80036b0,
  r2: 0x1,
  r3: 0x80000000,
  r12: 0xb,
  lr: 0x800020d,
  pc: 0x8001750,
  xpsr: 0xa1000200
}

ここにはいくつかのフィールドがありますが、最も重要なのは pc、つまり Program Counter レジスタです。 このレジスタ内のアドレスは、例外を引き起こした命令を指しています。 問題のある命令の周辺を逆アセンブルしてみましょう。

(gdb) disassemble /m ef.pc
Dump of assembler code for function core::ptr::read_volatile<u32>:
1046    pub unsafe fn read_volatile<T>(src: *const T) -> T {
   0x0800174c <+0>:     sub     sp, #12
   0x0800174e <+2>:     str     r0, [sp, #4]

1047        if cfg!(debug_assertions) && !is_aligned_and_not_null(src) {
1048            // コード生成への影響を小さく保つため、panic は行わない。
1049            abort();
1050        }
1051        // SAFETY: 呼び出し元は `volatile_load` の安全性契約を守らなければならない。
1052        unsafe { intrinsics::volatile_load(src) }
   0x08001750 <+4>:     ldr     r0, [r0, #0]
   0x08001752 <+6>:     str     r0, [sp, #8]
   0x08001754 <+8>:     ldr     r0, [sp, #8]
   0x08001756 <+10>:    str     r0, [sp, #0]
   0x08001758 <+12>:    b.n     0x800175a <core::ptr::read_volatile<u32>+14>

1053    }
   0x0800175a <+14>:    ldr     r0, [sp, #0]
   0x0800175c <+16>:    add     sp, #12
   0x0800175e <+18>:    bx      lr

End of assembler dump.

例外は ldr r0, [r0, #0] 命令、つまり読み取り命令によって発生しました。この命令は r0 レジスタが示すアドレスのメモリを読み取ろうとしました。ちなみに、r0 は CPU (プロセッサ) レジスタであり、メモリマップトレジスタではありません。たとえば GPIO_BSRR のように、対応するアドレスを持っているわけではありません。

例外が発生したまさにその瞬間に、r0 レジスタの値が何だったのかを確認できたら便利だと 思いませんか? 実は、もうすでに確認しています! 先ほど出力した ef の値にある r0 フィールドが、例外発生時の r0 レジスタの値です。もう一度示します。

(gdb) print/x *ef
$1 = cortex_m_rt::ExceptionFrame {
  r0: 0x48001800,
  r1: 0x80036b0,
  r2: 0x1,
  r3: 0x80000000,
  r12: 0xb,
  lr: 0x800020d,
  pc: 0x8001750,
  xpsr: 0xa1000200
}

r0 には 0x4800_1800 という値が入っており、これは read_volatile 関数に渡した無効なアドレスです。

離れた場所での不気味な作用

BSRR は Port E のピンを制御できる唯一のレジスタではありません。ODR レジスタでも ピンの値を変更できます。さらに、ODR では Port E の現在の出力 状態も取得できます。

ODR については次に記載されています:

11.4.6 節 GPIO ポート出力データレジスタ - 239 ページ

このプログラムを見てみましょう。このプログラムの鍵 となるのは fn iprint_odr です。この関数は ODR の現在の 値を ITM コンソールに表示します

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::{entry, iprintln, ITM};

// odr の現在の内容を表示する
fn iprint_odr(itm: &mut ITM) {
    const GPIOE_ODR: u32 = 0x4800_1014;

    unsafe {
        iprintln!(
            &mut itm.stim[0],
            "ODR = 0x{:04x}",
            ptr::read_volatile(GPIOE_ODR as *const u16)
        );
    }
}

#[entry]
fn main() -> ! {
    let mut itm= aux7::init().0;

    unsafe {
        // 魔法のアドレスたち!
        const GPIOE_BSRR: u32 = 0x4800_1018;

        // ODR の初期内容を表示する
        iprint_odr(&mut itm);

        // "North" LED(赤)を点灯する
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);
        iprint_odr(&mut itm);

        // "East" LED(緑)を点灯する
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);
        iprint_odr(&mut itm);

        // "North" LED を消灯する
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));
        iprint_odr(&mut itm);

        // "East" LED を消灯する
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
        iprint_odr(&mut itm);
    }

    loop {}
}

このプログラムを実行すると

$ cargo run
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:22
22      #[entry]

(gdb) continue
Continuing.

itmdump のコンソールには次のように表示されます:

$ # itmdump のコンソール
(..)
ODR = 0x0000
ODR = 0x0200
ODR = 0x0a00
ODR = 0x0800
ODR = 0x0000

副作用です! 実際には変更していない同じアドレスを複数回読み取っているにもかかわらず、 BSRR に書き込まれるたびにその値が変化することがわかります。

型安全な操作

直前まで扱っていたレジスタ ODR には、ドキュメントに次のように書かれていました。

ビット 31:16 は予約済みであり、リセット値のまま保持しなければならない

そのため、このレジスタのそれらのビットには書き込んではいけません。そうしないと、まずいことが起こるかもしれません。

さらに、レジスタごとに読み取り/書き込み権限が異なるという事実もあります。書き込み専用のものもあれば、読み書きできるものもあり、当然ながら読み取り専用のものもあります。

最後に、16進数アドレスを直接扱うのはエラーを招きやすいです。無効なメモリアドレスにアクセスしようとすると例外が発生し、そのせいでプログラムの実行が妨げられることは、すでに見たとおりです。

レジスタを「安全」に操作するための API があれば便利ではないでしょうか? 理想を言えば、その API はここで挙げた 3 点、つまり実際のアドレスを直接いじらないこと、読み取り/書き込み権限を守ること、そしてレジスタの予約済み部分の変更を防ぐことをエンコードしているべきです。

そして実際に、そのようなものがあります! aux7::init() は実際には、GPIOE ペリフェラルのレジスタを型安全に操作するための API を提供する値を返します。

覚えているかもしれませんが、あるペリフェラルに関連付けられたレジスタ群はレジスタブロックと呼ばれ、連続したメモリ領域に配置されています。この型安全 API では、各レジスタブロックは struct としてモデル化され、その各フィールドが 1 つのレジスタを表します。各レジスタフィールドは、たとえば u32 上の異なる newtype であり、その読み取り/書き込み権限に応じて readwritemodify のいずれかのメソッドの組み合わせを公開します。最後に、これらのメソッドは u32 のようなプリミティブ値を受け取るのではなく、ビルダーパターンを使って構築でき、レジスタの予約済み部分の変更を防ぐさらに別の newtype を受け取ります。

この API に慣れるいちばんよい方法は、今動いている例をこれに移植することです。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux7::{entry, iprintln, ITM, RegisterBlock};

#[entry]
fn main() -> ! {
    let gpioe = aux7::init().1;

    // 北の LED を点灯
    gpioe.bsrr.write(|w| w.bs9().set_bit());

    // 東の LED を点灯
    gpioe.bsrr.write(|w| w.bs11().set_bit());

    // 北の LED を消灯
    gpioe.bsrr.write(|w| w.br9().set_bit());

    // 東の LED を消灯
    gpioe.bsrr.write(|w| w.br11().set_bit());

    loop {}
}

最初に気付くことは、マジックアドレスが一切出てこないことです。代わりに、たとえば GPIOE レジスタブロック内の BSRR レジスタを指すのに gpioe.bsrr という、より人間にとって分かりやすい方法を使います。

次に、このクロージャを受け取る write メソッドがあります。恒等クロージャ(|w| w)を使うと、このメソッドはレジスタをその デフォルト(リセット)値、つまりマイクロコントローラの電源投入 / リセット直後の値に設定します。BSRR レジスタではその値は 0x0 です。今回はレジスタに 0 以外の値を書き込みたいので、デフォルト値の一部のビットを設定するために bs9br9 のようなビルダーメソッドを使います。

このプログラムを実行してみましょう! プログラムをデバッグしている 最中に、いくつか面白いことができます。

gpioeGPIOE レジスタブロックへの参照です。print gpioe はレジスタブロックのベースアドレスを返します。

$ cargo run
(..)

Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:9
9           let gpioe = aux7::init().1;

(gdb) next
12          gpioe.bsrr.write(|w| w.bs9().set_bit());

(gdb) print gpioe
$1 = (*mut stm32f3::stm32f303::gpioc::RegisterBlock) 0x48001000

しかし、代わりに print *gpioe を実行すると、レジスタブロックの 全体像 を得られます。つまり、その各レジスタの値が表示されます。

(gdb) print *gpioe
$2 = stm32f3::stm32f303::gpioc::RegisterBlock {
  moder: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_MODER> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 1431633920
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_MODER>
  },
  otyper: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OTYPER> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OTYPER>
  },
  ospeedr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OSPEEDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OSPEEDR>
  },
  pupdr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_PUPDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_PUPDR>
  },
  idr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_IDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 204
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_IDR>
  },
  odr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_ODR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_ODR>
  },
  bsrr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BSRR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BSRR>
  },
  lckr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_LCKR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_LCKR>
  },
  afrl: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRL> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRL>
  },
  afrh: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRH> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRH>
  },
  brr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BRR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BRR>
  }
}

こうした newtype やクロージャは、大きくて肥大化したプログラムを生成しそうに聞こえます。しかし、実際にプログラムを LTO を有効にした release モードでコンパイルすると、write_volatile と 16 進数アドレスを使った「unsafe」版とまったく同じ命令が生成されることが分かります!

cargo objdump を使ってアセンブリコードを release.txt に書き出します:

cargo objdump --bin registers --release -- -d --no-show-raw-insn --print-imm-hex > release.txt

次に、release.txtmain を検索します

0800023e <main>:
 800023e:      	push	{r7, lr}
 8000240:      	mov	r7, sp
 8000242:      	bl	#0x2
 8000246:      	trap

08000248 <registers::__cortex_m_rt_main::h199f1359501d5c71>:
 8000248:      	push	{r7, lr}
 800024a:      	mov	r7, sp
 800024c:      	bl	#0x22
 8000250:      	movw	r0, #0x1018
 8000254:      	mov.w	r1, #0x200
 8000258:      	movt	r0, #0x4800
 800025c:      	str	r1, [r0]
 800025e:      	mov.w	r1, #0x800
 8000262:      	str	r1, [r0]
 8000264:      	mov.w	r1, #0x2000000
 8000268:      	str	r1, [r0]
 800026a:      	mov.w	r1, #0x8000000
 800026e:      	str	r1, [r0]
 8000270:      	b	#-0x4 <registers::__cortex_m_rt_main::h199f1359501d5c71+0x28>

この一連の作業で最もすばらしい点は、GPIOE API を実装するために、誰もコードを1行も書く必要がなかったことです。すべてのコードは、svd2rust ツールを使って、System View Description (SVD) ファイルから自動生成されました。この SVD ファイルは、実際にはマイクロコントローラベンダーが提供する XML ファイルであり、そのマイクロコントローラのレジスタマップが含まれています。このファイルには、レジスタブロックのレイアウト、ベースアドレス、各レジスタの読み取り/書き込み権限、レジスタのレイアウト、レジスタに予約済みビットがあるかどうか、そしてそのほか多くの有用な情報が含まれています。

LEDs、再び

前のセクションでは、初期化済み(設定済み)のペリフェラルを渡しました(aux7::init で初期化しました)。そのため、LED を制御するには BSRR に書き込むだけで十分でした。しかし、ペリフェラルはマイクロコントローラの起動直後には 初期化 されていません。

このセクションでは、レジスタを使ってもう少し楽しんでいきます。今回は初期化を一切行わないので、LED を再び駆動できるように、GPIOE のピンをデジタル出力ピンとして初期化して設定する必要があります。

これはスターターコードです。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux8::entry;

#[entry]
fn main() -> ! {
    let (gpioe, rcc) = aux8::init();

    // TODO initialize GPIOE

    // Turn on all the LEDs in the compass
    gpioe.odr.write(|w| {
        w.odr8().set_bit();
        w.odr9().set_bit();
        w.odr10().set_bit();
        w.odr11().set_bit();
        w.odr12().set_bit();
        w.odr13().set_bit();
        w.odr14().set_bit();
        w.odr15().set_bit()
    });

    aux8::bkpt();

    loop {}
}

スターターコードを実行すると、今回は何も起こらないことがわかります。さらに、GPIOE のレジスタブロックを表示すると、gpioe.odr.write 文が実行された後でも、すべてのレジスタがゼロとして読み出されることがわかります!

$ cargo run
Breakpoint 1, main () at src/08-leds-again/src/main.rs:9
9           let (gpioe, rcc) = aux8::init();

(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x08000f3c in __bkpt ()

(gdb) finish
Run till exit from #0  0x08000f3c in __bkpt ()
main () at src/08-leds-again/src/main.rs:25
25          aux8::bkpt();

(gdb) p/x *gpioe
$1 = stm32f30x::gpioc::RegisterBlock {
  moder: stm32f30x::gpioc::MODER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  otyper: stm32f30x::gpioc::OTYPER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  ospeedr: stm32f30x::gpioc::OSPEEDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  pupdr: stm32f30x::gpioc::PUPDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  idr: stm32f30x::gpioc::IDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  odr: stm32f30x::gpioc::ODR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  bsrr: stm32f30x::gpioc::BSRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  lckr: stm32f30x::gpioc::LCKR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrl: stm32f30x::gpioc::AFRL {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrh: stm32f30x::gpioc::AFRH {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  brr: stm32f30x::gpioc::BRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  }
}

電源

電力を節約するために、ほとんどのペリフェラルは電源オフの状態で起動することがわかります。これが、マイクロコントローラのブート直後の状態です。

Reset and Clock Control(RCC)ペリフェラルは、他のすべてのペリフェラルの電源をオンまたはオフにするために使用できます。

RCC レジスタブロック内のレジスタ一覧は、次で確認できます。

セクション 9.4.14 - RCC register map - 166 ページ - Reference Manual

他のペリフェラルの電源状態を制御するレジスタは次のとおりです。

  • AHBENR
  • APB1ENR
  • APB2ENR

これらのレジスタ内の各ビットは、GPIOE を含む単一のペリフェラルの電源状態を制御します。

このセクションでのあなたの課題は、GPIOE ペリフェラルの電源をオンにすることです。必要なことは次のとおりです。

  • 先ほど挙げた 3 つのレジスタのうち、どれに電源状態を制御するビットがあるのかを突き止める。
  • GPIOE ペリフェラルの電源をオンにするには、そのビットを 01 のどちらに設定しなければならないのかを突き止める。
  • 最後に、GPIOE ペリフェラルをオンにするために、正しいレジスタを変更するようスターターコードを変更する。

成功すれば、gpioe.odr.write 文が ODR レジスタの値を変更できるようになっているはずです。

なお、これだけでは実際に LED を点灯させるには不十分です。

設定

GPIOE 周辺機能を有効化した後でも、まだ設定が必要です。この場合、LED を駆動できるように ピンをデジタル 出力 として設定したいのであり、デフォルトでは、ほとんどの ピンはデジタル 入力 として設定されています。

次の箇所で、GPIOE レジスタブロックに含まれるレジスタの一覧を確認できます:

セクション 11.4.12 - GPIO レジスタ - 243 ページ - リファレンスマニュアル

今回扱うレジスタは MODER です。

このセクションでのあなたの課題は、スターターコードをさらに更新して、正しい GPIOE ピンをデジタル出力として設定することです。やることは次のとおりです:

  • デジタル出力として設定する必要があるピンが どれ なのかを特定すること。(ヒント: User Manual のセクション 6.4「LEDs」 (18 ページ)を確認してください。)
  • MODER レジスタのビットが何をするのかを理解するために、ドキュメントを読むこと。
  • MODER レジスタを変更して、ピンをデジタル出力として設定すること。

成功すれば、プログラムを実行したときに 8 個の LED が点灯します。

解答

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux8::entry;

#[entry]
fn main() -> ! {
    let (gpioe, rcc) = aux8::init();

    // GPIOE ペリフェラルを有効にする
    rcc.ahbenr.write(|w| w.iopeen().set_bit());

    // ピンを出力として設定する
    gpioe.moder.write(|w| {
        w.moder8().output();
        w.moder9().output();
        w.moder10().output();
        w.moder11().output();
        w.moder12().output();
        w.moder13().output();
        w.moder14().output();
        w.moder15().output()
    });

    // コンパス上のすべての LED を点灯する
    gpioe.odr.write(|w| {
        w.odr8().set_bit();
        w.odr9().set_bit();
        w.odr10().set_bit();
        w.odr11().set_bit();
        w.odr12().set_bit();
        w.odr13().set_bit();
        w.odr14().set_bit();
        w.odr15().set_bit()
    });

    aux8::bkpt();

    loop {}
}

クロックとタイマー

このセクションでは、LED ルーレットアプリケーションを再実装します。今回は Led 抽象化を返しますが、その代わりに Delay 抽象化を取り上げます :-)

以下がスターターコードです。delay 関数は未実装なので、このプログラムを実行すると LED は非常に速く点滅し、常に点灯しているように見えます。

#![no_main]
#![no_std]

use aux9::{entry, switch_hal::OutputSwitch, tim6};

#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    // TODO implement this
}

#[entry]
fn main() -> ! {
    let (leds, rcc, tim6) = aux9::init();
    let mut leds = leds.into_array();

    // TODO initialize TIM6

    let ms = 50;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().unwrap();
            delay(tim6, ms);
            leds[curr].off().unwrap();
            delay(tim6, ms);
        }
    }
}

for ループによる遅延

最初の課題は、ペリフェラルを一切使わずに delay 関数を実装することです。明らかな解決策は、これを for ループによる遅延として実装することです。

#![allow(unused)]
fn main() {
#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    for _ in 0..1_000 {}
}
}

もちろん、上の実装は誤っています。ms の値が何であっても、常に同じ遅延が生成されるからです。

このセクションでは、次のことを行います。

  • delay 関数を修正し、入力 ms に比例した遅延を生成するようにしてください。
  • delay 関数を調整し、LED ルーレットが 4 秒間に約 5 周する速度(周期 800 ミリ秒)で回転するようにしてください。
  • マイクロコントローラ内のプロセッサは 72 MHz でクロック駆動されており、ほとんどの命令を 1「tick」、つまりクロックの 1 サイクルで実行します。1 秒の遅延を生成するには、delay 関数は何回の(for)ループを実行する必要があると 思いますか
  • delay(1000) は実際には何回 for ループを実行しますか?
  • プログラムを release モードでコンパイルして実行すると、何が起こりますか?

NOP

前のセクションでプログラムをリリースモードでコンパイルし、実際に逆アセンブル結果を見たのであれば、 delay 関数が最適化によって取り除かれ、main の内部からは一度も呼び出されていないことに 気づいたはずです。

LLVM は、その関数は何も価値のあることをしていないと判断し、単に削除しました。

LLVM が for ループによる遅延を最適化しないようにする方法があります。volatile なアセンブリ命令を 追加することです。どんな命令でも構いませんが、この場合は副作用がないため、NOP(No OPeration)は 特に適した選択です。

for ループによる遅延は次のようになります。

#![allow(unused)]
fn main() {
#[inline(never)]
fn delay(_tim6: &tim6::RegisterBlock, ms: u16) {
    const K: u16 = 3; // この値は調整が必要です
    for _ in 0..(K * ms) {
        aux9::nop()
    }
}
}

そして今度は、プログラムをリリースモードでコンパイルしても、delay は LLVM によって 取り除かれません。

$ cargo objdump --bin clocks-and-timers --release -- -d --no-show-raw-insn
clocks-and-timers:      file format ELF32-arm-little

Disassembly of section .text:
clocks_and_timers::delay::h711ce9bd68a6328f:
 8000188:       push    {r4, r5, r7, lr}
 800018a:       movs    r4, #0
 800018c:       adds    r4, #1
 800018e:       uxth    r5, r4
 8000190:       bl      #4666
 8000194:       cmp     r5, #150
 8000196:       blo     #-14 <clocks_and_timers::delay::h711ce9bd68a6328f+0x4>
 8000198:       pop     {r4, r5, r7, pc}

では、これを試してみましょう。プログラムをデバッグモードでコンパイルして実行し、その後で プログラムをリリースモードでコンパイルして実行してください。両者の違いは何でしょうか。 その違いの主な原因は何だと思いますか。両者を同等に、あるいは少なくとも再びもっと似たものにする 方法を思いつきますか。

ワンショットタイマー

ここまでで、遅延を実装する方法として for ループによる遅延がよくないやり方であることを、あなたに納得してもらえていたらと思います。

ここからは、ハードウェアタイマー を使って遅延を実装します。(ハードウェア)タイマーの基本的な機能は、 … 時間を正確に追跡することです。タイマーもまたマイクロコントローラで利用できる別のペリフェラルであり、 そのためレジスタを使って制御できます。

私たちが使っているマイクロコントローラには、異なる種類のタイマー (基本タイマー、汎用タイマー、アドバンストタイマー)が複数(実際には 10 個以上)用意されています。 タイマーによって 分解能(ビット数)が異なり、単に時間を追跡する以上の用途に使えるものもあります。

ここでは 基本 タイマーの 1 つである TIM6 を使用します。これは、私たちの マイクロコントローラで利用できる中でも最も単純なタイマーの 1 つです。基本タイマーに関するドキュメントは 次のセクションにあります。

セクション 22 タイマー - 670 ページ - リファレンスマニュアル

そのレジスタは次に記載されています。

セクション 22.4.9 TIM6/TIM7 レジスタマップ - 682 ページ - リファレンスマニュアル

このセクションで使用するレジスタは次のとおりです。

  • SR、ステータスレジスタ
  • EGR、イベント生成レジスタ
  • CNT、カウンタレジスタ
  • PSC、プリスケーラレジスタ
  • ARR、オートリロードレジスタ

タイマーは ワンショット タイマーとして使用します。これは、ある意味では目覚まし時計のように動作します。 一定時間後にタイマーが発火するよう設定し、その後タイマーが発火するまで待機します。ドキュメントでは この動作モードを ワンパルスモード と呼んでいます。

基本タイマーをワンパルスモードに設定したときの動作は、次のようになります。

  • カウンタはユーザーによって有効化されます(CR1.CEN = 1)。
  • CNT レジスタは値をゼロにリセットし、各ティックごとに値が 1 ずつ増加します。
  • CNT レジスタが ARR レジスタの値に達すると、カウンタはハードウェアによって無効化され (CR1.CEN = 0)、アップデートイベント が発生します(SR.UIF = 1)。

TIM6 は APB1 クロックで駆動されます。この周波数は、必ずしもプロセッサの周波数と一致するとは限りません。 つまり、APB1 クロックはより速く動作している場合もあれば、より遅く動作している場合もあります。しかし、 デフォルトでは APB1 とプロセッサはどちらも 8 MHz でクロックされています。

ワンパルスモードの機能説明で言及したティックは、APB1 クロックの 1 ティックと 同じでは ありませんCNT レジスタは 1 秒あたり apb1 / (psc + 1) 回の周波数で増加します。ここで、apb1 は APB1 クロックの周波数、psc は プリスケーラレジスタ PSC の値です。

初期化

他のすべてのペリフェラルと同様に、このタイマーも使用する前に初期化する必要があります。そして前のセクションと同様に、初期化には 2 つの手順が含まれます。まずタイマーの電源を入れ、その後で設定を行います。

タイマーの電源を入れるのは簡単です。TIM6EN ビットを 1 に設定するだけです。このビットは RCC レジスタブロックの APB1ENR レジスタ内にあります。

#![allow(unused)]
fn main() {
    // TIM6 タイマーの電源を入れる
    rcc.apb1enr.modify(|_, w| w.tim6en().set_bit());
}

設定の部分は少しだけ複雑です。

まず、タイマーがワンパルスモードで動作するように設定する必要があります。

#![allow(unused)]
fn main() {
    // OPM ワンパルスモードを選択
    // CEN ひとまずカウンターは無効のままにしておく
    tim6.cr1.write(|w| w.opm().set_bit().cen().clear_bit());
}

次に、delay 関数は引数としてミリ秒単位の値を取るため、CNT カウンターを 1 KHz の周波数で動作させたいと考えます。1 KHz であれば周期は 1 ミリ秒になります。そのためにはプリスケーラを設定する必要があります。

#![allow(unused)]
fn main() {
    // カウンターが 1 KHz で動作するようにプリスケーラを設定する
    tim6.psc.write(|w| w.psc().bits(psc));
}

プリスケーラ psc の値は、自分で求めてみてください。カウンターの周波数は apb1 / (psc + 1) であり、apb1 は 8 MHz であることを思い出してください。

ビジーウェイティング

タイマーはこれで正しく初期化されているはずです。あとは、このタイマーを使って delay 関数 を実装するだけです。

最初に行う必要があるのは、自動再ロードレジスタ (ARR) を設定して、タイマーが ms ミリ秒後に発生するようにすることです。カウンターは 1 KHz で動作するため、自動再ロード値は ms と同じになります。

#![allow(unused)]
fn main() {
    // タイマーが `ms` ティック後に発生するように設定する
    // 1 ティック = 1 ms
    tim6.arr.write(|w| w.arr().bits(ms));
}

次に、カウンターを有効にする必要があります。すると、すぐにカウントを開始します。

#![allow(unused)]
fn main() {
    // CEN: カウンターを有効にする
    tim6.cr1.modify(|_, w| w.cen().set_bit());
}

次に、カウンターが自動再ロードレジスタの値 ms に達するまで待つ必要があります。そうなれば、 ms ミリ秒が経過したことがわかります。この条件は 更新イベント と呼ばれ、 ステータスレジスタ (SR) の UIF ビットで示されます。

#![allow(unused)]
fn main() {
    // アラームが発生するまで待機する(更新イベントが発生するまで)
    while !tim6.sr.read().uif().bit_is_set() {}
}

このように、何らかの条件が満たされるまでただ待つ、今回でいえば UIF1 になるまで待つ パターンは ビジーウェイティング と呼ばれ、このテキストでもあと何度か登場します :-).

最後に、この UIF ビットをクリア(0 に設定)しなければなりません。そうしないと、次に delay 関数に入ったとき、更新イベントがすでに発生したものとみなして、ビジーウェイティングの部分を スキップしてしまいます。

#![allow(unused)]
fn main() {
    // 更新イベントフラグをクリアする
    tim6.sr.modify(|_, w| w.uif().clear_bit());
}

では、これらをすべてまとめて、期待どおりに動作するか確認してください。

すべてをまとめる

#![no_main]
#![no_std]

use aux9::{entry, switch_hal::OutputSwitch, tim6};

#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    // タイマーが `ms` ティック後に発火するように設定する
    // 1 ティック = 1 ms
    tim6.arr.write(|w| w.arr().bits(ms));

    // CEN: カウンタを有効にする
    tim6.cr1.modify(|_, w| w.cen().set_bit());

    // アラームが発生するまで待機する(更新イベントが発生するまで)
    while !tim6.sr.read().uif().bit_is_set() {}

    // 更新イベントフラグをクリアする
    tim6.sr.modify(|_, w| w.uif().clear_bit());
}

#[entry]
fn main() -> ! {
    let (leds, rcc, tim6) = aux9::init();
    let mut leds = leds.into_array();

    // TIM6 タイマーの電源をオンにする
    rcc.apb1enr.modify(|_, w| w.tim6en().set_bit());

    // OPM ワンパルスモードを選択する
    // CEN 今のところカウンタは無効のままにする
    tim6.cr1.write(|w| w.opm().set_bit().cen().clear_bit());

    // カウンタが 1 KHz で動作するようにプリスケーラを設定する
    // APB1_CLOCK = 8 MHz
    // PSC = 7999
    // 8 MHz / (7999 + 1) = 1 KHz
    // カウンタ(CNT)は 1 ミリ秒ごとに増加する
    tim6.psc.write(|w| w.psc().bits(7_999));

    let ms = 50;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().unwrap();
            delay(tim6, ms);
            leds[curr].off().unwrap();
            delay(tim6, ms);
        }
    }
}

シリアル通信

これが今回使用するものです。あなたのコンピューターにも付いているとよいのですが!

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

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

このプロトコルを使って、マイクロコントローラーとあなたのコンピューターの間でデータをやり取りします。これまで使用してきた ITM プロトコルとは異なり、シリアル通信プロトコルでは、コンピューターからマイクロコントローラーへデータを送ることができます。

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

このプロトコルはフレームで動作します。各フレームは 1 つの スタート ビット、5 ~ 9 ビットのペイロード(データ)、そして 1 ~ 2 個の ストップビット を持ちます。このプロトコルの速度は ボーレート と呼ばれ、1 秒あたりのビット数(bps)で表されます。一般的なボーレートは 9600、19200、38400、57600、115200 bps です。

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

今日のコンピューターはシリアル通信プロトコルをサポートしていません。そのため、コンピューターをマイクロコントローラーに直接接続することはできません。そこで登場するのがシリアルモジュールです。このモジュールは両者の間に入り、マイクロコントローラーにはシリアルインターフェイスを、コンピューターには USB インターフェイスを提供します。マイクロコントローラーからは、あなたのコンピューターは別のシリアルデバイスとして見え、コンピューターからは、マイクロコントローラーは仮想シリアルデバイスとして見えます。

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

*nix ツール

Discovery ボードの新しいリビジョン

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

$ # Linux
$ dmesg | tail | grep -i tty
[13560.675310] cdc_acm 1-1.1:1.2: ttyACM0: USB ACM device

これが USB <-> シリアルデバイスです。Linux では、tty*(通常は ttyACM* または ttyUSB*)という名前になります。

デバイスが現れない場合は、おそらく古いリビジョンのボードを使っています。 古いリビジョン向けの手順が書かれている次のセクションを確認してください。 新しいリビジョンを使っている場合は、次のセクションを飛ばして 「minicom」セクションに進んでください。

Discovery ボードの古いリビジョン / 外付けシリアルモジュール

シリアルモジュールをコンピューターに接続し、OS がそれにどの名前を割り当てたか確認しましょう。

mac では、USB デバイスは /dev/cu.usbserial-* のような名前になります。これを dmesg で見つけることはできないので、代わりに ls -l /dev | grep cu.usb を使い、以下の コマンドを適宜読み替えてください!

$ dmesg | grep -i tty
(..)
[  +0.000155] usb 3-2: FTDI USB Serial Device converter now attached to ttyUSB0

では、この ttyUSB0 とは何でしょうか? もちろんファイルです! *nix ではすべてがファイルです:

$ ls -l /dev/ttyUSB0
crw-rw-rw- 1 root uucp 188, 0 Oct 27 00:00 /dev/ttyUSB0

上記のパーミッションが crw-rw---- の場合、udev ルールが正しく設定されていません udev ルール を参照してください

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

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

シリアルモジュールの TX(赤)LED が、一度だけすばやく点滅するはずです!

すべてのリビジョン: minicom

シリアルデバイスを echo で扱うのは、決して使いやすい方法ではありません。そこで、 キーボードを使ってシリアルデバイスとやり取りするために 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) が表示されます。

これでキーボードを使ってデータを送信できます! 何か入力してみてください。なお、 TUI はあなたが入力した内容を エコーバックしません が、外付け モジュールを使用している場合は、キーを押すたびにモジュール上のどこかの LED が点滅するのが 見える かもしれません

minicom コマンド

minicom はキーボードショートカットでコマンドを提供します。Linux では、ショートカットは Ctrl+A で始まります。mac では、ショートカットは Meta キーで始まります。便利なコマンドをいくつか以下に示します。

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

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

Windows 向けツール

まず、Discovery ボードを取り外します。

Discovery ボードまたはシリアルモジュールを接続する前に、ターミナルで次のコマンドを実行します:

$ mode

すると、コンピューターに接続されているデバイスの一覧が表示されます。名前が COM で始まるものは シリアルデバイスです。これがこれから扱う種類のデバイスです。シリアルモジュールを接続するに、 mode が出力するすべての COM ポートを控えておいてください。

次に、Discovery ボードを接続し、mode コマンドをもう一度実行します。一覧に新しい COM ポートが現れた場合は、新しいリビジョンの Discovery を使っています。 その COM ポートが Discovery のシリアル機能に割り当てられたポートです。 次の段落は飛ばしてかまいません。

新しい COM ポートが現れなかった場合は、おそらく古いリビジョンの Discovery を使っています。次にシリアルモジュールを接続してください。新しい COM ポートが現れるはずです。 それがシリアルモジュールの 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 ボタンをクリックします。すると、コンソールが表示されます:

このコンソールで入力すると、シリアルモジュールの TX(赤)LED が点滅するはずです。キーを押すたびに LED が 1 回点滅します。入力した内容はコンソールにエコーバックされないため、画面には 何も表示されません。

ループバック

データの送信はテストしました。次は受信をテストする番です。ただし、こちらにデータを送ってくれる別のデバイスはありません……本当にそうでしょうか?

そこで登場するのが、ループバックです。

自分自身にデータを送れます!本番環境ではあまり役に立ちませんが、デバッグには非常に役立ちます。

古いボードリビジョン / 外付けシリアルモジュール

上の図のように、オス-オスのジャンパーワイヤを使って、シリアルモジュールの TXO ピンと RXI ピンを接続してください。

では、minicom/PuTTY に何かテキストを入力して観察してみてください。何が起こるでしょうか?

3 つのことが確認できるはずです:

  • これまでと同様に、キーを押すたびに TX(赤)LED が点滅します。
  • しかし今度は、キーを押すたびに RX(緑)LED も点滅します!これは、シリアルモジュールが何らかのデータ、つまりたった今自分で送信したデータを受信していることを示しています。
  • 最後に、minicom/PuTTY のコンソールでは、入力した内容がそのままコンソールにエコーバックされるはずです。

新しいボードリビジョン

ボードの新しいリビジョンをお持ちの場合は、SWO ピンで行ったのと同じように、メス-メスのジャンパーワイヤで PC4 ピンと PC5 ピンを短絡してループバックを設定できます。

これで、自分自身にデータを送れるようになるはずです。

では、minicom/PuTTY に何かテキストを入力して観察してみてください。

: 既存のファームウェアがシリアルピン(PC4 と PC5)に何らかの妙なことをしている可能性を排除するため、minicom/PuTTY にテキストを入力している間はリセットボタンを押したままにすることをおすすめします。

すべて正常に動作していれば、入力した内容が minicom/PuTTY コンソールにエコーバックされるはずです。


minicom/PuTTY を使ったシリアルポート経由でのデータの送受信に慣れてきたところで、次はマイクロコントローラとコンピュータを通信させてみましょう!

USART

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

この章全体を通して、シリアル通信を使ってマイクロコントローラーとコンピューターの間で情報をやり取りします。しかしその前に、まずすべてを配線する必要があります。

前にも述べたように、このプロトコルでは 2 本のデータ線を使用します: TX と RX です。TX は transmitter を表し、RX は receiver を表します。ただし、transmitter と receiver は相対的な用語です。どちらの線が transmitter でどちらの線が receiver であるかは、通信のどちら側からその線を見ているかによって決まります。

新しいボードリビジョン

より新しいリビジョンのボードを使っていて、オンボードの USB <-> Serial 機能を使用している場合、auxiliary クレートはピン PC4 を TX 線として、ピン PC5 を RX 線として設定します。

前の節で ループバック機能 をテストするために PC4 ピンと PC4 ピンを接続していた場合は、 そのワイヤーを必ず取り外してください。そうしないと、この後のシリアル通信は何も表示されずに失敗します。

必要な配線はすべてすでにボード上で接続されているため、自分で何かを配線する必要はありません。 そのまま 次の節 に進んでください。

古いボードリビジョン / 外部シリアルモジュール

外部の USB <-> Serial モジュールを使用している場合は、Cargo.toml 内の aux11 クレート依存関係で adapter フィーチャーを有効にする必要があります

[dependencies.aux11]
path = "auxiliary"
# 外部シリアルアダプターを使用する場合はこれを有効にする
features = ["adapter"] # <- これをアンコメント

マイクロコントローラーの TX 線としてピン PA9 を、RX 線として PA10 を使用します。言い換えると、 ピン PA9 はそのワイヤーにデータを出力し、ピン PA10 はそのワイヤー上のデータを受信します。

TX ピンと RX ピンには別のピンの組み合わせを使うこともできました。Data Sheet の 44 ページには、 使用可能だった他のすべてのピンが一覧になった表があります。

シリアルモジュールにも TX ピンと RX ピンがあります。これらのピンは 交差 させて接続する必要があります。つまり、 マイクロコントローラーの TX ピンをシリアルモジュールの RX ピンに、マイクロコントローラーの RX ピンをシリアルモジュールの TX ピンに接続します。以下の配線図は、必要な接続をすべて示しています。

マイクロコントローラーとシリアルモジュールを接続するための推奨手順は次のとおりです。

  • OpenOCD と itmdump を終了します
  • F3 とシリアルモジュールから USB ケーブルを取り外します。
  • メス-オス(F/M)ワイヤーを使って、F3 の GND ピンの 1 つをシリアルモジュールの GND ピンに接続します。 できれば黒いものを使ってください。
  • F/M ワイヤーを使って、F3 の背面にある PA9 ピンをシリアルモジュールの RXI ピンに接続します。
  • F/M ワイヤーを使って、F3 の背面にある PA10 ピンをシリアルモジュールの TXO ピンに接続します。
  • 次に USB ケーブルを F3 に接続します。
  • 最後に USB ケーブルを Serial モジュールに接続します。
  • OpenOCD と itmdump を再起動します

これで全部配線できました! 続いて、データを双方向に送受信してみましょう。

1バイトを送信する

最初のタスクは、シリアル接続を介してマイクロコントローラーからコンピューターへ 1 バイトを 送信することです。

今回は、すでに初期化済みの USART ペリフェラルをこちらで用意します。あなたが扱う必要があるのは、 データの送受信を担うレジスタだけです。

11-usart ディレクトリに移動して、その中のスターターコードを実行しましょう。minicom/PuTTY を 開いておいてください。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // Send a single character
    usart1
        .tdr
        .write(|w| w.tdr().bits(u16::from(b'X')) );

    loop {}
}

このプログラムは TDR レジスタに書き込みます。これにより、USART ペリフェラルはシリアル インターフェースを通じて 1 バイトの情報を送信します。

受信側であるコンピューターでは、minicom/PuTTY のターミナルに文字 X が表示されるはずです。

文字列を送信する

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

マイクロコントローラーからコンピューターへ、文字列 "The quick brown fox jumps over the lazy dog." を送信してほしいです。

今度は、あなたがプログラムを書いてください。

デバッガーの中で、文を1つずつ実行しながらプログラムを実行してください。何が見えますか?

次に、プログラムをもう一度実行しますが、今度は continue コマンドを使って 一気に 実行してください。今回は何が起こりますか?

最後に、プログラムを release モードでビルドし、再び一気に実行してください。今回は何が起こりますか?

オーバーラン

プログラムを次のように書いた場合:

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        usart1
            .tdr
            .write(|w| w.tdr().bits(u16::from(*byte)));
    }

    loop {}
}

おそらく、デバッグモードでコンパイルしたプログラムを実行すると、コンピューター上では次のようなものが表示されたはずです。

$ # minicom の端末
(..)
The uic brwn oxjums oer helaz do.

そして、リリースモードでコンパイルした場合は、おそらく次のようなものしか得られなかったはずです:

$ # minicom の端末
(..)
T

何が問題だったのでしょうか?

ご存じのとおり、バイトを配線越しに送信するには比較的長い時間がかかります。計算はすでに済ませてあるので、 ここでは以前の説明を引用します:

1 スタートビット、8 データビット、1 ストップビット、ボーレート 115200 bps という一般的な構成では、理論上は 1 秒あたり 11,520 フレームを送信できます。各フレームは 1 バイトのデータを運ぶので、 その結果、データレートは 11.52 KB/s になります

このパングラムの長さは 45 バイトです。つまり、文字列を送信するには少なくとも 3,900 マイクロ秒 (45 bytes / (11,520 bytes/s) = 3,906 us)かかることになります。プロセッサは 8 MHz で動作しており、 1 命令の実行には 125 ナノ秒しかかからないため、for ループは 3,900 マイクロ秒より短い時間で完了する可能性が高いです。

実際に、for ループの実行にどれくらい時間がかかるかを計測できます。aux11::init()MonoTimer(単調タイマー)値を返し、std::time のものに似た Instant API を公開しています。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, mono_timer, mut itm) = aux11::init();

    let instant = mono_timer.now();
    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        usart1.tdr.write(|w| w.tdr().bits(u16::from(*byte)));
    }
    let elapsed = instant.elapsed(); // in ticks

    iprintln!(
        &mut itm.stim[0],
        "`for` loop took {} ticks ({} us)",
        elapsed,
        elapsed as f32 / mono_timer.frequency().0 as f32 * 1e6
    );

    loop {}
}

デバッグモードでは、私の環境では次のようになります:

$ # itmdump 端末
(..)
`for` loop took 22415 ticks (2801.875 us)

これは 3,900 マイクロ秒未満ですが、それほど大きく離れているわけではありません。そのため、失われる情報は 数バイトだけで済みます。

要するに、プロセッサはハードウェアが実際に処理できる速度よりも速いレートでバイトを送信しようとしており、 その結果データ損失が発生しています。この状態はバッファ オーバーラン と呼ばれます。

これをどう回避すればよいでしょうか? ステータスレジスタ(ISR)には TXE というフラグがあり、データ損失を起こさずに TDR レジスタへ書き込んでも「安全」かどうかを示します。

これを使ってプロセッサを減速させましょう。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, mono_timer, mut itm) = aux11::init();

    let instant = mono_timer.now();
    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        // wait until it's safe to write to TDR
        while usart1.isr.read().txe().bit_is_clear() {} // <- NEW!

        usart1
            .tdr
            .write(|w| w.tdr().bits(u16::from(*byte)));
    }
    let elapsed = instant.elapsed(); // in ticks

    iprintln!(
        &mut itm.stim[0],
        "`for` loop took {} ticks ({} us)",
        elapsed,
        elapsed as f32 / mono_timer.frequency().0 as f32 * 1e6
    );

    loop {}
}

今回は、デバッグモードでもリリースモードでも、プログラムを実行すると受信側では完全な文字列が 得られるはずです。

$ # minicom/PuTTY のコンソール
(..)
The quick brown fox jumps over the lazy dog.

また、for ループのタイミングも理論値である 3,900 マイクロ秒に近くなるはずです。以下の タイミングはデバッグ版のものです。

$ # itmdump 端末
(..)
`for` loop took 30499 ticks (3812.375 us)

uprintln!

次の演習では、uprint! マクロファミリーを実装します。目標は、この コード行を動作させることです。

#![allow(unused)]
fn main() {
    uprintln!(serial, "The answer is {}", 40 + 2);
}

これによって、文字列 "The answer is 42" がシリアルインターフェース経由で送信されなければなりません。

どう進めればよいのでしょうか。println!std における実装を見ると参考になります。

#![allow(unused)]
fn main() {
// src/libstd/macros.rs
macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
}

今のところは単純に見えます。組み込みの format_args! マクロが必要です(これはコンパイラに実装されているため、 実際に何をしているのかを見ることはできません)。このマクロはまったく同じ方法で使う必要があります。この _print 関数は何をしているのでしょうか。

#![allow(unused)]
fn main() {
// src/libstd/io/stdio.rs
pub fn _print(args: fmt::Arguments) {
    let result = match LOCAL_STDOUT.state() {
        LocalKeyState::Uninitialized |
        LocalKeyState::Destroyed => stdout().write_fmt(args),
        LocalKeyState::Valid => {
            LOCAL_STDOUT.with(|s| {
                if s.borrow_state() == BorrowState::Unused {
                    if let Some(w) = s.borrow_mut().as_mut() {
                        return w.write_fmt(args);
                    }
                }
                stdout().write_fmt(args)
            })
        }
    };
    if let Err(e) = result {
        panic!("failed printing to stdout: {}", e);
    }
}
}

これは複雑に 見えます が、私たちが関心を持つ唯一の部分は w.write_fmt(args)stdout().write_fmt(args) だけです。最終的に print! が行うのは、format_args! の出力を 引数として fmt::Write::write_fmt メソッドを呼び出すことです。

幸いなことに、fmt::Write::write_fmt メソッドも実装する必要はありません。これはデフォルト メソッドだからです。実装しなければならないのは fmt::Write::write_str メソッドだけです。

それではやってみましょう。

これがマクロ側の様子です。あとは、write_str メソッドの実装を あなたが用意するだけです。

先ほど見たように、Writestd::fmt にあります。std にはアクセスできませんが、Writecore::fmt でも利用できます。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use core::fmt::{self, Write};

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln, usart1};

macro_rules! uprint {
    ($serial:expr, $($arg:tt)*) => {
        $serial.write_fmt(format_args!($($arg)*)).ok()
    };
}

macro_rules! uprintln {
    ($serial:expr, $fmt:expr) => {
        uprint!($serial, concat!($fmt, "\n"))
    };
    ($serial:expr, $fmt:expr, $($arg:tt)*) => {
        uprint!($serial, concat!($fmt, "\n"), $($arg)*)
    };
}

struct SerialPort {
    usart1: &'static mut usart1::RegisterBlock,
}

impl fmt::Write for SerialPort {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        // TODO implement this
        // hint: this will look very similar to the previous program
        Ok(())
    }
}

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    let mut serial = SerialPort { usart1 };

    uprintln!(serial, "The answer is {}", 40 + 2);

    loop {}
}

1 バイトを受信する

これまでは、マイクロコントローラーからコンピュータへデータを送信してきました。今度は逆を試してみましょう。つまり、コンピュータからデータを受信します。

RDR レジスタには、RX ラインから入ってきたデータが格納されます。このレジスタを読み取れば、チャネルの反対側が送信したデータを取得できます。では、(新しい)データを受信したことはどのように知ればよいのでしょうか。ステータスレジスタ ISR には、そのためのビットである RXNE があります。このフラグをビジーウェイトすればよいだけです。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    loop {
        // Wait until there's data available
        while usart1.isr.read().rxne().bit_is_clear() {}

        // Retrieve the data
        let _byte = usart1.rdr.read().rdr().bits() as u8;

        aux11::bkpt();
    }
}

このプログラムを試してみましょう! continue を使ってそのまま実行を続け、minicom/PuTTY のコンソールで 1 文字だけ入力してください。何が起こるでしょうか。_byte 変数の内容はどうなっているでしょうか。

(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x8003d48 in __bkpt ()

(gdb) finish
Run till exit from #0  0x8003d48 in __bkpt ()
usart::main () at src/11-usart/src/main.rs:19
19              aux11::bkpt();

(gdb) p/c _byte
$1 = 97 'a'

エコーサーバー

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

これは簡単に実装できるはずです。(ヒント: 1 バイトずつ行ってください)

文字列を逆順にする

それでは次に、クライアントが送信したテキストを逆順にして応答するようにして、サーバーをもう少し面白くしてみましょう。サーバーは、 クライアントが ENTER キーを押すたびに応答を返します。各サーバー応答は新しい行に 出力されます。

今回はバッファが必要です。heapless::Vec を使えます。スターターコードは次のとおりです。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};
use heapless::Vec;

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // A buffer with 32 bytes of capacity
    let mut buffer: Vec<u8, 32> = Vec::new();

    loop {
        buffer.clear();

        // TODO Receive a user request. Each user request ends with ENTER
        // NOTE `buffer.push` returns a `Result`. Handle the error by responding
        // with an error message.

        // TODO Send back the reversed string
    }
}

私の解答

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};
use heapless::Vec;

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // A buffer with 32 bytes of capacity
    let mut buffer: Vec<u8, 32> = Vec::new();

    loop {
        buffer.clear();

        loop {
            while usart1.isr.read().rxne().bit_is_clear() {}
            let byte = usart1.rdr.read().rdr().bits() as u8;

            if buffer.push(byte).is_err() {
                // buffer full
                for byte in b"error: buffer full\n\r" {
                    while usart1.isr.read().txe().bit_is_clear() {}
                    usart1
                        .tdr
                        .write(|w| w.tdr().bits(u16::from(*byte)));
                }

                break;
            }

            // Carriage return
            if byte == 13 {
                // Respond
                for byte in buffer.iter().rev().chain(&[b'\n', b'\r']) {
                    while usart1.isr.read().txe().bit_is_clear() {}
                    usart1
                        .tdr
                        .write(|w| w.tdr().bits(u16::from(*byte)));
                }

                break;
            }
        }
    }
}

Bluetooth のセットアップ

そろそろ配線をいくつか減らしましょう。シリアル通信は USB プロトコル上でエミュレートできるだけでなく、Bluetooth プロトコル上でもエミュレートできます。この Bluetooth 上でのシリアル通信 プロトコルは RFCOMM として知られています。

Bluetooth モジュールをマイクロコントローラーと一緒に使う前に、まずは minicom/PuTTY を使ってこれとやり取りしてみましょう。

最初に必要なのは、Bluetooth モジュールの電源を入れることです。以下の接続を使って、 F3 の電源の一部をそれと共有する必要があります。

これを配線するための推奨手順は次のとおりです。

  • OpenOCD と itmdump を終了する
  • F3 とシリアルモジュールから USB ケーブルを外す。
  • メス-メス(F/F)ワイヤーを使って、F3 の GND ピンを Bluetooth の GND ピンに接続する。できれば 黒いものを使う。
  • F/F ワイヤーを使って、F3 の 5V ピンを Bluetooth の VCC ピンに接続する。できれば赤いものを使う。
  • その後、USB ケーブルを F3 に接続し直す。
  • OpenOCD と itmdump を再起動する

Bluetooth モジュール上の 2 つの LED(青と赤)は、F3 ボードの電源を入れた直後に 点滅し始めるはずです。

次に行うのは、コンピューターと Bluetooth モジュールをペアリングすることです。AFAIK、Windows と mac のユーザーは、 OS のデフォルトの Bluetooth マネージャーを使うだけでペアリングできます。Bluetooth モジュールのデフォルトの PIN は 1234 です。

Linux ユーザーは、これらの手順の一部に従う必要があります。

Linux

グラフィカルな Bluetooth マネージャーがある場合は、それを使ってコンピューターを Bluetooth モジュールとペアリングし、これらの手順の大半を省略できます。ただし、おそらく this step は引き続き必要です。

電源をオンにする

まず、コンピューターの Bluetooth トランシーバーが OFF になっている可能性があります。hciconfig で状態を確認し、必要に応じて ON にしてください。

$ hciconfig
hci0:   Type: Primary  Bus: USB
        BD Address: 68:17:29:XX:XX:XX  ACL MTU: 310:10  SCO MTU: 64:8
        DOWN  <--
        RX bytes:580 acl:0 sco:0 events:31 errors:0
        TX bytes:368 acl:0 sco:0 commands:30 errors:0

$ sudo hciconfig hci0 up

$ hciconfig
hci0:   Type: Primary  Bus: USB
        BD Address: 68:17:29:XX:XX:XX  ACL MTU: 310:10  SCO MTU: 64:8
        UP RUNNING  <--
        RX bytes:1190 acl:0 sco:0 events:67 errors:0
        TX bytes:1072 acl:0 sco:0 commands:66 errors:0

次に、BlueZ(Bluetooth)デーモンを起動する必要があります。

  • systemd ベースの Linux ディストリビューションでは、次を使用します。
$ sudo systemctl start bluetooth
  • Ubuntu(または upstart ベースの Linux ディストリビューション)では、次を使用します。
$ sudo /etc/init.d/bluetooth start

また、rfkill list の結果によっては、Bluetooth のブロックを解除する必要がある場合もあります。

$ rfkill list
9: hci0: Bluetooth
        Soft blocked: yes # <--
        Hard blocked: no

$ sudo rfkill unblock bluetooth

$ rfkill list
9: hci0: Bluetooth
        Soft blocked: no  # <--
        Hard blocked: no

スキャン

$ hcitool scan
Scanning ...
        20:16:05:XX:XX:XX       Ferris
$ #                             ^^^^^^

ペアリング

$ bluetoothctl
[bluetooth]# scan on
[bluetooth]# agent on
[bluetooth]# pair 20:16:05:XX:XX:XX
Attempting to pair with 20:16:05:XX:XX:XX
[CHG] Device 20:16:05:XX:XX:XX Connected: yes
Request PIN code
[agent] Enter PIN code: 1234

rfcomm device

Bluetooth モジュール用のデバイスファイルを /dev に作成します。これにより、/dev/ttyUSB0 を使ったときと同じように使用できるようになります。

$ sudo rfcomm bind 0 20:16:05:XX:XX:XX

bind の引数として 0 を使用したため、/dev/rfcomm0 が Bluetooth モジュールに割り当てられるデバイスファイルになります。

デバイスファイルは、次のコマンドでいつでも解放(削除)できます。

$ # 今はこのコマンドを実際には実行しないでください!
$ sudo rfcomm release 0

ループバック、再び

コンピュータを Bluetooth モジュールとペアリングすると、OS がデバイスファイル / COM ポートを作成しているはずです。Linux では /dev/rfcomm*、mac では /dev/cu.*、Windows では新しい COM ポートになっているはずです。

これで、minicom/PuTTY を使って Bluetooth モジュールをテストできます。このモジュールには、シリアルモジュールにあったような送信および受信イベント用の LED インジケーターがないため、ループバック接続を使ってモジュールをテストします。

F/F ワイヤを使って、モジュールの TXD ピンを RXD ピンに接続するだけです。

次に、minicom/PuTTY を使ってデバイスに接続します。

$ minicom -D /dev/rfcomm0

接続すると、Bluetooth モジュールの点滅パターンは、長い休止の後にすばやく 2 回点滅するものに変わるはずです。

minicom/PuTTY のターミナル内で入力すると、入力した内容がそのままエコーバックされるはずです。

AT コマンド

AT コマンド

Bluetooth モジュールと F3 は、同じボーレートで通信できるように設定する必要があります。チュートリアルのコードでは、UART1 シリアルデバイスを 115200 のボーレートで初期化します。HC-05 Bluetooth モジュールは、デフォルトでは 9600 のボーレートに設定されています。

Bluetooth モジュールは AT モードをサポートしており、これを使うと設定や各種パラメーターを確認および変更できます。AT モードを利用するには、次の図のように Bluetooth モジュールを F3 および FTDI に接続してください。

AT モードに入るための推奨手順:

  • F3 と FTDI をコンピューターから取り外します。
  • F3 の GND ピンを Bluetooth の GND ピンに、メス/メス (F/F) ワイヤー (できれば黒色のもの)で接続します。
  • F3 の 5V ピンを Bluetooth の VCC ピンに、F/F ワイヤー(できれば 赤色のもの)で接続します。
  • FTDI の RXI ピンを Bluetooth の TXD ピンに、メス/オス (F/M) ワイヤーで接続します。
  • FTDI の TXO ピンを Bluetooth の RXD ピンに、メス/オス (F/M) ワイヤーで接続します。
  • 次に、FTDI を USB ケーブルでコンピューターに接続します。
  • 続いて、Bluetooth モジュール上のボタンを同時に押したままにしながら、F3 を USB ケーブルでコンピューターに接続します(少しコツが要ります)。
  • その後、ボタンを離すと Bluetooth モジュールが AT モードに入ります。これは、Bluetooth モジュール上の赤色 LED がゆっくりしたパターン(約 1〜2 秒間隔で点滅)で点滅していることから確認できます。

AT モードは常に 38400 のボーレートで動作するため、ターミナルプログラムをそのボーレートに設定して FTDI デバイスに接続してください。

シリアル接続が確立すると、ERROR: (0) が繰り返し大量に表示されることがあります。その場合は、ENTER キーを押してエラー表示を止めてください。

動作確認

$ at
OK
OK
(etc...)

再度 ENTER を押すまで、OK が繰り返し返されます。

デバイス名を変更する

$ at+name=ferris
OK

Bluetooth モジュールの現在のボーレートを確認する

at+uart?
+UART:9600,0,0
OK
+UART:9600,0,0
OK
(etc ...)

ボーレートを変更する

$ at+uart=115200,0,0
OK

Bluetooth経由のシリアル

Bluetooth モジュールが minicom/PuTTY で動作することを確認できたので、次はそれを マイクロコントローラに接続しましょう:

これを配線する際の推奨手順は次のとおりです:

  • OpenOCD と itmdump を閉じます。
  • F3 をコンピュータから取り外します。
  • メス-メス(F/F)ワイヤー(できれば黒)を使って、F3 の GND ピンをモジュールの GND ピンに接続します。
  • F/F ワイヤー(できれば赤)を使って、F3 の 5V ピンをモジュールの VCC ピンに接続します。
  • F/F ワイヤーを使って、F3 の裏面にある PA9(TX)ピンを Bluetooth の RXD ピンに接続します。
  • F/F ワイヤーを使って、F3 の裏面にある PA10(RX)ピンを Bluetooth の TXD ピンに接続します。
  • 次に、USB ケーブルを使って F3 とコンピュータを接続します。
  • OpenOCD と itmdump を再起動します。

以上です! section 11 で書いたすべてのプログラムを、変更なしで実行できるはずです! 正しいシリアルデバイス / COM ポートを開くように注意してください。

Bluetooth デバイスとの通信に問題がある場合は、より低いボーレートで USART1 を初期化する必要があるかもしれません。115,200 bps から 9,600 bps に下げると改善する場合があります。詳細はこちらのコードを参照してください。

I2C

先ほど、シリアル通信プロトコルを見ました。これは非常に広く使われているプロトコルです。とても シンプルであり、この単純さによって Bluetooth や USB のようなほかのプロトコルの上に実装しやすくなっているためです。

しかし、この単純さは欠点でもあります。デジタル センサーの読み取りのような、より複雑なデータ交換では、 センサーベンダーはその上に別のプロトコルを考案する必要があります。

私たちにとって(不)運なことに、組み込み分野にはほかにもたくさんの通信プロトコルがあります。その 一部はデジタルセンサーで広く使われています。

私たちが使用している F3 ボードには、加速度計、磁力計、 ジャイロスコープという 3 つのモーションセンサーが搭載されています。加速度計と磁力計は単一のコンポーネントに パッケージ化されており、I2C バス経由でアクセスできます。

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

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

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

一般的なプロトコル

I2C プロトコルは、複数のデバイス間の通信をサポートしなければならないため、シリアル通信プロトコルよりも複雑です。例を使って、どのように動作するのか見ていきましょう。

マスター -> スレーブ

マスターがスレーブにデータを送信したい場合:

  1. マスター: START をブロードキャストする
  2. M: スレーブアドレス(7 ビット)+ WRITE に設定された R/W(8 番目)ビットをブロードキャストする
  3. スレーブ: ACK(確認応答)を返す
  4. M: 1 バイト送信する
  5. S: ACK を返す
  6. 手順 4 と 5 を 0 回以上繰り返す
  7. M: STOP をブロードキャストする、または(RESTART をブロードキャストして (2) に戻る)

スレーブアドレスは 7 ビット長ではなく 10 ビット長であってもかまいません。それ以外は 変わりません。

マスター <- スレーブ

マスターがスレーブからデータを読み取りたい場合:

  1. M: START をブロードキャストする
  2. M: スレーブアドレス(7 ビット)+ READ に設定された R/W(8 番目)ビットをブロードキャストする
  3. S: ACK で応答する
  4. S: 1 バイト送信する
  5. M: ACK で応答する
  6. 手順 4 と 5 を 0 回以上繰り返す
  7. M: STOP をブロードキャストする、または(RESTART をブロードキャストして (2) に戻る)

スレーブアドレスは 7 ビット長ではなく 10 ビット長であってもかまいません。それ以外は 変わりません。

LSM303DLHC

* 注記: 新しい(2020/09 ごろ以降の)Discovery ボードには、LSM303DLHC ではなく LSM303AGR が搭載されている場合があります。
詳細については、これ のような GitHub issue を確認してください。

F3 に搭載されているセンサーのうち、磁力計と加速度計の 2 つは、LSM303DLHC 集積回路という単一の コンポーネントにまとめられています。これら 2 つのセンサーには I2C バス経由でアクセスできます。各 センサーは I2C スレーブとして振る舞い、異なる アドレスを持っています。

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

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

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

セクション 5.1.1 I2C Operation - 20 ページ - LSM303DLHC データシート

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

セクション 7 レジスタの説明 - 25 ページ - LSM303DLHC データシート

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

それでは、ここまでの理論を実践に移しましょう!

USARTペリフェラルのときと同じように、あなたが main に到達する前に、必要な初期化はすべてこちらで済ませてあります。そのため、あなたが扱う必要があるのは次のレジスタだけです:

  • CR2. コントロールレジスタ 2。
  • ISR. 割り込みおよびステータスレジスタ。
  • TXDR. 送信データレジスタ。
  • RXDR. 受信データレジスタ。

これらのレジスタは、リファレンスマニュアルの次のセクションに記載されています:

セクション 28.7 I2C レジスタ - 868 ページ - リファレンスマニュアル

I2C1 ペリフェラルを、PB6SCL)および PB7SDA)のピンと組み合わせて使用します。

今回は、センサーがボード上にあり、すでにマイクロコントローラに接続されているため、配線する必要はありません。ただし、操作しやすくするために、シリアル / Bluetooth モジュールを F3 から取り外しておくことをお勧めします。後で、このボードをかなり動かすことになります。

あなたの課題は、磁力計の IRA_REG_M レジスタの内容を読み取るプログラムを書くことです。このレジスタは読み取り専用で、常に 0b01001000 という値を含んでいます。

マイクロコントローラは I2C マスターの役割を担い、LSM303DLHC 内部の磁力計が I2C スレーブになります。

以下がスターターコードです。TODO を実装する必要があります。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// Slave address
const MAGNETOMETER: u16 = 0b0011_1100;

// Addresses of the magnetometer's registers
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, _delay, mut itm) = aux14::init();

    // Stage 1: Send the address of the register we want to read to the
    // magnetometer
    {
        // TODO Broadcast START

        // TODO Broadcast the MAGNETOMETER address with the R/W bit set to Write

        // TODO Send the address of the register that we want to read: IRA_REG_M
    }

    // Stage 2: Receive the contents of the register we asked for
    let byte = {
        // TODO Broadcast RESTART

        // TODO Broadcast the MAGNETOMETER address with the R/W bit set to Read

        // TODO Receive the contents of the register

        // TODO Broadcast STOP
        0
    };

    // Expected output: 0x0A - 0b01001000
    iprintln!(&mut itm.stim[0], "0x{:02X} - 0b{:08b}", IRA_REG_M, byte);

    loop {}
}

追加の助けとして、扱うことになる正確なビットフィールドは次のとおりです:

  • CR2: SADD1, RD_WRN, NBYTES, START, AUTOEND
  • ISR: TXIS, RXNE, TC
  • TXDR: TXDATA
  • RXDR: RXDATA

解答

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// スレーブアドレス
const MAGNETOMETER: u16 = 0b0011_1100;

// 磁力計のレジスタのアドレス
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, _delay, mut itm) = aux14::init();

    // ステージ1: 読み取りたいレジスタのアドレスを
    // 磁力計に送信する
    {
        // START をブロードキャストする
        // R/W ビットを Write に設定した MAGNETOMETER アドレスをブロードキャストする
        i2c1.cr2.write(|w| {
            w.start().set_bit();
            w.sadd().bits(MAGNETOMETER);
            w.rd_wrn().clear_bit();
            w.nbytes().bits(1);
            w.autoend().clear_bit()
        });

        // さらにデータを送信できるようになるまで待つ
        while i2c1.isr.read().txis().bit_is_clear() {}

        // 読み取りたいレジスタのアドレスを送信する: IRA_REG_M
        i2c1.txdr.write(|w| w.txdata().bits(IRA_REG_M));

        // 前のバイトが送信されるまで待つ
        while i2c1.isr.read().tc().bit_is_clear() {}
    }

    // ステージ2: 要求したレジスタの内容を受信する
    let byte = {
        // RESTART をブロードキャストする
        // R/W ビットを Read に設定した MAGNETOMETER アドレスをブロードキャストする
        i2c1.cr2.modify(|_, w| {
            w.start().set_bit();
            w.nbytes().bits(1);
            w.rd_wrn().set_bit();
            w.autoend().set_bit()
        });

        // レジスタの内容を受信するまで待つ
        while i2c1.isr.read().rxne().bit_is_clear() {}

        // STOP をブロードキャストする(`AUTOEND = 1` のため自動)

        i2c1.rxdr.read().rxdata().bits()
    };

    // 期待される出力: 0x0A - 0b01001000
    iprintln!(&mut itm.stim[0], "0x{:02X} - 0b{:08b}", IRA_REG_M, byte);

    loop {}
}

複数のレジスタを読み取る

IRA_REG_M レジスタを読むことは I2C プロトコルの理解を確認するには良いテストでしたが、そのレジスタには面白みのない情報しか入っていません。

今回は、実際にセンサーの測定値を提供する磁力計のレジスタを読み取ります。 対象となる連続したレジスタは 6 つあり、先頭はアドレス 0x03OUT_X_H_M です。

前回のプログラムを修正して、これら 6 つのレジスタを読み取るようにします。必要な変更はほんのわずかです。

磁力計に要求するアドレスを IRA_REG_M から OUT_X_H_M に変更する必要があります。

#![allow(unused)]
fn main() {
    // 読み取りたいレジスタのアドレスを送信する: OUT_X_H_M
    i2c1.txdr.write(|w| w.txdata().bits(OUT_X_H_M));
}

スレーブには、1 バイトではなく 6 バイトを要求する必要があります。

#![allow(unused)]
fn main() {
    // RESTART を送出する
    // R/W ビットを Read に設定した MAGNETOMETER アドレスを送出する
    i2c1.cr2.modify(|_, w| {
        w.start().set_bit();
        w.nbytes().bits(6);
        w.rd_wrn().set_bit();
        w.autoend().set_bit()
    });
}

そして、1 バイトだけを読むのではなくバッファを埋めます。

#![allow(unused)]
fn main() {
    let mut buffer = [0u8; 6];
    for byte in &mut buffer {
        // レジスタの内容を受信するまで待つ
        while i2c1.isr.read().rxne().bit_is_clear() {}

        *byte = i2c1.rxdr.read().rxdata().bits();
    }

    // STOP を送出する(`AUTOEND = 1` のため自動)
}

データスループットを下げるためのディレイと一緒に、これらをすべてループ内にまとめると次のようになります。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// スレーブアドレス
const MAGNETOMETER: u16 = 0b0011_1100;

// 磁力計のレジスタのアドレス
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, mut delay, mut itm) = aux14::init();

    loop {
        // START を送出する
        // R/W ビットを Write に設定した MAGNETOMETER アドレスを送出する
        i2c1.cr2.write(|w| {
            w.start().set_bit();
            w.sadd().bits(MAGNETOMETER);
            w.rd_wrn().clear_bit();
            w.nbytes().bits(1);
            w.autoend().clear_bit()
        });

        // さらにデータを送信できるようになるまで待つ
        while i2c1.isr.read().txis().bit_is_clear() {}

        // 読み取りたいレジスタのアドレスを送信する: OUT_X_H_M
        i2c1.txdr.write(|w| w.txdata().bits(OUT_X_H_M));

        // 前のバイトが送信されるまで待つ
        while i2c1.isr.read().tc().bit_is_clear() {}

        // RESTART を送出する
        // R/W ビットを Read に設定した MAGNETOMETER アドレスを送出する
        i2c1.cr2.modify(|_, w| {
            w.start().set_bit();
            w.nbytes().bits(6);
            w.rd_wrn().set_bit();
            w.autoend().set_bit()
        });

        let mut buffer = [0u8; 6];
        for byte in &mut buffer {
            // 何かを受信するまで待つ
            while i2c1.isr.read().rxne().bit_is_clear() {}

            *byte = i2c1.rxdr.read().rxdata().bits();
        }
        // STOP を送出する(`AUTOEND = 1` のため自動)

        iprintln!(&mut itm.stim[0], "{:?}", buffer);

        delay.delay_ms(1_000_u16);
    }
}

これを実行すると、itmdump のコンソールには毎秒 1 回、新しい 6 バイトの配列が表示されるはずです。ボードを動かすと、配列内の値が変化するはずです。

$ # itmdump ターミナル
(..)
[0, 45, 255, 251, 0, 193]
[0, 44, 255, 249, 0, 193]
[0, 49, 255, 250, 0, 195]

しかし、このままではこれらのバイト列はあまり意味をなしません。実際の測定値に変換してみましょう。

#![allow(unused)]
fn main() {
        let x_h = u16::from(buffer[0]);
        let x_l = u16::from(buffer[1]);
        let z_h = u16::from(buffer[2]);
        let z_l = u16::from(buffer[3]);
        let y_h = u16::from(buffer[4]);
        let y_l = u16::from(buffer[5]);

        let x = ((x_h << 8) + x_l) as i16;
        let y = ((y_h << 8) + y_l) as i16;
        let z = ((z_h << 8) + z_l) as i16;

        iprintln!(&mut itm.stim[0], "{:?}", (x, y, z));
}

これで見やすくなるはずです。

$ # `itmdump ターミナル
(..)
(44, 196, -7)
(45, 195, -6)
(46, 196, -9)

これは、地球の磁場を磁力計の XYZ 軸に沿って分解したものです。

次のセクションでは、これらの数値をどう解釈するかを学びます。

LEDコンパス

このセクションでは、F3 上の LED を使ってコンパスを実装します。本物のコンパスと同じように、この LED コンパスも何らかの方法で北を指さなければなりません。これは 8 個ある LED のうち 1 つを点灯させることで実現します。点灯している LED が北を向くようにします。

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

以下を見てください。磁力計には 3 つの軸が対応付けられています。

上には X 軸と Y 軸しか示されていません。Z 軸は画面の「手前」を向いています。

以下のスターターコードを実行して、磁力計の読み取り値に慣れましょう。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*};

#[entry]
fn main() -> ! {
    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        iprintln!(&mut itm.stim[0], "{:?}", lsm303dlhc.mag().unwrap());
        delay.delay_ms(1_000_u16);
    }
}

この lsm303dlhc モジュールは、LSM303DLHC に対する高レベル API を提供します。内部では、前のセクションで実装したのと同じ I2C ルーチンを実行していますが、X、Y、Z の値をタプルではなく I16x3 構造体で返します。

今いる場所で北がどちらにあるかを確認してください。次に、基板が「北向き」にそろうように回転させます。North LED(LD3)が北を向いているはずです。

では、スターターコードを実行して出力を観察してください。X、Y、Z の値はどのように表示されますか?

$ # itmdump terminal
(..)
I16x3 { x: 45, y: 194, z: -3 }
I16x3 { x: 46, y: 195, z: -8 }
I16x3 { x: 47, y: 197, z: -2 }

次に、基板を地面と平行に保ったまま 90 度回転させてください。今度は X、Y、Z にどのような値が表示されますか? その後、さらに 90 度回転させてください。どのような値が表示されますか?

試行 1

LED コンパスを実装するいちばん単純な方法は何でしょうか。たとえ完全ではないとしても。

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

たとえば、次のような場合にはどの LED を点灯させるでしょうか。EMF は Earth’s Magnetic Field の略で、緑の矢印は EMF の向きを示しています(北を指しています)。

Southeast LED、ですよね?

その状況では、磁場の X 成分と Y 成分の 符号 はどうなっているでしょうか。どちらも 正です。

X 成分と Y 成分の符号だけを見れば、磁場がどの象限に属するかを 判断できます。

先ほどの例では、磁場は第1象限にあり(x と y は正でした)、SouthEast LED を点灯させるのが 妥当でした。同様に、磁場が別の象限にあるなら、別の LED を点灯させることができます。

そのロジックを試してみましょう。スターターコードは次のとおりです。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        // X 成分と Y 成分の符号を見て、磁場がどの
        // 象限にあるかを判断する
        let dir = match (x > 0, y > 0) {
            // 象限 ???
            (true, true) => Direction::Southeast,
            // 象限 ???
            (false, true) => panic!("TODO"),
            // 象限 ???
            (false, false) => panic!("TODO"),
            // 象限 ???
            (true, false) => panic!("TODO"),
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(1_000_u16);
    }
}

led モジュールには、方位にちなんだ 8 つのバリアントを持つ Direction 列挙型があります: North, East, Southwest などです。これらの各バリアントは、コンパスにある 8 個の LED のうち 1 つを表します。Leds の値は Direction enum を使ってインデックス指定でき、その 結果として、その Direction を向く LED が得られます。

解答 1

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        // X 成分と Y 成分の符号を見て、磁場がどの
        // 象限にあるかを判定する
        let dir = match (x > 0, y > 0) {
            // 第 I 象限
            (true, true) => Direction::Southeast,
            // 第 II 象限
            (false, true) => Direction::Northeast,
            // 第 III 象限
            (false, false) => Direction::Northwest,
            // 第 IV 象限
            (true, false) => Direction::Southwest,
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(1_000_u16);
    }
}

その2

今回は、磁場が磁力計の X 軸と Y 軸となす角度を正確に求めるために、数学を使います。

atan2 関数を使います。この関数は、-PI から PI の範囲の角度を返します。以下の 図は、この角度がどのように測定されるかを示しています:

この図では明示的には示されていませんが、X 軸は右を向き、Y 軸は上を向いています。

これがスターターコードです。ラジアン単位の theta は、すでに計算されています。theta の値に基づいて、どの LED を点灯するかを選ぶ必要があります。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

// これは便利ですよ ;-)
use core::f32::consts::PI;

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};
// このトレイトは `atan2` メソッドを提供します
use m::Float;

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        let _theta = (y as f32).atan2(x as f32); // ラジアン単位

        // FIXME `theta` の値に基づいて指す方向を選んでください
        let dir = Direction::Southeast;

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(100_u8);
    }
}

提案 / ヒント:

  • 円を 1 周すると 360 度です。
  • PI ラジアンは 180 度に相当します。
  • theta が 0 なら、どの LED を点灯しますか?
  • 代わりに theta が 0 に非常に近い値なら、どの LED を点灯しますか?
  • theta が増え続けるとしたら、どの値で別の LED を点灯しますか?

解答 2

#![deny(unsafe_code)]
#![no_main]
#![no_std]

// これは便利です ;-)
use core::f32::consts::PI;

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};
use m::Float;

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        let theta = (y as f32).atan2(x as f32); // ラジアン単位

        let dir = if theta < -7. * PI / 8. {
            Direction::North
        } else if theta < -5. * PI / 8. {
            Direction::Northwest
        } else if theta < -3. * PI / 8. {
            Direction::West
        } else if theta < -PI / 8. {
            Direction::Southwest
        } else if theta < PI / 8. {
            Direction::South
        } else if theta < 3. * PI / 8. {
            Direction::Southeast
        } else if theta < 5. * PI / 8. {
            Direction::East
        } else if theta < 7. * PI / 8. {
            Direction::Northeast
        } else {
            Direction::North
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(100_u8);
    }
}

大きさ

これまでは磁場の方向を扱ってきましたが、その実際の大きさはどれくらいなのでしょうか? magnetic_field 関数が報告する数値には単位がありません。これらの値をどのようにしてガウスに変換できるでしょうか?

その疑問にはドキュメントが答えてくれます。

セクション 2.1 センサー特性 - 10 ページ - LSM303DLHC データシート

そのページの表には、GN ビットの値に応じて異なる値を持つ magnetic gain setting が示されています。デフォルトでは、それらの GN ビットは 001 に設定されています。これは、X 軸と Y 軸の magnetic gain が 1100 LSB / Gauss であり、Z 軸の magnetic gain が 980 LSB / Gauss であることを意味します。LSB は Least Significant Bits の略で、1100 LSB / Gauss という数値は、1100 の読み取り値が 1 Gauss に相当し、2200 の読み取り値が 2 Gauss に相当し、以下同様であることを示しています。

したがって、必要なのは、センサーが出力する X、Y、Z の値を、それぞれ対応する gain で割ることです。そうすれば、磁場の X、Y、Z 成分をガウス単位で得られます。

さらに少し計算を加えると、磁場の X、Y、Z 成分からその大きさを求めることができます。

#![allow(unused)]
fn main() {
let magnitude = (x * x + y * y + z * z).sqrt();
}

これらすべてをプログラムにまとめると、次のようになります。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, I16x3};
use m::Float;

#[entry]
fn main() -> ! {
    const XY_GAIN: f32 = 1100.; // LSB / G
    const Z_GAIN: f32 = 980.; // LSB / G

    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        let I16x3 { x, y, z } = lsm303dlhc.mag().unwrap();

        let x = f32::from(x) / XY_GAIN;
        let y = f32::from(y) / XY_GAIN;
        let z = f32::from(z) / Z_GAIN;

        let mag = (x * x + y * y + z * z).sqrt();

        iprintln!(&mut itm.stim[0], "{} mG", mag * 1_000.);

        delay.delay_ms(500_u16);
    }
}

このプログラムは、磁場の大きさ(強さ)をミリガウス(mG)で報告します。地球の磁場の大きさは 250 mG から 650 mG の範囲にあります(大きさは地理的な位置によって変化します)ので、その範囲内、またはそれに近い値が表示されるはずです – 私の環境ではおよそ 210 mG の大きさが見えます。

いくつか質問です。

ボードを動かさない状態で、どのような値が見えますか? 常に同じ値が見えますか?

ボードを回転させると、大きさは変化しますか? 変化するべきでしょうか?

キャリブレーション

ボードを回転させると、磁力計に対する地球の磁場の向きは変わるはずですが、 その大きさは変わらないはずです! ところが、磁力計はボードを回転させると 磁場の大きさが変化していることを示します。

なぜそうなるのでしょうか? 実は、正しい値を返すには磁力計を キャリブレーションする必要があります。

このキャリブレーションにはかなり多くの数学(行列)が関わるため、ここでは扱いませんが、 興味があれば、このアプリケーションノートに手順が説明されています。代わりに、この セクションでは、どれだけずれているのかを可視化することにします。

次の実験をしてみましょう。ボードをさまざまな方向にゆっくり回転させながら、 磁力計の読み取り値を記録します。読み取り値をタブ区切り値(TSV)として整形するために、 iprintln マクロを使います。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, I16x3};

#[entry]
fn main() -> ! {
    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        let I16x3 { x, y, z } = lsm303dlhc.mag().unwrap();

        iprintln!(&mut itm.stim[0], "{}\t{}\t{}", x, y, z);

        delay.delay_ms(100_u8);
    }
}

コンソールには次のような出力が表示されるはずです。

$ # itmdump コンソール
-76     213     -54
-76     213     -54
-76     213     -54
-76     213     -54
-73     213     -55

次のようにして、これをファイルにパイプできます。

$ # 注意! 実行中の別の `itmdump` インスタンスがある場合は終了してください
$ itmdump -F -f itm.txt > emf.txt

数秒間データを記録しながら、ボードを多くの異なる方向に回転させてください。

次に、その TSV ファイルを表計算ソフトに取り込むか(または以下に示す Python スクリプトを使って)、 最初の 2 列を散布図としてプロットしてください。

#!/usr/bin/python

import csv
import math
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import sys

# プロットスタイルを適用する
sns.set()

x = []
y = []

with open(sys.argv[1], 'r') as f:
    rows = csv.reader(f, delimiter='\t')

    for row in rows:
        # データが欠けている行を破棄する
        if len(row) != 3 or not row[0] or not row[1]:
            continue

        x.append(int(row[0]))
        y.append(int(row[1]))

r = math.ceil(max(max(np.abs(x)), max(np.abs(y))) / 100) * 100

plt.plot(x, y, '.')
plt.xlim(-r, r)
plt.ylim(-r, r)
plt.gca().set_aspect(1)
plt.tight_layout()

plt.savefig('emf.svg')
plt.close

ボードを平らな水平面の上で回転させた場合、磁場の Z 成分は 比較的一定のままであり、このプロットは原点を中心とする円周(楕円ではなく) になっているはずです。上のプロットのように、ボードをランダムな方向に回転させた 場合は、原点を中心とする多数の点からなる円が得られるはずです。 円の形からのずれは、磁力計をキャリブレーションする必要があることを示しています。

要点: センサーの読み取り値をただ信じてはいけません。妥当な値を出力していることを 確認してください。そうでない場合は、キャリブレーションしてください。

Punch-o-meter

このセクションでは、ボードに搭載されている加速度計を使って遊びます。

今回は何を作るのでしょうか? パンチメーターです! ジャブの威力を測定します。まあ、 実際に測るのは、加速度計が測定するのが加速度であるため、到達できる最大加速度です。 とはいえ、強さと加速度は比例するので、これは十分によい近似です。

加速度計も LSM303DLHC パッケージの中に内蔵されています。そして磁力計と同じように、 I2C バスを使ってアクセスできます。また、磁力計と同じ座標系を持っています。 座標系をもう一度示します。

前のユニットと同様に、高水準 API を使って、きれいにまとめられた struct として センサーの読み取り値を直接取得します。

重力は上向き?

最初に何をするのでしょうか?

サニティチェックを実行します!

スターターコードは、加速度計によって測定された加速度の X、Y、Z 成分を出力します。 これらの値はすでに「スケーリング」されており、単位は g です。ここで 1 g は、 重力加速度、つまりおよそ毎秒毎秒 9.8 メートルに等しくなります。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux16::{entry, iprint, iprintln, prelude::*, I16x3, Sensitivity};

#[entry]
fn main() -> ! {
    let (mut lsm303dlhc, mut delay, _mono_timer, mut itm) = aux16::init();

    // extend sensing range to `[-12g, +12g]`
    lsm303dlhc.set_accel_sensitivity(Sensitivity::G12).unwrap();
    loop {
        const SENSITIVITY: f32 = 12. / (1 << 14) as f32;

        let I16x3 { x, y, z } = lsm303dlhc.accel().unwrap();

        let x = f32::from(x) * SENSITIVITY;
        let y = f32::from(y) * SENSITIVITY;
        let z = f32::from(z) * SENSITIVITY;

        iprintln!(&mut itm.stim[0], "{:?}", (x, y, z));

        delay.delay_ms(1_000_u16);
    }
}

ボードを静止させた状態でこのプログラムを実行すると、出力は次のようになります。

$ # itmdump console
(..)
(0.0, 0.0, 1.078125)
(0.0, 0.0, 1.078125)
(0.0, 0.0, 1.171875)
(0.0, 0.0, 1.03125)
(0.0, 0.0, 1.078125)

ボードは動いていないのに加速度がゼロではないので、これは奇妙です。いったい何が起きているのでしょうか? これは重力に関係しているに違いありませんよね? 重力加速度は 1 g ですから。しかし、 重力は物体を下向きに引っ張るので、Z 軸方向の加速度は正ではなく負になるはずです ……

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

ここで起きているのは、加速度計が測定しているのが、あなたが観測している加速度ではなく、 ボードの 固有加速度 だということです。この固有加速度とは、自由落下している観測者から見た ボードの加速度です。自由落下している観測者は、1g の加速度で地球の中心に向かって移動しています。 その観測者の視点では、ボードは実際には 1g の加速度で上向きに(地球の中心から遠ざかる方向に) 動いています。これが、固有加速度が上を向く理由です。これはまた、もしボードが自由落下していたなら、 加速度計は固有加速度をゼロと報告する、ということも意味します。お願いですから、家では試さないでください。

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

課題

話を簡単にするため、ボードを水平に保ったまま、加速度は X 軸方向だけを測定します。そうすれば、以前に観測したあの 見かけの 1g を差し引く処理を扱わずに済みます。これは、その 1g がボードの向きに応じて X、Y、Z 成分を持ち得るため、難しいからです。

この punch-o-meter が行うべきことは次のとおりです。

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

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

私の解答

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux16::{entry, iprint, iprintln, prelude::*, I16x3, Sensitivity};
use m::Float;

#[entry]
fn main() -> ! {
    const SENSITIVITY: f32 = 12. / (1 << 14) as f32;
    const THRESHOLD: f32 = 0.5;

    let (mut lsm303dlhc, mut delay, mono_timer, mut itm) = aux16::init();

    lsm303dlhc.set_accel_sensitivity(Sensitivity::G12).unwrap();

    let measurement_time = mono_timer.frequency().0; // ティック単位で1秒
    let mut instant = None;
    let mut max_g = 0.;
    loop {
        let g_x = f32::from(lsm303dlhc.accel().unwrap().x).abs() * SENSITIVITY;

        match instant {
            None => {
                // 加速度がしきい値を超えたら、測定を開始する
                if g_x > THRESHOLD {
                    iprintln!(&mut itm.stim[0], "START!");

                    max_g = g_x;
                    instant = Some(mono_timer.now());
                }
            }
            // 測定中
            Some(ref instant) if instant.elapsed() < measurement_time => {
                if g_x > max_g {
                    max_g = g_x;
                }
            }
            _ => {
                // 最大値を報告する
                iprintln!(&mut itm.stim[0], "Max acceleration: {}g", max_g);

                // 測定完了
                instant = None;

                // リセット
                max_g = 0.;
            }
        }

        delay.delay_ms(50_u8);
    }
}

あなたが探索するために残されているもの

私たちはまだ表面を少しかじったにすぎません! あなたが探索できることは、まだたくさん残っています。

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

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

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

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

マルチタスク

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

プリエンプティブ・マルチタスクでは、現在実行中のタスクは、どの時点であっても別のタスクによって preempted(割り込み)される可能性があります。プリエンプションが起こると、最初のタスクは中断され、代わりにプロセッサが 2 つ目のタスクを実行します。やがて最初のタスクは再開されます。マイクロコントローラは、interrupts の形でプリエンプションのためのハードウェアサポートを提供します。

協調的マルチタスクでは、実行中のタスクは suspension point に到達するまで動作し続けます。プロセッサがそのサスペンションポイントに到達すると、現在のタスクの実行を停止し、代わりに別のタスクを実行します。やがて最初のタスクは再開されます。これら 2 つのマルチタスク方式の主な違いは、協調的マルチタスクでは、実行中のどの時点でも強制的にプリエンプトされるのではなく、known なサスペンションポイントで実行制御を yields することです。

スリープ

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

プロセッサがスリープすると、命令の実行を停止するため、電力を節約できます。 電力を節約するのはほとんど常に良い考えなので、マイクロコントローラは可能な限り多くの時間をスリープしているべきです。では、何らかの動作を実行するためにいつ起きる必要があるのかを、どのように知るのでしょうか? 「割り込み」はマイクロコントローラを起こすイベントの 1 つですが、ほかにもあり、wfiwfe はプロセッサを「スリープ」させる命令です。

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

マイクロコントローラ(私たちの STM32F3 のようなもの)には、多くの異なる機能があります。しかし、それらの多くは、さまざまな問題の解決に使える類似した機能を共有しています。

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

Direct Memory Access (DMA).

この周辺機器は、ある種の asynchronous memcpy です。これまでのプログラムでは、UART や I2C のような周辺機器に、1 バイトずつデータを送り込んできました。この DMA 周辺機器は、データの一括転送を行うために使用できます。RAM から RAM、UART のような周辺機器から RAM、あるいは RAM から周辺機器へも可能です。たとえば、USART1 からこのバッファへ 256 バイト読み込む、といった DMA 転送をスケジュールし、それをバックグラウンドで実行させておき、進行中にほかの作業を行いながら、何らかのレジスタをポーリングして完了したかどうかを確認できます。

割り込み

現実世界と相互作用するためには、ある種のイベントが発生したときに、マイクロコントローラが immediately 応答する必要があることがよくあります。

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

これらの割り込みは非常に便利である一方で、適切に扱うのは少し難しいこともあります。私たちはイベントにすばやく応答したいですが、同時にほかの作業も継続できるようにしたいのです。

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

パルス幅変調 (PWM)

一言で言えば、PWM とは、何かを周期的にオンにしてからオフにすることを繰り返しつつ、「オンの時間」と「オフの時間」の間にある割合(「デューティサイクル」)を保つことです。十分に高い周波数で LED に対してこれを使うと、LED を暗くできます。低いデューティサイクル、たとえばオン時間が 10% でオフ時間が 90% なら LED は非常に暗くなり、高いデューティサイクル、たとえばオン時間が 90% でオフ時間が 10% なら、LED はずっと明るくなります(ほぼ完全に給電されているかのように見えます)。

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

デジタル入力

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

(spoilers スイッチ / ボタンの 2 値状態を読み取るのは、聞こえるほど単純ではありません ;-)

Analog-to-Digital Converters (ADC)

世の中にはたくさんのデジタルセンサーがあります。I2C や SPI のようなプロトコルを使ってそれらを読み取れます。しかし、アナログセンサーも存在します! これらのセンサーは、検出している物理量の大きさに比例した電圧レベルを出力するだけです。

ADC 周辺機器は、その「アナログ」電圧レベル、たとえば 1.25 ボルトを、プロセッサが計算に使用できる [0, 65535] の範囲の「デジタル」な数値へ変換するために使用できます。

Digital-to-Analog Converters (DAC)

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

Real Time Clock (RTC)

この周辺機器は、「人間向けの形式」で時間を追跡するために使用できます。秒、分、時間、日、月、年です。この周辺機器は、「ティック」をこうした人にわかりやすい時間単位へ変換する処理を担います。うるう年や夏時間まで処理してくれます!

その他の通信プロトコル

SPI, I2S, SMBUS, CAN, IrDA, Ethernet, USB, Bluetooth, etc. さまざまなアプリケーションは、さまざまな通信プロトコルを使用します。ユーザー向けの アプリケーションには通常 USB コネクタがあります。USB は PC やスマートフォンで広く普及した プロトコルだからです。一方、車内では多数の CAN 「バス」が使われています。デジタルセンサーの中には SPI を使うものもあれば、I2C を使うもの、 さらに SMBUS を使うものもあります。

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

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

ジャイロスコープ

Punch-o-meter 演習の一環として、私たちは加速度計を使って 3 次元の加速度変化を測定しました。 このボードにはジャイロスコープと呼ばれるセンサーも搭載されており、これによって 3 次元の 「回転」の変化を測定できます。

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

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

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

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

センサーフュージョン

STM32F3DISCOVERY には 3 つのモーションセンサーが搭載されています。加速度計、 ジャイロスコープ、磁力計です。これらは単体では、それぞれ(固有)加速度、角速度、 (地球の)磁場を測定します。しかし、これらの物理量を「融合」することで、より有用なもの、 つまりボードの向きの「ロバスト」な測定値を得ることができます。ここでロバストとは、単一の センサーだけで可能な場合よりも測定誤差が少ないことを意味します。

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


では、次は何をすればよいでしょうか? いくつかの選択肢があります。

  • f3 ボードサポート crate のサンプルを見てみることができます。これらのサンプルはすべて、 手元の STM32F3DISCOVERY ボードで動作します。
  • Real Time for The Masses を見てみることもできます。これは、タスクの優先順位付けと デッドロックのない実行をサポートする、非常に効率的なプリエンプティブ マルチタスクフレームワークです。
  • Rust を別の開発ボードで動かしてみることもできます。始める最も簡単な方法は、 cortex-m-quickstart Cargo プロジェクトテンプレートを使うことです。
  • Rust の型システムが I/O 設定のバグをどのように防げるかを説明した このブログ記事を見てみることもできます。
  • Rust による組み込み開発に関する雑多なトピックについて、私の blog を見てみることもできます。
  • すべてのマイクロコントローラーで一般的に見られる組み込み I/O 機能のための抽象化(トレイト)を 構築することを目指す embedded-hal プロジェクトを見てみることもできます。
  • Weekly driver initiative に参加して、embedded-hal トレイトの上に構築され、 あらゆる種類のプラットフォーム(ARM Cortex-M、AVR、MSP430、RISCV など)で動作する 汎用ドライバーの作成を手伝うこともできます。

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

OpenOCD の問題

OpenOCD に接続できない - “Error: open failed”

症状

デバイスとの新しい接続を確立しようとすると、次のようなエラーが表示されます:

$ openocd -f (..)
(..)
Error: open failed
in procedure 'init'
in procedure 'ocd_bouncer'

原因

デバイスが接続されていない(または正しく接続されていない)か、正しい ST-LINK インターフェース設定が使われていません。

対処方法

Linux:

  • lsusb を使って USB 接続を確認してください。
  • デバイスを開くための権限が不足している可能性があります。sudo を付けて再試行してください。 それで動作する場合は、root 権限なしで OpenOCD を動かすために these instructions を利用できます。
  • ST-LINK に対して誤ったインターフェース設定を使っている可能性があります。 interface/stlink-v2-1.cfg の代わりに interface/stlink-v2.cfg を試してください。

Windows:

  • ST-LINK USB ドライバーが不足している可能性があります。インストール手順は here を参照してください。

OpenOCD に接続できない - “Polling again in X00ms”

症状

デバイスとの新しい接続を確立しようとすると、次のようなエラーが表示されます:

$ openocd -f (..)
(..)
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 100ms
Info : Previous state query failed, trying to reconnect
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 300ms
Info : Previous state query failed, trying to reconnect

原因

マイクロコントローラーが何らかのタイトな無限ループに陥っているか、継続的に例外を発生させている可能性があります。たとえば、例外ハンドラー自体が例外を発生させている場合です。

対処方法

  • OpenOCD が実行中であれば終了する
  • リセット(黒い)ボタンを押したままにする
  • OpenOCD コマンドを実行する
  • その後、リセットボタンを離す

OpenOCD の接続が切れた - “Polling again in X00ms”

症状

実行中の OpenOCD セッションで、突然次のようなエラーが発生します:

# openocd -f (..)
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 100ms
Info : Previous state query failed, trying to reconnect
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 300ms
Info : Previous state query failed, trying to reconnect

原因

USB 接続が切断されました。

対処方法

  • OpenOCD を終了する
  • USB ケーブルを抜いて、再度接続する。
  • OpenOCD を再起動する

デバイスに書き込めない - “Ignoring packet error, continuing…”

症状

デバイスへの書き込み中に、次のように表示されます:

$ arm-none-eabi-gdb $file
Start address 0x8000194, load size 31588
Transfer rate: 22 KB/sec, 5264 bytes/write.
Ignoring packet error, continuing...
Ignoring packet error, continuing...

原因

ITM に「出力」しているプログラムの実行中に itmdump を閉じました。この場合、現在の GDB セッションは ITM 出力がないことを除けば通常どおり動作しているように見えますが、次の GDB セッションでは前のセクションに示したメッセージでエラーになります。

または、monitor tpiu を発行したitmdump を呼び出したため、OpenOCD が書き込んでいたファイル / 名前付きパイプを itmdump が削除してしまいました。

対処方法

  • GDB、OpenOCD、itmdump を終了/強制終了する
  • itmdump が使用していたファイル / 名前付きパイプを削除する(たとえば、 itm.txt)。
  • OpenOCD を起動する
  • 次に、itmdump を起動する
  • 次に、monitor tpiu コマンドを実行する GDB セッションを開始する。

OpenOCD に接続できない - “Error: couldn’t bind [telnet] to socket: Address already in use”

症状

デバイスとの新しい接続を確立しようとすると、次のようなエラーが表示されます:

$ openocd -f (..)
(..)
Error: couldn't bind telnet to socket: Address already in use

原因

OpenOCD がアクセスを必要とするポート 3333、4444、6666 のうち 1 つ以上が、別のプロセスによって使用されています。これらの各ポートにはそれぞれ別の用途があり、3333 は gdb、4444 は telnet、6666 は TCL へのリモートプロシージャコール(RPC)コマンドに使われます。

対処方法

この問題を解決するには 2 つの方法があります。A) それらのポートを使用しているプロセスを停止する。B) OpenOCD が使用する空いている別のポートを指定する。

解決方法 A

Mac:

  • sudo lsof -PiTCP -sTCP:LISTEN を実行して、ポートを使用しているプロセスの一覧を取得する
  • 重要なポートを塞いでいるプロセスの pid を確認し、それぞれについて kill [pid] を実行して停止する。(そのプロセスがマシン上で重要な処理を実行していないことを確認できる場合に限ります!)

解決方法 B

All:

  • OpenOCD の起動時に設定詳細を渡して、各プロセスのいずれかについてデフォルトとは異なるポートを使うようにする。
  • たとえば、telnet 機能をデフォルトの 4444 ではなく 4441 で使うには、openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg -c "telnet_port 4441" を実行します
  • OpenOCD の Configuration Stage の詳細は、公式オンラインドキュメントの official docs online を参照してください。

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.

原因

nightly-2018-04-08 より古いツールチェーンを使っており、rustup target add thumbv7em-none-eabihf の実行を忘れています。

対処方法

nightly を更新し、thumbv7em-none-eabihf ターゲットをインストールしてください。

$ rustup update nightly

$ rustup target add thumbv7em-none-eabihf

GDB の使い方

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

一般的なデバッグ

注: 以下に示すコマンドの多くは短縮形で実行できます。たとえば、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 の値を表示

OpenOCD をリモートで制御する

  • monitor reset run: CPU をリセットし、最初から実行を開始
    • monitor reset: 上記と同じ
  • monitor reset init: CPU をリセットし、開始位置で実行を停止
  • monitor targets: 現在のターゲットの情報と状態を表示