CRC-16 Nibble Lookup Table

I already wrote about using smaller-than-256-byte lookup table. What we ended up with was a [smaller 32 byte lookup setup] for CRC-8. However, if you're sending a lot of data, CRC-8 won't do. Can one make a nibble lookup table for CRC-16 too?

Of course one can. It's actually exactly the same - just expand all variables to 16 bytes. In order to calculate lookup table, one can just use this code:

for (uint8_t i = 0; i < 16; i++) {
    uint16_t crc = (uint16_t)i;
    for (uint8_t j = 1; j <= 16; j++) {
        if (crc & 0x8000) {
            crc = (uint16_t)((crc << 1) ^ poly);
        } else {
            crc <<= 1;
        }
    }
    Crc16LutL[i] = crc;
}
for (uint8_t i = 0; i < 16; i++) {
    uint16_t crc = (uint16_t)i << 4;
    for (uint8_t j = 1; j <= 16; j++) {
        if (crc & 0x8000) {
            crc = (uint16_t)((crc << 1) ^ poly);
        } else {
            crc <<= 1;
        }
    }
    Crc16LutH[i] = crc;
}

I will be using polynomial 0xA2EB (0xD175 in Koopman's notation) since that one seems to be the best at this time. Mind you, in year from now, one might find another polynomial with better characteristics yet.

// Polynomial 0xA2EB
const uint16_t Crc16LutL[] = { 0x0000, 0xA2EB, 0xE73D, 0x45D6, 0x6C91, 0xCE7A, 0x8BAC, 0x2947, 0xD922, 0x7BC9, 0x3E1F, 0x9CF4, 0xB5B3, 0x1758, 0x528E, 0xF065, };
const uint16_t Crc16LutH[] = { 0x0000, 0x10AF, 0x215E, 0x31F1, 0x42BC, 0x5213, 0x63E2, 0x734D, 0x8578, 0x95D7, 0xA426, 0xB489, 0xC7C4, 0xD76B, 0xE69A, 0xF635, };

uint16_t crc16(uint8_t* data, uint8_t length) {
    uint16_t crc = 0;
    while (length--) {
        uint8_t combo = (crc >> 8) ^ *data++;
        crc = (crc << 8) ^ Crc16LutL[combo & 0x0F] ^ Crc16LutH[combo >> 4];
    }
    return crc;
}

These extra 64 bytes (two lookup tables, 32 bytes each) allows us to use shift-less CRC code. And yes, there will one shift operation in there but XC8 compiler should actually optimize that one away in most cases.

Since we do have a bit more complicated data handling, implemented on a microcontroller using XC8 compiler, this code needs 141 words of program memory (was 80 for CRC-8) and uses 16 bytes of RAM (was 4 for CRC-8). All in all, pretty handleable by almost any PIC microcontroller.

Interestingly, here you cannot use trick of removing const in order to move usage more toward memory. While RAM usage for such case indeed increases to 76 bytes, program memory usage doesn't go down at all.

As always, the example code is available for download.

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.