Actually drawing the graphics for the display is both the easiest and the hardest part. The basic algorithms for line drawing, fonts etc. have been known for decades, and there are a thousand implementations available online. However, there seems to be a lack of good open source graphics libraries for microcontrollers, and close to zero that could do display list rendering.
The point of e-ink is usually to minimize power consumption, allowing battery lifes of many months. Therefore it makes sense to use a low-power microcontroller also, such as the STM32L1 series. However, low power is usually mutually exclusive with large amounts of memory, as a large SRAM consumes a significant amount of current. Because the memory size is small and the display size is large, there is not enough space for a framebuffer, that is, not enough space to store the contents of the display.
Display list rendering
There are basically two ways to render graphics on a computer screen: immediate mode or display list. Immediate mode is the most common, because it is often easier to understand and implement. In immediate mode drawing, when you call a function like drawLine(), it immediately makes the changes to the display contents. If you want to change the display contents, you clear everything and redraw.
In display list rendering, your application defines a list of objects to be drawn on the screen. This list of objects contains drawing primitives such as "line from (1,1) to (50,50)" or "text at (30,30)". After the list is complete, or after it has been changed, a single call is made to render everything to the display.
There are two major advantages to display list rendering: first of all, there is no flicker. Because the rendering subsystem already has the full list of drawing primitives, it can render everything in one go. In immediate mode, there is usually a brief moment between when the background is cleared and the contents are drawn, which shows up as flicker on the screen.
The second advantage of display lists is that there is no need to store the whole contents of screen anywhere. The list of drawing primitives is usually smaller than what space all the pixels on the screen would take. The renderer can rasterize e.g. one horizontal line at a time, albeit it is somewhat faster to rasterize larger blocks at once.
Because the e-ink display is updated line-by-line and is quite slow by itself, there would be plenty of time to rasterize as we go. In fact, this is how I did my early demos of Mandelbrot fractal and checkerboard pattern. Clearly this would be the optimal way to render contents for e-ink if there is little RAM available.
Available graphics libraries
However, there are not that many graphics libraries that are suitable for microcontrollers. Libraries such as Anti-Grain Geometry or Cairo are very flexible, but compile to more than 100kB of code. There are a few commercial libraries, but for a hobby project I don't wish to pay for software, and would prefer open source anyway.
Unfortunately, all the open source graphics libraries I could find are immediate mode. Most of them aren't even flexible enough to implement display list rendering in an efficient way. I contemplated writing my own, as I have already done some display list rendering in the DSO Quad Logic Analyzer, but decided that it is too much work compared to the benefits.
Instead I settled to do some framebuffer emulation atop the µGFX library. Sadly, it is not open source, but atleast the source code is available under a restricted license and the community is active.
Emulating a frame buffer in limited RAM
E-ink displays have the great aspect that they retain their contents without refreshing, and the display supports partial updates. Therefore it is not necessary to store the full contents of the display, as we can just let the display retain the contents itself.
In theory, we could issue a partial redraw for every single pixel we want to draw. However, each scan through the e-ink display takes about 10-100 milliseconds, so this would be prohibitively slow. Instead, we can divide the display into blocks, and use what RAM we have to store as many of these blocks as possible. When the RAM is full, we will dump the accumulated blocks to the display panel in one go.
When the graphic library issues a request to write a pixel (in immediate mode, of course), we do not immediately write it to the display. Instead, the following happens:
- Check if there is already allocated a buffer for the region that contains the pixel. If yes, go to 4.
- Check if we have any free buffers left. If yes, allocate the buffer and go to 4.
- All our buffers are full, so write them to screen in one go. Then free and clear all the buffers, and allocate the buffer for the new pixel.
- Draw the pixel in the allocated memory buffer.
Because most drawing operations happen in a localized area of the display, we can manage with limited amount of RAM for the framebuffer emulation. In my case, I have allocated 5 kB for the buffers, whereas a full framebuffer for the whole 800x600 screen would take 60 kB.
Another aspect of using a small microcontroller with a large resolution display is that fonts tend to take a lot of space. On a 800x600 screen, it is not unreasonable to use a 48 pixel high font for text. However, such a font with the ISO8859-15 character set (190 glyphs) would easily take upwards of 50 kB of space.
Fortunately, fonts are quite readily compressible. Most letters have long continuous areas, and many different letters such as A, Å and Ä share common parts. Font compression is something that I have been playing around with for a long time, so I happened to have a suitable algorithm already thought out. I decided to finish it up and release it as open source. The library, called mcufont, is not yet very well documented, but it does work and manages to compress the above mentioned 48 pixel high font down to 11 kB.
Now that the software side is in order, I can present a mockup of a GUI for the thermometer project that I'm initially going to use this e-ink display for:
The display driver I have written is now included in µGFX. Because of the restrictive license, I also have a public domain copy of the driver on GitHub. It should be easy enough to adapt for any graphics framework.
– Petteri Aimonen on 22.10.2013