Enter QMK Bootloader By Holding a Button

As Framework 16 has QMK keyboard, of course I was tempted to mess with it. But here lies the problem - to update the keyboard, one has to get into the boot mode. And to get into the boot mode, one has to simultaneously press both Alt keys while plugging the keyboard in (or 2 and 6 key for the Numpad). That gets old really quickly. So, for my first modification, I decided to make this a bit easier.

Dedicating a key for the boot functionality was out of question. I wanted to have a full keyboard experience and not to sacrifice any keys. And that goes double for the Numpad as there isn’t too many keys there to start with. So, I needed a key to serve its normal function during the day and to turn into the bootloading villain during the night. One way to signal such intent would be a long press.

But which key is unlikely to be long pressed on my keyboard you might ask. While there are couple of candidates, there is only one villain among them - CapsLock. Rarely used intentionally, and even when used, never held for long. Oh, and look at that, we have a parallel key on the Numpad - NumLock.

With keys decided upon, it was time to modify the firmware. Fortunately, we don’t need to start from scratch as Framework already did the hard part of the job. Unfortunately, there is so many branches and the most obvious one (framework16-keyboards) is not matching the production hardware. The last tag, v0.2.9 as I’m writing this, seems to match the hardware I have so I started from that.

So, how do we change it? Well, it’s easy as adding a few lines to the process_record_user function in keymap.c. Something like this:

...
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
+    static uint16_t bootloader_key_timer = 0;
+    static bool bootloader_other_key_recorded = false;  // track if any key other than CapsLock has been pressed
+    if (keycode != KC_CAPS) { bootloader_other_key_recorded = true; }
+
     switch (keycode) {
+        case KC_CAPS:  // enter bootloader if CapsLock is held for 5 seconds
+            if (record->event.pressed) {
+                bootloader_key_timer = timer_read();
+                bootloader_other_key_recorded = false;  // start tracking other keys
+            } else {
+                if (!bootloader_other_key_recorded) {  // only go to bootloader if no other key has been pressed
+                    if (timer_elapsed(bootloader_key_timer) >= 5000) {
+                        bootloader_jump();
+                    }
+                }
+                bootloader_key_timer = 0;  // reset timer counter on release so it can be used for tracking if CapsLock is pressed
+            }
+            break;
+
         case FN_LOCK:
...

This code will start timer as soon as CapsLock is pressed and then just track if any other key has been pressed while CapsLock is still down. If yes, it will just behave as it normally would (i.e., no bootload function). However, if there was no other keypresses and key has been held down for 5 seconds, upon releasing the CapsLock, you will go into the bootloader mode without having to disassemble your input modules.

Not strictly necessary modification but it makes QMK development so much easier.


PS: Or you can download code from my repo containing a few additional changes (e.g., NumLock changing background level for the Numpad).

Preventing hibernation wake-up on Ubuntu

I have Ubuntu 23.10 with hibernation enabled on my Framework 13 but I noticed that it wakes up after a few minutes every time I put it into hibernation. That sort of defeats the purpose of hibernation so I had to investigate a bit.

My first step was checking what is enabled. Fortunately, we can find that information rather easily.

cat /proc/acpi/wakeup | grep enabled

After playing with a few things, I noticed that disabling XHCI actually does the trick most of the time.

echo "XHCI" | sudo tee /proc/acpi/wakeup

While this can be one solution, I wanted to be a bit more granular. So I started with listing all /sys devices that have a wakeup enabled.

for FILE in `sudo find /sys/devices -name 'wakeup' -print 2>/dev/null`; do
    if [[ -f $FILE ]] && [[ "`cat $FILE`" == "enabled" ]]; then
        dirname "`dirname "$FILE"`"
    fi
done

For each device found, you can check a few more details.

udevadm info -q all -a /sys/devices/pci0000:00/0000:00:15.3/i2c_designware.2/i2c-2/i2c-PIXA3854:00/

Based on those details, I would create an entry in /etc/udev/rules.d/42-disable-wakeup.rules for each suspicious device. For example, if I suspected my keyboard driver, I would create an entry like this.

ACTION=="add", SUBSYSTEM=="serio", DRIVER=="atkbd", ATTR{power/wakeup}="disabled"

Once I placed all suspicious entries in, I forced a rule reload using udevadm and tried hibernation out.

sudo udevadm control --reload-rules && sudo udevadm trigger
sudo systemctl hibernate

While this did solve my issue, it was overly restrictive. So, I removed entries one by one, testing hibernation each time. Once done, I had a list of devices that caused the wakeup isolated. On my Framework 13 with i5-1135G7, the winning combination file can be created using this command:

cat << EOF | sudo tee /etc/udev/rules.d/42-disable-wakeup.rules
ACTION=="add", SUBSYSTEM=="i2c", DRIVER=="i2c_hid_acpi", ATTRS{name}=="PIXA3854:00", ATTR{power/wakeup}="disabled"
ACTION=="add", SUBSYSTEM=="pci", DRIVER=="xhci_hcd", ATTRS{subsystem_device}=="0x0001", ATTRS{subsystem_vendor}=="0xf111", ATTR{power/wakeup}="disabled"
ACTION=="add", SUBSYSTEM=="serio", DRIVER=="atkbd", ATTR{power/wakeup}="disabled"
EOF

Your laptop might have a different list of culprits but the overall procedure should work the same.

[2025-01-11 Probably better way is creating a small pre-hibernate script that will hunt down these devices.]

80386

One always remembers their first love. For me, that was i386DX running at screaming 40 MHz.

So, why am I having this senior moment remembering things from my youth? Well, Ken Shirriff decided to spend a bit of time, some acid, and a lot of patience in order to reverse engineer i386 processor logic cells. It’s a fascinating read.

So, for all i386 afficienados, here are a few of his articles to read (in order of my choosing):

Full Windows on USB Drive

While I am primarily a Linux user these days, I do need Windows occasionally. And on my Framework laptop, I find using Storage Expansion Card (essentially an USB drive) a perfect medium for it. I already wrote about Windows To-Go installation. But what if I wanted something more substantial? A full installation maybe?

Well, we can do that with the help of a few manual commands.

To start, we boot Windows 11 (or 10) off the USB as we normally would. However, instead of proceeding with the normal install, we press Shift+F10 to get into the command prompt.

First, we start with partitioning using the diskpart utility. I opted for a bit simpler partition setup than what Windows would generate normally. Most notably, there is no recovery partition. If you want one, you can just follow Microsoft’s instructions once system is up and running.

Within diskpart, the first task is to select the disk. Make sure it is the USB disk on which you want to install Windows. Failure to do so will surely damage something. Once the disk is cleaned, we follow with the creation of a few basic partitions before exiting the tool back to the command prompt.

LIST DISK
SELECT DISK 

CLEAN
CONVERT GPT

CREATE PARTITION EFI SIZE=100
FORMAT QUICK FS=fat32
ASSIGN LETTER S

CREATE PARTITION MSR SIZE=128

CREATE PARTITION PRIMARY
FORMAT QUICK FS=ntfs
ASSIGN LETTER W

LIST VOLUME
EXIT

Now we can install Windows using a combo of dism (to copy the Windows image) and bcdboot (to sort out EFI boot). Please note that if you already have Windows installed, the source files will most probably be on drive D: instead of C:. Depending on how the bootable USB was created, you might also need to change install.wim to install.esd. If the installation contains multiple editions, you can select which one you want using the /Index:N parameter.

DISM /Get-WimInfo /WimFile:C:\Sources\install.wim
DISM /Apply-Image /ImageFile:C:\sources\install.wim /Index:1 /ApplyDir:W:\
BCDBOOT W:\Windows /s S: /f UEFI

Once both commands are done, exit the installation process and reboot into our new installation. If it’s not a default boot location, make sure to press F12 to select it.

And that’s it - now you have Windows on a completely separate drive without any intermingling with the Linux installation.

Linux USB Auto-Suspend for FTDI

As I was playing with a few FTDI devices of mine, I noticed each was pulling 100 mA or more. Considering they were intended for use in a laptop, that made me worry a bit. Darn IC should have suspend and these numbers should not be the norm. So I went to investigate on how to reduce this a bit.

The first tool is, of course, lsusb. Using lsusb -tv I got the following output:

/:  Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 10000M
    ID 1d6b:0003 Linux Foundation 3.0 root hub
/:  Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M`
    ID 1d6b:0002 Linux Foundation 2.0 root hub
    |__ Port 3: Dev 60, If 0, Class=Vendor Specific Class, Driver=ftdi_sio, 12M
        ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
    |__ Port 7: Dev 3, If 0, Class=Video, Driver=uvcvideo, 480M
        ID 0bda:5634 Realtek Semiconductor Corp.
    |__ Port 7: Dev 3, If 1, Class=Video, Driver=uvcvideo, 480M
        ID 0bda:5634 Realtek Semiconductor Corp.
    |__ Port 9: Dev 4, If 0, Class=Vendor Specific Class, Driver=, 12M
        ID 27c6:609c Shenzhen Goodix Technology Co.,Ltd.
    |__ Port 10: Dev 5, If 0, Class=Wireless, Driver=btusb, 12M
        ID 8087:0026 Intel Corp. AX201 Bluetooth
    |__ Port 10: Dev 5, If 1, Class=Wireless, Driver=btusb, 12M
        ID 8087:0026 Intel Corp. AX201 Bluetooth
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 10000M
    ID 1d6b:0003 Linux Foundation 3.0 root hub
    |__ Port 3: Dev 2, If 0, Class=Mass Storage, Driver=uas, 5000M
        ID 0bc2:ab30 Seagate RSS LLC
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    ID 1d6b:0002 Linux Foundation 2.0 root hub

Device I wanted to analyze was at bus 3, port 3. This knowledge allowed me to check its power level.

cat /sys/bus/usb/drivers/usb/3-3/power/level

Value on that was returned immediately confirmed what I suspected. The port standby was disabled. Another test was to write auto inside the same file and current consumption fell down after 2-3 seconds (essentially, USB standby timeout), as expected.

Unfortunately, as soon as the device was plugged back in, it reset its behavior to always-on. Well, udev rules to the rescue.

Only, I already had FTDI udev rules present on my system (/etc/udev/rules.d/99-libftdi.rules). It seems that one of the tools I used (ftdidev library being the prime suspect) already installed some rules. Any rules I add to that file might get overwritten eventually. So, I decided to create a new file for both FT232R and X Series chips.

cat << EOF | sudo tee /etc/udev/rules.d/42-ftdi.rules
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTR{power/control}:="auto"
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", ATTR{power/control}:="auto"
EOF

This will turn on auto standby handling for my FTDI devices while making sure that subsequent rule doesn’t overwrite it (thus :=). Exactly what I needed to make my laptop battery happy.


To apply this without reboot, you can use the following command:

sudo udevadm control --reload-rules && sudo udevadm trigger