When writing Rust code for ARM Cortex-M microcontrollers it is common to use the cortex-m-rt and the cortex-m crates. The first provides a minimum base and startup code to generate executables for Cortex-M, the second provides access to the core peripherals all Cortex-M-based microcontrollers share (such as the NVIC (interrupt controller) and SysTick timer) and some boilerplate code you commonly need on these controllers.
cortex-m-rt
also ensures the reset, interrupt and exception vector tables are present
and populated based on the interrupt routines defined by the application.
The vector table consists of the following parts, in the terms used in the cortex-m-rt
crate:
Linker section | Symbol name | Description |
---|---|---|
_stack_start |
- | Initial value loaded into the stack pointer |
.vector_table.reset_vector |
__RESET_VECTOR |
Program reset address |
.vector_table.exceptions |
__EXCEPTIONS |
Vector table for the Cortex-M common exceptions |
.vector_table.interrupts |
__INTERRUPTS |
Vector table for device-specific interrupts |
If the device
feature is enabled for the cortex-m-rt
crate, it does not populate
the vector table for device-specific interrupts. Instead, this is left up to the
Peripheral Access Crate (PAC), which is device specific.
The .text
and _stack_start
sections can also be moved/modified through configuration,
but the exception vector table can not.
The exception vector table is standardized by ARM and should therefore be the same on any Cortex-M based microcontroller. But of course, standards are meant to be broken, and microcontroller vendors tend to do so.
For example, some microcontrollers store boot configuration, markers or checksums in the ‘reserved’ fields in the exception vector table or require additional headers to be added before the table or application, in which case we need to modify the table in order for our code to run.
In this post we’ll achieve that without patching any external crates.
Setting up a basic project
Let’s set up a typical, minimal Cortex-M0 project to use as an example. We’ll leave out the PAC because we don’t need it for this example.
$ cargo new cmexample --bin
Created binary (application) `cmexample` package
$ cd cmexample/
$ cargo add cortex-m-rt
Adding cortex-m-rt v0.7.2 to dependencies.
Features:
- device
- set-sp
- set-vtor
$ cargo add -F critical-section-single-core cortex-m
Adding cortex-m v0.7.7 to dependencies.
Features:
+ critical-section-single-core
- cm7
- cm7-r0p1
- critical-section
- inline-asm
- linker-plugin-lto
- serde
- std
$ cargo add panic-halt
Adding panic-halt v0.2.0 to dependencies.
Let’s populate src/main.rs
with some sample code:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {}
}
As required by cortex-m-rt
, populate memory.x
:
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
Configure Cargo to use the thumbv6 target and to pick up the linker
script provided by cortex-m-rt
by filling .cargo/config.toml
:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv6m-none-eabi"
Examining the vector table
If all is well, after building, there is an ELF executable for ARM targets, which has a vector table and other sections:
$ file target/thumbv6m-none-eabi/release/cmexample
target/thumbv6m-none-eabi/release/cmexample: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
$ arm-none-eabi-size -Ax target/thumbv6m-none-eabi/release/cmexample
target/thumbv6m-none-eabi/release/cmexample :
section size addr
.vector_table 0xc0 0x0
.text 0x64 0xc0
.rodata 0x0 0x124
.data 0x0 0x20000000
.gnu.sgstubs 0x0 0x140
.bss 0x0 0x20000000
.uninit 0x0 0x20000000
.ARM.attributes 0x32 0x0
.debug_frame 0x6528 0x0
.debug_abbrev 0x1139 0x0
.debug_info 0x26271 0x0
.debug_aranges 0x1e00 0x0
.debug_ranges 0x153f0 0x0
.debug_str 0x3e653 0x0
.debug_pubnames 0x16152 0x0
.debug_pubtypes 0x252 0x0
.debug_line 0x25015 0x0
.debug_loc 0x74 0x0
.comment 0x6d 0x0
Total 0xbe705
Using objdump
we can examine the content of the vector table:
$ arm-none-eabi-objdump -s --section .vector_table target/thumbv6m-none-eabi/release/cmexample
target/thumbv6m-none-eabi/release/cmexample: file format elf32-littlearm
Contents of section .vector_table:
0000 00500020 c1000000 0b010000 0f010000 .P. ............
0010 00000000 00000000 00000000 00000000 ................
0020 00000000 00000000 00000000 0b010000 ................
0030 00000000 00000000 0b010000 0b010000 ................
0040 0b010000 0b010000 0b010000 0b010000 ................
0050 0b010000 0b010000 0b010000 0b010000 ................
0060 0b010000 0b010000 0b010000 0b010000 ................
0070 0b010000 0b010000 0b010000 0b010000 ................
0080 0b010000 0b010000 0b010000 0b010000 ................
0090 0b010000 0b010000 0b010000 0b010000 ................
00a0 0b010000 0b010000 0b010000 0b010000 ................
00b0 0b010000 0b010000 0b010000 0b010000 ................
All vectors point to the default handler. Let’s define a new handler, for the SysTick interrupt, for example:
#[exception]
fn SysTick() {
todo!();
}
Now we can see the SysTick vector at 0x3c
is populated:
$ arm-none-eabi-objdump -s --section .vector_table target/thumbv6m-none-eabi/release/cmexample
target/thumbv6m-none-eabi/release/cmexample: file format elf32-littlearm
Contents of section .vector_table:
0000 00500020 c1000000 23010000 91010000 .P. ....#.......
0010 00000000 00000000 00000000 00000000 ................
0020 00000000 00000000 00000000 23010000 ............#...
0030 00000000 00000000 23010000 09010000 ........#.......
0040 23010000 23010000 23010000 23010000 #...#...#...#...
0050 23010000 23010000 23010000 23010000 #...#...#...#...
0060 23010000 23010000 23010000 23010000 #...#...#...#...
0070 23010000 23010000 23010000 23010000 #...#...#...#...
0080 23010000 23010000 23010000 23010000 #...#...#...#...
0090 23010000 23010000 23010000 23010000 #...#...#...#...
00a0 23010000 23010000 23010000 23010000 #...#...#...#...
00b0 23010000 23010000 23010000 23010000 #...#...#...#...
Defining a custom vector table
NOTE: you normally do not need to do this! cortex-m-rt
and the PAC will populate
the table for you as you define exception handlers in your application. Only for controllers
with special requirements which cortex-m-rt
does not accommodate
(as mentioned above) you may need to do this.
The linker side of things
To take ownership of the vector table, we need to modify the linker script.
cortex_m_rt
generates this at compile time normally.
Easiest is to simply copy the generated link.x
script from the
build output directory:
$ cargo build --release
$ cp target/thumbv6m-none-eabi/release/build/cortex-m-rt-*/out/link.x custom_link.x
We also rename the file to something unambiguous to avoid name collisions.
Now we modify .cargo/config.toml
to use the new linker script:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "link-arg=-Tcustom_link.x",
]
[build]
target = "thumbv6m-none-eabi"
We also need to create a build.rs
file to instruct Cargo to put
our custom_link.x
in a place where the linker can find it:
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("custom_link.x"))
.unwrap()
.write_all(include_bytes!("custom_link.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=custom_link.x");
}
Now, we’re ready to start editting the linker script! The part we are interested in is:
/* # Sections */
SECTIONS
{
PROVIDE(_stack_start = ORIGIN(RAM) + LENGTH(RAM));
/* ## Sections in FLASH */
/* ### Vector table */
.vector_table ORIGIN(FLASH) :
{
__vector_table = .;
/* Initial Stack Pointer (SP) value */
LONG(_stack_start);
/* Reset vector */
KEEP(*(.vector_table.reset_vector)); /* this is the `__RESET_VECTOR` symbol */
__reset_vector = .;
/* Exceptions */
KEEP(*(.vector_table.exceptions)); /* this is the `__EXCEPTIONS` symbol */
__eexceptions = .;
/* Device specific interrupts */
KEEP(*(.vector_table.interrupts)); /* this is the `__INTERRUPTS` symbol */
} > FLASH
PROVIDE(_stext = ADDR(.vector_table) + SIZEOF(.vector_table));
You can shuffle things around here as needed or even add new sections, i.e. if you need to add an image header to your executable. For this example we’ll just take ownership of the ‘exceptions’ part of the table.
We need to do two things here: rename the section and also make sure the
old section doesn’t end up anywhere in the executable, because
it is still generated by the cortex-m-rt
crate.
We can use the special /DISCARD/ output section to achieve this.
The section above then looks like this:
/* # Sections */
SECTIONS
{
PROVIDE(_stack_start = ORIGIN(RAM) + LENGTH(RAM));
/* ## Sections in FLASH */
/* ### Vector table */
.vector_table ORIGIN(FLASH) :
{
__vector_table = .;
/* Initial Stack Pointer (SP) value */
LONG(_stack_start);
/* Reset vector */
KEEP(*(.vector_table.reset_vector)); /* this is the `__RESET_VECTOR` symbol */
__reset_vector = .;
/* Our custom exceptions table */
KEEP(*(.vector_table.custom_exceptions));
__eexceptions = .;
/* Device specific interrupts */
KEEP(*(.vector_table.interrupts)); /* this is the `__INTERRUPTS` symbol */
} > FLASH
/DISCARD/ :
{
*(.vector_table.exceptions);
}
PROVIDE(_stext = ADDR(.vector_table) + SIZEOF(.vector_table));
If you were to build the project now, you should see the following error,
because the custom_exceptions
section doesn’t contain any symbols yet:
rust-lld: error:
BUG(cortex-m-rt): the exception vectors are missing
The Rust side of things
Now we need to actually define the exceptions part of the vector table. We’ll
store it in src/lib.rs
in our own binary crate.
I copied most things from src/lib.rs in the cortex-m-rt crate.
I recommend you do the same, rather than just copy my code below, so you have code that matches your version
of the cortex-m-rt
crate.
We need the following things:
union Vector
,HardFaultTrampoline
,- externs to all the handler functions,
- the actual table (
__EXCEPTIONS
), renamed and moved to the new section.
For this example, I changed the values of the reserved fields 8 to 10 to demonstrate the changes working.
#![no_std]
use core::arch::global_asm;
global_asm!(
".cfi_sections .debug_frame
.section .HardFaultTrampoline, \"ax\"
.global HardFaultTrampline
.type HardFaultTrampline,%function
.thumb_func
.cfi_startproc
HardFaultTrampoline:",
"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 HardFaultTrampoline, . - HardFaultTrampoline",
);
pub union Vector {
handler: unsafe extern "C" fn(),
reserved: usize,
}
#[allow(dead_code)]
extern "C" {
fn Reset() -> !;
fn NonMaskableInt();
fn HardFaultTrampoline();
fn MemoryManagement();
fn BusFault();
fn UsageFault();
fn SecureFault();
fn SVCall();
fn DebugMonitor();
fn PendSV();
fn SysTick();
}
#[no_mangle]
#[link_section = ".vector_table.custom_exceptions"]
pub static _CUSTOM_EXCEPTIONS: [Vector; 14] = [
// Exception 2: Non Maskable Interrupt.
Vector {
handler: NonMaskableInt,
},
// Exception 3: Hard Fault Interrupt.
Vector {
handler: HardFaultTrampoline,
},
// 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: 0x44332211,
},
Vector {
reserved: 0x88776655,
},
Vector {
reserved: 0xccbbaa99,
},
// 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 },
];
To make the compiler include the symbol, add a use
to src/main.rs
:
#[allow(unused_imports)]
use cmexample::_CUSTOM_EXCEPTIONS as _;
Now it should build successfully and objdump
will show the custom
vector table, with the 3 ‘reserved’ slots starting from 0x20
filled:
$ arm-none-eabi-objdump -s --section .vector_table target/thumbv6m-none-eabi/release/cmexample
target/thumbv6m-none-eabi/release/cmexample: file format elf32-littlearm
Contents of section .vector_table:
0000 00500020 c1000000 23010000 a5010000 .P. ....#.......
0010 23010000 23010000 23010000 00000000 #...#...#.......
0020 11223344 55667788 99aabbcc 23010000 ."3DUfw.....#...
0030 23010000 00000000 23010000 09010000 #.......#.......
0040 23010000 23010000 23010000 23010000 #...#...#...#...
0050 23010000 23010000 23010000 23010000 #...#...#...#...
0060 23010000 23010000 23010000 23010000 #...#...#...#...
0070 23010000 23010000 23010000 23010000 #...#...#...#...
0080 23010000 23010000 23010000 23010000 #...#...#...#...
0090 23010000 23010000 23010000 23010000 #...#...#...#...
00a0 23010000 23010000 23010000 23010000 #...#...#...#...
00b0 23010000 23010000 23010000 23010000 #...#...#...#...