The EPD screen is called E-paper because once it is finished "printing", it will retain the content even if the power is disconnected. So as you can imagine, when we actually driving the EPD screens, we have three different operations: write (encoded 0b01), wipe (encoded 0b10), leave it as is (encoded 0b00). The first operation will turn a white pixel into black, the second will turn a black pixel into white and the third will do nothing. More about these operations later. But there is a big drawback of EPD displays, that is the EPD pixel need some time to turn itself from black to white or from white to black. And that time is usually more than 300ms so we say EPD have a long response time.
However, the EPD is usually refreshed at a higher frame rate, like 60Hz. So you can see under 60Hz setting, the pixel won't be able to fully turn around in one frame. So there are two ways, one if lower down the refresh rate, aka keeping one frame longer, another is adding more frames. For an example, if your screen have a response time of 1/3s(333ms), you can either lower the frame rate to 3Hz or send 20 same frames under 60Hz. My driver used the later method. The reason is that when using the first method, you can actually see the screen refreshed up to down since the data is being sent in that speed. The second method (more frames) would not have this effect. You see the whole screen get refreshed together. (though actually still up to down but you can't see) So here it the full idea: assume we start from a white screen which have a resolution of 2px * 1px, and we want it to display something like, one light pixel followed by a dark one: [ *]. So for the first pixel, it's already white (we start from white), so send the third operation (leave it as is) to the screen. For the second pixel, it need to be black, so we send the first operation (write) to the screen. And that’s one frame, we are running under 60Hz, so send 20 times. And we should get would we hoped.
But what if you stop driving a pixel before it totally turned around? Like, we send only 10 frames in the last example. The answer is that it would stay gray. And that's the fundamental of grayscale display on EPD panels. By controlling the driving time, we can create 4 shades or even 16 shades of gray. My driver used 4bpp(16shades) mode for better image quality, but if you can understand the principle, you can easily modify it to 4 shades or maybe 32 shades. Okay here is the thing. In 4bpp mode, there are 16 shades of gray. Let's continue with the assumption we made about the response time, 1/3s and begin with a white screen. If we evenly divide 1/3s into 15 slices, which is 1/45s each. So then we turn the frame rate to 45Hz, so one frame is 1/45s. For the pixel that's black, it’s obvious we want to send “write” command to the screen for 15frames, and it would turn black. For the pixel that’s white, just send “leave as is” in all 16 frames, it would leave white. That’s essentially the same as before. But if we want it to be the first shade of gray (which is, 1/15 of the brightness, note that 15/15 is black and 0/15 is white), simply send “write” in the first frame and “leave as is” in all other frames. So, the fourth shade of gray would be 4 frames of “write” plus 11 frames of “leave as is”. So what if we want 32 shades of gray? Adjust the frame rate to 93Hz and we are almost ready to go.
Unfortunately, things are not that easy. First of all, our STM32 software driving method is unable to achieve a refresh rate of 60Hz. Actually it can do only about 12Hz. So 4 frames is enough to drive the pixel from black to white or from white to black. It would be fine if I stick with monochrome mode or 2bpp (4-level) grayscale mode. But what if I want to do 4bpp mode, I have to use some tricks. Normally, the MCU send the data to source driver of the screen, when finished sending, it let the source driver latch the data and actually drive the pixels. In the meanwhile, the MCU started sending the data of next line. When finished, the MCU latch the data and tell the gate driver to switch to the next line. So the line driving time is roughly the time of the process of data sending. Now the data sends too slow due to the slow CPU/GPIO, so it set a lower limit of driving time for me. But as I have said before, more grayscales need higher frame rate, which actually means lower driving time per frame. In the normal way, the driving time is roughly the reciprocal of frame rate. But by using a trick, we can break this relationship, make driving time much lower than the reciprocal of frame rate. It’s actually easy. When MCU finished send one line, it would latch the data, and wait for a short period t1 of time, then turn off the source driver. So when actually sending the data, the source driver is off, the actual driving time is that t1. This method would make the frame rate even lower and make it slower to display a image since the time when MCU is sending the data is “wasted”. But it turned out to be a great workaround for me since I don’t often refresh the screen.
The second problem is that the brightness vs time is not linear. For example, 7/45s of driving time is supposed to give us a brightness of 7/15, but actually maybe 1/2, there is a little offset. Well, offset is generally okay but the real problem is that some times some shades get too close to distinguish. Thus, many details are lost. Believe or not, it’s even easier to resolve this using the workaround I mentioned before than using the normal way. I simply added a look up table of t1, so different frames can even have different driving times! The result turns out to be great.
Last I want to just briefly mention the actual data sent to EPD. The EPD use 2bpp format. Note that 2bpp does not mean 2bpp grayscale. It only means that in transmission, 1 pixel use 2 bits. Remember the three operations I said in the beginning? I said encoded 0bxx, that’s the thing that is actually send to the EPD. For example if I send 0b01001000 (0x48) to the screen, it would “write” the first pixel, “wipe” the third pixel, and do nothing to the second and fourth pixel.
That’s all. I’m not a native speaker, if you had any trouble reading this, please contact me, thanks.