Компоненты · 13 мин чтения

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
Цветной TFT-дисплей ILI9341: 240×320 пикселей, интерфейс SPI и 65 тысяч цветов. В сенсорных версиях на плате есть ещё микросхема XPT2046 и слот microSD.

Что такое 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×320240×320240×320
ПодключениеSPI, перемычкамиSPI, перемычками8-битная параллельная шина, надевается сверху на Uno
Занятые пины5 для дисплея, 8–9 с сенсором и SD5 для дисплея, 8–9 с сенсором и SDПочти все (D2–D9, A0–A4, D10–D13 для SD)
ДрайверILI9341ILI9341Бывает разный: ILI9341, ILI9325, ST7789 и другие
БиблиотекаAdafruit_ILI9341, TFT_eSPIAdafruit_ILI9341, TFT_eSPIMCUFRIEND_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 (через преобразователь уровней)ESP32Raspberry Pi Pico
VCC5V3V33V3(OUT)
GNDGNDGNDGND
CSD10GPIO 15GP17
RESETD8GPIO 4GP21
DCD9GPIO 2GP20
SDI (MOSI)D11GPIO 23GP19
SCKD13GPIO 18GP18
LED3.3V3V33V3(OUT)
SDO (MISO)D12 (напрямую)GPIO 19GP16
T_CLKD13GPIO 18GP10
T_CSD7GPIO 21GP13
T_DIND11GPIO 23GP11
T_DOD12 (напрямую)GPIO 19GP12
T_IRQD2 (напрямую)Не подключёнНе подключён
SD_CSD4GPIO 5Не подключён
SD_MOSI / SD_MISO / SD_SCKD11 / D12 / D13GPIO 23 / 19 / 18Не подключены
Пин VCC у модулей ILI9341 принимает 5 V, но сигнальные пины рассчитаны только на 3,3 V. Стабилизатор на плате понижает лишь питание; на линиях CS, DC, RESET, SCK и MOSI согласования уровней нет. Если подключить 5-вольтовые выходы Uno напрямую, часто получается белый экран, а иногда микросхема выходит из строя навсегда. Перемычка J1 на обратной стороне платы нужна, чтобы обойти стабилизатор: запаивай её, только если VCC питается от 3,3 V. Если подать 5 V на VCC при замкнутой J1, дисплей сгорит.

Согласование уровней 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

  1. Открой «Менеджер библиотек» (Library Manager) в Arduino IDE, найди Adafruit ILI9341 и установи. Когда IDE спросит о зависимостях, выбери Install all («Установить все»), чтобы установились также Adafruit GFX и Adafruit BusIO. Если это первая библиотека, поможет статья Как добавить библиотеку в Arduino.
  2. Для сенсора установи ещё библиотеку XPT2046_Touchscreen от Пола Стоффрегена (Paul Stoffregen). Библиотека SD входит в комплект Arduino IDE.
  3. Подключи дисплей по таблице выше, на Uno через преобразователь уровней. Для первого теста пины сенсора и SD можно оставить неподключёнными.
  4. Загрузи скетч ниже и открой монитор порта на скорости 9600 бод. На дисплее должны появиться текст и фигуры, а в мониторе порта — диагностические коды дисплея.
  5. Попробуй и готовые примеры: «Файл › Примеры» (File › Examples) › Adafruit ILI9341 › graphicstest измеряет, за сколько микросекунд выполняется каждая операция рисования.
C++ (Arduino IDE)
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 из библиотеки. Зелёному достаётся лишний бит, потому что человеческий глаз к зелёному чувствительнее.

Если каждый раз стирать меняющиеся числа через fillScreen() и рисовать заново, экран мерцает: на Uno заливка всего экрана при SPI на 8 MHz занимает не меньше 0,15 с. Вместо этого используй setTextColor(текст, фон), как в скетче, или очищай через fillRect() только небольшую область, которая меняется. Постоянные строки внутри F("...") к тому же экономят 2 КБ ОЗУ у Uno; о проблемах с памятью читай в статье Arduino: не хватает памяти.

Чтение сенсорного экрана XPT2046

У сенсорных модулей поверх стекла лежит тонкий резистивный слой. Там, где нажимает палец или стилус, соприкасаются два проводящих слоя; микросхема XPT2046 измеряет координаты X и Y этой точки 12-битным АЦП в виде «сырых» значений от 0 до 4095 и вдобавок сообщает силу нажатия (Z). Если Z меньше 400, библиотека считает, что касания нет. Микросхема сидит на той же шине SPI, что и дисплей, но общается на меньшей скорости, около 2 MHz; библиотека сама выставляет скорость при каждом чтении. Пин T_IRQ переходит в LOW при касании экрана и избавляет процессор от лишних опросов. Скетч ниже превращает экран в маленькую доску для рисования: цвет выбирается в квадратиках сверху, а экран очищается кнопкой «SIL» (стереть).

C++ (Arduino IDE)
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 тоже должны идти через преобразователь уровней.

C++ (Arduino IDE)
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-вольтовой логике ESP32 подключается к ILI9341 напрямую, гонит SPI на 40 MHz и рисует в разы быстрее Uno.

ESP32 работает от 3,3 V и может разгонять SPI до 40 MHz, поэтому преобразователь уровней не нужен, а заливка всего экрана теоретически сокращается примерно до 31 мс. Чтобы действительно использовать эту скорость, обычно берут библиотеку TFT_eSPI от Bodmer. Она обращается к аппаратному SPI ESP32 напрямую и поддерживает сглаженные шрифты и, самое главное, внеэкранные буферы — спрайты (sprites). В спрайт в ОЗУ можно рисовать сколько угодно, а потом отправить результат на экран за один раз, поэтому даже быстро меняющаяся графика не мерцает. Особенность TFT_eSPI в том, что пины задаются не в скетче, а в файле User_Setup.h внутри папки библиотеки. Открой его, закомментируй строки с пинами ESP8266 (NodeMCU), которые включены по умолчанию, и включи эти:

C++ (Arduino IDE)
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  2500000
C++ (Arduino IDE)
1#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.

При обновлении TFT_eSPI из Arduino IDE папка библиотеки перезаписывается, и настройки в User_Setup.h пропадают; если дисплей, который вчера работал, сегодня остаётся белым, первым делом смотри сюда, и держи резервную копию файла настроек. Если красный и синий поменялись местами, включи TFT_RGB_ORDER TFT_BGR; если цвета похожи на негатив, включи TFT_INVERSION_ON; некоторые панели лучше работают с ILI9341_2_DRIVER. TFT_eSPI 2.5.43 на некоторых платах с пакетом esp32 версий 3.x может падать и перезагружаться при старте; в таком случае вернись в «Менеджере плат» (Boards Manager) к пакету esp32 версии 2.0.x.

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.

MicroPython
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-кабель и другой источник питания
Чтобы сузить круг поиска, используй диагностические строки из первого скетча. Если монитор порта показывает Power Mode: 0x94 и Self Diagnostic: 0xC0, дисплей получает команды, и проблема в программе (ориентация, цвета или код рисования). Если значения 0x0 или 0xFF, Arduino не может связаться с дисплеем: проверь провода, преобразователь уровней и пины CS/DC. Такое чтение работает, только когда линия SDO (MISO) подключена.

Другие частые ошибки

  • Неправильные цвета: если красный показывается синим, панель работает в порядке 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)
#134Продвинутый
Premium

ESP32 HTTPClient + API погоды (с OLED-дисплеем SSD1306)

В этом проекте ESP32 через HTTPClient.h делает вызов REST API к сервису погоды (wttr.in). В отличие от прошлой версии с I2C LCD, здесь используется OLED SSD1306 (128×64), который позволяет крупные шрифты, векторные значки и богатую картинку на одном экране. Семь векторных значков погоды (солнечно, переменная облачность, облачно, дождь, снег, гроза, туман) рисуются во время работы (растровые файлы не нужны). Температура выводится крупными цифрами.

4.3(10)
19
Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)
#144Продвинутый
Premium

Регистратор температуры на ESP32 (DHT22 + DS1307 RTC + microSD + OLED)

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

4.7(7)
12
Смеситель цветов на ESP32 (3 потенциометра + RGB-светодиод + OLED)
#138Продвинутый
Premium

Смеситель цветов на ESP32 (3 потенциометра + RGB-светодиод + OLED)

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

4.7(6)
10
Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)
#142Продвинутый
Premium

Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)

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

0.0(0)
14
Игра в пинг-понг — SSD1306
#86Средний
Premium

Игра в пинг-понг — SSD1306

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

4.5(47)
65
OLED T-Rex Run: игра с динозавром на Arduino
#87Продвинутый
Premium

OLED T-Rex Run: игра с динозавром на Arduino

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

4.7(33)
53
I2C LCD-дисплей (16x2) на ESP32
#119Средний
Premium

I2C LCD-дисплей (16x2) на ESP32

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

4.9(23)
40
Игра-бегалка на LCD с Arduino
#98Средний
Premium

Игра-бегалка на LCD с Arduino

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

4.6(32)
55