DEV Community

Jens
Jens

Posted on

Under the Hook - The Cortex Runtime

Refer to the cortex-m-rt documentation and its source code for more information.

1. Memory Layout

To understand more about the memory layout on ARM Cortex-M devices, let's look at the link.x.in and memory.x file.

1.1 Linker Script

according to the developer notes in the linker.x.in file, we make a few clarifications

Naming Conventions of Linker Symbols

We need to clarify here, that,

  • symbols with the _ (single underscore) prefix are semi-public, in the way that:
    • users can ONLY override them in the linker script
    • users are NOT allowed to reference them in rust code as external symbols
  • symbols with the __ (double underscore) prefix are private, and
    • users should NOT in any way use or modify it

Weak Aliasing at the Linker Script Level

Symbols using PROVIDE are "weak aliases at the linker level", so that "users can override any of these aliases by defining the corresponding symbol themselves" .

  • EXTERN forces the linker to keep a symbol in the final binary (even if it is not needed)
  • PROVIDE provides default values that can be overridden by a user linker script For example, the each exception vectors can be overridden with use defined exception! macros
EXTERN(__EXCEPTIONS); /* depends on all the these PROVIDED symbols */

EXTERN(DefaultHandler);

PROVIDE(NonMaskableInt = DefaultHandler);
EXTERN(HardFaultTrampoline);
PROVIDE(MemoryManagement = DefaultHandler);
PROVIDE(BusFault = DefaultHandler);
PROVIDE(UsageFault = DefaultHandler);
PROVIDE(SecureFault = DefaultHandler);
PROVIDE(SVCall = DefaultHandler);
PROVIDE(DebugMonitor = DefaultHandler);
PROVIDE(PendSV = DefaultHandler);
PROVIDE(SysTick = DefaultHandler);

PROVIDE(DefaultHandler = DefaultHandler_);
PROVIDE(HardFault = HardFault_);
Enter fullscreen mode Exit fullscreen mode

Virtual and Load Memory Addresses

The location counter ALWAYS tracks VMA, and
To use LMA, use LOADADDR(...)

Code Section

The .text section only needs to be stored in the FLASH memory because it is only a sequence of read-only executable code.

.text {
    *(.text)
} > FLASH
Enter fullscreen mode Exit fullscreen mode

Since the .text section resides in the FLASH memory, there's no need of a write permission, we do not need to do anything special when booting.

Initialised Data Section

The .data section (for initialised static data). It should be read from the FLASH memory (Read Only) into the RAM (Read/Write). The loader needs to know its compile-time initialised value and its RAM location, so that when loading the data, it will copy it directly from FLASH to RAM.

.data {
    *(.data)
} > RAM AT> FLASH
Enter fullscreen mode Exit fullscreen mode

Here is how the .data section is initialised
The .data section needs to have a copy of initialised values of a static variable stored in FLASH, but it is mutable, so we need to copy it into our memory.
Note that r0 = __sdata and r1 = __edata are assigned as the VMA value (in RAM) of the variable, while r2 = __sidata is the LMA value (in FLASH) of the variable.

3:
    ldr r0, =__sdata
    ldr r1, =__edata
    ldr r2, =__sidata
4:
    cmp r1, r0
    beq 5f
    ldm r2!, {{r3}}
    stm r0!, {{r3}}
    b 4b
5:
Enter fullscreen mode Exit fullscreen mode
Uninitialised Data Section

The .bss section contains all uninitialised static data, it needs not to be stored in the FLASH because the loader only needs to know its address in RAM, such that when loading the address, the loader will assign it the default value.

.bss {
    *(.bss)
} > RAM
Enter fullscreen mode Exit fullscreen mode

Here is how the .bss section is initialised
As is introduced above, .bss saves the uninitialised static variables, it only records its VMA (i.e. the destination address in RAM), and is initialised with zeros, here's the code

1:
    ldr r0, =__sbss
    ldr r1, =__ebss
    movs r2, #0
2:
    cmp r1, r0
    beq 3f
    stm r0!, {{r2}}
    b 2b
3:
Enter fullscreen mode Exit fullscreen mode

Customisable Layout

The user is allowed to customise the memory layout inside the memory.x

  • device memory
    • i.e. the MEMORY section
  • start of the stack
    • It is places at the end of RAM by default (if omitted)
    • It grows downwards
  • start of the code (.text) section
    • It is placed at the beginning of FLASH by default (if omitted) Here is an example of such file
/* Linker script for the STM32F303VCT6 */
MEMORY
{
    FLASH : ORIGIN = 0x08000000, LENGTH = 256K

    /* .bss, .data and the heap go in this region */
    RAM : ORIGIN = 0x20000000, LENGTH = 40K

    /* Core coupled (faster) RAM dedicated to hold the stack */
    CCRAM : ORIGIN = 0x10000000, LENGTH = 8K
}

_stack_start = ORIGIN(CCRAM) + LENGTH(CCRAM);
Enter fullscreen mode Exit fullscreen mode

2. The Booting Process

The overall layout can be found in link.x.in

2.1 Initialising the Stack Pointer

We use the _stack_start symbol in the assembly code to load it into out stack pointer

ldr r0, =_stack_start
msr msp, r0
Enter fullscreen mode Exit fullscreen mode

Exception and Interrupt Handling

An overview of ARM Cortex-M exception handling can be found in Chris Coleman: A Practical guide to ARM Cortex-M Exception Handling. Most explanations related to the exception/interrupts and their handling mechanisms are __borrowed_ from this post.

[!info] Exception and Interrupt in ARM
in the ARM documentation, “interrupt” is used to describe a type of “exception”

Vector Table

A vector table stores the address of the handler routines for its corresponding exceptions/interrupts.

The in-memory layout (FLASH memory) of the vector table is as follows.

/* ## Sections in FLASH */
/* ### Vector table */
.vector_table ORIGIN(FLASH) :
{
  __vector_table = .;
  /* Initial Stack Pointer (SP) value.
   * We mask the bottom three bits to force 8-byte alignment.
   * Despite having an assert for this later, it's possible that a separate
   * linker script could override _stack_start after the assert is checked.
   */
  LONG(_stack_start & 0xFFFFFFF8);
  /* Reset vector */
  KEEP(*(.vector_table.reset_vector)); /* this is the `__RESET_VECTOR` symbol */
  /* Exceptions */
  __exceptions = .; /* start of exceptions */
  KEEP(*(.vector_table.exceptions)); /* this is the `__EXCEPTIONS` symbol */
  __eexceptions = .; /* end of exceptions */
  /* Device specific interrupts */
  KEEP(*(.vector_table.interrupts)); /* this is the `__INTERRUPTS` symbol */
} > FLASH
Enter fullscreen mode Exit fullscreen mode

In rust code, each vector is represented by the Vector struct. It is a Union type, can either be a handler pointer or reserved (as in reserved, or not used, by the ARM architecture)

#[repr(C)]
pub union Vector {
    handler: unsafe extern "C" fn(),
    reserved: usize,
}
Enter fullscreen mode Exit fullscreen mode

[!Info] Initial Stack Pointer
the first entry of the index table holds the reset (initial) value of the stack pointer (8-bit aligned).
We can find it in the link.x.in file, where it uses LONG(_stack_start & 0xFFFFFFF8) at the start of the .vector_table section

[!info] The Reset Exception
The Reset routine is executed when a chip resets, it is places at the second entry of the vector table.

Activation of Vector Tables

The address of the memory-mapped register VTOR (Vector Table Offset Register) is 0xe000ed08.

To set its value, we simply write to the address of our vector table (provided by the linker symbol__vector_table) to the corresponding VTOR address.

ldr r0, = 0xe000ed08
ldr r1, =__vector_table
str r1, [r0]
Enter fullscreen mode Exit fullscreen mode

Built-in Exceptions

[!info] SCS: System Control Space

Exception Handlers

For more information, please refer to the Exception Macro Documentation

Exception handlers can ONLY be called by hardware.

Exception handlers are divided into four categories, namely, the DefaultHandler, the HardFault handler, the NonMaskableInt-errupt exception handler , and Other exception handler.

#[derive(Debug, PartialEq)]
enum Exception {
    DefaultHandler,
    HardFault(HardFaultArgs),
    NonMaskableInt,
    Other,
}
Enter fullscreen mode Exit fullscreen mode
  • The HardFault exception is the catch all exception for assorted system failures
    • such as divide-by-zero exception, bad memory accesses etc.
    • Finer granularity fault handlers: MemManage, BusFault etc.
    • Its arguments are passed as macro arguments
  • The NonMaskableInt-errrupt exception is interrupt that cannot be disabled by setting PRIMASK or BASEPRIO.
    • Besides the Reset exception, it has the highest priority of all other exceptions
    • It is NOT generally safe to define handlers for non-maskable interrupt since it may break critical sections
    • Its implementation should guarantee that: it does not access any data that is
      • protected by a critical section, and
      • shared with other interrupts that may be preempted by the NMI handler
  • Other exceptions are
    • MemoryManagement
    • BusFault
    • UsageFault
    • SecureFault
    • SVCall: invoked when making a supervisor call (svc)
    • DebugMonitor
    • PendSV: system-level interrupts triggered by software
      •  Typically used by a RTOS to manage when the scheduler runs and when context switches take place.
    • SysTick: system-level interrupts triggered by software
      •  Typically used by a RTOS to manage when the scheduler runs and when context switches take place.

The parsing code makes sure that they are checked

let exn = match &*ident_s {
    "DefaultHandler" => {
        if !args.is_empty() {
            return parse::Error::new(Span::call_site(), "This attribute accepts no arguments")
                .to_compile_error()
                .into();
        }
        Exception::DefaultHandler
    }
    "HardFault" => Exception::HardFault(parse_macro_input!(args)),
    "NonMaskableInt" => {
        if !args.is_empty() {
            return parse::Error::new(Span::call_site(), "This attribute accepts no arguments")
                .to_compile_error()
                .into();
        }
        Exception::NonMaskableInt
    }
    // NOTE that at this point we don't check if the exception is available on the target (e.g.
    // MemoryManagement is not available on Cortex-M0)
    "MemoryManagement" | "BusFault" | "UsageFault" | "SecureFault" | "SVCall"
    | "DebugMonitor" | "PendSV" | "SysTick" => {
        if !args.is_empty() {
            return parse::Error::new(Span::call_site(), "This attribute accepts no arguments")
                .to_compile_error()
                .into();
        }
        Exception::Other
    }
    _ => {
        return parse::Error::new(ident.span(), "This is not a valid exception name")
            .to_compile_error()
            .into();
    }
};
Enter fullscreen mode Exit fullscreen mode

Exception Vector Table

To construct a vector table, we declare a slice with 14 entries of such Vectors, and place it in the .vector_table.exceptions section, which is defined in the linker script.

The entries to the exception vector tables are stored in__EXCEPTIONS (apart from the two above-mentioned entries)

#[cfg_attr(cortex_m, link_section = ".vector_table.exceptions")]
#[no_mangle]
pub static __EXCEPTIONS: [Vector; 14] = [
    // Exception 2: Non Maskable Interrupt.
    Vector {
        handler: NonMaskableInt,
    },
    // Exception 3: Hard Fault Interrupt.
    Vector { handler: HardFault },
    // Exception 4: Memory Management Interrupt [not on Cortex-M0 variants].
    #[cfg(not(armv6m))]
    Vector {
        handler: MemoryManagement,
    },
    #[cfg(armv6m)]
    Vector { reserved: 0 },
    // Exception 5: Bus Fault Interrupt [not on Cortex-M0 variants].
    #[cfg(not(armv6m))]
    Vector { handler: BusFault },
    #[cfg(armv6m)]
    Vector { reserved: 0 },
    // Exception 6: Usage Fault Interrupt [not on Cortex-M0 variants].
    #[cfg(not(armv6m))]
    Vector {
        handler: UsageFault,
    },
    #[cfg(armv6m)]
    Vector { reserved: 0 },
    // Exception 7: Secure Fault Interrupt [only on Armv8-M].
    #[cfg(armv8m)]
    Vector {
        handler: SecureFault,
    },
    #[cfg(not(armv8m))]
    Vector { reserved: 0 },
    // 8-10: Reserved
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    // Exception 11: SV Call Interrupt.
    Vector { handler: SVCall },
    // Exception 12: Debug Monitor Interrupt [not on Cortex-M0 variants].
    #[cfg(not(armv6m))]
    Vector {
        handler: DebugMonitor,
    },
    #[cfg(armv6m)]
    Vector { reserved: 0 },
    // 13: Reserved
    Vector { reserved: 0 },
    // Exception 14: Pend SV Interrupt [not on Cortex-M0 variants].
    Vector { handler: PendSV },
    // Exception 15: System Tick Interrupt.
    Vector { handler: SysTick },
];
Enter fullscreen mode Exit fullscreen mode

These handler symbols are by default the DefaultHandler, as declared in the linker script

PROVIDE(NonMaskableInt = DefaultHandler);
PROVIDE(MemoryManagement = DefaultHandler);
PROVIDE(BusFault = DefaultHandler);
PROVIDE(UsageFault = DefaultHandler);
PROVIDE(SecureFault = DefaultHandler);
PROVIDE(SVCall = DefaultHandler);
PROVIDE(DebugMonitor = DefaultHandler);
PROVIDE(PendSV = DefaultHandler);
PROVIDE(SysTick = DefaultHandler);
Enter fullscreen mode Exit fullscreen mode

These are their signatures in rust

extern "C" {
    fn Reset() -> !;
    fn NonMaskableInt();
    fn HardFault();
    #[cfg(not(armv6m))]
    fn MemoryManagement();
    #[cfg(not(armv6m))]
    fn BusFault();
    #[cfg(not(armv6m))]
    fn UsageFault();
    #[cfg(armv8m)]
    fn SecureFault();
    fn SVCall();
    #[cfg(not(armv6m))]
    fn DebugMonitor();
    fn PendSV();
    fn SysTick();
}
Enter fullscreen mode Exit fullscreen mode

External Interrupts

[!info] NVIC: Nested Vectored Interrupt Controller
All external interrupts are configured via the NCIV peripheral

External Interrupt Handlers

For more information, please refer to the Interrupt Macro Documentation

It is allowed to use static variables inside a interrupt handler. Every static variables are shared across each invocation of its handler.
It is achieved by converting static mut into mut &, and passing it as an argument to the interrupt handler (Its signature is extended to incorporate these extra arguments, and the original declarations are removed from the function).

We can use a code snipped of the interrupt macro to explain the process.

Firstly, we extract every static mut declaration in the block statements of the function

pub fn interrupt(args: TokenStream, input: TokenStream) -> TokenStream {
    // ...
    let (statics, stmts) = match extract_static_muts(f.block.stmts.iter().cloned()) {
        Err(e) => return e.to_compile_error().into(),
        Ok(x) => x,
    };
    //...
}

/// Extracts `static mut` vars from the beginning of the given statements
fn extract_static_muts(
    stmts: impl IntoIterator<Item = Stmt>,
) -> Result<(Vec<ItemStatic>, Vec<Stmt>), parse::Error> {
    let mut istmts = stmts.into_iter();

    let mut seen = HashSet::new();
    let mut statics = vec![];
    let mut stmts = vec![];
    for stmt in istmts.by_ref() {
        match stmt {
            Stmt::Item(Item::Static(var)) => match var.mutability {
                syn::StaticMutability::Mut(_) => {
                    if seen.contains(&var.ident) {
                        return Err(parse::Error::new(
                            var.ident.span(),
                            format!("the name `{}` is defined multiple times", var.ident),
                        ));
                    }

                    seen.insert(var.ident.clone());
                    statics.push(var);
                }
                _ => stmts.push(Stmt::Item(Item::Static(var))),
            },
            _ => {
                stmts.push(stmt);
                break;
            }
        }
    }

    stmts.extend(istmts);

    Ok((statics, stmts))
}
Enter fullscreen mode Exit fullscreen mode

Then we can push these static mut variables to the input argument list of the function. Note that the input field of a function signature is of type FnArg. It is either a receiver, i.e. all kinds of self, or a typed argument.

/// An argument in a function signature: the `n: usize` in `fn f(n: usize)`.
pub enum FnArg {
    /// The `self` argument of an associated method.
    Receiver(Receiver),
    /// A function argument accepted by pattern and type.
    Typed(PatType),
}

pub fn interrupt(args: TokenStream, input: TokenStream) -> TokenStream {
    // ...
    f.sig.inputs.extend(statics.iter().map(|statik| {
        let ident = &statik.ident;
        let ty = &statik.ty;
        let attrs = &statik.attrs;
        syn::parse::<FnArg>(quote!(#[allow(non_snake_case)] #(#attrs)* #ident: &mut #ty).into())
            .unwrap()
    }));
    //...
}
Enter fullscreen mode Exit fullscreen mode

Now, we can construct a wrapper function {interrupt-name}_trampoline as the corresponding interrupt handler by exporting it as {interupt-name}. The wrapper handler calls our user-defined interrupt handler, which is now called __cortex_m_rt_{interrupt-name}. The static mut resources are passed as arguments to our user-defined handler.

The arguments are constructed in a block which evaluates to the &mut value

pub fn interrupt(args: TokenStream, input: TokenStream) -> TokenStream {
    // ...
    let resource_args = statics
        .iter()
        .map(|statik| {
            let (ref cfgs, ref attrs) = extract_cfgs(statik.attrs.clone());
            let ident = &statik.ident;
            let ty = &statik.ty;
            let expr = &statik.expr;
            quote! {
                #(#cfgs)*
                {
                    #(#attrs)*
                    static mut #ident: #ty = #expr;
                    &mut #ident
                }
            }
        })
        .collect::<Vec<_>>();

    // ...
}
Enter fullscreen mode Exit fullscreen mode

The final layout of the code generated

pub fn interrupt(args: TokenStream, input: TokenStream) -> TokenStream {
    // ...
    quote!(
        #(#cfgs)*
        #(#attrs)*
        #[doc(hidden)]
        #[export_name = #ident_s]
        pub unsafe extern "C" fn #tramp_ident() {
            #ident(
                #(#resource_args),*
            )
        }

        #f
    )
    .into()
    // end of function
}
Enter fullscreen mode Exit fullscreen mode

External Interrupt Vector Table

It is initialised all interrupt handlers to the DefaultHandler declared in src/lib.rs.

#[cfg(all(any(not(feature = "device"), test), not(armv6m)))]
#[cfg_attr(cortex_m, link_section = ".vector_table.interrupts")]
#[no_mangle]
pub static __INTERRUPTS: [unsafe extern "C" fn(); 240] = [{
    extern "C" {
        fn DefaultHandler();
    }

    DefaultHandler
}; 240];
Enter fullscreen mode Exit fullscreen mode

Default Handlers

The library provided default handler is simply an infinite loop

#[no_mangle]
pub unsafe extern "C" fn DefaultHandler_() -> ! {
    #[allow(clippy::empty_loop)]
    loop {}
}
Enter fullscreen mode Exit fullscreen mode

But users can easily override it by providing their own DefaultHandler definitions, since PROVIDE is weak aliasing

PROVIDE(DefaultHandler = DefaultHandler_);
Enter fullscreen mode Exit fullscreen mode

The default handler for HardFault is also an infinite loop

#[cfg_attr(cortex_m, link_section = ".HardFault.default")]
#[no_mangle]
pub unsafe extern "C" fn HardFault_() -> ! {
    #[allow(clippy::empty_loop)]
    loop {}
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)