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.

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
| Supported | Not yet | |
|---|---|---|
| MCU | ESP32, ESP32-S2, ESP32-S3, ESP32-C3 | STM32, RP2040 |
| Panel | SPI: ILI9341, ILI9342, ST7789, ST7796, GC9A01, GC9107, ILI9488, ILI9486, ILI9481, HX8357D, ILI9225, SSD1351 | RGB, parallel (8/16-bit) and QSPI panels |
| Touch | XPT2046 (calibrated on the board), FT6236, FT6206, FT5x06, CST816S, CST820, GT911, CST328 | Encoder and button inputs |
| Board presets | ESP32 + 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" round | The RGB, parallel, QSPI, STM32 and RP2040 presets |
| Design | Every 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, icons | Peripherals (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
| File | What it is |
|---|---|
main.py | Runs 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.py | Display, touch and backlight bring-up, with the pins and bus settings at the top. |
ui.py | Every object as a module global (ui.ui_Button1), the theme, and init(). |
ui_<Screen>.py | One 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.py | What event code calls: screen changes and deletes, flags, increments. |
ui_events.py | Yours. 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.py | Levels for Set backlight actions, and the widgets that show them. |
app_vars.py, app_logic.py, hardware.py, lw_math.py | Variables (import app_vars as V), bindings and automations, analog and digital I/O, and formula arithmetic that behaves like the C export. |
*.bin | Images in LVGL 9's binary format. |
manifest.py | Freezes every module except main.py into the firmware. |
README.md | The 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
| Design | Arduino | MicroPython | UI frozen in | Files fit (A / MP) |
|---|---|---|---|---|
| lvgl_micropython alone | - | 2,804 KB | - | - / 1,228 KB |
| Hello CYD, 1 screen | 611 KB | 2,811 KB | 7 KB | 896 / 1,220 KB |
| Every widget, 4 screens | 641 KB | 2,836 KB | 33 KB | 896 / 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
| Design | Arduino: files, total, zip | MicroPython: files, total, zip |
|---|---|---|
| Hello CYD | 18 files, 52 KB (UI C 10 KB), 21 KB zipped | 14 files, 46 KB (Python 16 KB, fonts 20 KB), 24 KB zipped |
| Every widget | 28 files, 174 KB (UI C 124 KB), 42 KB zipped | 16 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.
| Design | Largest module | Free heap needed to import it | Frozen |
|---|---|---|---|
| Hello CYD | 3.6 KB | about 19 KB | none - it is already bytecode in flash |
| Every widget | 18 KB | about 90 KB - more than a board without PSRAM has | |
| Every style property (stress test) | 76 KB | about 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
- Objects live on the
uimodule, so an event on one screen reaches widgets on another. A deleted screen puts its names back toNone, and an action aimed at one is skipped rather than touching freed memory. - Deleting a screen that is on the display, or being loaded or animated away, is refused and printed on the REPL. LVGL would otherwise keep drawing freed memory and crash.
- Formulas follow the C export, not Python: dividing by zero gives infinity, a bad
reading never raises, integers are clamped to 32 bits and
%dformats truncate. - An exception in your code stops the task handler, with a traceback naming the file and line - the screen freezes where a C sketch would carry on.
Something wrong?
| Symptom | Cause and fix |
|---|---|
MemoryError when main.py imports the UI | Not 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 screen | The pins at the top of board.py; or the other display.init() type if the driver has two. |
| Colours inverted, or red and blue swapped | set_color_inversion() and color_byte_order in board.py. |
| Text in the wrong font | ui_fonts prints which file did not load - check fonts/ was copied. |
make.py says the app partition is too small | It 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.