A template for building applications for ARM Cortex-M microcontrollers
This project is developed and maintained by the Cortex-M team.
To build embedded programs using this template you'll need:
- Nightly Rust toolchain from 2018-08-28 or newer:
rustup default nightly
- The
cargo generate
subcommand:cargo install cargo-generate
rust-std
components (pre-compiledcore
crate) for the ARM Cortex-M targets. Run:
$ rustup target add thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf
This section list programs that are not required to build embedded programs but are useful or necessary for embedded development.
To flash (put the firmware on the target device) and debug embedded programs you'll need these additional programs:
- GDB with ARM support or LLDB, for debugging.
- QEMU with ARM support, for running embedded programs on the build machine.
- OpenOCD and similar, which make debugging possible at all.
To inspect the produced binaries you'll want cargo-binutils
.
If you are already familiar with the process of building and debugging embedded Rust programs feel free to skip this section!
To get you familiar with building and debugging embedded Rust programs we'll use the template with its default values to build a program for the LM3S6965, a microcontroller with a Cortex-M3 core that QEMU can emulate.
In this section we'll use a debugger (GDB or LLDB) and QEMU. Be sure to have them installed!
- Initialize the template
$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
- Build the example "Hello, world!" program
// example/hello.rs
#![no_main]
#![no_std]
#[macro_use]
extern crate cortex_m_rt;
extern crate cortex_m_semihosting as sh;
extern crate panic_semihosting;
use core::fmt::Write;
use sh::hio;
entry!(main);
fn main() -> ! {
let mut stdout = hio::hstdout().unwrap();
writeln!(stdout, "Hello, world!").unwrap();
loop {}
}
We'll cross compile the program for the thumbv7m-none-eabi
target. This target
covers Cortex-M3 devices like the one we are targeting.
$ cargo build --target thumbv7m-none-eabi --example hello
You'll find the output binary in the following path:
target/thumbv7m-none-eabi/debug/examples/hello
. The output is an ELF file.
If you have file
installed you can print the file type of the output to
confirm it's an ELF file:
$ ( cd target/thumbv7m-none-eabi/debug/examples && file hello )
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
If you have cargo-binutils
installed you can look at the ELF header of the
output:
NOTE
cargo-readelf
will be available in a future release of binutils (v0.1.3)
$ cargo readelf --example hello -- --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: 0x23A3
Start of program headers: 52 (bytes into file)
Start of section headers: 673340 (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: 21
Section header string table index: 19
If you look at the bottom of the Cargo configuration file (.cargo/config
)
you'll see that the thumbv7m-none-eabi
target has been set as the default
compilation target. This means that you do not need to pass the --target
flag to cross compile because it's already implied. So:
$ cargo build --example hello
Does the same as before.
- There's no step 3! Your build is done, but for completeness let's look at how to run this program on QEMU.
Execute this command and you are done.
$ 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 above command will block the terminal because the hello
program never
ends! To terminate QEMU input this in your terminal: Ctrl+A
, followed by X
.
Let me break down that long 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 flash (kind of) and run on the emulated machine.
First, we launch a QEMU instance:
$ 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
Note that this command won't print anything to the console but 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 ofmain
before we had a chance to launch the debugger!
Next, we start up LLDB in another terminal:
$ lldb target/thumbv7m-none-eabi/debug/examples/hello
And tell it to connect to the GDB server that QEMU created:
(lldb) gdb-remote 3333
Process 1 stopped
* thread #1, stop reason = signal SIGTRAP
frame #0: 0x000023a2 hello`Reset at lib.rs:475
472
473 #[doc(hidden)]
474 #[no_mangle]
-> 475 pub unsafe extern "C" fn Reset() -> ! {
476 extern "C" {
477 // This symbol will be provided by the user via the `entry!` macro
478 fn main() -> !;
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:
(lldb) breakpoint set -name hello::main
Breakpoint 1: where = hello`hello::main::h9cf19a1378cbd1b8 + 2 at hello.rs:20, address = 0x000006a8
(lldb) continue
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x000006a8 hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:20
17 entry!(main);
18
19 fn main() -> ! {
-> 20 let mut stdout = hio::hstdout().unwrap();
21 writeln!(stdout, "Hello, world!").unwrap();
22
23 loop {}
We are now close to the code that prints "Hello, world!". Let's move the program
forward using the next
command.
(lldb) next
Process 1 stopped
* thread #1, stop reason = step over
frame #0: 0x000006be hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:21
18
19 fn main() -> ! {
20 let mut stdout = hio::hstdout().unwrap();
-> 21 writeln!(stdout, "Hello, world!").unwrap();
22
23 loop {}
24 }
(lldb) next
Process 1 stopped
* thread #1, stop reason = step over
frame #0: 0x000006f6 hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:23
20 let mut stdout = hio::hstdout().unwrap();
21 writeln!(stdout, "Hello, world!").unwrap();
22
-> 23 loop {}
24 }
At this point you should see "Hello, world!" printed on the terminal that's
running qemu-system-arm
.
$ qemu-system-arm (..)
Hello, world!
That's it! You can now exit lldb
, which will also cause QEMU to terminate.
(lldb) exit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y
This section explains how to configure this template to build programs for some specific Cortex-M device.
- Before everything else, you need to know the characteristics of the target device:
-
What's the ARM core? e.g. Cortex-M3.
-
Does the ARM core include an FPU? e.g. the Cortex-M4F does.
-
How much Flash memory and RAM does the target device has? e.g. 40 KB of RAM
-
Where is Flash memory and RAM mapped in the address space? e.g.
0x2000_0000
is common for RAM.
You should be able to find this information in the data sheet and / or reference manual of your device.
As an example, we'll use the STM32F303VCT6 microcontroller. This device has:
-
a Cortex-M4F core that includes a single precision FPU
-
256 KB of Flash located at address
0x0800_0000
. -
40 KB of RAM located at address
0x2000_0000
. (There's another RAM region but for simplicity we'll ignore it).
- Initialize the template
$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
- Set the default compilation target in
.cargo/config
.
For the STM32F303VCT6, we pick the thumbv7em-none-eabihf
target as that covers
the Cortex-M4F core.
$ tail .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)
- Enter the memory region information into
memory.x
.
As we mentioned before the STM32F303VCT6 has 40 KB of RAM located at address
0x2000_0000
and 256 KB of Flash memory located at address 0x0800_0000
.
$ cat memory.x
MEMORY
{
/* NOTE K = KiBi = 1024 bytes */
FLASH : ORIGIN = 0x08000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 40K
}
- Build one of the examples:
$ cargo build --example hello
You are done! You should be able to flash and run this example on your device.
Teaching you how to flash and debug this program on your device is out of scope for this document due to the sheer variety of possible hardware / software combinations. But the steps required to flash and debug this program on the STM32F3DISCOVERY using OpenOCD are provided below as a reference.
On a terminal run openocd
to connect to the ST-LINK on the discovery board.
Run this command from the root of the template; openocd
will pick up the
openocd.cfg
file which indicates which interface file and target file to use.
$ cat openocd.cfg
source [find interface/stlink-v2-1.cfg]
source [find target/stm32f3x.cfg]
$ openocd
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.913879
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints
On another terminal run GDB, also from the root of the template.
$ cat openocd.gdb
target remote :3333
# print demangled symbols
set print asm-demangle on
# detect unhandled exceptions and hard faults
break DefaultHandler
break UserHardFault
monitor arm semihosting enable
load
$ arm-none-eabi-gdb -x openocd.gdb target/thumbv7em-none-eabihf/debug/examples/hello
(..)
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x21dc lma 0x8000400
Loading section .rodata, size 0x6a4 lma 0x80025e0
Start address 0x800238c, load size 11392
Transfer rate: 17 KB/sec, 3797 bytes/write.
(gdb) list
470 #[no_mangle]
471 pub static __RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
472
473 #[doc(hidden)]
474 #[no_mangle]
475 pub unsafe extern "C" fn Reset() -> ! {
476 extern "C" {
477 // This symbol will be provided by the user via the `entry!` macro
478 fn main() -> !;
479
The openocd.gdb
script will connect GDB to OpenOCD and then flash the program
into the device. After that you can do a normal debugging session.
If you continue
the program past the semihosting write operation you'll see
"Hello, world" printed on the OpenOCD console.
(gdb) continue
$ openocd
(..)
Hello, world!
TODO point the reader to embedded-hal, awesome-embedded-rust, etc.
Licensed under either of
-
Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
-
MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Contribution to this crate is organized under the terms of the Rust Code of Conduct, the maintainer of this crate, the Cortex-M team, promises to intervene to uphold that code of conduct.