Using e-paper displays on resource-constrained MCUs
In addition to code and power efficiency challenges, I occasionally find other challenges that attract me. Not long ago I started working with Aaron Christophel on his various electronic shelf label and e-paper projects. One of the ideas I had was to put more autonomy in the MCU boards controlling price labels so that they could do more than just receive images wirelessly from a server. The original project used a price label with an ARM MCU and plenty of RAM. I was able to run my TIFF G4 decoder on it and have the entire resulting image in RAM before sending it to the e-paper display:
The MCUs in Aaron's more recent (and larger) collection of devices contain an 8051-type CPU with limited FLASH space and a much more limited amount of RAM. I thought it would be a good challenge to see how much independent functionality (text/GFX) I could run on those 8-bit CPUs.
Let's start with an example e-paper display - a 2.9" black and white with 128x296 pixels.
If each pixel is 1-bit, then we need (128x296)/8 = 4736 bytes to hold the entire array of pixels in RAM. The traditional way to work with these displays is to prepare the image in RAM and then send the image to the display and tell the display controller to do a refresh. The controller chips of e-paper panels typically have two separate 1-bit planes of memory internally to hold all of the pixels. The three typical ways they're used:
- A current and previous buffer for fast updates of pixels that changed
- A 2-bit per pixel buffer to display 4-levels of gray
- A 2-bit per pixel buffer to hold red/black/white pixels
Similar to LCD and OLED controllers, the e-paper controllers allow you to define memory windows so that data can be written to a sub-region of the display without disturbing the other pixels. This is used by libraries like GxEPD to allow updating the display in smaller sections if you don't have enough RAM to hold the entire frame in memory. Still, this assumes that you'll be preparing the graphics in CPU RAM first, then sending the pixels to the e-paper's memory.
A low-memory approach
I thought it would be worthwhile to try a different approach. My OneBitDisplay already optionally supports working with OLED and LCD displays by writing data directly to the display's internal memory instead of preparing it in CPU RAM first. This allows you to display useful text and GFX (with some limits) without needing any significant local RAM. In the photo below, an ATtiny85 (512 bytes of RAM) is able to draw text on a SSD1306 (128x64 = 1K RAM internal buffer).
Since e-paper controllers are very similar, they should allow a similar approach. First, let's have a look at the memory layout of the very popular SSD1306 128x64 OLED display.
In the image above, the 'Page' refers to a row of 128 bytes where each column of 8 pixels is controlled by the 8 bits in a byte with the least significant bit (LSB) in the first row. The byte value 7 (00000111 binary) written to the first byte position will draw pixels (0,0), (0,1) and (0,2) lit with the other 5 pixels in the PAGE not lit. By writing character images oriented the same as this internal display RAM, rows of text in multiples of 8 pixels tall can be written to the display very easily without needing to first buffer it in local RAM. This is precisely what I do in OneBitDisplay when working without a local buffer.
Now let's take a look at the memory layout of e-paper displays.
The terminology is slightly different - source (column) and gate (row), but there is a similarity to the OLED memory layout if you tilt your head 90 degrees to the right 🙃. The bytes are arranged horizontally with the MSB (most significant bit) on the left. Some displays can flip the bit order in the other direction, but this is the default for most. In the case of our 2.9" example e-paper, the display memory is in portrait orientation and is 128 pixels (16 bytes) wide and 296 rows tall.
A Practical Example
The memory window feature of the display controller allows mimicking the 'PAGE' arrangement of OLEDs by defining an area 1 byte wide and N bytes tall. I used this to allow writing character graphics in vertical columns and was able to re-use the same "LSB on top" orientation by rotating the e-paper native direction 90 degrees counterclockwise. The code which drew the characters below is unchanged from the code which draws on OLED displays.
The photo above is an example of drawing on a 3-color e-paper display using just the display's internal RAM. Notice that the direction of the drawing is 90 degrees rotated from the display's native memory direction. Pixel (0,0) of the display memory is in the lower left corner next to the red "In red too". Beyond drawing rows of characters 8 lines at a time, another possible use case is decoding compressed images one line at a time. This is what I demonstrate in the video below:
Two highly compressed TIFF G4 images are stored in the Arduino Uno's FLASH memory. They're each compressed about 10:1. My latest release of my TIFF_G4 and OneBitDisplay library support doing direct decompression into the e-paper frame buffer. In the video above, pixel (0,0) of the e-paper memory is the upper right corner of the display. The image is decoded from the right edge leftward in vertical lines. Here's the TIFFDraw callback function showing how it's done. This function is called for each line of pixels as the image is decoded.
When the first line of image is received, the setPosition() method is called to set the memory window to the same size of the decoded image. It's not centered on the display in the demo code, but could be. The horizontal positioning can only be on byte boundaries to avoid having to shift and recombine all of the bytes of each line. The pixels generated by the TIFF decoder happen to be oriented the same as the e-paper memory (MSB on the left); this is the most common byte orientation for 1-bpp devices and files. They can be written to the display after being inverted since the e-paper colors are black=0, white=1 and TIFF G4 colors are black=1, white=0. Once the image is finished decoding, a full update command is sent to the e-paper. For the Arduino Uno and other AVR targets, I reduced the TIFF decoder buffer sizes to a total of 1K of RAM. The final output line buffer in the case of this display only needs to be 16 bytes (128 pixels).
My hope is that all of this effort and code enables new use cases for microcontrollers with very little RAM to be able to work independently with e-paper displays instead of just being receivers of graphics generated on some other computer. My Arduino Uno example of CCITT G4 image decoding is intended to break assumptions about what's possible with a tiny bit of RAM on an 8-bit CPU. I would love to hear about new ideas and use cases along these lines.