
TTGO T-Display는 LILYGO사에서 제조한 ESP32 기반의 소형 개발 보드입니다. 마이크로컨트롤러, WiFi, 블루투스 기능과 함께 컬러 LCD 디스플레이가 내장되어 있어 IoT 및 임베디드 디스플레이 프로젝트에 매우 유용합니다.
👉 알리익스프레스 참고: https://ko.aliexpress.com/item/4000509604970.html

가격은 15,000원 정도 하네요. 보시다시피 LCD 디스플레이가 기본적으로 연결되어 있는 상태이기 때문에 디스플레이 테스트 용도로 아주 편한거 같습니다.
기본적인 스펙
1.14인치 ST7789V HD LCD:
전원 공급

디스플레이는 SPI 통신으로 연결되어 있는 거 같네요. (MOSI, SCLK, CS 등)
TTGO T-Display는 주로 Arduino IDE나 MicroPython 플랫폼을 사용하여 프로그래밍합니다. 특히 Arduino IDE를 사용할 경우, ESP32 Dev Module 보드를 선택하고 TFT_eSPI와 같은 관련 라이브러리를 설치하여 디스플레이를 쉽게 제어할 수 있습니다.
내장된 컬러 디스플레이와 무선 통신 기능을 활용하여 다음과 같은 다양한 프로젝트에 적용할 수 있습니다.
스마트 시계 정도 만들어보면 재미있을 거 같네요. 아니면 온습도 시각화도 재밌을 거 같고요.
ST7789V는 시트로닉스(Sitronix)라는 회사에서 만든 TFT-LCD 디스플레이 컨트롤러/드라이버 칩입니다. 쉽게 말해, 마이크로컨트롤러(MCU)가 보내는 디지털 신호를 디스플레이가 이해할 수 있는 전기 신호로 바꿔주는 역할을 합니다.
핵심: LCD 패널과 마이크로컨트롤러 사이의 중개자 역할을 하는 필수 드라이버 IC

LCD 패널 자체는 단순한 픽셀 매트릭스일 뿐이므로, 실제로 화면을 제어하려면 전압 제어, 타이밍 생성, 색상 데이터 처리 등을 담당하는 컨트롤러 IC가 필수적으로 필요합니다.
마이크로컨트롤러가 "X, Y 좌표에 빨간색 점을 찍어라"는 명령을 내리면, ST7789V 칩은 이 명령을 받아들여 실제로 디스플레이의 특정 픽셀에 맞는 전압을 가해 빨간색을 표시하도록 제어합니다.
| 항목 | 내용 |
|---|---|
| 해상도 | 240(H) × 320(V) 픽셀 |
| 프레임 메모리 | 내부에 240×320×18비트 RAM 탑재 |
| 컬러 수 | 최대 262,144 색 (18비트, RGB666) |
| 지원 색 깊이 (Color Depth) | 12비트 (RGB444), 16비트 (RGB565), 18비트 (RGB666) 가능 |
| 인터페이스 | 여러 가지: Parallel MCU(8080 스타일, 8/9/16/18비트), RGB 인터페이스 (VSYNC, HSYNC, DOTCLK 등), Serial SPI 인터페이스 |
| 전력 및 절전 기능 | 일부 절전 모드, Idle 모드 / Partial display 모드 등 전력 소비 절약 기능 있음 |
| 화면 드라이버 출력 | 소스 라인 드라이버 240채널, 게이트 라인 드라이버 320채널 내장 |
TFT LCD는 기본적으로 픽셀(Pixel)의 행렬(matrix)로 되어 있습니다.
각 픽셀은 TFT(박막 트랜지스터) + 액정셀 + 컬러필터(R/G/B) 조합으로 만들어지고, 이를 행렬 배선으로 제어합니다.

게이트 라인(Gate Line)
게이트 라인은 LCD 패널의 가로 방향(행)에 해당하는 배선으로, 특정 행의 TFT 트랜지스터들을 동시에 켜고 끄는 스위칭 역할을 하며 ST7789V는 최대 320개의 게이트 라인을 순차적으로 제어할 수 있습니다.
화면 갱신 시 게이트 드라이버가 한 줄씩 순차적으로 게이트 라인을 활성화하면서 해당 행의 모든 픽셀에 소스 라인의 색상 데이터를 동시에 기록하는 방식으로, 이를 통해 전체 화면을 위에서 아래로 스캔하며 업데이트합니다.
소스 라인(Source Line)
소스 라인은 LCD 패널의 세로 방향(열)에 해당하는 배선으로, 각 픽셀의 색상 정보를 전달하는 역할을 하며 ST7789V는 최대 240개의 소스 라인을 제어할 수 있어 240픽셀 폭의 화면을 구동합니다.
소스 드라이버가 RGB 색상 데이터를 아날로그 전압으로 변환하여 각 소스 라인에 공급하고, 이 전압이 TFT 트랜지스터를 통해 액정 셀에 전달되어 픽셀의 밝기와 색상을 결정합니다.
핵심: 소스 라인(세로/색상 데이터) + 게이트 라인(가로/스위칭 제어) = 매트릭스 픽셀 제어!
실제로 BLOCK DIAGRAM 을 확인해보면 Display RAM도 있고, 소스/게이트도 있습니다.

Display RAM (240 × 320 × 18bits): 화면에 표시할 픽셀 데이터(RGB 6-6-6 비트 = 총 18비트)를 저장. 예: MCU가 빨간색 화면으로 채우라고 데이터를 쓰면, 이 RAM에 “빨강 값”이 가득 채워짐.
소스 드라이버 부분 (왼쪽): Data Latch → Level Shifter → DAC → 720 Source Buffer
게이트 드라이버 부분 (오른쪽): Gate Decoder → Level Shifter → 320 Gate Buffer
#1. 초기화 단계
하드웨어 리셋: MCU(예: ESP32)가 리셋(RESX) → ST7789V 내부 레지스터 초기화.
MCU가 명령(Instruction Register) 전송: 인터페이스 모드 설정 (SPI/8080 등), 픽셀 포맷 (16bit, 18bit, 24bit), 메모리 주소 영역 설정 (Column/Row 범위), 감마 보정 값, 전압 레퍼런스 값 설정
#2. 데이터 쓰기 (프레임 버퍼 업데이트)
MCU가 픽셀 데이터(RGB565 등)를 Display RAM(240×320×18bit) 에 기록합니다. RAM에 저장된 값이 나중에 실제 패널에 표시됩니다. RAM은 단순한 비트 저장 공간이므로, 실제 화면에 나타내려면 주기적으로 패널로 데이터를 뿌려야 합니다.
#3. 디스플레이(패널) 구동
ST7789V가 내부적으로 한 줄씩 패널을 그려주는 과정입니다.
#4. 지속적 갱신
프레임 리프레시: OSC(내부 오실레이터) + Display Control 이 주기적으로 반복해서 G1→G2→…→G320 → 다시 G1 으로 돌아가며 화면을 계속 갱신합니다.
보통 초당 60번(60Hz) 반복해서 우리가 보는 화면은 깜빡이지 않고 안정적으로 표시됨됩니다. 화면을 지속적으로 갱신하고 깜빡임을 방지합니다.
TFT는 박막 트랜지스터(Thin-Film Transistor)의 약자입니다. TFT는 LCD(액정 디스플레이) 기술의 한 종류로, LCD의 화질과 성능을 크게 향상시키는 데 사용되는 핵심 기술입니다.
기본적인 LCD는 화질이나 응답 속도가 떨어지는 단점이 있었습니다. TFT 기술은 이러한 문제를 해결하기 위해 등장했습니다.
💻 이미지 출처: 삼성 디스플레이

TFT 디스플레이는 각 픽셀(화면의 최소 단위)마다 매우 작은 트랜지스터를 하나씩 배치합니다. 이 트랜지스터는 각 픽셀에 전달되는 전압과 전류를 정확하게 제어하는 스위치 역할을 합니다. 이를 통해 다음과 같은 장점을 얻을 수 있습니다.
모든 TFT 디스플레이는 LCD의 한 종류지만, 모든 LCD가 TFT인 것은 아닙니다. TFT 기술을 사용한 LCD를 TFT-LCD라고 부르며, 흔히 "액티브 매트릭스(Active-Matrix)" LCD라고도 합니다. 대부분의 LCD가 TFT-LCD라고 보면 될 거 같습니다.
TFT LCD보다 OLED가 전반적으로 더 좋은 디스플레이 기술로 평가받습니다. 하지만 각각의 장단점이 분명하기 때문에 어떤 용도로 사용할지에 따라 더 적합한 기술이 달라질 수 있습니다.

TFT LCD와 OLED의 차이를 쉬운 비유로 요약을 하자면 다음과 같습니다.
그 외에도 TFT LCD는 가격이 저렴한 대신, 두껍고 무거우며, 낮은 응답 속도를 가지며, OLED는 가격이 비싼 대신, 얇고 가벼운 디자인, 빠른 응답 속도를 가집니다.
💻 이미지 출처: LG 디스플레이

TTGO T-Display를 사용하기 위해서는 TFT_eSPI를 사용하는게 일반적인 거 같습니다. 하지만 해당 라이브러리는 Arduino IDE에서 사용 가능하며 ESP-IDF로 개발하기 위해서는 다른 방식을 사용해야 됩니다.
💻 참고. Reddit > "Best way to use ST7789 in ESPIDF?"
드라이버를 직접 구현하는 방법은 ST7789V 데이터시트를 기반으로 SPI 통신을 통해 디스플레이의 레지스터를 직접 제어하는 코드를 작성해야 합니다. 픽셀을 그리고, 라인을 그리는 등 필요한 모든 그래픽 함수를 직접 만들어야 합니다. 이 과정은 시간이 많이 소요되고 높은 수준의 하드웨어 지식을 요구합니다.
다른 방법은 이미 만들어진 오픈소스 드라이버 컴포넌트를 사용하는 것입니다. esp-idf-st7789 나 esp32-lcd-panel 과 같은 ESP-IDF용 ST7789 드라이버 컴포넌트가 있습니다.
💻 참고. Reddit > "st7789 display with esp-idf"
자료를 더 조사하다보니, ESP_LCD API 를 사용할 수 있을 거 같습니다. 아무래도 ESP 공식 문서에서 제공하는 거 같아서 이게 괜찮아 보입니다.
그 외에도 LovyanGFX 라는 디스플레이 드라이버, 그리고 임베디드 그래픽 라이브러리 LVGL을 사용하는 방식도 있네요. lvgl이 생각보다 유명한가 봅니다. 이건 나중에 한번 사용해봐야겠네요.
TTGO T-Display는 SPI(Serial Peripheral Interface)를 기본 통신 방식으로 사용합니다. 내장된 ST7789V 디스플레이 컨트롤러가 SPI 인터페이스를 통해 ESP32 마이크로컨트롤러와 데이터를 주고받습니다.
아래 그림을 보면 알 수 있듯이, SPI 통신(MOSI, SCLK, CS) 을 하네요.

| 핀 명칭 | ESP32 GPIO 번호 | 용도 |
|---|---|---|
| MOSI (Master Out Slave In) | 19 | ESP32에서 디스플레이로 데이터를 보내는 라인입니다. |
| SCLK (Serial Clock) | 18 | 데이터 전송 속도를 동기화하는 클럭 신호입니다. |
| CS (Chip Select) | 5 | 여러 SPI 장치 중 디스플레이를 선택하는 신호입니다. |
| DC (Data/Command) | 16 | 전송되는 데이터가 명령인지 픽셀 데이터인지 구분하는 신호입니다. |
| RST (Reset) | 23 | 디스플레이를 초기화하는 리셋 신호입니다. |
| BL (Backlight) | 4 | 디스플레이 백라이트를 켜고 끄는 역할을 합니다. |
SPI(Serial Peripheral Interface)는 마이크로컨트롤러(MCU)와 주변 장치 간에 데이터를 전송하기 위한 동기식 직렬 통신 프로토콜입니다. 동시에 양방향 통신이 가능하여 빠르고 효율적인 데이터 전송이 필요한 장치(LCD, SD 카드 모듈, 센서 등)에 널리 사용됩니다.
SPI는 마스터-슬레이브(Master-Slave) 구조로 동작합니다. TTGO T-Display에서는 ESP32가 마스터 역할을 하고, ST7789V 디스플레이 컨트롤러가 슬레이브 역할을 합니다.
SPI 통신은 4개의 기본 신호 라인을 사용합니다.
동작 원리 (과정)
위에서 말했다싶이, ESP_LCD API 공식 문서와 ESP-IDF examples를 참고해서 테스트를 진행해보겠습니다.
먼저 필요한 헤더 파일입니다. 선택사항은 생략해도 되며, LCD 전용 헤더는 추가해줘야 합니다.
// 기본 시스템 헤더
#include <stdio.h> // printf, sprintf 등 표준 입출력 함수
#include <stdlib.h> // malloc, free 등 메모리 할당 함수
#include <string.h> // memset, memcpy 등 문자열/메모리 조작 함수
#include "freertos/FreeRTOS.h" // FreeRTOS 운영체제 기본 헤더
#include "freertos/task.h" // vTaskDelay, xTaskCreate 등 태스크 관리 함수
#include "esp_system.h" // ESP32 시스템 기본 함수들
#include "esp_log.h" // ESP_LOGI, ESP_LOGE 등 로깅 함수
#include "esp_err.h" // ESP_ERROR_CHECK, esp_err_t 등 에러 처리
// GPIO 관련 헤더
#include "driver/gpio.h" // GPIO 핀 제어 (백라이트, 리셋 핀 등)
#include "driver/spi_master.h" // SPI 통신 마스터 모드 함수들
// LCD 전용 헤더
#include "esp_lcd_panel_io.h" // LCD 패널 I/O 인터페이스 (SPI/I2C 통신)
#include "esp_lcd_panel_vendor.h" // 특정 LCD 드라이버 (ST7789, ILI9341 등)
#include "esp_lcd_panel_ops.h" // LCD 패널 조작 함수들 (init, reset, draw 등)
// 그래픽/폰트 관련 헤더 (선택사항)
#include "esp_lvgl_port.h" // LVGL 그래픽 라이브러리 포팅 레이어
#include "lvgl.h" // LVGL GUI 라이브러리 메인 헤더
다음으로 핀 정의를 해줍니다.
#define PIN_NUM_MOSI 19
#define PIN_NUM_SCLK 18
#define PIN_NUM_CS 5
#define PIN_NUM_DC 16
#define PIN_NUM_RST 23
#define PIN_NUM_BKLT 4
이제부터는 app_main 함수에 포함되는 내용입니다.
void app_main(void)
{
gpio_config_t bk_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << PIN_NUM_BKLT
};
// Initialize the GPIO of backlight
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
...
gpio_config_t 구조체를 사용하여 GPIO 설정을 정의합니다. GPIO_MODE_OUTPUT으로 출력 모드를 설정하고, pin_bit_mask에 백라이트 핀 번호를 비트마스크 형태로 지정합니다.
gpio_config(&bk_gpio_config)로 실제 GPIO를 초기화하고, ESP_ERROR_CHECK로 설정이 성공했는지 확인하여 오류 시 프로그램을 중단시킵니다. 이후 gpio_set_level()로 백라이트 ON/OFF 제어가 가능해집니다.
LCD 디스플레이와 통신하기 위한 SPI 버스를 초기화하는 코드입니다.
#define LCD_H_RES 240 // 가로(너비)
#define LCD_V_RES 135 // 세로(높이)
spi_bus_config_t bus_config = {
.sclk_io_num = PIN_NUM_SCLK,
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = -1,
.quadhd_io_num = -1,
.quadwp_io_num = -1,
.max_transfer_sz = LCD_H_RES * LCD_V_RES * 2 // 화면 버퍼 크기
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO));
SCLK(클럭), MOSI(데이터 출력) 핀을 지정하고 Quad SPI는 사용하지 않으므로 비활성화합니다. LCD는 단방향 출력만 하므로 MISO도 비활성화 해줍니다.
최대 전송 크기를 135 x 240로 설정합니다. 해당 픽셀 화면에서 각 픽셀당 2바이트(16비트 컬러)를 사용하므로 전체 화면 데이터를 한 번에 전송할 수 있는 버퍼 크기를 설정합니다.
SPI2_HOST로 SPI 버스를 초기화합니다. 참고로, ESP32에는 SPI0_HOST(플래시 전용), SPI1_HOST(플래시 전용), SPI2_HOST(범용), SPI3_HOST(범용) 총 4개의 SPI 컨트롤러가 있습니다. SPI2_HOST는 일반적으로 LCD, SD카드, 센서 등 외부 장치와 통신할 때 사용하는 범용 SPI 버스로, 사용자가 자유롭게 핀을 설정하고 DMA를 활용할 수 있어서 가장 많이 사용됩니다.
DMA 채널을 자동 할당(SPI_DMA_CH_AUTO)하여 고속 데이터 전송이 가능하도록 하고, 설정 완료 후 LCD와 SPI 통신할 준비를 마칩니다.
참고로, 최대 전송 크기 방식을 위에처럼 전체 화면을 한 번에 전송하는 방식으로 사용할 수 있고, 다른 방법으로는 메모리 효율을 위해 화면을 여러 개의 수평 라인 그룹으로 나누어 순차 전송하는 방식을 사용할 수 있다고 합니다.
// 전송 속도를 높이기 위해, 모든 SPI 전송은 여러 줄을 묶어서 보냅니다.
// 이 정의는 몇 줄을 묶을지 지정합니다.
// 더 많이 묶으면 메모리 사용량은 늘어나지만, 전송 설정/완료에 대한 오버헤드는 줄어듭니다.
// 240이 이 값으로 나누어떨어지는지 확인하세요. ex) 16 사용 가능
#define PARALLEL_LINES 16
// 설정
.max_transfer_sz = PARALLEL_LINES * LCD_H_RES * 2 + 8
첫 번째는 전체 화면 크기로 설정했지만 두 번째는 화면을 여러 줄씩 나누어 전송하는 방식을 사용합니다. 그러면 두 방식에 있어서, 내가 직접 보기에도 차이가 날까요? 🤔
전체 화면 전송은 한 번에 모든 픽셀이 업데이트되어 깜박임 없이 매끄러운 화면 전환이 가능하지만, 라인별 전송은 위에서 아래로 순차적으로 그려지는 과정이 보일 수 있습니다. 일반적인 경우라면 차이를 느끼기 거의 어렵고, 고속 애니메이션인 경우에는 차이가 느껴질 수 있다고 하네요.
SPI 방식 LCD 패널과 통신하기 위한 I/O 핸들러를 생성
#define LCD_PIXEL_CLK_HZ (10 * 1000 * 1000)
#define LCD_CMD_BITS 8
#define LCD_DATA_BITS 8
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = PIN_NUM_DC, // 데이터/명령 구분 핀
.cs_gpio_num = PIN_NUM_CS, // 칩 셀렉트 핀
.pclk_hz = LCD_PIXEL_CLK_HZ,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_DATA_BITS,
.spi_mode = 0,
.trans_queue_depth = 10,
};
// Attach the LCD to the SPI bus
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle));
클럭 주파수를 10MHz로 설정하고, SPI 모드 0을 사용하며, 전송 큐 깊이를 10으로 설정하여 동시에 처리할 수 있는 전송 작업 수를 제한합니다.
큐가 가득 차면 새로운 전송 요청은 대기하게 되고, 값이 클수록 더 많은 메모리를 사용하지만 더 부드러운 전송이 가능하므로 LCD 같은 고속 데이터 전송에서 중요합니다.
lcd_cmd_bits, lcd_param_bits 필드: LCD 명령어와 매개변수의 비트 길이를 명시적으로 지정할 수 있어 더 정확한 통신이 가능합니다. LCD 드라이버에 따라 명령어 형식이 다를 수 있는 상황에 대응할 수 있습니다.
ST7789 LCD 드라이버 칩을 위한 패널 핸들러를 생성하는 코드
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_NUM_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
};
// Initialize the LCD configuration
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
reset_gpio_num으로 LCD 리셋 핀을 지정합니다. rgb_ele_order 이건 뒤에서 설명하겠습니다.
ST7789 드라이버 특화 패널 생성: esp_lcd_new_panel_st7789()로 앞서 생성한 I/O 핸들러를 사용하여 ST7789 칩에 특화된 패널 객체를 생성합니다. 이는 해당 칩의 명령어와 초기화 시퀀스를 알고 있습니다.
생성된 panel_handle을 통해 이후 esp_lcd_panel_init(), esp_lcd_panel_draw_bitmap() 등의 고수준 함수로 LCD를 쉽게 제어할 수 있게 됩니다.
핵심: I/O 핸들러 + ST7789 드라이버 설정 → 고수준 LCD 제어용 패널 핸들러 생성!
#define LCD_BK_LINGH_ON_LEVEL 1
#define LCD_BK_LINGH_OFF_LEVEL 0
// Turn off backlight to avoid unpredictable display on the LCD screen while initializing
// the LCD panel driver. (Different LCD screens may need different levels)
ESP_ERROR_CHECK(gpio_set_level(PIN_NUM_BK_LIGHT, LCD_BK_LIGHT_OFF_LEVEL));
// Reset the display
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
// Initialize LCD panel
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
// Turn on the screen
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
// Swap x and y axis (Different LCD screens may need different options)
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_handle, true));
// Turn on backlight (Different LCD screens may need different levels)
ESP_ERROR_CHECK(gpio_set_level(PIN_NUM_BK_LIGHT, LCD_BK_LIGHT_ON_LEVEL));
TTGO T-Display는 1.14인치 135×240 픽셀 해상도로, 화면 비율은 약 9:16 입니다. 초기 화면 좌표값은 다음과 같습니다.

esp_lcd_panel_swap_xy(true)를 실행하면 X와 Y 축이 교환되어 원래 135×240(세로형)이 240×135(가로형)로 바뀝니다. 16:9 비율의 와이드 화면으로 사용할 수 있게 됩니다. 주의할 점은 (0,0) 좌표는 바뀌지 않습니다.

esp_lcd_panel_disp_on_off(true)를 실행해야 합니다. 대부분의 LCD 드라이버(ST7789 포함)는 초기화 후 기본적으로 디스플레이 출력이 꺼진 상태(sleep mode)로 시작하므로, 화면에 아무것도 표시되지 않습니다. 이 함수를 호출해야 LCD 내부 회로가 활성화되어 실제로 픽셀 데이터가 화면에 표시되며, 이 함수 없이는 데이터를 전송해도 검은 화면만 보입니다.
esp_lcd_panel_invert_color(true)는 색상을 반전시키는 명령입니다. 이는 LCD 패널마다 색상 매핑이 다르기 때문입니다. 0x0000이 검은색, 0xFFFF가 흰색으로 나오는지 확인하십쇼.
초기화까지 했다면 이제 LCD에 보여주고 싶은 걸 작성하면 됩니다. 저는 간단한 테스트로 전체화면으로 빨간색 배경을 띄워보도록 하겠습니다.
uint16_t *color_data = (uint16_t *)malloc(sizeof(uint16_t) * LCD_H_RES * LCD_V_RES);
for (int i = 0; i < LCD_H_RES * LCD_V_RES; i++) {
color_data[i] = 0xF800; // 0xF800(R), 0x07E0(G), 0x001F(B)
}
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, LCD_H_RES, LCD_V_RES, color_data));
free(color_data);
자... 여기서 부터 중요한 두가지 문제가 발생했습니다. 첫번째는 화면크기가 안맞다는 것이고, 두번째는 색상값이 다르다는 것입니다. 여기서 삽질을 좀 했습니다...
정말 화면이 135 x 240 이 맞는건지 의심이 들 정도로 전체 화면이 채워지질 않더군요. 값을 이것저것 넣어봐도 뭔가 이상했습니다. 그러다가 겨우 원인을 알아냈는데...
💻 참고: https://projectitis.com/getting-ttgo-esp32-based-t-display-to-work/
"중요! 아래 오프셋(52,40)은 디스플레이의 '일반' 세로 방향이지만 디스플레이가 회전(하드웨어 회전)을 사용하는 경우 변경됩니다. 그러니 이것을 조심하십시오. 답을 찾는 가장 좋은 장소는 Adafruit ST7899 드라이버 소스 코드"
엥? 오프셋? 그런게 있다고???? (💻 참고. TFT_eSPI 코드). 실제로 TFT_eSPI 코드를 확인해보니 위에서 말한 (52,40)이란 값이 있습니다.
switch (rotation) {
case 0: // Portrait
#ifdef CGRAM_OFFSET
if (_init_width == 135) // ✅
{
colstart = 52;
rowstart = 40;
}
else if(_init_height == 280)
{
colstart = 0;
rowstart = 20;
}
else if(_init_width == 172)
{
colstart = 34;
rowstart = 0;
}
else if(_init_width == 170)
{
colstart = 35;
rowstart = 0;
}
else
{
colstart = 0;
rowstart = 0;
}
#endif
case 1: // Landscape (Portrait + 90)
#ifdef CGRAM_OFFSET
if (_init_width == 135)
{
colstart = 40;
rowstart = 53;
}
else if(_init_height == 280)
{
colstart = 20;
rowstart = 0;
}
else if(_init_width == 172)
{
colstart = 0;
rowstart = 34;
}
else if(_init_width == 170)
{
colstart = 0;
rowstart = 35;
}
else
{
colstart = 0;
rowstart = 0;
}
그림으로 그려보면 다음과 같달까요?

회전 모드에 따라 오프셋이 달라집니다: 0도 회전(세로)에서는 colstart=52, rowstart=40이고, 90도 회전(가로)에서는 colstart=40, rowstart=53으로 X/Y 값이 바뀝니다.
이러한 오프셋이 생기는 이유가 뭘까요?
결론은 드라이버 IC가 지원하는 해상도와 TTGO T-Display 화면 크기 불일치로 좌표 보정이 필요하다는 거군요. 흑흑...
#define LCD_X_OFFSET 40
#define LCD_Y_OFFSET 52
ESP_ERROR_CHECK(
esp_lcd_panel_draw_bitmap(
panel_handle,
0 + LCD_X_OFFSET,
0 + LCD_Y_OFFSET,
LCD_H_RES + LCD_X_OFFSET,
LCD_V_RES + LCD_Y_OFFSET,
color_data)
);
이렇게 기존 좌표에 오프셋 값을 더 해주면 해결이 됩니다.
우리는 먼저 RGB565 색상 포맷을 알아야 합니다.
RGB565는 16비트로 색상을 표현하는 포맷으로, 빨강(R) 5비트 + 초록(G) 6비트 + 파랑(B) 5비트 = 총 16비트를 사용합니다. 초록에 1비트를 더 할당한 이유는 인간의 눈이 초록색에 가장 민감하기 때문입니다.
색상 표현 예시: 빨강(0xF800), 초록(0x07E0), 파랑(0x001F), 흰색(0xFFFF), 검정(0x0000)으로 표현되며, 총 65,536가지(2^16) 색상을 표현할 수 있습니다.
메모리 효율성이 뛰어나 24비트 RGB보다 절반의 메모리만 사용하면서도 충분한 색상 품질을 제공하여, LCD 디스플레이나 임베디드 시스템에서 널리 사용되는 표준 색상 포맷입니다.
💻 깃허브 이슈: https://github.com/espressif/esp-idf/issues/10459 (저와 완전 동일한 현상이군요)
파란색(0x001F)을 설정하면 초록색이, 빨간색(0xF800)을 설정하면 파란색이, 초록색(0x07E0)을 설정하면 빨간색이 표시됩니다. Adafruit_ST7789 라이브러리에서는 같은 LCD가 올바른 색상으로 표시되므로, ESP-IDF의 ST7789 드라이버에서 색상 매핑이나 엔디안 설정에 문제가 있을 가능성이 높습니다.
rgb_ele_order 를 LCD_RGB_ELEMENT_ORDER_BGR 로 바꿔줬더니... 빨간색은 제대로 나오는데, 파란색이 초록색으로 나오는 이슈는 계속 있습니다.
rgb_endian 설정을 바꿔봐도 문제가 해결되지 않았습니다. ㅠㅠ 도대체~~~!
💻 깃허브 이슈: https://github.com/espressif/esp-idf/issues/11416
ESP-IDF의 ST7789 드라이버에서 RGB 엔디안 설정 문제에 대한 버그를 설명하고 있습니다.
ESP-IDF의 잘못된 구현: ESP-IDF는 ST7789의 MADCTL 레지스터(36h)의 D3 비트를 RGB/BGR 엔디안 설정으로 사용하고 있지만, 실제로는 이 비트는 다른 용도입니다.
ST7789 데이터시트에 따르면, 실제 RGB 엔디안은 RAMCTRL 레지스터(B0h)의 두 번째 매개변수 D3 비트(ENDIAN)로 설정해야 합니다.
이슈 작성자가 제안한 해결 방법은 다음과 같습니다.
// LCD 초기화 후 추가 설정
esp_lcd_panel_io_tx_param(io_handle, RAMCTRL, (uint8_t[]){0x00, 0xE8}, 2);
TTGO T-Display S3 등에서 색상 순서 문제가 발생하며, 이 비트가 설정되지 않으면 빨강-파랑-초록 순서로 나타나는 문제가 생깁니다.
이렇게 해주니까 이제 해결이 되었습니다. 정말 어렵네요... 자료 찾기가 무엇보다 어렵습니다.
가로 모드 설정은 했지만 (0, 0)이 좌측 하단에 있는 건 뭔가 이상합니다. 좌측 상단으로 바꿔주고 싶은데 방법이 없을까요?

esp_lcd_panel_mirror을 사용하면 X축, Y축을 뒤집을 수 있습니다.
// X축 미러링 함, Y축 미러링 안함
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false));
기존에 화면 그렸던 내용이 계속 남아있는 문제가 있습니다. 따라서 LCD 화면을 초기화해서 이전 화면을 지우고 싶습니다.
그러기 위해서는 전체 화면을 검은색으로 덮어쓰는 방법이 있습니다.
// 화면 초기화
uint16_t line_buffer[LCD_H_RES];
memset(line_buffer, 0x00, sizeof(line_buffer));
for (int i = 0; i < LCD_V_RES; i++) {
ESP_ERROR_CHECK(
esp_lcd_panel_draw_bitmap(
panel_handle,
0 + LCD_X_OFFSET,
i + LCD_Y_OFFSET,
LCD_H_RES + LCD_X_OFFSET,
i + LCD_Y_OFFSET + 1,
line_buffer
)
);
}
효율적인 방법으로 작은 버퍼를 여러 번 그려서 메모리를 절약할 수 있습니다.
화면을 6등분하여 각각 다른 색상으로 채우기
uint16_t *color_data = (uint16_t *)malloc(sizeof(uint16_t) * LCD_H_RES * LCD_V_RES);
for (int y = 0; y < LCD_V_RES; y++) {
for (int x = 0; x < LCD_H_RES; x++) {
if (y < LCD_V_RES / 2) { // 상단 3구역
if (x < LCD_H_RES / 3) {
color_data[y * LCD_H_RES + x] = 0xF800; // 빨강
} else if (x < (LCD_H_RES * 2) / 3) {
color_data[y * LCD_H_RES + x] = 0x07E0; // 초록
} else {
color_data[y * LCD_H_RES + x] = 0x001F; // 파랑
}
} else { // 하단 3구역
if (x < LCD_H_RES / 3) {
color_data[y * LCD_H_RES + x] = 0xFFE0; // 노랑 (빨강 + 초록)
} else if (x < (LCD_H_RES * 2) / 3) {
color_data[y * LCD_H_RES + x] = 0xF81F; // 마젠타 (빨강 + 파랑)
} else {
color_data[y * LCD_H_RES + x] = 0x07FF; // 시안 (초록 + 파랑)
}
}
}
}
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel_handle, 0 + LCD_X_OFFSET, 0 + LCD_Y_OFFSET,
LCD_H_RES + LCD_X_OFFSET, LCD_V_RES + LCD_Y_OFFSET,
color_data));
free(color_data);
실제로는 이런 순서대로 채워지겠네요.

결과는 다음과 같습니다. 위 내용과 일치하네요. 좋습니다.
