An ESP-IDF project can include esp_lcd_panel_io.h successfully and still fail on esp_lcd_i80_bus_config_t or esp_lcd_new_i80_bus(). That combination looks contradictory until the build target is taken into account.

esp_lcd is a framework, not a promise that every ESP chip exposes every LCD transport. The selected SoC determines which low-level interfaces are compiled into the project. Panel-controller support, such as ILI9341, is another layer again.

That distinction matters when moving code between ESP32 variants.

The compiler error is often a capability signal

A typical failure looks like this:

error: unknown type name 'esp_lcd_i80_bus_config_t'
error: implicit declaration of function 'esp_lcd_new_i80_bus'
error: unknown type name 'esp_lcd_panel_io_i80_config_t'
error: implicit declaration of function 'esp_lcd_new_panel_io_i80'

The first useful diagnostic is not the source line. It is the compiler command around it.

For example, paths containing:

components/esp_hw_support/port/esp32c6/
components/hal/esp32c6/

show that CMake configured the project for ESP32-C6. The same source configured for another target can expose a different set of LCD interfaces.

ESP-IDF describes hardware support through SoC capability macros. A target with native Intel 8080 LCD support exposes the native I80 path; a target without that capability does not gain it merely because the generic esp_lcd headers are available.

Include the transport-specific header

There is a second issue that can produce a similar-looking error: including only the generic panel headers.

The native I80 API has its own header:

#include "esp_lcd_io_i80.h"
#include "esp_lcd_panel_ops.h"

That header declares the native I80 bus configuration and functions such as:

esp_lcd_i80_bus_config_t
esp_lcd_new_i80_bus()
esp_lcd_panel_io_i80_config_t
esp_lcd_new_panel_io_i80()

A correct include does not override the SoC boundary, though. Both conditions must hold: the code must include the appropriate API header, and the selected target must support that API.

This is why manually adding include directories is the wrong fix. If a declaration is intentionally excluded for the target, forcing a header path only moves the failure elsewhere.

The panel driver and transport are separate layers

ILI9341 introduces another common source of confusion.

The esp_lcd component is part of ESP-IDF. It provides the common panel abstraction and supported transport implementations. It is not installed from the ESP Component Registry as espressif/esp_lcd.

The ILI9341 controller driver is distributed separately as the espressif/esp_lcd_ili9341 component. A project can add it with the Component Manager:

idf.py add-dependency "espressif/esp_lcd_ili9341"

The application then includes its driver header:

#include "esp_lcd_ili9341.h"

That header provides esp_lcd_new_panel_ili9341().

The architecture is easier to reason about as three layers:

application / LVGL
        |
        v
ILI9341 panel driver
        |
        v
panel IO transport
(SPI, native I80, or another supported transport)
        |
        v
ESP SoC peripheral

Installing the ILI9341 component solves a missing panel-driver declaration. It does not create a native I80 peripheral on a chip that lacks one.

Native I80 is not the only way to drive an I80 panel

On chips with native I80 support, ESP-IDF uses esp_lcd_new_i80_bus() followed by esp_lcd_new_panel_io_i80(). The bus can use 8 or 16 data lines, and the panel IO then carries commands and pixel data to the controller.

A minimal bus configuration has this shape:

esp_lcd_i80_bus_config_t bus_config = {
    .clk_src = LCD_CLK_SRC_DEFAULT,
    .dc_gpio_num = PIN_DC,
    .wr_gpio_num = PIN_WR,
    .data_gpio_nums = {
        PIN_D0, PIN_D1, PIN_D2, PIN_D3,
        PIN_D4, PIN_D5, PIN_D6, PIN_D7,
    },
    .bus_width = 8,
    .max_transfer_bytes = 240 * 40 * sizeof(uint16_t),
};

Using LCD_CLK_SRC_DEFAULT is preferable to copying a clock-source constant from an example for another SoC. Available clock sources are target-dependent too.

Some newer ESP chips without a native I80 LCD peripheral have a Parallel IO peripheral. Current ESP-IDF documentation describes an esp_lcd Parlio transport that can simulate an 8-bit I80 interface on supported targets. Its API is different:

#include "esp_lcd_io_parl.h"

esp_lcd_panel_io_parl_config_t io_config = {
    .dc_gpio_num = PIN_DC,
    .clk_gpio_num = PIN_WR,
    .cs_gpio_num = PIN_CS,
    .data_gpio_nums = {
        PIN_D0, PIN_D1, PIN_D2, PIN_D3,
        PIN_D4, PIN_D5, PIN_D6, PIN_D7,
    },
    .data_width = 8,
    .pclk_hz = 10 * 1000 * 1000,
    .lcd_cmd_bits = 8,
    .lcd_param_bits = 8,
};

This is not the same API as native I80. Code written around esp_lcd_i80_bus_config_t therefore cannot be made portable to a Parlio-only target by changing GPIO numbers.

ESP-IDF version also matters because transport support evolves. Check the documentation for the exact target and IDF release used by the project rather than assuming an example for ESP32-S3 also applies to ESP32-C6.

The build target should be checked before pin mapping

GPIO mapping is usually discussed first with parallel displays because an 8-bit bus consumes many pins. In practice, target selection should come first.

Check it explicitly:

idf.py set-target esp32s3

or:

idf.py set-target esp32c6

Then rebuild from the configured target:

idf.py reconfigure
idf.py build

In VS Code, the ESP-IDF extension performs the same target configuration through its target-selection command. The generated compiler invocation is the final authority: if it contains esp32c6, the project is being compiled as ESP32-C6 regardless of which development board was intended.

Only after that should D0-D7, WR, DC, CS, reset, and backlight pins be assigned.

LVGL sits above the transport decision

LVGL does not require native I80. It needs a display flush path that can transfer rendered pixel buffers to the panel.

With esp_lcd, the useful boundary is normally esp_lcd_panel_draw_bitmap(). The panel handle hides the controller-specific command sequence, while the panel IO underneath determines whether the bytes travel through SPI, I80, or another supported interface.

That separation means an LVGL UI should not be tightly coupled to esp_lcd_new_i80_bus(). Keep bus creation and panel initialization in the display backend, then expose the initialized panel to the LVGL port.

The result is easier to move between an ESP32-S3 using native I80 and another target using SPI or a supported Parlio path.

Read missing declarations from the bottom up

When an LCD build fails, the order of investigation saves a lot of time:

  1. Confirm the actual ESP-IDF target in the build output.
  2. Check whether that SoC supports the intended LCD transport.
  3. Include the transport-specific header rather than only generic panel headers.
  4. Distinguish the built-in esp_lcd framework from an external panel-controller component.
  5. Add espressif/esp_lcd_ili9341 only when the ILI9341 driver itself is required.
  6. Configure GPIOs after the transport is known to exist for that target.

An unknown esp_lcd_i80_bus_config_t is therefore more useful than it first appears. It tells you to inspect the boundary between generic LCD code and target-specific hardware support, not to keep adding include paths until the compiler becomes quiet.