Rust Raspberry Pi OS Tutorials

repository·master·Indexed 12 days ago

https://github.com/rust-embedded/rust-raspberrypi-os-tutorials

A series of step-by-step tutorials for developing a monolithic operating system kernel from scratch in Rust, targeting the ARMv8-A architecture on Raspberry Pi 3 and 4. The project covers kernel organization, Board Support Package (BSP) design, runtime initialization, AArch64 CPU abstractions using the aarch64-cpu crate, and implementing safe global state via Mutex and NullLock.

Tokens
108.8K
Snippets
393
Records
515
Agent score
93%

What's inside Raspberry Pi OS Tutorials

  1. Overview of Tutorial 07 - Timestamps

    master

    This tutorial introduces hardware timer abstractions and their implementation for the ARM architectural timer in _arch/aarch64.

    Key improvements include:

    • UART Timestamps: Annotating UART prints with precise timestamps.
    • Improved GPIO Accuracy: Replacing cycle-based delays in the GPIO device driver with timer-based delays to increase accuracy.
    • Logging: Addition of a warn!() macro for warning-level logs.
  2. Overview of Integrated Testing in the Kernel

    master

    This tutorial implements a custom integrated testing framework for a #![no_std] Rust kernel. Because the standard library is unavailable, the native Rust #[test] attribute and cargo test cannot be used directly. Instead, the project utilizes Rust's unstable custom_test_frameworks feature to enable three types of testing:

    1. Unit Tests: Small, isolated tests using the #[test_case] attribute.
    2. Integration Tests: Self-contained tests located in the $CRATE/tests/ directory.
    3. Console I/O Tests: Integration tests that validate kernel behavior by sending strings/characters to the console (via UART) and verifying the responses.

    All testing is performed using QEMU rather than real hardware to facilitate automation.

  3. Overview of Tutorial 01: Wait Forever (Infinite Loop)

    master

    This tutorial demonstrates a minimal project framework where the code's primary function is to suspend CPU cores by executing kernel code that enters a wait state.

    Key technical details:

    • Code Organization: The project is structured into kernel, arch (architecture), and BSP (Board Support Package) modules. Conditional compilation is used to select the appropriate arch and BSP based on provided parameters.
    • Linker Script: Uses a custom kernel.ld with a load address of 0x80_000. Currently, it only contains the .text section.
    • Rust Attributes: The main.rs file uses #![no_std] and #![no_main] to indicate a bare-metal environment without the standard library or a standard runtime.
    • Execution Flow: The assembly function _start() executes the wfe (Wait For Event) instruction, which suspends all cores executing _start().
    • Panic Handling: A #[panic_handler] function must be defined to handle runtime panics.
  4. Overview of Tutorial 15: Precomputed Translation Tables

    master

    This tutorial focuses on moving the kernel from its current load address (0x8_0000) to the most significant area of the virtual memory space (ideally between 0xffff_0000_0000_0000 and 0xffff_ffff_ffff_ffff).

    Instead of computing translation tables dynamically during the boot process, this approach involves:

    1. Precomputing the kernel's translation tables immediately after kernel compilation.
    2. Patching these tables into the kernel binary ahead of time.

    While the current implementation still uses identity-mapping for the kernel binary, this setup provides the infrastructure required to map the kernel to a high virtual address in future steps.

  5. How processor architecture and BSP code visibility works

    master

    The project uses a specific visibility pattern to separate low-level implementation from the public kernel API:

    Architecture Code Visibility

    Architecture-specific code is stored in src/_arch/. To keep the module hierarchy clean, these files are re-exported into the top-level kernel modules.

    • Access Pattern: Even if a function is defined in src/_arch/aarch64/memory.rs, it is accessed via the top-level module: crate::memory::foo().
    • Implementation Detail: The _ prefix in _arch indicates it is not part of the standard module hierarchy but is pulled in via the #[path = "_arch/xxx/yyy.rs"] attribute.

    BSP Code Visibility

    BSP (Board Support Package) code contains board-specific definitions like memory maps or driver instances. Unlike architecture code, BSP modules do not automatically re-export their contents to the top-level kernel modules.

    • Access Pattern: You must call BSP content starting from the bsp namespace, for example: bsp::driver::driver_manager().
  6. Configure the Vector Base Address Register (VBAR_EL1)

    master

    Unlike architectures that use an array of function pointers, AArch64 uses the VBAR_EL1 (Vector Base Address Register) to point to a memory location containing actual code for the 16 handlers.

    Requirements for the Vector Table:

    • Structure: Handlers are placed back-to-back at specific offsets defined by the architecture.
    • Handler Size: Each handler entry in the table has a maximum space of 0x80 (128) bytes. To handle complex logic or context saving, handlers should immediately branch off to other functions.
    • Alignment: The Vector Base Address must be aligned to 0x800 (2048) bytes.
  7. Understand AArch64 Exception Types

    master

    In the AArch64 architecture, exceptions are categorized into four distinct types based on how they are triggered:

    • Synchronous: Triggered directly by the execution of a specific CPU instruction (e.g., a data abort like a page fault or a system call).
    • Interrupt Request (IRQ): Asynchronous exceptions triggered by external hardware devices (e.g., a timer) asserting a physical interrupt line.
    • Fast Interrupt Request (FIQ): High-priority asynchronous interrupts designed for super-fast processing. In this tutorial, FIQs are treated as dummy handlers that halt the CPU.
    • System Error (SError): Asynchronous exceptions used to signal fatal system errors, such as a transaction timeout on the SoC interconnect. These are implementation-specific to the SoC vendor.
  8. How the UART Chainloader protocol works

    master

    The UART Chainloader is a mechanism to load a kernel binary into RAM via a serial (UART) interface. The process follows these steps:

    1. Request: The chainloader sends a specific sequence (the character 0x03 repeated 3 times) to signal it is ready for a payload.
    2. Size Transfer: The loader reads 4 bytes from the UART to determine the size of the incoming binary.
    3. Payload Transfer: The loader reads the binary byte-by-byte from the UART and writes it to the memory address defined by board_default_load_addr() (typically 0x80000).
    4. Execution: Once the transfer is complete, the loader performs a jump to the start of the loaded binary.

    This allows for updating or booting kernels without needing an SD card or external storage, provided a serial connection is available.

  9. Understand the Runtime Initialization process

    master

    In this tutorial, the boot.s assembly file is extended to bridge the gap between hardware startup and Rust execution. The initialization sequence follows these steps:

    1. Core Setup: For all cores except core 0, specific initialization is performed.
    2. DRAM Initialization: The .bss section is zeroed out to prepare DRAM.
    3. Stack Configuration: The stack pointer is configured to allow function calls.
    4. Rust Handover: The assembly code jumps to _start_rust() (defined in arch/__arch_name__/cpu/boot.rs).
    5. Rust Entry: _start_rust() calls kernel_init(), which in this tutorial simply calls panic!() to pause execution.

    To see this initialization in action, run the QEMU emulator:

    make qemu
  10. Kernel Virtual Memory Layout (Higher-Half)

    master

    The higher-half kernel memory layout follows this structure:

    1. Kernel Code/Data: Starts at __kernel_virt_start_addr.
    2. MMIO Remap: Follows the kernel sections.
    3. Guard Page: An unmapped page to catch overflows.
    4. Boot Core Stack: Located at the bottom of the high-address range, growing towards higher addresses (or as defined by the stack pointer setup).

    Note: The stack is placed such that it grows from __boot_core_stack_start towards __boot_core_stack_end_exclusive.

  11. How AArch64 Exception Entry Works

    master

    When an exception is taken in AArch64, the processor undergoes several automatic state changes:

    • Exception Level (EL): The processor moves to the same or a higher EL, but never a lower one.
    • Program Status: The current program status is saved in the SPSR_ELx register at the target Exception Level.
    • Return Address: The preferred return address is saved in the ELR_ELx register. For synchronous exceptions, this is the instruction that caused the exception; for asynchronous exceptions, it is the first instruction that did not complete.
    • Interrupt Disabling: All exception types are disabled upon entry, preventing handlers from being interrupted by default.
    • Stack Pointer: The processor switches to the dedicated stack pointer of the target EL. For example, an exception in EL0 will cause SPSel to switch from 0 to 1, selecting SP_EL1 for the handler code.