How to draw to the screen?

e-ink devices tend to be low power and run without an X server. The way they draw to the screen is by using the framebuffer. The framebuffer is a piece of memory that directly represents what is on screen and can usually be found at /dev/fb0. Originally, the framebuffer represented the memory of what was being shown on the display, but these days the framebuffer is typically emulated: when you write to a framebuffer device, the kernel driver will copy that over to the display.

Either way, the framebuffer is a piece of memory that represents the pixels on screen. The pixels can be stored as 8 bit grayscale (Y8), 16 bit RGB (rgb565) or 32 bit RGBA.

The framebuffer details are usually obtained with a line like: ioctl(self.fd, FBIOGET_VSCREENINFO, &vinfo)), which will obtain the framebuffer’s width, height and stride, as well as the bit depth and rotation of the framebuffer.

The stride is the width of each line in pixels and may not line up with the actual width. For example, the remarkable width is 1404, but the stride is 1408. If you want to draw a straight line, you need to draw a pixel, then advance forward 1408 pixels before drawing the next one. If you advanced 1404 pixels the line will come out distorted.

On e-ink devices, writing to /dev/fb0 isn’t enough to update the display, though. After placing new data in the framebuffer, you need to signal the display to update using an ioctl that specifies the region to update and the waveform to use when updating that region. Each framebuffer driver will have a different call to use. A common driver is mxcfb and you need to include mxcfb.h which contains the relevant ioctls and structs associated with them.

A typical problem people run into when trying to update the screen is that their pixels don’t show up even after specifying the ioctl. This is likely because the wrong waveform was used.

What are waveforms?

Waveforms are how e-ink displays update what is displayed. They are typically proprietary (secret sauce) and contain the series of pulses to send to the screen in order to set a pixel to a specific value. They come in the form of LUTS (lookup tables), that tell you for a given temperature and a before and after value, what pulses you need to send. The fastest waveforms may contain 10 frames, while more detailed waveforms can have as many as 24 or more. There are 3 operations supported in the waveform: darken the pixel, lighten the pixel or do nothing.

The faster waveforms support less colors: DU (direct update) can only set pixels to black and white but updates extremely quickly. GC4 supports four colors and GC16 supports 16 colors. Waveforms also have algorithms for reducing “ghosting” - when a pixel stays dark even though it’s been changed to white.

See this link for more details on the framebuffer

How do I handle input?

If you are using QT (like many e-ink applications do), then you don’t have to worry too much because QT usually handles the input events for you. If you are not using QT, then you will likely use the linux input API. The API is standard across linux and consists of opening /dev/input/eventX (where X is a number) and reading events from it. The main inputs supported on e-ink devices tend to be touch (or multi-touch), buttons and a stylus. The input API generally sends back information describing the input as a series of events followed by a SYN event. Each datum consists of the event type, the event code and the event value.

As an example, the a touch display may send the following when you touch the display:

EV_KEY, ToolFinger, 1
EV_ABS, ABS_X, 300
EV_ABS, ABS_Y, 500
EV_SYN

When you lift your finger, the following may be sent:

EV_KEY, ToolFinger, 0
EV_SYN

Each input device will have different codes: you can find more info about them here. Luckily, you don’t have to handle them yourself, you can use a library like libinput to take care of the heavy lifting

How to compile for ARM processors

Typically, you want to compile your applications on a computer, but your machine is likely using an x86 compiler, like GCC. If you compile a program with it, it will not run on the e-ink device.

To compile for e-ink, you have to cross compiler - compile on one architecture for another one. Some tablets release their toolkits that include a compiler, like the remarkable, but you don’t need to use them: you can install a cross compiler on your own. The typical one to use is arm-linux-gnueabihf-gcc. The chances are that your application will be dynamically linked against glibc and other common libraries - but these libraries will likely not be available on the tablet. You can either install the libraries as .so files on the tablet after cross compiling them or you can compile your binary with the -static flag. You may also need to use the -static-libstd++ and -static-libgcc flags.

It’s a good idea to strip the binary after you’ve built it to reduce its size, otherwise you may end up with a binary that is several megabytes.

What about rotation?

The framebuffer typically supports rotations. You can rotate the framebuffer by 0, 90, 180 or 270 degrees. Once the framebuffer is rotated, you also need to rotate your input events to make sure they are consistent with the UI you are showing.

On the kobo, when the device is rotated, it will emit MSC_RAW input events on the button device. When those events are received, the running application will send an instruction to the framebuffer to rotate itself and then will make a note to translate the input events accordingly.