Display 실험실 #1: TTGO T-Display 사용해보기 (feat. ESP-LCD, ST7789)

기운찬곰·2025년 9월 22일

Display 실험실

목록 보기
1/2
post-thumbnail

TTGO T-Display

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

👉 알리익스프레스 참고: https://ko.aliexpress.com/item/4000509604970.html

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

주요 특징 및 사양

기본적인 스펙

  • MCU: ESP32 Tensilica Xtensa 듀얼 코어 LX6 마이크로프로세서
  • 무선 전송: Wi-Fi 802.11 b/g/n, Bluetooth V4.2+BLE 지원
  • 프로그래밍 환경: Arduino IDE, Micropython, VS Code
  • 온보드 기능: 듀얼 버튼 (GPIO0+GPIO35), 배터리 레벨 감지
  • 버전 옵션: 직렬 칩: CH9102/CP2104, 플래시: 4MB/16MB (해당 제품은 4MB 인듯)

1.14인치 ST7789V HD LCD:

  • 해상도 135X240, 1.14인치, 고밀도 260 ppi
  • 4선 SPI 직렬 버스 채택, 풀컬러, 풀뷰 IPS LCD
  • 드라이버 IC: ST7789V, Arduino 그래픽 라이브러리 TFT_eSPI 지원

전원 공급

  • USB/리튬 배터리의 듀얼 모드 전원 공급 지원
  • 리튬 배터리 소켓 사양: JST GH 1.25mm

디스플레이는 SPI 통신으로 연결되어 있는 거 같네요. (MOSI, SCLK, CS 등)

TTGO T-Display는 주로 Arduino IDE나 MicroPython 플랫폼을 사용하여 프로그래밍합니다. 특히 Arduino IDE를 사용할 경우, ESP32 Dev Module 보드를 선택하고 TFT_eSPI와 같은 관련 라이브러리를 설치하여 디스플레이를 쉽게 제어할 수 있습니다.

프로젝트 예시

내장된 컬러 디스플레이와 무선 통신 기능을 활용하여 다음과 같은 다양한 프로젝트에 적용할 수 있습니다.

  • 소형 IoT 장치의 상태 표시
  • 환경 센서 데이터 시각화 (온도, 습도 등)
  • 미니 게임 콘솔
  • 스마트 시계 또는 시계 프로젝트
  • 휴대용 데이터 로거
  • 무선 원격 제어 인터페이스

스마트 시계 정도 만들어보면 재미있을 거 같네요. 아니면 온습도 시각화도 재밌을 거 같고요.


ST7789V 란?

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)로 되어 있습니다.

  • 가로 방향: 행(Row, 행) - 240
  • 세로 방향: 컬럼(Column, 열) - 320

각 픽셀은 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

  • Data Latch: RAM에서 읽은 픽셀 데이터를 잠깐 저장.
  • Level Shifter: 로직 전압(예: 1.8V, 3.3V)을 LCD 구동 전압(예: 5~10V)으로 변환.
  • DAC: 디지털(RGB 데이터)을 아날로그 전압으로 바꿔서 액정 투과율을 조절.
  • 720 Source Buffer: 실제 패널의 소스 라인(S1~S720) 에 데이터를 공급. 왜 720이냐? RGB 각각을 따로 전압 라인으로 내보내기 때문에 (240픽셀 × 3컬러 = 720채널).

게이트 드라이버 부분 (오른쪽): Gate Decoder → Level Shifter → 320 Gate Buffer

  • Gate Decoder: 현재 어느 행(Row)을 선택할지 결정.
  • Level Shifter: 로직 신호를 TFT 게이트가 열릴 수 있는 고전압 신호로 변환.
  • 320 Gate Buffer: 패널의 세로 라인(G1~G320)을 차례대로 켜 줌.

동작 원리

#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가 내부적으로 한 줄씩 패널을 그려주는 과정입니다.

  1. 게이트 드라이버 (G1~G320): "이번에는 1번째 행(Row)을 열겠다" → Gate Decoder가 G1을 활성화. TFT의 한 행(320픽셀)이 동시에 켜집니다.
  2. 소스 드라이버 (S1~S720): Display RAM에서 1행의 픽셀 데이터(240픽셀 × RGB 3색 = 720채널)를 읽습니다. 데이터 래치 → DAC 변환 → 아날로그 전압으로 바꿔서 소스 라인에 출력합니다.
  3. 소스 라인의 아날로그 전압이 TFT 트랜지스터를 통해 액정 셀에 인가되어 액정 분자의 배열을 변경합니다. 변경된 액정 배열에 따라 백라이트가 선택적으로 투과되어 최종적으로 색상이 표시됩니다.
  4. 그리고 나서 다음 행으로 이동합니다. G1 OFF → G2 ON. 다시 소스 드라이버가 2번째 행 데이터 출력됩니다. 이 과정을 G1 ~ G320 까지 반복하면 한 프레임이 완성됩니다.

#4. 지속적 갱신

프레임 리프레시: OSC(내부 오실레이터) + Display Control 이 주기적으로 반복해서 G1→G2→…→G320 → 다시 G1 으로 돌아가며 화면을 계속 갱신합니다.

보통 초당 60번(60Hz) 반복해서 우리가 보는 화면은 깜빡이지 않고 안정적으로 표시됨됩니다. 화면을 지속적으로 갱신하고 깜빡임을 방지합니다.


TFT LCD 란?

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 vs. OLED

TFT LCD보다 OLED가 전반적으로 더 좋은 디스플레이 기술로 평가받습니다. 하지만 각각의 장단점이 분명하기 때문에 어떤 용도로 사용할지에 따라 더 적합한 기술이 달라질 수 있습니다.

TFT LCD와 OLED의 차이를 쉬운 비유로 요약을 하자면 다음과 같습니다.

  1. 발광 방식: TFT LCD는 손전등 + 색깔 필터와 같아서 뒤에서 백라이트가 항상 켜져 있고 액정이 셔터 역할을 하여 빛을 차단하거나 통과시키는 방식이고, OLED는 네온사인처럼 각 픽셀이 스스로 빛을 내는 자발광 방식입니다.
  2. 검은색 표현: LCD는 검은색을 표현할 때도 백라이트가 켜져 있어서 완전한 검정이 아닌 회색빛이 나지만, OLED는 해당 픽셀을 완전히 끄면 진짜 검은색이 나와서 명암비가 훨씬 뛰어납니다.
  3. 전력 소모와 수명: LCD는 밝은 화면에서 전력 소모가 일정하지만 OLED는 검은 화면에서 전력을 거의 안 쓰는 대신 밝은 화면에서 많이 소모하고, LCD는 수명이 길지만 OLED는 시간이 지나면 번인(잔상) 현상이 생길 수 있습니다.

그 외에도 TFT LCD는 가격이 저렴한 대신, 두껍고 무거우며, 낮은 응답 속도를 가지며, OLED는 가격이 비싼 대신, 얇고 가벼운 디자인, 빠른 응답 속도를 가집니다.

💻 이미지 출처: LG 디스플레이


TTGO T-Display 사용 방법

TTGO T-Display를 사용하기 위해서는 TFT_eSPI를 사용하는게 일반적인 거 같습니다. 하지만 해당 라이브러리는 Arduino IDE에서 사용 가능하며 ESP-IDF로 개발하기 위해서는 다른 방식을 사용해야 됩니다.

💻 참고. Reddit > "Best way to use ST7789 in ESPIDF?"

드라이버를 직접 구현하는 방법은 ST7789V 데이터시트를 기반으로 SPI 통신을 통해 디스플레이의 레지스터를 직접 제어하는 코드를 작성해야 합니다. 픽셀을 그리고, 라인을 그리는 등 필요한 모든 그래픽 함수를 직접 만들어야 합니다. 이 과정은 시간이 많이 소요되고 높은 수준의 하드웨어 지식을 요구합니다.


다른 방법은 이미 만들어진 오픈소스 드라이버 컴포넌트를 사용하는 것입니다. esp-idf-st7789esp32-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)19ESP32에서 디스플레이로 데이터를 보내는 라인입니다.
SCLK (Serial Clock)18데이터 전송 속도를 동기화하는 클럭 신호입니다.
CS (Chip Select)5여러 SPI 장치 중 디스플레이를 선택하는 신호입니다.
DC (Data/Command)16전송되는 데이터가 명령인지 픽셀 데이터인지 구분하는 신호입니다.
RST (Reset)23디스플레이를 초기화하는 리셋 신호입니다.
BL (Backlight)4디스플레이 백라이트를 켜고 끄는 역할을 합니다.

SPI 통신 방법

SPI(Serial Peripheral Interface)는 마이크로컨트롤러(MCU)와 주변 장치 간에 데이터를 전송하기 위한 동기식 직렬 통신 프로토콜입니다. 동시에 양방향 통신이 가능하여 빠르고 효율적인 데이터 전송이 필요한 장치(LCD, SD 카드 모듈, 센서 등)에 널리 사용됩니다.

SPI는 마스터-슬레이브(Master-Slave) 구조로 동작합니다. TTGO T-Display에서는 ESP32가 마스터 역할을 하고, ST7789V 디스플레이 컨트롤러가 슬레이브 역할을 합니다.

  • 마스터(Master): 통신을 주도하고 클럭 신호를 생성하는 장치입니다. 보통 마이크로컨트롤러가 마스터 역할을 합니다.
  • 슬레이브(Slave): 마스터의 명령에 따라 동작하는 장치입니다. 디스플레이 컨트롤러(예: ST7789V), 센서, 메모리 칩 등이 슬레이브 역할을 합니다.

SPI 통신은 4개의 기본 신호 라인을 사용합니다.

  • SCLK (Serial Clock): 마스터가 생성하는 클럭 신호입니다. 모든 데이터 전송은 이 클럭에 동기화되어 이루어집니다.
  • MOSI (Master Out Slave In): 마스터가 슬레이브로 데이터를 보내는 라인입니다.
  • MISO (Master In Slave Out): 슬레이브가 마스터로 데이터를 보내는 라인입니다.
  • CS (Chip Select) / SS (Slave Select): 마스터가 통신할 슬레이브를 선택하는 라인입니다. 이 핀이 LOW 상태일 때만 해당 슬레이브가 통신 준비를 합니다. 마스터는 여러 슬레이브와 연결될 수 있으며, 각 슬레이브는 고유한 CS 핀을 가집니다.

동작 원리 (과정)

  1. 슬레이브 선택: 마스터는 통신하려는 슬레이브의 CS 핀을 LOW로 만듭니다. 이 핀이 LOW가 되면 해당 슬레이브가 활성화됩니다.
  2. 데이터 전송: 마스터는 SCLK 핀을 통해 클럭 펄스를 생성하고, 이 클럭에 맞춰 MOSI 핀으로 데이터를 전송합니다. 동시에, 슬레이브는 MISO 핀을 통해 마스터로 데이터를 보냅니다.
  3. 동시 양방향 통신: SPI는 전이중(Full-duplex) 통신을 지원하여, 마스터가 데이터를 보내는 동시에 슬레이브로부터 데이터를 받을 수 있습니다.
  4. 통신 종료: 데이터 전송이 끝나면 마스터는 CS 핀을 다시 HIGH로 만듭니다. 이렇게 하면 슬레이브는 통신을 종료하고 대기 상태로 돌아갑니다.

ESP_LCD API 사용법

위에서 말했다싶이, 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

LCD 백라이트 제어용 GPIO 설정

이제부터는 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 제어가 가능해집니다.

SPI 버스 초기화

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

첫 번째는 전체 화면 크기로 설정했지만 두 번째는 화면을 여러 줄씩 나누어 전송하는 방식을 사용합니다. 그러면 두 방식에 있어서, 내가 직접 보기에도 차이가 날까요? 🤔

전체 화면 전송은 한 번에 모든 픽셀이 업데이트되어 깜박임 없이 매끄러운 화면 전환이 가능하지만, 라인별 전송은 위에서 아래로 순차적으로 그려지는 과정이 보일 수 있습니다. 일반적인 경우라면 차이를 느끼기 거의 어렵고, 고속 애니메이션인 경우에는 차이가 느껴질 수 있다고 하네요.

I/O 핸들러 생성

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으로 설정하여 동시에 처리할 수 있는 전송 작업 수를 제한합니다.

  • SPI 모드 0: CPOL=0, CPHA=0 설정으로, 클럭이 평상시에 LOW 상태이고(CPOL=0) 클럭의 상승 엣지에서 데이터를 샘플링하는(CPHA=0) 방식입니다. 대부분의 LCD 컨트롤러(ST7789, ILI9341 등)가 SPI 모드 0을 사용하므로 가장 일반적인 설정이며, 마스터와 슬레이브가 같은 SPI 모드를 사용해야 정상적인 통신이 가능합니다.
  • 전송 큐 깊이(Transaction Queue Depth): 동시에 대기할 수 있는 SPI 전송 작업의 최대 개수. 10이면 최대 10개의 SPI 전송 명령을 큐에 쌓아둘 수 있습니다. 비동기 전송에서 성능 향상을 위해 사용되며, CPU가 SPI 전송 완료를 기다리지 않고 다음 전송 작업을 미리 큐에 넣어두어 연속적인 데이터 전송이 가능합니다:

큐가 가득 차면 새로운 전송 요청은 대기하게 되고, 값이 클수록 더 많은 메모리를 사용하지만 더 부드러운 전송이 가능하므로 LCD 같은 고속 데이터 전송에서 중요합니다.

lcd_cmd_bits, lcd_param_bits 필드: LCD 명령어와 매개변수의 비트 길이를 명시적으로 지정할 수 있어 더 정확한 통신이 가능합니다. LCD 드라이버에 따라 명령어 형식이 다를 수 있는 상황에 대응할 수 있습니다.

Panel 핸들러 생성

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 제어용 패널 핸들러 생성!

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));
  1. 백라이트 끄기: LCD 초기화 중 예측 불가능한 화면 표시를 방지하기 위해 백라이트를 먼저 끕니다.
  2. LCD 하드웨어 리셋: esp_lcd_panel_reset()으로 LCD 칩을 물리적으로 리셋하여 깨끗한 상태로 만듭니다. (화면이 초기화되진 않습니다...)
  3. LCD 패널 초기화: esp_lcd_panel_init()으로 ST7789 칩의 내부 레지스터들을 설정하고 LCD를 사용 가능한 상태로 만듭니다.
  4. 화면 표시 활성화: esp_lcd_panel_disp_on_off(true)로 LCD 화면 출력을 켭니다.
  5. 색상 반전 설정: esp_lcd_panel_invert_color(true)로 색상을 반전시켜 올바른 색상이 표시되도록 조정합니다.
  6. X/Y 축 교환: esp_lcd_panel_swap_xy(true)로 화면 방향을 회전시켜 올바른 방향으로 표시되도록 합니다.
  7. 백라이트 켜기: 모든 초기화가 완료된 후 백라이트를 켜서 화면을 볼 수 있게 합니다.

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 값이 바뀝니다.

이러한 오프셋이 생기는 이유가 뭘까요?

  1. 드라이버 IC의 전체 해상도와 실제 LCD 패널 크기가 다르기 때문입니다 - ST7789는 240x320 해상도를 지원하지만, TTGO T-Display는 135x240 크기의 작은 패널만 사용하므로 전체 영역 중 일부만 활성화됩니다.
  2. LCD 패널이 드라이버 IC의 픽셀 매트릭스 중앙이나 특정 위치에 배치되어 있어서, 논리적 좌표 (0,0)이 실제 화면의 좌상단과 일치하지 않습니다 - 예를 들어 실제 화면 시작점이 (52, 40) 위치에 있을 수 있습니다.
  3. 제조 과정에서의 물리적 배치나 비용 절약을 위한 설계로 인해 발생하며, 이를 소프트웨어에서 colstart/rowstart 오프셋으로 보정하여 올바른 위치에 이미지가 표시되도록 합니다.

결론은 드라이버 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);

실제로는 이런 순서대로 채워지겠네요.

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


참고 자료

profile
행동하는 바보가 돼라. 생각을 즉시 행동으로 옮기는 사람이 되어라

0개의 댓글