QEMU
We'll start writing a program for the LM3S6965, a Cortex-M3 microcontroller. We have chosen this as our initial target because it can be emulated using QEMU so you don't need to fiddle with hardware in this section and we can focus on the tooling and the development process.
A non standard Rust program
We'll use the cortex-m-quickstart
project template so go generate a new
project from it.
- Using
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
- Using
git
Clone the repository
$ git clone https://github.com/rust-embedded/cortex-m-quickstart app
$ cd app
And then fill in the placeholders in the Cargo.toml
file
$ 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
- Using neither
Grab the latest snapshot of the cortex-m-quickstart
template and extract it.
Using the command line:
$ # NOTE there's also a tarball available: 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
OR 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.
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.
For convenience here's the source code of 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_abort; // requires nightly // extern crate panic_itm; // logs messages over ITM; requires ITM support // extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger use cortex_m_rt::entry; #[entry] fn main() -> ! { loop { // your code goes here } }
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.
extern crate panic_halt;
. This crate provides a panic_handler
that defines
the panicking behavior of the program. More on this later on.
#[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
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
in the
template has the answer:
$ tail -n6 .cargo/config
[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
. This compilation target has been set as the default so the
two commands below do the same:
$ cargo build --target thumbv7m-none-eabi
$ cargo build
Inspecting
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.
$ # `--bin app` is sugar for inspect the binary at `target/$TRIPLE/debug/app`
$ # `--bin app` will also (re)compile the binary, if necessary
$ 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
can print the size of the linker sections of the binary.
NOTE this output assumes that rust-embedded/cortex-m-rt#111 has been merged
$ # we use `--release` to inspect the optimized version
$ 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
A refresher on ELF linker sections
.text
contains the program instructions.rodata
contains constant values like strings.data
contains statically allocated variables whose initial values are not zero.bss
also contains statically allocated variables whose initial values are zero.vector_table
is a non-standard section that we use to store the vector (interrupt) table.ARM.attributes
and 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-hex
NOTE this output assumes that rust-embedded/cortex-m-rt#111 has been merged
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
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.
For convenience here's the source code of src/main.rs
:
$ cat examples/hello.rs
//! Prints "Hello, world!" on the host console using semihosting #![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 debug::exit(debug::EXIT_SUCCESS); loop {} }
This program uses something called semihosting to print text to the host console. When using real hardware this requires a debug session but when using QEMU this Just Works.
Let's start by compiling the example:
$ cargo build --example hello
The output binary will be located at
target/thumbv7m-none-eabi/debug/examples/hello
.
To run this binary on QEMU run the following command:
$ 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!
The command should successfully exit (exit code = 0) after printing the text. On *nix you can check that with the following command:
$ echo $?
0
Let me break down that long QEMU command for you:
-
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, a 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
has a commented out runner that invokes
QEMU; let's uncomment it:
$ head -n3 .cargo/config
[target.thumbv7m-none-eabi]
# uncomment this to make `cargo run` execute programs on QEMU
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
This runner only applies to the thumbv7m-none-eabi
target, which is our
default compilation target. Now cargo run
will compile the program and run it
on 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!
Debugging
Debugging is critical to embedded development. Let's see how it's done.
Debugging an embedded device involves remote debugging as the program that we want to debug won't be running on the machine that's running the debugger program (GDB or LLDB).
Remote debugging involves a client and a server. In a QEMU setup, the client will be a GDB (or LLDB) process and the server will be the QEMU process that's also running the embedded program.
In this section we'll use the hello
example we already compiled.
The first debugging step is to launch QEMU in debugging mode:
$ 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
This command won't print anything to the console and will block the terminal. We have passed two extra flags this time:
-
-gdb tcp::3333
. This tells QEMU to wait for a GDB connection on TCP port 3333. -
-S
. This tells QEMU to freeze the machine at startup. Without this the program would have reached the end of main before we had a chance to launch the debugger!
Next we launch GDB in another terminal and tell it to load the debug symbols of the example:
$ <gdb> -q target/thumbv7m-none-eabi/debug/examples/hello
NOTE: <gdb>
represents a GDB program capable of debugging ARM binaries.
This could be arm-none-eabi-gdb
, gdb-multiarch
or gdb
depending on your
system -- you may have to try all three.
Then within the GDB shell we connect to QEMU, which is waiting for a connection on TCP port 3333.
(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() -> ! {
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.
This reset handler will eventually call our main function. Let's skip all the
way there using a breakpoint and the continue
command:
(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();
We are now close to the code that prints "Hello, world!". Let's move forward
using the next
command.
(gdb) next
18 writeln!(stdout, "Hello, world!").unwrap();
(gdb) next
20 debug::exit(debug::EXIT_SUCCESS);
At this point you should see "Hello, world!" printed on the terminal that's
running qemu-system-arm
.
$ qemu-system-arm (..)
Hello, world!
Calling next
again will terminate the QEMU process.
(gdb) next
[Inferior 1 (Remote target) exited normally]
You can now exit the GDB session.
(gdb) quit