NodeMCU-32s + RA8875 Rendering Advice


I am attempting to get lvgl working on my NodeMCU-32s (ESP32 wroom) clone, and so far I can initialize the 7" display (using the RA8875 controller and code (from Adafruit) with a label. That part works fine.

The problem occurs when I go to update the label to change the text (e.g., using a task every 500 ms). the original text remains in place on the screen, however any subsequent text updates cause ghosted lines to be drawn around the original text. I even enabled the FPS counter in the bottom corner, and it does the same thing across the bottom.

What MCU/Processor/Board and compiler are you using?

NodeMCU-32s (ESP32 wroom)
CLion + PlatformIO

What LVGL version are you using?


What do you want to achieve?

To be able to update the UI, and it be reflected correctly on screen.

What have you tried so far?

I’ve tried enabling double-buffering to no avail.

Code to reproduce

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_RA8875.h>
#include <lvgl.h>

#define RA8875_INT      13
#define RA8875_CS       5
#define RA8875_RESET    12


Adafruit_RA8875 get_tft() {
    static Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RESET);
    return tft;

static lv_disp_buf_t disp_buf;
static lv_color_t buf0[BUFFER_SIZE], buf1[BUFFER_SIZE];

void display_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p);

void task(lv_task_t * task) {
    static uint8_t toggle = 0;
    auto * label = (lv_obj_t *) task->user_data;

    if ((toggle++ % 2) != 0) {
        lv_label_set_text(label, "Hello Arduino! (V7.9.X)");
    } else {
        lv_label_set_text(label, "Hello World!");

void setup() {

    Adafruit_RA8875 tft = get_tft();

    tft.PWM1config(true, RA8875_PWM_CLK_DIV1024);

    pinMode(RA8875_INT, INPUT);
    digitalWrite(RA8875_INT, HIGH);

    lv_disp_buf_init(&disp_buf, buf0, buf1, NBR_PIXELS_IN_BUFFER);

    lv_disp_drv_t disp_drv;
    disp_drv.flush_cb = display_flush;
    disp_drv.buffer = &disp_buf;

    lv_obj_t * label = lv_label_create(lv_scr_act(), nullptr);
    lv_label_set_text(label, "Hello Arduino! (V7.9.x)");
    lv_obj_align(label, nullptr, LV_ALIGN_CENTER, 0, 0);

    lv_task_create(task, 500, LV_TASK_PRIO_MID, label);

void loop() {

void display_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p) {
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    Adafruit_RA8875 tft = get_tft();

    tft.drawPixels(&color_p->full, (w * h), area->x1, area->y1);


Screenshot and/or video - video showing problem

A little more digging, and a lot of blind trial and error, and I’ve found some code that seems to give me what I’m looking for at the best performance I can muster.

void display_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p) {
    Adafruit_RA8875 tft = get_tft();

    int32_t row_count = (area->x2 - area->x1 + 1);
    uint16_t color_row[LV_HOR_RES]; // Maximum size should be the screen width

    for (int y = area->y1; y <= area->y2; y++) {
        for (int x = 0; x < row_count; x++) {
            color_row[x] = color_p++->full;

        tft.drawPixels(color_row, row_count, area->x1, y);


The screen still does the initial pixel-by-pixel draw on start up, but once that’s done it works well at around 33 fps and less than 20% CPU.

However, I’m open to anybody suggesting a better way, and/or how to initialize the screen all at once.

Thanks! It’s late, and I need to hit the proverbial sack.

May I ask why you don’t just use color_p directly then increment it by row_count, instead of copying it to the color_row array? Perhaps I missed something.

The Adafruit_8875::drawPixels() method takes a pointer to uint16_t. So I can’t just pass in the lv_color_t struct. It does the following inside of the method:

  while (num--) {

Where num is the value of row_count that is passed in. Not sure if any of that matters; but I wanted to try to answer your question (the best I could).

But unfortunately my display (I think) has decided to crap out on me, and it’s going to be a few weeks before I can get another one that works with my controller. :sob:

If LV_COLOR_DEPTH == 16 you can safely cast lv_color_t * to uint16_t *, as they are both the same size under the hood. The colors might be wrong; that can usually be corrected by toggling LV_COLOR_16_SWAP.