Need driver library for 3.5inch 480x320 display ST7796 with RP2040

I need a driver library for 3.5inch 480x320 display ST7796 that is connected to RP2040.

I used the Ili9341 driver library from which is dependent on st77xx.py. The hardware pins were edited as per my project.

import lvgl as lv
import ili9xxx
from st77xx import *
from machine import SPI, Pin


lv.init()
spi = SPI(1, baudrate=27000000, sck=Pin(10), mosi=Pin(11), miso=Pin(8))
drv = ili9xxx.Ili9341(spi=spi, dc=23, cs=29, rst=28)

I am getting this error:

Traceback (most recent call last):
  File "<stdin>", line 9, in <module>
  File "ili9xxx.py", line 170, in __init__
  File "st77xx.py", line 448, in __init__
RuntimeError: LVGL *must* be compiled with LV_COLOR_DEPTH=16 (currently LV_COLOR_DEPTH=32.

I also observed in the 480x320 resolution is not defined in the st77xx.py.

How to I change the current libraries to accommodate 3.5inch 480x320 display ST7796?

1 Like

Your error is unrelated to the resolution. As the error message says:

LVGL *must* be compiled with LV_COLOR_DEPTH=16 (currently LV_COLOR_DEPTH=32.

Anyway: 32 bits color depth (per pixel, not per channel) x 480 x 320 = 600kB . Bytes, not bits. That´s 300kB if you use 16 bits color depth. The RP2040 has 264kB SRAM, and you need some for running micropython.

My guess is that if you really want to use the RP2040 with this resolution you might want to get down to 8 bits per pixel (150kB) -if supported by the driver- and/or drop micropython for pure (C) LVGL.

Or use another target like ESP32 with (slow) SPIRAM. Or better, a beefy STM32H7 with 1MB of RAM.

But you´re definitely -i think- not going to get high-resolution +high framerate, micropython on the RP2040.

Or switch to a comparable size display with half the resolution.

See here: ESP32 320x480 low FPS animating - #10 by Marian_M
and here: Colors — LVGL documentation

@all: If something i wrote is inexact, please correct me.

1 Like

LVGL does not require the entire screen to be mapped to memory.
It can render the screen in parts.
The display driver can decide what is the size of the buffer it provides to LVGL for rendering. When the buffers are small, rendering will be done in multiple steps.

See the docs:

https://docs.lvgl.io/master/porting/disp.html#draw-buffers

See how “factor” argument is used here to control buffer size:

So a possible solution would be to set a higher factor value for smaller buffers.

Thank you for the accurate correction !

I had a look at this issue. I am also using the 480x320 display ST7796 driver but with RP2040. How do I make it compatible with RP2040?

1 Like

I tried creating a driver library for 3.5inch 480x320 display ST7796 with RP2040. Before building the LVGL firmware, I changed the value of LV_COLOR_DEPTH to 16 in lib\lv_bindings. I am getting this error

Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
  File "ili9xxx.py", line 323, in __init__
  File "ili9xxx.py", line 311, in __init__
MemoryError: memory allocation failed, allocating 536923960 bytes

main.py

import lvgl as lv
import ili9xxx
from machine import SPI, Pin

spi = SPI(1, baudrate=27000000, sck=Pin(10), mosi=Pin(11), miso=Pin(8))
drv = ili9xxx.St7796(spi=spi, dc=23, cs=29, rst=28)

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)
lv.scr_load(scr)

ili9xxx.py

from micropython import const
import lvgl as lv
import machine
import time
import struct

_MADCTL = const(0x36)
_MADCTL_BGR = const(0x08)  # colors are BGR (not RGB)
_MADCTL_RTL = const(0x04)  # refresh right to left

MADCTL_MH = const(0x04)  # Refresh 0=Left to Right, 1=Right to Left
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
ORIENTATION_TABLE = (MADCTL_MX, MADCTL_MV, MADCTL_MY, MADCTL_MY | MADCTL_MX | MADCTL_MV)


ST7796_PORTRAIT = const(0)
ST7796_LANDSCAPE = const(1)
ST7796_INV_PORTRAIT = const(2)
ST7796_INV_LANDSCAPE = const(3)

DISPLAY_TYPE_ST7789 = const(4)
DISPLAY_TYPE_ILI9488 = const(2)
COLOR_MODE_RGB = const(0x00)

class St77xx_hw(object):
    def __init__(self, *, cs, dc, spi, res, suppRes, bl=None, model=None, suppModel=[], rst=None, rot=ST7796_LANDSCAPE, bgr=False, rp2_dma=None):
        
        self.buf1 = bytearray(1)
        self.buf2 = bytearray(2)
        self.buf4 = bytearray(4)

        self.cs,self.dc,self.rst=[(machine.Pin(p,machine.Pin.OUT) if isinstance(p,int) else p) for p in (cs,dc,rst)]
        self.bl=bl
        if isinstance(self.bl,int): self.bl=machine.PWM(machine.Pin(self.bl,machine.Pin.OUT))
        elif isinstance(self.bl,machine.Pin): self.bl=machine.PWM(self.bl)
        assert isinstance(self.bl,(machine.PWM,type(None)))
        self.set_backlight(10) # set some backlight

        self.rot=rot
        self.bgr=bgr
        self.width,self.height=(0,0) # this is set later in hard_reset->config->apply_rotation

        if res not in suppRes: raise ValueError('Unsupported resolution %s; the driver supports: %s.'%(str(res),', '.join(str(r) for r in suppRes)))
        if suppModel and model not in suppModel: raise ValueError('Unsupported model %s; the driver supports: %s.'%(str(model),', '.join(str(r) for r in suppModel)))

        self.res=res
        self.model=model

        self.rp2_dma=rp2_dma
        self.spi=spi
        self.hard_reset()


    def off(self): self.set_backlight(0)

    def hard_reset(self):
        if self.rst:
            for v in (1,0,1):
                self.rst.value(v)
                time.sleep(.2)
            time.sleep(.2)
        self.config()
        
    def config(self):
        self.config_hw() # defined in child classes
        self.apply_rotation(self.rot)
        
    def set_backlight(self,percent):
        if self.bl is None: return
        self.bl.duty_u16(percent*655)
        
    def set_window(self, x, y, w, h):
        c0,r0=ST77XX_COL_ROW_MODEL_START_ROTMAP[self.res[0],self.res[1],self.model][self.rot%4]
        struct.pack_into('>hh', self.buf4, 0, c0+x, c0+x+w-1)
        self.write_register(ST77XX_CASET, self.buf4)
        struct.pack_into('>hh', self.buf4, 0, r0+y, r0+y+h-1)
        self.write_register(ST77XX_RASET, self.buf4)

    def apply_rotation(self,rot):
        self.rot=rot
        if (self.rot%2)==0: self.width,self.height=self.res
        else: self.height,self.width=self.res
        self.write_register(ST77XX_MADCTL,bytes([(ST77XX_MADCTL_BGR if self.bgr else 0)|ST77XX_MADCTL_ROTS[self.rot%4]]))

    def blit(self, x, y, w, h, buf, is_blocking=True):
        self.set_window(x, y, w, h)
        if self.rp2_dma: self._rp2_write_register_dma(ST77XX_RAMWR, buf, is_blocking)
        else: self.write_register(ST77XX_RAMWR, buf)

    def clear(self, color):
        bs=128 # write pixels in chunks; makes the fill much faster
        struct.pack_into('>h',self.buf2,0,color)
        buf=bs*bytes(self.buf2)
        npx=self.width*self.height
        self.set_window(0, 0, self.width, self.height)
        self.write_register(ST77XX_RAMWR, None)
        self.cs.value(0)
        self.dc.value(1)
        for _ in range(npx//bs): self.spi.write(buf)
        for _ in range(npx%bs): self.spi.write(self.buf2)
        self.cs.value(1)

    def write_register(self, reg, buf=None):
        struct.pack_into('B', self.buf1, 0, reg)
        self.cs.value(0)
        self.dc.value(0)
        self.spi.write(self.buf1)
        if buf is not None:
            self.dc.value(1)
            self.spi.write(buf)
        self.cs.value(1)

    def _rp2_write_register_dma(self, reg, buf, is_blocking=True):
        'If *is_blocking* is False, used should call wait_dma explicitly.'
        SPI1_BASE = 0x40040000 # FIXME: will be different for another SPI bus?
        SSPDR     = 0x008
        self.rp2_dma.config(
            src_addr = uctypes.addressof(buf),
            dst_addr = SPI1_BASE + SSPDR,
            count    = len(buf),
            src_inc  = True,
            dst_inc  = False,
            trig_dreq= self.rp2_dma.DREQ_SPI1_TX
        )
        struct.pack_into('B',self.buf1,0,reg)
        self.cs.value(0)

        self.dc.value(0)
        self.spi.write(self.buf1)

        self.dc.value(1)
        self.rp2_dma.enable()

        if is_blocking: self.rp2_wait_dma()

    def rp2_wait_dma(self):
        '''
        Wait for rp2-port DMA transfer to finish; no-op unless self.rp2_dma is defined.
        Can be used as callback before accessing shared SPI bus e.g. with the xpt2046 driver.
        '''
        if self.rp2_dma is None: return
        while self.rp2_dma.is_busy(): pass
        self.rp2_dma.disable()
        # wait to send last byte. It should take < 1uS @ 10MHz
        time.sleep_us(1)
        self.cs.value(1)

    def _run_seq(self,seq):
        '''
        Run sequence of (initialization) commands; those are given as list of tuples, which are either
        `(command,data)` or `(command,data,delay_ms)`
        '''
        for i,cmd in enumerate(seq):
            if len(cmd)==2: (reg,data),delay=cmd,0
            elif len(cmd)==3: reg,data,delay=cmd
            else: raise ValueError('Command #%d has %d items (must be 2 or 3)'%(i,len(cmd)))
            self.write_register(reg,data)
            if delay>0: time.sleep_ms(delay)

    def madctl(self, colormode, rotation, rotations):
        if rotation >= 0:
            return rotation | colormode
        index = abs(rotation) - 1
        if index > len(rotations):
                RuntimeError('Invalid display rot value specified during init.')
        return rotations[index] | colormode



class St7796_hw(St77xx_hw):
    def __init__(self, **kw):
        """ST7796 TFT Display Driver.

        Requires ``LV_COLOR_DEPTH=16`` when building lv_micropython to function.
        """
        super().__init__(
            res=(480, 320),
            suppRes=[
                (480, 320),
            ],
            model=None,
            suppModel=None,
            bgr=False,
            **kw,
        )
        
    def display_config(self):
        if lv.color_t.__SIZE__ == 4:
            display_type = DISPLAY_TYPE_ILI9488
            pixel_format = 0x06  # 262K-Colors
        elif lv.color_t.__SIZE__ == 2:
            pixel_format = 0x05  # 65K-Colors  55??
            display_type = DISPLAY_TYPE_ST7789
        else:
            raise RuntimeError('ST7796 micropython driver requires defining LV_COLOR_DEPTH=32 or LV_COLOR_DEPTH=16')
        return pixel_format
        
    def config_hw(self):
        self._run_seq(
            [
                (0x01,None, 120),
                (0x11, None, 120),
                (0xF0,b"\xc3"),
                (0xF0,b"\x96"),
                (0x36,bytes([self.madctl(COLOR_MODE_RGB, ST7796_LANDSCAPE, ORIENTATION_TABLE)])),
                #(0X36,b"\x0"),
                (0x3A,bytes([self.display_config()])),
                (0xB4,b"\x01"),
                (0xB6,b"\x80\x02\x3B"),
                (0xE8,b"\x40\x8A\x00\x00\x29\x19\xA5\x33"),
                (0xC1,b"\x06"),
                (0xC2,b"\xA7"),
                (0xC5,b"\x18", 120),
                (0xE0,b"\xF0\x09\x0B\x06\x04\x15\x2F\x54\x42\x3C\x17\x14\x18\x1B"),
                (0xE1,b"\xE0\x09\x0B\x06\x04\x03\x2B\x43\x42\x3B\x16\x14\x17\x1B", 120),
                (0xF0,b"\x3C"),
                (0xF0,b"\x69",120),
                (0x29,b""),
            ]
        )

    def apply_rotation(self, rot):
        self.rot = rot
        if (self.rot % 2) == 0:
            self.width, self.height = self.res
        else:
            self.height, self.width = self.res
        self.write_register(_MADCTL,bytes([_MADCTL_BGR | ORIENTATION_TABLE[self.rot % 4]]),)
    
    def madctl(self, colormode, rotation, rotations):

        # if rotation is 0 or positive use the value as is.

        if rotation >= 0:
            return rotation | colormode

        # otherwise use abs(rotation)-1 as index to retreive value from rotations set

        index = abs(rotation) - 1
        if index > len(rotations):
                RuntimeError('Invalid display rot value specified during init.')

        return rotations[index] | colormode

class St77xx_lvgl(object):
    '''LVGL wrapper for St77xx, not to be instantiated directly.

    * creates and registers LVGL display driver;
    * allocates buffers (double-buffered by default);
    * sets the driver callback to the disp_drv_flush_cb method.

    '''
    def disp_drv_flush_cb(self,disp_drv,area,color):
        # print(f"({area.x1},{area.y1}..{area.x2},{area.y2})")
        self.rp2_wait_dma() # wait if not yet done and DMA is being used
        # blit in background
        self.blit(area.x1,area.y1,w:=(area.x2-area.x1+1),h:=(area.y2-area.y1+1),color.__dereference__(2*w*h),is_blocking=False)
        self.disp_drv.flush_ready()
        
    def __init__(self,doublebuffer=True,factor=4):
        import lvgl as lv
        import lv_utils

        if lv.COLOR_DEPTH!=16: raise RuntimeError(f'LVGL *must* be compiled with LV_COLOR_DEPTH=16 (currently LV_COLOR_DEPTH={lv.COLOR_DEPTH}.')
        
        bufSize=(self.width*self.height*lv.color_t.__SIZE__)//factor

        if not lv.is_initialized(): lv.init()
        # create event loop if not yet present
        if not lv_utils.event_loop.is_running(): self.event_loop=lv_utils.event_loop()

        # attach all to self to avoid objects' refcount dropping to zero when the scope is exited
        self.disp_drv = lv.disp_create(self.width, self.height)  //line 311
        self.disp_drv.set_flush_cb(self.disp_drv_flush_cb)
        self.disp_drv.set_draw_buffers(bytearray(bufSize), bytearray(bufSize) if doublebuffer else None, bufSize, lv.DISP_RENDER_MODE.PARTIAL)
        self.disp_drv.set_color_format(lv.COLOR_FORMAT.NATIVE if self.bgr else lv.COLOR_FORMAT.NATIVE_REVERSED)


class St7796(St7796_hw, St77xx_lvgl):
    def __init__(self, doublebuffer=True, factor=4, **kw):
        """See :obj:`Ili9341_hw` for the meaning of the parameters."""
        import lvgl as lv

        St7796_hw.__init__(self, **kw)
        St77xx_lvgl.__init__(self, doublebuffer, factor)

@ kdschlosser can you have a look at this code. I used the library from this issue

@Mishal_Ferrao

change your factor to 8 and see if it goes.

IDK how much memory your board has. the factor sets the buffer size. the buffer size is calculates as

(display_width * display_height * 2) / factor.

If you are using double buffering it needs to make 2 buffers of that size, keep that in mind.

This line of code is also wrong.

        self.disp_drv.set_draw_buffers(bytearray(bufSize), bytearray(bufSize) if doublebuffer else None, bufSize, lv.DISP_RENDER_MODE.PARTIAL)

it should be

        self.disp_drv.set_draw_buffers(bytearray(bufSize), None if doublebuffer is False else bytearray(bufSize), bufSize, lv.DISP_RENDER_MODE.PARTIAL)

Did the following changes:

  1. Changed the set_draw_buffers function.
  2. Changed the value of factor from 4 to 8.

I am getting a black screen.

black screen is a good. So no errors.

Now all that we need to do is correct the main script so it loops and updates the tick and also updates the widgets.

import lvgl as lv
import ili9xxx
import time
from machine import SPI, Pin


spi = SPI(1, baudrate=27000000, sck=Pin(10), mosi=Pin(11), miso=Pin(8))
drv = ili9xxx.St7796(spi=spi, dc=23, cs=29, rst=28)

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)
lv.scr_load(scr)


start_time = time.ticks_ms()
while True:
    end_time = time.ticks_ms()
    diff = time.ticks_diff(end_time, start_time)
    if diff >= 1:
        start_time = end_time
        lv.tick_inc(diff)
        lv.task_handler()

give that a go and see if it works for ya.

anything you want to run in your program needs to be inside of that loop. Otherwise MicroPython will end up going into the repl and sit there waiting for user input over a serial connection. When you want to have a GUI run you will not be able to do this because the script needs to continually loop in order to update the GUI. You can use an ISR to handle the updating of the timer and scheduling a task that would call lv.task_handler but I would recommend doing this. The reason being is using an ISR is going to cause more overhead an you will only be able to do the updates every 10ms or so at best. might be even longer because of the board you are using. You cannot call lv.task_handler from the ISR due to memory allocation so you would have to use the ISR to schedule a task to call lv_task_handler. You have take make sure that you do not schedule too many tasks because it will consume all of the memory on the board do to how long LVGL can take to process the call to lv.task_handler. If you decide to go that route take care when writing the code.

I find it easier to just do what I did here at the bottom of the main.py script. It is also easier to bug test due to a complete stack trace being available.

actually scratch those last 2 posts. I just notice the code is not going to work properly. I need to rewrite it.

It’s the order in which things are being set up that isn’t right.

Some errors are still there. No sure how I did not get them before.

Traceback (most recent call last):
  File "<stdin>", line 34, in <module>
  File "ili9xxx.py", line 295, in disp_drv_flush_cb
  File "ili9xxx.py", line 124, in blit
  File "ili9xxx.py", line 111, in set_window
NameError: name 'ST77XX_COL_ROW_MODEL_START_ROTMAP' isn't defined

in st77xx.py the value of ST77XX_COL_ROW_MODEL_START_ROTMAP is as follows. But the display that I am using is 480x320

ST77XX_COL_ROW_MODEL_START_ROTMAP={
    # ST7789
    (240,320,None):[(0,0),(0,0),(0,0),(0,0)],
    (240,240,None):[(0,0),(0,0),(0,80),(80,0)],
    (135,240,None):[(52,40),(40,53),(53,40),(40,52)],
    # ST7735
    (128,160,'blacktab'):[(0,0),(0,0),(0,0),(0,0)],
    (128,160,'redtab'):[(2,1),(1,2),(2,1),(1,2)],
}

I was having a look at this issue. Its driver library for ST7796 but with ESP32.

give me some time. I am writing you a driver.

There are bound to be some problems with this. I keyed it up using Windows Notepad which is not exactly a code editor.
Give the code below a try, you do not need to compile again, all you need to do is replace the code in your main.py with the code below and upload it to your board.

I am sure you can figure out any basic problems like syntax errors or spelling mistakes that will come up. If there is something you are not sure about lemme know and I will fix it. This example does not make use of the DMA support for your board. I will add that support once we know this is working properly. the DMA stuff for your board is not straight forward and easy to do but I can get it done for yam just want to make sure it is working properly without DMA first.

from micropython import const
import lvgl as lv
import machine
import time
import struct
# import lv_utils

COLOR_MODE_RGB = const(0x00)
COLOR_MODE_BGR = const(0x08)


_MADCTL = const(0x36)
_MADCTL_RTL = const(0x04)  # refresh right to left

_MADCTL_MH = const(0x04)  # Refresh 0=Left to Right, 1=Right to Left
_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
ORIENTATION_TABLE = (_MADCTL_MX, _MADCTL_MV, _MADCTL_MY, _MADCTL_MY | _MADCTL_MX | _MADCTL_MV)


# Negative orientation constants indicate the MADCTL value will come from the ORIENTATION_TABLE,
# otherwise the rot value is used as the MADCTL value.
PORTRAIT = const(-1)
LANDSCAPE = const(-2)
INV_PORTRAIT = const(-3)
INV_LANDSCAPE = const(-4)



class St7796(object):
    def __init__(self, width, height, cs, dc, spi, bl=-1, rst=-1, rot=LANDSCAPE, colormode=COLOR_MODE_RGB, rp2_dma=None, bl_pwm=False, doublebuffer=False, factor=8):
        """See :obj:`Ili9341_hw` for the meaning of the parameters."""

        if lv.color_t.__SIZE__ != 2: 
            raise RuntimeError('LVGL *must* be compiled with LV_COLOR_DEPTH=16')

        bufSize = int((width * height * lv.color_t.__SIZE__) // factor)

        if not lv.is_initialized(): 
            lv.init()

        self.cmd_trans_data = bytearray(1)
        self.word_trans_data = bytearray(4)

        self.flush_buf1 = bytearray(bufSize)
        if doublebuffer:
            self.flush_buf2 = bytearray(bufSize)
        else:
            self.flush_buf2 = None

        if colormode == COLOR_MODE_RGB:
            colorformat = lv.COLOR_FORMAT.NATIVE_REVERSED
        else:
            colorformat = lv.COLOR_FORMAT.NATIVE
        
        self.colormode = colormode
        self.colorformat = colorformat

        self.disp_drv = lv.disp_create(width, height)
        self.disp_drv.set_flush_cb(self.flush_cb)
        self.disp_drv.set_draw_buffers(self.flush_buf1, self.flush_buf2, int(bufSize // lv.color_t.__SIZE__), lv.DISP_RENDER_MODE.PARTIAL)
        self.disp_drv.set_color_format(colorformat)
        
        if isinstance(cs, int):
            if cs == -1:
               cs = None
            else:
                cs = machine.Pin(cs, machine.Pin.OUT)

        if isinstance(dc, int):
            dc = machine.Pin(dc, machine.Pin.OUT)

        if isinstance(rst, int):
            if rst == -1:
               rst = None
            else:
                rst = machine.Pin(cs, machine.Pin.OUT)
                rst.value(1)

        if isinstance(bl, int):
            if bl == -1:
               bl = None
            else:
                bl = machine.Pin(cs, machine.Pin.OUT)

        if bl is not None and bl_pwm:
            bl = machine.PWM(bl)
            
            
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.bl = bl
        self.bl_pwm = bl_pwm

        self.rot = rot
        self.width = 0
        self.height = 0
        self.res = (width, height)

        self.rp2_dma = rp2_dma
        self.spi = spi
        self.init()
        
        # create event loop if not yet present
        #comment this section out if you do not want to use an ISR to handle the refreshing of the display
        # if not lv_utils.event_loop.is_running(): 
        #    self.event_loop=lv_utils.event_loop()

    def off(self): 
        self.set_backlight(0)

    def hard_reset(self):
        if self.rst:
            for v in (0,1):
                self.rst.value(v)
                time.sleep_ms(100)

        self.init()
        
    def set_backlight(self, percent):
        if self.bl is None: 
            return
        if self.bl_pwm:
            self.bl.duty_u16(percent * 655)
        elif percent <= 25:
            self.bl.value(0)
        else:
            self.bl.value(1)

    def blit(self, x, y, w, h, buf, is_blocking=True):
        # Memory write by DMA, disp_flush_ready when finished

        if self.rp2_dma: 
            self._rp2_write_register_dma(0x2C, buf, is_blocking)
        else: 
            self.send_cmd(0x2C, buf)


    def _rp2_write_register_dma(self, reg, buf, is_blocking=True):
        '''If *is_blocking* is False, used should call wait_dma explicitly.'''

        SPI1_BASE = 0x40040000 # FIXME: will be different for another SPI bus?
        SSPDR = 0x008
        self.rp2_dma.config(
            src_addr = uctypes.addressof(buf),
            dst_addr = SPI1_BASE + SSPDR,
            count    = len(buf),
            src_inc  = True,
            dst_inc  = False,
            trig_dreq= self.rp2_dma.DREQ_SPI1_TX
        )

        self.cmd_trans_data[0] = reg
        if self.cs:
            self.cs.value(0)

        self.dc.value(0)

        self.spi.write(self.cmd_trans_data)

        self.dc.value(1)
        self.rp2_dma.enable()

        if is_blocking: 
            self.rp2_wait_dma()

    def rp2_wait_dma(self):
        '''
        Wait for rp2-port DMA transfer to finish; no-op unless self.rp2_dma is defined.
        Can be used as callback before accessing shared SPI bus e.g. with the xpt2046 driver.
        '''
        if self.rp2_dma is None: 
            return

        while self.rp2_dma.is_busy(): 
            pass

        self.rp2_dma.disable()

        # wait to send last byte. It should take < 1uS @ 10MHz
        time.sleep_us(1)
        if self.cs:
            self.cs.value(1)
    
    def send_cmd(self, cmd, buf=None):
        if self.cs:
            self.cs.value(0)

        self.dc.value(0)
        self.cmd_trans_data[0] = cmd
        self.spi.write(self.cmd_trans_data)
        
        if buf is not None:
            self.dc.value(1)
            self.spi.write(buf)

        if self.cs:
            self.cs.value(1)

    def flush_cb(self, disp_drv, area, color):
        # print(f"({area.x1},{area.y1}..{area.x2},{area.y2})")
        self.rp2_wait_dma() # wait if not yet done and DMA is being used
        # blit in background

        # Column addresses
        x1 = area.x1
        x2 = area.x2

        self.word_trans_data[0] = (x1 >> 8) & 0xFF
        self.word_trans_data[1] = x1 & 0xFF
        self.word_trans_data[2] = (x2 >> 8) & 0xFF
        self.word_trans_data[3] = x2 & 0xFF
        self.send_cmd(0x2A, self.word_trans_data)

        # Page addresses        
        y1 = area.y1
        y2 = area.y2

        self.word_trans_data[0] = (y1 >> 8) & 0xFF
        self.word_trans_data[1] = y1 & 0xFF
        self.word_trans_data[2] = (y2 >> 8) & 0xFF
        self.word_trans_data[3] = y2 & 0xFF
        self.send_cmd(0x2B, self.word_trans_data)

        width = x2 - x1 + 1
        height = y2 - y1 + 1
        data_view = color.__dereference__(width * height * lv.color_t.__SIZE__)

        self.blit(x1, y1, width, height, data_view, is_blocking=False)

        self.disp_drv.flush_ready()

    def init(self):
        if self.rot in (-1, -3):
            self.width, self.height = self.res
        else:
            self.height, self.width = self.res

        self._run_seq(
            [
                (0x01,None, 120),
                (0x11, None, 120),
                (0xF0,b"\xc3"),
                (0xF0,b"\x96"),
                (0x36,bytes([bytes([self.madctl(self.colormode, rot, ORIENTATION_TABLE))),
                #(0X36,b"\x0"),
                (0x3A,b"\x05"),  # pixelformat
                (0xB4,b"\x01"),
                (0xB6,b"\x80\x02\x3B"),
                (0xE8,b"\x40\x8A\x00\x00\x29\x19\xA5\x33"),
                (0xC1,b"\x06"),
                (0xC2,b"\xA7"),
                (0xC5,b"\x18", 120),
                (0xE0,b"\xF0\x09\x0B\x06\x04\x15\x2F\x54\x42\x3C\x17\x14\x18\x1B"),
                (0xE1,b"\xE0\x09\x0B\x06\x04\x03\x2B\x43\x42\x3B\x16\x14\x17\x1B", 120),
                (0xF0,b"\x3C"),
                (0xF0,b"\x69",120),
                (0x29,b""),
            ]
        )

    def apply_rotation(self, rot):
        self.rot = rot

        if rot in (-1, -3):
            self.width, self.height = self.res
        else:
            self.height, self.width = self.res

        self.send_cmd(_MADCTL, bytes([self.madctl(self.colormode, rot, ORIENTATION_TABLE))
    
    def madctl(self, colormode, rotation, rotations):
        # if rotation is 0 or positive use the value as is.
        if rotation >= 0:
            return rotation | colormode

        # otherwise use abs(rotation)-1 as index to retreive value from rotations set

        index = abs(rotation) - 1
        if index > len(rotations):
            RuntimeError('Invalid display rot value specified during init.')

        return rotations[index] | colormode

    def _run_seq(self,seq):
        '''
        Run sequence of (initialization) commands; those are given as list of tuples, which are either
        `(command,data)` or `(command,data,delay_ms)`
        '''
        for i,cmd in enumerate(seq):
            if len(cmd) == 2:
              self.send_cmd(*cmd)
            elif len(cmd) == 3: 
                self.send_cmd(*cmd[:2])
                time.sleep_ms(cmd[-1])
            else: 
              raise ValueError('Command #%d has %d items (must be 2 or 3)'%(i,len(cmd)))


spi = maachine.SPI(1, baudrate=27000000, sck=machine.Pin(10), mosi=machine.Pin(11), miso=machine.Pin(8))
drv = St7796(width=320, height=480, factor=8, spi=spi, dc=23, cs=29, rst=28)

scr = lv.scr_act()
scr.set_style_bg_color(lv.color_hex(0x000000), lv.PART.MAIN)

btn = lv.btn(scr)
btn.set_style_bg_color(lv.color_hex(0xFF0000), lv.PART.MAIN)


label = lv.label(btn)
label.set_text("Hello World!")

btn.set_width(120)
btn.set_height(35)

btn.center()

start_time = time.ticks_ms()
while True:
    end_time = time.ticks_ms()
    diff = time.ticks_diff(end_time, start_time)
    if diff >= 1:
        start_time = end_time
        lv.tick_inc(diff)
        lv.task_handler()

Corrected the spelling mistakes and the missing parenthesis. I uploaded the code a couple of times but its giving me a blank screen

from micropython import const
import lvgl as lv
import machine
import time
import struct
# import lv_utils

COLOR_MODE_RGB = const(0x00)
COLOR_MODE_BGR = const(0x08)


_MADCTL = const(0x36)
_MADCTL_RTL = const(0x04)  # refresh right to left

_MADCTL_MH = const(0x04)  # Refresh 0=Left to Right, 1=Right to Left
_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
ORIENTATION_TABLE = (_MADCTL_MX, _MADCTL_MV, _MADCTL_MY, _MADCTL_MY | _MADCTL_MX | _MADCTL_MV)


# Negative orientation constants indicate the MADCTL value will come from the ORIENTATION_TABLE,
# otherwise the rot value is used as the MADCTL value.
PORTRAIT = const(-1)
LANDSCAPE = const(-2)
INV_PORTRAIT = const(-3)
INV_LANDSCAPE = const(-4)



class St7796(object):
    def __init__(self, width, height, cs, dc, spi, bl=-1, rst=-1, rot=LANDSCAPE, colormode=COLOR_MODE_RGB, rp2_dma=None, bl_pwm=False, doublebuffer=False, factor=8):
        """See :obj:`Ili9341_hw` for the meaning of the parameters."""

        if lv.color_t.__SIZE__ != 2: 
            raise RuntimeError('LVGL *must* be compiled with LV_COLOR_DEPTH=16')

        bufSize = int((width * height * lv.color_t.__SIZE__) // factor)

        if not lv.is_initialized(): 
            lv.init()

        self.cmd_trans_data = bytearray(1)
        self.word_trans_data = bytearray(4)

        self.flush_buf1 = bytearray(bufSize)
        if doublebuffer:
            self.flush_buf2 = bytearray(bufSize)
        else:
            self.flush_buf2 = None

        if colormode == COLOR_MODE_RGB:
            colorformat = lv.COLOR_FORMAT.NATIVE_REVERSED
        else:
            colorformat = lv.COLOR_FORMAT.NATIVE
        
        self.colormode = colormode
        self.colorformat = colorformat

        self.disp_drv = lv.disp_create(width, height)
        self.disp_drv.set_flush_cb(self.flush_cb)
        self.disp_drv.set_draw_buffers(self.flush_buf1, self.flush_buf2, int(bufSize // lv.color_t.__SIZE__), lv.DISP_RENDER_MODE.PARTIAL)
        self.disp_drv.set_color_format(colorformat)
        
        if isinstance(cs, int):
            if cs == -1:
               cs = None
            else:
                cs = machine.Pin(cs, machine.Pin.OUT)

        if isinstance(dc, int):
            dc = machine.Pin(dc, machine.Pin.OUT)

        if isinstance(rst, int):
            if rst == -1:
               rst = None
            else:
                rst = machine.Pin(cs, machine.Pin.OUT)
                rst.value(1)

        if isinstance(bl, int):
            if bl == -1:
               bl = None
            else:
                bl = machine.Pin(cs, machine.Pin.OUT)

        if bl is not None and bl_pwm:
            bl = machine.PWM(bl)
            
            
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.bl = bl
        self.bl_pwm = bl_pwm

        self.rot = rot
        self.width = 0
        self.height = 0
        self.res = (width, height)

        self.rp2_dma = rp2_dma
        self.spi = spi
        self.init()
        
        # create event loop if not yet present
        #comment this section out if you do not want to use an ISR to handle the refreshing of the display
        # if not lv_utils.event_loop.is_running(): 
        #    self.event_loop=lv_utils.event_loop()

    def off(self): 
        self.set_backlight(0)

    def hard_reset(self):
        if self.rst:
            for v in (0,1):
                self.rst.value(v)
                time.sleep_ms(100)

        self.init()
        
    def set_backlight(self, percent):
        if self.bl is None: 
            return
        if self.bl_pwm:
            self.bl.duty_u16(percent * 655)
        elif percent <= 25:
            self.bl.value(0)
        else:
            self.bl.value(1)

    def blit(self, x, y, w, h, buf, is_blocking=True):
        # Memory write by DMA, disp_flush_ready when finished

        if self.rp2_dma: 
            self._rp2_write_register_dma(0x2C, buf, is_blocking)
        else: 
            self.send_cmd(0x2C, buf)


    def _rp2_write_register_dma(self, reg, buf, is_blocking=True):
        '''If *is_blocking* is False, used should call wait_dma explicitly.'''

        SPI1_BASE = 0x40040000 # FIXME: will be different for another SPI bus?
        SSPDR = 0x008
        self.rp2_dma.config(
            src_addr = uctypes.addressof(buf),
            dst_addr = SPI1_BASE + SSPDR,
            count    = len(buf),
            src_inc  = True,
            dst_inc  = False,
            trig_dreq= self.rp2_dma.DREQ_SPI1_TX
        )

        self.cmd_trans_data[0] = reg
        if self.cs:
            self.cs.value(0)

        self.dc.value(0)

        self.spi.write(self.cmd_trans_data)

        self.dc.value(1)
        self.rp2_dma.enable()

        if is_blocking: 
            self.rp2_wait_dma()

    def rp2_wait_dma(self):
        '''
        Wait for rp2-port DMA transfer to finish; no-op unless self.rp2_dma is defined.
        Can be used as callback before accessing shared SPI bus e.g. with the xpt2046 driver.
        '''
        if self.rp2_dma is None: 
            return

        while self.rp2_dma.is_busy(): 
            pass

        self.rp2_dma.disable()

        # wait to send last byte. It should take < 1uS @ 10MHz
        time.sleep_us(1)
        if self.cs:
            self.cs.value(1)
    
    def send_cmd(self, cmd, buf=None):
        if self.cs:
            self.cs.value(0)

        self.dc.value(0)
        self.cmd_trans_data[0] = cmd
        self.spi.write(self.cmd_trans_data)
        
        if buf is not None:
            self.dc.value(1)
            self.spi.write(buf)

        if self.cs:
            self.cs.value(1)

    def flush_cb(self, disp_drv, area, color):
        # print(f"({area.x1},{area.y1}..{area.x2},{area.y2})")
        self.rp2_wait_dma() # wait if not yet done and DMA is being used
        # blit in background

        # Column addresses
        x1 = area.x1
        x2 = area.x2

        self.word_trans_data[0] = (x1 >> 8) & 0xFF
        self.word_trans_data[1] = x1 & 0xFF
        self.word_trans_data[2] = (x2 >> 8) & 0xFF
        self.word_trans_data[3] = x2 & 0xFF
        self.send_cmd(0x2A, self.word_trans_data)

        # Page addresses        
        y1 = area.y1
        y2 = area.y2

        self.word_trans_data[0] = (y1 >> 8) & 0xFF
        self.word_trans_data[1] = y1 & 0xFF
        self.word_trans_data[2] = (y2 >> 8) & 0xFF
        self.word_trans_data[3] = y2 & 0xFF
        self.send_cmd(0x2B, self.word_trans_data)

        width = x2 - x1 + 1
        height = y2 - y1 + 1
        data_view = color.__dereference__(width * height * lv.color_t.__SIZE__)

        self.blit(x1, y1, width, height, data_view, is_blocking=False)

        self.disp_drv.flush_ready()

    def init(self):
        if self.rot in (-1, -3):
            self.width, self.height = self.res
        else:
            self.height, self.width = self.res

        self._run_seq(
            [
                (0x01,None, 120),
                (0x11, None, 120),
                (0xF0,b"\xc3"),
                (0xF0,b"\x96"),
                (0x36,bytes([bytes([self.madctl(self.colormode, rot, ORIENTATION_TABLE)])])),
                #(0X36,b"\x0"),
                (0x3A,b"\x05"),  # pixelformat
                (0xB4,b"\x01"),
                (0xB6,b"\x80\x02\x3B"),
                (0xE8,b"\x40\x8A\x00\x00\x29\x19\xA5\x33"),
                (0xC1,b"\x06"),
                (0xC2,b"\xA7"),
                (0xC5,b"\x18", 120),
                (0xE0,b"\xF0\x09\x0B\x06\x04\x15\x2F\x54\x42\x3C\x17\x14\x18\x1B"),
                (0xE1,b"\xE0\x09\x0B\x06\x04\x03\x2B\x43\x42\x3B\x16\x14\x17\x1B", 120),
                (0xF0,b"\x3C"),
                (0xF0,b"\x69",120),
                (0x29,b""),
            ]
        )

    def apply_rotation(self, rot):
        self.rot = rot

        if rot in (-1, -3):
            self.width, self.height = self.res
        else:
            self.height, self.width = self.res

        self.send_cmd(_MADCTL, bytes([self.madctl(self.colormode, rot, ORIENTATION_TABLE)]))
    
    def madctl(self, colormode, rotation, rotations):
        # if rotation is 0 or positive use the value as is.
        if rotation >= 0:
            return rotation | colormode

        # otherwise use abs(rotation)-1 as index to retreive value from rotations set

        index = abs(rotation) - 1
        if index > len(rotations):
            RuntimeError('Invalid display rot value specified during init.')

        return rotations[index] | colormode

    def _run_seq(self,seq):
        '''
        Run sequence of (initialization) commands; those are given as list of tuples, which are either
        `(command,data)` or `(command,data,delay_ms)`
        '''
        for i,cmd in enumerate(seq):
            if len(cmd) == 2:
              self.send_cmd(*cmd)
            elif len(cmd) == 3: 
                self.send_cmd(*cmd[:2])
                time.sleep_ms(cmd[-1])
            else: 
              raise ValueError('Command #%d has %d items (must be 2 or 3)'%(i,len(cmd)))


spi = machine.SPI(1, baudrate=27000000, sck=machine.Pin(10), mosi=machine.Pin(11), miso=machine.Pin(8))
drv = St7796(width=320, height=480, factor=8, spi=spi, dc=23, cs=29, rst=28)

scr = lv.scr_act()
scr.set_style_bg_color(lv.color_hex(0x000000), lv.PART.MAIN)

btn = lv.btn(scr)
btn.set_style_bg_color(lv.color_hex(0xFF0000), lv.PART.MAIN)


label = lv.label(btn)
label.set_text("Hello World!")

btn.set_width(120)
btn.set_height(35)

btn.center()

start_time = time.ticks_ms()
while True:
    end_time = time.ticks_ms()
    diff = time.ticks_diff(end_time, start_time)
    if diff >= 1:
        start_time = end_time
        lv.tick_inc(diff)
        lv.task_handler()

does the back lighting turn on?

yes its turning on