これは Rustその2 Advent Calendar 2017 24日目の記事です。ARM Cortex-M アーキテクチャを採用した32ビット・マイクロコントローラ(マイコン)向けに、Rust でプログラムを開発する方法を紹介します。これらのマイコンでは Linux などの OS は動きませんので、OS なしで動作する「ベアメタル」なプログラムを書きます。
Rust を使うと、たとえマイコンであってもマルチスレッドでデータ競合のない「安全な」プログラムを簡単に開発できます。
今回は STM32F4 Discovery という Cortex-M4 を搭載した2千円台で買えるボードを使用します。Cortex-M0+ 以上のプロセッサを搭載したマイコンならどれでも同じ開発手法がとれるはずです。
高性能かつ低価格のマイコンを実現する Cortex-M アーキテクチャ
ARM Cortex-M シリーズのプロセッサを搭載したマイコンは、32ビットプロセッサの処理能力を持ちながら、価格も消費電力も8ビットや16ビットのマイコン並みに抑えられており、非常に魅力的です。
プロセッサの命令セットも thumb という16ビットのものを採用していますので、メモリ効率でも引けをとりません。 (2017年12月29日 修正: 命令セットは Thumb-2 という 16ビットと32ビットが混在したものを採用しているそうです。16ビット命令のみの Thumb では採用できなかった命令が追加されており、少ないコードおよびデータフットプリントで最大限の性能を発揮するそうです。詳細は こちらの日本語記事 を参照してください)
マイコンチップの開発メーカーは Cortex-M を中心に各社が得意とするペリフェラル(周辺回路)を加えた多彩なチップ(MCU)を製品化しています。このような MCU には IoT 向けのペリフェラルが内蔵された超低消費電力マイコンもあり、不正アクセスからデバイスやメモリを守るためのセキュリティ機能や、AES 256 に対応した暗号化エンジン、暗号強度の真乱数発生器などがハードウェアとして実装されています。また DSP 搭載チップなら画像や音声などの信号処理も可能です。
もちろん普通のマイコンで定番となっているペリフェラルも装備されていますので、各種センサーや液晶パネル、カメラデバイスや無線デバイスへの接続なども可能で、組み込みシステムに必要な機能は全て備わっています。
Rust で開発する利点
マイコンのアプリケーションは C や C++ 言語で開発するのが一般的です。その理由として、マイコンではメモリや電源(電池)などのリソースが極端に制限されており、他の多くの言語ではその環境に適したアプリケーションが開発できないことがあります。また既存のソフトウェア資産が C/C++ で書かれているのも理由のひとつです。
Rust はシステムプログラミングに適した言語ですので C/C++ のようにマイコンのアプリケーション開発にも適しています。Rust で開発する利点として以下が挙げられます。
-
メモリ安全なプログラムを書ける
- ポインタや配列を使う際、初期化されてないメモリ領域や解放済みのメモリ領域にアクセスすることがない
- マルチタスクプログラムであっても、メモリ競合が起きないことがコンパイル時に検証される
-
ゼロコスト抽象化
- 強力な型システム、トレイト、クロージャなどによる高度な抽象化を用いながら、コンパイル後のプログラムは C のそれに匹敵する実行効率を持つ
-
既存のソフトウェア資産を活用できる
- FFIを通じて C プログラムで書かれた各種ドライバソフトウェアと密に連携できる
この記事では最初の2つをカバーしています。
Rust による Cortex-M プログラミングは意外に快適
マイコン向けのプログラミングは PC 向けのものとは大きく勝手が異なります。まずマイコンでは OS が動いていませんので、プログラムから直接ハードウェアを制御することになります。また Rust の標準ライブラリ(std)もありません。あるのは core ライブラリだけです。Cortex-M プロセッサをフル回転させると電池の持ちが悪くなりますので、実用的なアプリケーションにするには、豊富な割り込み機能を活用して何かのイベントが起きない限りプロセッサを待機状態にさせておくことも大切です。
私自身このような環境での開発は初めてでしたので、かなり身構えていたのですが、やってみると意外に簡単・快適でした。なぜなら Xargo(関連記事) の作者としてよく知られる Jorge Aparicio (japaric) さんが Cortex-M 向けの Rust エコシステムをある程度まで作り上げていたからです。このエコシステムは Rust で書かれた複数のクレートとコマンドラインツールで構成されており、unsafe ブロックを一切使わずに、Rust だけでイベント駆動のプリエンプティブ・マルチタスクなアプリケーションを開発できます。
また Rust コンパイラが出力する実行ファイル(バイナリ)は、C のそれと同じ体裁をしていますので、PC 側で起動した GDB デバッガで、Rust のソースコードとそれに対応するアセンブリを見ながらリモートデバッグできます。またプロセッサに備わったセミホスティングというデバッグ機能を使って print デバッグも可能です。
今回製作するのはタイマーやスイッチをトリガーにして LED を点滅させる「Lチカ」プログラムです。これは組み込み界の「Hello world」と言われる初歩的な題材ですが、最後の方で紹介するプログラムでは割り込みを活用しており、超低消費電力で動作します。この開発手法をベースに、用途に合ったセンサーなどをマイコンに追加したり、FFI を通じて C などの他の言語で書かれたドライバー・ライブラリと連携したりすれば、実用的なアプリケーションを開発できるでしょう。
なお私は仕事では x86_64 系サーバー向けの分散データストアの開発をしており、組み込み系の知識も経験もほとんどありません。Cortex-M マイコンもこの記事を書くために1週間前に初めて触ったという状態です。実はボードを買ったのは今年の2月でしたが、まとまった時間がなかなか作れず、そのまま引き出しの中で眠ってました。
このようなレベルですので、内容に間違いや不足などあるかもしれません。そんな時はコメント欄で指摘・補足していただけると幸いです。
今回使用するボード STM32F4 Discovery
今回、私が使用するのは、STマイクロエレクトロニクス社(以降 ST)が販売する STM32F4 Discovery ボードです。このボードと USB ケーブル1本、そして母艦となる Mac/PC/ラズパイなどがあれば、他に何も足さずにこのマイコンの多くの機能を試せます。電源も USB から供給されますので、AC アダプターは不要です。日本国内では秋月電子通商、スイッチサイエンス、Digi-Key、ストロベリー・リナックスなどの通販サイトから2千円台で購入できます。
このボードは ST が自社製マイコンチップ STM32 シリーズの販売促進のために作っているため、開発を始めるのに必要なパーツが数多く実装されており、お買い得なボードだと思います。たとえばマイコンのフラッシュメモリにプログラムを書き込むために、プログラムライターというハードウェアが必要ですが、それの一種である ST-LINK/V2 がボードに実装されています。ちなみに ST-LINK/V2 を単体で買うと、それだけで3千円以上します。
STM32F4 Discovery ボードの主な装備
- ST-LINK/V2
- ボード上の MCU だけでなく、ジャンパ切り替えで外部の MCU と SWD 接続できる
- 4つのユーザー LED
- 3軸加速度センサー
- クラスDアンプ付きのオーディオ再生向け DAC(デジタル・アナログ変換器)
- micro USB コネクタ
マイコン(MCU)は同社の STM32F407VGT6 を使用しており、性能とコストのバランスのとれた Cortex-M4 プロセッサが搭載されています。
STM32F4xx MCU の主な仕様
- 最高168MHzで動作
- float のコプロセッサ搭載
- プログラムを格納するフラッシュメモリは1MB
- RAMは192KB
- ADC(アナログデジタル変換器)、DAC(デジタルアナログ変換器)
- SPI、I2C、USARTなどのシリアルインターフェイス
- カメラ(DCMI)インターフェイス
- USB OTG HS
- SDカードホストコントローラ
- Ethernet MAC 10/100
- 動作可能な環境温度は-40℃から+85℃まで
- MCUによっては+105℃まで対応
もし C 言語で開発するなら、ST が無償で提供する開発ツールやライブラリを使ったり、ARM 社が推進する mbed というクラウド上の開発ツールを使ったりできます。ST のコード生成ツールである STM32CubeMX を使うと、オープンソースの USB ドライバ、Ethernetドライバ、SDカードのファイルシステムなどが簡単に組み込めるようです。とはいえ、このボードに Ethernet コネクタや SD カードコネクタは実装されていないので、別途用意する必要はありますが。
なお、マイコンの機能やボード上の付属機能を減らして価格を抑えた ST の Nucleo シリーズや、逆にハイパフォーマンスなマイコン、フルカラー・タッチスクリーン、Ethernet コネクタや microSD コネクタなどを装備した STM32F7 Discovery シリーズなどもあります。Nucleo は Arduino 互換のコネクタを備えていますので、Arduino 向けの一部のシールド(拡張ボード)も使えるようです。
またストロベリー・リナックスが日本で製造している親指サイズで約2千円の Cortex-M3 マイコンボード STBee Mini など、面白そうな製品もあります。
今回紹介する japaric さん作のエコシステムは、ST のマイコンチップだけでなく、NXP セミコンダクターズなどの他社のマイコンチップでも使用できるはずです(試してはいませんが)。この記事で使用するクレートの対応状況は以下の通りです。
-
cortex-m-rt ランタイム
- Cortex-M を搭載する全てのマイコンに対応
-
cortex-m-rtfm リアルタイム・タスクスケジューラ
- Cortex-M0+ 以上を搭載するマイコンに対応
環境構築
前置きが長くなりました。開発に必要な環境(ソフトウェア)を整えましょう。ここでは Linux(Ubuntu 16.04 LTS x86_64)での環境構築手順を紹介します。Arch Linux での手順は japaric さんの記事 を、Windows や macOS の手順は こちら を参照してください。
ソフトウェアのインストール
以下のツールをインストールします。
- Rust Nightly ツールチェイン(デフォルトにするのが楽?)
- ARM クロスリンカ(GNU LD)
- ARM クロスデバッガ(GDB)
- OpenOCD(オンチップデバッガ、プログラムライター)
-
cargo clone
サブコマンド - Xargo
$ rustup default nightly
$ rustc -V
rustc 1.24.0-nightly (7eb64b86c 2017-12-20)
$ sudo apt install binutils-arm-none-eabi gdb-arm-none-eabi openocd
$ arm-none-eabi-ld -V | head -n1
GNU ld (2.26-4ubuntu1+8) 2.26
$ arm-none-eabi-gdb --version | head -n1
GNU gdb (7.10-1ubuntu3+9) 7.10
$ openocd -v 2>&1 | head -n1
Open On-Chip Debugger 0.9.0 (2015-09-02-10:42)
# cargo clone で必要
$ sudo apt install cmake libssl-dev libssh-dev pkg-config
$ cargo install cargo-clone
$ cargo install xargo
$ xargo -V
xargo 0.3.8
cargo 0.25.0-nightly (930f9d949 2017-12-05)
# Xargo が使用
$ rustup component add rust-src
また必須ではありませんが Cargo Edit もインストールしておくと便利です。Dependency を追加するための add
というサブコマンドが使えるようになります。
$ cargo install cargo-edit
OpenOCD の動作確認
続いて動作確認です。OpenOCD (Open On-Chip Debugger) はオープンソースの GDB リモートデバッグサーバー兼プログラムライター(フラッシュメモリライター)で、無償で使用できます。ホスト PC 側で実行し、ボード上のチップとは SWD や JTAG といった物理的なデバッグ・インターフェイスで通信します。
以下の手順で動作確認します。(ボードに触れる際、この季節は特に静電気に注意しましょう)
まず、ボードの JP1
ジャンパと CN3
ジャンパが購入時のままオン(プラグがささっている状態)になっていることを確認します。
USB Mini-B ケーブルで、PC とボードの CN1
(ST-LINK の USB ポート)を接続します。ボードの電源が入り、購入時のままならフラッシュメモリ上のデモプログラムが起動して、4つの LED(グリーン、オレンジ、レッド、ブルー)がルーレットのように点滅を続けます。
lsusb
コマンドでボードが認識されていることを確認します。
$ lsusb
...
Bus 001 Device 013: ID 0483:374b STMicroelectronics ST-LINK/V2.1 (Nucleo-F103RB)
...
OpenOCD サーバーを起動します。
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg
以下のように表示されれば成功です。(最後のメッセージを確認)
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg
Open On-Chip Debugger 0.9.0 (2015-09-02-10:42)
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>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v25 API v2 SWIM v14 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.895283
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
もし繋がらない場合は トラブルシューティング を読んでください。
なお OpenOCD の .cfg ファイルは /usr/share/openocd/scripts/ ディレクトリ配下にあります。別のボードをお使いの時は、その中からボードに合うものを見つけてください。
OpenOCD サーバーは起動したままにします。GDB 用のポートに telnet で接続してみましょう。別のターミナルから以下のようにコマンドを実行します。
$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> reset halt
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000188 msp: 0x20020000
> reset
>
halt
でマイコンが停止し、reset
で再起動します。Control + ]
、Control + d
で telnet を終了します。
トラブルシューティング
ここでは OpenOCD を使うにあたって、私が遭遇した問題と対処方法について説明します。
マイコンが省電力モードに入っている
以下のメッセージが出てリトライが繰り返される時は、マイコンがローパワーモードに入っていてデバッガの接続を受け付けなくなっています。
Error: jtag status contains invalid mode value - communication failure
対処方法ですが、OpenOCD を Ctrl + C で終了して、ボードのリセットボタン(黒いボタン)を押しながら OpenOCD をスタートしてください。接続できたらボタンを離して大丈夫です。
なお、DBGMCU_CR レジスタをいじると、ローパワーモードでデバッグ接続を受け付けることもできるようです。
USB デバイスへのアクセス権がない
以下のようなエラーで失敗する時は、ホスト側でログインしている Linux ユーザーに USB デバイスにアクセスする権限がありません。
Error: libusb_open() failed with LIBUSB_ERROR_ACCESS
Error: open failed
in procedure 'init'
in procedure 'ocd_bouncer'
ユーザーにアクセス権を付与する(適切なグループに入れてログインし直す)か、sudo
を付けて openocd
を実行してください。
ST-LINK のバージョン違い
以下のように USB のエラーなしで open failed する時は、ST-LINK のバージョン違いかもしれません。
Error: open failed
in procedure 'init'
in procedure 'ocd_bouncer'
そんな時は stlink-v2-1.cfg
を stlink-v2.cfg
に変えてみてください。lsusb
コマンドでバージョンが確認できると思います。
STM32F4 Discovery ボードの場合は、発売時期によって搭載されている ST-LINK のバージョンが異なるようです。私のボード STM32F407G-DISC1 は V2-1 だったのですが、V2 だと思い込んでいてはまりました。
Cargo プロジェクトテンプレート
それでは Rust のプロジェクトを作成しましょう。1から設定するのは面倒なので、japaric さんが作成したテンプレート・プロジェクトを使用します。
$ cargo clone cortex-m-quickstart
$ mv cortex-m-quickstart demo && cd $_
これから作成するプログラムは全てこのプロジェクトに入れます。Cargo.toml ファイル内のプロジェクト名と作者名などを変更します。
[package]
authors = ["..."]
name = "demo"
version = "0.1.0"
ターゲットの指定
.cargo/config ファイルにターゲットトリプルを追加します。
$ cat >> .cargo/config <<'EOF'
[build]
target = "thumbv7em-none-eabihf"
EOF
使用する Cortex-M プロセッサに合ったターゲットを指定してください。
-
thumbv6m-none-eabi
- Cortex-M0 と M0+ -
thumbv7m-none-eabi
- Cortex M3 -
thumbv7em-none-eabi
- Cortex M4 と M7、FPU なし -
thumbv7em-none-eabihf
- Cortex M4 と M7、FPU あり
Cargo のインクリメンタルコンパイル機能をオフにする
現状 Cargo のインクリメンタルコンパイル機能は thumb ターゲットと相性が悪いようです。(リンクが変なエラーで失敗するらしい) もしこの機能を使用してるならオフにしましょう。
$ unset CARGO_INCREMENTAL
.gdbinit スクリプトの読み込みを許可する
プロジェクトのルートディレクトリ(Cargo.tomlなどがあるディレクトリ)に、.gdbinit スクリプトが用意されています。GDB がこれを読み込める(safe loading できる)ように設定します。
$ echo 'set auto-load safe-path /' >> ~/.gdbinit
少し紛らわしいのですが、この設定を追記している .gdbinit スクリプトは、プロジェクトのルートディレクトリのものではなくて、ホームディレクトリ直下にあるものです。間違えないようにしてください。
なお特に理由がなければ gdb-dashboard という GDB のレイアウトを使用することをおすすめします。(後ほどスクリーンショットをお見せします)。以下の GitHub リポジトリから .gdbinit スクリプトをダウンロードして、ホームディレクトリ直下に保存してください。
保存後に上の echo
コマンドを実行して、このファイルに safe-path
を追加してください。
メモリ情報の設定
リンカ(LD)が必要とするメモリ情報を設定しましょう。エディタでプロジェクトルートにある memory.x というファイルを開きます。ここにはマイコンに搭載されているフラッシュメモリと RAM に関する情報を書きます。私のボードでは以下のようになりました。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (! rx) : ORIGIN = 0x20000000, LENGTH = 128K
}
これらの情報はマイコンごとに異なります。私がどうやって調べたか簡単に説明しましょう。まず以下のようにしてデータシートを入手しました。
- ボードが入っていたパッケージに書かれていた URL にウェブブラウザーでアクセスし、ST 社のウェブサイトから、STM32F4 Discovery ボードの User Manual(en.DM00039084.pdf)をダウンロード
- User Manual で搭載しているマイコンチップのモデル名を調べる → STM32F407VGT6 MCU
- ST 社のウェブサイトでモデル名で検索し、Data Sheet(en.DM00037051.pdf)をダウンロード
データシートは200ページくらいあります。Memory mapping という章に答えが書かれていました。
これによると FLASH
の開始番地は 0x0800 0000 で、終了番地は 0x080F FFFFです。memory.x ファイルでは ORIGIN
にこの開始番地を書きます。LENGTH
は番地から計算してもいいのですが、データシートの前の方にページに 1024 KB と書かれていますので、それを記入しました。
次に RAM
ですが、このチップには SRAM と CCM の2種類が搭載されています。SRAM は汎用的なメモリで、DMA という仕組みを使うと Cortex-M コアを介さずにペリフェラルとメモリ間でデータを転送できます。一方、CCM(Core Coupled Memory)は Cortex-M コア専用の SRAM で、ペリフェラルとの DMA 転送ができない代わりに高速に動作します。ルックアップテーブルのような頻繁にアクセスするデータを CCM に格納すると性能面で有利になります。
データシートでは以下のようになっていました。
- SRAM:
ORIGIN
は 0x2000 0000、LENGTH
は 112KB + 16KB の合計 128KB - CCM SRAM:
ORIGIN
は 0x1000 0000、LENGTH
は 64KB
しかし japaric さんのクレート(cortex-m-rt)では、現状 CCM を直接サポートしてなさそうだったので、通常の SRAM の情報を RAM
に設定しました。
cortex-m-rt ランタイム使用時のメモリレイアウト
これから作成するプログラムは全て cortex-m-rt クレートを直接または間接的に使用します。このクレートは Rust プログラムを実行するための最小限のランタイム環境を提供します。たとえば以下のようなことをします。
- プログラムが使用するグローバル変数などのデータをフラッシュメモリから SRAM へロードする
- スタック領域をセットアップする
- main 関数を呼び出す
- main 関数終了後の後処理をする(Wait for interrupttion 命令でプロセッサを待機状態にする)
cortex-m-rt 使用時の実行中のプログラムのメモリレイアウトは以下のようになります。
-
text 領域
- プログラムのマシンコードが置かれる
-
FLASH
で示されるフラッシュメモリに格納されており、Cortex-M コアはそこから直接マシンコードを読み出して実行する - STM32 シリーズのフラッシュメモリにはアクセラレータ回路が付いており、コアと同じ速度で、zero wait(待ち時間なし)で読み出せる
-
bss 領域
- 初期化されていないグローバル変数が置かれる
- プログラム起動時にフラッシュメモリから、
RAM
で示される SRAM の先頭のエリアへロードされる
-
data 領域
- 初期化されたグローバル変数や Rust の文字列リテラルなどが置かれる
- bss 領域と共に SRAM の先頭のエリアへロードされる
-
スタック領域
-
RAM
で示される SRAM の終了アドレスから、RAM の先頭に向かって伸びていく
-
ここで一つ注意することがあります。ヒープ領域は用意されません。Vec のような可変長のコンテナを使いたい場合は以下の選択肢があります。
-
heapless クレート を使用する
- ヒープ領域を必要としない(コンパイル時に最大サイズを指定した)データ構造が使用できる
- 標準ライブラリの Vec や RingBuffer と似たメソッドを持ったコンテナを提供
-
alloc-cortex-m クレート を使用する
- Cortex-M 向けのヒープアロケータ
この記事ではこれらのクレートは使用しません。heapless については japaric さんの ブログ記事 を、alloc-cortex-m についてはドキュメント中の サンプルコード を参照してください。
デバッグコンソールで Hello World!
Lチカの前に、普通の Hello world を実行してみましょう。examples/hello.rs ファイルを開くと、以下のような内容になっています。
それぞれが何をするのかわかるように、日本語のコメントを追加しました。
//! Prints "Hello, world!" on the OpenOCD console using semihosting
//!
//! ---
#![feature(used)]
// std クレートの代わりに core クレートを使用する。(thumb ターゲット向けの
// std クレートは存在しない)
#![no_std]
// cortex-m クレートは Cortex-M プロセッサ共通の機能に対する API を提供する。
extern crate cortex_m;
// cortex-m-rt クレートは Cortex-M マイコン向けの最小限のランタイムを提供する。
// extern crate 文を書くだけで(このクレートにリンクするだけで)、ランタイムを
// 使用できる。
extern crate cortex_m_rt;
// Cortex-M プロセッサのセミホスティング機能(デバッグ機能の一種)を使用する。
extern crate cortex_m_semihosting;
use core::fmt::Write;
use cortex_m::asm;
use cortex_m_semihosting::hio;
// main関数では標準出力としてセミホスティングが提供する出力先(OpenOCD の
// コンソール)を使用し、writeln!() でメッセージを表示する。
fn main() {
let mut stdout = hio::hstdout().unwrap();
writeln!(stdout, "Hello, world!").unwrap();
}
// このプログラムでは割り込みを活用しないので、全てを catch するダミー
// ハンドラを登録する。
#[link_section = ".vector_table.interrupts"]
#[used]
static INTERRUPTS: [extern "C" fn(); 240] = [default_handler; 240];
extern "C" fn default_handler() {
// 割り込みを受けるとデバッガがここで停止する。
asm::bkpt();
}
このプログラムは ARM マイコンに装備されているセミホスティングという仕組みを利用しています。これにより OpenOCD のコンソールにデバッグメッセージを表示できます。
ビルドしてみましょう。
$ xargo build --example hello
# ELF ファイルのヘッダを表示
$ arm-none-eabi-readelf -A target/thumbv7em-none-eabihf/debug/examples/hello
Attribute Section: aeabi
File Attributes
Tag_conformance: "2.09"
Tag_CPU_arch: v7E-M
Tag_CPU_arch_profile: Microcontroller
Tag_THUMB_ISA_use: Thumb-2
Tag_FP_arch: VFPv4-D16
Tag_ABI_PCS_GOT_use: direct
Tag_ABI_FP_denormal: Needed
Tag_ABI_FP_exceptions: Needed
Tag_ABI_FP_number_model: IEEE 754
Tag_ABI_align_needed: 8-byte
Tag_ABI_align_preserved: 8-byte, except leaf SP
Tag_ABI_HardFP_use: SP only
Tag_ABI_VFP_args: VFP registers
Tag_ABI_optimization_goals: Prefer Debug
Tag_CPU_unaligned_access: v6
Tag_FP_HP_extension: Allowed
Tag_ABI_FP_16bit_format: IEEE 754
マイコン上で実行します。OpenOCD サーバーが動作している状態で以下を実行します。
$ arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/examples/hello
これによりプロジェクトのルートディレクトリにある .gdbinit スクリプトが実行され、プログラムのエントリーポイントで Cortex-M プロセッサが停止します。
Source に表示されているように、プロセッサのリセット後に最初に呼ばれるのは cortex-m-rt クレートの reset_handler()
です。これから SRAM 上の bss と data 領域を初期化することもわかります。
main 関数にブレークポイントを設定して、実行を再開します。
> tbreak hello::main
> continue
なお continue
にはエイリアスとして c
が設定されています。
main 関数で停止します。
再度 c
で実行を再開します。writeln!()
が実行され、OpenOCD のターミナルに Hello, world! と表示されます。
Info : accepting 'gdb' connection on tcp/3333
Info : device id = 0x10076413
Info : flash size = 1024kbytes
semihosting is enabled
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000400 msp: 0x20020000, semihosting
target state: halted
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x20000042 msp: 0x20020000, semihosting
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000400 msp: 0x20020000, semihosting
Info : halted: PC: 0x08000402
Hello, world!
もし Hello, world! が表示されず、途中で exception で落ちてしまうようなら、おそらく memory.x の設定が間違っていてランタイムのセットアップに失敗しているのだと思われます。データシートを参照して修正してください。
フルデバイスサポート
Lチカに進む前に、デバイスサポートクレートを追加しましょう。マイコンチップに搭載されているペリフェラルにアクセスするには、レジスタと呼ばれるプロセッサのメモリ空間にマッピングされた領域を使用します。(ここでのレジスタという用語は、一般的なプロセッサの話に登場するレジスタとは異なります)。レジスタの情報は全てデータシートに書かれていますが、それを目で確認しながらプログラムを書くのは面倒なだけでなく、バグの原因にもなります。
デバイスサポートクレートには、対象のマイコンに搭載されたペリフェラルにアクセスするために必要となる構造体やメソッドが全て定義されています。たとえば STM32F407 MCU なら stm32f40x クレート が使えます。
このクレートには単一の lib.rs ファイルがあって、その内容はなんと40万行もあります。このファイルは System View Description(SVD)というファイルから、svd2rust という Rust で書かれたコマンドラインツールを使って自動生成されています。
SVD ファイルはデータシートを XML 化したもので、たとえば この GitHub レポジトリ に集められています。ここには500個ほどの SVD ファイルがありますが、もし自分が使っているマイコンに合うものがなかったら、メーカーに問い合わせれば送ってもらえるようです。
ところで現時点の stm32f40x クレートは割り込みハンドラの定義の部分が、この記事の後の方で使う cortex-m-rtfm クレートと相性が悪いようで、プログラムがコンパイルできませんでした。どちらのクレートを修正するべきか判断できなかったので、とりあえず stm32f40x クレートの方をフォークして、コンパイルできるように修正しました。今回はそのフォークを使用します。
Cargo.toml に以下の内容を追加します。
[dependencies.stm32f40x]
git = "https://github.com/tatsuya6502/stm32f40x-rs"
branch = "revert-irq"
features = ["rt"]
なおデバイスクレートの作成方法は、japaric さんの ブログ記事 を参照してください。実は素の SVD を変換するだけでは、必要なメソッドがなかったりします。SVD にパッチをあてて項目をいくつか追加する必要があります。(パッチの例)
Lチカ
ようやく本当の Hello world プログラムを書く準備が整いました。このプログラムでは STM32F4 Discovery ボードにある4つの LED のうち、ブルーの LED を1秒間隔で点滅させます(1秒点灯、1秒消灯の繰り返し)
(ムービーを貼ろうと思ったら、私が住んでいる中国上海からビデオを共有するうまい方法が見つからなかったので、スクリーンキャプチャでお茶を濁しておきます)
src ディレクトリ配下に bin ディレクトリを作り、blinky.rs ファイルを作成します。内容についてはソースコード中のコメントで説明します。
#![no_std]
// cast クレートはオーバーフローなどのチェック付きの型キャストを提供する。
// これを使わずに、Rust 組み込みのキャスト(as u32 など)を使ってもよい。
extern crate cast;
extern crate cortex_m;
extern crate cortex_m_rt;
// デバイスサポートクレートを使用する。
extern crate stm32f40x;
use core::u16;
use cast::{u16, u32};
use stm32f40x::{GPIOD, RCC, TIM7};
mod frequency {
/// TIM7(タイマー7)が接続されている APB1 バスの動作周波数。
pub const APB1: u32 = 8_000_000;
}
/// TIM7 にアップデートイベントを発行させる周期(1秒に1回)
const FREQUENCY: u32 = 1;
#[inline(never)]
fn main() {
// クリティカルセクション。このクロージャ内は割り込み禁止(non-preemptable)
cortex_m::interrupt::free(
|cs| {
// 初期化フェーズ
// クリティカルセクション内ではペリフェラルに排他的にアクセスできる。
let gpiod = GPIOD.borrow(cs);
let rcc = RCC.borrow(cs);
let tim7 = TIM7.borrow(cs);
// 使用するペリフェラルにクロックを供給する(電源をオンにする)
rcc.ahb1enr.modify(|_, w| w.gpioden().enabled());
rcc.apb1enr.modify(|_, w| w.tim7en().enabled());
// ブルーの LED が接続された RD15(D15ピン)を出力用に設定する。
gpiod.moder.modify(|_, w| w.moder15().output());
// TIM7 が1秒周期でタイムアウトするように設定する。
let ratio = frequency::APB1 / FREQUENCY;
let psc = u16((ratio - 1) / u32(u16::MAX)).unwrap();
tim7.psc.write(|w| w.psc().bits(psc));
let arr = u16(ratio / u32(psc + 1)).unwrap();
tim7.arr.write(|w| w.arr().bits(arr));
tim7.cr1.write(|w| w.opm().continuous());
// TIM7 をスタート
tim7.cr1.modify(|_, w| w.cen().enabled());
// アプリケーションロジック
let mut state = false;
loop {
// TIM7からのアップデートイベントが起きるまで待つ(busy wait)
while tim7.sr.read().uif().is_no_update() {
// no-op
}
// アップデートイベントフラグをクリアする。
tim7.sr.modify(|_, w| w.uif().clear());
// LED の点滅状態を表す変数を反転する。
state = !state;
// LED を点滅させる
if state {
// 点灯
gpiod.bsrr.write(|w| w.bs15().set());
} else {
// 消灯
gpiod.bsrr.write(|w| w.br15().reset());
}
}
},
);
}
Cargo.toml に cast クレートを追加します。
[dependencies.cast]
default-features = false
version = "0.2.2"
このプログラムは japaric さんの ブログ記事 に掲載されているプログラムをベースにしています。ただ、その記事では別のボード STM32F3 Discovery を使用しており、マイコンのモデルも LED が接続されているポートや内部バスの名称も異なります。
そこでデータシートを読むことになるのですが、初心者にはどうやって読んだらいいのやら。幸いこちらの日本語ブログ記事で必要な情報が入手できました。
私と同じボードで Lチカをするための手順が、データシートの読み方から C 言語によるペリフェラルの初期化のしかたまで解説されており、至れり尽くせりです。
ビルドして実行しましょう。最近の Xargo なら、run
コマンドで gdb の起動まで進みます。プログラムのエントリーポイントで停止しますので、c
コマンドで実行を再開します。
$ xargo run --bin blinky
...
>>> c
ビジー・ウエイト
さて、このプログラムには解決すべき課題があります。
// TIM7からのアップデートイベントが起きるまで待つ(busy wait)
while tim7.sr.read().uif().is_no_update() {
// no-op
}
1秒間隔のイベントを待つために全力でループを回しています。これでは電力を無駄に消費してしまいます。この後の章では cortex-m-rtfm クレートを導入して、この課題を解決します。
cortex-m-rtfm - プリエンプティブ・マルチタスクを実現する軽量フレームワーク
cortex-m-rtrm クレートは Cortex-M シリーズでイベント駆動のプリエンプティブ・マルチタスクを実現するフレームワークです。他の cortex-m-* クレートと同様、japaric さんが設計しました。2017年5月ごろ発表された後、Rust コミュニティからのフィードバックを受け、7月末には v2 として API が大幅に更改されました。この記事では v2 API を使用します。
プリエンプティブ・マルチタスク(preemptive multitasking)は Linux、macOS、Windows などの OS で採用されているマルチタスク手法です。preemption はタスク(別名、スレッド)の実行権を横取りできることを意味します。ハードウェアタイマーや IO などによる割り込みを機に OS がタスクの実行に介入し、他のタスクへ強制的にコンテキストスイッチ(切り替え)を起こします。
これに対して、ノン・プリエンプティブ・マルチタスク(別名、協調的マルチタスク)という手法もあります。これは個々のアプリケーションが適切なタイミングで自主的に OS に制御を渡し、コンテキストスイッチを促すというものです。(アプリから定期的に yield()
を呼び出すようなやり方です) 現在の macOS(Mac OS X)は前者を採用していますが、その前身の Macintosh System 7 から 9 までは後者を採用していました。(それより前の System では、マルチタスク自体が備わっていませんでした)
15年くらい昔の話ですが、私はデザインの仕事で Macintosh System 8 や 9 を使っていました。その時の経験からすると、協調的マルチタスクは応答性のいい手法とはいえませんでした。お行儀のよいアプリケーションばかりなら良いのですが、重いアプリがフォアグラウンドになるとアプリ切り替えの応答が悪化し、いったん切り替わるとその重いアプリの処理がほとんど進まなくなるなど、うまく動かないことが多かったです。当時のコンピュータは今よりもずっと非力だったので悪い点が目立ったかもしれません。
話が逸れましたが、マイコンでは機械制御だけでなく、音楽プレーヤーのようなものでも精密なタスクの実行タイミングが要求されます。もしメニューボタンを押すたびに音飛びするプレーヤーがあったら使いたくありませんよね。そのためにはタスクの実行時間を予測可能にしたり、優先度が高く設定されたタスクを遅延させずに実行することが求められます。これらの要求に応えるためには、プリエンプティブ・マルチタスクは必須といえるでしょう。
なお Linux のようなサーバーやデスクトップ PC 向けの OS のタスクのスケジューリングと組み込み用途のマイコンでのスケジューリングは要件が異なります。前者はたくさんのタスクをまんべんなく実行することが求められますが、後者ではタスクが想定した時間内に終わること(リアルタイム性)が求められます。たとえばデスクトップ向けの OS では普段一瞬で終わる処理が、ウィルスチェックをしている時には遅くなるといったことが起こります。これではリアルタイム性があるとはいえません。リアルタイム性のあるスケジューリングをするには、優先度の高いタスクが優先度の低いタスクに邪魔されない(preemption が起きない)ようにしなければなりません。
ところで Cortex-M くらいのプロセッサになると、既存の多くのリアルタイム OS(RTOS)が動作します。RTOS は有償のものだけでなく、オープンソースで無償で使えるものもいくつかあります。RTOS はマルチタスクの機能だけでなく、API を抽象化することで異なるマイコン間での移植性を高められるなど様々なメリットがあります。その一方でメモリの使用量が増えるなど、リソースにとってはあまり優しくありません。
一方、cortex-m-rtfm クレートは RTOS ほどの機能はありませんが、Cortex-M シリーズに特化した設計を採用しており、プリエンプティブ・マルチタスクのスケジューリングやコンテキストスイッチの負荷が軽いのが特徴のひとつです。
たとえばタスク・スケジューリングは Cortex-M プロセッサに内蔵された NVIC(Nested Vector Interruption Controller)という割り込みコントローラで実現されており、ソフトウェアによる制御は行われません。また全てのタスクで1つのコールスタックを共有しており、コンテキストスイッチのオーバーヘッドは実測で関数呼び出しのオーバーヘッドの約2倍に抑えられています。(リアルタイム性のために優先度の高いタスクの実行が終わるまでは優先度の低いタスクの実行は中断されるので、1つのコールスタックでコンテキストスイッチを実現できます)
さらに Rust の型システムを活用することで、効率的かつデータ競合安全な環境を実現しています。このようなことを既存の RTOS で実現するのは難しいでしょう。
デバッグコンソールでもう一度 Hello World!
手始めに最初の Hello world プログラムを cortex-m-rtfm 向けに書き換えましょう。
$ cargo add cortex-m-rtfm
src/bin/ 配下に hello_rtfm.rs ファイルを作成します。
// proc macro を使用可能にする。
#![feature(proc_macro)]
#![no_std]
extern crate cortex_m;
// rtfm へのリネームは必須。
extern crate cortex_m_rtfm as rtfm;
extern crate cortex_m_semihosting as semihosting;
extern crate stm32f40x;
use core::fmt::Write;
use rtfm::app;
use semihosting::hio;
// app! マクロでは、デバイスとしてデバイスサポートクレートの名前を書く。
app! {
device: stm32f40x,
}
// init() 関数はプログラムの起動時に1回だけ呼ばれる。
fn init(_p: init::Peripherals) {
// OpenOCD コンソールに "Hello," と表示する。
writeln!(hio::hstdout().unwrap(), "Hello,").unwrap();
}
// idle() 関数は他に実行するタスクがない時に実行される。戻り値の型が
// ! であることに注意。この関数は終了してはいけない。
fn idle() -> ! {
// OpenOCD コンソールに "world!" と表示する。
writeln!(hio::hstdout().unwrap(), "world!").unwrap();
loop {
// 割り込みがあるまで動作を停止する。loop により繰り返す。
rtfm::wfi();
}
}
割り込みはフレームワークが処理するので、割り込みハンドラ(INTERRUPTS
static 変数や default_handler()
関数)は不要です。
実行してみましょう。
$ xargo run --bin hello_rtfm
...
>>> c
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg
...
xPSR: 0x01000000 pc: 0x08000188 msp: 0x20020000, semihosting
Info : halted: PC: 0x0800018a
Hello,
world!
エコな Lチカ
では先ほどの Lチカでの課題を解決しましょう。そこでは1秒間隔のイベントを待つために全力でループを回していました。
// TIM7からのアップデートイベントが起きるまで待つ(busy wait)
while tim7.sr.read().uif().is_no_update() {
// no-op
}
LED を点滅させるロジックを「タスク」と呼ばれる作業単位として定義し、タイマーからの1秒間隔のイベントが発生したら、それが呼び出されるように設定します。これにより MCU が無駄な電力を使わなくなります。
タスクを定義する
プログラムの前半ではタスクとタスクが使用するリソースを定義します。なおタイマーとして、先ほどの TIM7 ではなく、SYS_TICK 割り込みを生成するシステムタイマー(SYST)を使用します。
#![feature(proc_macro)]
#![no_std]
extern crate cortex_m;
extern crate cortex_m_rtfm as rtfm;
extern crate stm32f40x;
use cortex_m::peripheral::SystClkSource;
use rtfm::{app, Threshold};
app! {
device: stm32f40x,
// タスクが使用するリソースを定義する。
resources: {
static BLUE_LED_ON: bool = false;
},
tasks: {
// タスクとして、SYS_TICK 割り込みで起動されるタスクを定義する。
SYS_TICK: {
// 関数名は sys_tick。
path: sys_tick,
// 以下のリソースを関数内で使用する。
resources: [
BLUE_LED_ON,
GPIOD, // stm32f40x::GPIOD
],
},
}
}
init()
とタスクの実装
init()
では先ほどの blinky.rs と同じように LED が接続されている GPIO D とシステムタイマーを設定します。
idle()
関数では loop { rtfm::wfi() }
でプロセッサを待機状態にします。
// init() 関数はグローバルクリティカルセクションとして実行される。
// preemption は起こらないので、リソースを排他的に使用できる。
fn init(p: init::Peripherals, _r: init::Resources) {
// GPIO D の電源をオンにする。
p.RCC.ahb1enr.modify(|_, w| w.gpioden().enabled());
// PD15ピンを出力に設定する。
p.GPIOD.moder.modify(|_, w| w.moder15().output());
// システムタイマーを設定して、1秒ごとに SYS_TICK 割り込みを起こす。
p.SYST.set_clock_source(SystClkSource::Core);
p.SYST.set_reload(8_000_000); // 1秒
p.SYST.enable_interrupt();
p.SYST.enable_counter();
}
// idle ではなにもしない。
fn idle() -> ! {
loop {
rtfm::wfi();
}
}
// SYS_TICK 割り込みで起動されるタスク。
fn sys_tick(_t: &mut Threshold, r: SYS_TICK::Resources) {
// このタスクとリソースを共有している優先度の高いタスクがないため
// クリティカルセクションなしで、リソースにアクセスできる。
**r.BLUE_LED_ON = !**r.BLUE_LED_ON;
// ブルーの LED を点滅させる。
if **r.BLUE_LED_ON {
r.GPIOD.bsrr.write(|w| w.bs15().set());
} else {
r.GPIOD.bsrr.write(|w| w.br15().reset());
}
}
SYS_TICK 割り込みで起動される sys_tick()
関数は blinky.rs と同じ方法で LED を点滅されますが、1つだけ違いがあります。それは blinky.rs ではこの処理がクリティカルセクション内で実行されていたため、割り込みを受け付けません。一方、 sys_tick()
関数ではクリティカルセクションが必要ありません。なぜなら、Rust コンパイラが、このプログラムではデータ競合が発生しないことを知っているからです。(データ競合が起こるケースは次の章で紹介します)
タスクの実行が終わったら、idle()
関数の実行が再開され、結果的に次の SYS_TICK 割り込みがあるまでプロセッサが待機状態になります。エコ(省電力)なプログラムになりました。
マルチタスクな Lチカ
最後にプリエンプティブ・マルチタスクの例を紹介します。今度はブルーとオレンジの LED に加え、User ボタン(水色のボタン)を使用します。
- プログラム起動。ブルーの LED が1秒間隔で点滅
- User ボタンを押すと、オレンジ の LED の点滅に切り替わる
- もう一度 User ボタンを押すと、ブルーの LED の点滅に戻る
- 以降、2と3を繰り返す
LED の状態を表すオブジェクトを定義
まずプログラムの前半です。どの LED を点滅しているか管理しやすいように LEDState
という構造体を導入しました。
#![feature(proc_macro)]
#![no_std]
extern crate cortex_m;
extern crate cortex_m_rtfm as rtfm;
extern crate stm32f40x;
use cortex_m::peripheral::SystClkSource;
use rtfm::{app, Threshold};
// LED の色
pub enum LEDColor {
Blue,
Orange,
}
// LED の状態
pub struct LEDState {
color: LEDColor,
on: bool,
}
impl LEDState {
fn is_on(&self) -> bool {
self.on
}
// 別の色の LED へ切り替える
fn next_color(&mut self) {
match self.color {
LEDColor::Blue => self.color = LEDColor::Orange,
LEDColor::Orange => self.color = LEDColor::Blue,
}
}
// LED を点滅させる
pub fn blink(&mut self, gpio: &mut ::stm32f40x::GPIOD) {
self.on = !self.on;
match self.color {
LEDColor::Blue => {
if self.on {
gpio.bsrr.write(|w| w.bs15().set());
} else {
gpio.bsrr.write(|w| w.br15().reset());
}
},
LEDColor::Orange => {
if self.on {
gpio.bsrr.write(|w| w.bs13().set());
} else {
gpio.bsrr.write(|w| w.br13().reset());
}
}
}
}
}
リソースやタスクの定義
次に app 定義です。
app! {
device: stm32f40x,
resources: {
// LED の状態
static LED_STATE: LEDState = LEDState {
color: LEDColor::Blue,
on: false,
};
// このリソースは後ほど2つのタスクで共有される
static CHANGE_COLOR_REQUESTED: bool = false;
},
tasks: {
SYS_TICK: {
path: sys_tick,
priority: 2, // 優先度:高
resources: [
LED_STATE,
CHANGE_COLOR_REQUESTED,
GPIOD, // stm32f40x::GPIOD
],
},
// User ボタンが接続された割り込みライン EXTI0 への割り込みを
// 処理するタスク。
EXTI0: {
path: handle_exti0,
priority: 1, // 優先度:低
resources: [
CHANGE_COLOR_REQUESTED,
EXTI, // stm32f40x::EXTI
GPIOA, // stm32f40x::GPIOA
],
},
}
}
EXTI0
割り込みで起動されるタスクを追加しました。これにより User ボタンが押された時に handle_exti0()
関数が呼ばれます。
今回は説明のために2つのタスクに異なる優先度(priority)を設定しました。EXTI0
に設定された 1
が最低で、数字が大きい方が優先度が高くなります。つまり 2
と設定された LED 点滅タスク SYS_TICK
→ sys_tick()
の方が高い優先度を持ちます。なおデフォルトの優先度は 1
で、最大値はプロセッサによって異なります。
このようにタスクが複数あって優先度が異なる場合、優先度の低いタスクは優先度の高いタスクに preempt されます。つまり handle_exti0()
の実行中にシステムタイマーイベント(SYS_TICK
)が起きると、handle_exti0()
の実行が中断され、sys_tick()
が実行されます。そして sys_tick()
が終了すると、handle_exti0()
の実行が再開します。
ちなみに、もし2つのタスクの優先度が同じだったら preemption は起きません。お互いに相手のタスクが終わった後に実行が開始されます。
リソースに static 変数 CHANGE_COLOR_REQUESTED: bool
を追加しました。このリソースは handle_exti0()
と sys_tick()
で共有されます。handle_exti0()
は sys_tick()
によって preempt されますので、データ競合が起きないように守る必要があります。
init()
と idle()
の実装
init()
と idle()
の実装は以下のようになります。今までの延長ですね。
fn init(p: init::Peripherals, _r: init::Resources) {
// GPIO A と GPIO D の電源をオンにする。
p.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());
p.RCC.ahb1enr.modify(|_, w| w.gpioden().enabled());
// オレンジ LED(PD13)とブルー LED(PD15)を出力に設定する
p.GPIOD.moder.modify(|_, w| w.moder13().output());
p.GPIOD.moder.modify(|_, w| w.moder15().output());
// システムタイマーを設定して、1秒ごとに SYS_TICK 割り込みを起こす。
p.SYST.set_clock_source(SystClkSource::Core);
p.SYST.set_reload(8_000_000); // 1秒
p.SYST.enable_interrupt();
p.SYST.enable_counter();
// User ボタン(PA0)を押した時に割り込みを発生させる。
// IMR(Interrup Mask Register)で PA0 からの割り込みを有効にする。
p.EXTI.imr.modify(|_, w| w.mr0().set_bit());
// FTSR(Falling Trigger Selection Register)で、PA0 を押した時
// (ピンの信号レベルが High から Low に変わった時)に EXTI0 割り込みを
// 発生させる。
p.EXTI.ftsr.modify(|_, w| w.tr0().set_bit());
// FTSR の逆向きのトリガーとなる RTSR(Rising Trigger Selection
// Register)もあるが、今回は使用しない。
}
fn idle() -> ! {
loop {
rtfm::wfi();
}
}
STM32F4xx の割り込み機構に関してはこちらのスライドでだいたい理解できました。
2017年12月29日 追記: Cortex-M シリーズのアーキテクチャについて、以下の日本語記事を見つけました。網羅的でありながらとてもわかりやすく、大変参考になりました。
初心者講座 には RTOS 編などもあり、私には役立つ情報ばかりです。
タスクの実装
最後にタスクの実装です。handle_exti0()
では CHANGE_COLOR_REQUESTED
をデータ競合から守る必要があります。
fn sys_tick(_t: &mut Threshold, r: SYS_TICK::Resources) {
// 色変更のリクエストがきていて、LED が消灯していたら、リクエストを処理する。
if **r.CHANGE_COLOR_REQUESTED && !r.LED_STATE.is_on() {
r.LED_STATE.next_color();
**r.CHANGE_COLOR_REQUESTED = false;
}
r.LED_STATE.blink(&mut **r.GPIOD);
}
fn handle_exti0(t: &mut Threshold, mut r: EXTI0::Resources) {
use rtfm::Resource;
// PR(ペンディングリクエスト)レジスターで、PA0 からの割り込みなのか確認する。
if r.EXTI.pr.read().pr0().bit_is_set() {
// ここになにか時間のかかる処理があると想定
// ...処理...
// 時間のかかる処理が終わったら、CHANGE_COLOR_REQUESTED リソースを
// true にする。このリソースは、優先度の高い SYS_TICK タスクと共有して
// いるので、ここでは直接アクセスできない。claim_mut() でローカル
// クリティカルセクションに入ってからアクセスする。
r.CHANGE_COLOR_REQUESTED.claim_mut(t, |change_color_requested, _t| {
**change_color_requested = true;
});
// 割り込みを処理済みにする。
r.EXTI.pr.write(|w| w.pr0().set_bit());
}
}
コメントに書いた通り、preempt される側の handle_exti0
では、CHANGE_COLOR_REQUESTED
リソースのデータ競合を避けるために claim_mut()
でローカル・クリティカルセクションに入ってからリソースにアクセスする必要があります。
ローカル・クリティカルセクションに入っている間は、対象のリソースを共有するタスクからの preemption は防がれます。これによりそのリソースの内容が複数のタスクによって壊される(不整合な状態になる)ことが避けられます。もしそれを忘れて handle_exti0
側で claim_mut()
を使わずに CHANGE_COLOR_REQUESTED
にアクセスしようとすると、コンパイルエラーになります。
このクリティカルセクションは ローカル と呼ばれていることから想像できるように、全ての preemption をブロックするわけではありません。別のリソースに対する preemption は行われますので、アプリケーションのリアルタイム性は最大限に維持できます。
バイナリのサイズ
最後に生成されたバイナリの大きさを確認しましょう。GNU size コマンドでわかります。
$ xargo build --release --bin blinky3_rtfm
$ arm-none-eabi-size target/thumbv7em-none-eabihf/release/blinky3_rtfm
text data bss dec hex filename
1440 0 4 1444 5a4 target/thumbv7em-none-eabihf/release/blinky3_rtfm
わずか 1,444 バイトです。これは OS なしのベアボーン環境であることを思い出してください。プリエンプティブ・マルチタスクなアプリをマイコンで動かすための全ての情報がここに記録されています。
おわりに
いかがでしたか? この記事をきっかけに Rust による Cortex-M プログラミングに興味を持っていただけたらと思います。
この記事では紹介しきれななかったことがたくさんあります。たとえば、
- あるタスクから、別のタスクの実行をリクエストする
- シリアルインターフェイス(USART)で PC と通信する
- シリアル - USB 変換ケーブルは買ってあったが時間不足で試せず
- ゼロコスト抽象化の威力を紹介する
- release モードでビルドしたバイナリを逆アセンブルする
- ボードクレートを紹介する
- デバイスサポートクレートは MCU レベルの構造体やメソッドを提供するが、ボードクレートは MCU が載ったボード特有のデバイスに対する構造体やメソッドを提供する。これを使うと、ボードに実装された LED はどの GPIO につながっているか調べなくても使える
これらについては japaric さんのブログで詳しく紹介されていますので、ぜひ読んでみてください。
- Embedded in Rust: http://blog.japaric.io/tags/arm-cortex-m/
最後にお知らせです。Slack に日本語の Rust コミュニティがあります。Rust に関する日本語での情報共有にぜひご活用ください。もちろん Cortex-M ベアボーンに関する質問も OK です。
- サインアップページ: http://rust-jp.herokuapp.com/
それでは、Happy Holidays!