Does the Elecrow ESP32 4.2” E-paper Display support partial updates with LVGL?

I built a smart planner for kids using the Elecrow ESP32 4.2” E-paper Display, LVGL 9. It includes a timetable, Google Calendar and Google Tasks integration, and more!

However, I’m having trouble implementing partial refresh with LVGL

Currently, I’m using the following for full and fast refresh:

EditEPD_Init(); 
EPD_Display(Image_BW); // Full refresh 

EPD_Init_Fast(Fast_Seconds_1_s); 
EPD_Display_Fast(Image_BW); // Fast refresh

I tried using:

EPD_Display_Part(0, 0, w, h, Image_BW);

…but it doesn’t work as expected. Has anyone managed to get partial refresh working with this display and LVGL? Any suggestions or examples would be appreciated!

LVGL setup :

#include "lvgl_setup.h"

// LVGL display
lv_display_t *display = NULL;

// LVGL buffers - increased for better performance
static uint8_t lvgl_buf[EPD_W * 40]; // Buffer for 40 rows

// Function to set a pixel in the image buffer (replacing Paint_SetPixel)
void SetPixel(uint16_t x, uint16_t y, uint8_t color) {
    uint32_t addr;
    uint8_t rdata;
    
    // Calculate address in buffer
    addr = x / 8 + y * ((EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1));
    rdata = Image_BW[addr];
    
    if (color == 0) { // BLACK
        Image_BW[addr] = rdata & ~(0x80 >> (x % 8)); // Set bit to 0
    } else {
        Image_BW[addr] = rdata | (0x80 >> (x % 8));  // Set bit to 1
    }
}

// Function to clear the buffer
void ClearBuffer(uint8_t color) {
    uint16_t width_byte = (EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1);
    uint16_t height = EPD_H;
    
    for (uint16_t y = 0; y < height; y++) {
        for (uint16_t x = 0; x < width_byte; x++) {
            uint32_t addr = x + y * width_byte;
            Image_BW[addr] = color;
        }
    }
}

// Flush callback for LVGL
static void epd_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
    // Calculate dimensions
    uint32_t width = area->x2 - area->x1 + 1;
    uint32_t height = area->y2 - area->y1 + 1;
    
    // Convert LVGL buffer to EPD format
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            uint8_t pixel = px_map[y * width + x];
            // Map: 0-127 -> BLACK, 128-255 -> WHITE
            SetPixel(area->x1 + x, area->y1 + y, pixel < 128 ? 0 : 1);
        }
    }
    
    // Tell LVGL we're done with this section
    lv_display_flush_ready(disp);
}

// Initialize LVGL
void lvgl_init() {
    // Initialize LVGL
    lv_init();
    
    // Create LVGL display
    display = lv_display_create(EPD_W, EPD_H);
    lv_display_set_flush_cb(display, epd_flush_cb);
    
    // Set up buffer with enough size to reduce number of flush operations
    lv_display_set_buffers(display, lvgl_buf, NULL, sizeof(lvgl_buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
    
    // Set color format to 8-bit grayscale for E-Paper
    lv_display_set_color_format(display, LV_COLOR_FORMAT_L8);
    
    // Set white background
    lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0xFFFFFF), LV_PART_MAIN);
}

EPD by Elecrow :

#include "EPD.h"

void EPD_ReadBusy(void)
{
  while (1)
  {
    if (EPD_ReadBUSY == 0)
    {
      break;
    }
  }
}

void EPD_RESET(void)
{
  EPD_RES_Set();
  delay(100);
  EPD_RES_Clr();
  delay(10);
  EPD_RES_Set();
  delay(10);
}

void EPD_Sleep(void)
{
  EPD_WR_REG(0x10);
  EPD_WR_DATA8(0x01);
  delay(50);
}


void EPD_Update(void)
{
  EPD_WR_REG(0x22);
  EPD_WR_DATA8(0xF7);
  EPD_WR_REG(0x20);
  EPD_ReadBusy();
}
void EPD_Update_Fast(void)
{
  EPD_WR_REG(0x22);
  EPD_WR_DATA8(0xC7);
  EPD_WR_REG(0x20);
  EPD_ReadBusy();
}

void EPD_Update_Part(void)
{
  EPD_WR_REG(0x22);
  EPD_WR_DATA8(0xFF);
  EPD_WR_REG(0x20);
  EPD_ReadBusy();
}


void EPD_Address_Set(uint16_t xs, uint16_t ys, uint16_t xe, uint16_t ye)
{
  EPD_WR_REG(0x44); // SET_RAM_X_ADDRESS_START_END_POSITION
  EPD_WR_DATA8((xs >> 3) & 0xFF);
  EPD_WR_DATA8((xe >> 3) & 0xFF);

  EPD_WR_REG(0x45); // SET_RAM_Y_ADDRESS_START_END_POSITION
  EPD_WR_DATA8(ys & 0xFF);
  EPD_WR_DATA8((ys >> 8) & 0xFF);
  EPD_WR_DATA8(ye & 0xFF);
  EPD_WR_DATA8((ye >> 8) & 0xFF);
}


void EPD_SetCursor(uint16_t xs, uint16_t ys)
{
  EPD_WR_REG(0x4E); // SET_RAM_X_ADDRESS_COUNTER
  EPD_WR_DATA8(xs & 0xFF);

  EPD_WR_REG(0x4F); // SET_RAM_Y_ADDRESS_COUNTER
  EPD_WR_DATA8(ys & 0xFF);
  EPD_WR_DATA8((ys >> 8) & 0xFF);
}


void EPD_Init(void)
{
  EPD_RESET();
  EPD_ReadBusy();
  EPD_WR_REG(0x12);   // soft  reset
  EPD_ReadBusy();
  EPD_WR_REG(0x21); //  Display update control
  EPD_WR_DATA8(0x40);
  EPD_WR_DATA8(0x00);
  EPD_WR_REG(0x3C); //BorderWavefrom
  EPD_WR_DATA8(0x05);
  EPD_WR_REG(0x11); // data  entry  mode
  EPD_WR_DATA8(0x03);   // X-mode
  EPD_Address_Set(0, 0, EPD_W - 1, EPD_H - 1);
  EPD_SetCursor(0, 0);
  EPD_ReadBusy();
}

void EPD_Init_Fast(uint8_t mode)
{
  EPD_RESET();
  EPD_ReadBusy();
  EPD_WR_REG(0x12);   // soft  reset
  EPD_ReadBusy();
  EPD_WR_REG(0x21);
  EPD_WR_DATA8(0x40);
  EPD_WR_DATA8(0x00);
  EPD_WR_REG(0x3C);
  EPD_WR_DATA8(0x05);
  if (mode == Fast_Seconds_1_5s)
  {
    EPD_WR_REG(0x1A); // Write to temperature register
    EPD_WR_DATA8(0x6E);
  }
  else if (mode == Fast_Seconds_1_s)
  {
    EPD_WR_REG(0x1A); // Write to temperature register
    EPD_WR_DATA8(0x5A);
  }
  EPD_WR_REG(0x22); // Load temperature value
  EPD_WR_DATA8(0x91);
  EPD_WR_REG(0x20);
  EPD_ReadBusy();
  EPD_WR_REG(0x11); // data  entry  mode
  EPD_WR_DATA8(0x03);   // X-mode
  EPD_Address_Set(0, 0, EPD_W - 1, EPD_H - 1);
  EPD_SetCursor(0, 0);
  EPD_ReadBusy();
}


void EPD_Clear(void)
{
  uint16_t i, j, Width, Height;
  Width = (EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1);
  Height = EPD_H;
  EPD_Init();
  EPD_WR_REG(0x24);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(0xFF);
    }
  }

  EPD_WR_REG(0x26);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(0xFF);
    }
  }
    EPD_Update();
//  EPD_Update_Fast();
//  EPD_Update_Part();
}


void EPD_Clear_R26A6H(void)
{

  uint16_t i, j, Width, Height;
  Width = (EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1);
  Height = EPD_H;
  
  EPD_Init();
  EPD_WR_REG(0x26);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(0xFF);
    }
  }

  EPD_WR_REG(0xA6);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(0xFF);
    }
  }
}

void EPD_Display(const uint8_t *Image)
{
  uint16_t i, j, Width, Height;
  Width = (EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1);
  Height = EPD_H;
  EPD_WR_REG(0x24);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(Image[i + j * Width]);
    }
  }
  EPD_Update();
}


void EPD_Display_Fast(const uint8_t *Image)
{
  uint16_t i, j, Width, Height;
  Width = (EPD_W % 8 == 0) ? (EPD_W / 8) : (EPD_W / 8 + 1);
  Height = EPD_H;
  EPD_WR_REG(0x24);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(Image[i + j * Width]);
    }
  }
  EPD_Update_Fast();
}


void EPD_Display_Part(uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, const uint8_t *Image)
{
  uint16_t Width, Height, i, j;
  Width = (sizex % 8 == 0) ? (sizex / 8) : (sizex / 8 + 1);
  Height = sizey;
  EPD_WR_REG(0x3C); //BorderWavefrom,
  EPD_WR_DATA8(0x80);
  EPD_WR_REG(0x21);
  EPD_WR_DATA8(0x00);
  EPD_WR_DATA8(0x00);
  EPD_WR_REG(0x3C);
  EPD_WR_DATA8(0x80);
  EPD_Address_Set(x, y, x + sizex - 1, y + sizey - 1);
  EPD_SetCursor(x, y);
  EPD_WR_REG(0x24);
  for (j = 0; j < Height; j++)
  {
    for (i = 0; i < Width; i++)
    {
      EPD_WR_DATA8(Image[i + j * Width]);
    }
  }
  EPD_Update_Part();
}

void EPD_Init_Part(void)
{
  EPD_RESET();
  EPD_ReadBusy();
  EPD_WR_REG(0x12);   // soft reset
  EPD_ReadBusy();
  EPD_WR_REG(0x21);   // Display update control
  EPD_WR_DATA8(0x00);
  EPD_WR_DATA8(0x00);
  EPD_WR_REG(0x3C);   // BorderWaveform
  EPD_WR_DATA8(0x80);
  EPD_WR_REG(0x11);   // data entry mode
  EPD_WR_DATA8(0x03); // X-mode
  EPD_ReadBusy();
}

Complete code :

Elecrow examples :