How e-ink works
An E-inkE-ink: A display made of tiny capsules of black and white pigment, moved by electric fields. It keeps its image without power. pixel is a capsule of pigment that keeps its color without power but takes about half a second to change, so Inkshelf redraws only what changed, gently, and flashes the whole panel clean every few pages.
Your laptop's screen redraws itself 60 times a second whether anything changed or not. The InkPad 3's screen does the opposite: it does nothing at all until it is told to change, and then it takes a visible moment to do it. Every choice Inkshelf makes about drawing comes from that one fact. This chapter shows what is inside the panel, and the three rules that follow from it.
Think of a magnetic drawing board for children: a pen pulls dark filings up to the surface, and the drawing stays there with no battery. Sliding the bar under the board wipes it clean, but everything blinks. An e-ink panel is that board, with 2.6 million tiny cells and a computer holding the pen.
Tiny capsules of pigment
Each pixel of the panel holds a clear liquid with two kinds of tiny particles: white ones with one electric charge, black ones with the other. Under each pixel sits an electrode; above the whole panel, a transparent one. Put a voltage between them, and one color is pulled up to the glass while the other sinks. What you see is whichever color sits on top.
Two properties follow, and they shape everything else:
- It is bistable. Once the particles have moved, they stay, with no power at all. That is why an e-reader lasts weeks on a charge, and why the panel keeps showing the last page when the app is not drawing.
- It is slow. Particles have to swim through a liquid. Changing a pixel takes hundreds of
milliseconds, not the 16 ms of a computer screen. They swim even slower in the cold: Inkshelf
asks the driver to adjust to the panel's temperature (
tempUseAmbientininternal/device/pocketbook/eink_refresh.go).
Grays are particles stopped part of the way. To land a pixel exactly on one of 16 grays, the panel does not send one long push but a carefully timed sequence of pushes and pulls.
Waveforms: recipes of voltages
That sequence is a WaveformWaveform: The sequence of voltages the panel applies to change pixels. Fast ones show few grays; slow ones show 16 and clean ghosts.: a recipe of voltages, frame after frame, that takes
each pixel from its old gray to its new one. The panel's controller has several recipes, and each
trades speed for quality. The firmware numbers them, and Inkshelf names them in its Waveform
type:
| Waveform | Number | Grays | What it is for | Time on an InkPad 3 |
|---|---|---|---|---|
| GC16GC16: The slow, full-quality waveform: 16 grays, and every pixel is driven, so it flashes and wipes ghosts. | 2 | 16 | The cleanest: drives every pixel. As a full refresh it flashes black, then white | 600 ms full, 490 ms partial |
| GL16GL16: The 16-gray waveform for page turns: it drives only the pixels that change, so it does not flash, but ghosts build up. | 5 | 16 | Drives only the pixels that change; made for text on white, flickers less | 490 ms |
| DUDU / A2: Fast waveforms that only know black and white (DU) or are tuned for animation (A2): quick, but crude. | 1 | 2 | Black and white only, for quick feedback | 265 ms |
| A2 | 4 | 2 | Black and white, fastest, leaves the most ghosting | not measured |
| GC4, DU4 | 3, 8 | 4 | Four grays, rarely useful for books | not measured |
The times are the ones the EmulatorEmulator: A stand-in for the reader in a web page on your computer: the real event loop and screens, with a fake firmware. uses to imitate the panel
(fullRefreshTime, partialRefreshTime and fastRefreshTime in
internal/device/pocketbook/emulated_firmware.go), measured on a reader. Half a
second is the number to remember: that is how long every page turn spends waiting for the
panel, whatever the app does.
A refresh also comes in two modes. A partial update (updateModePartial) only touches
the pixels whose value changed. A full update (updateModeFull) drives every pixel of the
area, which is what makes the screen blink.
Ghosting, and the flash every few pages
A partial refresh is not perfect. A pixel that went from black to white is almost white, but a trace of the black stays. Turn a few pages and these traces pile up into a faint mess of old pages behind the new one: GhostingGhosting: A faint trace of the previous image left after a partial refresh. A full (flashing) refresh clears it.. A Full refreshFull refresh: A refresh that drives every pixel of the area, through black, so the panel flashes but is clean afterwards. drives every pixel through black and white before landing on the image, which wipes the traces, but the whole screen blinks.
So Inkshelf mixes the two. A normal page turn is a partial refresh with GL16 (the reader's
pageWaveform): no blink, half a second. Every few turns, the turn is a full GC16 refresh
instead, and the ghosts are gone:
- every 5th turn in a comic (
fullEveryininternal/app/openbook/openbook.go), - every 6th turn in a text book (
textFullEveryininternal/app/openbook/text.go): black text on white ghosts less than pictures.
The app also flashes when it comes back to the ForegroundForeground: The app the reader shows. Only the foreground app may read input and refresh the screen. (another app may have drawn anything meanwhile) and when a screen changes completely, for example when a book opens.
// fullRefreshDue reports whether the next page turn refreshes the panel whole.
func fullRefreshDue(app *state.State) bool {
every := fullEvery
if app.Reader.Text != nil {
every = textFullEvery
}
return app.Reader.Turns%every == every-1
}
Turns counts the turns since the book was opened, starting at 0, so turns 5, 10, 15… of a
comic flash.
Refresh only what changed
A refresh is not only a waveform: it is also a rectangle. The app tells the panel which
area to update, and only that area waits half a second. When you open the reader's toolbar,
Inkshelf refreshes only the toolbar's strip at the top (refreshAreaLocked in
internal/app/screens/reader/reader.go). When a page turns, it refreshes the
page's area and the area of the page before, in case the two pictures have different sizes.
Asking for a refresh does not wait for it to end: the request returns at once, and the panel works on its own. That raises a question. What if a second refresh comes while the first is still running? If they touch the same pixels, the new pixels would be changed under a refresh that is still driving them. If they don't, there is no reason to wait.
That is exactly the rule: Inkshelf waits only when the new rectangle overlaps the one still being refreshed.
They overlap: refresh 2 waits 430 ms for refresh 1 to end. All done at 980 ms.
How Inkshelf does it
Two functions do the drawing side of this: pocketbook.Refresh(rect, waveform) for a partial
refresh, and pocketbook.FlashRefresh() for a full GC16 refresh of the whole screen. On the
reader, both end in flush, which waits if needed, copies the area from the app's
CanvasCanvas: Where Inkshelf draws: a 1404×1872 gray image in shared memory, upright. A refresh copies it, rotated, to the framebuffer. into the FramebufferFramebuffer: The block of memory (/dev/fb0) the panel controller reads pixels from. Writing to it changes nothing on screen until a refresh., and sends the request to the panel's
driver with an ioctlioctl: A system call that sends a device-specific command to a driver, here to ask the panel for a refresh (MXCFB_SEND_UPDATE).:
// waitForOverlappingRefresh waits for the last refresh to end if it overlaps
// panelRect. Refreshes elsewhere go on meanwhile.
func (reader *readerFirmware) waitForOverlappingRefresh(panelRect image.Rectangle) {
if reader.lastMarker == 0 || !panelRect.Overlaps(reader.lastRect) {
return
}
marker := reader.lastMarker
// The ioctl fails when the refresh is already over: nothing to wait for.
_ = ioctl(reader.framebufferFD, mxcfbWaitForUpdate, unsafe.Pointer(&marker))
}
Each refresh gets a number, its marker. Waiting means asking the driver "tell me when refresh number N is over".
Going deeper
- The driver speaks i.MX6. The InkPad 3 has an Allwinner chip, but PocketBook's panel
driver (
epdc.ko) offers the ioctls of NXP's i.MX6 e-ink driver on/dev/fb0:MXCFB_SEND_UPDATE(0x4040462e) starts a refresh and returns at once,MXCFB_WAIT_FOR_UPDATE_COMPLETE(0x4004462f) waits for a marker. The request is a 64-bytemxcfb_update_data: top, left, width, height, waveform, mode, marker, temperature (mxcfbUpdateineink_refresh.go). - Rotated rectangles. The panel is mounted turned a quarter, so the rectangle sent to the
driver is the app's rectangle rotated into framebuffer coordinates (
framebufferRect). The overlap test is done on those, which gives the same answer. - Only the last refresh is remembered.
flushkeeps onelastRectand onelastMarker. With three refreshes A, B, C where B does not overlap A but C does, C compares itself only with B and does not wait for A. In practice the app's refreshes come one after the other on one thread, and the case does not arise often. - What a page turn costs besides the panel. The copy into the framebuffer, which also
rotates the pixels, takes about 22 ms a page turn (
copyRotated).flushlogs its wait, copy and send times toapplications/inkshelf.log: that is how page turns are timed on the reader. - Visible or not. Before every refresh,
flushchecks that the app is in the foreground and that the firmware does not suppress refreshes; otherwise it does nothing. - DU and A2 are unused. Inkshelf only uses GL16 (page turns) and GC16 (flashes, other
screens, error pages). The fast waveforms are in
Waveformbecause the firmware has them. - The emulator is pessimistic. With
-eink, the emulated panel sleeps for the refresh time after every refresh, overlapping or not, so it shows the worst case (refreshTimeinemulated_firmware.go). - Timings drift. The 490 / 600 / 265 ms above come from the emulator's constants and the
package documentation. An older comment next to
pageWaveforminreader.gosays 435 ms for GL16 and 576 ms for GC16, from an earlier measurement. Measure on your own reader with the log if the exact number matters.
Where it lives in the code
internal/device/pocketbook/eink_refresh.go · Waveform, Refresh, FlashRefreshThe waveforms, and the two calls the app refreshes the panel with.internal/device/pocketbook/eink_refresh.go · mxcfbUpdateEncodes the request for the panel: rectangle, waveform, mode, marker, temperature.internal/device/pocketbook/framebuffer_linux_arm.go · flushOn the reader: wait if needed, copy and rotate, send the ioctl, log the times.internal/device/pocketbook/framebuffer_linux_arm.go · waitForOverlappingRefreshWaits for the last refresh only if it overlaps the new one.internal/device/pocketbook/emulated_firmware.go · refreshTimeThe measured refresh times, used by the emulator.internal/app/openbook/openbook.go · fullRefreshDueEvery 5th turn of a comic, every 6th of a text (textFullEvery in text.go), flashes.internal/app/screens/reader/reader.go · pageWaveform, refreshAreaLockedGL16 for page turns, and the smallest area to refresh.
The full story: E-ink and the firmware.
Next: the panel shows only 16 grays, so a comic page must be turned into them. That is From a scan to 16 grays.