Using LVGL on my bespoke multiple display ILI9341 PCB

I have designed and built my own multiple display PCB, which you can see in operation here.

The PCB uses an ESP32 WROOM, MCP port expander (simply to control the back lights) as I needed more IOs.

I am using Arduino IDE v 2, although I code using Visual Studio.

All of the displays are initialised at the same time and then they are individually controlled by enabling and disabling the chip selects when writing to the displays as follows:

Global Scope:

// VSPI Class (default).

Adafruit_ILI9341 tft = Adafruit_ILI9341(VSPI_CS0, VSPI_DC, VSPI_RST); // CS0 is a dummy pin

Setup as follows:

// Set all tft chip select outputs low to configure all displays the same using tft.begin.

digitalWrite(VSPI_CS1, LOW);
digitalWrite(VSPI_CS2, LOW);
digitalWrite(VSPI_CS3, LOW);
digitalWrite(VSPI_CS4, LOW);
digitalWrite(VSPI_CS5, LOW);
digitalWrite(VSPI_CS6, LOW);
digitalWrite(VSPI_CS7, LOW);
digitalWrite(VSPI_CS8, LOW);

delay(100);

// Send screen configuration.

tft.begin(40000000); // 40000000 27000000
tft.setRotation(3);
tft.setCursor(0, 0);

Hopefully as you will see from the YouTube video, it all works well.

However, I would like to expand my design and significantly improve the display graphics as I am now designing a Stock Market Ticker, so I turned to LVGL. However I am struggling to understand how I can keep to my initial PCB design. I have plans to expand my PCB design to other display types and display layouts so I would like to solve this before I design the PCBs

I thought I could simply create multiple screens using LVGL as follows:

Global Scope:

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf1[DRAW_BUF_SIZE / 4];
uint32_t draw_buf2[DRAW_BUF_SIZE / 4];

Within Setup as follows:

// Initialise LVGL

lv_init();

lv_display_t* disp1;

lv_display_t* disp2;

disp1 = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
lv_display_set_flush_cb(disp1, my_disp_flush);
lv_display_set_buffers(disp1, draw_buf1, NULL, sizeof(draw_buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);

disp2 = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
lv_display_set_flush_cb(disp2, my_disp_flush);
lv_display_set_buffers(disp2, draw_buf2, NULL, sizeof(draw_buf2), LV_DISPLAY_RENDER_MODE_PARTIAL);

However, after this, I do not understand how I can link each display and its corresponding chip select to the Adafruit tft definition. You cannot do this with the TFT_eSPI library as that only allows one chip select.

I was wondering if I should call the library multiple times as follows:

// VSPI Class (default).

Adafruit_ILI9341 tft1 = Adafruit_ILI9341(VSPI_CS1, VSPI_DC, VSPI_RST); // CS0 is a dummy pin
Adafruit_ILI9341 tft2 = Adafruit_ILI9341(VSPI_CS2, VSPI_DC, VSPI_RST); // CS0 is a dummy pin
Adafruit_ILI9341 tft3 = Adafruit_ILI9341(VSPI_CS3, VSPI_DC, VSPI_RST); // CS0 is a dummy pin

More than happy to send a coffee card to anyone who can help me figure this out

Hi Christopher,

First of all the video with the 8 displays looks great!

I think I don’t understand what the exact goal/problem is.

It seems to me it’d be possible to have multiple LVGL display and enable the CS pin in the flush callback. You can use the same draw buffer in each display if you are using a blocking SPI transfer.

With lv_display_set_user_data(disp, (void *) idx) you can set user data and get in the flush_cb to know which CS PIN to adjust. And yu can get the user data like this:
idx = (uintptr_t) lv_display_get_user_data(disp);

Am I missing something?

My suggestion to you is to not use the Arduino IDE as it doesn’t have a complete ESP32 SDK.

If you compile using the ESP-IDF build system you will have access to the esp_lcd component. You can also add LVGL as well as the touch and display drivers as components using the component manager that is apart of the build system. This will handle your issue and it will actually run better because of not having the additional layer of Arduino wrappers around the ESP32 SDK functions.

The esp_lcd component will manage everything that is needed including the CS line. It will also allow you to set up double buffering in DMA memory which will help speed things up as well.

@kisvegabor
Since LVGL is not thread safe it would be impossible for it to render to more than a single frame buffer at a time without using DMA memory. So sharing a single buffer between all the displays is possible. But if DMA memory and double buffering is wanting to be used is there a way that all of the displays can be tied together so to speak so they would all know which one is transmitting and which one is active? This way if a buffer was transmitting to one display the second buffer could be rendered for a completely different display while the transmitting was taking place. The flag for the flush being ready would need to be tied together and the flip flop from the active to the idle buffer would also need to be connected across all of the displays. If there was a callback that could be registered just prior to changing the active buffer then the user would be able to set the active buffer to be the same for the rest of the displays. Maybe there is a callback or an event that already exists that is close enough to when the active buffer is changed to make this work?

To directly answer your question regarding the CS pin.

This is a long form but it shows the basic idea of how to go about handling it. It will keep the references you need and the information that is needed when flushing you are able to collect when the flush is being done…

typedef struct {
    Adafruit_ILI9341 tft;
    uint32_t cs_pin;
     lv_display_t * display;
} ili9341_display;


ili9341_display displays[8] = {NULL};


void flush_cb(lv_display_t *disp, lv_area_t *area, uint8_t *color)
{
    ili9341_display *display = lv_display_get_user_data(disp);
    digitalWrite(display->cs_pin, LOW);

    // call functions to send the color data using "display->tft"

    digitalWrite(display->cs_pin, HIGH);

    // any additional code needed in the flush function
}

void setup()
{
    ili9341_display *tft0 = malloc(sizeof(ili9341_display));
    ili9341_display *tft1 = malloc(sizeof(ili9341_display));
    ili9341_display *tft2 = malloc(sizeof(ili9341_display));
    ili9341_display *tft3 = malloc(sizeof(ili9341_display));
    ili9341_display *tft4 = malloc(sizeof(ili9341_display));
    ili9341_display *tft5 = malloc(sizeof(ili9341_display));
    ili9341_display *tft6 = malloc(sizeof(ili9341_display));
    ili9341_display *tft7 = malloc(sizeof(ili9341_display));

    tft0->display = lv_display_create();
    tft1->display = lv_display_create();
    tft2->display = lv_display_create();
    tft3->display = lv_display_create();
    tft4->display = lv_display_create();
    tft5->display = lv_display_create();
    tft6->display = lv_display_create();
    tft7->display = lv_display_create();

    tft0->cs_pin = CS0;
    tft1->cs_pin = CS1;
    tft2->cs_pin = CS2;
    tft3->cs_pin = CS3;
    tft4->cs_pin = CS4;
    tft5->cs_pin = CS5;
    tft6->cs_pin = CS6;
    tft7->cs_pin = CS7;

    tft0->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft1->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft2->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft3->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft4->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft5->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft6->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);
    tft7->tft = Adafruit_ILI9341(-1, VSPI_DC, VSPI_RST);

    // other display setup code

    lv_display_set_user_data(tft0->display, (void *)tft0);
    lv_display_set_user_data(tft1->display, (void *)tft1);
    lv_display_set_user_data(tft2->display, (void *)tft2);
    lv_display_set_user_data(tft3->display, (void *)tft3);
    lv_display_set_user_data(tft4->display, (void *)tft4);
    lv_display_set_user_data(tft5->display, (void *)tft5);
    lv_display_set_user_data(tft6->display, (void *)tft6);
    lv_display_set_user_data(tft7->display, (void *)tft7);

    lv_display_set_flush_cb(tft0->display, flush_cb);
    lv_display_set_flush_cb(tft1->display, flush_cb);
    lv_display_set_flush_cb(tft2->display, flush_cb);
    lv_display_set_flush_cb(tft3->display, flush_cb);
    lv_display_set_flush_cb(tft4->display, flush_cb);
    lv_display_set_flush_cb(tft5->display, flush_cb);
    lv_display_set_flush_cb(tft6->display, flush_cb);
    lv_display_set_flush_cb(tft7->display, flush_cb);

    displays[0] = tft0;
    displays[1] = tft1;
    displays[2] = tft2;
    displays[3] = tft3;
    displays[4] = tft4;
    displays[5] = tft5;
    displays[6] = tft6;
    displays[7] = tft7;

}
    

Hi,

Thank you, I am looking to improve the design, making it modular and allowing screens to be interconnected, building them out, plus using the 2.8" and 3.2" versions.

I am more into the electronic design than coding, so I’ll need to work through your post to get to grips with it

KD Schlosser has given a second example which I understand, so perhaps between the two Ill be able to move it on.

Regards,

Christopher

Hi,

Thanks for your two posts, I appreciate the comment on the Arduino IDE which one day I will have to embrace, but for now your second post gives a great start.

I do not need the screens to update quickly or at high frequency, such as a Odometer for example, as the data will come in at various intervals separated by 5 to 10 seconds, which is plenty of time to redraw the screens as needed. I think when I need high frequency refresh, I’ll have no choice but to move away from the Arduino framework.

In the meantime, I will work through your 2nd post which I understand when reading it and test accordingly.

Unlike the Adafruit libraries and also the TFT_eSPI from Bodmer, I am struggling with the concept of LVGL on how it works, although I did manage to get the examples working easily.

Thanks to you and Kisvegabor for your replies. I am likely to follow this up with more questions shortly.

Regards,

Christopher

no worries m8. If there are any other questions you have ask away.

The code I gave for for example purposes and it will need to be adjusted to suit your needs. it was to show the general idea of how to go about doing it.