Fixing Waveshare e-paper display partial updates
I’m in the process of making a custom “smart frame”, i.e. a device that looks like a photo frame but contains an e-paper (aka e-ink) display that shows useful information (date and time, weather, calendar…)
For that project I acquired a Waveshare 7.5" black-and-white e-paper display. Maybe I’ll make a post about the frame when it’s finished, but for now I’d like to talk about an issue I found in the official driver of the e-paper screen. And how, with some guesswork, I managed to fix it.
Types of e-paper display update methods
If you’ve owned a Kindle or some other ebook reader, you’ve probably noticed that the display doesn’t refresh the same way every time. Most of the time when you navigate from one page to the other, the new page appears immediately, just like it would on a normal screen. But sometimes, maybe every 5-10 page turns, the display flickers black and white a few times before showing the new page.
These multiple update methods are required because of how an e-paper display works, with tiny pigments that need to move in microcapsules. The fast updates can’t always move them all and occasional coercion via some flickering is required to erase remaining afterimages and improve contrast.
The Waveshare display’s firmware and original driver actually implements 4 update types:
- bilevel (pure black or pure white) full refresh, that takes about 4-5 s and flickers multiple times; it’s the best method for avoiding residual images and obtaining the best contrast
- bilevel fast refresh that takes about 1.5 s, flickers once, showing the negative of the desired image before displaying it
- bilevel partial refresh that takes about 0.5 s and directly displays the new image with no flickering at all
- grayscale refresh that can display 2 gray tones in addition to black and white, takes ~2 s and flickers a bit; we’ll put that method aside for this article and only consider the bilevel methods
Note that partial refresh is called that, “partial”, because it is the only method that allows refreshing only part of the display. But it is also the only refresh method that doesn’t flicker. In other words, it makes sense to use “partial refresh” even when refreshing the whole screen, just to avoid the flickering.
Varying the refresh methods…
As can be experienced with ebook readers, it is recommended to vary the update methods, as using only the non-flickering partial refresh may lead to poor contrast, remaining afterimages, and could even damage the display over time.
In my “smart frame”, I want to show a clock (among other things) and therefore need to refresh the display every minute. Therefore I opted for this schedule of refresh methods:
- every hour, on the hour, use a full refresh
- every 10 minutes, use a fast refresh
- otherwise, every minute, use a partial refresh
That way, 9 out of 10 updates will not flicker but the screen will still be fully refreshed regularly.
… and the issue with partial updates
However, after implementing the schedule, I immediately noticed that something was very wrong with partial updates. For example, one minute after displaying 10:40 correctly with a fast refresh, the partial refresh that should have shown 10:41 instead produced this strange result:

Partial updates may lack some contrast in the areas going from black to white or vice-versa, but they should not be noisy like that.
What was strange was that I tested partial updates before, and Waveshare’s demo code contains them and they do work fine. But in the demo code, the partial updates are done immediately after the previous (full or fast) refresh. Could the issue be time-sensitive?
Indeed, when issuing 10:41 via partial refresh immediately after showing 10:40, the correct result was achieved:

It seems the issue was caused by waiting one minute before the partial update. What about intermediate delays?
So… what’s in electronics and gets noisy after a few seconds? Well, of course, RAM does.
The RAM volatility hypothesis
I didn’t mention it before, but if you have to wait between display updates, you’re supposed to put the display to sleep. This de-energizes the display and its microcontroller, which not only saves power but is also recommended to avoid damage to the display and to extend its life. So, of course, that’s what I was doing since I only needed to update the display once a minute.
At that point, I had a pretty strong hypothesis in mind:
- partial updates need to read some of the MCU’s RAM, very likely a buffer set by the preceding update, representing what is currently on the display, because they need to know which pixels they need to flip or not
- when putting the display to sleep, that RAM stops being actively refreshed
- as more and more time passes, the RAM gets more and more corrupted due to its volatility
- finally, when waking up the screen and issuing the partial refresh, the corrupted RAM leads to visible corruption on the display
But what could I do about it? I could stop using partial updates and just use fast refresh every minute, that would work fine. But I wasn’t too happy about having the display flicker every minute as it’s a bit distracting. Partial updates would be really nice, if only they worked after putting the display to sleep…
A tale of two buffers
So I took a look at Waveshare’s driver code, hoping to find something.
(Well actually, I looked at my own fork of Waveshare’s driver, since I had already improved it in other ways that are irrelevant here; I hadn’t touched the partial update logic and I confirmed that the original code had the same issue. Still, in this section, I’ll show Waveshare’s code.)
And I did find something interesting!
Both full & fast refresh use the same display
method which sends a 0x10
command followed by image data, then a 0x13
command followed by another image data.
def display(self, image):
# ...
self.send_command(0x10)
self.send_data2(image1)
self.send_command(0x13)
self.send_data2(image)
# ...
More precisely, the image sent after the first command is the negative of the image we want to display, the second one is the actual image to display. The image and its negative is what flickers on the screen with these update methods.
Even the grayscale refresh method uses the two 0x10
and 0x13
commands followed by image data, even though in that case it’s the low and high bits of each 2-bit/4-gray pixel.
It looks a lot like these two commands are used to send data to two distinct buffers in the RAM of the display’s MCU, doesn’t it?
What about partial refreshes?
They use a different method, display_Partial
.
We can see our 0x13
command and its image data, as well as a comment that confirms our suspicion:
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
# ...
self.send_command(0x13) #Write Black and White image to RAM
self.send_data2(image1)
# ...
But, no 0x10
.
What if…
What if the partial update still uses what’s in the 0x10
buffer even if it doesn’t set it?
What if it expects it to be what’s been set by the previous display
method, and uses that as the basis for what’s currently on the display and which pixels it needs to flip?
What if it’s that buffer whose RAM gets corrupted?
Could we keep the partial update from using that corrupted data?
We can just keep a copy of the buffer in the driver; and use the 0x10
command to set the buffer with that data after waking up from sleep, when doing the partial refresh.
In essence, we only need to add a few lines:
def display(self, image):
# ...
self.send_command(0x10)
self.send_data2(image1)
# ADDED:
self.buf10_copy = image1 # Save a good copy of the 0x10 buffer for later...
self.send_command(0x13)
self.send_data2(image)
# ...
def display_Partial(self, Image, Xstart, Ystart, Xend, Yend):
# ...
# ADDED:
self.send_command(0x10)
self.send_data2(self.buf10_copy) # ...restore the data of the 0x10 buffer
self.send_command(0x13) #Write Black and White image to RAM
self.send_data2(image1)
# ...
So I just tried that. Guess what? It worked. The “10:41” partial update, even after sleeping a full minute, didn’t show any noise corruption anymore.
The code
If you too want working partial-updates-after-sleep with your Waveshare 7.5" screen, take a look at my driver implementing the fix described above:
https://github.com/hchargois/betterepd7in5/
Actually even if you don’t care about partial updates, you might prefer it over Waveshare’s original driver since it’s faster, less CPU-hungry, simpler and safer to use.
And it’s on PyPI so you simply have to pip install betterepd7in5
(or actually uv add betterepd7in5
because you know, it’s 2025, just use uv
).
If you’re curious and want to see how the fix is actually implemented, look at this commit.
The implementation is actually just a tiny bit more complex than just saving and restoring the buffer data as explained above. Because partial updates can follow one another, and also because partial updates can potentially update only part of the screen, instead of saving and updating the actual buffer data, I found it simpler to keep track of the currently displayed image as an actual PIL image, so I could paste the updates on it in the correct positions.
Also, because restoring the buffer contents takes some (very small but non-nil) amount of time, for best performance it checks whether the display was put to sleep before the partial update. If not, the RAM could not have become corrupted so there’s no need to spend time restoring the buffer.