typoena.dev / stories / waveform
Reference · e-ink
What's a waveform?
Ink you push around
Each pixel is a microscopic sealed capsule of clear fluid holding two kinds of charged pigment, black and white, that carry opposite charge. Put an electric field across the capsule and the particles migrate: one colour rises to the surface, the other sinks out of sight. Cut the field and they stay exactly where they are. That bistability is why the panel holds your page with the power off.
The catch is that these particles have mass and swim through a thick fluid. Moving them takes real time, tens to hundreds of milliseconds, and where they settle depends on where they started. So you can't just flip a pixel to black.
A recipe, not a switch
Instead, you coax the ink into place with a timed sequence of voltage pulses: push one way, ease off, nudge back. That sequence is the waveform. The panel redraws in fixed frames (roughly fifty a second), and each step of the recipe lasts a whole number of frames.
Two rules bound every recipe. First, it has to stay roughly balanced between pushing and pulling; lean too hard one way for too long and the ink degrades and ghosts permanently. Second, the costly one: more frames make a fuller, darker result, but frames are time. The length of that main push is the delay you feel when a letter appears.
Full, partial, fast
There's a family of waveforms, and choosing between them is the whole game:
- — A full refresh is the flash you sometimes see: every pixel driven through black and white to scrub away any residue. Immaculate, but slow.
- — A partial refresh touches only the pixels that changed, moving them straight to their new state with no flash. It's fast and quiet, but it leaves a faint trace each time, so a full refresh comes around now and then to clean up.
- — A fast refresh is an even shorter partial that trades a little crispness for speed. This is the dial that matters for typing.
Under the hood: the bytes
None of this is abstract on the panel. The recipe is a literal table of bytes you write to the display's controller. On this one (a Solomon SSD1683-class driver) the command is 0x32, "write LUT", and it takes a fixed 153-byte block.
Those 153 bytes split three ways: 60 choose the voltage level for each phase, 84 set the timing (how many frames each phase runs), and a trailing 9 hold frame-rate and gate-scan config. The voltage part packs a two-bit code per step: 00 hold (0 V), 01 push one way, 10 push the other. The whole shake-drive-settle shape from the diagram above is that grid of two-bit codes, and the frame counts are what turn it into milliseconds.
Two wrinkles make it fiddly. This is a wide panel driven as two controllers side by side, so the table has to be written to both (a master 0x32 and a slave 0x32|0x80), or the halves refresh differently and drift apart. And once it's loaded you have to tell the panel to actually use it. Writing 0xCF to the update-control register 0x22 plays the table you just wrote; the factory partial writes 0xFF there instead, which throws it away and reloads the panel's own built-in one. That single byte is the whole difference between your recipe and the factory's.
The fixed floor
This is what sets the wall. The recipe's length isn't computed from how many pixels moved; it's stored in the panel's factory lookup table and played back whole. So a partial refresh takes the same half-second whether you changed one letter or the entire screen. Trimming rows only shaves the little bit of time around the waveform; the waveform itself doesn't budge.
That wall, and the three rounds that proved exactly where it sits, is the per-keystroke story.