Home

Awesome

IMPORTANT READ ME FIRST

LVGL binding for Micropython


This project is a spinoff of the lv_micropython and lv_binding_micropython projects. The goal of this project is to make it easier to compile, create a common API so it is easy to add new drivers and to support more connection topologies to displays and input devices.

What is MicroPython?

MicroPython is just how it sounds. It is a micro version of Python. It is written to run on microcontrollers It has a small memory footprint and small binary size as well as provides access to the hardware related bits of a microcontroller.

What is LVGL?

LVGL is a graphics framework writtemn for C99. It is also written to run on resource constrained devices. It is a feature rich framework that provides a plethora of different controls (widgets) as well as the ability to make your owmn custom controls.

What is a binding?

A Binding is a code that encapsulates code written in one programming language so it is accessable from another programming language. It is best to think of it as a translator, in the case of this project it translates Python to C99 and vice versa. It allows us access to the LVGL code by using the Python programming language.

<br>

Important Update

I have altered how the RGBBus driver works. Due to low framerates from LVGL needing to render the whole screen each time a small change is made and LVGL also having to keep the 2 frame buffers in sync I have decided to try and bring a little bit of my coding ability to the show to see if I am able to overcome some of the performance issues.

This comes at the cost of additional memory but due to the buffers needing to reside in SPIRAM because of their size I figured what's a couple hundred K more. What I have done is this.

The 2 full frame buffers are no longer accessable to the user. These are kept tucked away in C code. The user is able to allocate partial buffers of any size they want. I have not messed about with this to see if there is a sweet spot with the size but I would imagine that 1/10 the display size is a good starting point. I am running a task on the second core of the ESP32 and in that task is where the buffer copying takes place. This is ideal because it is able to offload that work so there is no drop in performance in the user code. MicroPython only uses one core of the ESP32 and that is what makes this an ideal thing to do.

If you use 2 partial buffers while one of being copied to the full frame buffer LVGL is able to fill the other partial buffer. Another nifty thing I have done is I am handling the rotation of the screen in that task as well. This should provide much better performance than having LVGL handle the rotation.

So make sure when you are creating the frame buffers for the RGB display that you make them a fraction of the size of what they used to be.

Table of Contents

Supported display and touch hardware


Supported Display IC's

Supported Touch IC's

Special use drivers

<br>

Build Instructions


I have changed the design of the binding so it is no longer a dependancy of MicroPython. Instead MicroPython is now a dependency of the binding. By doing this I have simplified the process up updating the MicroPython version. Only small changes are now needed to support newer versions of MicroPython.

In order to make this all work I have written a Python script that handles Building the binding. The only prerequesits are that you have a C compiler installed (gcc, clang, msvc) and the necessary support libs.

<br>

Requirements


To compile you will need Python >= 3.10 for for all build types.

Compiling for ESP32:

Compiling for RP2:

Compiling for STM32:

Compiling for Ubuntu (Linux):

use apt-get install {requirements} for Ubuntu like Linux variants.

Compiling for macOS:

Compiling for Windows:

<br>

Command line syntax/parameters


python3 make.py {build target} {build options} {target options} {global options}
<br>

Build Target (required)

This is a required option and it is positional. It must be the first option provided. Choices are:

<br>

Build Options (optional)

This is a positoional argument and it must be the second one in the build command if it is used. Choices are:

<br>

Target Options (optional)

Target options is broken down into 2 sections

python3 make.py {build target} {build options} {{model/variant} {model/variant specific options}} {global options}
Model/Variant

The model is the processor model being used or the build type. the build type is what is specified when compiling to run on macOS or Unix.

When compiling for macOS or Unix you are able to specify the build type. That is done by using the VARIANT option. The syntax for this option is VARIANT={build type}

Here are the available build types:

When compiling for all others you use the BOARD option. The symntax for this option is BOARD={board name}. Some "boards" are generic and can have different variants. To specify what board variamnmt is being used you use the following option, BOARD_VARIANT={variant}.

This is a list of the boards that MicroPython supports. Some of these boards might not have enough available program storage or RAM to be able to run the binding. I will at some point look at the specs for each of them to see if they will be able to run this binding.

Any sub items listed under a board is a variant that is available for that board.

NOTE: You cannot specify a variant unless the board has been specified.

Model/Variant specific options

ESP32 options

Common options that are available across all esp32 targets:

Options specific to the ESP32-S3 processors:

Options specific to the ESP32-S2 processors:

<br>

Global Options (optional)


These are options that are available across all targets, boards and board variants. The global options are broken down into 2 secions

python3 make.py {build target} {build options} {target options} {{input/output} {other}}
<br>
Input/Output

The above options are able to be repeated if you want to include multiple drivers.

<br>
Other global options
<br>

Example build commands


Build for an ESP32-S3 processor with Octal SPIRAM and the given display and input drivers

python3 make.py esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7796 INDEV=gt911

If you have problems on builds after the first one, you can try adding the "clean" keyword to clear out residue from previous builds.

I will provide directions on how to use the driver framework and also the drivers that are included with the binding in the coming weeks.

<br>

SDL fpr Unix is working properly. Make sure you review the requirements needed to compile for unix!!! The build system compiles the latest version of SDL2 so the list is pretty long for the requirements.

To build for Unix use the following build command

python3 make.py unix DISPLAY=sdl_display INDEV=sdl_pointer

Couple of notes:

Python code examples


Unix/macOS

from micropython import const  # NOQA
import lcd_bus  # NOQA


_WIDTH = const(480)
_HEIGHT = const(320)

bus = lcd_bus.SDLBus(flags=0)

buf1 = bus.allocate_framebuffer(_WIDTH * _HEIGHT * 3, 0)

import lvgl as lv  # NOQA
import sdl_display  # NOQA


display = sdl_display.SDLDisplay(
    data_bus=bus,
    display_width=_WIDTH,
    display_height=_HEIGHT,
    frame_buffer1=buf1,
    color_space=lv.COLOR_FORMAT.RGB888
)
display.init()

import sdl_pointer
import task_handler

mouse = sdl_pointer.SDLPointer()

# the duration needs to be set to 5 to have a good response from the mouse.
# There is a thread that runs that facilitates double buffering. 
th = task_handler.TaskHandler(duration=5)

scrn = lv.screen_active()
scrn.set_style_bg_color(lv.color_hex(0x000000), 0)

slider = lv.slider(scrn)
slider.set_size(300, 25)
slider.center()

The touch screen drivers will handle the rotation that you set to the display. There is a single caviat to this. You MUST set up and initilize the display then create the touch drivers and after that has been done you can set the rotation. The touch driver must exist prior to the display rotation being set.

For the ESP32 SOC's there is NVRAM that is available to store data in. That data is persistant between restarts of the ESP32. This feature is pur to use to store calibration data for the touch screen. In the exmaple below it shows how to properly create a display driver and touch driver and how to set the rotation and also the calibration storage.

<br>

MCU


I8080 display with I2C touch input

import lcd_bus
from micropython import const

# display settings
_WIDTH = const(320)
_HEIGHT = const(480)
_BL = const(45)
_RST = const(4)
_DC = const(0)
_WR = const(47)
_FREQ = const(20000000)
_DATA0 = const(9)
_DATA1 = const(46)
_DATA2 = const(3)
_DATA3 = const(8)
_DATA4 = const(18)
_DATA5 = const(17)
_DATA6 = const(16)
_DATA7 = const(15)
_BUFFER_SIZE = const(30720)

_SCL = const(5)
_SDA = const(6)
_TP_FREQ = const(100000)

display_bus = lcd_bus.I80Bus(
    dc=_DC,
    wr=_WR,
    freq=_FREQ,
    data0=_DATA0,
    data1=_DATA1,
    data2=_DATA2,
    data3=_DATA3,
    data4=_DATA4,
    data5=_DATA5,
    data6=_DATA6,
    data7=_DATA7
)

fb1 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
fb2 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)

import st7796  # NOQA
import lvgl as lv  # NOQA

lv.init()

display = st7796.ST7796(
    data_bus=display_bus,
    frame_buffer1=fb1,
    frame_buffer2=fb2,
    display_width=_WIDTH,
    display_height=_HEIGHT,
    backlight_pin=_BL,
    # reset=_RST,
    # reset_state=st7796.STATE_LOW,
    color_space=lv.COLOR_FORMAT.RGB565,
    color_byte_order=st7796.BYTE_ORDER_BGR,
    rgb565_byte_swap=True,
)

import i2c  # NOQA
import task_handler  # NOQA
import ft6x36  # NOQA

display.init()

i2c_bus = i2c.I2C.Bus(host=0, scl=_SCL, sda=_SDA, freq=_TP_FREQ, use_locks=False)
touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=ft6x36.I2C_ADDR, reg_bits=ft6x36.BITS)

indev = ft6x36.FT6x36(touch_dev)

display.invert_colors()

if not indev.is_calibrated:
    display.set_backlight(100)
    indev.calibrate()

# you want to rotate the display after the calibration has been done in order
# to keep the corners oriented properly.
display.set_rotation(lv.DISPLAY_ROTATION._90)

display.set_backlight(100)

th = task_handler.TaskHandler()

scrn = lv.screen_active()
scrn.set_style_bg_color(lv.color_hex(0x000000), 0)

slider = lv.slider(scrn)
slider.set_size(300, 50)
slider.center()

label = lv.label(scrn)
label.set_text('HELLO WORLD!')
label.align(lv.ALIGN.CENTER, 0, -50)
<br>

SPI bus with SPI touch (same SPI bus)


import lcd_bus
from micropython import const
import machine


# display settings
_WIDTH = const(320)
_HEIGHT = const(480)
_BL = const(45)
_RST = const(4)
_DC = const(0)

_MOSI = const(11)
_MISO = const(13)
_SCK = const(12)
_HOST = const(1)  # SPI2

_LCD_CS = const(10)
_LCD_FREQ = const(80000000)

_TOUCH_CS = const(18)
_TOUCH_FREQ = const(10000000)

spi_bus = machine.SPI.Bus(
    host=_HOST,
    mosi=_MOSI,
    miso=_MISO,
    sck=_SCK
)

display_bus = lcd_bus.SPIBus(
    spi_bus=spi_bus,
    freq=_LCD_FREQ,
    dc=_DC,
    cs=_LCD_CS,
)

# we are going to let the display driver sort out the best freame buffer size and where to allocate it to.
# fb1 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
# fb2 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)

import st7796  # NOQA
import lvgl as lv  # NOQA


display = st7796.ST7796(
    data_bus=display_bus,
    display_width=_WIDTH,
    display_height=_HEIGHT,
    backlight_pin=_BL,
    color_space=lv.COLOR_FORMAT.RGB565,
    color_byte_order=st7796.BYTE_ORDER_RGB,
    rgb565_byte_swap=True,
)

import task_handler  # NOQA
import xpt2046  # NOQA

display.set_power(True)
display.init()
display.set_backlight(100)

touch_dev = machine.SPI.Device(
    spi_bus=spi_bus,
    freq=_TOUCH_FREQ,
    cs=_TOUCH_CS
)

indev = xpt2046.XPT2046(touch_dev)

th = task_handler.TaskHandler()

scrn = lv.screen_active()
scrn.set_style_bg_color(lv.color_hex(0x000000), 0)

slider = lv.slider(scrn)
slider.set_size(300, 50)
slider.center()

label = lv.label(scrn)
label.set_text('HELLO WORLD!')
label.align(lv.ALIGN.CENTER, 0, -50)

You are able to force the calibration at any time by calling indev.calibrate() regardless of what indev.is_calibrate returns. This makes it possible to redo the calibration by either using a pin that you can check the state of or through a button in your UI that you provide to the user.

Thank again and enjoy!!

<br> <br>

NOTE: SPI host 0 on the ESP32 is reserved for use with SPIRAM and flash.

<br> <br>

NOT USED AT THIS TIME


Bit orders are a tuple of durations. The first 2 numbers define a bit as 0 and the second 2 define a bit as 1. Negitive numbers are the duration to hold low and positive are for how long to hold high "Res" or "Reset" is sent at the end of the data.

NameBit 0<br/>Duration 1Bit 0<br/>Duration 2Bit 1<br/>Duration 1Bit 1<br/>Duration 2ResOrder
APA105<br/>APA109<br/>APA109<br/>SK6805<br/>SK6812<br/>SK6818300-900600-600-800GRB
WS2813300-300750-300-300GRB
APA104350-13601360-350-240RGB
SK6822350-13601360-350-500RGB
WS2812350-800700-600-5000GRB
WS2818A<br/>WS2818B<br/>WS2851<br/>WS2815B<br/>WS2815<br/>WS2811<br/>WS2814220-580580-220-280RGB
WS2818220-750750-220-300RGB
WS2816A<br/>WS2816B<br/>WS2816C200-800520-480-280GRB
WS2812B400-850800-450-5000GRB
SK6813240-800740-200-800GRB
<br>

Projects made with this Binding...


https://github.com/fabse-hack/temp_humidity_micropython_lvgl