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 QEMUso you don't need to fiddle with hardware in this section and we can focus onthe tooling and the development process.

IMPORTANTWe'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 selectedfor your project. Or, you could also name your project "app" and avoid thesubstitutions.

Creating a non standard Rust program

We'll use the cortex-m-quickstart project template to generate a newproject from it.

Using cargo-generate

First install cargo-generate

  1. cargo install cargo-generate

Then generate a new project

  1. cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
  1. Project Name: app
  2. Creating project called `app`...
  3. Done! New project created /tmp/app
  1. cd app

Using git

Clone the repository

  1. git clone https://github.com/rust-embedded/cortex-m-quickstart app
  2. cd app

And then fill in the placeholders in the Cargo.toml file

  1. [package]
  2. authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
  3. edition = "2018"
  4. name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
  5. version = "0.1.0"
  6. # ..
  7. [[bin]]
  8. name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
  9. test = false
  10. bench = false

Using neither

Grab the latest snapshot of the cortex-m-quickstart template and extract it.

  1. curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
  2. unzip master.zip
  3. mv cortex-m-quickstart-master app
  4. cd app

Or you can browse to cortex-m-quickstart, click the green "Clone ordownload" button and then click "Download ZIP".

Then fill in the placeholders in the Cargo.toml file as done in the secondpart of the "Using git" version.

Program Overview

For convenience here are the most important parts of the source code in src/main.rs:

  1. #![no_std]
  2. #![no_main]
  3. extern crate panic_halt;
  4. use cortex_m_rt::entry;
  5. #[entry]
  6. fn main() -> ! {
  7. loop {
  8. // your code goes here
  9. }
  10. }

This program is a bit different from a standard Rust program so let's take acloser look.

#![nostd] 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 maininterface that most Rust programs use. The main (no pun intended) reason to gowith no_main is that using the main interface in no_std context requiresnightly.

extern crate panic_halt;. This crate provides a panic_handler that definesthe panicking behavior of the program. We will cover this in more detail in thePanicking chapter of the book.

#[entry] is an attribute provided by the cortex-m-rt crate that's usedto mark the entry point of the program. As we are not using the standard maininterface we need another way to indicate the entry point of the program andthat'd be #[entry].

fn main() -> !. Our program will be the only process running on the targethardware 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 thecompilation target ($TRIPLE) should be. Luckily, the .cargo/config in thetemplate has the answer:

  1. tail -n6 .cargo/config
  1. [build]
  2. # Pick ONE of these compilation targets
  3. # target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
  4. target = "thumbv7m-none-eabi" # Cortex-M3
  5. # target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
  6. # target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

To cross compile for the Cortex-M3 architecture we have to usethumbv7m-none-eabi. This compilation target has been set as the default so thetwo commands below do the same:

  1. cargo build --target thumbv7m-none-eabi
  2. cargo build

Inspecting

Now we have a non-native ELF binary in target/thumbv7m-none-eabi/debug/app. Wecan inspect it using cargo-binutils.

With cargo-readobj we can print the ELF headers to confirm that this is an ARMbinary.

  1. cargo readobj --bin app -- -file-headers

Note that:

  • —bin app is sugar for inspect the binary at target/$TRIPLE/debug/app
  • —bin app will also (re)compile the binary, if necessary
  1. ELF Header:
  2. Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  3. Class: ELF32
  4. Data: 2's complement, little endian
  5. Version: 1 (current)
  6. OS/ABI: UNIX - System V
  7. ABI Version: 0x0
  8. Type: EXEC (Executable file)
  9. Machine: ARM
  10. Version: 0x1
  11. Entry point address: 0x405
  12. Start of program headers: 52 (bytes into file)
  13. Start of section headers: 153204 (bytes into file)
  14. Flags: 0x5000200
  15. Size of this header: 52 (bytes)
  16. Size of program headers: 32 (bytes)
  17. Number of program headers: 2
  18. Size of section headers: 40 (bytes)
  19. Number of section headers: 19
  20. 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 beenmerged

  1. cargo size --bin app --release -- -A

we use —release to inspect the optimized version

  1. app :
  2. section size addr
  3. .vector_table 1024 0x0
  4. .text 92 0x400
  5. .rodata 0 0x45c
  6. .data 0 0x20000000
  7. .bss 0 0x20000000
  8. .debug_str 2958 0x0
  9. .debug_loc 19 0x0
  10. .debug_abbrev 567 0x0
  11. .debug_info 4929 0x0
  12. .debug_ranges 40 0x0
  13. .debug_macinfo 1 0x0
  14. .debug_pubnames 2035 0x0
  15. .debug_pubtypes 1892 0x0
  16. .ARM.attributes 46 0x0
  17. .debug_frame 100 0x0
  18. .debug_line 867 0x0
  19. 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 arenot zero
  • .bss also contains statically allocated variables whose initial valuesare zero
  • .vectortable is a non-standard section that we use to store the vector(interrupt) table
  • .ARM.attributes and the .debug* sections contain metadata and willnot be loaded onto the target when flashing the binary.

IMPORTANT: ELF files contain metadata like debug information so their sizeon disk does not accurately reflect the space the program will occupy whenflashed on a device. Always use cargo-size to check how big a binary reallyis.

cargo-objdump can be used to disassemble the binary.

  1. cargo objdump --bin app --release -- -disassemble -no-show-raw-insn -print-imm-hex

NOTE this output can differ on your system. New versions of rustc, LLVMand libraries can generate different assembly. We truncated some of the instructionsto keep the snippet small.

  1. app: file format ELF32-arm-little
  2. Disassembly of section .text:
  3. main:
  4. 400: bl #0x256
  5. 404: b #-0x4 <main+0x4>
  6. Reset:
  7. 406: bl #0x24e
  8. 40a: movw r0, #0x0
  9. < .. truncated any more instructions .. >
  10. DefaultHandler_:
  11. 656: b #-0x4 <DefaultHandler_>
  12. UsageFault:
  13. 657: strb r7, [r4, #0x3]
  14. DefaultPreInit:
  15. 658: bx lr
  16. __pre_init:
  17. 659: strb r7, [r0, #0x1]
  18. __nop:
  19. 65a: bx lr
  20. HardFaultTrampoline:
  21. 65c: mrs r0, msp
  22. 660: b #-0x2 <HardFault_>
  23. HardFault_:
  24. 662: b #-0x4 <HardFault_>
  25. HardFault:
  26. 663: <unknown>

Running

Next, let's see how to run an embedded program on QEMU! This time we'll use thehello example which actually does something.

For convenience here's the source code of examples/hello.rs:

  1. //! Prints "Hello, world!" on the host console using semihosting
  2. #![no_main]
  3. #![no_std]
  4. extern crate panic_halt;
  5. use cortex_m_rt::entry;
  6. use cortex_m_semihosting::{debug, hprintln};
  7. #[entry]
  8. fn main() -> ! {
  9. hprintln!("Hello, world!").unwrap();
  10. // exit QEMU
  11. // NOTE do not run this on hardware; it can corrupt OpenOCD state
  12. debug::exit(debug::EXIT_SUCCESS);
  13. loop {}
  14. }

This program uses something called semihosting to print text to the _host_console. When using real hardware this requires a debug session but when usingQEMU this Just Works.

Let's start by compiling the example:

  1. cargo build --example hello

The output binary will be located attarget/thumbv7m-none-eabi/debug/examples/hello.

To run this binary on QEMU run the following command:

  1. qemu-system-arm \
  2. -cpu cortex-m3 \
  3. -machine lm3s6965evb \
  4. -nographic \
  5. -semihosting-config enable=on,target=native \
  6. -kernel target/thumbv7m-none-eabi/debug/examples/hello
  1. Hello, world!

The command should successfully exit (exit code = 0) after printing the text. On*nix you can check that with the following command:

  1. echo $?
  1. 0

Let's break down that QEMU command:

  • qemu-system-arm. This is the QEMU emulator. There are a few variants ofthese QEMU binaries; this one does full system emulation of ARM machineshence the name.

  • -cpu cortex-m3. This tells QEMU to emulate a Cortex-M3 CPU. Specifying theCPU model lets us catch some miscompilation errors: for example, running aprogram compiled for the Cortex-M4F, which has a hardware FPU, will make QEMUerror during its execution.

  • -machine lm3s6965evb. This tells QEMU to emulate the LM3S6965EVB, aevaluation board that contains a LM3S6965 microcontroller.

  • -nographic. This tells QEMU to not launch its GUI.

  • -semihosting-config (..). This tells QEMU to enable semihosting. Semihostinglets the emulated device, among other things, use the host stdout, stderr andstdin and create files on the host.

  • -kernel $file. This tells QEMU which binary to load and run on the emulatedmachine.

Typing out that long QEMU command is too much work! We can set a custom runnerto simplify the process. .cargo/config has a commented out runner that invokesQEMU; let's uncomment it:

  1. head -n3 .cargo/config
  1. [target.thumbv7m-none-eabi]
  2. # uncomment this to make `cargo run` execute programs on QEMU
  3. 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 ourdefault compilation target. Now cargo run will compile the program and run iton QEMU:

  1. cargo run --example hello --release
  1. Compiling app v0.1.0 (file:///tmp/app)
  2. Finished release [optimized + debuginfo] target(s) in 0.26s
  3. Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
  4. 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 wewant to debug won't be running on the machine that's running the debuggerprogram (GDB or LLDB).

Remote debugging involves a client and a server. In a QEMU setup, the clientwill be a GDB (or LLDB) process and the server will be the QEMU process that'salso 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:

  1. qemu-system-arm \
  2. -cpu cortex-m3 \
  3. -machine lm3s6965evb \
  4. -nographic \
  5. -semihosting-config enable=on,target=native \
  6. -gdb tcp::3333 \
  7. -S \
  8. -kernel target/thumbv7m-none-eabi/debug/examples/hello

This command won't print anything to the console and will block the terminal. Wehave passed two extra flags this time:

  • -gdb tcp::3333. This tells QEMU to wait for a GDB connection on TCPport 3333.

  • -S. This tells QEMU to freeze the machine at startup. Without this theprogram would have reached the end of main before we had a chance to launchthe debugger!

Next we launch GDB in another terminal and tell it to load the debug symbols ofthe example:

  1. gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello

NOTE: you might need another version of gdb instead of gdb-multiarch dependingon which one you installed in the installation chapter. This could also bearm-none-eabi-gdb or just gdb.

Then within the GDB shell we connect to QEMU, which is waiting for a connectionon TCP port 3333.

  1. target remote :3333
  1. Remote debugging using :3333
  2. Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
  3. 473 pub unsafe extern "C" fn Reset() -> ! {

You'll see that the process is halted and that the program counter is pointingto a function named Reset. That is the reset handler: what Cortex-M coresexecute upon booting.

This reset handler will eventually call our main function. Let's skip all theway there using a breakpoint and the continue command:

  1. break main
  1. Breakpoint 1 at 0x400: file examples/panic.rs, line 29.
  1. continue
  1. Continuing.
  2. Breakpoint 1, main () at examples/hello.rs:17
  3. 17 let mut stdout = hio::hstdout().unwrap();

We are now close to the code that prints "Hello, world!". Let's move forwardusing the next command.

  1. next
  1. 18 writeln!(stdout, "Hello, world!").unwrap();
  1. next
  1. 20 debug::exit(debug::EXIT_SUCCESS);

At this point you should see "Hello, world!" printed on the terminal that'srunning qemu-system-arm.

  1. $ qemu-system-arm (..)
  2. Hello, world!

Calling next again will terminate the QEMU process.

  1. next
  1. [Inferior 1 (Remote target) exited normally]

You can now exit the GDB session.

  1. quit