LVGL running on a M5stack device on 60+ FPS

I ran LVGL on my Tab5 at a max of 62 FPS: but here’s a difference on my old lvgl V9.3 code from August 2025 (runs on arduino):

#include <Arduino.h>
#include <M5GFX.h>
#include <lvgl.h>
M5GFX display;

void lv_disp_flush(lv_display_t * disply, const lv_area_t * area, uint8_t * px_map){uint16_t * buf16 = (uint16_t *)px_map;uint32_t w = (area->x2 - area->x1 + 1);uint32_t h = (area->y2 - area->y1 + 1);display.pushImageDMA(area->x1,area->y1, w,h,buf16);lv_disp_flush_ready(disply);}
static void lv_indev_read(lv_indev_t * indev, lv_indev_data_t * data){lgfx::touch_point_t tp[3];uint8_t touchpad = display.getTouchRaw(tp,3);if (touchpad > 0){data->point.x = tp[0].x;data->point.y = tp[0].y;data->state = LV_INDEV_STATE_PRESSED;} else {data->state = LV_INDEV_STATE_RELEASED;}}
uint32_t my_get_millis(void){return (esp_timer_get_time() / 1000LL);}
void setup()
{
    /*Initialize Tab5 MIPI-DSI display*/
    display.init();
    display.setRotation(3);
    Serial.begin(115200);//for debug

    /*Initialize LVGL*/
    lv_init();
    lv_tick_set_cb(my_get_millis);lv_display_t * disply = lv_display_create(display.height(),display.width());lv_display_set_color_format(disply, LV_COLOR_FORMAT_RGB565_SWAPPED);static uint8_t buf1[1280 * 720 / 10 * 2];
    lv_display_set_buffers(disply, buf1, NULL, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);lv_display_set_flush_cb(disply, lv_disp_flush);lv_indev_t * indev = lv_indev_create();lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);lv_indev_set_read_cb(indev, lv_indev_read);lv_display_set_rotation(disply,LV_DISPLAY_ROTATION_90);
    /*Start UI*/
    lv_obj_set_style_bg_color(lv_screen_active(),lv_color_black(),0);
    static lv_point_precise_t line_points[] = { {200, 170}, {200, 30} };
    static lv_point_precise_t line_points2[] = { {200, 170}, {200, 30} };
    static lv_style_t style_line;
    lv_style_init(&style_line);
    lv_style_set_line_width(&style_line, 8);
    lv_style_set_line_color(&style_line, lv_palette_main(LV_PALETTE_BLUE));
    lv_style_set_line_rounded(&style_line, true);
    lv_obj_t * line1;
    line1 = lv_line_create(lv_screen_active());
    lv_line_set_points(line1,line_points, 2);     //Set the points
    lv_obj_add_style(line1, &style_line, 0);
    lv_obj_align(line1, LV_ALIGN_CENTER,  0, -50);
    lv_obj_t * line2;
    line2 = lv_line_create(lv_screen_active());
    lv_line_set_points(line2,line_points2, 2);     //Set the points
    lv_obj_add_style(line2, &style_line, 0);
    lv_obj_align(line2, LV_ALIGN_CENTER,  -190, -50);
    lv_obj_t * arc = lv_arc_create(lv_screen_active());
    lv_obj_set_size(arc, 200, 200);
    lv_arc_set_rotation(arc, 135);
    lv_arc_set_bg_angles(arc, 225, 45);
    lv_arc_set_value(arc, 100);
    lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
    lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_align(arc, LV_ALIGN_BOTTOM_MID, 0, -50);
    display.setBrightness(255);
}



void loop()
{
  lv_timer_handler();
  delay(1);
}

and the new one: (I had to mod the old one’s code in this text currently to show the new example):

#include <Arduino.h>
#include <M5GFX.h>
#include <lvgl.h>
M5GFX display;

void lv_disp_flush(lv_display_t * disply, const lv_area_t * area, uint8_t * px_map){uint16_t * buf16 = (uint16_t *)px_map;uint32_t w = (area->x2 - area->x1 + 1);uint32_t h = (area->y2 - area->y1 + 1);display.pushImageDMA(area->x1,area->y1, w,h,buf16);lv_disp_flush_ready(disply);}
static void lv_indev_read(lv_indev_t * indev, lv_indev_data_t * data){lgfx::touch_point_t tp[3];uint8_t touchpad = display.getTouchRaw(tp,3);if (touchpad > 0){data->point.x = tp[0].x;data->point.y = tp[0].y;data->state = LV_INDEV_STATE_PRESSED;} else {data->state = LV_INDEV_STATE_RELEASED;}}
uint32_t my_get_millis(void){return (esp_timer_get_time() / 1000LL);}
void setup()
{
    /*Initialize Tab5 MIPI-DSI display*/
    display.init();
    display.setRotation(3);
    Serial.begin(115200);//for debug

    /*Initialize LVGL*/
    lv_init();
    lv_tick_set_cb(my_get_millis);lv_display_t * disply = lv_display_create(display.height(),display.width());lv_display_set_color_format(disply, LV_COLOR_FORMAT_RGB565_SWAPPED);static uint8_t* buf1 = (uint8_t*)ps_malloc(M5.Display.width()*M5.Display.height()*(LV_COLOR_DEPTH/16));static uint8_t* buf2 = (uint8_t*)ps_malloc(M5.Display.width()*M5.Display.height()*(LV_COLOR_DEPTH/16));
    lv_display_set_buffers(disply, buf1, buf2, (M5.Display.width()*M5.Display.height()*(LV_COLOR_DEPTH/16)), LV_DISPLAY_RENDER_MODE_PARTIAL);lv_display_set_flush_cb(disply, lv_disp_flush);lv_indev_t * indev = lv_indev_create();lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);lv_indev_set_read_cb(indev, lv_indev_read);lv_display_set_rotation(disply,LV_DISPLAY_ROTATION_90);
    /*Start UI*/
    lv_obj_set_style_bg_color(lv_screen_active(),lv_color_black(),0);
    static lv_point_precise_t line_points[] = { {200, 170}, {200, 30} };
    static lv_point_precise_t line_points2[] = { {200, 170}, {200, 30} };
    static lv_style_t style_line;
    lv_style_init(&style_line);
    lv_style_set_line_width(&style_line, 8);
    lv_style_set_line_color(&style_line, lv_palette_main(LV_PALETTE_BLUE));
    lv_style_set_line_rounded(&style_line, true);
    lv_obj_t * line1;
    line1 = lv_line_create(lv_screen_active());
    lv_line_set_points(line1,line_points, 2);     //Set the points
    lv_obj_add_style(line1, &style_line, 0);
    lv_obj_align(line1, LV_ALIGN_CENTER,  0, -50);
    lv_obj_t * line2;
    line2 = lv_line_create(lv_screen_active());
    lv_line_set_points(line2,line_points2, 2);     //Set the points
    lv_obj_add_style(line2, &style_line, 0);
    lv_obj_align(line2, LV_ALIGN_CENTER,  -190, -50);
    lv_obj_t * arc = lv_arc_create(lv_screen_active());
    lv_obj_set_size(arc, 200, 200);
    lv_arc_set_rotation(arc, 135);
    lv_arc_set_bg_angles(arc, 225, 45);
    lv_arc_set_value(arc, 100);
    lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
    lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_align(arc, LV_ALIGN_BOTTOM_MID, 0, -50);
    display.setBrightness(255);
}



void loop()
{
  lv_timer_handler();
  delay(1);
}

I will be using the 60 FPS version in a project of mine.

1 Like

Hey,

Reaching 60 FPS is pretty impressive! Can you port a video?

1 Like

What I see in my Tab5 application is pretty slow to be honest. As soon as there is just some “GUI stuff” going on, the FPS drops down quite a bit (around 10 FPS at most).
Changing tabs in tabview etc. is also a bit sluggish.

What you said is true, it only stays 60FPS while idle, but one of the apps in one of my projects somehow kept the 60 FPS rate, due to the small player size. (the player’s area is 2500 pixels, width is 50 pixels, length is also 50 pixels)

WOOO HOOO BLAZING SADDLES…

LOL… That’s on a PC so it doesn’t really count.

Here’s an image instead:


The stats in the image might not be clear but it says 60 - 62 FPS, 0% CPU, 1ms.

that’s with it sitting there idle and not with it rendering anything to the display. When rendering your FPS is probably in the 15 area.

I understand, In some lists, it’s around 15, but in my changelog list on my project (lots of lv_labels), it was worse than 15, it was 4 fps… Maybe there might be a way to prevent that…

what are you using for an MCU that runs the display? and what kind of connection is there to the display?? Do you know what the display driver IC model number is?

I am using an esp32-p4 connected to ILI9881C within MIPI-DSI, basically the product in that link is what I used for this 60 FPS code on the first post: M5Stack Tab5 IoT Development Kit (ESP32-P4) | m5stack-store

I can get you way better frame rates then what you are seeing. I am talking you would not be seeing a drop down to 15 frame per second that’s for sure. It would always stay above 45 frames if not more…

1 Like

Cool! I would need help on widgets that generally need scrolling, same with the screens every time it loads, so how can you do that? I just did a benchmark test and got a very poor 7 FPS average.

It’s complicated to explain. But to give you the simple version. DSI displays do not have any VRAM/GRAM onboard so the MCU is what is responsible for updating the display. Displays that use display IC.s that have onboard VRAM like I8080 and SPI displays the display IC is what handles sending the data to the display. With that kind of a design only the parts that get updated need to be sent to the display IC. The IC writes the data to it’s internal memory.

The lack of VRAM mean you cannot send only the parts that updated. you need to send what s called a full frame to the display. That is the entire frame buffer data. That means that LVGL is not only responsible for rendering but it is also responsible for keeping the frame buffers in sync with each other by not only rendering the data to one buffer but it has to keep track of what has been updated and when the buffers get swapped out it then needs to render that data to the buffer that is no longer being sent. Once that is done then LVGL can render any new updates. This process is time consuming to do and having a single core do all of the work really slows things down.

I have written a driver for RGB displays for the ESP32 and those kinds of display are like the DSI displays which means they do not have internal VRAM either. The driver I wrote allows LVGL to only render the data that has updated and it doesn’t need to keep track of those updates in order to write it to another buffer. The driver I wrote handles all of this internally. It is also able to handle keeping the buffers in sync but it also add the ability to rotate what is being display as well. It is able to do this without blocking LVGL at all. so LVGL is able to render at the same time as the syncing is taking place and both of those are able to take place while the transmitting is taking place.

What I did was I moved the buffer syncing and the transmitting code so it runs on the second core. and by using DMA memory for we are able to transmit without it being a blocking call so the CPU is free to do something else other than sending the frame data to the display.

That’s the quick and dirty version of it. How it actually runs is a lot more complicated than that but that should give you a general idea.

I actually use PSRAM for LVGL, with my custom code.

I already tried to run LVGL in the M5Stack Tab5

The display is originally in portrait mode. When using it in landscape mode you have to use software rotation. Software rotation becomes slow in such a big resolution. That is why I saw FPS dropping from 60 to around 10 to 15 when scrolling the screen.

It didn’t matter if I enabled PPA or PSRAM. In landscape it will drop the FPS.

The Tab5 will probably be a good solution for more static screens without so much scrolling or things moving around.

I may need to do more research on the API to see what really makes it faster in landscape.