Tock Startup

This document walks through how all of the components of Tock start up.

When a microcontroller boots (or resets, or services an interrupt) it loads an address for a function from a table indexed by interrupt type known as the vector table. The location of the vector table in memory is chip-specific, thus it is placed in a special section for linking.

Cortex-M microcontrollers expect a vector table to be at address 0x00000000. This can either be a software bootloader or the Tock kernel itself.

RISC-V gives hardware designers a great deal of design freedom for how booting works. Typically, after coming out of reset, a RISC-V processor will start executing out of ROM but this may be configurable. The HiFive1 board, for example, supports booting out ROM, One-Time programmable (OTP) memory or a QSPI flash controller.

Optional Bootloader

Many Tock boards (including Hail and imix) use a software bootloader that executes when the MCU first boots. The bootloader provides a way to talk to the chip over serial and to load new code, as well as potentially other administrative tasks. When the bootloader has finished, it tells the MCU that the vector table has moved (to a known address), and then jumps to a new address.

Tock first instructions

ARM Vector Table and IRQ table

On ARM chips, Tock splits the vector table into two sections, .vectors which hold the first 16 entries, common to all ARM cores, and .irqs, which is appended to the end and holds chip-specific interrupts.

In the source code then, the vector table will appear as an array that is marked to be placed into the .vectors section.

In Rust, a vector table will look something like this:

#![allow(unused)]
fn main() {
#[link_section=".vectors"]
#[used] // Ensures that the symbol is kept until the final binary
pub static BASE_VECTORS: [unsafe extern fn(); 16] = [
    _estack,                        // Initial stack pointer value
    tock_kernel_reset_handler,      // Tock's reset handler function
    /* NMI */ unhandled_interrupt,  // Generic handler function
    ...
}

In C, a vector table will look something like this:

__attribute__ ((section(".vectors")))
interrupt_function_t interrupt_table[] = {
        (interrupt_function_t) (&_estack),
        tock_kernel_reset_handler,
        NMI_Handler,

At the time of this writing (November 2018), typical chips (like the sam4l and nrf52) use the same handler for all interrupts, and look something like:

#![allow(unused)]
fn main() {
#[link_section = ".vectors"]
#[used] // Ensures that the symbol is kept until the final binary
pub static IRQS: [unsafe extern "C" fn(); 80] = [generic_isr; 80];
}

RISC-V

All RISC-V boards are linked to run the _start function as the first function that gets run before jumping to main. This is currently inline assembly as of this writing:

#![allow(unused)]
fn main() {
#[cfg(all(target_arch = "riscv32", target_os = "none"))]
#[link_section = ".riscv.start"]
#[export_name = "_start"]
#[naked]
pub extern "C" fn _start() {
    unsafe {
        asm! ("

}

Reset Handler

On boot, the MCU calls the reset handler function defined in vector table. In Tock, the implementation of the reset handler function is architecture specific and handles memory initialization.

Memory Initialization

The main operation the reset handler does is setup the kernel's memory by copying it from flash. For the SAM4L, this is in the initialize_ram_jump_to_main() function in arch/cortex-m/src/lib.rs. Once finished the reset handler jumps to the main() function defined by each board.

The memory initialization function is implemented in assembly as Rust expects that memory is correctly initialized before any Rust instructions execute.

RISC-V Trap setup

The mtvec register needs to be set on RISC-V to handle traps. Setting of the vectors is handled by chip specific functions. The common RISC-V trap handler is _start_trap, defined in arch/rv32i/src/lib.rs.

MCU Setup

Any normal MCU initialization is typically handled next. This includes things like enabling the correct clocks or setting up DMA channels.

Peripheral and Capsule Initialization

After the MCU is set up, main initializes peripherals and capsules. Peripherals are on-chip subsystems, such as UARTs, ADCs, and SPI buses; they are chip-specific code that read and write memory-mapped I/O registers and are found in the corresponding chips directory. While peripherals are chip-specific implementations, they typically provide hardware-independent traits, called hardware independent layer (HIL) traits, found in kernel/src/hil.

Capsules are software abstractions and services; they are chip-independent and found in the capsules directory. For example, on the imix and hail platforms, the SAM4L SPI peripheral is implemented in chips/sam4l/src/spi.rs, while the capsule that virtualizes the SPI so multiple capsules can share it is in capsules/src/virtual_spi.rs. This virtualizer can be chip-independent because the chip-specific code implements the SPI HIL (kernel/src/hil/spi.rs). The capsule that implements a system call API to the SPI for processes is in capsules/src/spi.rs.

Boards that initialize many peripherals and capsules use the Component trait to encapsulate this complexity from main. The Component trait (kernel/src/component.rs) encapsulates any initialization a particular peripheral, capsule, or set of capsules need inside a call to the function finalize(). Changing what the build of the kernel includes involve changing just which Components are initialized, rather than changing many lines of main. Components are typically found in the components crate in the /boards folder, but may also be board-specific and found inside a components subdirectory of the board directory, e.g. boards/imix/src/imix_components.

Application Startup

Once the kernel components have been setup and initialized, the applications must be loaded. This procedure essentially iterates over the processes stored in flash, extracts and validates their Tock Binary Format header, and adds them to an internal array of process structs.

An example version of this loop is in kernel/src/process.rs as the load_processes() function. After setting up pointers, it tries to create a process from the starting address in flash and with a given amount of memory remaining. If the header is validated, it tries to load the process into memory and initialize all of the bookkeeping in the kernel associated with the process. This can fail if the process needs more memory than is available on the chip. If the process is successfully loaded the kernel importantly notes the address of the application's entry function which is called when the process is started.

The load process loop ends when the kernel runs out of statically allocated memory to store processes in, available RAM for processes, or there is an invalid TBF header in flash.

Scheduler Execution

Tock provides a Scheduler trait that serves as an abstraction to allow for plugging in different scheduling algorithms. Schedulers should be initialized at the end of the reset handler. The final thing that the reset handler must do is call kernel.kernel_loop(). This starts the Tock scheduler and the main operation of the kernel.