Documentation / Shipping it

MicroPython export

The same design as Python for lvgl_micropython on an ESP32 - what it covers, how to build it, and what it costs in flash next to the Arduino export.

Besides the Arduino project, the export dialog can write a design out as MicroPython for lvgl_micropython, the MicroPython firmware with LVGL built in. Screens, widgets, styles, events, variables, bindings and automations all come across. Nothing is compiled on your machine except the firmware, and the UI can be edited on the board and re-run from the REPL without a rebuild.

The export dialog on its MicroPython tab, with a Python screen module open.
The MicroPython tab after a build, with a screen module open.

Choosing it

The export dialog has two tabs across the top: Arduino (C++) and MicroPython. The choice is saved with the project, so a MicroPython design opens on MicroPython next time. Switching tabs checks the design again for that export. What the MicroPython export cannot build yet is listed as an error with the reason, and Generate code stays off until it is fixed - or switch back to Arduino, which can. About this export names the firmware build the design needs and anything that goes on the board as a file.

A MicroPython build is one generation, metered exactly like an Arduino one - see Accounts and limits. A design it refuses costs nothing.

What it covers

SupportedNot yet
MCUESP32, ESP32-S2, ESP32-S3, ESP32-C3STM32, RP2040
PanelSPI: ILI9341, ILI9342, ST7789, ST7796, GC9A01, GC9107, ILI9488, ILI9486, ILI9481, HX8357D, ILI9225, SSD1351RGB, parallel (8/16-bit) and QSPI panels
TouchXPT2046 (calibrated on the board), FT6236, FT6206, FT5x06, CST816S, CST820, GT911, CST328Encoder and button inputs
Board presetsESP32 + ILI9341 2.8" SPI, ESP32-2432S028R "Cheap Yellow Display", ESP32-2432S028C (capacitive CYD), WT32-SC01, M5Stack Core2, ESP32-S3 + ILI9341 SPI, ESP32-S3 + GC9A01 1.28" roundThe RGB, parallel, QSPI, STM32 and RP2040 presets
DesignEvery widget and option, every style property and state, every trigger and action, variables, formulas, bindings, automations, analog inputs, digital I/O, backlight, images, library and imported fonts, iconsPeripherals (their drivers are Arduino libraries) and I/O extenders; custom calendar day names; styles on a Ticks part, which LVGL 9 does not have

The firmware is lvgl_micropython at commit d2d26467, which is LVGL 9.4. A design drawn for LVGL 8 is exported as LVGL 9 all the same.

Every file

FileWhat it is
main.pyRuns at boot: brings the board up, mounts the S: drive fonts and images are read from, builds the UI and starts the task handler.
board.pyDisplay, touch and backlight bring-up, with the pins and bus settings at the top.
ui.pyEvery object as a module global (ui.ui_Button1), the theme, and init().
ui_<Screen>.pyOne per screen: creates its widgets, styles and events.
ui_<Screen>__part2.py ...The rest of a screen too big for one module - see below.
ui_helpers.pyWhat event code calls: screen changes and deletes, flags, increments.
ui_events.pyYours. Stubs for Call function actions.
ui_fonts.py, fonts/Fonts the firmware does not have built in, loaded from /fonts with a built-in fallback, and a UI_ICON_... string for every icon.
ui_backlight.pyLevels for Set backlight actions, and the widgets that show them.
app_vars.py, app_logic.py, hardware.py, lw_math.pyVariables (import app_vars as V), bindings and automations, analog and digital I/O, and formula arithmetic that behaves like the C export.
*.binImages in LVGL 9's binary format.
manifest.pyFreezes every module except main.py into the firmware.
README.mdThe exact firmware command for this board, what to copy, and what each file is.

Building and flashing

lvgl_micropython compiles the display and touch drivers into the firmware, so the firmware is built for the board - and that build can take the UI with it. The generated README carries the command for your board; for a CYD it is:

git clone https://github.com/lvgl-micropython/lvgl_micropython
cd lvgl_micropython
git checkout d2d26467
python3 make.py esp32 BOARD=ESP32_GENERIC DISPLAY=ili9341 INDEV=xpt2046 \
  FROZEN_MANIFEST=/full/path/to/MyUI_micropython/manifest.py deploy

It needs Linux or macOS - on Windows, WSL - with the ESP-IDF prerequisites lvgl_micropython's README lists. deploy flashes the board when the build finishes (add PORT=/dev/ttyUSB0 if it is not found). Then copy what is not frozen with mpremote:

pip install mpremote
mpremote cp main.py :
mpremote cp -r fonts :
mpremote reset

A .py copied to the board replaces the frozen module of the same name, so ui_events.py - or anything else - can change without a rebuild. The UI runs on an LVGL timer, which leaves the REPL free: mpremote repl, then import ui and poke at ui.ui_Screen1. A resistive panel asks for a few taps on the first boot and keeps the result in NVS; board.S.indev.calibrate() runs it again.

Without freezing - a board with PSRAM and a small design - build the firmware without FROZEN_MANIFEST and copy the whole folder: mpremote cp -r . :.

Sizes compared with the Arduino export

Measured on 15 September 2026 by building both exports of the same designs for the CYD (ESP32-2432S028R, 4 MB flash, no PSRAM). Arduino: LVGL 9.2.2 and TFT_eSPI, built by PlatformIO with the generated platformio.ini. MicroPython: lvgl_micropython d2d26467 (LVGL 9.4, ESP-IDF 5.5), BOARD=ESP32_GENERIC DISPLAY=ili9341 INDEV=xpt2046, with the design frozen in. KB is 1024 bytes.

Firmware and flash

DesignArduinoMicroPythonUI frozen inFiles fit (A / MP)
lvgl_micropython alone-2,804 KB-- / 1,228 KB
Hello CYD, 1 screen611 KB2,811 KB7 KB896 / 1,220 KB
Every widget, 4 screens641 KB2,836 KB33 KB896 / 1,192 KB

Arduino and MicroPython are the firmware images; UI frozen in is how much of the MicroPython image is the design; files fit is the flash filesystem left for fonts and images. The every-widget design has 397 objects.

The gap is the runtime, not your design. An Arduino build compiles only what the design uses: lv_conf.h turns on just its widgets and fonts, and the linker drops the rest. The lvgl_micropython firmware carries the MicroPython interpreter and all of LVGL 9.4 through its binding, whatever the design uses - 2.7 MB before the first screen. On top of that the UI is compact bytecode: frozen in, the Hello CYD example adds 7 KB to the firmware and every widget across four screens adds 33 KB.

Flash left over goes the other way. The Arduino export uses the Huge APP partition scheme, a fixed 3 MB for the program and 896 KB for LittleFS, so most of that 3 MB sits empty. make.py sizes lvgl_micropython's program partition to the firmware and gives the rest of the chip to the filesystem, which is where MicroPython keeps fonts and images - so a small frozen design has more room for files than the Arduino build does. A big one takes it back: our stress test, two screens with every style property on every part, is 1.06 MB of Python in 33 modules and adds 349 KB frozen, leaving 876 KB for files.

What the export itself weighs

DesignArduino: files, total, zipMicroPython: files, total, zip
Hello CYD18 files, 52 KB (UI C 10 KB), 21 KB zipped14 files, 46 KB (Python 16 KB, fonts 20 KB), 24 KB zipped
Every widget28 files, 174 KB (UI C 124 KB), 42 KB zipped16 files, 88 KB (Python 81 KB), 20 KB zipped

The Hello CYD example uses Montserrat 20 and 28. The firmware has only 12, 14 and 16 built in, so those two go on the board as uncompressed LVGL font files - 20 KB of the filesystem - where the Arduino build compiles them into the program. Images are always files on MicroPython, including ones set to compile in. An L8 greyscale image is written as RGB565, because LVGL 9.4 cannot draw L8 from a file: two bytes a pixel instead of one.

RAM, and why the UI is frozen

MicroPython compiles a .py when it is imported, and that takes about five times the file's size in free heap, all at once. An ESP32 without PSRAM has around 100 KB left once LVGL is running, so the export cuts each screen into modules of about 16 KB - ui_Screen1.py, ui_Screen1__part2.py and so on - that are imported one at a time and dropped. One widget is never split across modules, so a widget with hundreds of style overrides still makes a large one.

DesignLargest moduleFree heap needed to import itFrozen
Hello CYD3.6 KBabout 19 KBnone - it is already bytecode in flash
Every widget18 KBabout 90 KB - more than a board without PSRAM has
Every style property (stress test)76 KBabout 381 KB

That is why the README's first step builds the UI into the firmware: frozen modules run from flash and need no RAM to compile at all. The README says how much the largest module needs, and warns when it is more than a board without PSRAM has.

How it behaves differently

Something wrong?

SymptomCause and fix
MemoryError when main.py imports the UINot enough heap to compile a module. Build the firmware with FROZEN_MANIFEST, and delete any copies of the frozen .py files from the board - a file on the board wins over the frozen module.
Blank or white screenThe pins at the top of board.py; or the other display.init() type if the driver has two.
Colours inverted, or red and blue swappedset_color_inversion() and color_byte_order in board.py.
Text in the wrong fontui_fonts prints which file did not load - check fonts/ was copied.
make.py says the app partition is too smallIt resizes the partition and carries on; the build still succeeds. Add --flash-size=8 or 16 on a module with more flash.

How it is tested

The generator's test suite builds designs that between them use every widget option, style property, trigger, action, binding, variable source and automation, and every board preset, and boots each one twice: under CPython against a strict stand-in for the lvgl module that checks every call's name, arguments and types against the binding, and on lvgl_micropython's own unix port - the real LVGL 9.4 binding - after MicroPython's compiler has compiled every file. There each screen gets taps, long presses and swipes through a real input device, a minute of timers, and is deleted and rebuilt. The CYD firmware is built from the generated README command with the largest designs frozen in.