Wandering Hackaday.io for projects, I stumbled upon this project(click) by Stephen Holdaway. In this project, he solved a frustrating task faced by every dual boot user, which is sitting and waiting to choose os (Windows) from the GRUB menu any time when we want to switch to windows. He was able to add a hardware switch to determine the OS to boot each time the computer is turned on.
He achieved this by configuring STM32 Microcontroller as a USB mass-storage device. He has documented his entire journey through the research and implementation of the project in hackaday post(click). Please go through his post to get a better understanding of the implementations.
In this project, I will show how I managed to port the changes to Raspberry Pi Pico. You can find my version in this GitHub Repo (Click).
ConceptGNU GRUB is a program that runs before any Operating Systems are loaded. Through this menu, we can select which OS to load. GRUB offers very limited modules to work with. This means it cannot read data from a microcontroller connected via USB. But it can read data from storage disks.
So we can trick GRUB to read data from the microcontroller, by enumerating our micro as a mass-storage device.
Hence we enumerate our raspberry pi pico as a mass-storage device, via tinyUSB library, which will be having a file switch.cfg file, to which pico will write the switch position i.e 1 for ON 0 for OFF.
We have to add a script in GRUB, that's functions to read switch.cfg file and set the default to 0(Ubuntu )/2(Windows).
GRUB when loads, runs our custom scripts, which in turn searches for our device by its UUID identifiers, and if exits read the switch.cfg file. After getting the switch position it sets the default os selection respectively.
In summary,
- pico will configure itself as a mass-storage device.
- grub menu calls our script and asks for the particular file.
- Pico responds to the read request by adding the switch position in the switch.cfg file.
- the script in grub extracts the info from the file and sets the default option from the extracted data.
I have used the cdc_mscexample by tinyUSB to achieve this. The example configures the pico as a mass-storage device and creates a FAT12 filesystem and enumerates a README.txt file.
I changed the README.txt to switch.cfg and added the line "set os_hw_switch=0\n" to the file.
#define SWITCH_CFG_CONTENTS \
"set os_hw_switch=0\n"
...
//------------- Block3: Readme Content -------------//
SWITCH_CFG_CONTENTS
Now we have configured pico as a mass-storage device. After copying the uf2 file to pico, it enumerates as a storage device. We will be needing the UUID id of the device for the GRUB script, which is UUID="0000-1234".
$ sudo blkid
...
/dev/sda: SEC_TYPE="msdos" LABEL_FATBOOT="TinyUSB MSC" LABEL="TinyUSB MSC" UUID="0000-1234" BLOCK_SIZE="512" TYPE="vfat"
CircuitNow we need to read the switch position and change the content of the switch.cfg file accordingly i.e
- if the switch is ON: set os_hw_switch=1\n
- if the switch is OFF: set os_hw_switch=0\n
I have used GPIO_PIN 28 as the switch pin, which is set to pull down.
read_switch_value return the switch position i.e '1' is on (pulled high) and '0' is off (pulled low).
//-------------------------main.c---------------------
#define SWITCH_PIN 28
// read switch value
uint8_t read_switch_value()
{
return gpio_get(SWITCH_PIN) ? '1' : '0';
}
int main(void)
{
gpio_init(SWITCH_PIN);
//configure pin as INPUT
gpio_set_dir(SWITCH_PIN, false);
//configure pin as PULL_DOWN
gpio_set_pulls (SWITCH_PIN,false,true);
To write switch position to switch.cfg, I have used readGRUBConfig() which calls the read_switch_value function, and set the output buffer with the switch position.
I found that when reading the third block3 lba is set to 3, hence intercepting the call and calling readGrubConfig and passing the buffer where the content of the file will be copied.
//-------------------------msc_disk.c---------------------
static char grubConfigStr[] = "set os_hw_switch=0\n";
static void readGrubConfig(uint8_t* output)
{
// Modify config string with current switch value
grubConfigStr[sizeof(grubConfigStr)-3] = read_switch_value();
memcpy(output, &grubConfigStr, sizeof(grubConfigStr));
}
// Callback invoked when received READ10 command.
// Copy disk's data to buffer (up to bufsize) and return number of copied bytes.
int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize)
{
(void) lun;
// when reading the file
if(lba == 3){
readGrubConfig(buffer);
return bufsize;
}
...
...
}
Compile the Pico codeWe need to add pico stdlib to our code to get the gpio pin access.
//-------------------------main.c-----------------------------------
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "bsp/board.h"
#include "tusb.h"
...
#include "pico/stdlib.h"
To make the project:
$ mkdir build
$ cd build
$ cmake ..
$ make
Configuring GRUB to read the file contentI have added these changes in my Ubuntu 20.10.
$ sudo vim /etc/grub.d/40_custom
....
# Look for hardware switch device by its hard-coded filesystem ID
search --no-floppy --fs-uuid --set hdswitch 0000-1234
# If found, read dynamic config file and select appropriate entry for each position
if [ "${hdswitch}" ] ; then
source ($hdswitch)/switch.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
First, we search for our filesystem. GRUB has a subcommand search just for this.
- -no-floppy option prevents searching floppy devices
- -fs--uuid 0000-1234 searches a file system with UUID of 0000-1234.
If any device is found, the first device found is set as the value of the environment variable.
--set hdswitchhdswitch is our environment variable and is set with disk name if found.
Next, we source the switch.cfg file if the hdswitch variable is set, which creates another environment variable os_hw_switch
with the switch position i.e either 0/1.
We read the value of the os_hw_switch
and set the default to 0 or 2 respectively. 0 because Ubuntu is at 0th position and windows at 2nd position in the GRUB menu.
Lastly, if hdswitch was not set, we set the default to GRUB_DEFAULT.
Now we need to update our grub:
$ sudo update-grub
And we are done.
TestRestart the system with the device attached.
If everything is working fine, switching on the button would select windows or ubuntu if switched off.
Comments