ILI9341 и Arduino: подключение TFT-дисплея, сенсорный экран и картинки с SD
Как подключить TFT-дисплей ILI9341 к Arduino: согласование уровней 3,3 V на Uno, Adafruit GFX, сенсор XPT2046, BMP с SD-карты, TFT_eSPI на ESP32 и решение проблемы белого экрана.
Связка ILI9341 и Arduino — самый дешёвый и популярный способ добавить в проект дисплей на 240×320 пикселей и 65 тысяч цветов. Эти TFT-модули на 2,4 и 2,8 дюйма с красной платой подключаются по SPI всего несколькими проводами, а в сенсорных версиях на плате есть ещё контроллер касаний XPT2046 и слот для microSD-карты. Один модуль показывает цветную графику, сенсорные кнопки и картинки с карты. Он идеален для меню, приборных панелей, игр и фоторамок, где OLED или символьного LCD уже не хватает.
В этом руководстве пошагово разберём, как устроен модуль, как безопасно соединить 5-вольтовые выходы Arduino Uno с 3,3-вольтовыми входами дисплея, как рисовать текст, фигуры и цвета с библиотеками Adafruit_ILI9341 и Adafruit GFX, как читать сенсорный экран и как показывать BMP-картинки с SD-карты. Потом перейдём к гораздо более быстрому выводу на ESP32 с TFT_eSPI, к MicroPython на Raspberry Pi Pico и ко всем причинам «белого экрана», с которым почти каждый сталкивается хотя бы раз.

Что такое TFT-дисплей ILI9341 и как он работает?
ILI9341 — это микросхема драйвера, которая стоит за стеклом. В ней около 172 КБ видеопамяти (GRAM), где хранятся все 240×320 пикселей. Arduino отправляет в эту память команды и данные о цвете по SPI, а микросхема сама обновляет картинку десятки раз в секунду. Поэтому изображение не нужно держать в ОЗУ Arduino: даже Uno с 2 КБ ОЗУ справляется с большим цветным экраном. Чтобы что-то нарисовать, сначала выбирается окно (x, y, ширина, высота), а затем пиксели этого окна передаются один за другим. Пин DC сообщает микросхеме, что сейчас передаётся: команда (LOW) или данные (HIGH). Цвета обычно передаются в формате RGB565, то есть 16 бит на пиксель: 5 бит красного, 6 бит зелёного и 5 бит синего. Как работает шина SPI, подробно описано в статье Что такое SPI.
| Характеристика | Значение |
|---|---|
| Микросхема драйвера | ILI9341 |
| Разрешение | 240×320 пикселей (320×240 в горизонтальной ориентации) |
| Цвет | 65 536 цветов в RGB565 (микросхема поддерживает 262 тысячи) |
| Размеры экрана | 2,2″, 2,4″, 2,8″ и 3,2″ (все 240×320) |
| Интерфейс | 4-проводной SPI: CS, DC, SCK, MOSI (MISO для чтения) |
| Питание (VCC) | 3,3–5 V (на плате есть стабилизатор 3,3 V) |
| Логический уровень | 3,3 V (5 V не выдерживает) |
| Потребляемая мощность | Около 0,3–0,6 W, в основном подсветка |
| Сенсор | Резистивный, контроллер XPT2046 (в сенсорных моделях) |
| Дополнительно | Слот для microSD-карты (отдельные пины SPI) |
2,4 или 2,8 дюйма? SPI-модуль или шилд для Uno
| Характеристика | SPI-модуль 2,4″ | SPI-модуль 2,8″ | Шилд для Uno 2,4″ |
|---|---|---|---|
| Разрешение | 240×320 | 240×320 | 240×320 |
| Подключение | SPI, перемычками | SPI, перемычками | 8-битная параллельная шина, надевается сверху на Uno |
| Занятые пины | 5 для дисплея, 8–9 с сенсором и SD | 5 для дисплея, 8–9 с сенсором и SD | Почти все (D2–D9, A0–A4, D10–D13 для SD) |
| Драйвер | ILI9341 | ILI9341 | Бывает разный: ILI9341, ILI9325, ST7789 и другие |
| Библиотека | Adafruit_ILI9341, TFT_eSPI | Adafruit_ILI9341, TFT_eSPI | MCUFRIEND_kbv |
| Сенсор | XPT2046 (SPI) | XPT2046 (SPI) | 4-проводной аналоговый (библиотека TouchScreen) |
| ESP32 и Pico | Подходит | Подходит | Рассчитан на Uno и Mega |
В этой статье речь идёт об SPI-модуле: он работает и с ESP32, и с Pico, а на Uno оставляет свободными большинство пинов. У двух SPI-модулей полностью одинаковые пины и код; у 2,8-дюймового экрана пиксели чуть крупнее, поэтому по сенсорным кнопкам легче попадать. Если дисплей вставляется прямо сверху в Uno как шилд, SPI-библиотеки с ним не справятся, и обычно получается белый экран. Для такого шилда установи библиотеку MCUFRIEND_kbv, прочитай ID драйвера через tft.readID() (у ILI9341 это 0x9341) и запусти дисплей командой tft.begin(ID). Меньший, но более чёткий вариант описан в статье TFT-дисплей ST7789, а сравнить все варианты поможет статья Виды дисплеев для Arduino.
Распиновка ILI9341 и таблица подключения
На одном краю модуля находится основной 14-контактный разъём: первые 9 пинов относятся к дисплею, а последние 5, с префиксом T_, к сенсору. 4 пина на противоположном краю (SD_CS, SD_MOSI, SD_MISO, SD_SCK) идут к слоту карты. Все три устройства могут делить одну шину SPI (SCK, MOSI, MISO); раздельными должны быть только их пины CS. Таблица ниже подходит ко всем скетчам этой статьи; на Pico сенсор подключён к отдельной шине SPI из-за разницы в скорости.
| Пин модуля | Arduino Uno (через преобразователь уровней) | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| VCC | 5V | 3V3 | 3V3(OUT) |
| GND | GND | GND | GND |
| CS | D10 | GPIO 15 | GP17 |
| RESET | D8 | GPIO 4 | GP21 |
| DC | D9 | GPIO 2 | GP20 |
| SDI (MOSI) | D11 | GPIO 23 | GP19 |
| SCK | D13 | GPIO 18 | GP18 |
| LED | 3.3V | 3V3 | 3V3(OUT) |
| SDO (MISO) | D12 (напрямую) | GPIO 19 | GP16 |
| T_CLK | D13 | GPIO 18 | GP10 |
| T_CS | D7 | GPIO 21 | GP13 |
| T_DIN | D11 | GPIO 23 | GP11 |
| T_DO | D12 (напрямую) | GPIO 19 | GP12 |
| T_IRQ | D2 (напрямую) | Не подключён | Не подключён |
| SD_CS | D4 | GPIO 5 | Не подключён |
| SD_MOSI / SD_MISO / SD_SCK | D11 / D12 / D13 | GPIO 23 / 19 / 18 | Не подключены |
Согласование уровней 3,3 V на Arduino Uno
ESP32 и Raspberry Pi Pico и так работают от 3,3 V, поэтому дисплей подключается к ним напрямую. На 5-вольтовых платах вроде Uno, Nano и Mega каждую линию, идущую от Arduino к дисплею, нужно понизить до 3,3 V: CS, DC, RESET, SCK, MOSI, а если используются сенсор и SD, то ещё T_CLK, T_DIN, T_CS, SD_CS, SD_MOSI и SD_SCK. Линиям SDO, T_DO, SD_MISO и T_IRQ, которые идут от дисплея к Arduino, преобразователь не нужен: Uno считает HIGH всё, что выше 3 V, и 3,3-вольтовый выход этому условию соответствует. На практике есть четыре способа:
- Буферная микросхема 74HC4050 (самое чистое решение): питается от 3,3 V, допускает 5 V на входах и преобразует 6 линий в одном корпусе. Она быстрая и без проблем работает с SPI на 8 MHz. Старая CD4050 тоже подойдёт, но она медленная; если изображение портится, придётся снизить скорость SPI. Одному дисплею хватает 5 линий; с сенсором и SD добавляются T_CS и SD_CS, всего 7 линий, поэтому для седьмой понадобится вторая микросхема или делитель напряжения.
- Резистивные делители напряжения: на каждой линии поставь 1 kΩ между пином Arduino и пином дисплея и 2 kΩ между пином дисплея и GND; получится 5 V × 2 / 3 ≈ 3,3 V. Другие пары резисторов можно подобрать с помощью утилиты делителя напряжения. Это дёшево, но 10 резисторов на 5 линий загромождают макетную плату.
- Готовый модуль преобразователя уровней: 4- или 8-канальные модули на MOSFET BSS138 легко подключить, но их подтягивающие резисторы на высоких скоростях сглаживают фронты сигнала. Если изображение портится, снизь SPI до 4 MHz командой tft.begin(4000000). Способы сравниваются в статье Преобразователь логических уровней.
- Способ, которого лучше избегать: популярный приём «последовательный резистор 10 kΩ на каждой линии» сбрасывает лишнее напряжение на внутренние защитные диоды микросхемы. Какое-то время это может работать, но ненадёжно и со временем может повредить дисплей.
Пошагово: первый тест с Adafruit_ILI9341
- Открой «Менеджер библиотек» (Library Manager) в Arduino IDE, найди Adafruit ILI9341 и установи. Когда IDE спросит о зависимостях, выбери Install all («Установить все»), чтобы установились также Adafruit GFX и Adafruit BusIO. Если это первая библиотека, поможет статья Как добавить библиотеку в Arduino.
- Для сенсора установи ещё библиотеку XPT2046_Touchscreen от Пола Стоффрегена (Paul Stoffregen). Библиотека SD входит в комплект Arduino IDE.
- Подключи дисплей по таблице выше, на Uno через преобразователь уровней. Для первого теста пины сенсора и SD можно оставить неподключёнными.
- Загрузи скетч ниже и открой монитор порта на скорости 9600 бод. На дисплее должны появиться текст и фигуры, а в мониторе порта — диагностические коды дисплея.
- Попробуй и готовые примеры: «Файл › Примеры» (File › Examples) › Adafruit ILI9341 › graphicstest измеряет, за сколько микросекунд выполняется каждая операция рисования.
1#include <SPI.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_ILI9341.h>
4
5#define TFT_CS 10 // ekran seçme / chip select
6#define TFT_DC 9 // komut/veri seçimi / command/data select
7#define TFT_RST 8 // donanım reset / hardware reset
8
9Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST); // donanım SPI: D11 MOSI, D13 SCK / hardware SPI
10uint16_t orange; // kendi rengimiz / our own colour
11
12void setup() {
13 Serial.begin(9600);
14 tft.begin(); // Uno'da 8 MHz SPI / 8 MHz SPI on Uno
15
16 // Teşhis: çalışan ekranda 0x94 ve 0xC0 beklenir / Diagnostics: a working display reports 0x94 and 0xC0
17 Serial.print(F("Power Mode: 0x"));
18 Serial.println(tft.readcommand8(ILI9341_RDMODE), HEX);
19 Serial.print(F("Self Diagnostic: 0x"));
20 Serial.println(tft.readcommand8(ILI9341_RDSELFDIAG), HEX);
21
22 tft.setRotation(1); // yatay, 320x240 / landscape, 320x240
23 tft.fillScreen(ILI9341_BLACK);
24 orange = tft.color565(255, 128, 0); // RGB'den RGB565'e / RGB to RGB565
25
26 tft.setTextColor(ILI9341_WHITE);
27 tft.setTextSize(3); // 18x24 piksel karakter / 18x24 px characters
28 tft.setCursor(10, 10);
29 tft.print(F("BlueGrays"));
30 tft.setTextColor(ILI9341_CYAN);
31 tft.setTextSize(2);
32 tft.setCursor(10, 45);
33 tft.print(F("ILI9341 320x240"));
34
35 tft.fillRect(10, 75, 90, 60, ILI9341_RED); // dolu dikdörtgen / filled rectangle
36 tft.drawRect(115, 75, 90, 60, ILI9341_GREEN); // çerçeve / outline
37 tft.fillCircle(265, 105, 30, ILI9341_BLUE); // dolu daire / filled circle
38 tft.fillRoundRect(10, 150, 140, 50, 10, orange); // yuvarlak köşeli düğme / rounded button
39 tft.drawTriangle(180, 200, 240, 150, 300, 200, ILI9341_YELLOW);
40 tft.drawFastHLine(0, 220, 320, ILI9341_MAGENTA); // yatay çizgi / horizontal line
41}
42
43void loop() {
44 // Arka plan rengiyle yazınca eski rakam silinir, ekran titremez / writing with a background colour erases the old digits, no flicker
45 tft.setTextColor(ILI9341_WHITE, orange);
46 tft.setTextSize(2);
47 tft.setCursor(22, 167);
48 tft.print(millis() / 1000);
49 tft.print(F(" s"));
50 delay(200);
51}| Функция | Что делает |
|---|---|
| tft.begin(частота) | Запускает дисплей; если частота не указана, на Uno используется SPI на 8 MHz, на ESP32 — на 40 MHz |
| tft.setRotation(0–3) | Выбирает ориентацию: 0 и 2 — портретная (240×320), 1 и 3 — альбомная (320×240) |
| tft.fillScreen(цвет) | Заливает весь экран одним цветом |
| tft.setTextColor(текст, фон) | Задаёт цвет текста; с цветом фона аккуратно затирает старый текст |
| tft.setTextSize(n) | Увеличивает текст в n раз; размер 1 = символы 6×8 пикселей |
| tft.drawRect / fillRect(x, y, ширина, высота, цвет) | Контур прямоугольника или залитый прямоугольник |
| tft.fillRoundRect(x, y, ширина, высота, r, цвет) | Залитый прямоугольник со скруглёнными углами; идеален для кнопок |
| tft.drawCircle / fillCircle(x, y, r, цвет) | Окружность или залитый круг |
| tft.color565(r, g, b) | Переводит значения RGB от 0 до 255 в 16-битный цвет |
| tft.readcommand8(команда) | Читает регистр дисплея (MISO должен быть подключён); для поиска неисправностей |
Готовые константы цветов (их 19, например ILI9341_RED, ILI9341_NAVY и ILI9341_ORANGE) идут вместе с библиотекой; для своего цвета используй color565(). Функция укладывает рядом старшие 5 бит красного, старшие 6 бит зелёного и старшие 5 бит синего: цвет = (R >> 3) << 11 | (G >> 2) << 5 | (B >> 3). Например, для оранжевого (255, 165, 0) 31 << 11 = 0xF800 и 41 << 5 = 0x0520 складываются в 0xFD20, а это ровно значение ILI9341_ORANGE из библиотеки. Зелёному достаётся лишний бит, потому что человеческий глаз к зелёному чувствительнее.
Чтение сенсорного экрана XPT2046
У сенсорных модулей поверх стекла лежит тонкий резистивный слой. Там, где нажимает палец или стилус, соприкасаются два проводящих слоя; микросхема XPT2046 измеряет координаты X и Y этой точки 12-битным АЦП в виде «сырых» значений от 0 до 4095 и вдобавок сообщает силу нажатия (Z). Если Z меньше 400, библиотека считает, что касания нет. Микросхема сидит на той же шине SPI, что и дисплей, но общается на меньшей скорости, около 2 MHz; библиотека сама выставляет скорость при каждом чтении. Пин T_IRQ переходит в LOW при касании экрана и избавляет процессор от лишних опросов. Скетч ниже превращает экран в маленькую доску для рисования: цвет выбирается в квадратиках сверху, а экран очищается кнопкой «SIL» (стереть).
1#include <SPI.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_ILI9341.h>
4#include <XPT2046_Touchscreen.h>
5
6#define TFT_CS 10
7#define TFT_DC 9
8#define TFT_RST 8
9#define T_CS 7 // dokunmatik seçme / touch chip select
10#define T_IRQ 2 // dokununca LOW olur / goes LOW on touch
11#define SD_CS 4 // SD kullanılmıyor, HIGH tutulur / SD unused, kept HIGH
12
13// Kalibrasyon: Seri Monitör'deki ham değerlerle değiştir / Calibration: replace with the raw values from Serial Monitor
14#define TS_MINX 300
15#define TS_MAXX 3800
16#define TS_MINY 300
17#define TS_MAXY 3800
18
19Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
20XPT2046_Touchscreen ts(T_CS, T_IRQ);
21
22uint16_t brush = ILI9341_YELLOW; // fırça rengi / brush colour
23
24void drawPalette() {
25 tft.fillRect(0, 0, 40, 40, ILI9341_RED);
26 tft.fillRect(40, 0, 40, 40, ILI9341_GREEN);
27 tft.fillRect(80, 0, 40, 40, ILI9341_BLUE);
28 tft.fillRect(120, 0, 40, 40, ILI9341_YELLOW);
29 tft.drawRect(260, 0, 60, 40, ILI9341_WHITE); // silme düğmesi / clear button
30 tft.setTextColor(ILI9341_WHITE);
31 tft.setTextSize(2);
32 tft.setCursor(273, 13);
33 tft.print(F("SIL"));
34}
35
36void setup() {
37 Serial.begin(9600);
38 pinMode(SD_CS, OUTPUT);
39 digitalWrite(SD_CS, HIGH); // SD kart hattı meşgul etmesin / keep the SD card off the bus
40 tft.begin();
41 tft.setRotation(1); // ekran yatay / display landscape
42 ts.begin();
43 ts.setRotation(1); // dokunmatik de aynı yönde / touch in the same orientation
44 tft.fillScreen(ILI9341_BLACK);
45 drawPalette();
46}
47
48void loop() {
49 if (!ts.touched()) return; // dokunma yok / no touch
50 TS_Point p = ts.getPoint(); // ham x, y (0-4095) ve basınç z / raw x, y and pressure z
51 Serial.print(p.x); Serial.print(',');
52 Serial.print(p.y); Serial.print(',');
53 Serial.println(p.z);
54
55 int16_t x = constrain(map(p.x, TS_MINX, TS_MAXX, 0, 319), 0, 319); // ekran koordinatı / screen coordinate
56 int16_t y = constrain(map(p.y, TS_MINY, TS_MAXY, 0, 239), 0, 239);
57
58 if (y < 40) { // üst şerit: palet ve düğme / top strip: palette and button
59 if (x < 40) brush = ILI9341_RED;
60 else if (x < 80) brush = ILI9341_GREEN;
61 else if (x < 120) brush = ILI9341_BLUE;
62 else if (x < 160) brush = ILI9341_YELLOW;
63 else if (x >= 260) { tft.fillScreen(ILI9341_BLACK); drawPalette(); }
64 } else {
65 tft.fillCircle(x, y, 3, brush); // parmağın altına nokta / dot under the finger
66 }
67}В самых углах экрана «сырые» значения не равны 0 и 4095; на большинстве модулей края лежат примерно в пределах 200–400 и 3700–3900. Для калибровки открой монитор порта, коснись стилусом левого верхнего и правого нижнего углов и впиши увиденные значения x и y в константы TS_MINX, TS_MAXX, TS_MINY и TS_MAXY. Если точка появляется зеркально относительно пальца, поменяй местами min и max этой оси в map(); если X и Y полностью перепутаны, попробуй значения ts.setRotation() от 0 до 3. Настоящее меню работает так же: на экране рисуются кнопки, а программа проверяет, в какой прямоугольник попала точка касания. Резистивный слой реагирует и на палец, но для мелких кнопок что-то с тонким кончиком, как у стилуса, точнее.
Вывод BMP-картинок с SD-карты
Слот microSD на обратной стороне модуля позволяет хранить логотипы, фоны и фотографии на карте и выводить их на экран. Чтобы подготовить картинку, уменьши фотографию до 320×240 пикселей и сохрани её в Paint как 24-разрядный рисунок (.bmp). Библиотека SD для Arduino работает с короткими именами файлов, поэтому дай файлу имя не длиннее 8 символов, например RESIM.BMP, и отформатируй карту в FAT32. Подробности о самой карте и решение ошибки initialization failed (ошибка инициализации) есть в статье Модуль SD-карты для Arduino. Помни, что этот слот тоже 3,3-вольтовый: на Uno линии SD_CS, SD_MOSI и SD_SCK тоже должны идти через преобразователь уровней.
1#include <SPI.h>
2#include <SD.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_ILI9341.h>
5
6#define TFT_CS 10
7#define TFT_DC 9
8#define TFT_RST 8
9#define SD_CS 4
10#define T_CS 7
11#define CHUNK 20 // tek okumada 20 piksel (60 bayt) / 20 pixels (60 bytes) per read
12
13Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
14
15uint16_t read16(File &f) { // BMP sayıları küçük uçlu / BMP numbers are little-endian
16 uint16_t lo = f.read();
17 uint16_t hi = f.read();
18 return lo | (hi << 8);
19}
20
21uint32_t read32(File &f) {
22 uint32_t lo = read16(f);
23 uint32_t hi = read16(f);
24 return lo | (hi << 16);
25}
26
27bool drawBmp(const char *name, int16_t x, int16_t y) {
28 File f = SD.open(name);
29 if (!f) return false; // dosya yok / file not found
30 if (read16(f) != 0x4D42) { f.close(); return false; } // "BM" imzası / "BM" signature
31 read32(f); // dosya boyutu / file size
32 read32(f); // ayrılmış alan / reserved
33 uint32_t offset = read32(f); // piksel verisinin başı / start of pixel data
34 read32(f); // başlık boyutu / header size
35 int32_t w = (int32_t)read32(f);
36 int32_t h = (int32_t)read32(f);
37 read16(f); // düzlem sayısı / colour planes
38 uint16_t depth = read16(f);
39 uint32_t comp = read32(f);
40 if (depth != 24 || comp != 0) { f.close(); return false; } // yalnız 24 bit, sıkıştırmasız / 24-bit uncompressed only
41
42 bool bottomUp = true; // BMP satırları alttan üste / rows are stored bottom-up
43 if (h < 0) { h = -h; bottomUp = false; }
44 uint32_t rowSize = (w * 3 + 3) & ~3UL; // satırlar 4 baytın katı / rows padded to 4 bytes
45 int32_t drawW = w, drawH = h;
46 if (x + drawW > tft.width()) drawW = tft.width() - x; // ekrana sığmayanı kırp / clip to the screen
47 if (y + drawH > tft.height()) drawH = tft.height() - y;
48
49 uint8_t sdBuf[CHUNK * 3];
50 uint16_t line[CHUNK];
51 for (int32_t row = 0; row < drawH; row++) {
52 uint32_t pos = offset + (bottomUp ? (h - 1 - row) : row) * rowSize;
53 f.seek(pos); // satırın başına git / jump to the start of the row
54 for (int32_t col = 0; col < drawW; col += CHUNK) {
55 int16_t n = drawW - col;
56 if (n > CHUNK) n = CHUNK;
57 f.read(sdBuf, n * 3);
58 for (int16_t i = 0; i < n; i++) { // BGR'den RGB565'e / BGR to RGB565
59 line[i] = tft.color565(sdBuf[i * 3 + 2], sdBuf[i * 3 + 1], sdBuf[i * 3]);
60 }
61 tft.drawRGBBitmap(x + col, y + row, line, n, 1); // parçayı ekrana bas / push the chunk to the screen
62 }
63 }
64 f.close();
65 return true;
66}
67
68void setup() {
69 Serial.begin(9600);
70 pinMode(T_CS, OUTPUT);
71 digitalWrite(T_CS, HIGH); // dokunmatik hattı meşgul etmesin / keep touch off the bus
72 tft.begin();
73 tft.setRotation(1); // 320x240 yatay / landscape
74 tft.fillScreen(ILI9341_BLACK);
75 tft.setTextSize(2);
76
77 if (!SD.begin(SD_CS)) { // kart başlatılamadı / card init failed
78 tft.setTextColor(ILI9341_RED);
79 tft.setCursor(10, 10);
80 tft.print(F("SD kart bulunamadi"));
81 return;
82 }
83 uint32_t t = millis();
84 if (!drawBmp("RESIM.BMP", 0, 0)) {
85 tft.setTextColor(ILI9341_YELLOW);
86 tft.setCursor(10, 10);
87 tft.print(F("RESIM.BMP okunamadi"));
88 }
89 Serial.print(F("Cizim suresi / Draw time (ms): "));
90 Serial.println(millis() - t);
91}
92
93void loop() {
94}Первые 54 байта BMP-файла — это заголовок: там записаны сигнатура «BM», место, где начинаются данные пикселей, ширина, высота и глубина цвета. Пиксели хранятся в порядке синий, зелёный, красный (BGR) и обычно снизу вверх, а каждая строка дополняется до размера, кратного 4 байтам. Поэтому скетч для каждой строки экрана переходит в нужное место файла, читает сразу по 20 пикселей (60 байт), переводит их в RGB565 и выводит через drawRGBBitmap(). Работа небольшими порциями укладывается в ОЗУ Uno и позволяет SD-карте и дисплею по очереди пользоваться одной шиной SPI. Время отрисовки выводится в монитор порта; на Uno полноэкранная картинка проступает строка за строкой за несколько секунд. Чтобы показывать PNG или JPEG и рисовать гораздо быстрее, переходи на ESP32 с библиотеками TFT_eSPI и TJpg_Decoder.
Быстрый вывод на ESP32 с TFT_eSPI

ESP32 работает от 3,3 V и может разгонять SPI до 40 MHz, поэтому преобразователь уровней не нужен, а заливка всего экрана теоретически сокращается примерно до 31 мс. Чтобы действительно использовать эту скорость, обычно берут библиотеку TFT_eSPI от Bodmer. Она обращается к аппаратному SPI ESP32 напрямую и поддерживает сглаженные шрифты и, самое главное, внеэкранные буферы — спрайты (sprites). В спрайт в ОЗУ можно рисовать сколько угодно, а потом отправить результат на экран за один раз, поэтому даже быстро меняющаяся графика не мерцает. Особенность TFT_eSPI в том, что пины задаются не в скетче, а в файле User_Setup.h внутри папки библиотеки. Открой его, закомментируй строки с пинами ESP8266 (NodeMCU), которые включены по умолчанию, и включи эти:
1// TFT_eSPI/User_Setup.h içinde yalnız bu satırlar açık kalsın / keep only these lines active in TFT_eSPI/User_Setup.h
2#define ILI9341_DRIVER
3
4#define TFT_MISO 19
5#define TFT_MOSI 23
6#define TFT_SCLK 18
7#define TFT_CS 15 // ekran seçme / chip select
8#define TFT_DC 2 // komut/veri / data/command
9#define TFT_RST 4 // sıfırlama / reset
10#define TOUCH_CS 21 // dokunmatik seçme (T_CS) / touch chip select (T_CS)
11
12#define LOAD_GLCD // 1: 8 piksel yazı tipi / 8 px font
13#define LOAD_FONT2 // 2: 16 piksel / 16 px
14#define LOAD_FONT4 // 4: 26 piksel / 26 px
15#define LOAD_FONT7 // 7: 7 segment rakamlar / 7-segment digits
16#define LOAD_GFXFF // Adafruit FreeFont'lar / Adafruit FreeFonts
17#define SMOOTH_FONT
18
19#define SPI_FREQUENCY 40000000 // kablolar uzunsa 27000000 / 27 MHz with long wires
20#define SPI_READ_FREQUENCY 20000000
21#define SPI_TOUCH_FREQUENCY 25000001#include <SPI.h>
2#include <TFT_eSPI.h> // pinler User_Setup.h içinde / pins are set in User_Setup.h
3
4TFT_eSPI tft = TFT_eSPI();
5TFT_eSprite graph = TFT_eSprite(&tft); // ekran dışı çizim alanı / off-screen canvas
6
7// Touch_calibrate örneğinin verdiği 5 sayıyla değiştir / replace with the 5 numbers from the Touch_calibrate example
8uint16_t calData[5] = {286, 3534, 283, 3600, 6};
9
10const int GW = 300, GH = 100; // grafik boyutu / graph size
11uint8_t samples[GW]; // son 300 ölçüm / last 300 readings
12uint32_t frames = 0, fps = 0, lastFps = 0;
13
14void setup() {
15 tft.init();
16 tft.setRotation(1); // yatay, 320x240 / landscape, 320x240
17 tft.setTouch(calData);
18 tft.fillScreen(TFT_BLACK);
19 tft.setTextColor(TFT_WHITE, TFT_BLACK);
20 tft.drawString("ESP32 + TFT_eSPI", 10, 8, 4); // 4 numaralı yazı tipi, 26 px / font 4, 26 px
21 graph.createSprite(GW, GH); // 300x100x2 bayt = 60 KB RAM / 60 KB of RAM
22}
23
24void loop() {
25 memmove(samples, samples + 1, GW - 1); // grafiği sola kaydır / scroll the graph left
26 samples[GW - 1] = map(analogRead(34), 0, 4095, 0, GH - 1); // GPIO 34'teki potansiyometre / pot on GPIO 34
27
28 graph.fillSprite(TFT_BLACK);
29 for (int y = 0; y < GH; y += 25) graph.drawFastHLine(0, y, GW, TFT_DARKGREY); // ızgara / grid
30 for (int x = 1; x < GW; x++) {
31 graph.drawLine(x - 1, GH - 1 - samples[x - 1], x, GH - 1 - samples[x], TFT_GREEN);
32 }
33 graph.setTextColor(TFT_YELLOW);
34 graph.drawString(String(fps) + " FPS", 4, 4, 2);
35 graph.pushSprite(10, 50); // tek seferde ekrana gönder, titreme yok / push in one go, no flicker
36
37 uint16_t tx, ty;
38 if (tft.getTouch(&tx, &ty)) { // dokunulduysa / if touched
39 tft.fillRect(10, 170, 300, 30, TFT_BLACK);
40 tft.setTextColor(TFT_CYAN, TFT_BLACK);
41 tft.drawString("Dokunma / Touch: " + String(tx) + ", " + String(ty), 10, 175, 2);
42 }
43
44 frames++;
45 if (millis() - lastFps >= 1000) { // saniyede bir FPS hesapla / compute FPS once per second
46 fps = frames;
47 frames = 0;
48 lastFps = millis();
49 }
50}Скетч рисует значение потенциометра на GPIO 34 в виде бегущего графика и показывает в углу спрайта число кадров в секунду (FPS). Спрайт 300×100 пикселей в 16-битном цвете занимает 60 КБ ОЗУ; это примерно в 30 раз больше всей памяти Uno, но для ESP32 не проблема. Для сенсора пины T_CLK, T_DIN и T_DO подключаются к линиям SCK, MOSI и MISO дисплея, отдельным остаётся только T_CS (GPIO 21). Чтобы получить свои калибровочные значения, один раз запусти пример Generic > Touch_calibrate из TFT_eSPI, коснись стрелок, которые появятся в углах, и скопируй 5 чисел из монитора порта в массив calData. Готовая строка Setup42_ILI9341_ESP32.h в User_Setup_Select.h выбирает те же пины дисплея, но строку TOUCH_CS в неё придётся добавить вручную. Если ESP32 для тебя в новинку, сначала прочитай Что такое ESP32.
ILI9341 с Raspberry Pi Pico и MicroPython
Pico — тоже 3,3-вольтовая плата, поэтому дисплей подключается напрямую. Самый распространённый драйвер для MicroPython — пара файлов ili9341.py и xpt2046.py из репозитория micropython-ili9341 от rdagger; достаточно скопировать оба файла на Pico через Thonny. В коде ниже дисплей висит на SPI0 с частотой 40 MHz, а контроллер касаний — на отдельной шине SPI1 с частотой 1 MHz, так что разница в скорости двух микросхем не мешает. Код рисует текст и фигуры, а затем ставит жёлтые точки там, где ты касаешься экрана, — получается простая доска для рисования. Если Pico и Thonny пока незнакомы, начни со статьи Что такое Raspberry Pi Pico.
1from machine import Pin, SPI
2from time import sleep_ms
3from ili9341 import Display, color565 # ili9341.py Pico'da olmalı / must be on the Pico
4from xpt2046 import Touch # xpt2046.py Pico'da olmalı / must be on the Pico
5
6# Ekran: SPI0, 40 MHz / Display: SPI0, 40 MHz
7spi_tft = SPI(0, baudrate=40000000, sck=Pin(18), mosi=Pin(19), miso=Pin(16))
8tft = Display(spi_tft, cs=Pin(17), dc=Pin(20), rst=Pin(21), width=240, height=320, rotation=0)
9
10# Dokunmatik: ayrı SPI1, 1 MHz / Touch: separate SPI1, 1 MHz
11spi_touch = SPI(1, baudrate=1000000, sck=Pin(10), mosi=Pin(11), miso=Pin(12))
12touch = Touch(spi_touch, cs=Pin(13), width=240, height=320)
13
14WHITE = color565(255, 255, 255)
15YELLOW = color565(255, 255, 0)
16
17tft.clear(color565(0, 0, 0)) # ekranı temizle / clear the screen
18tft.draw_text8x8(10, 10, "BlueGrays + Pico", WHITE)
19tft.fill_rectangle(10, 30, 100, 50, color565(255, 0, 0)) # dolu dikdörtgen / filled rectangle
20tft.draw_rectangle(130, 30, 100, 50, color565(0, 255, 0)) # çerçeve / outline
21tft.fill_circle(60, 130, 30, color565(0, 128, 255)) # dolu daire / filled circle
22tft.draw_line(0, 175, 239, 175, color565(255, 0, 255)) # çizgi / line
23tft.draw_text8x8(10, 185, "Ekrana dokun / Touch me", YELLOW)
24
25while True:
26 raw = touch.raw_touch() # dokunma yoksa None / None when not touched
27 if raw is not None:
28 x, y = touch.normalize(*raw) # ham değeri piksele çevir / raw value to pixels
29 y = 319 - y # eksen ters ise / if the axis is inverted
30 x = min(max(x, 3), 236) # ekran dışına taşma / keep inside the screen
31 y = min(max(y, 3), 316)
32 tft.fill_circle(x, y, 3, YELLOW)
33 sleep_ms(20)В портретной ориентации (rotation=0) ширина равна 240, а высота 320; чтобы использовать экран горизонтально, передай width=320 и height=240 вместе с rotation=90. Калибровка драйвера сенсора по умолчанию (x_min=100, x_max=1962) подходит не каждому модулю; если точка смещается, выведи значения raw_touch() и передай в Touch() свои x_min, x_max, y_min и y_max. Для вывода картинок функция драйвера draw_image() читает файлы .raw, переведённые в RGB565; это преобразование на компьютере выполняет скрипт utils/img2rgb565.py из репозитория.
Проблема белого экрана и как её решить
Самая известная проблема ILI9341 — экран остаётся полностью белым, хотя подсветка горит. Белый экран на самом деле подсказка: питание и подсветка работают, но микросхема драйвера так и не была инициализирована. Когда панель не получает команд, пиксели пропускают свет, и экран выглядит белым. Значит, проблема почти всегда в связи. Пройди по таблице сверху вниз:
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Белый экран, подключение к Uno напрямую | Сигналы 5 V, нет преобразователя уровней | Поставь 74HC4050 или делители напряжения; если микросхема повреждена, замени модуль |
| Белый экран, провода подключены | Перепутаны CS и DC или номера пинов в коде не совпадают с подключением | Сверь определения TFT_CS, TFT_DC и TFT_RST с таблицей по одному |
| Белый экран, RESET «висит в воздухе» | RESET не подключён или стоит не на том пине | Подключи RESET к пину из кода или к 3,3 V; никогда не оставляй его неподключённым |
| Белый экран на ESP32 (TFT_eSPI) | User_Setup.h не отредактирован или стёрт при обновлении | Отключи строки ESP8266 и заново пропиши пины ESP32 и ILI9341_DRIVER |
| Белый экран время от времени, изображение сдвигается или искажается | Длинные перемычки, слишком быстрый SPI, плохой контакт в макетной плате | Провода короче 10–15 см; снизь SPI до 20–27 MHz, а на Uno до 4 MHz |
| Белый экран только при подключённом сенсоре или SD | Пин CS другого устройства «висит» и занимает шину | Каждый неиспользуемый пин CS сделай в коде OUTPUT и подай на него HIGH |
| Не работает ни с каким кодом | Дисплей не на ILI9341 (ST7789, ILI9488) или это параллельный шилд | Прочитай надпись на обороте; экран 3,5 дюйма 320×480 — это ILI9488, для шилда нужна MCUFRIEND_kbv |
| Белый экран, подсветка мерцает | Не хватает питания | Питай VCC от 5 V, попробуй другой USB-кабель и другой источник питания |
Другие частые ошибки
- Неправильные цвета: если красный показывается синим, панель работает в порядке BGR; измени настройку TFT_RGB_ORDER в TFT_eSPI. Если цвета похожи на негатив, попробуй tft.invertDisplay(true).
- Изображение перевёрнуто или зеркально: поменяй значение setRotation() в пределах от 0 до 3 и заново подстрой под него оси сенсора.
- Дисплей работает, а сенсор или SD нет: проверь, что T_CS и SD_CS совпадают с пинами в коде. На некоторых модулях выход SDO дисплея не освобождает шину; в таком случае попробуй отключить пин SDO дисплея — перестанет работать только диагностическое чтение.
- BMP не показывается: карта не в FAT32, имя файла длиннее 8 символов или картинка не 24-битная. Не забудь выбрать в Paint 24 бита; 16- и 32-битные BMP этот скетч не рисует.
- Не выводится кириллица: в стандартном шрифте GFX нет русских букв и букв с диакритикой вроде é. В TFT_eSPI можно создать свой сглаженный шрифт или взять шрифт GFX с нужными символами.
- Дисплей медленный: полное обновление экрана на Uno занимает не меньше 0,15 с. Для анимаций и игр переходи на ESP32 или Pico и перерисовывай только ту область, которая меняется.
- Модуль греется: возможно, на VCC подано 5 V при замкнутой перемычке J1 или перепутаны VCC и GND; немедленно отключи питание.
Идеи проектов с ILI9341
- Сенсорная панель умного дома: управляй светом и реле большими кнопками на экране; идеи есть в статье Проект умного дома на Arduino.
- Цветной экран погоды: показывай погоду, полученную из интернета через ESP32, с иконками и крупными цифрами (Метеостанция на ESP32).
- Панель датчиков: выводи температуру, влажность и давление с BME280 в виде стрелочных индикаторов с графиком за последние 24 часа.
- Ретро-консоль: играй в цветную «Змейку», Pong или «Тетрис»; для управления добавь модуль джойстика, а идеи ищи в статье Игровые проекты на Arduino.
- Цифровая фоторамка: показывай BMP-картинки с SD-карты по очереди, меняя их каждые несколько секунд.
- Мини-осциллограф: рисуй значения analogRead плавной осциллограммой с помощью спрайта на ESP32.
- Сенсорный кодовый замок: нарисуй на экране цифровую клавиатуру и открывай реле или сервопривод при вводе правильного кода.
ILI9341 — самый доступный способ дать проекту цветной сенсорный интерфейс: начни с Uno, чтобы освоить основные функции рисования, а когда понадобятся скорость и плавная анимация, переходи на ESP32 с TFT_eSPI. Если хочется начать с экрана поменьше, хорошая отправная точка — статья OLED-дисплей (SSD1306). В приложении BlueGrays есть проекты экрана погоды на ESP32, регистратора температуры, парктроника и игр с дисплеем — с пошаговыми схемами подключения и кодом.
Частые вопросы
Работает ли дисплей ILI9341 от 5 V?
Пин VCC модуля принимает 3,3–5 V, потому что на плате стоит стабилизатор 3,3 V. А вот сигнальные пины, такие как CS, DC, SCK и MOSI, рассчитаны только на 3,3 V. С Arduino Uno на эти линии нужно поставить преобразователь уровней, например 74HC4050, или делители напряжения.
Почему ILI9341 показывает только белый экран?
Белый экран означает, что подсветка горит, но микросхема драйвера так и не была инициализирована. Самые частые причины: неправильно подключены CS, DC или RESET, на Uno нет преобразователя уровней, в TFT_eSPI не отредактирован User_Setup.h или дисплей на самом деле не на ILI9341.
Какую библиотеку использовать для ILI9341?
Для Arduino Uno, Nano и Mega проще всего взять Adafruit_ILI9341 вместе с Adafruit GFX. На платах ESP32, ESP8266 и RP2040 предпочитают гораздо более быструю TFT_eSPI. А для параллельных 2,4-дюймовых шилдов, которые надеваются на Uno, нужна MCUFRIEND_kbv.
Как откалибровать сенсорный экран ILI9341?
Контроллер касаний XPT2046 выдаёт «сырые» значения от 0 до 4095. Запиши самые маленькие и самые большие значения, которые видны в мониторе порта при касании углов, и переведи их в экранные координаты через map(). С TFT_eSPI это сделает пример Touch_calibrate: он выведет готовый массив калибровки.
Чем отличаются ILI9341 на 2,4 и 2,8 дюйма?
У обоих разрешение 240×320 пикселей, одинаковые пины и код. У 2,8-дюймового экрана пиксели чуть крупнее, поэтому текст легче читать, а по сенсорным кнопкам легче попадать. 2,4-дюймовый модуль влезает в корпуса поменьше.
Можно ли использовать ILI9341 с Arduino Nano?
Да. Пины SPI у Nano такие же, как у Uno: D13 SCK, D11 MOSI и D12 MISO. Nano тоже работает с 5-вольтовой логикой, поэтому нужен преобразователь уровней. 2 КБ ОЗУ хватает, чтобы управлять дисплеем, но не чтобы держать в памяти большие картинки, поэтому их приходится читать с SD-карты небольшими частями.
Что собрать после прочтения
Все проекты
ESP32 HTTPClient + API погоды (с OLED-дисплеем SSD1306)
В этом проекте ESP32 через HTTPClient.h делает вызов REST API к сервису погоды (wttr.in). В отличие от прошлой версии с I2C LCD, здесь используется OLED SSD1306 (128×64), который позволяет крупные шрифты, векторные значки и богатую картинку на одном экране. Семь векторных значков погоды (солнечно, переменная облачность, облачно, дождь, снег, гроза, туман) рисуются во время работы (растровые файлы не нужны). Температура выводится крупными цифрами.

Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
Собери на ESP32 регистратор температуры с отметками времени! Каждую минуту читай температуру и влажность с DHT22, бери реальное время из DS1307 RTC и записывай всё на карту microSD в формате CSV. OLED показывает текущую температуру (крупным шрифтом), влажность в процентах, время и счётчик записей. Файл /temp_log.csv на SD-карте открывается в Excel, Google Sheets или Python pandas — идеально для теплиц, комнат, лабораторий и хранения продуктов.

Смеситель цветов на ESP32 (3 потенциометра + RGB-светодиод + OLED)
Умная лампа для смешивания цветов на ESP32: вращая три потенциометра, можно получить любой цвет! Когда крутишь потенциометры красного, зелёного и синего, RGB-светодиод с общим катодом меняет цвет в реальном времени. OLED-дисплей SSD1306 показывает значения каждого цвета в диапазоне 0–255, полосы заполнения и веб-код цвета HEX (например, #FF8040). Мини-студия цвета, которая учит теории цвета RGB, чтению АЦП, регулировке яркости через PWM и дизайну экрана I2C.

Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)
ESP32-версия автомобильного парктроника! Ультразвуковой датчик HC-SR04 измеряет расстояние до предмета перед ним. По мере уменьшения расстояния шкала из 5 светодиодов заполняется (зелёный → жёлтый → красный), а интервал сигналов зуммера сокращается. Когда предмет совсем близко (меньше 10 см), звучит непрерывная тревога и горят все светодиоды. OLED SSD1306 показывает расстояние в см крупным шрифтом и сообщение состояния (STOP / CAUTION / CLOSE / STOP! / DANGER).

Игра в пинг-понг — SSD1306
Мяч, две ракетки и ты: на маленьком OLED-экране ты играешь в настольный теннис — одну из самых первых видеоигр. Две кнопки двигают твою ракетку справа вверх и вниз, а левой ракеткой управляет компьюте

OLED T-Rex Run: игра с динозавром на Arduino
Игра с динозавром, которая появляется, когда пропадает интернет, теперь у тебя на столе: динозавр бежит по маленькому OLED-экрану, навстречу едут кактусы, а прыгаешь ты одной кнопкой. Счёт растёт в ре

I2C LCD-дисплей (16x2) на ESP32
В этом проекте символьный LCD-экран 16x2 подключается к ESP32 по протоколу I2C и выводит текст. Благодаря I2C экраном можно управлять всего двумя пинами данных (SDA и SCL), не тратя 6+ пинов, как при классическом параллельном подключении. Код печатает постоянное сообщение в первой строке и счётчик, растущий каждую секунду, во второй. Кроме того, при запуске работает сканер I2C, который выводит адрес дисплея в монитор порта.

Игра-бегалка на LCD с Arduino
Бесконечная игра-бегалка с горизонтальной прокруткой на символьном LCD-экране 16x2. Герой бежит сам, а игрок перепрыгивает препятствия нажатием кнопки. Случайные препятствия появляются в верхней и нижней строке. Пассивный зуммер играет вступительную мелодию, звук прыжка, праздничный звук на 50 очков и мелодию конца игры. При запуске показывается заставка «BlueGrays - Arduino Projects». Счёт обновляется сразу в правом верхнем углу экрана.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

