Raspberry Pi Bootloader

2025 – 2026

A bare-metal bootloader for the Raspberry Pi Compute Module 4 (BCM2711, Cortex-A72). It loads first, validates the kernel image, and hands off control to the RTOS — with support for firmware updates over UART (DFU) and image loading from eMMC. Written from scratch in C and ARM assembly.

The project is available on GitHub.

Startup Sequence

The default RPi GPU firmware hands control to the reset handler at 0x8000 in HYP mode.

startup_flow

Memory Layout

The boot chain is goes through three stages. They are all linked at fixed addresses so the next stage can be cleanly loaded.

Bootstrap: The bootstrap image lives in the FAT32 partition provided by the RPi firmware and is loaded by the built-in GPU firmware. It brings up just enough (UART + eMMC) to CRC-validate and load the bootloader at 0x10000.

Bootloader: The bootloader validates the app's CRC or alternatively it can trusts the preserved boot flags on a DFU-triggered warm reset. It is then responsible for loading the app. A 4 KB shared-flags region at 0x88000 carries the BootFlags struct (magic, reset reason, DFU-requested flag, CRC-valid flag).

Application The app itself lives in one of two A/B slots on eMMC, so a bad flash can be rolled back without bricking the board.

Bootloader and DFU

The bootloader image loads first and is responsible for validating and jumping to the kernel. On startup it checks the shared boot flags: if a firmware update was requested, it enters DFU mode; otherwise it validates the kernel CRC and jumps to 0x88400. The DFU protocol operates over UART and uses a simple framed packet format with a 32-bit CRC for error detection. A freshly flashed slot boots on trial, and the bootloader tracks trial attempts and watchdog reset counts in a small WdtMeta struct on eMMC, rolling back to the other A/B slot if the new one never confirms itself.

Bootloader startup flow

What bootloader_init() and bootloader_execute() do on every boot, from resolving the trial/watchdog state to jumping into the app:

The reset reason and DFU-request flags shared with the app at 0x88000:

typedef enum {
  RESET_REASON_COLD     = 0,
  RESET_REASON_WATCHDOG = 1,
  RESET_REASON_SOFTWARE = 2,
} ResetReason;

typedef struct {
  uint32_t magic;           // BOOT_FLAGS_MAGIC when valid
  uint32_t reset_reason;    // ResetReason
  uint32_t dfu_requested;   // non-zero -> enter DFU on next boot
  uint32_t fw_crc_ok;       // non-zero -> bootloader verified firmware CRC
} BootFlags;                // shared RAM at 0x88000 (BOOT_FLAGS_START_ADDR)

The trial/watchdog metadata persisted on eMMC, read once at boot and updated by the trial and tolerance logic above:

typedef enum {
  WATCHDOG_RESET_POLICY_FORCE_UPDATE = 0,  // force into the DFU receive loop
  WATCHDOG_RESET_POLICY_JUMP_SLOT_B  = 1,  // reserved (not implemented)
  WATCHDOG_RESET_POLICY_ROLLBACK     = 2,  // roll back to the other A/B slot
} WatchdogResetPolicy;

typedef struct {
  uint32_t magic;
  uint32_t wdt_reset_count;
  int32_t  wdt_reset_tolerance;
  uint32_t wdt_reset_policy;
  uint32_t wdt_reset_reason;
  uint32_t active_app_slot;    // APP_SLOT_A / APP_SLOT_B (see emmc.h)
  uint32_t app_slot_trial;     // 1 = active slot unconfirmed (A/B trial)
  uint32_t trial_boot_count;   // bootloader re-entries while on trial
  uint32_t crc;                // CRC32 over all preceding fields; MUST be last
} WdtMeta;                     // persisted in eMMC EMMC_SECTOR_METADATA

DFU packet format

The framed packet format the DFU protocol uses over UART:

DFU receive loop flow

What dfu_receive() does with each packet as it comes in, from the trigger handshake through committing the new slot:

DFU & startup process viewer on the RTOS client

DFU is supported through scripts and also via the rtos client GUI. Here's a demo of how the DFU looks from the GUI. Notice how the designated app slot changes after flashing a new app.