Skip to content

02. Understanding the Raspberry Pi Boot Process

How the Raspberry Pi 4B Boots

Unlike your laptop, which has a BIOS or UEFI, the Raspberry Pi has a unique, GPU-driven boot sequence.

The Sequence

graph TD
    Power["Power On"] --> ROM["Boot ROM (SoC)"]
    ROM -->|Reads SD Card| SD["Start4.elf (GPU Firmware)"]
    SD -->|Reads config.txt| Config["Configure System"]
    Config -->|Loads kernel8.img| Kernel["Load Kernel to 0x80000"]
    Kernel --> CPU["Release CPU Core 0"]
    CPU --> Start["Execute _start"]
  1. Boot ROM: When powered on, the small code inside the chip (SoC) wakes up and looks for an SD card.
  2. GPU Takes Charge: The GPU (not the CPU!) loads start4.elf (firmware) from the SD card.
  3. Kernel Load: The firmware reads config.txt, configures hardware, and loads your kernel8.img into memory at address 0x80000.
  4. Action!: Finally, the GPU wakes up the ARM CPU (Core 0) and tells it: "Go to address 0x80000 and start running!"

Raspberry Pi 4 vs Earlier Models

  • Pi 3 and earlier: Required bootcode.bin on the SD card
  • Pi 4: Has internal boot ROM, so bootcode.bin is not needed

Why kernel8.img?

  • kernel.img = ARMv6 (32-bit)
  • kernel7.img = ARMv7 (32-bit)
  • kernel8.img = ARMv8 (64-bit) ← We're using this

The Linker Script

The linker script (kernel/linker.ld) is like a map for the compiler. It tells the compiler exactly where to place different parts of our code in the Raspberry Pi's RAM.

block-beta
    columns 1
    space
    block:Code
        text0["0x80000"]
        text[".text (Your Code)"]
    end
    block:Data
        rodata[".rodata (Constants)"]
        data[".data (Variables)"]
        bss[".bss (Zeroed Data)"]
    end
    block:Stack
        stack["Stack (Grows Down)"]
        top["0x84000 (approx)"]
    end
    space
ENTRY(_start)

SECTIONS
{
    . = 0x80000; /* Kernel load address for RPi 4 (64-bit) */

    .text : { KEEP(*(.text.boot)) *(.text .text.*) }
    .rodata : { *(.rodata .rodata.*) }
    .data : { *(.data .data.*) }
    .bss (NOLOAD) : {
        . = ALIGN(16);
        __bss_start = .;
        *(.bss .bss.*)
        __bss_end = .;
    }
    . = ALIGN(16);
    . += 0x4000; /* 16KB Stack */
    __stack_top = .;
}

Key Points to Remember

  • 0x80000: This is the "Meeting Point". The GPU promises to put your code here.
  • BSS Section: This is where "uninitalized variables" live. We must manually clear this area to 0.
  • Stack: This is the "scratchpad" memory for function calls. We set it up to grow downwards from a safe location.

Writing the Boot Code

File: kernel/boot.S

.section ".text.boot"

.global _start

_start:
    /* Check processor ID */
    mrs x0, mpidr_el1
    and x0, x0, #0xFF
    cbz x0, master      /* If CPU ID is 0, jump to master */
    b proc_hang         /* Otherwise, hang */

proc_hang:
    wfe                 /* Wait For Event (low power) */
    b proc_hang

master:
    /* Clear BSS */
    adr x0, __bss_start
    adr x1, __bss_end
    sub x1, x1, x0
    cbz x1, run_main

clear_bss:
    str xzr, [x0], #8   /* Store zero, post-increment */
    sub x1, x1, #1
    cbnz x1, clear_bss

run_main:
    /* Set up stack */
    adr x0, __stack_top
    mov sp, x0

    /* Jump to C code */
    bl kernel_main

    /* If kernel_main returns, hang */
    b proc_hang

Code Explanation

Code Explanation (Beginner Friendly)

Assembly might look scary, but it's just a sequence of very small steps. The CPU has "Registers" which are like small sticky notes or scratchpads where it can hold numbers temporarily.

  • x0, x1, ... x30: These are general-purpose registers (your scratchpads).
  • w0 vs x0: w0 is the 32-bit version (half the sticky note), x0 is the full 64-bit version.

1. Multi-Core Handling ("Who am I?")

The Raspberry Pi 4 has 4 CPU Cores (Brain 0, Brain 1, Brain 2, Brain 3). When powered on, all of them wake up at once. If they all try to run our code, chaos happens! We need to put everyone to sleep except "Brain 0".

/* 1. Read the CPU ID into register x0 */
mrs x0, mpidr_el1  
/* 
   'mrs' = Move System Register. 
   Imagine copying a value from a special system readout (mpidr_el1) 
   onto our sticky note (x0).
*/

/* 2. Filter out unnecessary info */
and x0, x0, #0xFF  
/* 
   'and' = Logical AND operation. 
   The system ID has lots of extra data. We only want the last part (the ID).
   This clears everything else on sticky note x0, leaving only the ID number.
*/

/* 3. Check if ID is 0 */
cbz x0, master     
/* 
   'cbz' = Compare Branch Zero.
   "If the number on sticky note x0 is Zero, jump to the label 'master'."
   Meaning: If I am Brain 0, I go to work.
*/

/* 4. Everyone else goes here */
b proc_hang        
/* 
   'b' = Branch (Jump). 
   If I wasn't Brain 0, I skip the work and go to 'proc_hang' to sleep.
*/

Clearing BSS

adr x0, __bss_start
adr x1, __bss_end

The BSS section contains uninitialized global variables. We must zero them out before running C code.

Setting the Stack

adr x0, __stack_top
mov sp, x0

The C code needs a stack. We point the stack pointer (sp) to our reserved 16KB region.

Testing the Build

1
2
3
cd simpian-os/build
make
ls -lh kernel8.img

You should see kernel8.img (around 1-2KB).

Deploying to the Raspberry Pi

Important: Raspberry Pi 4 Boot Requirements

The Raspberry Pi 4 has an internal boot ROM, so bootcode.bin is not required (unlike Pi 3 and earlier). However, bare-metal kernels require proper device tree files (DTB) and overlays to boot correctly.

The easiest and most reliable way to test your bare-metal kernel:

  1. Prepare an SD card with Raspberry Pi OS (using Raspberry Pi Imager)
  2. Mount the boot partition on your PC
  3. Backup the original kernel:
    cp kernel8.img kernel8.img.backup
    
  4. Copy your kernel:
    cp /path/to/simpian-os/build/kernel8.img .
    
  5. Insert SD card into Raspberry Pi and power on

This method works because the Raspberry Pi OS boot partition already contains all required files:

  • start4.elf / fixup4.dat - GPU firmware
  • bcm2711-rpi-4-b.dtb - Device tree for Pi 4B
  • overlays/ - Device tree overlays
  • config.txt - Boot configuration

To restore Raspberry Pi OS, simply rename kernel8.img.backup back to kernel8.img.

Alternative: Minimal Boot Partition (Advanced)

If you want to create a minimal boot partition from scratch:

  1. Format SD Card as FAT32 (MBR, not GPT)

  2. Download required files:

    # GPU firmware
    wget https://github.com/raspberrypi/firmware/raw/master/boot/start4.elf
    wget https://github.com/raspberrypi/firmware/raw/master/boot/fixup4.dat
    
    # Device tree for Raspberry Pi 4B
    wget https://github.com/raspberrypi/firmware/raw/master/boot/bcm2711-rpi-4-b.dtb
    
    # Overlays (minimum required)
    mkdir -p overlays
    wget -P overlays https://github.com/raspberrypi/firmware/raw/master/boot/overlays/overlay_map.dtb
    
  3. Create config.txt:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    arm_64bit=1
    kernel=kernel8.img
    disable_overscan=1
    enable_uart=1
    
    # Mini UART requires stable core frequency
    # Without these, UART baud rate will be unstable
    initial_turbo=0
    core_freq_min=500
    

    !!! warning "Mini UART Timing Requirements" The Raspberry Pi 4's Mini UART (UART1) baud rate depends on the GPU core frequency. Dynamic frequency scaling causes timing issues, making serial output garbled or unreadable.

    1
    2
    3
    4
     - **`initial_turbo=0`**: Disables initial CPU turbo mode which affects UART timing
     - **`core_freq_min=500`**: Locks minimum GPU core frequency to 500MHz
    
     Without these settings, you may see **garbled characters** or **no UART output** even if your code is correct. This is especially critical when using USB-to-TTL serial adapters for debugging.
    

    The disable_overscan=1 setting is important for framebuffer graphics to work correctly with the requested resolution.

  4. Copy all files to SD card:

    cp start4.elf fixup4.dat bcm2711-rpi-4-b.dtb config.txt kernel8.img /mnt/sdcard/
    cp -r overlays /mnt/sdcard/
    
  5. Power on

Configuring Screen Resolution (config.txt)

If you are using a display and want to set a specific resolution (e.g., for the framebuffer driver later), you can add HDMI settings to config.txt.

Example: Force 1920x1080 (Full HD)

# Force HDMI
hdmi_force_hotplug=1

# DMT Mode (Monitor)
hdmi_group=2

# 1920x1080 @ 60Hz
hdmi_mode=82

# Framebuffer size (must match your code)
framebuffer_width=1920
framebuffer_height=1080

# Disable overscan to use full screen
disable_overscan=1

Common hdmi_mode values (for hdmi_group=2): - 82: 1920x1080 - 85: 1280x720 - 16: 1024x768 - 9: 800x600

Troubleshooting: Black Screen

If your kernel boots but the screen remains black (and you are sure your code is correct), it might be a resolution mismatch. 1. Try adding disable_overscan=1 to config.txt. 2. Ensure framebuffer_init() in your code matches the resolution in config.txt (if set). 3. If using a specific hdmi_mode, ensure your monitor supports it.

Debugging Tip

If you see a rainbow screen that stays forever, the GPU firmware loaded but your kernel failed to start. Check that all required files are present and that kernel8.img is a valid AArch64 binary.

At this point, the Raspberry Pi will boot your kernel, but you won't see any output yet since we haven't implemented UART communication.

Next Steps

In the next article, we'll implement the UART driver and print "Hello World" to see our kernel actually running!