Basic IoT with ESP32 – Fast refresh of epaper displays

This post discusses something about epaper I have been able to learn.

How ePaper displays work

ePaper (also called eInk) displays use colored particles (also called pigments), some are positively charged and some are negatively charged depending on their colors. For every pixel, ePaper panel can apply either positive or negative voltage to each side of the panel to drive the relevant particles closer or further from the display, making the pixel in the desired color.

In this Section we will discuss a typical way to drive pigments in popular tri-colour ePaper panels. However, every vendor always has some specific ways to handle the the driving in their own panels, thus we will keep the discussion at a high level and abstract.

The next figure shows a Black and White (B&W) panel whose top side is charged negative and bottom side is charged positive. As a consequence, the positively charged black pigments go to the top and the negatively charged white pigments go to the bottom. Hence, the panel looks black. Each pixel has separate and isolated chambers of colored pigments floating in clear liquid, so it can individually flip the voltage of its top and bottom sides to be either black or white.

A Black and White display. Black pigments are attracted to the top, making the pixel black.

The next diagram shows a more sophisticated case, when we need to handle three colors of Red, Black and White (R, B and W), meanwhile pigments can be only either positively or negatively charged. The trick here is to make reg particles of bigger size while they are also positively charged like black particles. Because of a bigger size, red particles move significantly more slowly than black ones. Therefore, when applying voltage at the both sides of a pixel in a normal way, black particles always move faster than the red ones and always arrive the destination before the red ones, thus the pixel would never have the color of red.

Red pigments stay in between the black and white ones, and the pixels look red.

To make a pixel look red, we would need to make the pixel look black first, like in the previous picture. Then we would flip the voltage so that the red and black pigments move to the bottom. However, the moment that the black pigments have passed the red ones, as they move fasters, but they still between the red and white layers of pigments, the pixel would look red. The panel would need to keep the order of those layers consistently to keep the pixel red consistently. Each vendor may have different trick to maintain the order.

For some pixel, the black pigments are driven towards the other side, leaving the the red ones behind. Now the pixels look red.

The above discussion explains why epaper datasheets from vendors mention waveforms. The process of applying different voltage over specific periods of time produces a form of wave, as shown in the next picture. Each batch of epaper display product and each vendor has specific waveform data, which is calibrated when manufacturing. A user would obtain such data from vendors when buying and load that information, called Look Up Table (LUT) waveform (discussed later), into a display when powering it up. For the sake of convenience, some vendors integrate such waveform data into the display memory. It is called One Time Programming waveform.

Sample of waveform of voltage applied at one side of a pixel over time

How to control an ePaper display from code

Every batch of epaper displays should come with a datasheet, which describes how to use the displays in technical details. The technical specification could range from mechanical, electronical to interfacing. The most important part would be the the specification of commands, i.e., how to talk with the display. The datasheet should also mention about the interfaces, normally SPI, for interacting with the display. We would send and retrieve data via those interfaces. Normally there should be a flow chart showing an example of booting and updating the display, as shown next.

A sample flow of controlling a epaper display

For a display to show colors correctly, it needs to load correct waveform data (using some commands via the interface), either in OTP or LUT way (see previous Section). OTP waveform is convenient and produces the best possible color representation, but it is sometimes limited. One may have to modify the given LUT data to make the refresh rate faster. However, OTP waveform is highly recommended for a long lifetime of the product. Next code show a LUT array in C programming (found at https://github.com/krzychb/esp-epaper-29-dke).

const unsigned char lut_full_update[] =
{
    0x90, 0x50, 0xa0, 0x50, 0x50, 0x00, 0x00,
    0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00,
    0x90, 0x50, 0xa0, 0x50, 0x50, 0x00, 0x00,
    0x00, 0x00, 0x10, 0xa0, 0xa0, 0x80, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x17, 0x04, 0x00, 0x00, 0x00,
    0x0b, 0x04, 0x00, 0x00, 0x00,
    0x06, 0x05, 0x00, 0x00, 0x00,
    0x04, 0x05, 0x00, 0x00, 0x00,
    0x01, 0x0e, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00
};

How to tweak the refresh procedure

As discussed in the previous Section, OTP waveform is convenient but it is built-in and hidden from users. LUT data provided by vendors is more flexible, however it may be limited, for example the vendors may limit the refresh rate to optimize their display’s conditions. Slow refresh rate is suitable when a display is not updated so often, for example a supermarket price tag. However when we want to implement a clock, we would need to refresh the display at least once per second. The good news is that it is possible to tweak the LUT to achieve what we want with some trade-off. The bad news is that normally vendors do not publish instructions about how to. Lets have a closer look at the LUT array in the previous picture.

Each line normally represents a specific period of time of the waveform. Inside each line, some values represent the voltage, and some represent the time to apply the voltage. Each vendor uses a specific format for each version of their display. One can try to experiment the display by trying different values to understand the format. Next Section shows the modification we used on our display and the result

Demo

Tweaking code

The display we have is DEPG0290RHS75BF6CP-H0, and unfortunately we have not been able to find its datasheet or sample code. The LUT shown above is the most suitable for the display and found from the Internet. After playing with the data, we have found that zeroizing the second last row provides a much faster refresh rate. Please the array shown next

const unsigned char lut_partial_update[] =
{
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x20, 0xa0, 0x80, 0x00, 0x00, 0x00, 0x00,
    0x50, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x03, 0x05, 0x00, 0x00, 0x00,
    0x01, 0x08, 0x00, 0x00, 0x00,
    0x02, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00
};

There are some caveats as follows

  • The Red color is not updated by this modified LUT. Updating Red color requires several rounds of driving particles, so it would slow down the refresh rate. We would use Red only for some static areas on the display.
  • This LUT is unlikely the best one for a fast refresh rate. However it is good enough for our applications so we will not try any further.
  • After some refresh rounds, we apply a full refresh again to reset the display’s condition. Otherwise, the display may be permanently broken if the tweaked LUT is applied for a long time.
  • Using this LUT may shorten the life of the display, and it is not recommended by vendors. One should use it at his/her own risk.

The following code block shows a demo of the modified LUT. We start with a slow full refresh, and every second we make a fast refresh, and then after 60 times of fast refreshes we make a full refresh again. There is one configuration for the full refresh mode and one for the fast refresh mode.

void e_paper_task(void *pvParameter)
{
    epaper_handle_t device = NULL;

    //For fast bw mode
    epaper_conf_t epaper_conf_fastbw = {
        .busy_pin = BUSY_PIN,
        .cs_pin = CS_PIN,
        .dc_pin = DC_PIN,
        .miso_pin = MISO_PIN,
        .mosi_pin = MOSI_PIN,
        .reset_pin = RST_PIN,
        .sck_pin = SCK_PIN,

        .rst_active_level = 0,
        .busy_active_level = 1,

        .dc_lev_data = 1,
        .dc_lev_cmd = 0,

        .clk_freq_hz = 20 * 1000 * 1000,
        .spi_host = HSPI_HOST,

        .width = EPD_WIDTH,
        .height = EPD_HEIGHT,
        .color_inv = 1,

        .fast_bw_mode = true,
    };

    //For slow full color mode
    epaper_conf_t epaper_conf_slowbwr = {
        .busy_pin = BUSY_PIN,
        .cs_pin = CS_PIN,
        .dc_pin = DC_PIN,
        .miso_pin = MISO_PIN,
        .mosi_pin = MOSI_PIN,
        .reset_pin = RST_PIN,
        .sck_pin = SCK_PIN,

        .rst_active_level = 0,
        .busy_active_level = 1,

        .dc_lev_data = 1,
        .dc_lev_cmd = 0,

        .clk_freq_hz = 20 * 1000 * 1000,
        .spi_host = HSPI_HOST,

        .width = EPD_WIDTH,
        .height = EPD_HEIGHT,
        .color_inv = 1,

        .fast_bw_mode = false,
    };

    
 
    //Fast mode by default
    device = NULL;
 
    int fast_refresh_count = 0;
    char count_str[12];

    int TIME_TO_FULL_REFRESH = 60;
    fast_refresh_count = TIME_TO_FULL_REFRESH; // This is to force a proper clean in the beginning.
    while(1){
        
		if (fast_refresh_count  == TIME_TO_FULL_REFRESH) {
            if (device != NULL) {
                iot_epaper_delete(device, true); 
            }          
            device = iot_epaper_create(NULL, &epaper_conf_slowbwr);
            iot_epaper_set_rotate(device, E_PAPER_ROTATE_90);
           

            iot_epaper_clean_paint(device, WHITE);	//clean the whole screen with WHITE			
		
            iot_epaper_draw_string(device, 75, 10, "EPAPER DEMO", &epaper_font_20, RED);
            iot_epaper_draw_string(device, 40, 35, "DEPG0290RHS75BF6CP-H0", &epaper_font_16, BLACK);
            iot_epaper_draw_line(device, 10, 55 , 150, 70, BLACK); //For horizontal or vertical lines, dont use this function.		
            iot_epaper_draw_filled_rectangle(device, 160, 55, 280, 70, RED);		
            iot_epaper_draw_filled_circle( device, 80, 100, 50, BLACK); //This circle is intentionally written to expand beyond the boundary and overlap the above line, just for testing.		
            iot_epaper_draw_circle( device, 200, 100, 20, RED);	

            iot_epaper_display_frame(device);

           
            fast_refresh_count = 0;
            iot_epaper_delete(device, true);
            device = NULL;

            vTaskDelay(5000 / portTICK_PERIOD_MS); //Delay for 5 seconds to show the display with RED prints.
           
        } else {
            if  (device == NULL) {
                device = iot_epaper_create(NULL, &epaper_conf_fastbw);
                iot_epaper_set_rotate(device, E_PAPER_ROTATE_90);
            }

            memset(count_str, 0x00, sizeof(count_str));          
            sprintf(count_str, "%d", fast_refresh_count);

            iot_epaper_clean_paint(device, WHITE);	//clean the whole screen buffer with WHITE	
            iot_epaper_draw_string(device, 25, 35, "Fast refresh count=", &epaper_font_16, BLACK);
            iot_epaper_draw_string(device, 255, 35, count_str, &epaper_font_16, BLACK);
            
            //This circle is intentionally written to expand beyond the boundary and overlap the above line, just for testing.		
            iot_epaper_draw_filled_circle( device, 150, 90, (3 - fast_refresh_count % 3)*10 , BLACK); 
           

            iot_epaper_display_frame(device);

            fast_refresh_count++;

        }
		
		
		vTaskDelay(1000 / portTICK_PERIOD_MS);	
        
    }

    iot_epaper_delete(device, true); 
}

void app_main()
{
    ESP_LOGI(TAG, "Starting example");
    xTaskCreate(&e_paper_task, "epaper_task", 4 * 1024, NULL, 5, NULL);
}

Result

The next video shows the result of the above code. You may have observed that

  • The initial refresh takes much longer time (15 seconds) than the fast refresh of a counter
  • Only the initial refresh works with the Red color, and the Red pixels do not change their color, even when a black circle overlaps the areas.
  • The Red areas start in Black first, and then become Red. That behaviour matches our theory explained in previous Sections.
  • After 60 times of fast refreshes, the display does a slow full refresh again, taking about 15 seconds with a lot of flickering

Full source code

The example code can be found at https://github.com/lngo/esp32-iot/tree/main/apps/base-fast-refresh