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]です。

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-multiarchgdbになります。 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