QEMU
まず、Cortex-M3 マイクロコントローラーである LM3S6965 向けのプログラムを 書き始めます。 これを最初のターゲットに選んだのは、QEMU を使ってエミュレートできるためで、 このセクションではハードウェアをいじる必要がなく、ツール類と 開発プロセスに集中できるからです。
重要 このチュートリアルでは、プロジェクト名として “app” という名前を使います。 “app” という単語を見かけたら、その箇所をあなたが選んだ プロジェクト名に置き換えてください。あるいは、プロジェクト名を “app” にして、 置き換えを不要にすることもできます。
標準的ではない Rust プログラムを作成する
新しいプロジェクトを生成するために、cortex-m-quickstart のプロジェクトテンプレートを
使います。作成されるプロジェクトには、最小限のアプリケーションが含まれます。これは
新しい組み込み Rust アプリケーションのよい出発点になります。さらに、プロジェクトには
examples ディレクトリも含まれており、いくつかの独立したアプリケーションを通して、
組み込み Rust の主要な機能の一部を示しています。
cargo-generate を使う
まず cargo-generate をインストールします
cargo install cargo-generate
次に新しいプロジェクトを生成します
cargo generate --git https://github.com/knurling-rs/app-template
Project Name: app
Creating project called `app`...
Done! New project created /tmp/app
cd app
git を使う
リポジトリをクローンします
git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd app
その後、Cargo.toml ファイル内のプレースホルダーを埋めます
[package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "app"
version = "0.1.0"
# ..
[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "app"
test = false
bench = false
どちらも使わない場合
cortex-m-quickstart テンプレートの最新スナップショットを取得して展開します。
curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd app
あるいは、cortex-m-quickstart をブラウザーで開き、緑色の “Clone or
download” ボタンをクリックし、その後 “Download ZIP” をクリックしてもかまいません。
その後、「git を使う」版の後半で行ったのと同じように、
Cargo.toml ファイル内のプレースホルダーを埋めます。
プログラムの概要
便宜上、src/main.rs のソースコードの中で最も重要な部分を以下に示します:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// ここにコードを書きます
}
}
このプログラムは標準的な Rust プログラムとは少し異なるので、 詳しく見ていきましょう。
#![no_std] は、このプログラムが標準クレートである
std に リンクしない ことを示します。代わりに、そのサブセットである
core クレートにリンクします。
#![no_main] は、このプログラムが大半の Rust プログラムで使われる標準の main
インターフェースを使わないことを示します。no_main を使う主な(しゃれではなく)理由は、
no_std コンテキストで main インターフェースを使うには
nightly が必要だからです。
use panic_halt as _;。このクレートは、プログラムの panic 時の挙動を定義する
panic_handler を提供します。これについては、本書の
パニックの章でさらに詳しく説明します。
#[entry] は、cortex-m-rt クレートが提供する属性で、
プログラムのエントリポイントを示すために使われます。標準の main
インターフェースを使っていないため、プログラムのエントリポイントを示す
別の方法が必要であり、それが #[entry] です。
fn main() -> !。このプログラムはターゲット
ハードウェア上で動作する 唯一 のプロセスなので、終了してほしくありません!
そのことをコンパイル時に保証するために、発散関数(関数シグネチャ中の -> !
の部分)を使っています。
クロスコンパイル
まず最初に、ターゲットマイクロコントローラー、この場合は
LM3S6965 のメモリーレイアウトが必要です。そうでないと、ビルド時にイメージのリンクに失敗します。プロジェクトの
ルートに memory.x という名前のファイルを作成し、以下の内容を貼り付けてください:
MEMORY
{
/* 注 1 K = 1 KiBi = 1024 バイト */
/* TODO これらのメモリー領域を、使用するデバイスのメモリーレイアウトに合わせて調整してください */
/* これらの値は、QEMU がエミュレートできる数少ないデバイスの 1 つである LM3S6965 に対応しています */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* ここにコールスタックが割り当てられます。 */
/* スタックはフルディセンディング型です。 */
/* この変数を使って、コールスタックと静的
変数を異なるメモリー領域に配置したい場合があります。以下にデフォルト値を示します */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* このシンボルを使うと .text セクションの配置位置をカスタマイズできます */
/* 省略した場合、.text セクションは .vector_table
セクションの直後に配置されます */
/* これは、ベクターテーブルの直後に何らかの設定を格納する
マイクロコントローラーでのみ必要です */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* 非初期化変数をカスタム RAM 領域に配置する例。 */
/* これは、上で RAM2 領域を定義しており、Rust
ソースで、そこに配置したいデータに `#[link_section = ".ram2bss"]` 属性を追加している
ことを前提としています。 */
/* このセクションはランタイムによってゼロ初期化されない点に注意してください! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
次のステップは、Cortex-M3 アーキテクチャ向けにプログラムを
クロスコンパイルすることです。コンパイルターゲット($TRIPLE)が
何であるべきかわかっていれば、cargo build --target $TRIPLE を実行するだけで済みます。
幸いなことに、テンプレート内の .cargo/config.toml にその答えがあります:
tail -n6 .cargo/config.toml
[build]
# これらのコンパイルターゲットのうち 1 つを選んでください
# target = "thumbv6m-none-eabi" # Cortex-M0 と Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 と Cortex-M7(FPU なし)
# target = "thumbv7em-none-eabihf" # Cortex-M4F と Cortex-M7F(FPU あり)
Cortex-M3 アーキテクチャ向けにクロスコンパイルするには
thumbv7m-none-eabi を使う必要があります。このターゲットは、Rust
ツールチェーンをインストールしただけでは自動的には追加されないので、まだであれば、
このタイミングでツールチェーンに追加しておくとよいでしょう:
rustup target add thumbv7m-none-eabi
thumbv7m-none-eabi コンパイルターゲットはすでに
.cargo/config.toml ファイルでデフォルトに設定されているため、
以下の 2 つのコマンドは同じ意味になります:
cargo build --target thumbv7m-none-eabi
cargo build
確認
これで、target/thumbv7m-none-eabi/debug/app にネイティブではない ELF バイナリができました。これを
cargo-binutils を使って調べることができます。
cargo-readobj を使うと ELF ヘッダーを表示でき、これが ARM
バイナリであることを確認できます。
cargo readobj --bin app -- --file-headers
以下に注意してください。
--bin appはtarget/$TRIPLE/debug/appにあるバイナリを調べるための糖衣構文です--bin appは必要に応じてバイナリを(再)コンパイルします
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: 0x0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x405
Start of program headers: 52 (bytes into file)
Start of section headers: 153204 (bytes into file)
Flags: 0x5000200
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 19
Section header string table index: 18
cargo-size は、バイナリのリンカーセクションのサイズを表示できます。
cargo size --bin app --release -- -A
最適化されたバージョンを調べるために --release を使います
app :
section size addr
.vector_table 1024 0x0
.text 92 0x400
.rodata 0 0x45c
.data 0 0x20000000
.bss 0 0x20000000
.debug_str 2958 0x0
.debug_loc 19 0x0
.debug_abbrev 567 0x0
.debug_info 4929 0x0
.debug_ranges 40 0x0
.debug_macinfo 1 0x0
.debug_pubnames 2035 0x0
.debug_pubtypes 1892 0x0
.ARM.attributes 46 0x0
.debug_frame 100 0x0
.debug_line 867 0x0
Total 14570
ELF リンカーセクションのおさらい
.textにはプログラム命令が含まれます.rodataには文字列のような定数値が含まれます.dataには、初期値がゼロではない静的に確保された変数が含まれます.bssにも、初期値がゼロである静的に確保された変数が含まれます.vector_tableは、ベクターテーブル(割り込みテーブル)を格納するために使用する、標準外のセクションです.ARM.attributesと.debug_*セクションにはメタデータが含まれており、バイナリを書き込む際にターゲットへロードされることはありません。
重要: ELF ファイルにはデバッグ情報のようなメタデータが含まれているため、ディスク上のサイズは、そのプログラムをデバイスに書き込んだときに占有する容量を正確には反映しません。バイナリの実際の大きさを確認するには、必ず cargo-size を使ってください。
cargo-objdump を使うと、バイナリを逆アセンブルできます。
cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex
注記 上記のコマンドで
Unknown command line argumentというエラーが出る場合は、次のバグレポートを参照してください: https://github.com/rust-embedded/book/issues/269
注記 この出力はあなたのシステムでは異なる場合があります。新しいバージョンの rustc、LLVM、およびライブラリは、異なるアセンブリを生成することがあります。スニペットを小さく保つために、一部の命令は省略しています。
app: file format ELF32-arm-little
Disassembly of section .text:
main:
400: bl #0x256
404: b #-0x4 <main+0x4>
Reset:
406: bl #0x24e
40a: movw r0, #0x0
< .. truncated any more instructions .. >
DefaultHandler_:
656: b #-0x4 <DefaultHandler_>
UsageFault:
657: strb r7, [r4, #0x3]
DefaultPreInit:
658: bx lr
__pre_init:
659: strb r7, [r0, #0x1]
__nop:
65a: bx lr
HardFaultTrampoline:
65c: mrs r0, msp
660: b #-0x2 <HardFault_>
HardFault_:
662: b #-0x4 <HardFault_>
HardFault:
663: <unknown>
実行
次に、QEMU 上で組み込みプログラムを実行する方法を見ていきましょう! 今回は、実際に何かを行う hello の例を使います。デフォルトでは、この例は [defmt] と RTT を使ってテキストを出力します。
注記
defmtは、Embedded Rust エコシステムで広く使われているサードパーティ依存関係(つまりコア以外)です。
ホスト側で defmt が生成したメッセージを読み取ってデコードするには、RTT のトランスポート出力を semihosting に切り替える必要があります。実機を使う場合、これにはデバッグセッションが必要ですが、QEMU を使う場合はそのままで動作します。
依存関係を切り替えましょう。
cargo remove defmt-rtt
cargo add defmt-semihosting
src/lib.rs を開き、use defmt_rtt as _; を use defmt_semihosting as _; に置き換えてください。
これで、このサンプルをビルドできます。
cargo build --bin hello
出力されるバイナリは
target/thumbv7m-none-eabi/debug/hello
に配置されます。
このバイナリを QEMU 上で実行するには、通常は次のコマンドで十分です。
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/hello
ただし今回のケースでは defmt を使っているため、ホストは出力をデコードできません。代わりに、Ferrous Systems が提供する qemu-run というツールが必要になります。
git clone git@github.com:knurling-rs/defmt.git
cd defmt/qemu-run/
cargo run -- --machine lm3s6965evb ../qemu-rs/target/thumbv7m-none-eabi/debug/hello
Hello, world!
このコマンドは、テキストを出力したあと正常に終了するはずです(終了コード = 0)。*nix では、次のコマンドでそれを確認できます。
echo $?
0
その QEMU コマンドを分解して見ていきましょう。
-
qemu-system-arm。これは QEMU エミュレータです。QEMU のバイナリにはいくつかの種類があり、これは ARM マシン全体の システム エミュレーションを行うものなので、この名前になっています。 -
-cpu cortex-m3。これは QEMU に Cortex-M3 CPU をエミュレートするよう指示します。CPU モデルを明示することで、いくつかの誤コンパイルを検出できます。たとえば、ハードウェア FPU を持つ Cortex-M4F 向けにコンパイルされたプログラムを実行すると、QEMU は実行中にエラーになります。 -
-machine lm3s6965evb。これは QEMU に LM3S6965EVB をエミュレートするよう指示します。これは LM3S6965 マイクロコントローラを搭載した評価ボードです。 -
-nographic。これは QEMU に GUI を起動しないよう指示します。 -
-semihosting-config (..)。これは QEMU に semihosting を有効にするよう指示します。semihosting を使うと、エミュレートされたデバイスは、とりわけ、ホストの stdout、stderr、stdin を使用したり、ホスト上にファイルを作成したりできます。 -
-kernel $file。これは QEMU に、どのバイナリをロードしてエミュレートされたマシン上で実行するかを指示します。
この長い QEMU コマンドを毎回入力するのは大変です! この手順を簡単にするために、カスタム runner を設定できます。.cargo/config.toml には、QEMU を呼び出す runner がコメントアウトされた状態で入っています。これをコメント解除しましょう。
head -n3 .cargo/config.toml
[target.thumbv7m-none-eabi]
# `cargo run` が QEMU 上でプログラムを実行するようにするには、これのコメントを解除します
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
このランナーは、デフォルトのコンパイルターゲットである thumbv7m-none-eabi
ターゲットにのみ適用されます。これで cargo run はプログラムをコンパイルし、
QEMU 上で実行します:
cargo run --example hello --release
Compiling app v0.1.0 (file:///tmp/app)
Finished release [optimized + debuginfo] target(s) in 0.26s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
Hello, world!
デバッグ
デバッグは組み込み開発において極めて重要です。どのように行うのか見ていきましょう。
組み込みデバイスのデバッグでは リモート デバッグを行います。これは、デバッグ したいプログラムが、デバッガプログラム(GDB または LLDB)を実行している マシン上では動作しないためです。
リモートデバッグにはクライアントとサーバーが関わります。QEMU を使う構成では、 クライアントは GDB(または LLDB)のプロセスであり、サーバーは組み込み プログラムも実行している QEMU プロセスです。
このセクションでは、すでにコンパイルした hello のサンプルを使用します。
デバッグの最初の手順は、QEMU をデバッグモードで起動することです:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-gdb tcp::3333 \
-S \
-kernel target/thumbv7m-none-eabi/debug/examples/hello
このコマンドはコンソールに何も出力せず、端末を占有したままになります。今回は 追加で 2 つのフラグを指定しています:
-
-gdb tcp::3333。これは、TCP ポート 3333 での GDB 接続を待つよう QEMU に 指示します。 -
-S。これは、起動時にマシンを停止したままにするよう QEMU に指示します。 これがないと、デバッガを起動する前にプログラムが main の終わりまで到達して しまいます!
次に、別の端末で GDB を起動し、このサンプルのデバッグシンボルを読み込むよう 指示します:
gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello
注記: インストールの章でどれをインストールしたかによっては、gdb-multiarch ではなく別のバージョンの gdb が必要になるかもしれません。たとえば
arm-none-eabi-gdb や単に gdb の場合もあります。
その後、GDB シェル内で、TCP ポート 3333 で接続を待っている QEMU に接続します。
target remote :3333
Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473 pub unsafe extern "C" fn Reset() -> ! {
プロセスが停止しており、プログラムカウンタが Reset という名前の関数を指して
いることが分かります。これがリセットハンドラです。Cortex-M コアはブート時に
これを実行します。
環境によっては、上に示したように
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473の行が表示される代わりに、gdb が次のような警告を表示することがあります:
core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254src/libcore/num/bignum.rs: No such file or directory.これは既知の不具合です。これらの警告はそのまま無視して構いません。おそらく Reset() にいます。
このリセットハンドラは最終的に私たちの main 関数を呼び出します。ブレーク
ポイントと continue コマンドを使って、そこまで一気に進みましょう。
ブレークポイントを設定するために、まず list コマンドでコードのどこで停止
したいか確認しましょう。
list main
これで、examples/hello.rs ファイルのソースコードが表示されます。
6 use panic_halt as _;
7
8 use cortex_m_rt::entry;
9 use cortex_m_semihosting::{debug, hprintln};
10
11 #[entry]
12 fn main() -> ! {
13 hprintln!("Hello, world!").unwrap();
14
15 // QEMU を終了
“Hello, world!” の直前、つまり 13 行目にブレークポイントを追加したいので、
break コマンドを使います:
break 13
これで continue コマンドを使って、main 関数まで実行するよう gdb に指示
できます:
continue
Continuing.
Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13 hprintln!("Hello, world!").unwrap();
これで “Hello, world!” を出力するコードの直前まで来ました。next
コマンドで先に進みましょう。
next
16 debug::exit(debug::EXIT_SUCCESS);
この時点で、qemu-system-arm を実行している端末に “Hello, world!” が表示
されるはずです。
$ qemu-system-arm (..)
Hello, world!
もう一度 next を実行すると、QEMU プロセスが終了します。
next
[Inferior 1 (Remote target) exited normally]
これで GDB セッションを終了できます。
quit