Running Go on a PocketBook
Inkshelf is one static ARM program, built by a plain go build, that opens the
screen itself, draws on a canvas, and copies it, turned a quarter, into the framebuffer that the
panel shows.
PocketBook sells its readers with an SDK: a C library, libinkview, that draws the screen, reads the buttons and talks to the reader's system. Apps for the reader are normally written against it. Inkshelf is not. This chapter explains why, and what it does instead, from the build on your computer to the first pixel on the glass.
Using the SDK is like travelling with a heavy adapter kit: it works, but you must carry it everywhere and it only fits one plug. Inkshelf learned the local plugs instead, and travels with nothing but itself.
Why no SDK
The SDK is written in C. Go can call C, through cgocgo: Go's bridge to C code. Inkshelf does not use it, so a plain go build makes the reader's program on any computer., but the price is high:
- cgo needs a C compiler that makes code for the reader's ARM processor: the SDK's cross-compiler, with the reader's C headers and libraries, installed on every computer that builds the app;
- the program is then tied to a C library, libinkview, that must be on the reader in the right version.
Without C, all of that disappears. Go's own compiler makes ARM code from any computer, Mac,
Linux or Windows. The Makefile · build target is a single line:
TARGET := CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7
$(TARGET) go build -trimpath -ldflags="-s -w" -o build/inkshelf.app ./cmd/inkshelf
GOOS=linux GOARCH=arm names the target, GOARM=7 the reader's Cortex-A7, and
CGO_ENABLED=0 promises there is no C anywhere. The result, build/inkshelf.app, is an ordinary
Linux executable (an ELF file) for ARM, statically linked: everything it needs is inside.
The catch: libinkview's work has to be done by hand. That is the package
internal/device/pocketbook. What libinkview does was learned by disassembling
it and by running small probe programs on the reader. The next chapters show what it found.
Getting the app onto the reader
The reader shows up as a USB disk when you plug it in and choose "PC link". make deploy runs
deploy.sh, which finds the reader's volume under /Volumes (a folder holding
both applications and system), copies build/inkshelf.app into applications/, checks the
copy byte for byte, copies the launcher icons, then ejects.
One detail shows how small things bite. macOS likes to write hidden ._name files next to what
it copies, to store extra metadata. The reader's launcher lists every file in applications/
as an app, so each ._inkshelf.app would appear as a broken app. deploy.sh sets
COPYFILE_DISABLE=1, copies with cp -X, and deletes any ._ files left over.
The launcher's menu is a JSON file on the reader, system/config/desktop/view.json: an entry
there, edited once by hand, points at /mnt/ext1/applications/inkshelf.app and its icons.
From a tap on the icon to the first screen
When you tap the icon, the firmware starts the program as a new taskTask: What the PocketBook firmware calls a running app. It knows each one by its process group id (pgid).. main then sets up the
log, builds the app, and hands it to pocketbook.Run, which opens everything the reader shares
with apps.
Three of those objects (the global state, the task queue, /hwevent) are how the app and the
firmware talk: they are the subject of the next chapter. The other two,
the framebuffer and the canvas, are how pixels reach the panel.
Three layers: canvas, framebuffer, panel
A framebufferFramebuffer: The block of memory (/dev/fb0) the panel controller reads pixels from. Writing to it changes nothing on screen until a refresh. is memory whose bytes are the screen's pixels. On
the InkPad 3 it is /dev/fb0, one byte per pixel, 0 for black and 255 for white. On an LCD,
writing there would change the screen at once. On e-inkE-ink: A display made of tiny capsules of black and white pigment, moved by electric fields. It keeps its image without power. it does not: the
panel only changes when it is told to refresh an area, 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). to
its driver.
The app does not even write into the framebuffer. It draws on a
canvasCanvas: Where Inkshelf draws: a 1404×1872 gray image in shared memory, upright. A refresh copies it, rotated, to the framebuffer.: a 1404 × 1872 gray image, upright, in shared memory the firmware
gave the task (its task switcher shows it as a thumbnail). Fill, DrawImage and the text
drawing only change the canvas. Then the app calls Refresh(rect, waveform), and only then:
flushchecksvisible(): an app in the background must not touch the panel;- it waits, if the previous refresh is still running over the same area;
copyRotatedcopies the rectangle from the canvas into the framebuffer, turning it;- the ioctl
MXCFB_SEND_UPDATEasks the panel to show that rectangle with a waveformWaveform: The sequence of voltages the panel applies to change pixels. Fast ones show few grays; slow ones show 16 and clean ghosts.. It returns at once; the panel takes about half a second.
Keeping the canvas separate means the app can draw a whole page in many small steps and show it in one refresh, and that a page can be ready on the canvas before the panel is free.
The panel is mounted sideways
The panel's natural orientation is landscape: its framebuffer is 1872 pixels wide and 1404 high. You hold the reader in portrait, 1404 × 1872. So every refresh turns the picture a quarter: screen pixel (x, y) is framebuffer column y, row 1403 − x.
The canvas’s top row a b c d becomes the framebuffer’s left column, read from the bottom up. Each canvas row becomes a framebuffer column.
A row of the canvas becomes a column of the framebuffer. Copying means reading along one and writing down the other, which is where the time goes.
Copying fast: 4 × 4 blocks
The copy runs on every page turn, and framebuffer memory is slow: on an InkPad 3, about 19 ns a byte when written a byte at a time. A full page is 1404 × 1872 bytes, about 2.6 million. Worse, Go on 32-bit ARM never merges byte stores into word stores by itself.
So copyRotated moves pixels four at a time. It cuts the rectangle into 32 × 32 tiles (so the
rows it reads and the rows it writes stay in the cache), and each tile into 4 × 4 blocks. A block
is read as four 32-bit words, one per row, transposed into four words, one per column, with
shifts and masks, and written as four words. The result: about 22 ms a page turn, against
48 ms byte by byte.
even01, odd01 := interleaveBytes(row0, row1)
even23, odd23 := interleaveBytes(row2, row3)
column0, column2 := interleaveHalves(even01, even23)
column1, column3 := interleaveHalves(odd01, odd23)
Going deeper
- Edges. Only whole, word-aligned blocks go through the fast path:
wordBlocksfinds them (a canvas column whose address is a multiple of 4, a framebuffer column y that is a multiple of 4). The strips around them, above, below, left and right, are copied byte by byte bycopyRotatedBytes. If the memory is not laid out for words at all, everything goes byte by byte. - A hot path.
copyRotatedis one of the functions markedHOT PATHin the code. Its shape was tuned against the ARM assembly:interleaveBytes,interleaveHalvesand the tile iterator must be inlined. A change to it needsBenchmarkCopyRotatedbefore and after, and the same inlining list fromgo build -gcflags=-mforGOOS=linux GOARCH=arm. - The rectangle the panel gets.
framebufferRectturns the screen rectangle the same way:Rect(r.Min.Y, 1404 − r.Max.X, r.Max.Y, 1404 − r.Min.X). That is whatMXCFB_SEND_UPDATEreceives, as top, left, width and height. - Overlapping refreshes.
flushremembers the marker number and area of the last refresh. It waits for it (MXCFB_WAIT_FOR_UPDATE_COMPLETE) only if the new area overlaps it, so refreshes of different areas run side by side. Each refresh logs its wait, copy and send times toapplications/inkshelf.log: that is how page turns are timed on the reader.
Where it lives in the code
Makefile · buildThe one-line cross-compile: CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7.deploy.shCopies the app and icons to a reader over USB, without macOS ._ files, then ejects.cmd/inkshelf/main.go · mainLogging, app.New, then pocketbook.Run.cmd/inkshelf/logging.go · setupLoggingSends the log, stdout and stderr to applications/inkshelf.log.internal/device/pocketbook/firmware_linux_arm.go · openFirmwareOpens the framebuffer, the firmware’s shared state and queues, and the task canvas.internal/device/pocketbook/framebuffer_linux_arm.go · flushvisible, wait if overlapping, copyRotated, MXCFB_SEND_UPDATE.internal/device/pocketbook/canvas.go · FillDrawing functions: they change only the canvas.internal/device/pocketbook/framebuffer_copy.go · copyRotatedThe rotated copy in 4×4 blocks of words (HOT PATH).internal/device/pocketbook/eink_refresh.go · RefreshRefresh, FlashRefresh, the waveforms and the MXCFB ioctl numbers.
The full story: E-ink and the firmware.
Next: the app is not alone on the reader. Talking to the firmware shows how it shares the device with the firmware and the other apps.