Skip to content

Repository files navigation

BootSwitch

A Raspberry Pi Pico that acts as a UEFI-bootable USB Mass Storage device, allowing a headless dual-boot machine to select its operating system via a physical toggle switch — no keyboard, no display required.

How It Works

[Toggle Switch] → [Pico GPIO15] → [USB MSC Virtual Drive]
                                          ↓
                              UEFI loads /EFI/BOOT/BOOTX64.EFI
                                          ↓
                                   GRUB reads grub.cfg
                              (generated live from GPIO state)
                                          ↓
                         chainload Linux  ←→  chainload Windows

The Pico presents itself as a FAT16 USB drive containing a minimal GRUB EFI binary and a dynamically generated grub.cfg. UEFI boots from the Pico, GRUB reads its config, and chainloads the bootloader on the target OS drive.

No data is stored on the Pico between reboots — the config is generated on every sector read based on the live GPIO state. Flipping the switch takes effect on the next boot.

Hardware

  • Raspberry Pi Pico (RP2040, 2 MB flash)
  • Any toggle switch between Pin 20 (GPIO15) and Pin 38 (GND)
Pico Pin 20 (GPIO15) ──── [switch] ──── Pico Pin 38 (GND)

Switch open  (HIGH, internal pull-up active) → Linux
Switch closed (LOW)                          → Windows

No resistors or additional components needed.

Building

Prerequisites

Step 1 — Build the GRUB EFI binary (on Linux)

# NixOS
nix-shell -p grub2_efi --run \
  "grub-mkimage -o bootx64.efi -O x86_64-efi -p /boot/grub \
  chain part_gpt part_msdos fat normal configfile search"

# Ubuntu/Debian
grub-mkimage -o bootx64.efi -O x86_64-efi -p /boot/grub \
  chain part_gpt part_msdos fat normal configfile search

Step 2 — Convert to a C header

nix-shell -p python3 --run "python3 -c \"
data = open('bootx64.efi', 'rb').read()
print('#pragma once')
print('#include <stdint.h>')
print(f'#define GRUB_BINARY_SIZE {len(data)}u')
print('const uint8_t grub_binary[] = {')
for i in range(0, len(data), 16):
    chunk = data[i:i+16]
    print('    ' + ', '.join(f'0x{b:02x}' for b in chunk) + ',')
print('};')
\" > grub_binary.h"

On Ubuntu/Debian replace nix-shell -p python3 --run with just python3 -c.

Step 3 — Copy grub_binary.h to the project

scp user@linux-machine:~/grub_binary.h /path/to/BootSwitch/

Step 4 — Compile and flash

  1. Open the project in VS Code
  2. Click Compile in the Pico extension toolbar
  3. Hold the BOOTSEL button on the Pico, plug it into your Mac/PC via USB
  4. The Pico mounts as RPI-RP2 — drag build/BootSwitch.uf2 onto it
  5. The Pico reboots automatically

Step 5 — Set boot order in UEFI

Enter your UEFI firmware settings and set the Pico (listed as a USB drive) as the first boot device.

Adapting to a Different System

Different OS bootloaders

Edit the two config strings in msc_disk.c:

// Switch open (pull-up) → this OS boots
static const char CFG_LINUX[] =
    "set timeout=0\n"
    "search --file --set=root /EFI/systemd/systemd-bootx64.efi\n"
    "chainloader /EFI/systemd/systemd-bootx64.efi\n"
    "boot\n";

// Switch closed (GND) → this OS boots
static const char CFG_WINDOWS[] =
    "set timeout=0\n"
    "search --file --set=root /EFI/Microsoft/Boot/bootmgfw.efi\n"
    "chainloader /EFI/Microsoft/Boot/bootmgfw.efi\n"
    "boot\n";

Use search --file --set=root with a path to a file that uniquely identifies the target EFI partition (e.g. /EFI/ubuntu/grubx64.efi, /EFI/Microsoft/Boot/bootmgfw.efi). Then chainload the appropriate EFI binary.

To find the right paths, run efibootmgr -v on Linux:

nix-shell -p efibootmgr --run "efibootmgr -v"  # NixOS
efibootmgr -v                                    # Ubuntu/Debian

Different GPIO pin

Change SWITCH_PIN in BootSwitch.c:

#define SWITCH_PIN 15  // change to any available GPIO

Switch logic (invert)

By default: open = Linux, closed = Windows (internal pull-up, switch to GND).

To invert, swap the two config strings in msc_disk.c, or wire the switch to 3.3V instead of GND and change gpio_pull_up to gpio_pull_down in BootSwitch.c.

Virtual Disk Layout

The Pico presents a 2.15 MB FAT16 drive. All sectors are generated on the fly — nothing is stored in RAM.

LBA    0        MBR (partition table)
LBA    1        VBR (FAT16 boot sector)
LBA    2 –  18  FAT copy 1
LBA   19 –  35  FAT copy 2
LBA   36 –  67  Root directory
LBA   68        /EFI/
LBA   69        /EFI/BOOT/
LBA   70 – 1093 /EFI/BOOT/BOOTX64.EFI  (GRUB, 512 KB)
LBA  1094        /boot/
LBA  1095        /boot/grub/
LBA  1096        /boot/grub/grub.cfg    ← dynamic, based on GPIO
LBA  1097+       free space (zeroes)

Verifying the Drive

After flashing, plug the Pico into any computer. It will mount as BOOTSWITCH. You can inspect it to verify:

ls /Volumes/BOOTSWITCH/
cat /Volumes/BOOTSWITCH/boot/grub/grub.cfg

The config should reflect the current switch position.

License

The source code in this repository is MIT licensed.

grub_binary.h — the GRUB EFI binary converted to a C array — is not included in this repository because GRUB is licensed under the GNU General Public License v3. You build it yourself from your system's GRUB package as described in the Building section above.

About

A Raspberry Pi Pico that acts as a UEFI-bootable USB Mass Storage device, allowing a headless dual-boot machine to select its operating system via a physical toggle switch — no keyboard, no display required.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages