RS-232 Framework Expansion Card

Boards for this project were sponsored by PCBWay (affiliate link).

A while ago I created a CAN-bus expansion card for Framework laptop. However, I consider this an utter failure. While the card did work, the lack of CAN-bus capable parts meant you cannot actually create one yourself. At least until this IC shortage comes to an end. However, experience was fun enough that I decided to make something else I do need and that one could actually manufacture in today’s world – a RS-232 expansion card.

For those uninitiated, RS-232 is an overall standard that defined connection between different devices for a long time. As many standards go, RS-232 is quite an expansive beast with many different signals. However, over the time, it became really common to have it boiled down to a three-wire setup: RXD (receive), TXD (transmit), and GND (ground). While RS-232 did disappear from desktops and laptops alike, it’s far from dead. Be it industrial equipment or a network switch, RS-232 still has its place.

Design was easy enough. To get from USB to TTL UART, I used MCP2221, but any similar chip will work equally well. Resulting TTL signal is then fed into MAX232 where charge pump brings it to RS-232 voltage levels, about ±8 V. This is quite sufficient to be fully RS-232 compliant. Even better, MAX232 will allow for receiving a full RS-232 ±15V signal with some margin. The end result should be that you can interconnect with pretty much any RS-232 compliant device out there.

Unlike for my CAN-bus expansion card, here I went for a slightly bigger connector of JST XH variety. While this one actually required a bit of modification to the expansion card casing, it’s actually much more convenient than its 2.0 mm brethren. First of all, spacing is 2.5 mm which is close enough to 0.1″ spacing that you can easily use a standard jumper wires to connect. This means you don’t need any specialized tools or bother with creating a cable yourself. Further more, if you plan to have something more permanent connected and you want the polarity protection you get from JST XH, it’s trivial to find many cables with 3-pin JST XH already present.

Pinout was another difficult choice and in the end I decided onto RXD GND TXD. Benefits of this approach are mostly in the area of safety. If GND wire was at pin 1, there is some place for doubt whether you start counting on left or right. With it being in the middle, that doubt is removed. Further more, to do the self-test, one just needs to connect two outer pins together. Again, it describes connection uniquely without even mentioning pin numbers. Yes, you need pin numbers for RXD and TXD regardless but swapping those two will never cause any damage so the guessing game is less dangerous.

As noted above, the boards were manufactured by PCBWay and there were a few things to watch out for. Due to the connector, these boards had to be 0.8mm. PCBWay does support this thickness without any extra cost and it’s only a click away.

Another thing to watch for, was that this board really does require 6/6 manufacturing process. Interestingly, I accidentally had one via that was too close but my board passed all the checks PCBWay had. When I got it back, I saw that via’s ring was adjusted to be slightly thinner on the offending side so everything ended up working just fine. While I appreciate such help, I would actually prefer the check to fail so I can correct the boards myself instead of silently fixing the issue.

One important thing that had me worried was if small PCB features needed for USB type-C edge connector would be routed correctly. What I discovered by accident was that some board houses do their routing with a 0.068″ mill end which is a bit too large for these cutouts. If that happens, you simply cannot fit this connector onto it. What you want is 0.04″ mill end to correctly handle this board outline. Either someone was paying attention or PCBWay does this by default. Either way, I got my boards perfectly milled.

If you look at pictures carefully, you will also notice something is off with the silk screen and here I must take a full fault for this. I do love Bahnschrift font and how well it renders at small sizes so I decided to use it for silk-screen markings. But I got greedy and went with its condensed variant that, in retrospective, had no chance of being readable. This one was purely on me.

In any case, boards were in my hands within 10 days. This also meant they arrived faster than I was ready for them but I cannot really hold this against PCBWay. :)

As far as case goes, I toyed with an idea to fit it into the existing USB A case. While this might have been just possible, it also meant for each device one would need to dismantle a perfectly good official expansion card. Thus, I went with 3D printed case with slight modifications. When combined with friction-fit top, card actually doesn’t look half bad.

With everything else out of the way, now comes the part where I tell you about potential issues. The major one is that this is non-insulated interface. RS-322 driver can easily handle ±30 V so this would not be an issue normally. However, for signals to be measured, one needs to have both communicating devices on the same ground potential. And therein lies the major trouble. If you accidentally connect something that is not ground into this, you will pass a significant current and maybe burn down the transceiver. In the worst case you could also damage the laptop as all grounds are connected together.

All that said, as long as you connect ground to ground you should be fine. If you are not sure about the ground potential on the other side, a neat trick is to simply disconnect your laptop from the AC adapter. If you run your laptop of the battery, its ground will “float” and thus you again can rely on ±30 V range of MAX232 chip. This simple trick will give you enough leeway to make many mistakes.

Lastly, there is a slightly unconventional fast (20 ms) fuse on the ground path that might save your butt if things go really bad. Notice that I said “might” and not “will” here. Fuses are great but even a fast fuse will take a decent amount of time to break the connection. Fortunately, type-C ports are quite rugged so odds are decent the motherboard won’t be damaged. But I wouldn’t bet on it.

My personal approach when connecting anything to this expansion card would be to make sure I’m connecting ground on both sides and, as an extra precaution, to test connection with laptop disconnected for the mains. If communication works fine, you can connect laptop back to the mains.

Also note this is nothing specific to this adapter – all non-insulated USB adapters (and most of them are) suffer from the same potential problems. However, due to the size of this expansion card, it’s much easier to misconnect wires then when you’re connecting to the DB-9.

In any case, as always, the design is freely available. If you don’t want to bother soldering one yourself, they are also available for purchase (USA only, at this time).

Dual Booting Ubuntu 22.04 and Windows 11 on Surface Go

While I installed Ubuntu before on my Surface Go, it always came at the cost of removing the Windows. Love them or hate them, Windows are sometime useful so dual boot would be ideal solution. With Surface Go having micro-SD card expansion slot, idea is clear – let’s dual boot Windows on internal disk and Ubuntu on SD card.

While you have Windows still running, prepare two USB drives. One will need to contain Windows installation image you can obtain via Microsoft’s Windows Installation Media Creator. Onto the other write Ubuntu 22.04 image using Rufus utility. Make sure to use GPT partition scheme targeting UEFI systems.

First we need to partition disk and install Linux for which we have to boot from Ubuntu USB drive. To do this go to Recovery Options and select Restart now. From the boot menu then select Use a device and finally use Linpus lite. If you are using Ubuntu, there is no need to disable secure boot or meddle with USB boot order as 22.04 fully supports secure boot (actually Microsoft signs their boot apps). However, you might want to change boot order to have an USB device first as you’ll need this later.

While you could proceed from here with normal Ubuntu install, I like a bit more involved process that includes a bit of command line. Since we need root prompt, we should open Terminal and get those root credentials going.

sudo -i

The very next step should be setting up a few variables – host, user name, and disk(s). This way we can use them going forward and avoid accidental mistakes.


Disk setup is really minimal. Notice that both boot and EFI partition will need to be on internal disk as BIOS doesn’t know how to boot from micro-SD card.

blkdiscard -f $DISK1
sgdisk --zap-all $DISK1
sgdisk -n1:1M:+127M -t1:EF00 -c1:EFI $DISK1
sgdisk -n2:0:+640M -t2:8300 -c2:Boot $DISK1
sgdisk --print $DISK1

blkdiscard -f $DISK2
sgdisk --zap-all $DISK2
sgdisk -n1:1M:0 -t1:8309 -c1:Ubuntu $DISK2
sgdisk --print $DISK2

I usually encrypt just the root partition as having boot partition unencrypted does offer advantages and having standard kernels exposed is not much of a security issue.

cryptsetup luksFormat -q --cipher aes-xts-plain64 --key-size 256 \
--pbkdf pbkdf2 --hash sha256 ${DISK2}p1

Since crypt device name is displayed on every startup, for Surface Go I like to use host name here.

cryptsetup luksOpen ${DISK2}p1 ${HOST^}

At last we can prepare all needed partitions.

yes | mkfs.ext4 /dev/mapper/${HOST^}
mkdir /mnt/install
mount /dev/mapper/${HOST^} /mnt/install/

yes | mkfs.ext4 ${DISK1}p2
mkdir /mnt/install/boot
mount ${DISK1}p2 /mnt/install/boot/

mkfs.msdos -F 32 -n EFI -i 4d65646f ${DISK1}p1
mkdir /mnt/install/boot/efi
mount ${DISK1}p1 /mnt/install/boot/efi

To start the fun we need debootstrap package. Do make sure you have Wireless network connected at this time as otherwise operation will not succeed.

apt update ; apt install --yes debootstrap

And then we can get basic OS on the disk. This will take a while.

debootstrap $(basename `ls -d /cdrom/dists/*/ | head -1`) /mnt/install/

Our newly copied system is lacking a few files and we should make sure they exist before proceeding.

echo $HOST > /mnt/install/etc/hostname
sed "s/ubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
sed '/cdrom/d' /etc/apt/sources.list > /mnt/install/etc/apt/sources.list
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

If you are installing via WiFi, you might as well copy your wireless credentials:

mkdir -p /mnt/install/etc/NetworkManager/system-connections/
cp /etc/NetworkManager/system-connections/* /mnt/install/etc/NetworkManager/system-connections/

Also, since we plan to do dual boot with Widnows, we need to tell Linux to leave local time in BIOS.

echo UTC=no >> /mnt/install/etc/default/rc5

Finally we’re ready to “chroot” into our new system.

mount --rbind /dev /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys /mnt/install/sys
chroot /mnt/install \
bash --login

For new system we need to setup the locale and the time zone.

locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales

dpkg-reconfigure tzdata

Now we’re ready to onboard the latest Linux image.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by boot environment packages.

apt install --yes initramfs-tools cryptsetup keyutils grub-efi-amd64-signed shim-signed

Since we’re dealing with encrypted data, we should auto mount it via crypttab. If there are multiple encrypted drives or partitions, keyscript really comes in handy to open them all with the same password. As it doesn’t have negative consequences, I just add it even for a single disk setup.

echo "${HOST^} UUID=$(blkid -s UUID -o value ${DISK2}p1) none \
luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
cat /etc/crypttab

To mount boot and EFI partition, we need to do some fstab setup too:

echo "UUID=$(blkid -s UUID -o value /dev/mapper/${HOST^}) \
/ ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value ${DISK1}p2) \
/boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value ${DISK1}p1) \
/boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
cat /etc/fstab

Now we update our boot environment.

KERNEL=`ls /usr/lib/modules/ | cut -d/ -f1 | sed 's/linux-image-//'`
update-initramfs -u -k $KERNEL

Grub update is what makes EFI tick.

mem_sleep_default=deep\"/" /etc/default/grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
--recheck --no-floppy

Finally we install out GUI environment. I personally like ubuntu-desktop-minimal but you can opt for ubuntu-desktop. In any case, it’ll take a considerable amount of time.

apt install --yes ubuntu-desktop-minimal

Short package upgrade will not hurt.

add-apt-repository universe
apt update ; apt dist-upgrade --yes

The only remaining task before restart is to create the user, assign a few extra groups to it, and make sure its home has correct owner.

adduser --disabled-password --gecos '' $USER
usermod -a -G adm,cdrom,dip,lpadmin,plugdev,sudo $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

As install is ready, we can exit our chroot environment.


And unmount our disk:

umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}

After the reboot you should be able to enjoy your Ubuntu installation.


If all went fine, congratulations, you have your Ubuntu up and running. But this is not the end as we still need to get Windows going.

Assuming you adjusted boot order in BIOS to boot of USB device first, just plug in USB drive with Windows 11 installation image and reboot the system to get into the Windows setup. You can also boot it from grub but I find just changing the boot order simpler.

Either way, you can proceed as normal with Windows installation, taking care to select the unassigned disk space on internal drive as install destination. Windows will then use the existing EFI partition to setup boot loader and remaining space for data.

Once you uncheck and delete all the nonsense that Windows installs by default, we need to boot back into Linux. In order to do this, go to Recovery Options and click on Restart now. This should result in boot menu where you should go into Use a device and you should see ubuntu there. If everything went right, this will boot you into Ubuntu.

Technically, if you want Windows to be your primary OS, you can stop at this. However, I want Linux to be default and thus a bit of chicanery is needed. We need to move Microsoft’s boot manager to other location. If you don’t do this, Surface’s BIOS will helpfully use it instead of grub. Removing it sorts this issue.

sudo mv /boot/efi/EFI/Microsoft /boot/efi/EFI/Microsoft2

And now finally we just add Windows boot entry to our grub menu.

cat << EOF | sudo tee /etc/grub.d/25_windows
exec tail -n +3 \$0
menuentry 'Windows' --class os {
search --no-floppy--fs-uuid --set=root 4D65-646F
chainloader (\${root})/EFI/Microsoft2/Boot/bootmgfw.efi

sudo chmod +x /etc/grub.d/25_windows
echo 'GRUB_RECORDFAIL_TIMEOUT=$GRUB_TIMEOUT' | sudo tee -a /etc/default/grub
sudo sed 's/GRUB_TIMEOUT=0/GRUB_TIMEOUT=1/' /etc/default/grub
sudo update-grub

This will boot Ubuntu by default but allow you to get into Windows as needed. If you would rather have it remember what you booted last. That’s easy enough too with some grub modifications.

Testing Native ZFS Encryption Speed (Ubuntu 22.04)

With the new Ubuntu LTS release, it came time to repeat my ZFS encryption testing. Is ZFS speed better, worse, or the same?

I won’t go into the test procedure much since I explained it back when I did it the first time. Outside of really minor differences in the exact disk size, procedure didn’t change. What did change is that I am not doing it on virtual machine anymore.

These tests I did on Framework laptop with i5-1135G7 processor and 32GB of RAM. It’s a bit more consistent setup than the virtual machine I used before. Due to this change, numbers are not really comparable to ones from previous tests but that should be fine – our main interest is in the relative numbers.

First of all, we can see that CCM encryption is not worth a dime if you have any AES-capable processor. Difference between CCM and any other encryption I tested is huge with CCM being 5-6 times slower. Only once I turned off the AES support in BIOS does its inclusion make even a minimal sense as this actually improves its performance. And no, it doesn’t suck less – it’s just that all other encryption methods suck more.

Assuming our machine has a processor made in the last 5 or so years, the native ZFS GCM encryption becomes the clear winner. Yes, 128-bit variant is a bit faster than 256-bit one (as expected) but difference is small enough that it probably wont matter. What will matter is that any GCM wins over LUKS. Yes, reads are slightly faster using standard XTS LUKS but writes are clearly favoring the native ZFS encryption.

Unless you really need the ultimate cryptographic opacity a LUKS encryption brings, a native ZFS encryption using GCM is still a way to go. And yes, even though GCM modes are performant, we still lose about 10-15% in writes and about 30% on reads when compared to no encryption at all. Mind you, as with all synthetic tests giving you the worst figures, the real performance loss is much lower.

Make what you want of it, but I’ll keep encrypting my drives. They’re plenty fast.

PS: You can take a peek at the raw data if you’re so inclined.

Wireguard on Mikrotik RouterOS 7 (and an Ubuntu Client Setup)

With an upgrade to Mikrotuk RouterOS 7.2, my OpenVPN setup started showing signs of distress in the form of a connection loss every hour or so. Instead of downgrading to the previously good version, I decided to abandon OpenVPN altogether. I figured it was about time to get Wireguard going.

As with OpenVPN setup, I will show all steps assuming you’re comfortable with both RouterOS and Ubuntu command line. And yes, an Ubuntu setup will work pretty much for any other linux with just a few minot changes.

First we need to create a Wireguard interface on the Mikrotik router. Here make a note of the “SERVER-PUBLIC” key. You will need it later.

/interface wireguard
add listen-port=51820 name=wireguard1
Flags: X - disabled; R - running
0 R name="wireguard1" listen-port=51820 private-key="SERVER-PRIVATE" public-key="SERVER-PUBLIC"

With the interface created, we need to add IP address for it. In my case, I choose in a completely separate subnet for this purpose.

/ip address
add address= network= interface=wireguard1

Finally, assuming you have a firewall sorted out, we need to add two rules – one for Wireguard itself and another one to allow communication with other nodes connected to the same router. I will add both of them at the very beginning but you should adjust their location to fit with your setup.

/ip firewall filter
add chain=input protocol=udp dst-port=51820 action=accept place-before=0
add chain=forward in-interface=wireguard1 action=accept place-before=1

To allow Wireguard clients access to Internet, we also need to do some masquerade (assuming ether1 is your Internet interface).

/ip firewall nat
chain=srcnat src-address= out-interface=ether1 action=masquerade

Now we need to get onto Ubuntu client and set wireguard there. The first step is, of course, to install some packages.

sudo apt update
sudo apt install --yes wireguard

Before anything else, we need a private and public key created. I like to get them both into variables instead of the files. Yes, it’s not as secure but for a single-user computer it’s good enough. Do make note of client’s public key as we’ll need it soon.

WG_PRIVATE_KEY=`wg genkey`
WG_PUBLIC_KEY=`echo $WG_PRIVATE_KEY | wg pubkey`

Now we need to create a Wireguard configuration file. Make sure to replace “SERVER-PUBLIC” with whatever public key you generated on server (not client!) and for endpoint make sure you give IP (or DNS name) of your router.

cat << EOF | sudo tee /etc/wireguard/wg1.conf
PrivateKey = $WG_PRIVATE_KEY
Address =

Endpoint =
AllowedIPs =

Once we have the config file ready, we need to get back to RouterOS and add our client as a peer using its public key. Note that this “CLIENT-PUBLIC” is a public key we got in Ubuntu just a few moments ago.

/interface wireguard peers
add interface=wireguard1 allowed-address= public-key="CLIENT-PUBLIC"

If everything went fine, you should have VPN properly configured. The easiest way of checking it is to simply bring interface up and check the route. It should show us using Wireguard interface (and IP) with pings flowing freely.

sudo wg-quick up wg0

ip route get dev wg0 table 51820 src uid 1000


If we want this connection to be up every time we boot the system, we can enable it as a service

sudo systemctl enable wg-quick@wg0.service

And that’s all folks.