LVGL port to be used with epaper displays

Hi there,
I’ve been since long looking forward to integrate LVGL to render in paralell epaper displays. And finally I got one Lilygo EPD47 epaper that has touch (L58 is the IC for touch, still not ported as an LVGL driver)
So I forked the repository and I’m doing some tests:

github. com/martinberlin/lv_port_esp32-epaper/tree/develop

First blocker is: That LVGL does not provide a way to natively handle the 4 bit framebuffer that this epaper uses (16 levels of gray) But that is of course possible by using the set_px_cb callback (Of course paying a performance cost)

Second blocker I found is: Rendering is working OK on paralell using EPDiy component as a bridge between the two components. But it’s generating some black areas that I still didn’t figure out where they come from, picture here:
pbs.twimg. com/media/E2dZGinWUAMKo6a?format=jpg&name=large

So I have to figure out this part. Here my set pixel callback function:

void il3820_set_px_cb(lv_disp_drv_t * disp_drv, uint8_t* buf,
    lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
    lv_color_t color, lv_opa_t opa)
    //printf("%d ",(int16_t)color.full);
    // Test using RGB232
    uint16_t in_color = (uint16_t)color.full ;
    int16_t epd_color = EPD_WHITE;
    if (in_color>2 && in_color<250) {
        epd_color = in_color/3; //Make it darker
    // Only monochrome:All what is not white, turn black
    if ((int16_t)color.full<254) {
        epd_color = EPD_BLACK;
    } */
    display.drawPixel((int16_t)x, (int16_t)y, epd_color);

Any recommendations on how to avoid this black areas?
Is RGB232 the best mode to start with and convert to 16 levels of gray?

Thanks a lot and I’m looking forward integrating the Touch driver this weekend.

You can avoid the overhead of calling set_px_cb by preforming the LVGL to ePaper color mapping on the fly in your block render function.

You can see an example in the link below. In that case the mapping is from LVGL 8 bit color to the TFT’s 16 bit colors. The mapping uses a precomputed lookup table and is also used to distribute the bits to the pins used and to generate the WR pulse that that TFT needs.

As for the color depth to use for the internal LVGL buffer, I would probably go with 8 bits gray scales if LVGL supports it.


It seems the set_px_cp is used incorrectly. I assume display.drawPixel writes to the display directly, but the set pixel callback should set the pixel in buf assuming the buffer’s width is buf_w.
So something like (not tested):

uint32_t ofs = buf_w * y + x;
uint32_t byte_index = ofs / 2;   // as there are 2 pixels on a byte
if(ofs & 1) {
  //set the color on the lower part
  buf[byte_index] &= 0xF0;
  buf[byte_index] |= (color << 4);
} else {
  //set the color on the upper part
  buf[byte_index] &= 0x0F;
  buf[byte_index] |= color;

Hello @kisvegabor
The drawPixel draws the pixel in the buffer only. With update is finally sending to the display, but many thanks for your reply.
Is now working better I just needed to add more buffer memory. I will look into @zapta answer maybe that’s a way to improve speed considerably.

Updated my fork to add Lilygo EPD47 touch controller that is L58 similar to FocalTech existing ones but with an INT pin ( low on event )

Driver updated with partial update:

1 Like

Hello @kisvegabor
I followed your approach and come a bit further

There is still something off, but is enough to render simple interfaces on epaper, I hope someone else can try it out and help with ideas.
I left here a quick way to try it:

Only requirement is an epaper display like Lilygo EPD047 or any other that is supported by EPDiy esp32 project. Thanks a lot for your help!
I’m really dissapointed by this Forum restrictions where you cannot even put a simple link on the content. Why such a restriction? If it’s to avoid spam I understand but at least links to github and similar should be allowed.

1 Like

Links to common sites are allowed. I try to avoid mentioning the exact list of allowed sites to make it harder for bots and bot creators to abuse.

Twitter was not on the list of common sites until now (I’ve just added it), as it’s very infrequently used by new users.

The restriction also gets lifted once you have commented enough times for the system to know you are not a spammer.

1 Like

I may be missing something but the 0xf0 and 0x0f masks seem inconsistent with the rest of the code here. :wink:

This is my set_px_cb callback now:

void epdiy_set_px_cb(lv_disp_drv_t * disp_drv, uint8_t* buf,
    lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
    lv_color_t color, lv_opa_t opa)
    // Test using RGB232. Darker the color otherwise is too light for this epaper:
    int16_t epd_color = 255;
    if ((int16_t)color.full<250) {
        epd_color = (int16_t)color.full/3;

    int16_t x1 = (int16_t)x;
    int16_t y1 = (int16_t)y;
    // framebuffer is the epaper buffer that is sent via parallel 8 wire using EPDiy
    //Instead of using epd_draw_pixel: Set pixel directly in buffer
    //epd_draw_pixel(x1, y1, epd_color, framebuffer);
    // 4 bit per pixel (16 grayscales)
    uint8_t *buf_ptr = &framebuffer[y1 * buf_w / 2 + x1 / 2];
    if (x % 2) {
        *buf_ptr = (*buf_ptr & 0x0F) | (epd_color & 0xF0);
    } else {
        *buf_ptr = (*buf_ptr & 0xF0) | (epd_color >> 4);

Proof of concept video

@kisvegabor I managed to get it working but I’m still struggling with this (added a video to see if you can get an idea of what the base problem is) Many thanks for taking a look!
EPDiy parallel epaper driver
lvgl_epaper_drivers/epdiy_epaper.cpp at master · martinberlin/lvgl_epaper_drivers · GitHub

Ups, yes, they are swapped :slight_smile:

It’s be great to figure out if it’s an lvgl issue or a driver issue.
Some questions:

  • where epd_hl_update_area is implemented?
  • can you see any issue without lvgl? E.g. draw a 100x100 rectangle to x=10, y=10, clear the screen, draw it to 20,20, clear, 30,30 etc.
  • If the above rectangle drawing works, use lvgl to create a button and please test if the touch area is on the button and or next to it.

Hello @kisvegabor
I try to answer and then I will make some tests at home

where epd_hl_update_area is implemented?

epd_hl_update_area is implemented in EPDiy esp32 parallel driver for epapers. It’s part of the high level API of this driver. What it does is to refresh only that area of the display, all other parts remain untouched.

can you see any issue without lvgl? E.g. draw a 100x100 rectangle to x=10, y=10, clear the screen, draw it to 20,20, clear, 30,30 etc.

When using only EPDiy I can do this without any problem, and refresh only that part of the screen, it seems to works as expected (Both with full update or partial refresh)

If the above rectangle drawing works, use lvgl to create a button and please test if the touch area is on the button and or next to it.

I tried this also. On the first refresh the touch area seems placed correctly over the button. I can see it also in Serial when outputting x and y from the Touch driver I2C. On next refreshes, as you can see in the video, there is some kind of offset happening. In that case the touch is displaced and below the button some millimiters.
More than an issue with the touch for me it seems the partial update that is not happening correctly. The first rendering is always fine and when there is full refresh usually too, the widgets demo in slide mode, works great and I can see all including Charts being rendered properly on this fast parallel epaper. The problem seems to be when I use partial update.

Shall I try to make a rounder callback that forces always a full screen refresh to see if that is the problem?

Yes, it’d be great!

Please try this too: Create a button and in its click event place the button to a different position. It should also trigger partial refresh but a more simple and easier to debug one. Does it work this way?

Please try this too: Create a button and in its click event place the button to a different position

Hello tried this and made a short video:

Updating the width is fine. Now if I try to change position it already messes up the framebuffer. That seems to be the issue. Initial drawing is fine.
Touch is working correctly. But redrawing elements that change position is a problem.

Cool! Now we have simple code that we can debug.

Now please printf the areas sent to flush_cb when the button’s position changes.

After that try to feed the same areas to the driver without LVGL. Does it work fine?

Awesome. I think now I realize what is going on. Printing x,y and area width/height on flush. Let’s say I make a button and make it just 10 px down:

 lv_obj_set_pos(btn, 0, 10);

The button get’s printed correctly at first. Then I click on it, this first test just making it bigger, and it get’s printed again but this time at 0,0 (Is like if px_cb callback would draw it at 0,0 don’t understand why) Anyways the print to Serial reveals this:

epdiy_flush 2 x:0 y:10 w:960 h:42

Right x,y coordinates and right area since the button takes the whole width. But the buffer is filled from 0 to 42, instead of the 10 to 52 expected pixels.
2nd test doing a lv_obj_set_pos(btn, 0, 100); on the button callback:

epdiy_flush 4 x:0 y:98 w:960 h:46

Flush showing again right area to refresh. But it does not refresh anything the EPDiy library since the buffer in that area didn’t change.
Doing refreshes without LVGL and sending just squares with EPDiy and partial refresh correctly refreshes this areas.
I’m offering you to send a full pfledged Lilygo T5 E-Paper with touch for free to any address you desire if you feel like trying this out. I think it will be awesome to have a fast epaper where it’s possible to use LVGL and I will love to contribute to the project somehow.

cb_px callback debugging. Checking the first line outputting every x & y (%2==0) so it prints half, it reveals:

0 y0 c255 2 y0 c255 4 y0 c255 6 y0 c255 8 y0 c255 10 y0 c255 12 y0 c255 14 y0 c255 16 y0 c255 18 y0 c255 20 y0 c255 22 y0 c255 24 y0 c255 26 y0 c255 28 y0 c255 -> First lines up to Y10 they are color 255 (white) that is ok.

But then the top black line of the button is missing. And the whole button seems to be printed in the same place as before, skipping that 10 px offset. Very strange.

After reading your test results I noticed something in your set_px_cb.

Seemingly the problem is with this line

 uint8_t *buf_ptr = &framebuffer[y1 * buf_w / 2 + x1 / 2];

You use framebuffer but you should use the buf parameter. buf is relative to the current are being redrawn. So y = 0 means the first line of buf in flush_cb y will be 10 meaning to copy the buffer to y = 10 of framebuffer.

Very good find. But still there is something I don’t get, because framebuffer is really the global EPDiy buffer that is later send by parallel to the epaper. So if I use buf there, it will not update the framebuffer. Maybe if you can elaborate just some lines more I will get it:

    //How can I update this below to use *buf
    uint8_t *buf_ptr = &framebuffer[(int16_t)y * buf_w / 2 +(int16_t) x / 2];
    if (x % 2) {
        *buf_ptr = (*buf_ptr & 0x0F) | (epd_color & 0xF0);
    } else {
        *buf_ptr = (*buf_ptr & 0xF0) | (epd_color >> 4);

Basically what I don’t get is that LVGL does not know that buf is associated to framebuffer. This is part that I need to solve.

In flush_cb you need to copy color_p (which is buf) to framebuffer to the area pointed by the area parameter.

To make copying easier you can set a rounder_cb to make x coordinates even (byte aligned)

Ok now I understand better how this two things are related. Then buf:

void epdiy_set_px_cb(lv_disp_drv_t * disp_drv, uint8_t* buf [...])

Is the same that comes here as color_map (It’s not called color_p in V7.0 or I miss something?)

void epdiy_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)

But it’s not a uint8_t* pointer right?
I tried but so far I’m getting a hardware reset. Maybe is there any existing driver that is doing this already so I can take a look?
I tried il3820.c that seems to be another epaper but is done differently there.