rtfm-app
Real Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollers
Documentation
Features
This is a crate useful as an introduction for bare metal programming. It targets the STM32F401re/STM32F11re Nucleo platform, but examples are mostly generic to any STMF4xx.
The set of examples covers.
-
bare0
, a minimal bare metal application in Rust. Here we will discuss in detail how the code is compiled into a binary, and how the genertad binary actually looks like in detail. We will also look at how the binary can be loaded onto the target MCU, and executed/debugged. -
bare1
, extends the minimal example by tracing printouts to the host (over bothsemihosting
andITM
). -
bare2
, shows how raw access to peripherals is possible, and blink a LED. The problemen here is that its easy to make mistakes (and hard to find errors). -
bare3
, here we develop aC
like API for accessing register blocks in a structured way. Even though the programming API is offers some help to the progrmammer, it is still easy to make mistakes, (like writing to the wrong bit-field, or writing an illegal value). -
bare4
, here we look at an API, machine generated from the vendors discription of the MCU. Using this API, bare metal programming becomes less error prone (fields are correctly aligned and values always in range). -
an
app!
system configuration with three task with two shared resources. -
the use of the
cortex-m-debug
crate for simple tracing overitm
andsemihosting
-
vscode
integration:- Task configurations:
ctrl-shift-b
for building (Debug/Release) - Launch configurations:
ctrl-shift-d
for entering Debug. Select (Debug/Release) and press arrow to start.
- Task configurations:
-
a file
r
demonstratinggdb
automation.
Requirements
- The
Native Debug
extension. - A running
gdb
server (e.g.,openocd
), connecting on port 3333. -
ITM
tracing truogh the file/tmp/itm.log
. - Access to
arm-none-eabi-gdb
.
ITMDump
Install via
cargo install itm
Note Version 0.1.1 of ITM has a different syntax, the latest, version 0.2.0, see below.
Before running openocd run:
If this is the first time or if no /tmp/itm.log exists:
> mkfifo /tmp/itm.log
When the FIFO-file exists, do the following:
> itmdump -Ff /tmp/itm.log
ITM Version 0.1.1:
>itmdump /tmp/itm.log
Openocd
Start openocd
in a terminal window. (Status and semihosting output is visible here.)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
The configuration broken down:
-
stlink.cfg
defines the specifics for the STMicroelectronics ST-LINK/V1, ST-LINK/V2, ST-LINK/V2-1 in-circuit debugger/programmer interface.
interface hla
hla_layout stlink
hla_device_desc "ST-LINK"
hla_vid_pid 0x0483 0x3744 0x0483 0x3748 0x0483 0x374b
hla
relates to the interface driver (low level transport in between openocd
and the st-link programmer).
-
stm32f4x.cfg
is a script for the stm32f4x family, defining memory areas etc. It also defines the handling ofevents
, e.g.,-
trace-config
- enbling tracing. -
reset-start
- requesting adapter speed to a (conservative) 2000kHz (actual speed will be negotiated). -
reset-init
- setting the CPU to 64MHz (its 16MHz by default), and requesting adapter speed to 8000kHz (actual speed will be negotiated). -
examine-end
- debug behavior:- Enable debug during low power modes (uses more power).
- Stop watchdog counters during halt.
-
Launch configuration
{
"type": "gdb",
"request": "attach",
"name": "Debug",
"gdbpath": "/usr/bin/arm-none-eabi-gdb",
"executable": "./target/thumbv7em-none-eabihf/debug/app",
"target": ":3333",
"remote": true,
"autorun": [
"monitor reset init",
"monitor arm semihosting enable",
"monitor tpiu config internal /tmp/itm.log uart off 64000000",
"monitor itm port 0 on",
"load",
"monitor reset init"
],
"cwd": "${workspaceRoot}"
},
...
}
The autorun script broken down:
-
monitor reset init
Halts the processor and applies the
reset-init
configuration. -
monitor arm semihosting enable
Enables
semihosting
(breakpoint
handling for semihosting). -
monitor tpiu config internal /tmp/itm.log uart off 64000000
-
monitor itm port 0 on
Enables
itm
debugging assuming a clock speed of 64MHz. -
load
Loads the binary (set in "executable": "./target/thumbv7em-none-eabihf/debug/app") to the target.
load
performs areset-start
after finishing. -
monitor reset init
Set the MCU to 64MHz (again).
GDB
The gdb
tool allows to program and debug the target, e.g., set breakpoints, inspect memory, etc. gdb
can be highly automated through scripting. In the DEBUG
console you can interact directly with gdb
. The integration between the console and vscode
is rudimentary. The vscode
Native Debug
view is in general not notified/updated (by stepping/continuing through vscode
icons/shortcuts the view gets in synch). Even so, manual interaction with gdb
can be very useful, an example is to restart the program without reloading it (there is no shortcut/icon for this functionality in the Native Debug
extension). Enter in the DEBUG
console:
source r
or simply:
so r
gdb
commands can typically be abbreviated, in this case so
for source
. Notice, you may run gdb
(arm-none-eabi-gdb
) directly in a terminal, in this case you get a both tabbing of commands and history (e.g., pressing enter repeats the last command).
The content of the file r
is simply:
monitor reset init
Examples
In the examples
folder you find:
bare0.rs
This is a simple bare metal applicaiton:
//! bare0.rs
//! Simple bare metal application
// feature to ensure symbols to be linked
#![feature(used)]
// build without the Rust standard library
#![no_std]
// Minimal runtime / startup for Cortex-M microcontrollers
extern crate cortex_m_rt;
static mut X: u32 = 10;
fn main() {
let mut x = unsafe { X };
loop {
x += 1;
unsafe {
X += 1;
assert!(x == X);
}
}
}
// As we are not using interrupts, we just register a dummy catch all handler
#[link_section = ".vector_table.interrupts"]
#[used]
static INTERRUPTS: [extern "C" fn(); 240] = [default_handler; 240];
extern "C" fn default_handler() {}
-
The cortex-m-rt crate provides a minimalistic runtime and startup of the ARM Cortex M. Providing:
- Linker script, telling the linker howto generate the binary.
- The
reset
handler, initiating the memory (and FPU if available on the target), and calls the usermain
function. - Default exception handlers.
-
The user defined
main
function defines the applicaiton behavior. -
User defined interrupt handlers, in this case just dummy handlers (never returning).
As seen, we need to tell the compiler in what section
the interrupt "vector" (array) should go. The ARM-Cortex-M architecture defines 16 exceptions and serves a maximum of 240 interrupt sources.
The target specific memory layout is given by the file memory.x
.
/* STM32F401re */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 96K
}
Compilng and Inspecting the binary
First compile the example either through the vscode
Tasks (Ctrl-Shift-B) or using the console:
> xargo build --example bare0
Before running the program, we can have a look at the genertate binary.
> arm-none-eabi-objdump -h target/thumbv7em-none-eabihf/debug/examples/bare0
target/thumbv7em-none-eabihf/debug/examples/bare0: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .vector_table 00000400 08000000 08000000 00010000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .text 0000052e 08000400 08000400 00010400 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .rodata 000000b8 08000930 08000930 00010930 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .bss 00000004 20000000 20000000 00030000 2**2
ALLOC
4 .data 00000004 20000004 080009e8 00020004 2**2
CONTENTS, ALLOC, LOAD, DATA
...
target/thumbv7em-none-eabihf/debug/examples/bare0: file format elf32-littlearm
-
.vector_table
section contains theexception
andinterrupt
handler addresses. In total 0x400 bytes (in hex), i.e., 256 word size vectors (1k byte). -
.text
section holds the program code (code fromcortex-m-rt
and the usermain
in our case). In total 0x52e bytes (in hex). -
.rodata
section, constants (includingX_INIT
). In total 0xb8 bytes. -
.bss
section, zero-initated static heap variables. Here theY
is stored (u32
is 4 bytes). -
.data
section, static heap variables that is initiated at run-time (bycortex-m-rt
before usermain
is called.
We can further inspect the binary:
> arm-none-eabi-objdump -s -j .vector_table target/thumbv7em-none-eabihf/debug/examples/bare0
target/thumbv7em-none-eabihf/debug/examples/bare0: file format elf32-littlearm
Contents of section .vector_table:
8000000 00800120 01040008 c3070008 c3070008 ... ............
8000010 c3070008 c3070008 c3070008 00000000 ................
8000020 00000000 00000000 00000000 c3070008 ................
8000030 c3070008 00000000 c3070008 c3070008 ................
8000040 5f050008 5f050008 5f050008 5f050008 _..._..._..._...
target/thumbv7em-none-eabihf/debug/examples/bare0: file format elf32-littlearm
Gives a raw dump of the .vector_table
. The first 16 entries are the exception handlers, where the first entry (0x0800000) is the stack pointer (0x00800120 raw = 0x20018000 interpreted as little-endian). The stack pointer is set just outside the ram area (96kb starting at 0x20000000), recall that the stack pointer is decremented first upon stack allocations so this is "safe". The second vector (0x0800004), is the reset
vector (0x80000401). (The odd address casts the processor into thumb
mode using 16 bit instructions for higher code density.)
An even more detailed view can be obtianed:
arm-none-eabi-objdump -C -d -j .vector_table target/thumbv7em-none-eabihf/debug/examples/bare0
target/thumbv7em-none-eabihf/debug/examples/bare0: file format elf32-littlearm
Disassembly of section .vector_table:
08000000 <_svector_table>:
8000000: 20018000 .word 0x20018000
08000004 <cortex_m_rt::RESET_VECTOR>:
8000004: 08000401 ....
08000008 <EXCEPTIONS>:
8000008: 080007c3 080007c3 080007c3 080007c3 ................
8000018: 080007c3 00000000 00000000 00000000 ................
8000028: 00000000 080007c3 080007c3 00000000 ................
8000038: 080007c3 080007c3 ........
08000040 <_eexceptions>:
8000040: 0800055f .word 0x0800055f
This is more informative, here we see the address as interpreted in little-endian. also we see, that the cortex_m_rt::RESET_VECTOR
is located at address 08000004 (pointing to the handler located at 0x08000401.). The interrupt vectors all point to
0x0800055f (odd address here for thumb
mode).
Now lets have a closer look at the user reset
(startup code):
arm-none-eabi-objdump -C -S target/thumbv7em-none-eabihf/debug/examples/bare0 > objdump.out
In the objdump.out
file we find:
08000400 <cortex_m_rt::reset_handler>:
/// The reset handler
///
/// This is the entry point of all programs
#[cfg(target_arch = "arm")]
#[link_section = ".reset_handler"]
unsafe extern "C" fn reset_handler() -> ! {
8000400: b580 push {r7, lr}
8000402: 466f mov r7, sp
8000404: b082 sub sp, #8
r0::zero_bss(&mut _sbss, &mut _ebss);
8000406: f240 0000 movw r0, #0
800040a: f2c2 0000 movt r0, #8192 ; 0x2000
800040e: f240 0104 movw r1, #4
8000412: f2c2 0100 movt r1, #8192 ; 0x2000
8000416: f000 f8c3 bl 80005a0 <r0::zero_bss>
800041a: e7ff b.n 800041c <cortex_m_rt::reset_handler+0x1c>
r0::init_data(&mut _sdata, &mut _edata, &_sidata);
...
main()
8000458: f000 f99d bl 8000796 <cortex_m_rt::reset_handler::main>
800045c: e7ff b.n 800045e <_einterrupts+0x5e>
}
}
// If `main` returns, then we go into "reactive" mode and simply attend
// interrupts as they occur.
loop {
800045e: e7ff b.n 8000460 <_einterrupts+0x60>
asm!("wfi" :::: "volatile");
8000460: bf30 wfi
loop {
8000462: e7fd b.n 8000460 <_einterrupts+0x60>
Here we see that the reset_handler
intitates static heap variables (by 0 or value accordingly) before calling main
(and puts the processor in sleep mode in case main
returns). The user main
has the symbol bare0::main
, which is unknown to the cortex-m-rt
library (defining the generic reset_handler
). Hence the, reset_handler
calls a compiler generated start item main
, that in turn trampolines to bare0::main
(the user main
).
Finally, we can have a look at the user main
.
0800048e <bare0::main>:
const X_INIT: u32 = 10;
static mut X: u32 = X_INIT;
static mut Y: u32 = 0;
fn main() {
800048e: b580 push {r7, lr}
8000490: 466f mov r7, sp
8000492: b084 sub sp, #16
let mut x = unsafe { X };
8000494: f240 0004 movw r0, #4
8000498: f2c2 0000 movt r0, #8192 ; 0x2000
800049c: 6800 ldr r0, [r0, #0]
800049e: 9002 str r0, [sp, #8]
loop {
80004a0: e7ff b.n 80004a2 <bare0::main+0x14>
x += 1;
80004a2: 9802 ldr r0, [sp, #8]
80004a4: 1c41 adds r1, r0, #1
...
We see that the compiler has genereted quite some code for the small main
function.
We can compile the bare0
application in -- release mode instead.
> xargo build --release --example bare0
> arm-none-eabi-objdump -S -C target/thumbv7em-none-eabihf/release/examples/bare0 > objdump_release
The resulting user main now looks like this.
8000476 <_ZN5bare04main17h1039292c3948856dE.llvm.453292E5>:
loop {
x += 1;
unsafe {
X += 1;
Y = X;
assert!(x == X && X == Y);
8000476: e7fe b.n 8000476 <_ZN5bare04main17h1039292c3948856dE.llvm.453292E5>
The compiler has optimized away ALL your code!!! (8000476: e7fe b.n 8000476
implements an infinite loop). Why you might ask, well it figures out that your the program has no observable effect (it will not output or change anything).
We can make a small change to the program (violating the assertion, by commenting out X += 1;
. After compilation the oject dump of user main looks like this:
08000476 <_ZN5bare04main17h1039292c3948856dE.llvm.E032C40E>:
static mut X: u32 = X_INIT;
static mut Y: u32 = 0;
#[inline(never)]
fn main() {
8000476: b580 push {r7, lr}
8000478: 466f mov r7, sp
loop {
x += 1;
unsafe {
//X += 1;
Y = X;
assert!(x == X && X == Y);
800047a: f240 5034 movw r0, #1332 ; 0x534
800047e: f6c0 0000 movt r0, #2048 ; 0x800
8000482: f000 f802 bl 800048a <core::panicking::panic>
8000486: defe udf #254 ; 0xfe
So the Rust compiler is able to figure out that the assertion will be violated and merely calls the panic
routine. So be aware, the Rust compiler is extremely aggressive in optimizing your code.
On a side note: the semantics of integer additions is slightly different between dev
(normal/non-optimized) and --release
(optimized) builds. In dev
build the arithmics are checked and overflows result in a panic
, in --release
, arithmetics are unchecked (for performance reasons), and oveflows wrap (under two's complement semantics). To avoid ambiguity, you may use methods defined in the std/core library:
-
wrapping_add
,wrapping_sub
,returns the straight two’s complement result, -
saturating_add
,saturating_sub
, returns the largest/smallest value (as appropriate) of the type when overflow occurs, -
overflowing_add
,overflowing_sub
, returns the two’s complement result along with a boolean indicating if overflow occured, and -
checked_add
,checked_sub
, returns anOption
that’sNone
when overflow occurs, andSome(v)
elsewise.
Those methods never panic
, but code might be verbose, e.g., expressing x - y + z
under wrapping arithmetics equates to x.wrapping_sub(y).wrapping_add(z)
. To this end you may choose to use the Wrapping type.
bare0
example
Debugging the Openocd should run in a console:
> openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
For this example let us run gdb
from console:
> arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/examples/bare0
...
(gdb) target remote :3333
...
(gdb) monitor reset init
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x52e lma 0x8000400
Loading section .rodata, size 0xb8 lma 0x8000930
Loading section .data, size 0x4 lma 0x80009e8
Start address 0x8000400, load size 2538
Transfer rate: 4 KB/sec, 634 bytes/write.
(gdb) si
0x08000402 335 unsafe extern "C" fn reset_handler() -> ! {
-
target remote :3333
, connectsgdb
to the target. -
monitor reset init
, resets the MCU and sets the clock to 64MHz. -
load
, loads thebare0
(elf
) binary to the target. -
si
, steps one machine (assembly) instruction.
We see now that we are in the reset_handler
(defined in the cortex-m-rt
crate).
(gdb) b bare0::main
Breakpoint 1 at 0x8000494: file examples/bare0.rs, line 19.
(gdb) c
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, bare0::main () at examples/bare0.rs:19
warning: Source file is more recent than executable.
19 let mut x = unsafe { X };
(gdb) p bare0::X
$1 = 10
(gdb) p bare0::Y
$2 = 0
(gdb) p b
No symbol 'b' in current context
(gdb) p x
No symbol 'x' in current context
(gdb)
License
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.
Contribution
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.