Just the Hibernation Steps, Please

For a while now I use Ubuntu family of Linux on my Framework. While the exact flavor might change (Kubuntu rules! :)), two things remain the same. ZFS and hibernation. If you want to install system from scratch, you can find post for many different Ubuntu releases. What I don’t have is a separate post with just a hibernation steps. So, here it is.

First, let’s discuss prerequisites. You MUST have a separate swap partition if you want to hibernate to a file on ZFS. My setup is usually something like this:

PartitionTypeSize (MB)Description
EFIEF00FAT32255Small EFI partition
Boot8300EXT41,792Linux boot partition
Swap8200-65,536Swap, minimum 40% of RAM
(else)8309(whatever)one or more partitions for system and data

If you don’t have a separate partition, you can stop reading now and find somebody smarter. And no, the swap partition doesn’t need to be same size or larger than the amount of RAM you have. Minimum is actually 40% of your RAM (controlled by image_size parameter). Now, I’ve been guilty of having the swap the same size as RAM too. But that is not a requirement.

You also must have the Secure Boot disabled. If you don’t have it disabled, it will all seemingly work but system will never restore.

Steps I am giving here are for encrypted swap. I personally think that you should NEVER have unencrypted data on disk. NEVER. However, I am aware that most of people don’t care and see password entry as a chore. In one of the next posts, I will add instructions for non-encrypted swap too. So, if you are “one of those”, stay tuned.

Order of operations is quite fungible but, for the purpose of this guide, I will start with config files. There are two and whether you will modify them both depends on what you want to achieve.

In sleep.conf, I adjust suspend settings to allow for Suspend-then-Hibernate setup. My preference is to have system hibernate after 13 minutes of sleep but you can change that to your liking.

sudo sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateMode=.*/HibernateMode=platform shutdown/'           /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

Second config file is to reassign power button to hibernation. This is just my preference and, if you want power button to stay as-is, you can omit this step. For this, you need to install pm-utils package, if not already present on your system. Again, these are settings I like so adjust as needed.

sudo apt install -y pm-utils
sudo sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sudo sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

With this sorted out, you need to make sure computer doesn’t wake up. This step can be skipped quite often, but not so with Framework. With Framework laptops you need to manually disable wakeup for i2c_hid_acpi and xhci_hcd devices.

These commands will generate script I personally use and allow for its execution upon sleep.

cat << EOF | sudo tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF
sudo chmod +x /usr/lib/systemd/system-sleep/framework

Now, we can setup swap. For this I strongly recommend using variables as to avoid any naming issues. Replacewith your partition (e.g. /dev/disk/by-id/whatever-part3).

PART=<part>
UUID=`sudo blkid -s PARTUUID -o value $PART`

Assuming your swap is not initialized, you need to do so.

sudo cryptsetup luksFormat -q --type luks2 \
  --sector-size 4096 \
  --cipher aes-xts-plain64 --key-size 256 \
  --pbkdf argon2i /dev/disk/by-partuuid/$UUID
sudo cryptsetup luksOpen \
  --persistent --allow-discards \
  --perf-no_write_workqueue --perf-no_read_workqueue \
  /dev/disk/by-partuuid/$UUID $UUID
mkswap /dev/mapper/$UUID

Of course, adding this to both crypttab and fstab is also needed for swap to work properly.

echo "$UUID PARTUUID=$UUID none luks,discard,initramfs,keyscript=decrypt_keyctl" | sudo tee -a /etc/crypttab
echo "/dev/mapper/$UUID none swap sw,nofail 0 0" | sudo tee -a /etc/fstab

The final step is adding swap as RESUME into grub. Note that swap will be identified based on partition UUID.

sudo sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
  rtc_cmos.use_acpi_alarm=1 \
  RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$UUID)\"/" \
  /etc/default/grub

With these things in place, you should be able to use hibernate. To check, use systemctl.

sudo systemctl hibernate

And yes, you can probably use these steps with any laptop, not just Framework. However, I tested this on Framework and thus will not make other claims. :)

Sweet Dreams, My Dear Framework

Setting up sleep on my Framework 13 was a bit annoying but, once set, it worked perfectly. However, the same solution didn’t work on my Framework 16. While my installation is far from standard, the hibernation steps are quite straighforward:

  1. Setup swap partition RESUME variable in grub loader
  2. Adjust sleep.conf
  3. Disable wakeup for troublesome components so your laptop doesn’t wake immediately

And it was the step 3 that presented the problem - the darn thing kept waking up.

Since I sorted this out with Framework 13, I figured I can do the same for Framework 16. Even better, I found a forum post that actually told me which components need more of a sleep.

echo disabled > /sys/devices/pci0000:00/0000:00:08.1/0000:c1:00.3/usb1/1-4/1-4.3/power/wakeup
echo disabled > /sys/devices/platform/AMDI0010:03/i2c-1/i2c-PIXA3854:00/power/wakeup
echo disabled >/sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4b/PNP0C09:00/PNP0C0D:00/power/wakeup

Yes, it was different than my approach of disabling them in udev rules, but the same idea overall. And it even has the same suspects as for Framework 13, most notably Touchpad (i2c_hid_acpi), lid switch (button), and USB xHCI (xhci_hcd). Full of hope, I tried that and my computer still woke up.

My next step was to check the file (yes, I blindly copy/pasted it) and problem was obvious. My devices were at a different path. So I adjusted, tried it again, finished up a blog post, and called it a day. I mean, everything was working perfectly. Or so I thought.

After a few days, I placed computer into hibernate only for the darn thing to wake up on me. What the heck? I though I solved that issue. So I checked and noticed my devices were at slightly different location. Hm, maybe in all the fuss around finishing up blog post I accidentally made an error. So I addjusted paths and, with everything working correctly, called it a day.

But, guess what, in a few days I got the same issue again. And this time I was certain I had it done correctly. However, device paths were changed again. With so many independent USB devices, plug-and-play was moving stuff around every time system was rebooted.

So, I needed to script my /usr/lib/systemd/system-sleep/framework a bit smarter. At the end I ended up with this:

#!/bin/sh
case $1 in
  pre)
    for DRIVER_LINK in $(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=$(dirname $DRIVER_LINK)
      if [ ! -f "$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=$( basename $(readlink -f $DRIVER_LINK) )
      if [ "$DRIVER" = "i2c_hid_acpi" ] || [ "$DRIVER" = "xhci_hcd" ]; then
        echo disabled > $DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac

This will search for naughty devices every time hibernate is called upon and turn off wakeup. If PnP moves them, no worries, script will find them again.

And yes, the same script works for both Framework 13 and 16.


P.S.: While I mention button driveer in text, script actually doesn’t disable wakeup on lid switch. I kinda like computer to wake when I open it.

Dark Mode Trickery

One addition to these pages you might have noticed is, at the very bottom, an icon allowing you to switch between dark and light mode. And it’s not just a simple switch, it’s a tri-switch! While it allows for fixed light and dark modes, it also includes an automatic mode (i.e., based on your system settings).

And yes, there are quite a few ways that smarter people have used, but neither one worked exactly how I wanted. So, let’s see yet another way to do the same thing.

First step is, of course, setting up CSS. All colors for light scheme get to be defined in section with prefers-color-scheme: light, while dark colors get their prefers-color-scheme: dark section. I personally like to use these to setup variables to be used later, but you can define styles directly too.

@media (prefers-color-scheme: light) {
  /* styles */
}
@media (prefers-color-scheme: dark) {
  /* styles */
}

Next step is setting up a “button” for switching between themes. While we define three links, neither one of them is shown by default - we’ll sort that out later in the code.

<span>
  <a href="" id="color-scheme-auto" style="display: none;">Auto</a>
  <a href="" id="color-scheme-dark" style="display: none;">Dark</a>
  <a href="" id="color-scheme-light" style="display: none;">Light</a>
</span>

And yes, for my pages I don’t actually use text but icons. Below are links for Lucide icons I use currently, but you can go with whichever set you want.

  •  (automatic)
  •  (dark)
  •  (light)

Lastly, we come to the code and I’m just gonna drop the whole thing here. Explanations as to what each section does will be below.

<script>
  let systemScheme = 'light';
  if (window.matchMedia('(prefers-color-scheme: light)').matches) { systemScheme = 'light'; }
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) { systemScheme = 'dark'; }

  let savedScheme = localStorage.getItem("color-scheme");
  let currentScheme = systemScheme;
  switch (savedScheme) {
    case "light":
      currentScheme = "light";
      break;
    case "dark":
      currentScheme = "dark";
      break;
    default:
      savedScheme = "auto";
      break;
  }

  if (currentScheme !== systemScheme) { // swap at start so there's no flash
    for (var s = 0; s < document.styleSheets.length; s++) {
      try {
        for (var i = 0; i < document.styleSheets[s].cssRules.length; i++) {
          rule = document.styleSheets[s].cssRules[i];
          if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
            ruleMedia = rule.media.mediaText;
            if (ruleMedia.includes("light")) {
              newRuleMedia = ruleMedia.replace("light", "dark");
            } else if (ruleMedia.includes("dark")) {
              newRuleMedia = ruleMedia.replace("dark", "light");
            }
            if (newRuleMedia !== null) {
              rule.media.deleteMedium(ruleMedia);
              rule.media.appendMedium(newRuleMedia);
            }
          }
        }
      } catch (e) { }
    }
  }

  function nextColorScheme() {
    switch (savedScheme) {
      case "light": localStorage.removeItem("color-scheme"); break;
      case "dark": localStorage.setItem("color-scheme", "light"); break;
      default: localStorage.setItem("color-scheme", "dark"); break;
    }
    window.location.reload();  // to force button update
  }

  function updateButtons() {
    switch (savedScheme) {
      case "light": document.getElementById("color-scheme-light").style.display = 'inline'; break;
      case "dark": document.getElementById("color-scheme-dark").style.display = 'inline'; break;
      default: document.getElementById("color-scheme-auto").style.display = 'inline'; break;
    }
  }

  document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('color-scheme-auto').addEventListener('click', nextColorScheme);
    document.getElementById('color-scheme-dark').addEventListener('click', nextColorScheme);
    document.getElementById('color-scheme-light').addEventListener('click', nextColorScheme);
    updateButtons();
  });
</script>

The first portion just determines system scheme and stores it in systemScheme variable. Variable will contain whatever system tells the preferred scheme should be - either light or dark.

Next portion is all about loading what user (maybe) saved the last time. For this purpose we’re using localStorage and the result gets stored in savedScheme variable. Its state will set currentScheme variable to match either what is stored or the system scheme if we have no better idea (i.e., automatic mode).

End result of this variable game is decision if currentScheme differs from systemScheme. If they are different, we simply swap dark and light settings around. This swap is actually what does all the heavy lifting.

The nextColorScheme method checks the current state (savedState variable) and moves to the next one. States are written as light and dark. For automatic handling, code simply deletes storage altogether. Once that is done, it won’t attempt to sort out any swaps needed to get colors in line. Nope, it will simply reload the page and let the loading code sort it out.

The updateButtons is what displays whatever scheme is selected for a bit of user feedback.

The last portion of code will add an event listener to the click event of each scheme “button” (identified by id) so that each click calls nextColorScheme method. Here is also where we call updateButtons method to show the current state.

With all this in place, our theme switching should just work.

AuxPower1U: Mistakes were made

This is a post 11 in the series (previous: Main Controller PCB).

This post is sponsored by PCBWay.


With AuxPower1U being an actual physical object, I can see there were some mistakes. Most of them I worked around for and they’re already fixed in the repository for potential future version. But some of them will remain as a design choice.

First one has nothing to do with electronics but with my aluminium plate (aka “heat sink”). I planned for tapping holes so I can screw power supplies directly. Unfortunately, my model actually swapped diameter and radius and thus I got holes much bigger than I wanted. Classic error. So I used just screws and washers to get over that. But that actually made me think - do I really want to make all those tapping holes? Answer is no - longer screw and nut are good enough.

With that out of way, next portion are the PCB errors that were fixed courtesy of PCBWay respinning my desing. While the first revision board could be fixed using bodge wires, and I did so for firmware development, errors were big enough that I really needed a new PCB.

Current monitoring was the bigest miss here. I used my trusty ZXCT1009 - something I used many times ago. So, of course, I forgot to include ground resistor which made its readings go wild. Connecting resistor via bodge wire fixed that error. But, only when testing at 30V released the magic smoke, I checked the data sheet more closely. Yes, ZXCT1009 is limited to 20V. This never came up as most of my designs stick to 12V at most. But it became problem now.

However, finding a chip that goes up to 60V proved to be a difficult task. There actually are not too many simple chips that go that high and I didn’t want something that has more than 3 pins. Well, I think I found my new favorite current monitor - HV7801.

While HV7801 has more than 3 pins, it still comes in SOT-23 package and requires no external components. It actually occupies less PCB realestate than ZXCT1009. Downside is that its gain is fixed to 5x thus requiring me to use 1.024V ADC range. With ZXCT1009 it’s much easier to get resistor dividers than are nicely “rounded” for ADC and fit almost any range. With HV7801 you get what you get. Despite all of this, if I need current measurement that doesn’t need to be precise and I have 12-bit ADC, this chip is awesome.

Speaking of current measurement, I used 0.1Ω 3W resistor at first. It’s the same resistor I used in many other projects and it was always an overkill. What I temporarily forgot is that my low voltage ranges (e.g., 15V) will need to pass a lot of current. Thus, my trusty 0.1Ω resistor was getting way more heat than appropriate. After actually running the numbers, I decided to go with 0.025Ω 3W. Still gives nice figures for microcontroller calculations while producing way less heat.

Unfortunately, since my MCP9701A temperature sensor outputs around 800 mA at the room temperature, usage of HV7801 also meant I have to switch ADC ranges every time I measure the current. MCP9701A at 1.024V ADC range would only allow measurement up to 32°C. Even its 9700A brother (that I didn’t have in stock) would only allow up to 62°C. Switching to 2.048V just for temperature measurement is not the end of the world but it does mean slower measurement and it probably has implications for precision too.

One other case of “the forgotten resistor” is a pull-up for output MOSFETs. Early in the design phase I decided that resetting the board should not bring outputs down. Idea behind it is that, even if my board misbehaves, current will flow. There are merits on starting the system with all devices off but, since this would control power to my wireless, I decided it should fail-closed. And for that you need pull-ups. That I have forgotten.

Since we’re talking about resistors, I also had to increased ones used for LEDs. Those things were just too bright. While this means absolutely nothing once box it closed, it meant a lot during debugging since I could actually look at the PCB without burning my retinas. See, high-efficiency LEDs are not always good. :)

Speaking of debugging, I originally had my ICSP (i.e., debugging) lines shared with UART. This was necessary as I didn’t have enough pins and had to make some share functionality. Later in the project I added an I²C extender and thus aleviated the issue. However, I forgot to decouple UART lines from ICSP thus making the debugging of UART communication impossible. Solution was simple enough - just move those lines.

With all these errors, you can see why revision B was needed. While many of errors could be sorted by bodging wire here and there, and indeed they were for the purpose of firmware development, it was just too many to ever trust that board. Revision B saved the day.

Goodbye, Wordpress

As you might have noted, I took a couple of weeks from posting here. Why? Well, I decided to figure out what to do with my website. Most notable its backend.

You see, for a while now, I have not been happy with Wordpress. My main issue has been their push toward the block editor. I just don’t find it working well for my style of writing. It was just too cubersome and I ended switching to Markdown editor for my processing needs. Unfortunately, that is also not an ideal experience, especially because this markdown processor is not the newest beast and it often messes up a perfectly valid syntax (e.g. manually wrapped paragraphs). Of course, Wordpress’ latest scandal definitely didn’t improve anything.

And yes, this is not the first time I’m changing how these pages are generated. Before switching to Wordpress, I was running my own ASP.NET engine. Before that, I was using Blogspot. Before that, I was doing raw HTML. And there were many intermediate steps in between. It’s safe to say that every few years I look into alternatives to what I’m currently running.

This year, for the first time, I tried 11ty and… I fell in love. Interestingly, this static site generator is not a new beast. However, I somehow missed it a year or so ago when I tried Hugo and MkDocs, neither or which really worked for me.

11ty was trivial to get up working and expanding its functionality is just smidgen more difficult. Before I started, I made a list of things I wanted to work. Yes, my site isn’t overly complicated but I was surprised how easy was to get all things going.

Automatic thumbnails - a few hours to implement from scratch. Search - working within an hour (courtesy of Pagefind) albeit followed by a few hours of figuring out look&feel. Import (courtesy of wordpress-export-to-markdown script), albeit rather basic, done in an hour too. Custom scripts for calculators present on some pages, it’s markdown and just works.

Based on length of my hiatus, you can see there were many additional steps. For example, I didn’t find a theme I liked so building my own took ages. Figuring out the structure and finding the good balance in preserving links and getting new functionality took a while too. What 11ty allowed was for neither of those steps to take too long individually. There is just something special in seeing immediate progress. And it’s not as if I worked on this 24/7 since I have a daily job and I also squeezed in visiting Croatia for a week.

Now, finally, my static 11ty site is in good enough state to share it with the world. Not all is working yet, but I am comfortable enough that I can deal with rest of issues in the background.

The first thing you’ll notice is absence of comments and this is something I’ll miss the most. I simply didn’t find a good enough solution to deal with them. To replace that, I do have a “Contact” at the bottom of every page now so you can contact me. Poor replacement, but all that’s there for now. Mind you, my site never had an overwhelming number of comments. But those precious occasional discussions are gone for now.

The second issue is that not all stuff was imported correctly. Older things posts that used Wordpress shortcodes to display code got quite mangled in the process of conversion. I am working on fixing those and eventually they will be converted to a proper markdown. However, since that impacts only posts older than 2 years, I decided to simply fix that eventually instead of postponing the move to 2025.

With this engine swap behind me, normal program will continue the next week.


PS: Well, Matt Mullenweg is keeping up his efforts to destroy WordPress community. And yes, WordPress has always tried to bend developers to their will (remember Suffusion removal). But lately things seem a bit more insane than usual. I guess timing of my move is fortunate.