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  #...#...#...#...