Close

Hardware OS selection switch

A project log for Hardware boot selection switch

A physical switch for my computer to choose between Linux and Windows

stephen-holdawayStephen Holdaway 05/03/2021 at 20:4110 Comments

Dual-booting Linux and Windows is a great way to get the best of both worlds, but there's one thing that's always bothered me. To boot into Linux, I simply press the power button and walk away. To boot into Windows on the other hand requires a tactical, precision-timed strike on the keyboard to change the selection when GRUB briefly reveals itself:

Now I could just increase the GRUB selection timeout, or remove it entirely, but I'd still need to wait around to make an operating system selection. I could use the mode in GRUB that remembers the last OS selection, but I'd still need to be around to change it half the time. I could make a "reboot into Windows" action in Linux, but I'm just as often booting from a powered-off state as I am rebooting from Linux.

Since I always know which operating system I want ahead of time, why not make a physical switch to select between Linux and Windows?

Scripting in the boot loader

GNU GRUB is a popular Linux boot loader - a program that runs before any operating system is loaded to decide what to boot and how to boot it. You'll normally interact with a menu like the one pictured above, but under the hood GRUB is configured by a simplified scripting language. Underneath that it's a system of modules written in C.

My only hands-on experience with GRUB script to date has been those occasional times where a system fails to boot and you find yourself plunged into a shell with the prompt "grub>". You fairly quickly discover that while this looks like typical Linux shell with ls, cat and tab completion, it is very much not. Running outside of an operating system, GRUB has to supply all of its own tools, so the functionality available is fairly bare-bones.

Arbitrary USB device access in GRUB?

My first thought was creating a USB device with a custom ("vendor-specific") interface to read out the switch position, which in non-USB terms is kind of like a bare serial connection. This is straight forward enough to access from an operating system, but I wasn't sure if GRUB could handle it.

GRUB does implement native USB support in its ehci, uhci and ohci modules, but there's a catch - loading any of these disables the normal mechanism used to access disks through the BIOS to avoid conflicts, leaving you with no disk access. There is a nativedisk module for accessing disks independently of the BIOS, but slowness aside, using this module critically means that GRUB can't chain-load Windows (explanation), making this approach a write-off.

In short, native access to arbitrary USB devices from GRUB isn't practical for this project, but USB isn't off the table entirely...

Pretending to be a USB mass-storage device

Instead of making a custom USB interface, we can leverage the fact that the BIOS already provides GRUB with access to all attached storage devices. All we would need to do is present our device as storage, containing a file whose contents indicate the switch position.

This is conceptually simple, but there are a few layers to it:

  1. Provide the mass-storage class descriptor, indicating one of several storage protocols to use (SCSI, ATA).
  2. Implement the chosen storage protocol. This is a set of commands to interrogate the storage device's capabilities, capacity, layout and other metadata in addition to standard requests to read and write sectors.
  3. Emulate a valid filesystem when read from, without actually having any storage medium.

Using the USB-capable STM32 boards and code I already have from USB Status Light, changing a vendor class device to a mass-storage class device was a matter of changing a few bytes in the existing USB descriptors.

For the storage protocol layer, I was happy to find that libopencm3 has a built-in SCSI mass-storage implementation with simple read_block(address) and write_block(address) callbacks, hiding the complexity of the storage protocol:

Thanks to this, I was up and running fairly quickly with a recognisable storage device, even if it could only read out endless streams  of 0x00 bytes at this stage. I did need to dig into the 581 page SCSI reference manual later on to resolve an issue with Windows, but this saved a lot of time:

Getting familiar with FAT

The next step was emulating a filesystem that GRUB could understand. I've formatted plenty of storage devices in my time, but I've never gone down to the byte level to understand the actual data and structures on disk. Without going into too much detail, I picked FAT12 as it's fairly well documented and has a simple layout:

After a couple of evenings of reading and tinkering, I had a setup where a list of virtual files could be defined, with the directory entries and file contents generated on-the-fly when requested by the unsuspecting host:

struct VirtualFile {
    char* longName;
    struct FatDirEntry dir;
    void (*read) (uint8_t* output);
};

static void readSwtich(uint8_t* output) { // Output is a zeroed-out 512 byte buffer (one block)
    output[0] = gpio_get(GPIOA, GPIO6) ? '1' : '0';
}

static struct VirtualFile _virtualFiles[] = {
    {
        .longName = "switch_position",
        .dir = { .name = "SWITCH~1", .ext = "   ", .size = 1 },
        .read = readSwtich
    },
    // ...
};

Dumping the block device looks like this:

0000h is the boot sector, 0200h is the first copy of the FAT, 0400h is the second copy of the FAT and 0600h is the root directory entry. There are three 32 byte directory entries - two long-filename entries followed by the actual directory entry for that file at 640h. The virtual file content is read out at 4800h.

I was a little worried that read-caching would cause issues with this approach, but thankfully there appears to be minimal or no read-caching in GRUB. Mounting the filesystem in Linux or Windows does cache reads heavily though, so this wouldn't be much use inside an OS.

Reading a file in GRUB

I had originally envisaged simply reading my "switch_position" file into a variable in GRUB and using that in an if/else condition. It turns out however that GRUB doesn't have any built-in support for loading the contents of a file into a variable, nor does it support command substitution like typical Linux shells do:

# This is BASH and what I wanted to do
# GRUB script unfortunately doesn't support this kind of substitution
pos=$(cat /path/to/switch_position)
echo $pos

I briefly looked into compiling a custom GRUB module to add a "read_file_into_variable" command, but compiling this and dumping in the /boot/grub/modules folder didn't seem like a stable long-term solution. It also wasn't particularly beginner-friendly and would need a lot of futzing around on any system it was going to be used on.

After some further poking around, I came to a much cleaner solution: using GRUB's "source" command to load additional config from a virtual file. This approach works out of the box, and in theory on any version of GRUB:

Booting based on switch position

Now that we are finally able to use the switch position in GRUB's shell, the only thing left to do is to modify the system boot config to make its boot selection based on this information. In /etc/grub.d/00_header I added this to the generated output (escaping removed to make it easier to read):

# Look for hardware switch device by its hard-coded filesystem ID
search --no-floppy --fs-uuid --set hdswitch 55AA-6922

# If found, read dynamic config file and select appropriate entry for each position
if [ "${hdswitch}" ] ; then
  source ($hdswitch)/switch_position_grub.cfg

  if [ "${os_hw_switch}" == 0 ] ; then
    # Boot Linux
    set default="0"
  elif [ "${os_hw_switch}" == 1 ] ; then
    # Boot Windows
    set default="2"
  else
    # Fallback to default
    set default="${GRUB_DEFAULT}"
  fi

else
  set default="${GRUB_DEFAULT}"
fi

After running update-grub to generate the new boot config, I'm happy to report that this setup work superbly:

Hardware installation

With a little aluminium flat-bar, a couple of screws and some drilling, this helpful little device is now a permanent fixture under the lip of my desk, within arms reach:

Making your own

If you'd like to make your own spin on this, you'll just need any USB-capable STM32 microcontroller and a switch of your choosing. The switch connects a single GPIO pin to ground when turned on, pulling it low. When the switch is off, the GPIO pin is pulled high by an internal pull-up.

The code and instructions to compile can be found on GitHub.

As this leverages libopencm3, it should be possible to build this for any USB-capable STM32 chip with minor additions to the code. I've added support for the common STM32F103 "blue pill" dev board as a starting point, in addition to the STM32F070 chip used on the custom board pictured here.

Discussions

Jonathan wrote 04/17/2024 at 14:47 point

Would this be easily ported to work with the raspberry pi Pico/rp2040?

  Are you sure? yes | no

Stephen Holdaway wrote 04/17/2024 at 22:58 point

Yep, this same setup could work on any part with USB device capability. The main thing you'll need to find is a MSC (mass-storage class) implementation for the Pico / tinyusb, as that's where most of the complexity is with this functionality. I'm leaning on an implementation provided in opencm3 for that.

  Are you sure? yes | no

adrianhall17 wrote 07/26/2021 at 02:09 point

I decided today that I needed this. The first search hit is somebody who wants exactly the same thing for exactly the same reasons. How often does that happen? Thanks!

  Are you sure? yes | no

Rakinuzzaman wrote 05/17/2021 at 09:34 point

Is this possible to implement using Arduino pro micro ( Atmega 32u4, that supports usb-HID) ?

  Are you sure? yes | no

Bob Dole wrote 05/09/2021 at 10:46 point

Good stuff. One thought that comes to my mind is, for people that want to avoid the fiddling with the STM part, maybe it'd be possible to susbtitude it for some piece of custom HW that allows you to select between 2 cheap USB devices with a switch, each one indicating GRUB what OS to launch. I mean, if this is possible, I find it quite easier to do, although of course, depends on each one habilities.

  Are you sure? yes | no

Stephen Holdaway wrote 05/09/2021 at 11:50 point

You could absolutely switch the 5V line to a single USB drive, then check if that filesystem is present to determine which OS should boot. The switch would need to be in the desired position at the time the BIOS does its drive detection and not change until after boot, but yeah - no custom firmware required!

I've been tinkering with USB projects on and off for a few years, so this was a relatively small step for me to make, but I totally understand that I've built this upon a mountain of knowledge. I set out to make a USB device at the start, so by the time I arrived at the filesystem solution it was more about the engineering challenge of "can I do this" than implementing the lowest effort solution.

I like the possibilities this fake mass storage approach opens up for more complex selections though, and I'm happy these three evenings of work have inspired people.

  Are you sure? yes | no

Karakurt wrote 05/07/2021 at 17:49 point

Awesome!
But you have told barely anything about hardware side...

  Are you sure? yes | no

Stephen Holdaway wrote 05/08/2021 at 06:39 point

Thanks for highlighting this! I've added some info to the project page and the end of this article. There's no special hardware required - it should be possible to get this to run on any USB-capable STM32 chip/board.

I've tested with the STM32F103 and made that the default in the GitHub repository, since it's a much more common part than the one pictured here

  Are you sure? yes | no

Karakurt wrote 05/08/2021 at 14:22 point

Thank you!

Now I'll for sure will built it into my laptop!

I have no idea what to use the second option for... 

But that's the question that can be answered later)

  Are you sure? yes | no

miimote wrote 05/07/2021 at 14:54 point

I love your solution. I'll give a try soon.

  Are you sure? yes | no