ePaper Picture Frame

This ePaper picture frame was a gift to my girlfriend for Christmas. We like to get each other homemade objects that are full of memories. So I thought, "Hey! I have a great idea! I'll make something that can hold endless amounts of memories!" Okay, corny jokes aside, this picture frame is what I decided to make for her.

While looking into ePaper screens, I came across a 5.65" 7-color eInk screen. Specifically this one from Waveshare. Seeing this screen is actually what gave me the idea of an ePaper frame a while ago I just never had an opportunity to make one.

I wanted the frame to possess a few qualities:

  1. Behave like a standard picture frame. This means thin, elegant, stand it on a surface or hang it on a wall.
  2. Replicate images at a high quality
  3. Don't disturb my girlfriend (As little user interaction as possible)

I knew that satisfying the first point would require the electronics to be battery operated. However, this meant that the battery would have to be a lithium-ion battery or similar because I need a thin form factor to avoid changing the dimensions of the frame. I wasn't too worried about the thickness of the electronics because the ePaper screen was only a few mm thick, and the other PCBs should only be a couple mm at most. (This would come back to bite me haha)

From previous work with ePaper screens, I knew a good way to replicate images on this type of medium would be using Floyd-Steinberg dithering. That wouldn't really be too much of an issue as many modern image editors have this feature built in. (Guess what, this would also come back to bite me a bit)

As for not disturbing my girlfriend, I knew that meant that it needed to have a long battery life so she didn't have to plug it in often. My goal was around a year of battery if possible.

View my code on GitHub

February 7th, 2022

Rust Image Converter

To help teach myself more Rust and make the platform for scalable in the future, I wanted to write my own image Ditherer and conversion program. Doing this would reduce the amount of time converting images since it could all be done in a single step in bulk.

The Rust converter code can be found here: ePaper-Frame/rust-converter.

I set the code up such that images would be dithered based on the palette available for the screen. This makes it possible to convert an image to multiple different screens without having to redefine palettes.

As of now, the converter only dithers images and has the ability to convert them to the frame's custom format, however the dithering is the main feature.

This converter still needs a lot of work, but it's going in the right direction. Mainly:

December 23rd, 2021

It's complete!!

After a few headaches, the final project is finally complete and ready to show off:

The front of the frame: Picture of the front

The back of the frame with the backing removed: Picture of the back No, I am not proud of the internal soldering. I assembled this at home over Christmas break, so I didn't have any of my high quality tools. Instead of soldering with my trusty 888d, I did it with a standard outlet soldering iron and a spool of solder made for 16 gauge wire, not 22-24 gauge.

The strange blue cables with the spade connector are used for disconnecting the low power timer so the Atmel can be reprogrammed if need be. Speaking of the low power timer, it's at that slight angle to get it out from underneath the hinge on the back of the frame. The back of the frame has a small hole drilled over the button of the timer to allow my girlfriend to change the picture at her will instead of waiting for the next random interval. The tiny blue coated cable connected to the button of the timer sends a voltage to a GPIO pin on the 32u4 to let the firmware know it was woken up by a button press instead of a standard timer wakeup.

Overall thoughts

I really, really loved this project. Enough so that I'm going to make myself one. I'm really proud of the outcome because from the outside it looks just like a standard picture frame, and the pictures look much better than I thought they would.


When I make these in the future I have a few improvements I'm going to make:

December 22nd, 2021

Frame Parts

This is just a quick update.

I went to Michaels today and picked up two 5x7 frames. Since the ePaper screen is a non-standard size, I also had to have a custom matte made. BIG shoutout to the guy in the framing department at the Michaels in Conway, NH. He was an absolute pleasure to work with and was excited to work on something "so different". He even modified one of my frames to fit the electronics and a custom matte insert free-of-charge. I was out of there with two frames and a custom matte for $15.

10/10 would have him matte my frames again.


While installing the electronics into the frame I noticed something nasty. The ePaper's PCB was actually much thicker than I thought it was. Thick enough that I have to modify the backing of the frame to fit a few of the electronics. Namely, the battery connector and PCB standoffs. I could remove the PCB standoff, but they're actually about as tall as some of the components on the PCB, so they were needed to protect them anyways.

For the final product, I also decided to leave the photoresistor out. I couldn't find a great place to mount it behind the frame in a sturdy way. Also, to be quite honest, I didn't have enough time to test the required light levels for a shutoff event.

Random Changes

One thought that may have crossed your mind: how did you get random image changing to work?

The low power timer triggers once every two hours and I want the image to change approximately once a day; therefore, the screen has a one in twelve chance to change the picture.

The random seed function I implemented for this was nuts, and honestly, not particularly good either. I could write an entire blog entry about what that does to "multiply" entropy.

December 20th, 2021

Image Updates

Come to find out, using GxEPD2 wasn't making my life easier, it was making it harder.

I was better off directly writing data bytes to the screen. So I did. Waveshare's documentation listed the screen's 4-bit color codes. The following table is the amended color code table:

Color Display Color Display Code True Color
Red 0xF800 0x0 0xA6534F
Orange 0xFD20 0x1 0xC16653
Yellow 0xFFE0 0x2 0xDDCD5E
Green 0x07E0 0x3 0x4C6E55
Blue 0x001F 0x4 0x39476C
White 0xFFFF 0x5 0xF0F0F0
Black 0x0000 0x6 0x060606
Clean N/A 0x7 0xD2A691

I also found out that the display supported an eighth color, "clean", which had a tan appearance on the screen. This means I could expand my color palette to eight colors, which would hopefully increase the color accuracy of the photos.

As for the new display update code, I found some very basic SPI code for the display on the Waveshare website. I took a look at this and created a wrapper class to send commands to the screen. Believe it or not, this was actually much simpler than fiddling around with GxEPD2.

Image Format

Now that the color data was being sent directly to the screen instead of through some external library, I had to change up my image format generation a little bit. Instead of using a bitmap to store the file, I decided to create a "raw" image format that stored the direct bytestream of the screen's commands.

Writing colors onto the screen was actually a fairly simple process. The color of each pixel was stored using a 4-bit value (well technically 3, but they had a bit of padding). This actually meant that the color value of two pixels was packed into a single byte.


Color Code Pixel 0 Pixel 1
0x24 0x2 - Yellow 0x4 - Blue
0x57 0x5 - White 0x7 - Clean

This made the custom format rather simple. I created a quick little converter with C (based on STB image) that read the floyd-steinberg images and converted the paletted colors to the display's color codes. This process started at the top left of every image and moved to the bottom right, row by row.

The top of the image file contained the "display code", which is a display model signature used on the firmware to make sure the proper image format is used. In this case, the display signature was: 0x565c.

Note: I would like to rewrite this conversion program in another language for more modularity. Perhaps Rust?

Display Updates

With this new image format, updating the images on the screen was dead easy. This is the exact code used:

/* Draw the image */
img.seekData(); // Makes sure file cursor is past the signature
epd.start();    // Put display into "set screen" mode
for (int i = 0; i < 448; i++)
    for (int j = 0; j < 600 / 2; j++) {
epd.stop();     // Refresh and shutdown

The best part: the image update happens in about 33 seconds! I know this doesn't sound great, but the screen is rated for 30 seconds of refresh time. 33 seconds ain't bad!

Now all that's left to do is seal it up in an enclosure and format some images!

December 17th, 2021

New Hardware Woes

Now that the hardware was laid out and soldered into place (other than the timer), the software design was started.

This is where the problems begin...

I wrote some wrappers for SD card I/O and used it to read files instead of the flash memory I used previously on the ESP32. Slight problem I had forgotten about. PNGs (at least pngle) need at least 30k for a static RAM buffer for decoding the PNG. The 32u4 didn't have this ability. This meant I had to switch my images to bitmaps.

No problem, bitmap loaders are simple to implement. A few hours later, I had a bitmap loader implemented and converted my images to bitmap. When I went to read them from the SD card however, it was dreadfully slow (over 30 minutes to read a file) and upload it to the ePaper screen.

This is because of how GxEPD2 updates the screen. It requires screen pixel memory on the MCU to buffer. There is barely any free ram on the 32u4, so there are countless pages of pixels required to update the screen. The way GxEPD2 works is that the entire file has to be read for every page, and there are 448 pages. This means that the image file has to be read from the SD card 448 times. Not ideal.

Hopefully, by the next update I will have figured this out.

December 14th, 2021

New Hardware

I received the MCU, Timer, and Battery, so I started to place the boards on the back of the ePaper PCB to decide pin assignments.

I settled on the following:

Hardware Routing Don't mind the disconnected battery, the Fritzing model for the 32u4 Adalogger doesn't have connections at the battery hookup.

I also decided to add a photoresistor to the design to only change the picture when the room is dark. This way, the image will only change once at night and can avoid changing during the day when people aren't home.

Now, onto the fun part! Soldering!

December 4th, 2021

ePaper Testing

The ePaper frame arrived yesterday. While whating for the other parts to arrive I decided to start working on some test code to get images displayed on the screen.

I had a couple ESP32's laying around that I hooked up to the screen to display images.

To make my life easier, I settled on using PlatformIO to manage the project.

To control the ePaper screen. I used GxEPD2, a library I'd used in the past to access ePaper displays on ESP32. To read .png images, I used the library pngle, which makes it easier to read PNG images on embedded systems.

Test Image

To get the image formatted, I converted it to indexed using Floyd-Steinburg using a standard 7 color palette with the same colors supported by the display: Red, Orange, Yellow, Green, Blue, White and Black.

It looks okay; however, I think I think I know why the image looks kinda grainy and not very accurate. It has to do with how the palette selection works. The screen only displays images based on defined colors, so the image has to match those colors. The issue is that they look different while displayed on the screen since the screen isn't very color accurate.

To find the proper colors, I screen-picked the colors off the image of the screen on Waveshare's website. The following table shows the updated colors used for the palette.

Color Display Color True Color
Red 0xF800 0xA6534F
Orange 0xFD20 0xC16653
Yellow 0xFFE0 0xDDCD5E
Green 0x07E0 0x4C6E55
Blue 0x001F 0x39476C
White 0xFFFF 0xF0F0F0
Black 0x0000 0x060606

Note: the display colors are stored in 5-6-5 bit format: 0bRRRRRGGGGGGBBBBB. The true colors are how the colors look once they're on the display. However, if 0xA6534F is passed into the display code, it will not display red, so the code has to check and convert each true color to the respective display color.

Once updating the palette to the true colors, the color accuracy of the dithered image was noticably more accurate once displayed on the ePaper screen.

Sorry I realized I never took a picture of the update color algorithm

November 23rd, 2021

Parts Ordered

After doing a bit of research I discovered the parts I wanted the frame to use.