Mystery of a High ADC Reading

Microchip makes reading ADC easy enough. Select a channel, start measurement, read a registed - doesn't get much easier than that. However, in one project of mine I got a bit stumped. While most of my readings were spot on, one was stubornly too high.

As I went over PIC16F15386 documentation a bit I saw a following note: "It is recommended that ... the user selects Vss channel before connecting to the channel with the lower voltage." Whether due to a high channel count of some other pecularity of this PIC, capacitance was strong with this one. One of the rare times when reading instructions actually solved an issue.

Well, solved might have been too optimistic of a statement. While my low voltage ADC channel now read a correct value, my higher voltage inputs read slightly too low. Yes, I am aware I sound like a daddy bear looking at his bed but I too wanted my readings to be just right and for the darn Goldilocks to leave my home.

What I found working for me is doing the following: switch channel to GND, (dummy) read ADC, switch to the correct channel, do the first (proper) reading, do the second reading, average the two readings. In code it would be something like this:

ADCON0bits.CHS = 0b011011;    // select Vss channel
ADCON0bits.GO_nDONE = 1;      // start an A/D conversion cycle
while (ADCON0bits.GO_nDONE);  // wait for conversion to complete

ADCON0bits.CHS = channel;     // select channel

ADCON0bits.GO_nDONE = 1;      // start an A/D conversion cycle
while (ADCON0bits.GO_nDONE);  // wait for conversion to complete
uint16_t value1 = ADRES;      // read value

ADCON0bits.GO_nDONE = 1;      // start an A/D conversion cycle
while (ADCON0bits.GO_nDONE);  // wait for conversion to complete
uint16_t value2 = ADRES;      // read value

ADCON0bits.CHS = 0b011011;    // select Vss channel

return (value1 + value2 + 1) / 2;

Now, the obvious issue here is that three readings are done when only one is needed. Since we want to do averaging, there is nothing that can be done about reading it twice. However, if you are not pooling it all the time, you can pretty much skip the first (dummy) reading as switching to Vss channel at the end of routine does the trick.

While 16-bit operations are not the most pleasant thing 8-bit PIC can do, it's actually not too bad as we're talking about only 3 additions and one right shift (division by 2 gets optimized). Not good, but not terrible.

Even better, this works flawlessly with ADCs that have no issues with reading being too high or too low. This means I don't need to worry if I use routine with a different PIC microcontroller.

All in all, it's a cheap way to get a correct reading.

External Type-C Display Corruption on Windows 11

I've resisted Windows 11 for a while now but eventually I had to succumb and install it on one of my laptops. I figured Framework laptop would do fine. And indeed, all was well. Until I connected the external monitor.

At first corruption was so bad that I though the monitor surely was broken somehow but connecting it to other computers proved it working. I though maybe USB cable was malfunctioning but all other cables gave similar result.

Interestingly, every time I plugged the cable in I got a slightly different result. It ranged from just a few green pixels to almost correctly looking screen.

As a last resort, I decided to try messing with monitor's refresh rate and it took me a while to find it in menus, all the way behind System > Display > Advanced Display. There I had two settings, asinine 59.93 Hz, and a nice round 60 Hz. Wouldn't you know it, switching refresh rate to 60 Hz solved the issue!

However, I found that strange. Why? Because darn monitor worked at 59.94 Hz in Windows 10. Hm... Yes, it's not a typo. Windows 10 thought 59.94 Hz is appropriate frequency while Windows 11 decided to go with 59.93 Hz. Why the difference between releases - who knows.

In any case, using 60 Hz solved that issue and now I can be annoyed by Windows 11 on both monitors.


PS: Yes, Ubuntu 22.04 on the same computer with the same external monitor works just fine.

PPS: Yes, 59.94 Hz is more correct frequency as it's double the NTSC rate. Not sure from where Windows 11 got 59.93 Hz from.

I2C Framework Expansion Card

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

Some time ago I created an UART expansion card for Framework laptop. While card served me well, but MCP2221 around which the whole circuit was created has more to give - I2C. But for that, a bit of rethinking was needed.

The first order of business was a connector. UART realistically requires only 3 wires and thus connector was reasonably small. But, if one wants to express both UART and I2C, at least 5 wires are needed. Still manaegable but not sufficient for what I had in mind.

As I wanted this card to help me test and troubleshoot standalone I2C devices, I also needed to source power. Well, easy enough to expose my internal regulator output on another pin. And that gives us total of 6 pins.

However, if you count pins on my finialized device you will see a 7-pin connector. In order to minimize risk and keep UART connections straight in the middle I decided to add an empty pin with a nice side effect of isolating power pin from other thus making accidental connection less likely.

Since I already had UART PCB as a template, creating a new one was easy enough so in no time I uploaded it to PCBWay for manufacturing. 15 minutes later I got a message that something is wrong.

Failure was due to "non-plated slots size should be greater than 0.8mm" and it included a nice picture showing the issue. In a moment I figured an issue - my wide connector was using enough space to leave only slivers of PCB material remaining. Since I was always looking at it on the screen, I never got the feeling how thin PCB that was. However, my reviewer (Elena) did.

After looking into a few different solutions, I decided to maintain PCB's shape and just cut the bottom off. If you look into PCB carefully, you will see just a sliver of the slot remaining. While it might look like an accident, it actually helps with positioning around the stub in case.

Courtesy of fast shipping, PCBs were in my hand in less than two weeks. First thing I noticed was subpar silk screen printing. Yes, I pushed things a bit using such a small lettering but PCBWay usually does better job. It seems that truetype font I used is simply not compatible with their silkscreening process. I know for a fact that vector fonts normally used by PCB tools work flawlessly as I use PCBWay for a lot of non-sponsored content. But truetype font seems to be their cryptonite.

Boards themselves are what you would expect from HASL finish. As you can see on the picture, surface is not as leveled as you would get with ENIG but I found no issues positioning narrow-pitch type-C connector and soldering it in place.

While PCB outline is not really complicated, it's not trivial either. I had other manufacturers mess board outline routing by using mill end that's a bit too big. But PCBWay always routed it perfectly (and not just for sponsored boards). I pretty much consider them a first choice when it comes to framework expansion cards.

In any case, a short soldering session later and I had my device ready for testing. UART portion works as you would expect it. System sees it as a serial port and with correct baud rate world is your oyster.

I2C side requires download of a I2C/SMBus Terminal utility. Utility is easy enough that anyone familiar with I2C will quickly get the hang of it. Also you can look in other downloads if you want to create something custom.

Just keep in mind that I2C is not relly designed to be used via USB and inherent latency makes any high-speed communication impossible. Yes, your device might work at 400 kHz, but you still need to wait after every transfer for reply to come back. Inneficient at best.

Regardless, for testing, this is a decent addition for my Framework laptop toolbox.


PS: I got information on the silk screen from PCBWay and issue stems from how truetype font got rendered into gerber on my side. The end result is bunch of small lines that are way too short. In order to make each of those lines visible, they get expanded a bit and thus the result ends up looking too thick and blury. Now, some houses won't have issue with this as they might drop the lines instead of widening them up but that will probably cause issue with some other truetype font rendering. There is big enough "impedance mismatch" between how fonts and how gerbers work that I would recommend staying clear away from them completely as result will differ both from font to font and fab to fab. Every PCB tool has a vector font that's properly setup for usage in gerber and you should stick with that one. Lesson learned. :)

PPS: Source files are available on GitHub.

Manually Installing Ubuntu 22.04 on Surface Go (with Hibernation)

Just a few months ago I had a post about installing Ubuntu 22.04 on Surface Go. And that guide is still valid. However, while using my Surface Go during vacation, I noticed I miss hibernation. Yes, deep sleep is nice enough but hibernation is much sweeter deal when you expect your device to wake up after a longer time period.

Do note I am a huge fan of encryption and thus this guide will have both data and swap encrypted thus complicating setup a bit.

As always, all starts with a creation of Ubuntu installation media and booting into it. If done from Windows, you can use the original instructions for both. If you already have Linux installed, you can check how to do it from grub. Either way, I'll asume you have it all booted and that you selected "Try Ubuntu" when offered.

From there we need to get into Terminal and become a root user.

sudo -i

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

DISK=/dev/mmcblk0
HOST=smeagol
USER=josip

Disk setup is a bit more wasteful than what you would get following the original guide. While EFI and boot partitions are the same size, the swap partition has been increased to match RAM size (4 GB in my case). While you don't necessarily need it that big, it will help with hibernation if you do. And yes, I'm cheating a bit since the final swap size will be a bit under as encryption headers will take a bit of space. If you really need all MMC space you can get, system will work (most of the time) fine with 2 GB too.

blkdiscard -f $DISK
sgdisk --zap-all                     $DISK
sgdisk -n1:1M:+63M -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+640M -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+4G   -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:0     -t4:8309 -c4:Root $DISK
sgdisk --print                       $DISK

While one could encrypt boot partition too, I usually don't do it as it prevents double prompt (grub has to unlock boot partition separately of others) and that is too annoying for my taste. I do encrypt both data and swap of course. Make sure they use the same password if you don't want to always enter password twice.

cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i ${DISK}p4

cryptsetup luksFormat -q --type luks2 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i ${DISK}p3

Once encryption is done, we need to load the devices. For the main partition I like to use hostname as it's displayed in the prompt.

cryptsetup luksOpen ${DISK}p4 ${HOST}

cryptsetup luksOpen ${DISK}p3 swap

Now we can format (and mount) all partitions.

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

mkswap /dev/mapper/swap

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

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

To start the fun we need the 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/*/ | grep -v stable | 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/

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 \
    /usr/bin/env DISK=$DISK HOST=$HOST USER=$USER \
    bash --login

For the 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. And yes, there is a Surface Go specific kernel, but it seems that 5.17 you get with OEM kernels is as good.

apt update
apt install --yes --no-install-recommends linux-oem-22.04

Then we install 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..

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

To mount partitions, 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 ${DISK}p2) \
    /boot ext4 noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "PARTUUID=$(blkid -s PARTUUID -o value ${DISK}p1) \
    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=5s 0 1" >> /etc/fstab
echo "UUID=$(blkid -s UUID -o value /dev/mapper/swap) \
    none swap defaults 0 0" >> /etc/fstab
cat /etc/fstab

While defaults are actually matching most of the needed values, I like to explicilty list them and manually configure the time after which sleep will be followed by automatic hybernation (10 minutes in my case).

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' \
    /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=10min/' \
    /etc/systemd/sleep.conf

Lid switch can be then easily setup to use suspend-then-hibernate.

sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'
    /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=suspend-then-hibernate/'
    /etc/systemd/logind.conf

I also like to adjust swappiness to make system a bit more lively.

echo "vm.swappiness=10" >> /etc/sysctl.conf

With all that out of way, we can finally update our boot environment.

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

Grub update is the last part we need to make system bootable. And no, the second initramfs update is not optional as it needs to pickup RESUME variable.

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/swap)\"/" \
    /etc/default/grub
update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu \
    --recheck --no-floppy
update-initramfs -u -k all

Finally we install out GUI environment. I personally like ubuntu-desktop-minimal but you can opt for ubuntu-desktop.

apt install --yes ubuntu-desktop-minimal

Since this will not install any browser, you can add Firefox package too (apt install firefox) but I like to download Chrome.

cd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install ./google-chrome-stable_current_amd64.deb

Having power button do a hibernate action requires a bit more effort. And while you can use suspend-then-hibernate here too, I personally prefer to have it linked to straight hibernate.

gsettings set org.gnome.settings-daemon.plugins.power power-button-action nothing
echo 'event=button/power' > /etc/acpi/events/power
echo 'action=sudo systemctl hibernate' >> /etc/acpi/events/power

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 '' -u 1002 $USER
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USER
echo "$USER ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USER
passwd $USER

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

exit

And now we can unmount our disk, followed by a reboot.

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

reboot

If everything went fine, we can now hibernate.

systemctl hibernate

If you see hibernate not supported message, turn off the secure boot. At this time hibernation is not supported when secure boot is turned on.