Booting a Cortex-M Microcontroller
In this deck, we’re talking specifically about Arm Cortex-M based microcontrollers.
Other Arm processors, and processors from other companies may vary.
Terms
- Processor - the core that executes instructions
- SoC - the system-on-a-chip that contains a processor, some peripherals, and usually some memory
- Flash - the flash memory that the code and the constants live in
- RAM - the random-access memory that the global variables, heap and stack live in
An example
- Arm Cortex-M4 - a 32-bit processor core aimed at microcontrollers
- Use the
thumbv7em-none-eabiorthumbv7em-none-eabihftargets
- Use the
- nRF52840 - a SoC from Nordic Semi that uses that processor core
An example (2)
- Arm Cortex-M0+ - a smaller, simpler, 32-bit processor core
- Use the
thumbv6m-none-eabitarget
- Use the
- RP2040 - a SoC from Raspberry Pi that has two of those processor cores
Booting a Cortex-M
The Arm Architecture Reference Manual explains we must provide:
The chip does everything else.
Note:
There are fourteen defined Exception Handlers (if the chip does not support a particular Exception, you must use the special value 0x0000_0000). The number of interrupt handlers is defined by the SoC - the Arm NVIC can handle up to 240 interrupts in Armv7-M or 480 interrupts in Armv8-M.
The steps
- Make an array, or struct, with those two (or more) words in it
- Convince the linker to put it at the right memory address
- Profit
C vector table
__attribute__ ((section(".vector_table"))) unsigned long myvectors[] =
{
(unsigned long) &_stack_top,
(unsigned long) rst_handler,
(unsigned long) nmi_handler,
// ...
}
Rust vector table - type definitions
This is possible in Rust as well, but is a bit more involved due to stronger typing rules.
extern "C" {
static mut _stack_top: usize
}
pub struct VectorTable {
stack_top: *const usize,
rst_handler: extern "C" fn(),
nmi_handler: extern "C" fn(),
// ...
}
Rust vector table
#[link_section=".vector_table"]
#[no_mangle]
static VECTOR_TABLE: VectorTable = VectorTable {
// Create a raw pointer from the stack top address.
stack_top: &raw const _stack_top,
rst_handler,
nmi_handler,
// ...
}
Note:
The cortex-m-rt crate does not use a dedicated VectorTable struct. Instead it places some of the
individual vector table components into dedicated segments and then places all components in the
correct order inside the linker script.
Memory Layout
Most embedded applications written in C/C++ and Rust have a very similar memory and binary layout. Many systems use separate flash and RAM memory regions:
Note:
Some of those memory segments need to be set up by code! .bss is a RAM segment which needs to be
zero-initialized, and .data is a segment in RAM where the initial values are stored in the
flash memory and need to be copied from there.
Some larger embedded applications might also use a heap. In embedded applications, those heaps are
oftentimes also statically allocated and might be a part of the .bss or .uninit segments.
Memory Layout - Unified
Some systems also have a run-time layout where code and data are located in the same memory region:
C Reset Handler
Can be written in C! But it’s hazardous.
extern unsigned long _start_data_flash, _start_data, _end_data;
extern unsigned long _bss_start, _bss_end;
void rst_handler(void) {
unsigned long *src = &_start_data_flash;
unsigned long *dest = &_start_data;
while (dest < &_end_data) {
*dest++ = *src++;
}
dest = &_bss_start,
while (dest < &_bss_end) {
*dest++ = 0;
}
main();
while(1) { }
}
Note:
Global variables are not initialised when this function is executed. What if the C code touches an uninitialised global variable? C programmers don’t worry so much about this. Rust programmers definitely worry about this.
Rust Reset Handler (1)
extern "C" {
static mut _start_data_flash: usize;
static mut _start_data: usize;
static mut _end_data: usize;
static mut _bss_start: usize;
static mut _bss_end: usize;
}
Rust Reset Handler (2)
use core::ptr::{addr_of, addr_of_mut};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rst_handler() {
unsafe {
let src = addr_of!(_start_data_flash);
let dest = addr_of_mut!(_start_data);
let size = addr_of_mut!(_end_data).offset_from(dest);
for i in 0..size {
dest.offset(i).write_volatile(src.offset(i).read());
}
let dest = addr_of_mut!(_bss_start);
let size = addr_of_mut!(_bss_end).offset_from(dest);
for i in 0..size {
dest.offset(i).write_volatile(0);
}
}
}
Sadly, this is UB.
Note:
This is Undefined Behaviour because globals haven’t been initialised yet and it is illegal to execute any Rust code in the presence of global variables with invalid values (e.g. a bool with an integer value of 2). It’s also arguably UB because you’re using write_volatile to write outside the bounds the objects we have declared to Rust (we said that _start_data was only a single u32).
It is now reasonably settled that this is bad in theory, but it’s debatable whether it’s currently bad in practice (cortex-m-rt got away with it for years). I believe that in time it will get worse in practice, so don’t do it.
The cortex-m-rt crate
Does all this work for you, in raw Arm assembly language - so it’s actually sound.
See Reset, Linker script, and Vector table
The #[entry] macro
- Attaches your
fn main()to the reset function in cmrt - Hides your
fn main()so no-one else can call it - Remaps
static mut FOO: Ttostatic FOO: &mut Tso they are safe
Using the crate
Linker scripts
- In Rust they work exactly like they do in
clangorgcc - Same
.text,.rodata,.data,.bsssections cortex-m-rtprovideslink.x, which pulls in amemory.xyou supply- You must tell the linker to use
link.x, with:- A build-script
rustflagsin.cargo/config.toml, or- The
RUSTFLAGSenvironment variable