Skip to main content

Crate aarch32_rt

Crate aarch32_rt 

Source
Expand description

§Run-time support for AArch32 Processors

This library implements a simple Arm vector table, suitable for getting into a Rust application running in System Mode. It also provides a reference start up method. Most AArch32 based systems will require chip specific start-up code, so the start-up method can be overridden.

The default startup routine provided by this crate does not include any special handling for multi-core support because this is oftentimes implementation defined and the exact handling depends on the specific chip in use. Many implementations only run the startup routine with one core and will keep other cores in reset until they are woken up by an implementation specific mechanism. For other implementations where multi-core specific startup adaptions are necessary, the startup routine can be overwritten by the user.

§Features

  • eabi-fpu: Enables the FPU, even if you selected a soft-float ABI target.

  • fpu-d32: Make the interrupt context store routines save the upper double-precision registers.

    If your program is using all 32 double-precision registers (e.g. if you have set the +d32 target feature) then you need to enable this option otherwise important FPU state may be lost when an exception occurs.

§Information about the Run-Time

Transferring from System Mode to User Mode (i.e. implementing an RTOS) is not handled here.

If your processor starts in Hyp mode, this runtime will be transfer it to System mode. If you wish to write a hypervisor, you will need to replace this library with something more advanced.

We assume that a set of symbols exist, either for constants or for C compatible functions or for naked raw-assembly functions. They are described in the next three sections.

§Constants

  • _num_cores - the number of CPU core (and hence the number of copies of each stack). Must be > 0.
  • __sbss - the start of zero-initialised data in RAM. Must be 4-byte aligned.
  • __ebss - the end of zero-initialised data in RAM. Must be 4-byte aligned.
  • _fiq_stack_size - the number of bytes to be reserved for stack space when in FIQ mode; will be padded to a multiple of 8.
  • _irq_stack_size - the number of bytes to be reserved for stack space when in FIQ mode; will be padded to a multiple of 8.
  • _svc_stack_size - the number of bytes to be reserved for stack space when in SVC mode; will be padded to a multiple of 8.
  • _und_stack_size - the number of bytes to be reserved for stack space when in Undefined mode; will be padded to a multiple of 8.
  • _abt_stack_size - the number of bytes to be reserved for stack space when in Abort mode; will be padded to a multiple of 8.
  • _hyp_stack_size - the number of bytes to be reserved for stack space when in Hyp mode; will be padded to a multiple of 8.
  • _sys_stack_size - the number of bytes to be reserved for stack space when in System mode; will be padded to a multiple of 8.
  • __sdata - the start of initialised data in RAM. Must be 4-byte aligned.
  • __edata - the end of initialised data in RAM. Must be 4-byte aligned.
  • __sidata - the start of the initialisation values for data, in read-only memory. Must be 4-byte aligned.

Using our default start-up function _default_start, the memory between __sbss and __ebss is zeroed, and the memory between __sdata and __edata is initialised with the data found at __sidata.

§Stacks

Stacks are located in .stacks section which is mapped to the STACKS memory region. Per default, the stacks are pushed to the end of the STACKS by a filler section. We allocate stacks for each core, based on the _num_cores linker symbol.

The stacks look like:

+------------------+ <---- ORIGIN(STACKS) + LENGTH(STACKS)
|     SYS Stack    | } _sys_stack_size * _num_cores bytes
+------------------+
|     FIQ Stack    | } _fiq_stack_size * _num_cores bytes
+------------------+
|     IRQ Stack    | } _irq_stack_size * _num_cores bytes
+------------------+
|     HYP Stack    | } _hyp_stack_size * _num_cores bytes (only used on Armv8-R)
+------------------+
|     ABT Stack    | } _abt_stack_size * _num_cores bytes
+------------------+
|     SVC Stack    | } _svc_stack_size * _num_cores bytes
+------------------+
|     UND Stack    | } _und_stack_size * _num_cores bytes
+------------------+
|  filler section  |
+------------------+ <---- ORIGIN(STACKS)

Our linker script PROVIDEs a symbol _pack_stacks. By setting this symbol to 0 in memory.x, the stacks can be moved to the beginning of the STACKS region or the end of the previous section located in STACKS or its alias.

§C-Compatible Functions

§Main Function

The symbol kmain should be an extern "C" function. It is called in SYS mode after all the global variables have been initialised. There is no default - this function is mandatory.

#[unsafe(no_mangle)]
extern "C" fn kmain() -> ! {
    loop { }
}

You can also create a ‘kmain’ function by using the #[entry] attribute on a normal Rust function. The function will be renamed in such a way that the start-up assembly code can find it, but normal Rust code cannot. Therefore you can be assured that the function will only be called once (unless someone resorts to unsafe Rust to import the kmain symbol as an extern "C" fn).

use aarch32_rt::entry;

#[entry]
fn my_main() -> ! {
    loop { }
}

§Undefined Handler

The symbol _undefined_handler should be an extern "C" function. It is called in UND mode when an Undefined Instruction Exception occurs.

Our linker script PROVIDEs a default _undefined_handler symbol which is an alias for the _default_handler function. You can override it by defining your own _undefined_handler function, like:

/// Does not return
#[unsafe(no_mangle)]
extern "C" fn _undefined_handler(addr: usize) -> ! {
    loop { }
}

or:

/// Execution will continue from the returned address.
///
/// Return `addr` to go back and execute the faulting instruction again.
#[unsafe(no_mangle)]
unsafe extern "C" fn _undefined_handler(addr: usize) -> usize {
    // do stuff here, then return to the address *after* the one
    // that failed
    addr + 4
}

You can create a _undefined_handler function by using the #[exception(Undefined)] attribute on a Rust function with the appropriate arguments and return type.

use aarch32_rt::exception;

#[exception(Undefined)]
fn my_handler(addr: usize) -> ! {
    loop { }
}

or:

use aarch32_rt::exception;

#[exception(Undefined)]
unsafe fn my_handler(addr: usize) -> usize {
    // do stuff here, then return the address to return to
    addr + 4
}

§Supervisor Call Handler

The symbol _svc_handler should be an extern "C" function. It is called in SVC mode when an Supervisor Call Exception occurs.

Returning from this function will cause execution to resume at the function the triggered the exception, immediately after the SVC instruction. You cannot control where execution resumes. The function is passed the literal integer argument to the svc instruction, which is extracted from the machine code for you by the default assembly trampoline, along with registers r0 through r5, in the form of a reference to a Frame structure.

Our linker script PROVIDEs a default _svc_handler symbol which is an alias for the _default_handler function. You can override it by defining your own _svc_handler function, like:

#[unsafe(no_mangle)]
extern "C" fn _svc_handler(arg: u32, frame: &aarch32_rt::Frame) -> u32 {
    // do stuff here
    todo!()
}

You can also create a _svc_handler function by using the #[exception(SupervisorCall)] attribute on a normal Rust function.

use aarch32_rt::exception;

#[exception(SupervisorCall)]
fn svc_handler(arg: u32, frame: &aarch32_rt::Frame) -> u32 {
    // do stuff here
    todo!()
}

§Hypervisor Call Handler

The symbol _hvc_handler should be an extern "C" function. It is called in HYP mode when an Hypervisor Call Exception occurs.

Returning from this function will cause execution to resume at the function the triggered the exception, immediately after the HVC instruction. You cannot control where execution resumes. The function is passed contents of the Hypervisor Syndrome Register (HSR) register, which is fetched by the default assembly trampoline, along with registers r0 through r5, in the form of a reference to a Frame structure.

Our linker script PROVIDEs a default _hvc_handler symbol which is an alias for the _default_handler function. You can override it by defining your own _hvc_handler function, like:

#[unsafe(no_mangle)]
extern "C" fn _hvc_handler(hsr: u32, frame: &aarch32_rt::Frame) -> u32 {
    // do stuff here
    todo!()
}

You can also create a _hvc_handler function by using the #[exception(HypervisorCall)] attribute on a normal Rust function.

use aarch32_rt::exception;

#[exception(HypervisorCall)]
fn my_hvc_handler(hsr: u32, frame: &aarch32_rt::Frame) -> u32 {
    // do stuff here
    todo!()
}

If you wish to inspect the HSR value, you can use the aarch32-cpu crate:

let hsr = aarch32_cpu::register::Hsr::new_with_raw_value(hsr);

§Prefetch Abort Handler

The symbol _prefetch_abort_handler should be an extern "C" function. It is called in ABT mode when a Prefetch Abort Exception occurs.

Our linker script PROVIDEs a default _prefetch_abort_handler symbol which is an alias for the _default_handler function. You can override it by defining your own _undefined_handler function.

This function takes the address of faulting instruction, and can either not return:

#[unsafe(no_mangle)]
extern "C" fn _prefetch_abort_handler(addr: usize) -> ! {
    loop { }
}

Or it can return an address where execution should resume after the Exception handler is complete (which is unsafe):

#[unsafe(no_mangle)]
unsafe extern "C" fn _prefetch_abort_handler(addr: usize) -> usize {
    // do stuff, then go back to the instruction after the one that failed
    addr + 4
}

You can create a _prefetch_abort_handler function by using the #[exception(PrefetchAbort)] macro on a Rust function with the appropriate arguments and return type.

use aarch32_rt::exception;

#[exception(PrefetchAbort)]
fn my_handler(addr: usize) -> ! {
    loop { }
}

or:

use aarch32_rt::exception;

#[exception(PrefetchAbort)]
unsafe fn my_handler(addr: usize) -> usize {
    // do stuff, then go back to the instruction after the one that failed
    addr + 4
}

§Data Abort Handler

The symbol _data_abort_handler should be an extern "C" function. It is called in ABT mode when a Data Abort Exception occurs.

Our linker script PROVIDEs a default _data_abort_handler symbol which is an alias for the _default_handler function. You can override it by defining your own _undefined_handler function.

This function takes the address of faulting instruction, and can either not return:

#[unsafe(no_mangle)]
extern "C" fn _data_abort_handler(addr: usize) -> ! {
    loop { }
}

Or it can return an address where execution should resume after the Exception handler is complete (which is unsafe):

#[unsafe(no_mangle)]
unsafe extern "C" fn _data_abort_handler(addr: usize) -> usize {
    // do stuff, then go back to the instruction after the one that failed
    addr + 4
}

You can create a _data_abort_handler function by using the #[exception(DataAbort)] macro on a Rust function with the appropriate arguments and return type.

use aarch32_rt::exception;

#[exception(DataAbort)]
fn my_handler(addr: usize) -> ! {
    loop { }
}

or:

use aarch32_rt::exception;

#[exception(DataAbort)]
unsafe fn my_handler(addr: usize) -> usize {
    // do stuff, then go back to the instruction after the one that failed
    addr + 4
}

§IRQ Handler

The symbol _irq_handler should be an extern "C" function. It is called in SYS mode (not IRQ mode!) when an Interrupt occurs.

Returning from this function will cause execution to resume at wherever it was interrupted. You cannot control where execution resumes.

This function is entered with interrupts masked, but you may unmask (i.e. enable) interrupts inside this function if desired. You will probably want to talk to your interrupt controller first, otherwise you’ll just keep re-entering this interrupt handler recursively until you stack overflow.

Our linker script PROVIDEs a default _irq_handler symbol which is an alias for _default_handler. You can override it by defining your own _irq_handler function.

Expected prototype:

#[unsafe(no_mangle)]
extern "C" fn _irq_handler() {
    // 1. Talk to interrupt controller
    // 2. Handle interrupt
    // 3. Clear interrupt
}

You can also create a _irq_handler function by using the #[irq] attribute on a normal Rust function.

use aarch32_rt::irq;

#[irq]
fn my_irq_handler() {
    // 1. Talk to interrupt controller
    // 2. Handle interrupt
    // 3. Clear interrupt
}

§ASM functions

These are the naked ‘raw’ assembly functions the run-time requires:

  • _start - a Reset handler. Our linker script PROVIDEs a default function at _default_start but you can override it. The provided default start function will initialise all global variables and then call kmain in SYS mode. Some SoCs require a chip specific startup for tasks like MPU initialization or chip specific initialization routines, so if our start-up routine doesn’t work for you, supply your own _start function (but feel free to call our _default_start as part of it).

  • _asm_undefined_handler - a naked function to call when an Undefined Exception occurs. Our linker script PROVIDEs a default function at _asm_default_undefined_handler but you can override it. The provided default handler will call _undefined_handler in UND mode, saving state as required.

  • _asm_svc_handler - a naked function to call when an Supervisor Call (SVC) Exception occurs. Our linker script PROVIDEs a default function at _asm_default_svc_handler but you can override it. The provided default handler will call _svc_handler in SVC mode, saving state as required.

  • _asm_prefetch_abort_handler - a naked function to call when a Prefetch Abort Exception occurs. Our linker script PROVIDEs a default function at _asm_default_prefetch_abort_handler but you can override it. The provided default handler will call _prefetch_abort_handler, saving state as required. Note that Prefetch Abort Exceptions are handled in Abort Mode (ABT), Monitor Mode (MON) or Hyp Mode (HYP), depending on CPU configuration.

  • _asm_data_abort_handler - a naked function to call when a Data Abort Exception occurs. Our linker script PROVIDEs a default function at _asm_default_data_abort_handler but you can override it. The provided default handler will call _data_abort_handler in ABT mode, saving state as required.

  • _asm_irq_handler - a naked function to call when an Undefined Exception occurs. Our linker script PROVIDEs a default function at _asm_default_irq_handler but you can override it. The provided default handler will call _irq_handler in SYS mode (not IRQ mode), saving state as required.

  • _asm_fiq_handler - a naked function to call when a Fast Interrupt Request (FIQ) occurs. Our linker script PROVIDEs a default function at _asm_default_fiq_handler but you can override it. The provided default just spins forever.

§Outputs

This library produces global symbols called:

  • _vector_table - the start of the interrupt vector table
  • _default_start - the default Reset handler, that sets up some stacks and calls an extern "C" function called kmain.
  • _asm_default_undefined_handler - assembly language trampoline that calls _undefined_handler
  • _asm_default_svc_handler - assembly language trampoline that calls _svc_handler
  • _asm_default_prefetch_abort_handler - assembly language trampoline that calls _prefetch_abort_handler
  • _asm_default_data_abort_handler - assembly language trampoline that calls _data_abort_handler
  • _asm_default_irq_handler - assembly language trampoline that calls _irq_handler
  • _asm_default_fiq_handler - an FIQ handler that just spins
  • _default_handler - a C compatible function that spins forever.
  • _init_segments - initialises .bss and .data and zeroes the stacks
  • _stack_setup_preallocated - initialises UND, SVC, ABT, IRQ, FIQ and SYS stacks from the .stacks section defined in link.x, based on _xxx_stack_size values, and the core number given in r0
  • _xxx_stack_high_end and _xxx_stack_low_end where the former is the top and the latter the bottom of the stack for each mode (und, svc, abt, irq, fiq, sys)

The assembly language trampolines are required because AArch32 processors do not save a great deal of state on entry to an exception handler, unlike Armv7-M (and other M-Profile) processors. We must therefore save this state to the stack using assembly language, before transferring to an extern "C" function. Because FIQ is often performance-sensitive, we don’t supply an FIQ trampoline; if you want to use FIQ, you have to write your own assembly routine, allowing you to preserve only whatever state is important to you.

§Examples

You can find example code using QEMU inside the project repository

Modules§

sections
Code to get the memory regions from the linker script
stacks
Code for examining linker allocated stacks

Macros§

restore_fpu_context
This macro expands to code for restoring context on exit from an exception handler. It restores FPU state, assuming 16 DP registers (a ‘D16’ or ‘D16SP’ FPU configuration).
save_fpu_context
This macro expands to code for saving FPU context on entry to an exception handler. It pushes a multiple of eight bytes to preserve AAPCS alignment. It may damage R0-R3.

Structs§

Frame
Arguments stacked on interrupt

Functions§

_default_handler
Our default exception handler.

Attribute Macros§

entry
Creates an unsafe program entry point (i.e. a kmain function).
exception
Creates an unsafe exception handler.
irq
Creates an unsafe interrupt handler.