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
1.1.1 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
1.1.2 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 definedexception!
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_);
1.1.3 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
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
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:
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
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:
1.2 Embedonomicon Implementation
Here is another version of .data
section in Embedonomicon
.rodata {
*(.rodata .rodata.*);
} > FLASH
.data: AT(ADDR(.rodata) + SIZEOF(.rodata)) {
*(.data .data.*)
} > RAM
where AT(/* ... */)
specifies the LMA in FLASH
: at the end of the .rodata
section.
In this version, the initialisation step is performed in Rust code, before calling our user-defined entry function. ptr::write_bytes
and ptr::copy_nonoverlapping
are equivalent to the above assembly code.
#[no_mangle]
pub unsafe exterm "C" fn Reset() -> {
extern "C" {
static mut _sbss: u8;
static mut _ebss: u8;
static mut _sdata: u8;
static mut _edata: u8;
static _sidata: u8; // LMA of the .data section_
}
let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
ptr::write_bytes(&mut _sbss as *mut u8, 0, count);
let count = &_edata as *const u8 as usize - &_stada as *const u8 as usize;
ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, const);
extern "Rust" {
fn main() -> !;
}
main();
}
1.3 Customisable Layout
The user is allowed to customise the memory layout inside the memory.x
- device memory
- i.e. the
MEMORY
section
- i.e. the
- start of the stack
- It is places at the end of
RAM
by default (if omitted) - It grows downwards
- It is places at the end of
- 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
- It is placed at the beginning of
/* 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);
2. 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 special type of “exception”
- The first 0-15 interrupts are dedicated to system interrupts
- The other 16-255 interrupts are referred to as a user or peripheral interrupts ### 2.2 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
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,
}
[!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 thelink.x.in
file, where it usesLONG(_stack_start & 0xFFFFFFF8)
at the start of the.vector_table
section[!info] The Reset Exception
TheReset
routine is executed when a chip resets, it is places at the second entry of the vector table.
2.3 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]
2.4 Handlers
The user-defined handler is prefixed with __cortex_m_rt_
and the actual handler is a wrapper function post-fixed with _trampoline
and exported as the original name.
let ident = f.sig.ident.clone();
let ident_s = ident.to_string();
// ...
f.sig.ident = Ident::new(&format!("__cortex_m_rt_{}", f.sig.ident), Span::call_site());
let tramp_ident = Ident::new(&format!("{}_trampoline", f.sig.ident), Span::call_site());
let (ref cfgs, ref attrs) = extract_cfgs(f.attrs.clone());
quote!(
#(#cfgs)*
#(#attrs)*
#[doc(hidden)]
#[export_name = #ident_s]
// ...
)
The library provided default handler is simply an infinite loop
#[no_mangle]
pub unsafe extern "C" fn DefaultHandler_() -> ! {
#[allow(clippy::empty_loop)]
loop {}
}
But users can easily override it by providing their own DefaultHandler
definitions, since PROVIDE
is weak aliasing
PROVIDE(DefaultHandler = DefaultHandler_);
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 {}
}
2.5 Built-in Exceptions
2.5.1 Exception Mechanisms
Refer to the Pre-emption and Exception Exit documentation for further information
- Before entering ISR
- Push registers on selected stack (by
EXC_RETURN
) - Read the corresponding vector table entry
- Update
PC
to the handler instruction - Update
LR
(Link Register) toEXC_RETURN
- Push registers on selected stack (by
- Exiting ISR
- Pop Registers
- Load current active interrupt number. We can return to
- another exception (nested), or
- thread mode (no nested interrupt)
- Select stack pointer
- Returning to an exception:
SP <- SP_main
- Returning to thread mode:
SP <- SP_main or SP_process
- Returning to an exception:
2.5.2 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,
}
- 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 (whether to use
trampoline
) are passed as macro arguments
- The
NonMaskableInt
-errrupt exception is interrupt that cannot be disabled by settingPRIMASK
orBASEPRIO
.- 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
- Besides the
-
Other
exceptions are-
MemoryManagement
- Memory accesses that violates MMU rules
-
BusFault
- Access that violates the system bus
-
UsageFault
- Such as
divide-by-zero
exceptions
- Such as
SecureFault
-
SVCall
: invoked when making a supervisor call (svc
) DebugMonitor
-
PendSV
: raised to defer context switch to thread mode (user mode)- when multiple IRQ exceptions has been raised to minimise the IRQ delay
- refer to [this](https://www.sciencedirect.com/topics/computer-science/software-interrupt#:~:text=A%20typical%20use%20of%20PendSV,be%20triggered%20by%20the%20following%3A&text=Calling%20an%20SVC%20function&text=The%20system%20timer%20(SYSTICK) paper
-
SysTick
: system timer
-
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();
}
};
The DefaultHandler
for Exceptions
If the handler is an Exception::DefaultHandler
, it extracts the irq
number from the SCB_ICSR
register (memory-mapped) and pass it as an argument to the user-defined handler.
Note that the user-defined DefaultHandler
must has exactly one input argument, and can only have the "default", "empty tuple" not "never" return type. The constraints are checked by the macro.
f.sig.ident = Ident::new(&format!("__cortex_m_rt_{}", f.sig.ident), Span::call_site());
let tramp_ident = Ident::new(&format!("{}_trampoline", f.sig.ident), Span::call_site());
let ident = &f.sig.iden
let (ref cfgs, ref attrs) = extract_cfgs(f.attrs.clone()
quote!(
#(#cfgs)*
#(#attrs)*
#[doc(hidden)]
#[export_name = #ident_s]
pub unsafe extern "C" fn #tramp_ident() {
extern crate cor
const SCB_ICSR: *const u32 = 0xE000_ED04 as *const u3
let irqn = unsafe { (core::ptr::read_volatile(SCB_ICSR) & 0x1FF) as i16 - 16 };
#ident(irqn)
}
#f
)
The HardFault
Handler for Exceptions
We need to specify whether we need to use the trampoline
mode. If using the trampoline mode, we should check the stack mode and use the corresponding stack pointers.
The way it works is
- An assembly code marked
HardFault
which is a symbol used in the vector table - It passes the correct stack pointer and calls the trampoline code exported as
_HardFault
- The rust-written
_HardFault
calls the user-defined handler prefixed with__cirtex_m_rt_
The global HardFault
function is defined as follows.
// HardFault exceptions are bounced through this trampoline which grabs the stack pointer at
// the time of the exception and passes it to the user's HardFault handler in r0.
// Depending on the stack mode in EXC_RETURN, fetches stack from either MSP or PSP.
core::arch::global_asm!(
".cfi_sections .debug_frame
.section .HardFault.user, \"ax\"
.global HardFault
.type HardFault,%function
.thumb_func
.cfi_startproc
HardFault:",
"mov r0, lr
movs r1, #4
tst r0, r1
bne 0f
mrs r0, MSP
b _HardFault
0:
mrs r0, PSP
b _HardFault",
".cfi_endproc
.size HardFault, . - HardFault",
);
The _HardFault
function takes an ExcaptionFrame
and calls the user-defined handler.
quote!(
#(#cfgs)*
#(#attrs)*
#[doc(hidden)]
#[export_name = "_HardFault"]
unsafe extern "C" fn #tramp_ident(frame: &::cortex_m_rt::ExceptionFrame) {
#ident(frame)
}
#f
// ...
)
[!info] Preemptive Stack Framing
Refer to this documentation for further informationWhen the processor invokes an exception, it automatically pushes the following eight registers to the SP in the following order:
- Program Counter (PC)
- Processor Status Register (xPSR)
- r0-r3
- r12
- Link Register (LR).
After the push, our stack pointer now points to the
ExceptionFrame
structure
![[stack-contents.svg]]
So passing the correct stack pointer tor0
, is passing theframe
argument to the handler function
The exception frame is defined as follows
/// Registers stacked (pushed onto the stack) during an exception.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct ExceptionFrame {
r0: u32,
r1: u32,
r2: u32,
r3: u32,
r12: u32,
lr: u32,
pc: u32,
xpsr: u32,
}
If it is not using the trampoline
mode, the handler invocation is simply a function call to the handler function.
quote!(
#[export_name = "HardFault"]
// Only emit link_section when building for embedded targets,
// because some hosted platforms (used to check the build)
// cannot handle the long link section names.
#[cfg_attr(target_os = "none", link_section = ".HardFault.user")]
#f
)
NonMaskableInt
-errupts or Other
interrupts
It is allowed to use static variables inside a 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 exception(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))
}
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 exception(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()
}));
//...
}
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<_>>();
// ...
}
The final layout of the code generated
pub fn exception(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
}
2.5.3 Exception Vector Table
To construct a vector table, we declare a slice with 14 entries of such Vector
s, 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 },
];
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);
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();
}
2.6 External Interrupts
[!info] NVIC: Nested Vectored Interrupt Controller
All external interrupts are configured via the NCIV peripheral
![[Nested-vectored-interrupt-controller-NVIC-ARM-CortexM-microcontrollers.webp]]
2.6.1 External Interrupt Handlers
The codegen for external interrupt handlers are more or less the same for Other
interrupts.
For more information, please refer to the Interrupt Macro Documentation
2.6.2 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];
Top comments (0)