Color rendering differences between Arduino and ESP‑IDF on the same CYD style board

Hi everyone,

I’m debugging a color rendering issue when moving an LVGL project from Arduino/PlatformIO to ESP‑IDF on the same hardware (Cheap Yellow Display‑style ESP32 + ILI9341 + XPT2046).

I’ve attached a comparison image with 8 photos:

  • Left column of each pair: Arduino/PlatformIO build
  • Right column of each pair: ESP‑IDF build

You can see 4 pages:

  1. Solid RGB bars
  2. R‑G‑B horizontal gradient
  3. Basic widgets (button, checkbox, arc, switch)
  4. Typography test (text on white, blue strip, grayscale bars, and colored labels “RED / GREEN / BLUE”)

In all cases the Arduino build looks vivid and correct, while the ESP‑IDF build looks noticeably washed‑out / grayish, as if there is a fog/veil over the UI. Pure colors are slightly different, and theme‑colored widgets in particular are very desaturated.


Board / display

  • ESP32 (classic, dual core)
  • CYD‑style 2.4" TFT with ILI9341 controller (SPI)
  • XPT2046 resistive touch (SPI)

Arduino / PlatformIO setup (good colors)

  • Framework: Arduino (PlatformIO)
  • LVGL: 9.3.0
  • Display: TFT_eSPI (ILI9341 config)
  • Touch: XPT2046_Touchscreen

ESP‑IDF setup (washed‑out colors)

  • ESP‑IDF: 5.5
  • LVGL: tested 9.4.0 and 9.3.0 (same result in both)
  • LVGL configured via Kconfig (no lv_conf.h, “Do not use lv_conf.h” enabled)
  • Display stack:
    • esp_lcd_ili9341
    • esp_lvgl_port (LVGL 9 port)
  • Touch: esp_lcd_touch_xpt2046

Relevant display configuration (ESP‑IDF)

SPI and panel init:

#define LCD_H_RES 320
#define LCD_V_RES 240
#define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000)
#define LCD_BITS_PER_PIXEL 16
#define LCD_DRAW_BUFF_HEIGHT 40

const spi_bus_config_t buscfg = {
    .sclk_io_num = LCD_GPIO_SCLK,
    .mosi_io_num = LCD_GPIO_MOSI,
    .miso_io_num = LCD_GPIO_MISO,
    .quadwp_io_num = GPIO_NUM_NC,
    .quadhd_io_num = GPIO_NUM_NC,
    .max_transfer_sz = LCD_H_RES * LCD_DRAW_BUFF_HEIGHT * sizeof(uint16_t),
};
spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO);

const esp_lcd_panel_io_spi_config_t io_config = {
    .dc_gpio_num = LCD_GPIO_DC,
    .cs_gpio_num = LCD_GPIO_CS,
    .pclk_hz = LCD_PIXEL_CLOCK_HZ,
    .lcd_cmd_bits = 8,
    .lcd_param_bits = 8,
    .spi_mode = 0,
    .trans_queue_depth = 10,
};
esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &s_lcd_io);

const esp_lcd_panel_dev_config_t panel_config = {
    .reset_gpio_num = LCD_GPIO_RST,
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)
    .rgb_endian = LCD_RGB_ENDIAN_RGB,
#else
    .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
#endif
    .bits_per_pixel = LCD_BITS_PER_PIXEL, // 16
};
esp_lcd_new_panel_ili9341(s_lcd_io, &panel_config, &s_lcd_panel);
esp_lcd_panel_reset(s_lcd_panel);
esp_lcd_panel_init(s_lcd_panel);
esp_lcd_panel_disp_on_off(s_lcd_panel, true);

LVGL port config (vertical orientation; horizontal is similar):

const lvgl_port_display_cfg_t disp_cfg = {
    .io_handle = s_lcd_io,
    .panel_handle = s_lcd_panel,
    .buffer_size = LCD_H_RES * LCD_DRAW_BUFF_HEIGHT, // pixels
    .double_buffer = true,
    .vres = LCD_H_RES,   // 240
    .hres = LCD_V_RES,   // 320
    .monochrome = false,
#if LVGL_VERSION_MAJOR >= 9
    .color_format = LV_COLOR_FORMAT_RGB565,
#endif
    .rotation = {
        // USB connector pointing down
        .swap_xy = true,
        .mirror_x = true,
        .mirror_y = true,
    },
    .flags = {
        .buff_dma = true,
#if LVGL_VERSION_MAJOR >= 9
        .swap_bytes = true,
#endif
        .sw_rotate = false,
    },
};
void *disp = lvgl_port_add_disp(&disp_cfg);

What I observe

  • Arduino/TFT_eSPI build:
    • Solid bars: vivid, expected RGB colors.
    • Gradient: smooth transitions, no banding or desaturation.
    • Widgets: theme colors look saturated and “clean” (blue cards/buttons, etc.).
    • Typography page: grayscale bars and colored “RED / GREEN / BLUE” labels match what I’d expect from the chosen hex values.
  • ESP‑IDF/esp_lcd_ili9341 + esp_lvgl_port build (same LVGL UI code):
    • Solid bars: colors are roughly correct but slightly off compared to Arduino.
    • Gradient: looks flatter, less vivid.
    • Widgets: blue card / primary button / arc look significantly more gray and washed out. It really feels like a semi‑transparent gray overlay.
    • Typography: the blue strip and colored text are also less saturated.

Byte‑swap clearly needs to be enabled (swap_bytes = true), otherwise colors are obviously wrong. With swap enabled, geometry and basic RGB ordering are fine, but the overall rendering is much duller than with TFT_eSPI.

I initially suspected LVGL version differences (Arduino 9.3.0 vs IDF 9.4.0), but I’ve now tested LVGL 9.3.0 on both Arduino and IDF and the visual difference persists, so it seems specific to the display stack / initialization rather than LVGL core version.

Question

  • Has anyone seen this kind of “washed‑out / grayish” color difference when moving from Arduino (TFT_eSPI) to ESP‑IDF with esp_lvgl_port + esp_lcd_ili9341 on the same ILI9341 hardware?
  • What is the most likely cause in this stack, and what would be a recommended way to fix it so the IDF output matches the Arduino/TFT_eSPI colors more closely?

Can you try using the lv_conf.h instead of the Kconfig and see if that is the issue? I think that’s where I would start.

I is probably a difference in the values that are used to initialize the lcd hardware. Things like gamma. If you look in the driver code (ili9341) for both setups you should find the initialization that is used. Get the datasheet for that lcd controller and go down the registers that the driver is setting to see where the differences are. You can usually specify your own init list for the driver to use, so I would just take the good settings and convert them to the format needed by your driver.

Mike

Sorry for the late reply.

Mike, you were right. The issue was related to some vendor initialization values. After some AI-assisted searching, I found a few byte values related to alpha correction.

The following code solved the problem for me:

static const ili9341_lcd_init_cmd_t my_init_cmds[] = {
    {0xC5, (uint8_t[]){0x3E, 0x28}, 2, 0},  // VCOM Control 1
    {0xC7, (uint8_t[]){0x86}, 1, 0},        // VCOM Control 2
};

ili9341_vendor_config_t vendor_config = {
    .init_cmds = my_init_cmds,
    .init_cmds_size = sizeof(my_init_cmds) / sizeof(my_init_cmds[0]),
};
2 Likes