QEMU
Cortex-M3マイクロコントローラのLM3S6965用にプログラムを書くところから始めましょう。 このLM3S6965を最初のターゲットとして選んだ理由は、QEMUを使ってエミュレーションできるからです。 このセクションでは、ハードウェアをいじる必要がなく、ツールと開発プロセスに集中できます。
IMPORTANT We’ll use the name “app” for the project name in this tutorial. Whenever you see the word “app” you should replace it with the name you selected for your project. Or, you could also name your project “app” and avoid the substitutions.
標準ライブラリを使わないRustプログラム
We’ll use the cortex-m-quickstart project template to generate a new project from it. The created project will contain a barebone application: a good starting point for a new embedded rust application. In addition, the project will contain an examples directory, with several separate applications, highlighting some of the key embedded rust functionality.
Using cargo-generate
First install cargo-generate
cargo install cargo-generateThen generate a new project
cargo generate --git https://github.com/knurling-rs/app-template Project Name: app
Creating project called `app`...
Done! New project created /tmp/appcd appUsing git
Clone the repository
git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd appAnd then fill in the placeholders in the Cargo.toml file
[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 = falseUsing neither
Grab the latest snapshot of the cortex-m-quickstart template and extract it.
curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd appOr you can browse to cortex-m-quickstart, click the green “Clone or download” button and then click “Download ZIP”.
Then fill in the placeholders in the Cargo.toml file as done in the second part of the “Using git” version.
Program Overview
For convenience here are the most important parts of the source code in src/main.rs:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// あなたのコードはここに書きます
}
}
This program is a bit different from a standard Rust program so let’s take a closer look.
#![no_std] indicates that this program will not link to the standard crate, std. Instead it will link to its subset: the core crate.
#![no_main] indicates that this program won’t use the standard main interface that most Rust programs use. The main (no pun intended) reason to go with no_main is that using the main interface in no_std context requires nightly.
use panic_halt as _;. This crate provides a panic_handler that defines the panicking behavior of the program. We will cover this in more detail in the Panicking chapter of the book.
#[entry] is an attribute provided by the cortex-m-rt crate that’s used to mark the entry point of the program. As we are not using the standard main interface we need another way to indicate the entry point of the program and that’d be #[entry].
fn main() -> !. Our program will be the only process running on the target hardware so we don’t want it to end! We use a divergent function (the -> ! bit in the function signature) to ensure at compile time that’ll be the case.
Cross compiling
First of all we will need the memory layout for the target microcontroller, the LM3S6965 in our case. Otherwise the build will fail to link the image. Create a file named memory.x at the root of the project and paste the following content:
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
/* TODO Adjust these memory regions to match your device memory layout */
/* These values correspond to the LM3S6965, one of the few devices
/* QEMU can emulate */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* You may want to use this variable to locate the call stack and static
variables in different memory regions. Below is shown the default value */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* You can use this symbol to customize the location of the .text section */
/* If omitted the .text section will be placed right after the .vector_table
section */
/* This is required only on microcontrollers that store some configuration right
after the vector table */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* Example of putting non-initialized variables into custom RAM locations. */
/* This assumes you have defined a region RAM2 above, and in the Rust
sources added the attribute `#[link_section = ".ram2bss"]` to the data
you want to place there. */
/* Note that the section will not be zero-initialized by the runtime! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
The next step is to cross compile the program for the Cortex-M3 architecture. That’s as simple as running cargo build --target $TRIPLE if you know what the compilation target ($TRIPLE) should be. Luckily, the .cargo/config.toml in the template has the answer:
tail -n6 .cargo/config.toml[build]
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
To cross compile for the Cortex-M3 architecture we have to use thumbv7m-none-eabi. That target is not automatically installed when installing the Rust toolchain, it would now be a good time to add that target to the toolchain, if you haven’t done it yet:
rustup target add thumbv7m-none-eabiSince the thumbv7m-none-eabi compilation target has been set as the default in your .cargo/config.toml file, the two commands below do the same:
cargo build --target thumbv7m-none-eabi
cargo buildInspecting
Now we have a non-native ELF binary in target/thumbv7m-none-eabi/debug/app. We can inspect it using cargo-binutils.
With cargo-readobj we can print the ELF headers to confirm that this is an ARM binary.
cargo readobj --bin app -- --file-headersNote that:
--bin appis sugar for inspect the binary attarget/$TRIPLE/debug/app--bin appwill also (re)compile the binary, if necessary
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: 18cargo-size can print the size of the linker sections of the binary.
cargo size --bin app --release -- -Awe use --release to inspect the optimized version
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 14570A refresher on ELF linker sections
.textcontains the program instructions.rodatacontains constant values like strings.datacontains statically allocated variables whose initial values are not zero.bssalso contains statically allocated variables whose initial values are zero.vector_tableis a non-standard section that we use to store the vector (interrupt) table.ARM.attributesand the.debug_*sections contain metadata and will not be loaded onto the target when flashing the binary.
IMPORTANT: ELF files contain metadata like debug information so their size on disk does not accurately reflect the space the program will occupy when flashed on a device. Always use cargo-size to check how big a binary really is.
cargo-objdump can be used to disassemble the binary.
cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hexNOTE if the above command complains about Unknown command line argument see the following bug report: https://github.com/rust-embedded/book/issues/269NOTE this output can differ on your system. New versions of rustc, LLVM and libraries can generate different assembly. We truncated some of the instructions to keep the snippet small.
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>Running
Next, let’s see how to run an embedded program on QEMU! This time we’ll use the hello example which actually does something. By default, this example uses [defmt] and RTT to print text.
NOTE defmt is a third-party dependency (i.e. non-core) widely used in the Embedded Rust ecosystem.In order to read and decode the messages produced by defmt in the host, we need to switch the RTT transport output to semihosting. When using real hardware this requires a debug session but when using QEMU this Just Works.
Let’s switch the dependencies:
cargo remove defmt-rtt
cargo add defmt-semihostingOpen src/lib.rs and replace use defmt_rtt as _; by use defmt_semihosting as _;
Now we can build the example:
cargo build --bin helloThe output binary will be located at target/thumbv7m-none-eabi/debug/hello.
To run this binary on QEMU, the following command would be usually enough:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/helloIn our case, since we use defmt, the host will not be able to decode the output. Instead, we will need a tool by Ferrous Systems named 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/helloHello, world!The command should successfully exit (exit code = 0) after printing the text. On *nix you can check that with the following command:
echo $?0Let’s break down that QEMU command:
qemu-system-arm. This is the QEMU emulator. There are a few variants of these QEMU binaries; this one does full system emulation of ARM machines hence the name.-cpu cortex-m3. This tells QEMU to emulate a Cortex-M3 CPU. Specifying the CPU model lets us catch some miscompilation errors: for example, running a program compiled for the Cortex-M4F, which has a hardware FPU, will make QEMU error during its execution.-machine lm3s6965evb. This tells QEMU to emulate the LM3S6965EVB, an evaluation board that contains a LM3S6965 microcontroller.-nographic. This tells QEMU to not launch its GUI.-semihosting-config (..). This tells QEMU to enable semihosting. Semihosting lets the emulated device, among other things, use the host stdout, stderr and stdin and create files on the host.-kernel $file. This tells QEMU which binary to load and run on the emulated machine.
Typing out that long QEMU command is too much work! We can set a custom runner to simplify the process. .cargo/config.toml has a commented out runner that invokes QEMU; let’s uncomment it:
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 ru(
level: 2 + whole
)--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-multiarch -q target/thumbv7m-none-eabi/debug/examples/helloNOTE: you might need another version of gdb instead of gdb-multiarch depending on which one you installed in the installation chapter. This could also be arm-none-eabi-gdb or just gdb.
Then within the GDB shell we connect to QEMU, which is waiting for a connection on TCP port 3333.
target remote :3333Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473 pub unsafe extern "C" fn Reset() -> ! {You’ll see that the process is halted and that the program counter is pointing to a function named Reset. That is the reset handler: what Cortex-M cores execute upon booting.
Note that on some setup, instead of displaying the line
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473as shown above, gdb may print some warnings like:
core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254src/libcore/num/bignum.rs: No such file or directory.That’s a known glitch. You can safely ignore those warnings, you’re most likely at Reset().
This reset handler will eventually call our main function. Let’s skip all the way there using a breakpoint and the continue command. To set the breakpoint, let’s first take a look where we would like to break in our code, with the list command.
list mainThis will show the source code, from the file 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 // exit QEMUWe would like to add a breakpoint just before the “Hello, world!”, which is on line 13. We do that with the break command:
break 13We can now instruct gdb to run up to our main function, with the continue command:
continueContinuing.
Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13 hprintln!("Hello, world!").unwrap();「Hello, world!」を表示するコードに近づいてきました。 nextコマンドを使って、先へ進みましょう。
next16 debug::exit(debug::EXIT_SUCCESS);この時点で、qemu-system-armを実行している端末に「Hello, world」が表示されるはずです。
$ qemu-system-arm (..)
Hello, world!もう1度nextを実行すると、QEMUプロセスが終了します。
next[Inferior 1 (Remote target) exited normally]これでGDBセッションを終了できます。
quit