This page describes the sequence of execution from power-on (or QEMU start) until the kernel is running.

1. Load and Entry

The GPU (on real hardware) or QEMU loads the bootloader binary at physical address 0x80000. Execution starts at the first instruction of the image, which must be _start in boot/start.S. All four cores may enter here; only core 0 is allowed to continue. The ARM firmware passes the device tree binary (DTB) address in register x0.

2. Assembly Initialisation (boot/start.S)

Park secondary cores. Read mpidr_el1 and mask to the bottom 8 bits (aff0). If the result is non-zero, the core branches to _park_core and spins in a WFE loop. Only core 0 proceeds.

Detect exception level. Read CurrentEL and shift right by 2 to obtain the level number. If the level is 2 (EL2), jump to _from_el2. If it is 1 (EL1), jump to _el1_entry. Any other level (0 or 3) is treated as unsupported and the core parks.

EL2 to EL1 (if applicable). Set HCR_EL2 so that EL1 is AArch64. Set SCTLR_EL1 to zero (MMU and caches off). Set SPSR_EL2 so that on return, execution is at EL1 with SP_EL1 and interrupts masked. Set ELR_EL2 to the address of _el1_entry. Execute eret to drop to EL1.

EL1 setup. At _el1_entry, preserve the DTB address passed in x0 by copying it to x19 (a callee-saved register). Clear the MMU and cache bits in SCTLR_EL1 and execute an ISB. Load the address of _start (0x80000) into the stack pointer. Zero the BSS range between __bss_start and __bss_end. Then call neutron_main(x0) with the DTB address as a parameter. If it ever returns, execution falls through to _park_core.

3. C Bootloader (neutron/main.c)

UART and banner. neutron_main(dtb_addr) receives the DTB address and calls uart_init() to initialise the UART. It prints a colourised banner with bootloader identification.

CPU state. It reads the current exception level and MPIDR using inline assembly and prints them for debugging.

Mailbox and board identification. It calls mbox_get_board_revision() and mbox_get_arm_mem_size() to query board information. Based on the revision code, it prints the detected board type (Raspberry Pi Zero 2W, generic Raspberry Pi, or QEMU simulation).

Kernel source selection. Depending on CFG_EMBED_KERNEL (set from build.cfg):

  • Embedded kernel (CFG_EMBED_KERNEL = true): The packed kernel is linked into the image at build time via _binary_build_kernel_embed_bin_start. No SD/FAT32 access is needed.
  • SD kernel (CFG_EMBED_KERNEL = false): The bootloader initialises the SD card and loads the kernel from FAT32 storage.

The remaining steps are identical: NKRN magic verification, payload validation, and kernel handoff.

SD + FAT32 (only when not embedded). It calls sdcard_init() then fat32_mount() to initialise the SD card and read the MBR, mounting the first partition as FAT32. If either fails, a fatal error is printed and the system halts.

Load kernel file (only when not embedded). It calls fat32_read_file(CFG_KERNEL_FILENAME, CFG_KERNEL_STAGING_ADDR, ...) to read the kernel binary from FAT32. The filename is configured via build.cfg (default: kernel.bin). The file is loaded into the staging buffer at CFG_KERNEL_STAGING_ADDR (default 0x100000). If the file is not found or the read fails, a fatal error is printed and the system halts.

Magic check. It reads the first 4 bytes at the source address (either the staging buffer or the embedded image base) and verifies the NKRN magic signature (0x4E4B524E). If the magic is incorrect, a fatal error is printed suggesting the kernel needs to be packed with pack_kernel.py, and the system halts.

Validate and load. It calls bl_load_kernel(src, NULL) where src points to the start of the packed NKRN image. This function (in neutron/bootloader.c):

  • Validates the NKRN header (magic, version, sizes)
  • Verifies the payload CRC32 checksum
  • Displays kernel metadata (name, version, load address, entry address)
  • Copies the payload to the load address specified in the header
  • Fills the boot_info_t structure at well-known address 0x1000 with metadata If validation fails, a fatal error is displayed and the system halts.

Boot countdown. After successful validation, the bootloader displays the kernel entry point address and waits 3 seconds (via timed delays).

Kernel handoff. It retrieves the boot_info_t from address 0x1000, fills in board_revision and arm_mem_size from mailbox data, then calls bl_boot_kernel(entry_addr, boot_info, dtb_addr). This function performs a DSB (Data Synchronization Barrier) and ISB (Instruction Synchronization Barrier) to ensure all writes are visible, then branches to the kernel entry point using a custom calling convention: x0 = pointer to boot_info_t, x1 = DTB address. This function does not return.

4. Kernel Entry

The kernel runs at EL1 with MMU and caches off. Register x0 holds the physical address of boot_info_t; register x1 holds the DTB (device tree binary) address for platform-specific initialisation. The kernel is responsible for setting up its own stack, initialising any hardware it needs (including UART if it wants serial output), and setting up exception handling. The bootloader does not guarantee the state of peripherals after the jump.

Summary

  1. GPU/QEMU loads image at 0x80000; DTB address in x0; core 0 runs _start.
  2. Secondary cores park; core 0 drops from EL2 to EL1 (if needed), preserves DTB in x19, sets stack, zeroes BSS, calls neutron_main(dtb_addr).
  3. UART initialisation, banner, mailbox queries, board identification; then either embedded-kernel or SD+FAT32 load of the kernel; NKRN magic and CRC32 validation; kernel payload copied to configured load address; boot_info filled at 0x1000.
  4. Boot countdown; then jump to kernel entry with x0 = boot_info pointer and x1 = DTB address.

See Components for the source files that implement each step.