Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
1 result

labs

  • Clone with SSH
  • Clone with HTTPS
  • user avatar
    Emil Kitti authored
    dcdc6737
    History

    crates.io crates.io

    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 both semihosting and ITM).

    • 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 a C 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 over itm and semihosting

    • 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.
    • a file r demonstrating gdb 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 of events, 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 a reset-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 user main 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 the exception and interrupt handler addresses. In total 0x400 bytes (in hex), i.e., 256 word size vectors (1k byte).
    • .text section holds the program code (code from cortex-m-rt and the user main in our case). In total 0x52e bytes (in hex).
    • .rodata section, constants (including X_INIT). In total 0xb8 bytes.
    • .bss section, zero-initated static heap variables. Here the Y is stored (u32 is 4 bytes).
    • .data section, static heap variables that is initiated at run-time (by cortex-m-rt before user main 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 an Option that’s None when overflow occurs, and Some(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.

    Debugging the bare0 example

    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, connects gdb to the target.
    • monitor reset init, resets the MCU and sets the clock to 64MHz.
    • load, loads the bare0 (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

    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.