lvgl-micropython / lvgl_micropython

LVGL module for MicroPython
MIT License
61 stars 19 forks source link

White screen with ili9488 display #88

Closed Voinic closed 1 month ago

Voinic commented 1 month ago

Can't get it to work with ili9488 display with SPI interface on ESP32-S3. I've tried changing CS and DC to other pins, setting reset_state to high, changing color_space to RGB888 but nothing helped.

import micropython
import sys
import lcd_bus
import ili9488
import lvgl as lv
import task_handler

# Display settings
_WIDTH = const(320)
_HEIGHT = const(480)

# Common SPI bus settings
_SPI_HOST = const(2)
_MOSI_PIN = const(11)
_MISO_PIN = const(13)
_SCLK_PIN = const(12)

# Display SPI bus settings
_LCD_SPI_FREQ = const(40_000_000)
_LCD_CS_PIN = const(10)
# display additional pins
_LCD_RST_PIN = const(15)
_LCD_DC_PIN = const(14)
_LCD_BKL_PIN = const(16)

if not lv.is_initialized():
    lv.init()
    print("LVGL init done")

try:
    spi_bus = machine.SPI.Bus(
        host=_SPI_HOST,
        miso=_MISO_PIN,
        mosi=_MOSI_PIN,
        sck=_SCLK_PIN,
    )
except Exception as ex:
    print("SPI bus init error")
    sys.print_exception(ex)
    raise RuntimeError

try:
    display_bus = lcd_bus.SPIBus(
        spi_bus=spi_bus,
        freq=_LCD_SPI_FREQ,
        dc=_LCD_DC_PIN,
        cs=_LCD_CS_PIN
    )
except Exception as ex:
    print("Display bus init error")
    sys.print_exception(ex)
    raise RuntimeError

try:
    fb1 = display_bus.allocate_framebuffer(int(_WIDTH * _HEIGHT * 2 / 10), lcd_bus.MEMORY_SPIRAM)
except MemoryError:
    raise RuntimeError("MemoryError on first buffer")
try:
    fb2 = display_bus.allocate_framebuffer(int(_WIDTH * _HEIGHT * 2 / 10), lcd_bus.MEMORY_SPIRAM)
except MemoryError:
    raise RuntimeError("MemoryError on second buffer")

try:
    display = ili9488.ILI9488(
        data_bus=display_bus,
        display_width=_WIDTH,
        display_height=_HEIGHT,
        frame_buffer1=fb1,
        frame_buffer2=fb2,
        reset_pin=_LCD_RST_PIN,
        reset_state=ili9488.STATE_LOW,
        backlight_pin=_LCD_BKL_PIN,
        backlight_on_state=ili9488.STATE_HIGH,
        color_space=lv.COLOR_FORMAT.RGB565,
        #rgb565_byte_swap=False
    )
except Exception as ex:
    print("Display driver init error")
    sys.print_exception(ex)
    raise RuntimeError

display.init()
display.set_backlight(100)

scr = lv.obj()
btn = lv.button(scr)
btn.align(lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("Hello World!")
lv.screen_load(scr)

th = task_handler.TaskHandler()
kdschlosser commented 1 month ago

you should have your spi host set to 1

This is from the ESP-IDF

typedef enum {
//SPI1 can be used as GPSPI only on ESP32
    SPI1_HOST=0,    ///< SPI1
    SPI2_HOST=1,    ///< SPI2
#if SOC_SPI_PERIPH_NUM > 2
    SPI3_HOST=2,    ///< SPI3
#endif
    SPI_HOST_MAX,   ///< invalid host value
} spi_host_device_t;

and this is also from the ESP-IDF for the ESP32-S3

#define SPI2_FUNC_NUM           4
#define SPI2_IOMUX_PIN_NUM_HD   9
#define SPI2_IOMUX_PIN_NUM_CS   10
#define SPI2_IOMUX_PIN_NUM_MOSI 11
#define SPI2_IOMUX_PIN_NUM_CLK  12
#define SPI2_IOMUX_PIN_NUM_MISO 13
#define SPI2_IOMUX_PIN_NUM_WP   14

The pins you are using align with "SPI2" and "SPI2" aligns with the numerical value of 1 as seen in the enum in the first code block.

Don't ask me why they did that because it is confusing that they did. It would have been better if they called the first one SPI0 or if they started the numbering off at 1 instead of 0. either way it would have made more sense.

Also... unless there is a specific reason why you are putting the frame buffers in SPIRAM I would suggest not doing that. It is going to be a lot slower to transfer the data. Also you are not using DMA memory so using 2 frame buffers is not going to do anything except use up memory for no real reason. I would suggest letting the display driver hammer out the size and where to place the frame buffers. It will attempt to make 2 buffers and to place both buffers in SRAM and if that fails then it will attempt to make 2 buffers in SPIRAM. both of those attempts are done using DMA memory. If the second fails then it will attempt to make a single buffer in SRAM non DMA and if that fails then it will try the single buffer in SPIRAM.

I wrote the code for optimum performance so the buffer sizes that are created are exactly the same as what you are setting them to.

Also there is no need to call lv.init(). This is already done in the driver code.

If you are using an SPI interface you will need to set the RGB565 byte swap to True

kdschlosser commented 1 month ago

if you still have a white screen after making those changes then it is going to be a configuration issue with the display. I can make adjustments to the driver if that happens.

Voinic commented 1 month ago

Thanks for detailed explanation on framebuffers. I made the suggested changes to the code, but still no result... Another question, is there a way to test display drivers separately from LVGL?

kdschlosser commented 1 month ago

You changed the SPI host to a 1?

I want to make sure that is done. I can work on messing about with the init commands if that is done.

Voinic commented 1 month ago

Yes, my actual code is


import machine
import sys
import lcd_bus
import ili9488
import lvgl as lv

# Display settings
_WIDTH = const(320)
_HEIGHT = const(480)

# Common SPI bus settings
_SPI_HOST = const(1)
_MOSI_PIN = const(11)
_MISO_PIN = const(13)
_SCLK_PIN = const(12)

# Display SPI bus settings
_LCD_SPI_FREQ = const(40_000_000)
_LCD_CS_PIN = const(10)
# display additional pins
_LCD_RST_PIN = const(15)
_LCD_DC_PIN = const(14)
_LCD_BKL_PIN = const(16)

spi_bus = machine.SPI.Bus(
    host=_SPI_HOST,
    miso=_MISO_PIN,
    mosi=_MOSI_PIN,
    sck=_SCLK_PIN,
)

display_bus = lcd_bus.SPIBus(
    spi_bus=spi_bus,
    freq=_LCD_SPI_FREQ,
    dc=_LCD_DC_PIN,
    cs=_LCD_CS_PIN
)

display = ili9488.ILI9488(
    data_bus=display_bus,
    display_width=_WIDTH,
    display_height=_HEIGHT,
    reset_pin=_LCD_RST_PIN,
    reset_state=ili9488.STATE_LOW,
    backlight_pin=_LCD_BKL_PIN,
    backlight_on_state=ili9488.STATE_HIGH,
    color_space=lv.COLOR_FORMAT.RGB565,
    rgb565_byte_swap=True
)

display.init()
display.set_backlight(100)

scr = lv.obj()
btn = lv.button(scr)
btn.align(lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("Hello World!")
lv.screen_load(scr)

import task_handler

th = task_handler.TaskHandler()
Voinic commented 1 month ago

I made some changes in display driver according to https://github.com/lovyan03/LovyanGFX/blob/master/src/lgfx/v1/panel/Panel_ILI948x.hpp Now it works with color_space=lv.COLOR_FORMAT.RGB888 but image gets horizontally mirrored. Still don't work with RGB565.

My ili9488.py driver code:

from micropython import const  # NOQA
import lvgl as lv  # NOQA
import lcd_bus  # NOQA
import display_driver_framework

_IFMODE = const(0xB0)
_FRMCTR1 = const(0xB1)
_DIC = const(0xB4)
_DFC = const(0xB6)
_EM = const(0xB7)
_PWR1 = const(0xC0)
_PWR2 = const(0xC1)
_VCMPCTL = const(0xC5)
_PGC = const(0xE0)
_NGC = const(0xE1)
_AC3 = const(0xF7)
_MADCTL = const(0x36)
_COLMOD = const(0x3A)
_NOP = const(0x00)
_SLPOUT  = const(0x11)
_DISPON  = const(0x29)
_IDMOFF  = const(0x38)

STATE_HIGH = display_driver_framework.STATE_HIGH
STATE_LOW = display_driver_framework.STATE_LOW
STATE_PWM = display_driver_framework.STATE_PWM

BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB
BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR

class ILI9488(display_driver_framework.DisplayDriver):

    # The st7795 display controller has an internal framebuffer
    # arranged in 320 x 480
    # configuration. Physical displays with pixel sizes less than
    # 320 x 480 must supply a start_x and
    # start_y argument to indicate where the physical display begins
    # relative to the start of the
    # display controllers internal framebuffer.

    # this display driver supports RGB565 and also RGB666. RGB666 is going to
    # use twice as much memory as the RGB565. It is also going to slow down the
    # frame rate by 1/3, This is becasue of the extra byte of data that needs
    # to get sent. To use RGB666 the color depth MUST be set to 32.
    # so when compiling
    # make sure to have LV_COLOR_DEPTH=32 set in LVFLAGS when you call make.
    # For RGB565 you need to have LV_COLOR_DEPTH=16

    # the reason why we use a 32 bit color depth is because of how the data gets
    # written. The entire 8 bits for each byte gets sent. The controller simply
    # ignores the lowest 2 bits in the byte to make it a 6 bit color channel
    # We just have to tell lvgl that we want to use

    display_name = 'ILI9488'

    def init(self):
        param_buf = bytearray(15)
        param_mv = memoryview(param_buf)

        param_buf[:15] = bytearray([
            0x00, 0x03, 0x09, 0x08, 0x16,
            0x0A, 0x3F, 0x78, 0x4C, 0x09,
            0x0A, 0x08, 0x16, 0x1A, 0x0F
        ])

        self.set_params(_PGC, param_mv[:15])

        param_buf[:15] = bytearray([
            0x00, 0x16, 0x19, 0x03, 0x0F,
            0x05, 0x32, 0x45, 0x46, 0x04,
            0x0E, 0x0D, 0x35, 0x37, 0x0F
        ])
        self.set_params(_NGC, param_mv[:15])

        param_buf[0] = 0x17
        param_buf[1] = 0x15

        self.set_params(_PWR1, param_mv[:2])

        param_buf[0] = 0x41
        self.set_params(_PWR2, param_mv[:1])

        param_buf[0] = 0x00
        param_buf[1] = 0x12
        param_buf[3] = 0x80
        self.set_params(_VCMPCTL, param_mv[:3])

        param_buf[0] = (
            self._madctl(
                self._color_byte_order,
                self._ORIENTATION_TABLE
            )
        )
        self.set_params(_MADCTL, param_mv[:1])

        color_size = lv.color_format_get_size(self._color_space)
        if color_size == 2:  # NOQA
            pixel_format = 0x55
        elif color_size == 3:
            pixel_format = 0x66
        else:
            raise RuntimeError(
                'ILI9488 IC only supports '
                'lv.COLOR_FORMAT.RGB565 or lv.COLOR_FORMAT.RGB888'
            )

        param_buf[0] = pixel_format
        self.set_params(_COLMOD, param_mv[:1])

        param_buf[0] = 0x00
        self.set_params(_IFMODE, param_mv[:1])

        param_buf[0] = 0xA0
        self.set_params(_FRMCTR1, param_mv[:1])

        param_buf[0] = 0x02
        self.set_params(_DIC, param_mv[:1])

        param_buf[0] = 0x02
        param_buf[1] = 0x22
        param_buf[2] = 0x3B
        self.set_params(_DFC, param_mv[:3])

        param_buf[0] = 0xC6
        self.set_params(_EM, param_mv[:1])

        param_buf[:4] = bytearray([
            0xA9, 0x51, 0x2C, 0x82
        ])
        self._data_bus.tx_param(_AC3, param_mv[:4])

        param_buf[:4] = bytearray([
            0xA9, 0x51, 0x2C, 0x82
        ])
        self._data_bus.tx_param(_AC3, param_mv[:4])

        param_buf[:2] = bytearray([
            0x80, 0x78
        ])
        self._data_bus.tx_param(_SLPOUT, param_mv[:2])

        param_buf[0] = 0x00
        self._data_bus.tx_param(_IDMOFF, param_mv[:1])

        param_buf[:2] = bytearray([
            0x80, 0x64
        ])
        self._data_bus.tx_param(_DISPON, param_mv[:2])    

        self.set_params(_NOP)

        display_driver_framework.DisplayDriver.init(self)
kdschlosser commented 1 month ago

try not using the reset pin.

the display you have only supports RGB565 and RGB666 it doesn't support RGB888. LVGL doesn't support RGB666 so we need to get RGB565 working properly for ya.

There have been people that have reported the exact same issue you are having and it being linked to the SPI speed or the reset pin. so try the reset pin first and then if that doesn't work change the SPI speed to 20000000 and see what happens.

Voinic commented 1 month ago

I tried setting different SPI speeds from 10 to 80 MHz but result is the same: screen gets grey (not white as it was before) and nothing else. Connecting reset to reset pin of the microcontroller also did not help

Voinic commented 1 month ago

I found out that ILI9488 doesn't support RGB565 in 4-line SPI mode, only RGB666, so no further investigation needed. Unfortunately, RGB565 is unavailable for me.

I fixed some errors including rotation issue in my previous driver code. Now it works well (with RGB666 colorspace).

My actual display driver code:


from micropython import const  # NOQA
import time
import lvgl as lv  # NOQA
import lcd_bus  # NOQA
import display_driver_framework

_IFMODE = const(0xB0)
_FRMCTR1 = const(0xB1)
_DIC = const(0xB4)
_DFC = const(0xB6)
_EM = const(0xB7)
_PWR1 = const(0xC0)
_PWR2 = const(0xC1)
_VCMPCTL = const(0xC5)
_PGC = const(0xE0)
_NGC = const(0xE1)
_AC3 = const(0xF7)
_MADCTL = const(0x36)
_COLMOD = const(0x3A)
_NOP = const(0x00)
_SLPOUT  = const(0x11)
_DISPON  = const(0x29)
_SWRESET = const(0x01)

STATE_HIGH = display_driver_framework.STATE_HIGH
STATE_LOW = display_driver_framework.STATE_LOW
STATE_PWM = display_driver_framework.STATE_PWM

BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB
BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR

_MADCTL_MH = const(0x04)  # Refresh 0=Left to Right, 1=Right to Left
_MADCTL_BGR = const(0x08)  # BGR color order
_MADCTL_ML = const(0x10)  # Refresh 0=Top to Bottom, 1=Bottom to Top
_MADCTL_MV = const(0x20)  # 0=Normal, 1=Row/column exchange
_MADCTL_MX = const(0x40)  # 0=Left to Right, 1=Right to Left
_MADCTL_MY = const(0x80)  # 0=Top to Bottom, 1=Bottom to Top

class ILI9488(display_driver_framework.DisplayDriver):

    # The st7795 display controller has an internal framebuffer
    # arranged in 320 x 480
    # configuration. Physical displays with pixel sizes less than
    # 320 x 480 must supply a start_x and
    # start_y argument to indicate where the physical display begins
    # relative to the start of the
    # display controllers internal framebuffer.

    # this display driver supports RGB565 and also RGB666. RGB666 is going to
    # use twice as much memory as the RGB565. It is also going to slow down the
    # frame rate by 1/3, This is becasue of the extra byte of data that needs
    # to get sent. To use RGB666 the color depth MUST be set to 32.
    # so when compiling
    # make sure to have LV_COLOR_DEPTH=32 set in LVFLAGS when you call make.
    # For RGB565 you need to have LV_COLOR_DEPTH=16

    # the reason why we use a 32 bit color depth is because of how the data gets
    # written. The entire 8 bits for each byte gets sent. The controller simply
    # ignores the lowest 2 bits in the byte to make it a 6 bit color channel
    # We just have to tell lvgl that we want to use

    display_name = 'ILI9488'

    _ORIENTATION_TABLE = (
        _MADCTL_MX,
        _MADCTL_MV,
        _MADCTL_MY,
        _MADCTL_MX | _MADCTL_MY | _MADCTL_MV
    )

    def init(self):
        param_buf = bytearray(15)
        param_mv = memoryview(param_buf)

        time.sleep_ms(120)

        self.set_params(_SWRESET)
        time.sleep_ms(200)

        self.set_params(_SLPOUT)
        time.sleep_ms(120)

        param_buf[:15] = bytearray([
            0x00, 0x03, 0x09, 0x08, 0x16,
            0x0A, 0x3F, 0x78, 0x4C, 0x09,
            0x0A, 0x08, 0x16, 0x1A, 0x0F
        ])
        self.set_params(_PGC, param_mv[:15])

        param_buf[:15] = bytearray([
            0x00, 0x16, 0x19, 0x03, 0x0F,
            0x05, 0x32, 0x45, 0x46, 0x04,
            0x0E, 0x0D, 0x35, 0x37, 0x0F
        ])
        self.set_params(_NGC, param_mv[:15])

        param_buf[0] = 0x17
        param_buf[1] = 0x15
        self.set_params(_PWR1, param_mv[:2])

        param_buf[0] = 0x41
        self.set_params(_PWR2, param_mv[:1])

        param_buf[0] = 0x00
        param_buf[1] = 0x12
        param_buf[3] = 0x80
        self.set_params(_VCMPCTL, param_mv[:3])

        param_buf[0] = (
            self._madctl(
                self._color_byte_order,
                self._ORIENTATION_TABLE
            )
        )
        self.set_params(_MADCTL, param_mv[:1])

        color_size = lv.color_format_get_size(self._color_space)
        if color_size == 2:  # NOQA
            pixel_format = 0x55
        elif color_size == 3:
            pixel_format = 0x66
        else:
            raise RuntimeError(
                'ILI9488 IC only supports '
                'lv.COLOR_FORMAT.RGB565 or lv.COLOR_FORMAT.RGB888'
            )

        param_buf[0] = pixel_format
        self.set_params(_COLMOD, param_mv[:1])

        param_buf[0] = 0x00
        self.set_params(_IFMODE, param_mv[:1])

        param_buf[0] = 0xA0
        self.set_params(_FRMCTR1, param_mv[:1])

        param_buf[0] = 0x02
        self.set_params(_DIC, param_mv[:1])

        param_buf[0] = 0x02
        param_buf[1] = 0x02
        param_buf[2] = 0x3B
        self.set_params(_DFC, param_mv[:3])

        param_buf[0] = 0xC6
        self.set_params(_EM, param_mv[:1])

        param_buf[:4] = bytearray([
            0xA9, 0x51, 0x2C, 0x02
        ])
        self.set_params(_AC3, param_mv[:4])

        self.set_params(_DISPON)
        time.sleep_ms(100)

        display_driver_framework.DisplayDriver.init(self)
kdschlosser commented 1 month ago

I updated the driver with your code. I also added proper handling of the RGB666 color space. It will convert the colors from rgb888 to rgb666 properly. Give it a shot and lemme know if it works.

Voinic commented 1 month ago

I changed int(buf_len / 3) to buf_len // 3 at line 194 because it gives ViperTypeError: binary op truediv not implemented

As a result I got green stripes on the display IMG_20240801_225357

Did you meant for i in range(0, buf_len, 3) at line 194?

kdschlosser commented 1 month ago

Your right. I goofed that. I was thinking of doing it one way while typing a different way and I got them all jumbled together..

kdschlosser commented 1 month ago

It's fixed..

Voinic commented 1 month ago

I updated the driver but colors are distorted and clipped. Here is how it looks like IMG_20240802_114823

But when I comment out transformation to rgb666, colors seem normal: IMG_20240802_114859

What is the need to do this transformation if the display controller ignores the 2 LSBs in each byte of 24-bit color and everything works by itself?

kdschlosser commented 1 month ago

according to the docs it's not a matter of ignoring the 2 lowest bits. the spec states that the bits are shifted.

This would be the bits for red

r7, r6 r5, r4, r3, r2, r1, r0

and this is what is supposed to be sent

r5, r4, r3, r2, r1, r0, ignored, ignored

kdschlosser commented 1 month ago

After doing some digging I discovered that the datasheet for the display IC is wrong. using RGB888 without doing anything is the correct way to handle it. The lowest 2 bits are the ones that should get dropped and not the highest 2 bits.

I corrected the driver so it should be good to go now.

Voinic commented 1 month ago

It works

kdschlosser commented 1 month ago

YAY!!