← Back to blog
DebuggingMarch 11, 2026· 4 min read

The Byte That Froze My Display

Debugging sometimes feels like archaeology. You start with a tiny symptom, brush away layers of assumptions, and eventually uncover a story that began decades before your code existed.

This one started with a frozen e-ink display.


The byte that froze my display

While working with a LilyGo e-ink display firmware, text rendering occasionally stopped mid-draw. No crash. No error. Just a device stuck in a loop as if the firmware had forgotten how to move forward.

The system printed most text fine. ASCII, numbers, normal words — everything behaved. But occasionally a particular character would appear and the display would stop updating.

After some digging, the problematic character turned out to be something completely ordinary:

°

The degree symbol.

In UTF-8 encoding, that character is stored as two bytes:

C2 B0

But the firmware sometimes received only:

B0

Just one byte.

And that lonely byte managed to freeze the renderer.

That was the beginning of a short journey into how UTF-8 actually works.

Characters are an illusion


When we write text like:

Temperature 32°C

we think in characters.

Computers don't.

Computers see something closer to this:

54 65 6D 70 65 72 61 74 75 72 65 20 33 32 C2 B0 43

A sequence of bytes.

The program reading those bytes must convert them into Unicode code points — numeric identifiers for characters.

That translation is done by a UTF-8 parser.

The trick behind UTF-8


UTF-8 is clever because it uses variable-length characters.

Some characters take one byte.

Some take two.

Some take three or four.

The first byte tells the parser how many bytes belong to the character.

0xxxxxxx          -> ASCII (1 byte)
110xxxxx          -> start of 2 byte character
1110xxxx          -> start of 3 byte character
11110xxx          -> start of 4 byte character
10xxxxxx          -> continuation byte

That last pattern is critical.

10xxxxxx

means this byte cannot start a character.

It must continue a previous character.

Now look again at the problematic byte:

B0 = 10110000

That begins with 10.

Which means it is only a continuation byte.

It cannot stand alone.

The parser problem


The display firmware had a UTF-8 parser that assumed every byte could start a character.

When it encountered a continuation byte like B0 without its leading byte, the parser didn't know what to do.

Instead of skipping it, the loop kept trying to decode it again and again.

Effectively:

read byte
try decode
fail
retry
repeat forever

One malformed byte caused an infinite loop.

The fix


The correct behavior is simple: if a byte is not a valid UTF-8 leading byte, treat it as invalid and move forward.

The patch added this safety check:

c
if (bytes == 0 || bytes > 4) { /* Invalid UTF-8 leading byte (e.g. bare continuation byte 0xB0). Skip one byte and return the Unicode replacement character. */ (*string)++; return 0xFFFD; }

Two things happen here:

  1. The parser skips the invalid byte
  2. It returns a replacement character

That replacement character is:

U+FFFD

Displayed as:

This symbol basically means:

"Something invalid appeared here."

Instead of hanging forever, the parser continues safely.

The fix was submitted here: LilyGo-EPD47 PR #183

The quiet lesson from a small bug


A single malformed byte froze an entire display.

Not because UTF-8 is flawed, but because the parser assumed input would always be valid.

That assumption is rarely safe.

Data coming from files, networks, or sensors can always contain corruption.

Robust parsers must handle that gracefully.

In this case, the entire fix came down to one defensive decision:

skip invalid byte
return U+FFFD
continue parsing

A tiny safeguard, but one that prevents an infinite loop and keeps the system moving.

Sometimes the stability of a device depends on a character that simply means:

A quiet placeholder for chaos.