Chapter 169: LCD Interface with ParIO

Chapter Objectives

After completing this chapter, you will be able to:

  • Understand the typical architecture of parallel LCD modules and their controllers (e.g., ST7789, ILI9341).
  • Configure the ESP-IDF esp_lcd driver for a specific parallel LCD using an 8080 interface.
  • Write code to initialize a parallel LCD panel by sending setup commands.
  • Implement functions to draw pixels and fill screen areas with color.
  • Understand the role of Graphics RAM (GRAM) in LCD controllers.
  • Utilize DMA for efficient transfer of pixel data to the LCD.
  • Identify common issues and troubleshoot parallel LCD interfacing problems.

Introduction

In the previous chapter, we explored the fundamentals of Parallel I/O (ParIO) and its significance for high-speed data transfer. One of the most common and visually rewarding applications of ParIO in embedded systems is interfacing with Liquid Crystal Displays (LCDs). Many color graphic LCDs, especially those offering larger sizes and faster refresh rates, utilize parallel interfaces like the Intel 8080 bus for communication.

This chapter will guide you through the process of connecting and controlling a parallel LCD using an ESP32 microcontroller. We will leverage the esp_lcd driver component provided by ESP-IDF, building upon the ParIO concepts to send commands and pixel data to an LCD, bringing your projects to life with graphical output. We will primarily focus on LCDs that use an 8080-style parallel interface, common with controllers like the ST7789 or ILI9341.

Theory

Parallel LCD Architecture

A typical parallel graphic LCD module consists of several key components:

  • LCD Glass Panel: The physical display itself, containing liquid crystals, color filters (for color displays), and electrodes.
  • LCD Controller IC: A specialized integrated circuit (e.g., ST7789, ILI9341, SSD1963) mounted on the LCD module (often on a flexible PCB bonded to the glass). This chip is the “brain” of the LCD. Its responsibilities include:
    • Receiving commands and data from the host microcontroller (ESP32).
    • Storing pixel data in its internal Graphics RAM (GRAM).
    • Generating the precise voltage signals and timing required to drive the LCD glass panel and display the image stored in GRAM.
    • Managing display settings like contrast, orientation, and power modes.
  • Graphics RAM (GRAM): A dedicated block of memory within the LCD controller where the pixel data for the display is stored. When the ESP32 wants to display an image, it writes the pixel data into the GRAM. The controller then continuously reads this GRAM to refresh the LCD panel.
  • Backlight: Usually a set of LEDs providing illumination from behind the LCD panel, making the display visible.
  • Interface Connector: Provides pins for power, control signals, and the parallel data bus to connect to the host MCU.
    graph TD
        subgraph ESP32 Host
            MCU[<b>ESP32 MCU</b></font><br>Application Logic]
        end

        subgraph "Intel 8080 Interface"
            direction LR
            CS[<b>/CS</b><br>Chip Select]
            WR[<b>/WR</b><br>Write]
            RD[<b>/RD</b><br>Read]
            DC[<b>D/C</b><br>Data/Cmd]
            RST[<b>/RESET</b><br>Reset]
            DATA[<b>D0-D7</b><br>Data Bus]
        end

        subgraph "Parallel LCD Module"
            Controller["<b>LCD Controller IC</b></font><br>(e.g., ST7789, ILI9341)"]
            GRAM["<b>Graphics RAM (GRAM)</b></br>Stores Pixel Data"]
            Driver[<b>Panel Driver Circuitry</b>]
            Panel[<b>LCD Glass Panel</b></font>]
            Backlight[<b>Backlight LEDs</b>]
        end
        
        MCU -- ParIO Driver --> CS
        MCU -- ParIO Driver --> WR
        MCU -- ParIO Driver --> RD
        MCU -- ParIO Driver --> DC
        MCU -- ParIO Driver --> RST
        MCU -- ParIO Driver --> DATA

        CS --> Controller
        WR --> Controller
        RD --> Controller
        DC --> Controller
        RST --> Controller
        DATA <--> Controller

        Controller -- Manages --> GRAM
        Controller -- Controls --> Driver
        Driver -- Drives --> Panel
        Backlight -- Illuminates --> Panel

        classDef host fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF;
        classDef interface fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
        classDef module fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#4C1D95;
        classDef panel-elements fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0369A1;

        class MCU host;
        class CS,WR,RD,DC,RST,DATA interface;
        class Controller,GRAM,Driver module;
        class Panel,Backlight panel-elements;

Feature ST7789 / ST7789V ILI9341
Typical Resolutions 240×240, 240×320 240×320
GRAM Access Partial GRAM access; doesn’t always map directly to 240×320 space (e.g., might be 240×320 in a 240×240 GRAM). Requires windowing. Direct GRAM access, typically a 1:1 mapping for a 240×320 display.
Color Order (Default) RGB RGB
Key Init Commands Software Reset (0x01), Sleep Out (0x11), Pixel Format (0x3A), MADCTL (0x36), Display On (0x29) Software Reset (0x01), Sleep Out (0x11), Pixel Format (0x3A), MADCTL (0x36), Power/VCOM Controls, Display On (0x29)
Tearing Effect (TE) Pin Often available. Useful for synchronization. Often available. Useful for synchronization.
ESP-IDF Driver esp_lcd_new_panel_st7789 (Built-in support) esp_lcd_new_panel_ili9341 (Built-in support)
Common Use Case Very common in modern hobbyist IPS displays (square or rectangular). Extremely common in slightly older 2.2″-2.8″ TFT LCDs. A classic standard.

8080 Interface for LCDs (Recap & Specifics)

As discussed in Chapter 168, the Intel 8080 interface is a common asynchronous parallel bus. When used with LCDs:

  • Data Lines (D0-D7, D0-D15): Carry command codes, command parameters, or pixel data. 8-bit and 16-bit widths are common. For an 8-bit interface sending 16-bit pixel data (e.g., RGB565 format), two consecutive 8-bit transfers are needed per pixel.
  • Chip Select (/CS): Enables communication with the LCD controller.
  • Write Enable (/WR): The MCU strobes this line (typically active-low pulse) to write data/commands to the LCD. Data is usually latched on the rising edge of /WR.
  • Read Enable (/RD): The MCU strobes this line to read data/status from the LCD. (Note: Reading from LCDs is less common in simple display applications and esp_lcd driver primarily focuses on writing).
  • Data/Command (D/C or RS – Register Select):
    • D/C = Low: The byte(s) on the data bus are interpreted as a command for the LCD controller (e.g., set display orientation, define pixel format).
    • D/C = High: The byte(s) on the data bus are interpreted as data (e.g., parameters for a command, or pixel data to be written to GRAM).
  • Reset (/RESET or RES): An active-low signal used to reset the LCD controller to its default state. It’s good practice to pulse this at startup.

LCD Initialization Sequence

Before an LCD can display anything meaningful, it must be initialized. This involves sending a specific sequence of commands to the LCD controller. The exact sequence and command codes are specific to the LCD controller IC (e.g., ST7789, ILI9341) and can be found in its datasheet.

A typical initialization sequence might include:

sequenceDiagram
    actor MCU
    participant LCD Controller

    MCU->>LCD Controller: 1. Hardware Reset (Pulse /RESET pin Low, then High)
    Note right of MCU: Physical pin toggle

    MCU->>LCD Controller: 2. Send 'Software Reset' Command (e.g., 0x01)
    Note right of MCU: D/C Line is LOW
    
    MCU->>MCU: Wait (e.g., 120ms delay)
    
    MCU->>LCD Controller: 3. Send 'Sleep Out' Command
    Note right of MCU: D/C Line is LOW
    
    MCU->>MCU: Wait (e.g., 50ms delay)

    MCU->>LCD Controller: 4. Send 'Pixel Format Set' Command + Parameter
    Note right of MCU: D/C LOW for command,<br>D/C HIGH for parameter(s)<br>e.g., Set to RGB565 (16bpp)
    
    MCU->>LCD Controller: 5. Send 'Memory Access Control' (MADCTL) Command
    Note right of MCU: Sets orientation, RGB/BGR order

    MCU->>LCD Controller: 6. Send other vendor-specific settings...<br>(Timings, Voltages, etc.)

    MCU->>LCD Controller: 7. Send 'Display ON' Command
    Note right of MCU: LCD is now ready to accept pixel data

    rect rgb(209, 250, 229)
        Note over MCU,LCD Controller: LCD Ready for Drawing Operations
    end
  1. Hardware Reset: Pulse the /RESET pin low, then high.
  2. Software Reset: Send the software reset command.
  3. Sleep Out: Wake the controller from its default sleep state. A delay is often required after this command.
  4. Pixel Format: Configure the color format the MCU will be sending (e.g., 16-bit RGB565, 18-bit RGB666). This must match the data format the ESP32 will send.
  5. Memory Access Control (MADCTL): Set display orientation (portrait, landscape, flipped), color order (RGB or BGR).
  6. Interface Pixel Format: Define how pixel data is packed on the parallel bus.
  7. Display Inversion On/Off: Some displays might need color inversion.
  8. Porch Settings, Gate Control, VCOM Settings: Advanced timing and voltage settings, often provided as recommended values in the datasheet.
  9. Display On: Finally, turn the display on.

Warning: Failing to send the correct initialization sequence, or sending commands with incorrect parameters, will likely result in a blank screen, incorrect colors, or other visual artifacts. Always consult the specific datasheet for your LCD controller.

Drawing on the LCD

Once initialized, drawing on the LCD involves these general steps:

flowchart TD


    A[Start Drawing] --> B["Set Column Address (CASET)<br>Send Start/End Columns"];
    B -- D/C Low --> C["Set Page/Row Address (PASET)<br>Send Start/End Rows"];
    C -- D/C Low --> D["Send 'Memory Write' (RAMWR) Command"];
    
    subgraph "Pixel Data Loop"
        D -- D/C Low --> E{"Prepare Pixel Data<br>(e.g., from Frame Buffer)"};
        E -- D/C High --> F(Transfer Pixel Data Block);
        F --> G{More Data for Window?};
        G -- Yes --> E;
    end

    G -- No --> H[Drawing Complete];

    classDef start-end-node fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process-node fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef data-node fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef decision-node fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    
    class A,H start-end-node;
    class B,C,D,F process-node;
    class E data-node;
    class G decision-node;
  1. Set Window Coordinates (Address Window):
    • Send the “Column Address Set” (CASET) command with start and end column parameters.
    • Send the “Page Address Set” (PASET) command with start and end page (row) parameters.
    • This defines a rectangular region in the GRAM where subsequent pixel data will be written.
  2. Memory Write Command:
    • Send the “Memory Write” (RAMWR) command. This tells the LCD controller that subsequent data bytes are pixel data to be written into the active window in GRAM.
  3. Send Pixel Data:
    • Transmit the pixel data over the parallel bus with the D/C line high. The controller automatically fills the defined window.

Pixel Formats:

The way color is represented in data is crucial. A common format is RGB565, where each pixel takes 16 bits (2 bytes):

  • 5 bits for Red
  • 6 bits for Green (more bits as human eye is more sensitive to green)
  • 5 bits for Blue

Example: For an 8-bit parallel bus sending RGB565 data, each pixel requires two 8-bit transfers (e.g., MSB first, then LSB). The esp_lcd driver and DMA can handle this efficiently.

Frame Buffers and DMA

For simple graphics, you can send pixel data directly as you generate it. However, for more complex scenes or animations, using a frame buffer in the ESP32’s RAM (or PSRAM) is common:

  1. Allocate Frame Buffer: Reserve a block of memory in the ESP32 large enough to hold one full frame of pixel data (e.g., for a 240×320 LCD with RGB565, this is 240 * 320 * 2 bytes = 153,600 bytes).
  2. Draw to Frame Buffer: Your application’s drawing routines (lines, text, images) modify the pixel data in this MCU-side frame buffer.
  3. Flush to LCD (Push to GRAM): Once the frame is ready in the MCU’s buffer, the entire buffer (or a portion of it) is transferred to the LCD’s GRAM using the parallel interface. This is where DMA is highly beneficial. The esp_lcd_panel_draw_bitmap() function uses DMA to transfer the pixel data from the MCU’s memory to the LCD controller without significant CPU overhead.
graph TD
    subgraph "ESP32 System"
        CPU["<b>CPU</b><br>Runs Drawing Functions<br>e.g., draw_line(), draw_text()"]
        RAM["<b>MCU RAM / PSRAM</b>"]
        FB["<b>Frame Buffer</b><br><i>A complete image in memory</i>"]
        DMA["<b>DMA Controller</b>"]
        
        CPU -- Modifies --> FB
        RAM -- Contains --> FB
        CPU -- Configures --> DMA
    end

    subgraph "LCD Module"
        LCD["<b>LCD Controller</b>"]
        GRAM["<b>Graphics RAM (GRAM)</b><br><i>Stores the displayed image</i>"]
        
        LCD -- Refreshes from --> GRAM
    end

    DMA -- Reads from --> FB
    DMA -- Writes to --> LCD

    %% Notes as dummy nodes
    N1["<i>1. CPU prepares a full frame in the buffer.</i>"]:::noteStyle
    N2["<i>2. CPU tells DMA: 'Send this buffer to the LCD'.</i>"]:::noteStyle
    N3["<i>3. DMA transfers data without CPU help.</i>"]:::noteStyle
    N4["<i>4. LCD Controller continuously<br>shows what's in its GRAM.</i>"]:::noteStyle

    CPU --> N1
    CPU --> N2
    DMA --> N3
    GRAM --> N4

    linkStyle 0 stroke:#1E40AF,stroke-width:2px,color:blue;
    linkStyle 1 stroke:#1E40AF,stroke-width:2px,color:blue;
    linkStyle 2 stroke:#9333EA,stroke-width:2px,color:purple;
    linkStyle 3 stroke:#9CA3AF,stroke-width:1px,color:gray;
    linkStyle 4 stroke:#059669,stroke-width:3px,color:green;
    linkStyle 5 stroke:#059669,stroke-width:3px,color:green;

    classDef esp-sys fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF;
    classDef lcd-sys fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#4C1D95;
    classDef noteStyle fill:#FFFFFF00,color:#6B7280,stroke-dasharray: 3 3;

    class CPU,RAM,FB,DMA esp-sys;
    class LCD,GRAM lcd-sys;

This approach decouples drawing logic from the physical transfer, often leading to smoother animations and allowing more complex rendering.

Practical Examples

This example demonstrates how to initialize an ST7789-based LCD (a common controller for small color TFTs) using an 8-bit 8080 parallel interface on an ESP32-S3. We will initialize it and fill the screen with a solid color.

Assumptions:

  • LCD Resolution: 240×320 pixels.
  • Pixel Format: RGB565 (16 bits per pixel).
  • Interface: 8-bit 8080 parallel.

Prerequisites:

  1. ESP32-S3 development board.
  2. A parallel LCD module (e.g., ST7789 based, 240×320 resolution) compatible with 8080 interface.
  3. Correct wiring between ESP32-S3 and the LCD module.
  4. ESP-IDF v5.x installed and configured with VS Code.
  5. Logic analyzer (optional, but extremely helpful for debugging).

1. Wiring

Consult your LCD module’s datasheet and your ESP32-S3 board’s schematic. Example connections:

  • LCD D0-D7 -> ESP32-S3 Data GPIOs (e.g., GPIO 1-8)
  • LCD /CS -> ESP32-S3 CS GPIO (e.g., GPIO 9)
  • LCD /WR -> ESP32-S3 WR GPIO (e.g., GPIO 10)
  • LCD D/C (RS) -> ESP32-S3 DC GPIO (e.g., GPIO 11)
  • LCD /RESET -> ESP32-S3 RESET GPIO (e.g., GPIO 12)
  • LCD VCC -> 3.3V
  • LCD GND -> GND
  • LCD BLK (Backlight) -> 3.3V (or controlled by another GPIO via a transistor if dimming is needed)

Warning: Incorrect wiring can damage your ESP32 or LCD. Double-check all connections.

2. main/CMakeLists.txt

Plaintext
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver esp_lcd esp_log heap)
  • heap: For heap_caps_malloc if allocating DMA-capable memory for frame buffers (though this basic example might not use a full frame buffer explicitly, esp_lcd_panel_draw_bitmap uses DMA).

3. main/main.c

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_vendor.h" // For specific controller drivers like ST7789
#include "esp_log.h"
#include "soc/soc_caps.h"
#include "esp_heap_caps.h" // For MALLOC_CAP_DMA

static const char *TAG = "lcd_pario_example";

// LCD Configuration
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ (10 * 1000 * 1000) // 10 MHz - Adjust based on LCD specs
#define EXAMPLE_LCD_H_RES          240
#define EXAMPLE_LCD_V_RES          320
#define EXAMPLE_LCD_CMD_BITS       8
#define EXAMPLE_LCD_PARAM_BITS     8

// GPIO Configuration (Example for ESP32-S3) - CHANGE THESE TO MATCH YOUR WIRING
#define EXAMPLE_PIN_NUM_LCD_CS      GPIO_NUM_9
#define EXAMPLE_PIN_NUM_LCD_WR      GPIO_NUM_10
#define EXAMPLE_PIN_NUM_LCD_DC      GPIO_NUM_11
#define EXAMPLE_PIN_NUM_LCD_RST     GPIO_NUM_12 // Can be -1 if not controlled by MCU

#define EXAMPLE_PIN_NUM_LCD_D0      GPIO_NUM_1
#define EXAMPLE_PIN_NUM_LCD_D1      GPIO_NUM_2
#define EXAMPLE_PIN_NUM_LCD_D2      GPIO_NUM_3
#define EXAMPLE_PIN_NUM_LCD_D3      GPIO_NUM_4
#define EXAMPLE_PIN_NUM_LCD_D4      GPIO_NUM_5
#define EXAMPLE_PIN_NUM_LCD_D5      GPIO_NUM_6
#define EXAMPLE_PIN_NUM_LCD_D6      GPIO_NUM_7
#define EXAMPLE_PIN_NUM_LCD_D7      GPIO_NUM_8
// For 16-bit bus, add D8-D15

// Function to fill the screen with a color
static void fill_screen(esp_lcd_panel_handle_t panel_handle, uint16_t color) {
    // Allocate a buffer for one row (or a few rows)
    // For RGB565, each pixel is 2 bytes
    uint16_t *buffer = heap_caps_malloc(EXAMPLE_LCD_H_RES * 2, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
    if (!buffer) {
        ESP_LOGE(TAG, "Failed to allocate DMA buffer for screen fill");
        return;
    }

    for (int i = 0; i < EXAMPLE_LCD_H_RES; i++) {
        buffer[i] = color;
    }

    // Draw the buffer for each row
    for (int y = 0; y < EXAMPLE_LCD_V_RES; y++) {
        // esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, *color_data)
        // For a single row: x_start=0, y_start=y, x_end=EXAMPLE_LCD_H_RES, y_end=y+1
        esp_lcd_panel_draw_bitmap(panel_handle, 0, y, EXAMPLE_LCD_H_RES, y + 1, buffer);
    }
    heap_caps_free(buffer);
    ESP_LOGI(TAG, "Screen filled with color 0x%04X", color);
}


void app_main(void) {
    ESP_LOGI(TAG, "Initialize LCD over Parallel IO (8080 interface)");

#if SOC_LCD_I80_SUPPORTED
    ESP_LOGI(TAG, "SoC supports I80 interface");

    // 1. Configure I80 Bus
    esp_lcd_i80_bus_handle_t i80_bus = NULL;
    esp_lcd_i80_bus_config_t bus_config = {
        .dc_gpio_num = EXAMPLE_PIN_NUM_LCD_DC,
        .wr_gpio_num = EXAMPLE_PIN_NUM_LCD_WR,
        .clk_src = LCD_CLK_SRC_DEFAULT, // Or specific like LCD_CLK_SRC_PLL160M
        .data_gpio_nums = {
            EXAMPLE_PIN_NUM_LCD_D0, EXAMPLE_PIN_NUM_LCD_D1, EXAMPLE_PIN_NUM_LCD_D2, EXAMPLE_PIN_NUM_LCD_D3,
            EXAMPLE_PIN_NUM_LCD_D4, EXAMPLE_PIN_NUM_LCD_D5, EXAMPLE_PIN_NUM_LCD_D6, EXAMPLE_PIN_NUM_LCD_D7,
        },
        .bus_width = 8, // For 8-bit data bus
        .max_transfer_bytes = EXAMPLE_LCD_H_RES * 2 * 10, // Max buffer size for one transaction (e.g., 10 rows of 240px * 2 bytes/px)
                                                         // Adjust based on available RAM and performance needs
        .psram_trans_align = 0,    // Set if using PSRAM for buffers
        .sram_trans_align = 4      // ESP32-S3 DMA requires 4-byte alignment for SRAM
    };
    ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
    ESP_LOGI(TAG, "I80 bus created");

    // 2. Configure Panel IO
    esp_lcd_panel_io_handle_t io_handle = NULL;
    esp_lcd_panel_io_i80_config_t io_config = {
        .cs_gpio_num = EXAMPLE_PIN_NUM_LCD_CS,
        .pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,
        .trans_queue_depth = 10,
        .dc_levels = {
            .dc_idle_level = 0, // Or 1, depending on your D/C idle state needs
            .dc_cmd_level = 0,
            .dc_dummy_level = 0,
            .dc_data_level = 1,
        },
        .flags = {
            .cs_active_high = 0,
            .reverse_color_bits = 0, // For ST7789, typically false
            .swap_color_bytes = 0,   // For RGB565 on 8-bit bus, data is usually sent MSB then LSB. If LCD expects LSB first, set this.
            .pclk_active_neg = 0,    // WR is active low, latches on rising edge
            .pclk_idle_high = 1,     // WR idles high
        },
        // .on_color_trans_done = NULL, // Callback when color data transfer is done
        // .user_ctx = NULL,
        .lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS,
        .lcd_param_bits = EXAMPLE_LCD_PARAM_BITS,
    };
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle));
    ESP_LOGI(TAG, "I80 panel IO created");

    // 3. Configure LCD Panel Device
    esp_lcd_panel_handle_t panel_handle = NULL;
    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST,
        .rgb_endian = LCD_RGB_ENDIAN_RGB, // For ST7789, pixel format is RGB. Some controllers might be BGR.
                                          // This refers to the order of color components within a pixel, not byte order on the bus.
        .bits_per_pixel = 16, // For RGB565
        // .flags = {}, // Vendor specific flags
        // .vendor_config = NULL, // Pointer to vendor specific configuration
    };

    // For ST7789, we can use the built-in driver.
    // If you have a different controller or want generic init, you'd use esp_lcd_new_panel_io_i80
    // and then manually send init commands using esp_lcd_panel_io_tx_param.
    // Here, we use the ST7789 specific constructor which handles its init sequence.
    ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
    ESP_LOGI(TAG, "ST7789 panel device created");

    // Perform reset and initialization
    ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
    ESP_LOGI(TAG, "Panel reset done");
    ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
    ESP_LOGI(TAG, "Panel init done");

    // Turn on display
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
    ESP_LOGI(TAG, "Display turned ON");

    // Optional: Set orientation (ST7789 specific commands might be needed if not handled by driver)
    // esp_lcd_panel_swap_xy(panel_handle, true);
    // esp_lcd_panel_mirror(panel_handle, true, false);

    // Fill screen with colors
    ESP_LOGI(TAG, "Filling screen RED");
    fill_screen(panel_handle, 0xF800); // Red in RGB565
    vTaskDelay(pdMS_TO_TICKS(2000));

    ESP_LOGI(TAG, "Filling screen GREEN");
    fill_screen(panel_handle, 0x07E0); // Green in RGB565
    vTaskDelay(pdMS_TO_TICKS(2000));

    ESP_LOGI(TAG, "Filling screen BLUE");
    fill_screen(panel_handle, 0x001F); // Blue in RGB565
    vTaskDelay(pdMS_TO_TICKS(2000));

    ESP_LOGI(TAG, "Example finished. Idling...");

    // In a real application, you might delete handles when done
    // esp_lcd_panel_del(panel_handle);
    // esp_lcd_panel_io_del(io_handle);
    // esp_lcd_del_i80_bus(i80_bus);
#else
    ESP_LOGE(TAG, "SoC does not support I80 interface for this example.");
#endif // SOC_LCD_I80_SUPPORTED

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Explanation of main.c:

flowchart TD
    A["Start fill_screen(color)"] --> B["Allocate DMA-capable buffer<br>for one row of pixels"];
    B --> C{Allocation Successful?};
    C -- No --> C_NO[Log Error & Return];
    C -- Yes --> D["Fill the buffer with the<br>specified 'color' value"];
    
    D --> E["Initialize row counter 'y' = 0"];
    
    subgraph "For each row in display"
        direction TB
        E --> F{y < Vertical Resolution?};
        F -- Yes --> G["Call esp_lcd_panel_draw_bitmap<br>to send the buffer for row 'y'"];
        G --> H["Increment 'y'"];
        H --> F;
    end

    F -- No --> I[Free the DMA buffer];
    I --> J[End];

    classDef start-end-node fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process-node fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef error-node fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef decision-node fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    
    class A,J start-end-node;
    class B,D,E,G,H,I process-node;
    class C_NO error-node;
    class C,F decision-node;
  1. Includes & Definitions: Standard headers plus esp_lcd_panel_vendor.h for ST7789 support, and esp_heap_caps.h for DMA-capable memory. LCD resolution, clock, and GPIO pins are defined. Remember to change GPIO definitions to match your actual wiring!
  2. Bus and IO Configuration (bus_configio_config): Similar to Chapter 168, but pclk_hz and max_transfer_bytes are now tailored for LCD performance. max_transfer_bytes is set to handle several rows of pixel data to optimize DMA. swap_color_bytes in io_config.flags might be needed if your LCD expects the two bytes of a 16-bit pixel in a different order than the ESP32 sends them by default over an 8-bit bus.
  3. Panel Device Configuration (panel_config):
    • reset_gpio_num: Assigns the LCD reset pin.
    • rgb_endian: Defines the color component order within a pixel (e.g., LCD_RGB_ENDIAN_RGB or LCD_RGB_ENDIAN_BGR). This is important and depends on your LCD module. ST7789 is typically RGB.
    • bits_per_pixel: Set to 16 for RGB565.
  4. Panel Creation (esp_lcd_new_panel_st7789): This is a convenience function for ST7789 controllers. It takes the io_handle and panel_config and internally knows the ST7789 initialization sequence.
    • Alternative (Generic): If you had a different controller or wanted to send custom init commands, you would first create a generic IO panel: esp_lcd_new_panel(io_handle, &panel_config, &panel_handle); (using a generic esp_lcd_panel_dev_config_t). Then, you would manually send initialization commands using esp_lcd_panel_io_tx_param(io_handle, CMD_CODE, params, params_len);.
  5. Reset, Init, Display On:
    • esp_lcd_panel_reset(): Toggles the reset pin.
    • esp_lcd_panel_init(): Sends the initialization commands. For esp_lcd_new_panel_st7789, this executes the built-in ST7789 init sequence.
    • esp_lcd_panel_disp_on_off(panel_handle, true): Turns the display on (sends the “Display ON” command).
  6. fill_screen() Function:
    • Allocates a DMA-capable buffer for one row of pixels (heap_caps_malloc with MALLOC_CAP_DMA).
    • Fills this buffer with the specified color.
    • Iterates through each row of the display (y from 0 to EXAMPLE_LCD_V_RES - 1).
    • Calls esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data) to send the row buffer to the LCD.
      • x_start=0y_start=y
      • x_end=EXAMPLE_LCD_H_RESy_end=y+1 (to draw one row)
      • buffer points to the color data for that row.
    • This function demonstrates how to push pixel data to the LCD. For full-screen updates, a larger buffer (a full frame buffer) could be used if memory allows, or data can be streamed row-by-row or in chunks.
  7. Main Loop: Fills the screen with Red, Green, and Blue with delays in between.

4. Build, Flash, and Observe

  1. Connect LCD: Ensure your LCD is correctly wired to the ESP32-S3.
  2. Build: idf.py build
  3. Flash & Monitor: idf.py -p (PORT) flash monitor
  4. Observe: The LCD should initialize and then display solid Red, then Green, then Blue screens. The serial monitor will show log messages. If the screen is blank, white, or shows garbage, check wiring, GPIO assignments, LCD controller type (ensure it’s ST7789 compatible or adapt init), and pclk_hz. A logic analyzer on the data and control lines is invaluable for debugging.

Tip: The esp_lcd_panel_st7789.c (and similar files for other vendor controllers) in the ESP-IDF components directory (components/esp_lcd/src/esp_lcd_panel_vendor) contains the actual initialization sequences used by functions like esp_lcd_new_panel_st7789. This can be a useful reference.

Variant Notes

  • ESP32-S3, ESP32-C6, ESP32-H2: These are the best choices for parallel LCD interfacing due to their dedicated LCD_CAM peripheral, which provides hardware acceleration for 8080/6800 and RGB parallel interfaces, along with efficient DMA. The esp_lcd driver fully supports these.
  • ESP32-S2: Also has an LCD_CAM peripheral and works well with the esp_lcd driver for parallel LCDs.
  • ESP32 (Original): Does not have the dedicated LCD_CAM peripheral. It can drive some parallel LCDs using its I2S peripheral in “LCD mode” or “camera mode”.
    • The esp_lcd driver has some support for I2S-based parallel RGB LCDs on the original ESP32.
    • Driving an 8080-style interface via I2S is more complex and might require more manual configuration or custom driver code. It’s generally less straightforward than on S2/S3/C6/H2 for 8080 interfaces. Standard esp_lcd functions for 8080 might not directly apply or work as seamlessly.
  • ESP32-C3: Lacks dedicated hardware for high-speed parallel LCD interfaces. For graphical displays with ESP32-C3, SPI-based LCDs are the standard and recommended approach. Attempting ParIO would be limited to slow bit-banging.

Conclusion on Variants: For applications requiring parallel LCDs, especially with 8080 or RGB interfaces, strongly prefer ESP32-S2, ESP32-S3, ESP32-C6, or ESP32-H2.

Common Mistakes & Troubleshooting Tips

Symptom Potential Cause Troubleshooting Steps
Screen is completely blank or white (backlight may be on) Hardware wiring error, incorrect initialization, or wrong controller driver. 1. Triple-check all wiring (VCC, GND, CS, WR, D/C, RST, D0-D7).
2. Ensure you are using the correct driver (e.g., st7789 for an ST7789).
3. Verify the reset sequence and delays in your code match the datasheet.
Colors are swapped (e.g., Red appears as Blue) Incorrect color order configuration. 1. Change panel_config.rgb_endian from LCD_RGB_ENDIAN_RGB to LCD_RGB_ENDIAN_BGR (or vice-versa).
2. If using custom init, check the ‘MADCTL’ (Memory Access Control) command’s BGR bit.
Display is distorted, flickering, or has garbage data Timing issue (PCLK too high) or poor signal integrity. 1. Lower the pclk_hz in io_config to a safe value (e.g., 5MHz) and test.
2. Use shorter, shielded wires if possible.
3. Ensure a solid common ground between ESP32 and the LCD.
Guru Meditation Error (Crash) during drawing DMA buffer issue. 1. Ensure any buffer passed to esp_lcd_panel_draw_bitmap is allocated with MALLOC_CAP_DMA.
2. Check for buffer overflows; make sure the buffer size matches the drawing area.
Display is mirrored or upside down Incorrect Memory Access Control settings. 1. Use esp_lcd_panel_swap_xy() and esp_lcd_panel_mirror() after initialization.
2. For custom init, modify the bits in the MADCTL command (0x36) to control scan direction and XY swap.

Exercises

  1. Change Fill Colors: Modify the app_main function in the example to fill the screen with three different colors of your choice (e.g., Yellow, Cyan, Magenta). Find their RGB565 hex codes.
  2. Draw a Single Pixel Function: Implement a C function void draw_pixel(esp_lcd_panel_handle_t panel_handle, int x, int y, uint16_t color) that draws a single pixel at the given (x, y) coordinate with the specified color.
    • Hint: Use esp_lcd_panel_draw_bitmap(panel_handle, x, y, x + 1, y + 1, &color_data_for_one_pixel);
  3. ILI9341 Initialization Research: Research the initialization command sequence for an ILI9341 LCD controller (another common parallel LCD controller). List at least 5 key commands and their general purpose, noting any significant differences from a typical ST7789 sequence if apparent (e.g., different command codes for similar functions).
  4. Draw Stripes: Modify the fill_screen function (or create a new one) to draw alternating horizontal stripes of two different colors (e.g., 10 rows of black, 10 rows of white, repeating down the screen).

Summary

  • Parallel LCDs, often using 8080 interfaces, provide high-speed graphical output for embedded systems.
  • LCD modules contain a controller IC (like ST7789) with internal GRAM that stores pixel data.
  • The ESP-IDF esp_lcd driver simplifies interfacing with parallel LCDs on supported ESP32 variants (S2, S3, C6, H2).
  • A specific initialization sequence, defined by the LCD controller’s datasheet, is crucial before the LCD can be used.
  • Drawing involves setting an address window in GRAM and then writing pixel data, often using DMA for efficiency via functions like esp_lcd_panel_draw_bitmap.
  • Correct configuration of GPIOs, bus timing (pclk_hz), pixel format, and color order is essential for proper operation.
  • A logic analyzer is a powerful tool for debugging parallel LCD interface issues.

Further Reading

  • ESP-IDF API Reference – LCD Controller: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32s3/api-reference/peripherals/lcd.html (Select your ESP32 variant for specific details).
  • ST7789VW Datasheet: (Search online) – A common LCD controller. Reading its datasheet will provide deep insight into commands and operation.
  • ILI9341 Datasheet: (Search online) – Another popular LCD controller.
  • ESP-IDF LCD Examples: Check the examples directory within your ESP-IDF installation, particularly under peripherals/lcd.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top