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.
[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.
- 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.
- VS Code with the Raspberry Pi Pico extension
- Pico SDK (installed automatically by the extension)
- A Linux machine (or VM) to build the GRUB EFI binary
# 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 searchnix-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.
scp user@linux-machine:~/grub_binary.h /path/to/BootSwitch/- Open the project in VS Code
- Click Compile in the Pico extension toolbar
- Hold the BOOTSEL button on the Pico, plug it into your Mac/PC via USB
- The Pico mounts as
RPI-RP2— dragbuild/BootSwitch.uf2onto it - The Pico reboots automatically
Enter your UEFI firmware settings and set the Pico (listed as a USB drive) as the first boot device.
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/DebianChange SWITCH_PIN in BootSwitch.c:
#define SWITCH_PIN 15 // change to any available GPIOBy 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.
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)
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.cfgThe config should reflect the current switch position.
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.