Documentation / Hardware

Boards and displays

Every field on the Board, Display, Touch and Inputs tabs, and the tables behind them.

The Hardware tab describes the board the firmware will run on. A preset fills most of it in; this page explains every field so you can fill in a board that is not listed, or fix a preset that does not match the revision in your hand.

Verify the pinout. Vendors change pinouts between revisions of the same board and sell several variants under one name. Every preset carries a note saying what to check, and the generated README.md repeats it. Two minutes with the board's schematic before the first flash is the cheapest debugging you will do.

A board sold in two revisions

A few boards are sold under one name in two revisions on the same pins. The Waveshare ESP32-S3-Touch-AMOLED-1.8 (and the ESP32-C6 version) is one: a V1 has an SH8601 panel and an FT3168 touch chip, a V2 a CO5300 panel and a CST820 touch chip, with the picture 16 columns in. V1 has not shipped since 30 May 2026, and the back of the board is not always labelled.

Each revision has a preset of its own, and the export of either one runs on both: at start-up it asks the touch chip which revision answers and drives the panel that board has, as Waveshare's own firmware does. The Serial Monitor (115200) says which it found, for example touch: CST820 answering at 0x15 - this is the V2 board. This needs a design whose hardware is still the preset's own: if you changed its panel or touch settings, choose the preset that matches your board instead. Seen working on a V2 board.

Board

The Board tab has the preset list, filterable by brand, and below it what a preset decides first: the MCU; on an ESP32, the module's Flash and PSRAM and how its flash is divided (Partitions); and the FQBN - the fully qualified board name the Arduino CLI uses, with those settings as options, such as esp32:esp32:esp32s3:FlashSize=16M,PSRAM=opi,PartitionScheme=custom. Choosing a preset overwrites the Display, Touch and Inputs tabs, the graphics library and the module's memory; it leaves the partition layout, your buses, devices, I/O, variables and automations alone.

The Board tab with a preset chosen, its verify note under the list.
The Board tab with a preset chosen, its verify note under the list. The MCU, the module's memory and the FQBN follow it.

Flash, PSRAM and partitions

Flash and PSRAM are the code at the end of an ESP32 module's part number: ESP32-S3-WROOM-1-N16R8 has 16 MB of flash and 8 MB of PSRAM. A preset fills in its board's. PSRAM is listed as quad or octal because on an ESP32-S3 they are different builds - R2 and R4 modules are quad, R8 and R16 octal - and firmware built for the wrong kind finds no PSRAM at boot. The WT32-SC01 Plus, for one, is N16R2.

The Target MCU section for a Sunton 4.3-inch board: ESP32-S3, Flash 16 MB, PSRAM 8 MB octal, module N16R8, Partitions Big app with a 12 MB app and 3.9 MB of files, and the FQBN with those settings as options.
An N16R8 module with the Big app layout, and the FQBN that carries both.

Partitions divides the flash between the firmware and the filesystem your images and the touch calibration live on:

LayoutFirmware gets4 MB flash16 MB flash
Big appThree quarters3 MB app, 896 KB files12 MB app, 3.9 MB files
Half and halfHalf2 MB app, 1.9 MB files8 MB app, 7.9 MB files

The export writes the table as partitions.csv beside the sketch, which PlatformIO and the Arduino IDE both use, and puts the flash size and PSRAM kind in platformio.ini; its README lists the matching Arduino IDE settings. On 4 MB, Big app is the Arduino core's own Huge APP scheme and Half and half its No OTA (2MB APP/2MB SPIFFS). The image budget on the Assets tab is the filesystem partition. The MicroPython export takes the flash size and PSRAM kind, but sizes its own app partition to the firmware.

A design with a Firmware update widget is given two app partitions in place of one - the firmware running and the one arriving over the network - each half the size the layout would have given one. The filesystem is unchanged. Nothing has to be chosen: it follows from the widget being there, and going back to one is deleting it.

Presets

PresetMCUPanelResolutionTouchLibraryVerify
ESP32 + ILI9341 2.8" SPIESP32 (WROOM / WROVER)ILI9341, SPI240 × 320XPT2046TFT_eSPIWiring template rather than a fixed board - adjust to match how you actually wired it.
ESP32-2432S028R "Cheap Yellow Display"ESP32 (WROOM / WROVER)ILI9341, SPI320 × 240XPT2046TFT_eSPITouch runs on its own VSPI bus, separate from the display HSPI bus. The 2432S028 also ships in a capacitive CST820 variant. Colour order is RGB with inversion off, confirmed on hardware - if a clone shows a negative image or swaps red and blue, change them on the Display tab.
ESP32-2432S028R "Cheap Yellow Display", ST7789 versionESP32 (WROOM / WROVER)ST7789, SPI320 × 240XPT2046TFT_eSPIOften the one with two USB ports, though some have one; same pins as the original. Colour order is BGR with inversion off (a fix: the preset used to draw its colours the wrong way round). If you get a plain white screen the panel is an ILI9341: use the next row. Not seen on the glass here.
ESP32-2432S028 "Cheap Yellow Display", two USB ports, ILI9341ESP32 (WROOM / WROVER)ILI9341, SPI240 × 320XPT2046TFT_eSPIThe micro-USB and USB-C board in its ILI9341 batch, on the original CYD’s pins, stored portrait. Colour order BGR with inversion off, confirmed on a user’s board; touch was not reported on. A white screen means the ST7789 batch: use the row above. If colours look washed out, some of these boards have a gamma fault.
Guition JC2432W328R (2.8" resistive, one USB-C)ESP32 (WROOM / WROVER)ST7789, SPI320 × 240XPT2046TFT_eSPIGuition’s CYD in a clear case: backlight on GPIO 27, TF card, RGB LED, speaker and light sensor. From Guition’s own pin table; not seen on the glass. If the picture breaks up, lower the SPI speed on the Display tab.
Guition JC2432W328C (2.8" capacitive, one USB-C)ESP32 (WROOM / WROVER)ST7789, SPI320 × 240CST820TFT_eSPIThe same board with capacitive touch (CST820 on SDA 33, SCL 32, polled). Not seen on the glass.
ESP32-2432S028C (capacitive CYD)ESP32 (WROOM / WROVER)ILI9341, SPI320 × 240CST820TFT_eSPIGPIO21 drives the backlight here, which is the pin the CST820 interrupt would otherwise use, so touch is polled instead - no behaviour is lost. Colour order is RGB with inversion off, confirmed on hardware.
LCDWIKI 2.8" ESP32-32E (E32R28T)ESP32 (WROOM / WROVER)ILI9341, SPI320 × 240XPT2046TFT_eSPIThe panel, touch and backlight are on the Sunton CYD's pins. The RGB LED is GPIO 22 (red), 16 and 17, lit low; GPIO 4 is the speaker amplifier's enable, also low - on the Sunton preset those were the red LED and nothing. The SD card and the SPI socket (CS 27) share GPIO 18/19/23, and an ESP32 has no SPI controller left for them once the panel and touch have theirs, so they are not added; adding one stops touch answering. The battery reading is scaled from typical ADC figures - check it against a meter.
WT32-SC01ESP32 (WROOM / WROVER)ST7796 / ST7796S, SPI320 × 480FT6236 / FT6336ULovyanGFXMISO is not broken out - leave it at -1.
M5Stack Core2 (v1.0, AXP192)ESP32 (WROOM / WROVER)ILI9342 / ILI9342C, SPI320 × 240FT6236 / FT6336ULovyanGFXThe panel’s supply (LDO2), its reset (GPIO 4) and the backlight (DCDC3) are the AXP192’s, so the preset starts the power chip before the panel. The backlight switches on and off and does not dim. AXP192 on Arduino, ESP-IDF and MicroPython (ESPHome has no component for it). Compiled, not run on a board. A v1.1 has a different chip: use the next row.
M5Stack Core2 v1.1 (AXP2101)ESP32 (WROOM / WROVER)ILI9342C, SPI320 × 240FT6336ULovyanGFXThe same, through an AXP2101. Compiled, not run on a board.
ESP32-S3 + ILI9341 SPIESP32-S3ILI9341, SPI240 × 320XPT2046LovyanGFXWiring template - change the pins to match your build.
ESP32-S3 + GC9A01 1.28" roundESP32-S3GC9A01 / GC9A01A, SPI240 × 240CST816SGFX Library for ArduinoThe CST816S needs its RESET line wired or it will not answer on I2C.
Sunton ESP32-8048S043 (4.3" RGB)ESP32-S3ST7262, RGB800 × 480GT911GFX Library for ArduinoPSRAM must be enabled (OPI, 80 MHz) or the RGB framebuffer will not allocate.
Sunton ESP32-8048S070 (7" RGB)ESP32-S3ST7262, RGB800 × 480GT911GFX Library for ArduinoThe RGB data pin order differs from the 4.3" board. The timings are the ones ESPHome and the Sunton board definitions use for this panel (12.5 MHz, long front porches) - a picture that rolls or shifts sideways points at them. Confirm against your board revision before flashing.
WT32-SC01 PlusESP32-S3ST7796 / ST7796S, PAR8320 × 480FT6236 / FT6336ULovyanGFXCS is tied low on the board, so leave it at -1.
LilyGO T-Display-S3ESP32-S3ST7789, PAR8170 × 320CST816SGFX Library for ArduinoGPIO15 must be driven HIGH to enable the LCD power rail before init. The generated sketch does this for you.
LilyGO T-Display-S3 AMOLEDESP32-S3RM67162, QSPI240 × 536CST226SEGFX Library for ArduinoAMOLED brightness is set by a controller command, not a backlight GPIO.
Waveshare ESP32-S3-Touch-LCD-4.3ESP32-S3Generic RGB565 panel, RGB800 × 480GT911GFX Library for ArduinoTouch reset (EXIO1), backlight enable (EXIO2) and panel reset (EXIO3) sit on a CH422G expander on the I2C bus at GPIO 8/9; the generated code brings the expander up first and drives them. EXIO4 is the SD card CS and EXIO5 selects USB - both left at their defaults.
Waveshare ESP32-S3-Touch-LCD-7ESP32-S3Generic RGB565 panel, RGB800 × 480GT911GFX Library for ArduinoTouch reset (EXIO1), backlight (EXIO2), panel reset (EXIO3) and a second panel enable (EXIO6) are all on the CH422G at GPIO 8/9. ESPHome drives EXIO2 and EXIO6 together as enable pins, so if the panel is dark with the backlight apparently on, try swapping which of the two the backlight is on for your revision.
Waveshare ESP32-S3-Touch-LCD-3.5BESP32-S3AXS15231B, QSPI320 × 480AXS15231B (touch half)GFX Library for ArduinoPanel reset (EXIO1), the speaker amplifier (EXIO7) and the TF card's DAT3 (EXIO3) are on the TCA9554 at 0x20; touch, the ES8311, the AXP2101, the QMI8658 and the PCF85063 share the I2C bus on GPIO 8/7. The panel ignores the row address over QSPI, so the export draws whole frames from a canvas in PSRAM. MCLK is GPIO44, which is also the header's RXD - leave it alone while audio is on. USB-C goes straight to the S3, so the serial log needs USB CDC On Boot (the export sets it). The microphone goes into the ES8311 and comes back on GPIO 14 - switch it on under Hardware > Audio. The camera connector is wired but not driven.
LCDWIKI 3.5" ESP32-S3 (ES3C35P)ESP32-S3ST77922, QSPI320 × 480ST77922 (touch half)GFX Library for ArduinoThe ST77922 has no row/column swap and wants 4-pixel-aligned windows, so the export draws whole frames from a canvas in PSRAM, which also rotates them. Its reset is the board's RESET button, not a GPIO: after flashing other firmware, unplug it once if the screen stays dark. Touch (0x55), the ES8311 (0x18) and the P4 header share I2C on GPIO 38/39. The speaker amplifier is enabled by pulling GPIO1 low, and the WS2812 on GPIO40 is a single LED. USB-C goes straight to the S3, so the serial log needs USB CDC On Boot (the export sets it). GPIO45 and 46 on the P3 header are strapping pins. The microphone goes into the ES8311 and comes back on GPIO 16 - switch it on under Hardware > Audio.
ESP32-S3 + ST7701S 480x480 roundESP32-S3ST7701 / ST7701S, RGB480 × 480CST328GFX Library for ArduinoCS/SCK/MOSI carry the ST7701 init commands only; pixels go over the RGB bus. The init sequence is the type 1 one; a panel that stays black on a board wired like this usually wants a different type.
Guition ESP32-4848S040 (4" 480x480)ESP32-S3ST7701 / ST7701S, RGB480 × 480GT911GFX Library for ArduinoThe ST7701 init sequence is the type 9 one - this board is the reason that type exists, and the panel stays black with any other. Touch INT and RESET are not brought out, so the GT911 is polled and reset by software. GPIO0 and GPIO3 are strapping pins used as RGB data, which is safe: they are outputs only, and not driven until after boot.
STM32F407 + ILI9341 SPISTM32F4 (F401 / F407 / F411)ILI9341, SPI240 × 320XPT2046LovyanGFXNeeds the STM32duino core. SPI1 pins are remappable - check your variant.
Raspberry Pi Pico + ILI9341RP2040 (Pico / Pico W)ILI9341, SPI240 × 320XPT2046TFT_eSPIOnly 264 KB of RAM - keep the LVGL draw buffer at roughly 1/10 screen.
Pico + GC9A01 roundRP2040 (Pico / Pico W)GC9A01 / GC9A01A, SPI240 × 240noneTFT_eSPIPair it with a rotary encoder for input.

Custom / breadboard wiring starts from an ESP32-S3 with LovyanGFX and nothing assigned, for hardware you wired yourself.

Every preset also has a public page with its full pinout and what to check on your revision - see Boards.

Switching off on a battery: the power key

Some boards keep their battery connected only while the program holds a line high. On those the designer gives the board its maker's power key, on the Board tab under Power key (on to start with):

On USB the board stays on whatever it is told. Every export (Arduino, ESP-IDF, MicroPython and ESPHome) writes it, for these Waveshare boards: ESP32-S3-Touch-LCD-3.49, 1.46B, 1.69, 1.85, 2.8 and 2.8 V2; ESP32-S3-LCD-0.85 and 1.54; ESP32-C6-LCD-1.69 and C6-Touch-LCD-1.69; ESP32-S3-Touch-AMOLED-1.32 and both 2.41s; ESP32-C6-Touch-AMOLED-1.32 and 1.43.

Boards with an AXP2101 power chip (Waveshare's AMOLED 1.75, 1.8, 2.06 and 2.16 among them) need none of this: the chip switches the board off itself when PWR is held for about six seconds, and on with a press, whatever program runs.

Testing a board

Test my board, on the Board tab and in the Help menu, opens a known-good test project for the board the design is on: colour squares that name themselves, a frame on the edge of the glass, keys that say which was tapped, the backlight on a ring, and a line for each part the board has. Generate it and flash it like any design, and in a minute you know whether a dark or wrong screen is the board or the design. On a preset it is made from the preset as it ships; on Custom wiring, or a board under My boards, from the design's own wiring. It holds nothing that is part of Pro. What each screen shows is on the troubleshooting page, and in the README of the test's code.

Your own boards

A board you designed yourself is in no preset list, so its pins have to be set by hand. With My boards you set them once. Describe the board on the Hardware tabs as usual, starting from Custom / breadboard wiring or from the preset nearest to it. Then open My boards on the Board tab, choose Save this hardware as a board, and give it a name. The note is for you, for the revision or where the schematic is kept. The board is then a card under My boards, on the Board tab and in the board step of File > New project, and choosing it puts that hardware on a design as a preset does.

My boards on the Board tab with two saved boards. The first is selected, and a line under the list reads From your board: Bench PCB rev A.
Two saved boards, the first in use. The gear beside a card holds Update, Rename, Export and Delete.

A saved board holds the MCU, the module's flash, PSRAM and partition layout, the graphics library, the panel (controller, bus, pins, bus settings, size, rotation, colour order, offsets and corner radius), touch and the backlight. Include the devices on it is ticked to begin with, and adds the buses, I/O extenders, devices, analog and digital I/O, encoders and buttons, and the sound wiring. Sound arrives switched off in a new design, as it does with a preset. Screens, widgets, variables and automations belong to a design and are never part of a board.

Saving runs the checks the Check tab runs. Hardware with an error is not saved, and the dialog lists each error in the Check tab's own words: a pin the chip does not have, a pin wired to the module's flash, two signals on one GPIO. Warnings do not stop a save.

The Save this hardware as a board dialog: a name, a note, a summary of the microcontroller, module, panel and devices, and the Include the devices on it box, ticked.
Saving a board. The summary is read from the open design.

Using a board copies its hardware into the design. Devices you added to the design yourself stay, and the devices of the board it was on before leave, as when you change preset. The line under the list then reads From your board. After that the design and the board are separate, and the Board tab says when they differ:

A design made from one of your boards exports exactly the files the same pins set by hand export. Its README and sketch call it custom wiring. The board's name is kept in your account and not in the design, so it does not go with a design you publish to the gallery or attach to a fault report.

The gear beside a card has four entries. Update from this hardware replaces the board with the open design's hardware. Rename, or edit its note changes neither pins nor devices. Export to a file writes <name>.lwboard.json, to keep a copy or to hand the board to a colleague. Delete asks first, and leaves the designs made from the board as they are. Import a board file, under the list, reads an exported file and checks it exactly as a save is checked.

A name is used once per account, whatever its capitals. Saving under a name you already use asks whether to update that board. An account holds up to 30 boards. A board the project's target cannot build for is greyed and says why, as a preset is: an STM32 board in a MicroPython project, for one. Your boards are offered to nobody else and do not appear on the public Boards pages.

Part of Pro. Saving, importing, updating and renaming your own boards is part of Pro. A board already in your account can be used, exported and deleted on any plan, and a design made from one opens and exports on any plan.

MCUs

The MCU decides which GPIOs exist, which are input-only, which carry warnings (flash, strapping, UART), which have ADC channels, whether PSRAM is possible, and which graphics libraries and buses are available.

MCUPSRAMInput-only pinsNotes
ESP32 (WROOM / WROVER)yes34, 35, 36, 37, 38, 39No native RGB parallel peripheral - use SPI or an 8/16-bit i80 bus.
ESP32-S3yes-The only common Arduino target with a hardware RGB565 LCD peripheral. PSRAM is effectively required for RGB panels.
ESP32-S2yes46Single core - keep lv_timer_handler() off the same loop as heavy work.
ESP32-C3no-No PSRAM. Keep the draw buffer small and prefer displays up to 320x240.
ESP32-C6no-No PSRAM. SPI displays only in practice.
ESP32-P4yes-Has a MIPI-DSI host and a 2D graphics accelerator (PPA). Arduino core support is newer - verify your core version.
STM32F4 (F401 / F407 / F411)no-Use the STM32duino core. FSMC gives a fast parallel bus on F407 and up.
STM32F7no-Has an LTDC controller for true RGB panels plus DMA2D (Chrom-ART) acceleration.
STM32H7no-The strongest Arduino-compatible target for large panels. LTDC + DMA2D + external SDRAM.
RP2040 (Pico / Pico W)no-PIO can drive an 8-bit parallel bus. Limited RAM - use a small partial draw buffer.
RP2350 (Pico 2)yes-More RAM than RP2040 and optional PSRAM on boards such as the Pimoroni Pico Plus 2.

Display

The Display tab: the panel Controller, the Bus, the pins for that bus, the Width and Height, Rotation (0, 90, 180 or 270), Colour depth, Colour order (RGB or BGR - the fix for red and blue being swapped), Invert (the fix for a negative image), and the bus options - the SPI clock in MHz, the SPI mode, and 3-wire for a 9-bit serial panel with no DC pin. The validator checks the resolution against what the controller documents and warns when it is not one of them.

The Display tab: controller and bus, then geometry, rotation and colour, then the pin list. The Backlight section follows further down.
The Display tab: controller and bus, then geometry, rotation and colour, then the pin list. The Backlight section follows further down.

Buses and their pins

BusPins
SPISCK, MOSI, MISO (optional), CS, DC, RST (optional)
QSPISCK, D0, D1, D2, D3, CS, RST (optional)
PAR8WR, RD (optional), DC, CS, RST (optional), D0, D1, D2, D3, D4, D5, D6, D7
PAR16WR, RD (optional), DC, CS, RST (optional), D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14, D15
RGBDE, VSYNC, HSYNC, PCLK, R0, R1, R2, R3, R4, G0, G1, G2, G3, G4, G5, B0, B1, B2, B3, B4, CS (optional), SCK (optional), MOSI (optional), RST (optional)
DSIRST (optional), SDA (optional), SCL (optional)
I2CSDA, SCL, RST (optional)

Set a pin to -1 when the board does not wire it (a display MISO that is not broken out, a CS tied low). The validator treats optional pins as optional and complains about required ones.

Control lines on an I/O expander

On a growing number of boards the panel reset, the backlight enable and the touch reset are not wired to GPIOs at all - they sit behind an I/O expander on I2C. The Waveshare ESP32-S3-Touch-LCD-4.3 puts all three on a CH422G. For those, the RESET pin of the display and of the touch controller, and the backlight pin, can be set to a pin on one of the project's I/O extenders instead of a GPIO: the pin picker gains an On an I/O extender group once an extender exists on the I/O tab. The generated sketch then brings the expander's bus and the expander up first (hw_init_early()), pulses the resets through it, and drives the backlight through it - on or off, since an expander pin cannot PWM. The pin header records where each line went (#define LCD_RST -1 // RESET - on CH422G pin 3) and the README wiring table says CH422G pin 3 rather than a GPIO.

Presets bring their own kit

Some presets add more than pins. Choosing the Waveshare 4.3" adds the I2C bus on GPIO 8/9 and the CH422G on it, with the three control lines already routed; the two Cheap Yellow Displays add their CN1 connector as an I2C bus on GPIO 27/22; the M5Stack Core2 adds Port A on 32/33. These items are tagged with the board they belong to: they survive re-applying the same preset, are dropped when you choose a different board, and never touch buses or devices you added yourself. Add a sensor to a board bus exactly as you would to one of your own.

A kit can be more than a bus. Waveshare’s 4.3" and 7" boards arrive with their TF card slot, RS485 line, CAN bus and sensor input as well, each with a line on its card saying what is particular to it on that board; those two have a page of their own, which the Board tab links to when either is chosen. A part a target cannot drive is left out of the kit for that target, and the export’s README says which and why.

Backlight

Pin, Active level, and PWM dimming, which on an ESP32 uses an LEDC channel at the PWM frequency you set (analogWrite on RP2040 and STM32, LovyanGFX's own light driver on LovyanGFX boards), starting at the Default %. Without PWM the pin is simply driven to the active level. To let people change the brightness on the device, give a slider or a pair of buttons a Set backlight event; the preview dims the whole screen to match. The level is not saved, so every boot starts at the Default %. The sketch starts the backlight after the panel, because TFT_eSPI's begin() takes a PWM pin back for itself, and exposes backlight_set(percent) and backlight_get() for your own code.

An AMOLED has no backlight pin. The panel is its own light, and how bright it is is a command to its controller. On such a board the section says so and has one setting, Default %, and a Set backlight event works as it does anywhere else: a slider or a pair of buttons changes the panel's brightness, the preview dims with it, and backlight_set(percent) sends the command. That is so in all three exports that drive the board - in ESPHome the panel becomes a light called Backlight. Leave the pin unused unless the board really has a backlight line as well.

A backlight on an I/O extender's own PWM channel (Waveshare's IO extension has one) dims like a pin with PWM; on an ordinary extender pin it switches on and off.

A backlight that is a device on I2C. Some screens set their backlight as a register of a small chip on the I2C bus rather than on a pin: Waveshare's DSI-TOUCH-A screens for the ESP32-P4-NANO use register 0x96 of a chip at 0x45, written 0 to 255. Set Driven by to A device on I2C, and give the bus (the touch panel's own, or one of the design's I2C buses), the address, the register and what is written for 100 %. Start-up writes are sent once, in order, before the panel starts - on those Waveshare screens the same chip switches the panel's supplies and its reset, so their four writes are what bring the panel up at all. The presets for those screens fill all of it in. A Set backlight event dims it as it would a pin, and the serial monitor says if the chip did not answer. Arduino export only.

The Backlight section of the Display tab: pin, active level, PWM dimming, its frequency and the level every boot starts at.
The Backlight section of the Display tab: pin, active level, PWM dimming, its frequency and the level every boot starts at.

Screen off when idle

Below the Backlight section, Screen off when idle puts the light out when nothing has happened for a while, the way a watch or a phone does. Tick Switch off and give it Off after, in seconds. Dim first turns it down to Dim to % after Dim after seconds, before it goes off. Nothing happening means no touch, no turn of an encoder and no press of a key, which is LVGL's own count of inactivity. Two more things can count:

The touch that wakes a dark screen presses nothing. It is kept from the screen until the finger lifts, so the button under it is not pressed. A finger already down when the screen went out does not wake it until it has lifted and pressed again. A dimmed screen can still be seen, so a touch on one goes through to what it touches and lights the screen again.

The light comes back at the level it had. A Set backlight while the screen is dark or dimmed sets the level it comes back at. LVGL goes on running while the screen is dark - timers, bindings, drawing - so what lights up is current, and nothing is skipped.

The same works with the timeout off. Wake the screen and Screen off now are actions for any event, a timer or an encoder. Screen went off and Screen woke are triggers for an automation. Screen is on and Seconds with nothing happening are sources for a variable. In Preview the timeout runs: the panel dims and goes dark after its times, a click on a dark panel wakes it without pressing anything, and the data drawer has Move the board and Press the wake button for the motion and the button a browser does not have.

What goes dark. Whatever the board's backlight is: a GPIO with PWM goes to 0 % and back, a pin that only switches is switched (it cannot dim, so Dim first leaves it lit until it goes off), a line of an I/O expander is switched, Waveshare's IO extension's PWM channel goes to 0 %, a register on I2C is written 0, and an AMOLED is sent a brightness of 0. A board whose light cannot be switched - no backlight pin, no power-chip line and no brightness command - greys the section and says why.

Each export. Arduino, MicroPython and ESP-IDF write all of it. On ESP-IDF an AMOLED is greyed: that export does not send an AMOLED its brightness command yet. On Arduino an AMOLED is sent it through GFX Library for Arduino, so choose that library on the Board tab. ESPHome has its own idle handling and the export uses it as ESPHome has it: on_idle puts the light out and lvgl.pause stops drawing, a touch resumes it (ESPHome's resume_on_input, on by default, which also keeps that touch from the widgets), and Screen went off and Screen woke are on_pause and on_resume. ESPHome's own defaults stand: its light fades over a second, and it stops drawing while the screen is dark. Dimming first, motion, the wake button and the two variables have nothing of ESPHome's to be written as, so on ESPHome they are greyed and the export says it leaves them out.

Panel controllers

Forty, across SPI, QSPI, 8- and 16-bit parallel, RGB565 and MIPI-DSI. Each entry knows which buses it supports, the resolutions it is sold in, and the driver symbol every graphics library expects for it.

MIPI-DSI is the ESP32-P4's bus, and the Arduino export drives it through GFX Library for Arduino: the JD9365 (800×1280, Waveshare's 10.1" panel among them), the EK79007 (1024×600, Waveshare's ESP32-P4-WIFI6-Touch-LCD-7B and Elecrow's CrowPanel Advanced P4) and the JD9165 (1024×600, VIEWE's 7" ESP32-P4 SmartDisplay and Guition's JC1060P470C, whose panel makers' own start-up tables the export writes into the sketch). Guition's board changed its panel in 2026: one whose label says V2 takes the V2 sequence its preset starts with, an older one the library's table. It needs PSRAM, two data lanes and a start-up sequence that suits the glass, which the Display tab lets you choose. These panels are compiled and have not yet been seen on a board by us; the ILI9881C is listed and is not supported yet, and the designer says so where you would pick it. The MicroPython and ESPHome exports do not write MIPI-DSI panels yet.

ControllerBusesDocumented resolutions
ILI9341SPI, PAR8, PAR16240×320, 320×240
ILI9341 (alt init)SPI, PAR8240×320
ILI9342 / ILI9342CSPI320×240
ILI9481SPI, PAR8, PAR16320×480
ILI9486SPI, PAR8, PAR16320×480
ILI9488SPI, PAR8, PAR16320×480
ST7735SPI128×160, 128×128, 80×160
ST7735SSPI128×160, 80×160
ST7789SPI, PAR8240×320, 240×240, 135×240, 172×320
ST7796 / ST7796SSPI, PAR8, PAR16320×480
GC9A01 / GC9A01ASPI240×240
GC9107SPI128×128
HX8357DSPI, PAR8320×480
ILI9225SPI176×220
R61581PAR8, PAR16320×480
NT35510PAR8, PAR16480×800
SSD1963PAR8, PAR16480×272, 800×480, 800×600
RA8875SPI, PAR8480×272, 800×480
SSD1351SPI128×128, 128×96
SSD1331SPI96×64
SSD1306I2C, SPI128×64, 128×32
SH1106I2C, SPI128×64
ST77916QSPI360×360, 400×400
SH8601QSPI368×448, 390×390, 466×466
CO5300QSPI466×466, 410×502, 280×456, 368×448, 480×480
RM690B0QSPI450×600
RM67162QSPI, SPI240×536
NV3041AQSPI480×272
AXS15231BQSPI320×480, 180×640
Generic RGB565 panelRGB800×480, 480×272, 1024×600, 480×480
ST7262RGB800×480
EK9716BD3 / EK73002RGB800×480
ST7701 / ST7701SRGB480×480, 480×800, 368×448
GC9503VRGB480×480
EK79007DSI1024×600
JD9165DSI1024×600
ILI9881CDSI800×1280
JD9365DSI800×1280

Graphics library

The library that drives the panel and gives LVGL its flush callback. Which ones are offered depends on the MCU and the bus:

LibraryBusesMCUsNotes
LovyanGFXSPI, PAR8, PAR16, RGB, I2Cesp32, esp32s3, esp32s2, esp32c3, esp32c6, rp2040, rp2350, stm32f4, stm32f7, stm32h7Widest panel and bus coverage, DMA on ESP32. Configured with a C++ config class.
TFT_eSPISPI, PAR8, PAR16esp32, esp32s3, esp32s2, rp2040, rp2350, stm32f4, stm32f7Fastest for SPI panels on ESP32. Configured via User_Setup.h build defines. On an ESP32-S3 the export always sets USE_HSPI_PORT: the library’s own choice of SPI port resets the board in a loop on Arduino core 3. It is not offered on an ESP32-C3, where the same fault has no setting to fix it.
GFX Library for ArduinoSPI, QSPI, PAR8, PAR16, RGB, I2Cesp32, esp32s3, esp32s2, esp32c3, esp32c6, rp2040, rp2350Moshi Liu's library. Best QSPI/AMOLED and RGB coverage. Configured in code.
esp_lcd (ESP-IDF component)SPI, QSPI, PAR8, PAR16, RGB, DSI, I2Cesp32, esp32s3, esp32s2, esp32c3, esp32c6, esp32p4The vendor driver bundled with the ESP32 Arduino core. Lowest overhead, ESP32 only.

TFT_eSPI is configured by a generated User_Setup.h (and the same defines in platformio.ini); LovyanGFX by a generated LGFX_Config.hpp device class; GFX Library for Arduino and esp_lcd in the sketch itself.

Touch

The Touch tab: the Controller, then the pins and options for its bus. An I2C controller takes SDA, SCL, an optional INT and an optional RESET, its I2C address, the I2C clock and whether it shares the display's I2C bus; an SPI controller takes SCK, MOSI, MISO, CS and an optional IRQ, the SPI clock and whether it shares the display's SPI bus (on by default, and right for most boards). Sharing is what lets the validator accept the same SCK on both without flagging a collision.

Below that, Swap X/Y, Invert X and Invert Y describe how the touch layer is bonded to the glass, in the panel's own axes; the firmware then turns each press to follow the display's Rotation by itself. For a capacitive controller they are the whole answer - flip them until a press lands where you touch. For a resistive controller a Calibration block follows, with Calibrate touch on the board (on by default) and the raw minimum and maximum in each axis. With calibration on, the range and the three switches are only the starting point: the board measures all of them from your taps, as below.

Calibrating on the board

A resistive panel read with generic numbers lands tens of pixels off, so by default the firmware calibrates itself. The first time the board starts it shows a cross near each corner; touch the centre of each one. The four readings are fitted together, so one slightly off tap nudges the result rather than skewing it.

Nothing about the touch layer is taken on trust. The fit tries both ways of pairing the controller's two channels with the panel's two axes and keeps the one both axes follow, and the direction of each axis falls out of the same fit - so a touch layer bonded swapped or mirrored calibrates correctly whatever the Swap and Invert switches say. Every tap is then pushed back through the same code the running firmware reads touch with, and has to land within 30 pixels of its cross.

The screen says how it went. Touch calibrated shows which way round the axes turned out to be and that the file was saved. Calibration failed says why - the readings did not follow the crosses, or a tap landed too far off - and a tap tries again; if nobody taps for 15 seconds the board carries on with its previous values and tries again next boot. A calibration that worked but could not be written to flash says so too.

The Calibration section of the Touch tab for a resistive panel: calibrate on the board, what it does, and the starting range.
The Calibration section of the Touch tab for a resistive panel: calibrate on the board, what it does, and the starting range.

The result is kept in /touch_cal.txt on the flash filesystem. That file always exists - the export includes it as data/touch_cal.txt with the numbers from the Calibration block, and the board writes it with those numbers if it is missing - and reads like this:

touch_cal_ran=1
firmware=3f9c1e...
swap_xy=1
x_min=212
x_max=3871
y_min=3702
y_max=355

swap_xy=1 means the controller's X channel runs along the panel's Y axis, and a min above its max - y above - means that axis runs backwards.

It runs again after every new firmware upload. The file records a hash of the firmware it was made with; flashing different firmware changes that hash, so the next boot calibrates afresh. To recalibrate without reflashing, hold a finger on the screen for a second while powering on, or set touch_cal_ran=0 in the file.

Open the serial monitor at 115200 to watch it: why it ran, the raw x and y of every tap, both fits, the one it chose, where each tap lands afterwards, and the file as it was saved.

It is on for every resistive board and can be switched off on the Touch tab, which then fixes the Calibration block's range into the firmware. On LovyanGFX boards the library draws its own corner markers and the file holds its eight calibration values instead. Two limits: the file needs a flash filesystem, which is only set up for the ESP32 family so far - on the RP2040 and STM32 boards the range is used as it stands - and on boards whose image assets already live on flash it shares that filesystem and never formats it.

The Touch tab for the Cheap Yellow Display's XPT2046: its SPI pins and clock, then the axis switches, with Swap X/Y set by the preset.
The Touch tab for the Cheap Yellow Display's XPT2046: its SPI pins and clock, then the axis switches, with Swap X/Y set by the preset.

Most capacitive controllers are read by generated register code over Wire and need no library; the ones that do list it in LIBRARIES.md. All of the touch code - the reads, the recovery below, the calibration and the LVGL input device - is generated into touch.cpp, beside the sketch rather than inside it.

When a touch controller stops answering

A touch controller is not reset when the MCU reboots, so one that has wedged - interrupted mid-transfer by an upload, or browned out - would otherwise stay dead until the power is removed. The firmware resets it on every boot: it pulses RESET (on a GT911 with the INT pin held to pick its I2C address, which it otherwise chooses at random), clocks the bus free if the controller is holding SDA low, and prints whether it answered. While running, twenty failed reads in a row - about half a second - reset it again, at most every three seconds. A CST816S or CST820 is told not to fall asleep, since asleep it stops answering on I2C until touched. On LovyanGFX boards the library does the reset from the same RESET pin.

An XPT2046 has no reset pin and remembers its last mode through a reboot, including one with its pen interrupt switched off - so the firmware always leaves it switched on, sets it again at boot, and re-arms it twice a second while nothing is pressed.

An I2C touch controller owns Wire. A bus on the Buses tab with the same pins shares it; one on other pins - the capacitive CYD's CN1 connector, say - starts at Wire1.

ControllerBusLibrary
GT911I2CTAMC_GT911
GT9271I2Cbb_captouch
FT6206I2CAdafruit_FT6206
FT6236 / FT6336UI2CFT6236
FT5206 / FT5406 / FT5x06I2Cbb_captouch
CST816SI2CCST816S
CST820I2Cbb_captouch
CST226SEI2Cbb_captouch
CST328I2Cbb_captouch
TT21100I2Cesp_lcd_touch_tt21100
CHSC6540I2Cbb_captouch
AXS15231B (touch half)I2Cbb_captouch
FT3267I2Cbb_captouch
FT3168 / FT6146I2Cnone: read by the sketch
CST9217 / CST9220I2Cnone: read by the sketch
XPT2046SPIXPT2046_Touchscreen
ADS7846SPIXPT2046_Touchscreen
STMPE610SPIAdafruit_STMPE610
NS2009I2Cesp_lcd_touch_ns2009
TSC2007I2CAdafruit_TSC2007

Inputs

The Inputs tab adds LVGL input devices that are not touch:

How pins are checked

Every assigned pin is checked against the MCU on every edit, and the result appears in the Check tab, the Errors tab and the export dialog:

Rendering settings

In the export settings (Export > Export UI code > settings): Buffer height is the partial draw buffer in lines - 40 by default, and the first thing to lower on a small MCU; Double buffer allocates two so LVGL can render one while the other flushes; Buffer in PSRAM puts them in external RAM on boards that have it. The sketch reports draw buffer allocation failed - lower DRAW_BUF_LINES on the serial monitor if it cannot get the memory.