티스토리 뷰

728x90

XIAO ESP32C6 + ST7789 TFT 영상 재생 — 셋업 전체 기록

숨으로 바람개비를 돌리면 자석 센서가 회전을 감지하고, 그 주기에 맞춰 영상과 모터가 반응하는 인터랙티브 설치 작품을 만들면서 겪은 과정을 정리했다.


하드웨어

  • XIAO ESP32C6 (Seeed Studio)
  • ST7789 TFT 디스플레이 (2.0", 240×320)
  • MLX90393 자석 센서
  • N20 DC 모터 + 2N2222A 트랜지스터

1. 보드 설정

Arduino IDE에서 보드를 처음 잡을 때 ESP32 Family Device로 잡히는 경우가 있다. 이건 틀렸다.

ESP32 Family Device  ← 너무 넓은 분류, 핀 번호 등 세부 설정이 안 맞음
XIAO_ESP32C6         ← 정확한 모델 지정 필요

보드를 정확히 지정해야 GPIO 번호, 메모리 주소, 플래시 크기가 맞게 들어간다.


2. TFT 라이브러리 선택

처음에 TFT_eSPI 라이브러리를 시도했다. User_Setup.h에서 핀 번호를 직접 수정해야 하고 설정이 복잡하다. 결국 Adafruit_ST7789로 교체했다.

최종 사용 라이브러리: Adafruit_ST7789

이유:

  • 설정이 단순하고 코드에서 직접 핀을 지정할 수 있음
  • 하드웨어 SPI를 명시적으로 지정 가능
  • setSPISpeed() 지원

TFT 핀 연결

TFT XIAO ESP32C6

MOSI D10 (GPIO18)
SCK D8 (GPIO19)
CS D3 (GPIO21)
DC D4 (GPIO22)
RST D5 (GPIO23)
BLK 3.3V

3. 소프트웨어 SPI vs 하드웨어 SPI

처음 Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST) 방식으로 초기화하면 소프트웨어 SPI로 동작한다. 화면 출력 시간이 2169ms가 나왔다.

하드웨어 SPI로 바꾸면 24ms로 줄어든다.

// ❌ 소프트웨어 SPI (느림, 2169ms)
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST);

// ✅ 하드웨어 SPI (빠름, 24ms)
SPIClass* spi = new SPIClass(SPI);
Adafruit_ST7789 tft = Adafruit_ST7789(spi, TFT_CS, TFT_DC, TFT_RST);

// setup()에서
spi->begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
tft.init(240, 320, SPI_MODE0);
tft.setSPISpeed(40000000);

4. 영상 → TFT 출력 전체 파이프라인

전체 흐름

영상(mp4) → 프레임 추출(ffmpeg) → RGB565 변환(Python) → LittleFS 패키징 → Flash 업로드(esptool) → TFT 출력

Step 1. 영상 → 이미지 프레임 (ffmpeg)

5초 24fps 영상에서 6프레임마다 1장씩 총 21장 추출.

import subprocess, os, glob

videos = sorted(glob.glob("*.mp4"))

for video in videos:
    name = os.path.splitext(video)[0]
    output_dir = f"frames_{name}"
    os.makedirs(output_dir, exist_ok=True)
    
    cmd = [
        "ffmpeg", "-i", video,
        "-vf", "scale=200:200,select='not(mod(n\\,6))'",
        "-vsync", "vfr",
        "-q:v", "2",
        os.path.join(output_dir, "frame_%04d.jpg")
    ]
    subprocess.run(cmd)

Step 2. 이미지 → RGB565 변환 (Python)

TFT는 JPG를 직접 못 읽는다. 픽셀을 16bit 숫자로 변환한 .raw 파일로 저장해야 한다.

from PIL import Image
import os, glob, struct

input_dir = "frames"
output_dir = "data"
os.makedirs(output_dir, exist_ok=True)

files = sorted(glob.glob(os.path.join(input_dir, "*.jpg")))[:21]

for i, f in enumerate(files):
    img = Image.open(f).convert("RGB").resize((200, 200))
    pixels = []
    for r, g, b in img.getdata():
        rgb565 = ((b & 0xF8) << 8) | ((g & 0xFC) << 3) | (r >> 3)
        pixels.append(struct.pack("<H", rgb565))  # little-endian
    
    out_path = os.path.join(output_dir, f"img_{i:02d}.raw")
    with open(out_path, "wb") as out:
        out.write(b"".join(pixels))

Step 3. raw 파일 → LittleFS 이미지 (mklittlefs)

21개 .raw 파일을 하나의 littlefs.bin으로 패키징.

[Arduino 설치 경로]/packages/esp32/tools/mklittlefs/[버전]/mklittlefs \
  -c [data 폴더 경로] \
  -s 1966080 \
  -b 4096 \
  -p 256 \
  littlefs.bin

Step 4. littlefs.bin → XIAO Flash (esptool)

Flash의 0x210000 주소(SPIFFS/LittleFS 영역)에 직접 굽는다.

[Arduino 설치 경로]/packages/esp32/tools/esptool_py/[버전]/esptool \
  --chip esp32c6 \
  --port /dev/cu.usbmodem[포트번호] \
  --baud 921600 \
  write_flash 0x210000 littlefs.bin

Flash 메모리 구조

XIAO Flash 4MB
├── 0x000000  부트로더
├── 0x010000  스케치 (Arduino IDE로 업로드)
└── 0x210000  LittleFS (esptool로 업로드)
                  img_00.raw ~ img_20.raw

5. 이미지 출력 코드

❌ 잘못된 방법 (위에서 아래로 렌더링 보임)

// 줄마다 drawRGBBitmap 호출 → 스캔라인이 눈에 보임
for (int y = 0; y < IMG_HEIGHT; y++) {
    tft.drawRGBBitmap(x, y, lineBuf, IMG_WIDTH, 1);
}

✅ 올바른 방법 (한번에 전송)

uint16_t frameBuf[200 * 200];

// 전체 읽기
f.read((uint8_t*)frameBuf, IMG_WIDTH * IMG_HEIGHT * 2);

// 바이트 스왑 (색상 순서 보정)
for (int i = 0; i < IMG_WIDTH * IMG_HEIGHT; i++) {
    uint16_t val = frameBuf[i];
    frameBuf[i] = (val >> 8) | (val << 8);
}

// 한번에 전송
tft.startWrite();
tft.setAddrWindow(60, 20, IMG_WIDTH, IMG_HEIGHT);  // 중앙 배치
tft.writePixels(frameBuf, IMG_WIDTH * IMG_HEIGHT);
tft.endWrite();

6. 다른 장치들과 연결시 초기화 순서 

TFT가 먼저 시작되면, 뒤에 통신들을 못받는 에러가 난다.

1. Wire.begin(SDA, SCL)       // I2C 먼저
2. sensor.begin_I2C()
3. ledcAttach(MOTOR_PIN, ...)  // 모터
4. spi->begin(...)             // SPI
5. LittleFS.begin()
6. tft.init()                  // TFT 마지막

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함