
ILI9341 와 Arduino Uno 연결해보자
목차
1. 프로젝트 소개 및 목표
2. 하드웨어 구성
3. 소프트웨어 설정 코딩
4. 프로젝트 완성
5. 맺음
1. 프로젝트 소개 및 목표
1.1 프로젝트 개요
이번 프로젝트는 ILI9341 2.8 TFT SPI 240 x 320 V1.2 와 Arduino Uno를 결합하여 소형 이미지 뷰어 및 상호작용 디스플레이를 구현하는 것입니다.
단순히 LCD를 구동하는 것을 넘어, 외장 SD 메모리 카드에 저장된 이미지를 로드하고 디스플레이하며, 사용자가 화면을 터치했을 때 특정 텍스트("Hello World")가 지정된 위치에 출력되도록 하는 것을 목표로 합니다.
이 프로젝트를 통해 독자 여러분은 다음과 같은 핵심 기술을 습득할 수 있습니다.
SD 카드 파일 시스템 접근: Arduino에서 SD 카드를 초기화하고 이미지 파일을 읽어오는 방법.
ILI9341 LCD 제어: 대용량 이미지를 LCD에 효율적으로 출력하는 방법.
저항막(Resistive) 터치스크린 상호작용: 터치 좌표를 읽어와 디스플레이의 특정 동작을 트리거하는 방법.
1.2 사용된 주요 부품
이 프로젝트를 위해 다음의 핵심 부품들이 사용됩니다.

주의 : SD카드는 FAT32형식으로 포맷이 필요합니다. ( 32GB 이하 용량 필요 )
2. 하드웨어 구성
2.1 ILI9341 와 Arduino Uno 연결




Arduino Uno는 5V 로직을 사용하지만, ILI9341 칩은 3.3V 로직으로 작동합니다.
5V 신호를 3.3V로 직접 연결하면 모듈이 손상될 수 있으므로, SCK, MOSI, CS, DC, RESET, SD_CS, T_CS 의 데이터 핀은 반드시 전압분배를 통해 연결해야 합니다.
2.2 SD카드
프로젝트에 사용할 SD 카드는 다음 조건을 만족해야 합니다.
포맷: SD 카드는 반드시 FAT16 또는 FAT32 형식으로 포맷되어 있어야 합니다.
이미지 파일: 표시할 이미지는 BMP(Bitmap) 형식으로 저장해야 합니다.
대부분의 라이브러리는 BMP 파일 형식으로만 이미지를 읽고 디스플레이할 수 있습니다.
이미지 크기는 240 x 320 으로 조정합니다.
파일 이름: 루트 디렉토리에 image.bmp와 같이 짧고 간단한 이름으로 저장합니다.
위 사이트에서 Arduino IDE를 설치합니다.
3.2 라이브러리 설치
Arduino -> 스케치 -> 라이브러리 포함 -> 라이브러리 관리 에서 설치합니다.
Adafruit_GFX.h
Adafruit_ILI9341.h
SD.h
XPT2046_Touchscreen.h
3.3 코드 작성
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <SD.h>
#include "XPT2046_Touchscreen.h"
각 라이브러리를 선언합니다.
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST 8
#define T_CS 6 // T_IRQ 핀은 사용하지 않으며, 물리적으로 Arduino에서 분리해야 합니다.
#define SD_CS 7
아두이노에 연결된 각 핀을 정의합니다.
SPI 통신 핀(D11, D12, D13)은 라이브러리 내부적으로 사용됩니다.
// SD카드에 있는 이미지 이름을 정확이 입력합니다.
const char *IMAGE_FILENAME = "TEST1.BMP";
// LCD 객체
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// 🚩 T_IRQ 핀 없이 터치 객체 초기화 (T_CS 핀만 사용)
XPT2046_Touchscreen ts(T_CS);
// 전역 상태 변수
bool sdCardInitialized = false;
bool isShowingHello = true; // true면 Hello, false면 World
// 터치 상태 변수
bool lastTouchState = false; // 이전 터치 상태 (디바운싱 역할)
// -----------------------------------------------------
// 텍스트 위치 및 크기 상수 정의
// -----------------------------------------------------
#define TEXT_X 20
#define TEXT_Y 100 // 화면 중앙 근처
#define TEXT_SIZE 4
#define HELLO_COLOR ILI9341_MAGENTA
#define WORLD_COLOR ILI9341_CYAN
// 텍스트를 지우기 위한 최대 영역 크기 (Hello와 World 모두 5글자 기준)
#define CLEAR_WIDTH (6 * 6 * TEXT_SIZE)
#define CLEAR_HEIGHT (8 * TEXT_SIZE)
/**
* 현재 상태에 따라 "Hello" 또는 "World"를 표시합니다.
* 참고: 이 함수는 배경 이미지 위에 덮어씁니다.
*/
void drawToggleText(bool showHello) {
// 1. 이전 텍스트 영역을 검은색으로 지웁니다.
// (TEST1 이미지가 배경이므로, 이미지 영역은 지우지 않습니다. 대신 검은색으로 칠합니다)
tft.fillRect(TEXT_X, TEXT_Y, CLEAR_WIDTH, CLEAR_HEIGHT, ILI9341_BLACK);
// 2. 새 텍스트를 설정하고 그립니다.
tft.setCursor(TEXT_X, TEXT_Y);
tft.setTextSize(TEXT_SIZE);
if (showHello) {
tft.setTextColor(HELLO_COLOR);
tft.println("Hello");
Serial.println("Displaying: Hello");
} else {
tft.setTextColor(WORLD_COLOR);
tft.println("World");
Serial.println("Displaying: World");
}
}
// BMP 파일 처리 헬퍼 함수
uint16_t read16(File f) {
uint16_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
return result;
}
uint32_t read32(File f) {
uint32_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read();
return result;
}
uint16_t read16(File f): 16비트(2바이트) 정수 읽기
역할 : SD 카드 파일(File f)로부터 연속된 2바이트를 읽어와 16비트 부호 없는 정수(uint16_t)로 변환하여 반환합니다.
uint32_t read32(File f): 32비트(4바이트) 정수 읽기
역할 : SD 카드 파일(File f)로부터 연속된 4바이트를 읽어와 32비트 부호 없는 정수(uint32_t)로 변환하여 반환합니다.
void drawBmp(const char *filename, int x, int y) {
File bmpFile;
int bmpWidth, bmpHeight;
uint32_t bmpImageoffset;
uint16_t bmpDepth;
uint32_t rowSize;
bool goodBmp = false;
uint8_t r, g, b;
tft.fillScreen(ILI9341_BLACK);
if ((x >= tft.width()) || (y >= tft.height())) return;
Serial.print(F("Loading BMP: ")); Serial.println(filename);
bmpFile = SD.open(filename);
if (!bmpFile) {
Serial.println(F("File not found!"));
tft.setCursor(10, 150);
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.println("File not found!");
return;
}
if (read16(bmpFile) == 0x4D42) {
read32(bmpFile);
read32(bmpFile);
bmpImageoffset = read32(bmpFile);
read32(bmpFile);
bmpWidth = read32(bmpFile);
bmpHeight = read32(bmpFile);
if (read16(bmpFile) == 1) {
bmpDepth = read16(bmpFile);
if ((bmpDepth == 24) && (read32(bmpFile) == 0)) {
goodBmp = true;
Serial.print(F("Image W=")); Serial.print(bmpWidth);
Serial.print(F(" H=")); Serial.println(bmpHeight);
uint16_t w = bmpWidth;
uint16_t h = bmpHeight;
if ((x + w - 1) >= tft.width()) w = tft.width() - x;
if ((y + h - 1) >= tft.height()) h = tft.height() - y;
tft.setAddrWindow(x, y, x + w - 1, y + h - 1);
rowSize = (bmpWidth * 3 + 3) & ~3;
for (int row = h - 1; row >= 0; row--) {
uint32_t pos = bmpImageoffset + (uint32_t)row * rowSize;
if (bmpFile.position() != pos) {
bmpFile.seek(pos);
}
for (uint16_t col = 0; col < w; col++) {
b = bmpFile.read();
g = bmpFile.read();
r = bmpFile.read();
uint16_t color = tft.color565(r, g, b);
tft.pushColor(color);
}
uint32_t bytesToSkip = rowSize - (w * 3);
if (bytesToSkip > 0) {
bmpFile.seek(bmpFile.position() + bytesToSkip);
}
}
} else {
Serial.print(F("BMP Error: Color Depth (")); Serial.print(bmpDepth);
Serial.println(F("), or Compressed format not supported."));
}
} else {
Serial.println(F("BMP Error: Planes not 1."));
}
} else {
Serial.println(F("BMP Error: Not a BMP file."));
}
bmpFile.close();
if (goodBmp) {
Serial.println(F("BMP load done."));
}
else {
Serial.println(F("BMP load failed."));
tft.setCursor(10, 150);
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.println("IMAGE LOAD FAIL!");
}
}
DrawBmp 함수의 역할 요약
- SD 카드 모듈을 통해 지정된 파일 이름(TEST1.BMP)의 파일 스트림을 엽니다.
- 파일의 앞부분(헤더)을 읽어와 파일이 유효한 BMP인지 확인하고, 이미지 폭, 높이, 그리고 실제 픽셀 데이터가 시작되는 위치(오프셋)를 파악합니다.
- LCD의 그리기 영역(tft.setAddrWindow)을 설정하여 픽셀 데이터가 올바른 위치에 그려지도록 준비합니다.
- BMP 파일은 일반적으로 24비트 BGR(Blue, Green, Red) 색상 순서로 저장됩니다. drawBmp는 이 24비트 BGR 데이터를 읽어와 ILI9341이 사용하는 16비트 RGB 565 형식으로 변환한 후, LCD(tft.pushColor)에 한 픽셀씩 전송하여 화면에 이미지를 표시합니다.
- BMP 파일의 특성상 각 행의 끝에 붙는 불필요한 데이터(패딩)를 건너뛰어 이미지가 깨지는 것을 방지합니다.
// -----------------------------------------------------
// SETUP 및 LOOP
// -----------------------------------------------------
void setup() {
Serial.begin(9600);
Serial.println("--- Starting SD Image and Touch Toggle Test ---");
// 1. LCD 초기화
tft.begin();
tft.setRotation(0); // 세로 모드
tft.fillScreen(ILI9341_BLACK);
// 2. 터치 초기화
ts.begin();
ts.setRotation(tft.getRotation());
Serial.println("Touchscreen initialized (IRQ line ignored).");
// 🚩 안정화를 위해 잠시 기다립니다.
delay(100);
// 3. SD 카드 초기화 및 이미지 로드
if (SD.begin(SD_CS)) {
sdCardInitialized = true;
drawBmp(IMAGE_FILENAME, 0, 0);
} else {
Serial.println("SD card initialization failed!");
tft.fillScreen(ILI9341_RED);
tft.setCursor(20, 100);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(3);
tft.println("SD FAIL!");
}
// 4. 초기 텍스트 출력
drawToggleText(isShowingHello); // 초기 상태: "Hello"
// 🚨 SETUP 종료 전 잔여 터치 이벤트를 강제로 무시합니다.
if (ts.touched()) {
ts.getPoint(); // 터치 레지스터를 읽어 상태를 클리어합니다.
lastTouchState = true; // loop()의 첫 실행에서 터치 이벤트 처리를 막습니다.
Serial.println("Initial spurious touch was detected and actively suppressed.");
} else {
lastTouchState = false;
}
Serial.println("Setup finished. Entering loop.");
}
void loop() {
if (!sdCardInitialized) return; // SD 카드 초기화 실패 시 루프 실행 중지
// 🚩 터치 상태 감지
if (ts.touched()) {
// 터치 디바운싱: 이전에 눌린 상태가 아니었다면 (새로운 터치 감지)
if (!lastTouchState) {
// 터치 좌표를 얻어옵니다.
TS_Point p = ts.getPoint();
// 🚨 Z 값이 20 이상이면 유효한 터치로 간주 (낮은 민감도로 설정)
if (p.z > 20) {
// 상태 토글: Hello <-> World
isShowingHello = !isShowingHello;
Serial.print(">>> TOUCH EVENT TRIGGERED! New state isShowingHello: ");
Serial.println(isShowingHello ? "true (Hello)" : "false (World)");
// LCD 업데이트
drawToggleText(isShowingHello);
// 터치 이벤트를 기록하여 다음 루프에서 중복 실행을 막습니다.
lastTouchState = true;
}
}
} else {
// 터치가 해제되면 상태를 리셋하여 다음 터치를 감지할 수 있도록 합니다.
lastTouchState = false;
}
delay(10); // 루프 지연
}
3.4 컴퓨터와 Arduino 전원 연결
Arduino 전원을 연결해줍니다.
도구 -> 보드 -> Arduino Uno 를 선택합니다.
도구 -> 포트 -> COM4를 연결합니다. ( 컴퓨터마다 숫자가 다를 수 있습니다. )
업로드 버튼을 눌러줍니다.
4. 프로젝트 완성
4.1 최종 동작 확인
모든 코드를 업로드하고 배선을 확인했다면, 이제 최종 동작을 확인해 봅시다.
- 이미지 로드 확인 : 아두이노에 전원을 인가했을 때, SD 카드에 저장한 TEST1.BMP 파일이 LCD 화면에 정상적으로 표시되는지 확인합니다.
- 터치 토글 확인 : 화면의 아무 곳이나 터치했을 때, 텍스트가 "Hello"에서 "World"로, 또는 그 반대로 바뀌는지 확인합니다.



4.2 프로젝트 응용 아이디어
단순히 이미지를 표시하고 텍스트를 토글하는 것을 넘어, 이 프로젝트는 다양한 응용의 기초가 될 수 있습니다.
- GUI 버튼 구현 : 현재는 전체 화면 터치 시 동작하지만, 화면 내 특정 좌표(예: 버튼 모양으로 영역 지정)를 터치했을 때만 반응하도록 코드를 수정하여 실제 GUI 버튼을 구현할 수 있습니다.
- 슬라이드 쇼 제작 : TEST1.BMP 대신 여러 이미지를 SD 카드에 저장하고, 터치할 때마다 다음 이미지로 넘어가도록 코드를 확장하면 멋진 디지털 액자나 이미지 뷰어가 됩니다.
- IoT 대시보드 : Wi-Fi 모듈이 내장된 ESP32 보드를 사용하고, SD 카드의 이미지를 배경으로 활용한 후, 그 위에 서버에서 받아온 온도, 습도, 날씨 정보 등을 표시하는 대시보드를 제작할 수 있습니다.
5. 맺음말
5.1 프로젝트를 마치며
ILI9341 TFT LCD와 Arduino Uno를 결합하여 SD 카드 이미지 로드와 터치 인터랙션을 구현하는 이번 프로젝트를 무사히 마무리하게 되어 매우 뿌듯합니다.
가장 어려웠던 부분은 SPI 통신 버스의 공유와 BMP 파일의 복잡한 헤더 처리였습니다.
특히, BMP 파일을 픽셀 데이터로 변환하고, ILI9341이 사용하는 16비트 색상 형식(RGB 565)으로 정확히 변환하는 과정은 아두이노의 낮은 성능 환경에서 매우 중요한 디버깅 포인트였습니다.
이 과정을 통해 하드웨어의 제약 조건과 효율적인 데이터 처리 방식에 대해 깊이 이해할 수 있었습니다.
이 프로젝트는 단순히 이미지를 띄우는 것 이상의 의미를 가집니다.
SD 카드에서 대용량 데이터를 읽어와 표시하고, 터치 입력을 통해 사용자와 상호작용하는 가장 기본적인 틀을 마련했습니다.
여러분도 이 코드를 기반으로 나만의 대시보드, 게임, 혹은 스마트 제어판을 만들어 보시길 추천합니다.

TEL (062-226-1777, 010-9891-7244), E-mail (ipmes@ipmes.co.kr)
임베디드 시스템 | PCB 설계 제작 | 펌웨어 개발 | 신호처리 | 응용프로그램
'연구노트 > 회로 설계' 카테고리의 다른 글
| [트랜지스터] 트랜지스터 구조와 원리 [ 2부 ] (0) | 2026.04.24 |
|---|---|
| [트랜지스터] 트랜지스터란 무엇인가? [ 1부 ] (0) | 2026.04.22 |
| OSC와 CRYSTAL의 회로 적용 (0) | 2026.03.27 |
| DAC 기초 (0) | 2026.03.27 |
| ADC(Analog to Digital Converter) 기초 (0) | 2026.03.27 |