QEMU
Cortex-M3マイクロコントローラのLM3S6965用にプログラムを書くところから始めましょう。 このLM3S6965を最初のターゲットとして選んだ理由は、QEMUを使ってエミュレーションできるからです。 このセクションでは、ハードウェアをいじる必要がなく、ツールと開発プロセスに集中できます。
標準ライブラリを使わないRustプログラム
cortex-m-quickstart
プロジェクトテンプレートを使用し、新しいプロジェクトを生成します。
cargo-generate
を利用する場合
$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
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
のプレースホルダを埋めます。
$ cat Cargo.toml
[package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
version = "0.1.0"
# ..
[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
test = false
bench = false
- どちらも使わない場合
cortex-m-quickstart
テンプレートの最新スナップショットを入手し、展開します。
コマンドラインを利用する場合:
$ # 注記 tar形式でも入手可能です: archive/master.tar.gz
$ 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」をクリックします。
次に、Cargo.toml
ファイルのプレースホルダを「git
を利用する場合」の2つ目のパートにある通り埋めます。
重要 このチュートリアルでは、「app」という名前をプロジェクト名に使います。 「app」という単語が出てきた場合、それをあなたのプロジェクトにつけた名前に置き替えなければなりません。 または、プロジェクトに「app」という名前をつけると、置き替える必要がなくなります。
これは、src/main.rs
のソースコードです。
$ cat src/main.rs
#![no_std] #![no_main] // pick a panicking behavior // パニック発生時の挙動を選びます // extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics extern crate panic_halt; // パニックをキャッチするため、`rust_begin_unwind`にブレイクポイントを設定できます // extern crate panic_abort; // requires nightly // extern crate panic_abort; // nightlyが必要です // extern crate panic_itm; // logs messages over ITM; requires ITM support // extern crate panic_itm; // ITMを介してメッセージをログ出力します; ITMサポートが必要です // extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger // extern crate panic_semihosting; // ホストの標準エラーにメッセージをログ出力します; デバッガが必要です。 use cortex_m_rt::entry; #[entry] fn main() -> ! { loop { // your code goes here // あなたのコードはここに書きます } }
このプログラムは、標準的なRustプログラムとは少し異なりますので、もう少し詳しく見てみましょう。
#![no_std]
はこのプログラムが、標準クレートであるstd
にリンクしないことを意味します。
代わりに、そのサブセットであるcore
クレートにリンクします。
#![no_main]
は、ほとんどのRustプログラムが使用する標準のmain
インタフェースを、
このプログラムでは使用しないことを示します。
no_main
を利用する主な理由は、no_std
の状況でmain
インタフェースを使用するにはnightlyが必要だからです。
extern crate panic_halt;
。このクレートは、プログラムのパニック発生時の挙動を定義するpanic_handler
を提供します。
後ほど、より詳しく説明します。
[#[entry]
]は、cortex-m-rt
クレートが提供するアトリビュートで、プログラムのエントリポイントを示すために使用します。
標準のmain
インタフェースを使用しないので、プログラムのエントリポイントを示す別の方法が必要です。それが、#[entry]
です。
[#[entry]
]: https://docs.rs/cortex-m-rt-macros/latest/cortex_m_rt_macros/attr.entry.html
fn main() -> !
。ターゲットハードウェア上で動作しているのは私たちのプログラムだけなので、
終了させたくありません。
コンパイル時、確実にそうなるように、発散する関数を使います(関数シグネチャの-> !
部分)。
クロスコンパイル
次のステップは、プログラムをCortex-M3アーキテクチャ向けにクロスコンパイルすることです。
これはコンパイルターゲット($TRIPLE
)が何かわかっていれば、cargo build --target $TRIPLE
を実行するだけで簡単にできます。
コンパイルターゲットが何かは、テンプレート中の.cargo/config
を見ればわかります。
$ tail -n6 .cargo/config
[build]
# 以下のコンパイルターゲットから1つを選びます
# target = "thumbv6m-none-eabi" # Cortex-M0およびCortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4およびCortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4FおよびCortex-M7F (with FPU)
Cortex-M3アーキテクチャ向けにクロスコンパイルするためには、thumbv7m-none-eabi
を使う必要があります。
このコンパイルターゲットは、デフォルトとして設定されているため、下記2つのコマンドは同じ意味になります。
$ cargo build --target thumbv7m-none-eabi
$ cargo build
確認
今、target/thumbv7m-none-eabi/debug/app
に、非ネイティブなバイナリがあります。
cargo-binutils
を使って、このバイナリを確認することができます。
このバイナリがARMバイナリであることを確かめるために、cargo-readobj
でELFヘッダを表示できます。
$ # `--bin app`は`target/$TRIPLE/debug/app`のバイナリを確認するためのシンタックスシュガーです
$ # `--bin app`は必要に応じて、バイナリを(再)コンパイルもします
$ cargo readobj --bin app -- -file-headers
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
はバイナリのリンカセクションのサイズを表示できます。
注記 この出力は、rust-embedded/cortex-m-rt#111がマージされていることを前提とします
$ # 最適化されたバイナリを確認するために`--release`を使います。
$ cargo size --bin app --release -- -A
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
は、初期値が0ではない静的に割り当てられた変数が格納されています.bss
も静的に割り当てられた変数が格納されますが、その初期値は0です.vector_table
は、非標準のセクションです。(割り込み)ベクタテーブルを格納するために使用します.ARM.attributes
と.debug_*
セクションはメタデータを含んでおり、バイナリをフラッシュに書き込む際、 ターゲットボード上にロードされません
重要: ELFファイルは、デバッグ情報といったメタデータを含んでいるため、そのディスク上のサイズは、
プログラムがデバイスに書き込まれた時に専有するスペースを正確に反映していません。
実際のバイナリサイズを確認するために、常にcargo-size
を使用して下さい。
cargo-objdump
は、バイナリをディスアセンブルするために使用できます。
$ cargo objdump --bin app --release -- -disassemble -no-show-raw-insn -print-imm-hex
注記 この出力は、rust-embedded/cortex-m-rt#111がマージされていることを前提とします
app: file format ELF32-arm-little
Disassembly of section .text:
Reset:
400: bl #0x36
404: movw r0, #0x0
408: movw r1, #0x0
40c: movt r0, #0x2000
410: movt r1, #0x2000
414: bl #0x2c
418: movw r0, #0x0
41c: movw r1, #0x45c
420: movw r2, #0x0
424: movt r0, #0x2000
428: movt r1, #0x0
42c: movt r2, #0x2000
430: bl #0x1c
434: b #-0x4 <Reset+0x34>
HardFault_:
436: b #-0x4 <HardFault_>
UsageFault:
438: b #-0x4 <UsageFault>
__pre_init:
43a: bx lr
HardFault:
43c: mrs r0, msp
440: bl #-0xe
__zero_bss:
444: movs r2, #0x0
446: b #0x0 <__zero_bss+0x6>
448: stm r0!, {r2}
44a: cmp r0, r1
44c: blo #-0x8 <__zero_bss+0x4>
44e: bx lr
__init_data:
450: b #0x2 <__init_data+0x6>
452: ldm r1!, {r3}
454: stm r0!, {r3}
456: cmp r0, r2
458: blo #-0xa <__init_data+0x2>
45a: bx lr
実行
次は、QEMUで組込みプログラムを実行する方法を見ていきましょう。
今回は、実際に何かを行うhello
の例を使います。
便宜上のsrc/main.rs
のソースコードです:
$ cat examples/hello.rs
//! Prints "Hello, world!" on the host console using semihosting //! セミホスティングを使って"Hello, world!"をホストのコンソールに表示します #![no_main] #![no_std] extern crate panic_halt; use core::fmt::Write; use cortex_m_rt::entry; use cortex_m_semihosting::{debug, hio}; #[entry] fn main() -> ! { let mut stdout = hio::hstdout().unwrap(); writeln!(stdout, "Hello, world!").unwrap(); // exit QEMU or the debugger section // QEMUもしくはデバッガセッションを終了します debug::exit(debug::EXIT_SUCCESS); loop {} }
このプログラムは、ホストコンソールにテキストを表示するために、セミホスティングと呼ばれるものを使います。 実際のハードウェアを使用する場合、セミホスティングはデバッグセッションを必要としますが、 QEMUを使う場合、これで機能します。
例をコンパイルすることから始めましょう。
$ cargo build --example hello
target/thumbv7m-none-eabi/debug/examples/hello
に出力バイナリがあります。
QEMU上でこのバイナリを動かすために、次のコマンドを実行して下さい。
$ qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/examples/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をエミュレーションするように伝えます。 LM3S6965EVBは、LM3S6965マイクロコントローラを搭載している評価ボードです。
-nographic
。QEMUがGUIを起動しないようにします。
-semihosting-config (..)
。QEMUのセミホスティングを有効にします。セミホスティングにより、 エミュレーションされたデバイスは、ホストの標準出力、標準エラー、標準入力を使用できるようになり、 ホスト上にファイルを作成することができます。
-kernel $file
。QEMUに、エミュレーションしたマシン上にロードして、実行するバイナリを教えます。
この長いQEMUコマンドを入力するのは大変過ぎます。このプロセスを簡略化するために、
カスタムランナーを設定できます。.cargo/config
には、QEMUを起動するランナーが、
コメントアウトされた状態であります。コメントアウトを外して下さい。
$ head -n3 .cargo/config
[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
。QEMUがTCPポート3333番で、GDBコネクションを待つようにします。
-S
。QEMUが、起動時に、マシンをフリーズします。このフラグがないと、 デバッガを起動する前に、プログラムがmain関数の終わりに到達してしまいます。
次に別の端末でGDBを起動し、hello
の例のデバッグシンボルをロードします。
$ <gdb> -q target/thumbv7m-none-eabi/debug/examples/hello
注記: <gdb>
はARMバイナリをデバッグ可能なGDBを意味します。
あなたが利用しているシステムに依存して、arm-none-eabi-gdb
か、gdb-multiarch
、gdb
になります。
3つ全てを試してみる必要があるかもしれません。
すると、GDBシェルは、TCPポート3333番で接続を待っていたQEMUに接続します。
(gdb) 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
という名前の関数を指していることがわかります。
Reset
関数は、Cortex-Mコアが起動時に実行するリセットハンドラです。
このリセットハンドラは、最終的に、私たちのメイン関数を呼び出します。
ブレイクポイントとcontinue
コマンドを使って、メイン関数呼び出しまでスキップしましょう。
(gdb) break main
Breakpoint 1 at 0x400: file examples/panic.rs, line 29.
(gdb) continue
Continuing.
Breakpoint 1, main () at examples/hello.rs:17
17 let mut stdout = hio::hstdout().unwrap();
「Hello, world!」を表示するコードに近づいてきました。
next
コマンドを使って、先へ進みましょう。
(gdb) next
18 writeln!(stdout, "Hello, world!").unwrap();
(gdb) next
20 debug::exit(debug::EXIT_SUCCESS);
この時点で、qemu-system-arm
を実行している端末に「Hello, world」が表示されるはずです。
$ qemu-system-arm (..)
Hello, world!
もう1度next
を実行すると、QEMUプロセスが終了します。
(gdb) next
[Inferior 1 (Remote target) exited normally]
これでGDBセッションを終了できます。
(gdb) quit