ST7796S driver lv_micropython

I would like to implement the ST7796S display driver, this display.
A Raspi Python Port already exists.
In this forum I have already learned that the ST7796S may be like the ili9488.
TFT_eSPI also supports the screen. I don’t own the module myself, I want to make sure that I can use it with lv_micropython first.
So I would like to know what I would have to rewrite in the source code to get the screen to work.
With best regards!

Hi @Christian_Draxl!

That’s great!
I’ll be happy to help you with this, hoping that you could eventually contribute the driver back to LVGL.

You didn’t explicitly say which platform you are targeting.
A driver is usually implemented for a specific architecture. ESP32 driver would be different from Raspi driver, for example.
In the links you sent there are references to Raspi, ESP32, STM32… Which one are you going to use?

While Python and Micropython implement the same Python3 syntax, they are different languages.
This means that a Python driver cannot directly be used on Micropython, but you can probably port much of the code.

One of the main differences is library support. The Raspi Python Port for example imports numpy, spidev, RPi.GPIO libraries which are not available on Micropython.

Sometimes a driver is implemented in C and has a Python API. Writing Python API for a C library is very different between the “original” Python and Micropython, but if you have a C driver for your target architecture then there is a technique to automatically convert the C API into Micropython API.

There are of course other differences between Python and Micropython, a major one is memory management (Python manages reference counting while Micropython uses Garbage Collection), but the end-user experience in both cases is “writing Python3 code”.

This is good!
We already have a pure-Micropython driver for ILI9341 and ILI9488 for ESP32.
Are you targeting ESP32? If so, it should be pretty easy adding ST7796S over there.

If you are targeting Raspi then you can consider using the frame-buffer Micropython driver.
The Linux kernel supports many displays with the Frame Buffer API so this could be a good “generic” solution for many displays.

Yes, I would like to use the display primarily for the ESP32 (in my case WROVER with 8MB PSRAM).

I suppose we can basically expand the (ili9XXX.py based on the comparison between the ili9488 and the ST7796?

Yes.

On this family of displays the interface (SPI) and commands are very similar, and most of the differences are the initialization sequence and the color mode.
Here is the ILI9488 specific initialization.

So all you need to do is derive a new subclass from ili9XXX, set self.init_cmds, self.display_name and self.display_type and call the parent constructor.

If you use the 16-bit/pixel color mode (RGB=(565) 65K colors) then you can probably set display_type to DISPLAY_TYPE_ILI9341 since ILI9341 also uses the same 16 bit color mode. (ILI9488 currently uses 24-bit color mode (RGB=(888) 16.7M colors) )

Were there any development efforts on this? Just got a lv_micropython build going and was wondering if any progress has been made before I take a deeper look into it.

Hi @MrTimcakes!

Not that I’m aware of.

For ESP32 the starting point is ili9XXX.py.
For generic driver (slower, but should work on other platforms) see st77xx.py.

In the future we hope that Micropython code would also be able to use native LVGL drivers from lv_drivers, but this is a long-term goal that might take some time.

Feel free to open a PR on lv_binding_micropython!

Cheers, yeah I’d looked at the ST77XX generic driver and made some sym links to include them for an ESP32 build. But I didn’t get anywhere that evening as the generic driver supports the ST7789 (240x320), but I’ve not looked at the datasheets to see how similar the initialisation procedure is to the ST7796 (320x480).

If I make any progress on it of course I’d submit a PR

st77xx author here. In my experience the easiest was to look at a few existing drivers which have the initialization sorted out already (e.g. here which is exceptionally cleanly written — don’t know about functionality, did not test), then write it using (new) symbolic constants where it makes sense (e.g. here, checking against the datasheet along the way. Later, you might add things like hardware display orientation (that particular driver has it here). You can use the test st77xx-test.py script (as described here). The last (easy, with st77xx), is to use the driver with LVGL. Good luck.

If anyone wants to give this a go they can try it. Just like using any other ili9xxx display. I am getting an st7796 display on Wednesday to test with so I will be able to tinker about more but until then give this a go and see what happens

import ili9XXX
import lvgl as lv


RGB666 = 1
RGB565 = 2


class st7796(ili9XXX.ili9XXX):

    # 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.

    def __init__(self,
        miso=-1, mosi=19, clk=18, cs=13, dc=12, rst=4, power=-1, backlight=15, backlight_on=1, power_on=0,
        spihost=ili9XXX.HSPI_HOST, spimode=0, mhz=80, hybrid=True, width=320, height=480, start_x=0, start_y=0,
        colormode=ili9XXX.COLOR_MODE_RGB, rot=ili9XXX.PORTRAIT, invert=False, double_buffer=True, half_duplex=True,
        asynchronous=False, initialize=True, color_format=lv.COLOR_FORMAT.NATIVE_REVERSE, pixel_format=RGB666):
        
        if pixel_format == RGB666:
            pixel_format = 0x06  # 262K-Colors
            factor = 4
            if lv.color_t.__SIZE__ != 4:
                raise RuntimeError(
                    'ST7796 micropython driver '
                    'requires defining LV_COLOR_DEPTH=32'
                )
        elif pixel_format == RGB565:
            factor = 2
            pixel_format = 0x05  # 65K-Colors
            if lv.color_t.__SIZE__ != 2:
                raise RuntimeError(
                    'ST7796 micropython driver '
                    'requires defining LV_COLOR_DEPTH=16'
                )
            
        else:
            raise RuntimeError(
                'Invalid pixel format, only RGB565 and RGB666 can be used'
            )

        self.display_name = 'ST7796'

        self.init_cmds = [
            {'cmd': 0x01, 'data': bytes([]), 'delay':120},  # SWRESET
            {'cmd': 0x11, 'data': bytes([]), 'delay':120},  # SLPOUT
            {'cmd': 0xF0, 'data': bytes([0xC3])},  # CSCON
            {'cmd': 0xF0, 'data': bytes([0x96])},  # CSCON
            {'cmd': 0x36, 'data': bytes([self.madctl(colormode, rot, (ili9XXX.MADCTL_MX | ili9XXX.MADCTL_MY, ili9XXX.MADCTL_MV | ili9XXX.MADCTL_MY, 0, ili9XXX.MADCTL_MX | ili9XXX.MADCTL_MV))])},  # MADCTL
            {'cmd': 0x3A, 'data': bytes([pixel_format])},  # Interface_Pixel_Format
            {'cmd': 0xB4, 'data': bytes([0x01])},  # INVTR
            {'cmd': 0xB6, 'data': bytes([0x80, 0x02, 0x3B])},  # DFC
            {'cmd': 0xE8, 'data': bytes([0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33])},  # DOCA
            {'cmd': 0xC1, 'data': bytes([0x06])},  # PWR2
            {'cmd': 0xC2, 'data': bytes([0xA7])},  # PWR3
            {'cmd': 0xC5, 'data': bytes([0x18]), 'delay':120},  # VCMPCTL
            {'cmd': 0xE0, 'data': bytes([0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B])},  # PGC
            {'cmd': 0xE1, 'data': bytes([0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]), 'delay':120},  # NGC
            {'cmd': 0xF0, 'data': bytes([0x3C])},  # CSCON
            {'cmd': 0xF0, 'data': bytes([0x69]), 'delay':120},  # CSCON
            {'cmd': 0x29, 'data': bytes([])}  # DISPON
        ]

        super().__init__(miso=miso, mosi=mosi, clk=clk, cs=cs, dc=dc, rst=rst, power=power, backlight=backlight,
            backlight_on=backlight_on, power_on=power_on, spihost=spihost, spimode=spimode, mhz=mhz, factor=factor, hybrid=hybrid,
            width=width, height=height, start_x=start_x, start_y=start_y, invert=invert, double_buffer=double_buffer,
            half_duplex=half_duplex, display_type=DISPLAY_TYPE_ST7796, asynchronous=asynchronous,
            initialize=initialize, color_format=color_format)

This code works


import espidf as esp
import lvgl as lv
import ili9XXX


class st7796(ili9XXX.ili9XXX):

    # 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

    def __init__(
        self,
        miso=-1,
        mosi=19,
        clk=18,
        cs=13,
        dc=12,
        rst=4,
        power=-1,
        backlight=15,
        backlight_on=1,
        power_on=0,
        spihost=esp.HSPI_HOST,
        spimode=0,
        mhz=80,
        hybrid=True,
        width=320,
        height=480,
        start_x=0,
        start_y=0,
        colormode=ili9XXX.COLOR_MODE_RGB,
        rot=ili9XXX.LANDSCAPE,
        invert=False,
        double_buffer=False,
        half_duplex=True,
        asynchronous=False,
        initialize=True,
        color_format=lv.COLOR_FORMAT.NATIVE
    ):

        if lv.color_t.__SIZE__ == 4:
            display_type = ili9XXX.DISPLAY_TYPE_ILI9488
            pixel_format = 0x06  # 262K-Colors
        elif lv.color_t.__SIZE__ == 2:
            pixel_format = 0x05  # 65K-Colors  55??
            display_type = ili9XXX.DISPLAY_TYPE_ST7789

        else:
            raise RuntimeError(
                'ST7796 micropython driver '
                'requires defining LV_COLOR_DEPTH=32 or LV_COLOR_DEPTH=16'
            )

        self.display_name = 'ST7796'

        self.init_cmds = [
            {'cmd': 0x01, 'delay':120},  # SWRESET
            {'cmd': 0x11, 'delay':120},  # SLPOUT
            {'cmd': 0xF0, 'data': bytes([0xC3])},  # CSCON  Enable extension command 2 partI
            {'cmd': 0xF0, 'data': bytes([0x96])},  # CSCON  Enable extension command 2 partII
            {'cmd': 0x36, 'data': bytes([self.madctl(colormode, rot, ili9XXX.ORIENTATION_TABLE)])},  # MADCTL
            # Interface_Pixel_Format
            {'cmd': 0x3A, 'data': bytes([pixel_format])},
            {'cmd': 0xB4, 'data': bytes([0x01])},  # INVTR  1-dot inversion
            {'cmd': 0xB6, 'data': bytes([
                0x80,  # Bypass
                0x02,  # Source Output Scan from S1 to S960, Gate Output scan from G1 to G480, scan cycle=2
                0x3B  # LCD Drive Line=8*(59+1)
            ])},  # DFC
            {'cmd': 0xE8, 'data': bytes([
                0x40,
                0x8A,
                0x00,
                0x00,
                0x29,  # Source eqaulizing period time= 22.5 us
                0x19,  # Timing for "Gate start"=25 (Tclk)
                0xA5,  # Timing for "Gate End"=37 (Tclk), Gate driver EQ function ON
                0x33
            ])},  # DOCA
            {'cmd': 0xC1, 'data': bytes([0x06])},  # PWR2 VAP(GVDD)=3.85+( vcom+vcom offset), VAN(GVCL)=-3.85+( vcom+vcom offset)
            {'cmd': 0xC2, 'data': bytes([0xA7])},  # PWR3 Source driving current level=low, Gamma driving current level=High
            {'cmd': 0xC5, 'data': bytes([0x18]), 'delay':120},  # VCMPCTL VCOM=0.9
            {'cmd': 0xE0, 'data': bytes([
                0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F,
                0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B
            ])},  # PGC
            {'cmd': 0xE1, 'data': bytes([
                0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B,
                0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B
            ]), 'delay':120},  # NGC
            {'cmd': 0xF0, 'data': bytes([0x3C])},  # CSCON  Disable extension command 2 partI
            {'cmd': 0xF0, 'data': bytes([0x69]), 'delay':120},  # CSCON Disable extension command 2 partII
            {'cmd': 0x29}  # DISPON
        ]

        super().__init__(
            miso=miso,
            mosi=mosi,
            clk=clk,
            cs=cs,
            dc=dc,
            rst=rst,
            power=power,
            backlight=backlight,
            backlight_on=backlight_on,
            power_on=power_on,
            spihost=spihost,
            spimode=spimode,
            mhz=mhz,
            hybrid=hybrid,
            width=width,
            height=height,
            start_x=start_x,
            start_y=start_y,
            invert=invert,
            double_buffer=double_buffer,
            half_duplex=half_duplex,
            display_type=display_type,
            asynchronous=asynchronous,
            initialize=initialize,
            color_format=color_format
        )

# #define PIN_SDA 18
# #define PIN_SCL 19
# #define TFT_MISO 12
# #define TFT_MOSI 13
# #define TFT_SCLK 14
# #define TFT_CS   15
# #define TFT_DC   21
# #define TFT_RST  22
# #define TFT_BL   23


LED = 23
RESET = 22
MOSI = 13
CS = 15
DC_RS = 21
SCK = 14
MISO = 12

# init display
disp = st7796(
    miso=MISO,
    mosi=MOSI,
    clk=SCK,
    cs=CS,
    dc=DC_RS,
    rst=RESET,
    backlight=LED,
    power=-1,
    width=480,
    height=320,
    rot=ili9XXX.LANDSCAPE
)

#
# screen = Screen()
#
# ui.Main(screen)
# lv.scr_load(screen)

scr = lv.obj()
btn = lv.btn(scr)
btn.set_style_bg_color(lv.color_hex(0xFF0000), lv.PART.MAIN | lv.STATE.DEFAULT)
scr.set_style_bg_color(lv.color_hex(0x000000), lv.PART.MAIN | lv.STATE.DEFAULT)
label = lv.label(btn)
label.set_text("Hello World!")
btn.set_style_width(120, lv.PART.MAIN | lv.STATE.DEFAULT)
btn.set_style_height(35, lv.PART.MAIN | lv.STATE.DEFAULT)

bh = 18
bw = 60

by = 160 - bh
bx = 240 - bw

btn.set_x(bx)
btn.set_y(by)
# Load the screen

lv.scr_load(scr)

I get this error with your code:

Not enough DMA-able memory to allocate display buffer.

trying it with a WT-SC01 board flashed with lv_micropython

any advice?

Try higher factor values.
Higher factor means the screen is updated in smaller parts.

yessir. Change the factor to 8 and also set double_buffer to True

One other thing it to initialize the display driver ASAP in your code. Other things can end up using the DMA memory on the ESP32 and because the chip on your display is a WROVER-B there is no DMA memory available on the SPIRAM. There is on the ESP32C3 series but not with the earlier versions of the SOC.