ILI9341 Arduinoガイド:TFTディスプレイの配線、タッチ、SDカードの画像表示
ILI9341 TFTディスプレイをArduinoにつなぐ方法。Unoでの3.3 Vレベル変換、Adafruit GFX、XPT2046タッチ、SDカードからのBMP表示、ESP32のTFT_eSPI、白画面の直し方を解説します。
ILI9341 Arduinoの組み合わせは、240×320ピクセル・65K色のディスプレイをプロジェクトに追加する、最も安く最も定番の方法です。赤い基板に載った2.4インチと2.8インチのTFTモジュールは、SPIで数本の配線をするだけでつながります。タッチ付きのモデルには、XPT2046タッチコントローラーとmicroSDカードスロットも載っています。1枚のモジュールで、カラーのグラフィック、タッチボタン、カードから読み込んだ画像を表示できます。OLEDやキャラクタLCDでは物足りなくなったメニュー、ダッシュボード、ゲーム、フォトフレームに最適です。
このガイドでは、モジュールの仕組み、Arduino Unoの5 V出力をディスプレイの3.3 V入力に安全につなぐ方法、Adafruit_ILI9341とAdafruit GFXライブラリで文字・図形・色を描く方法、タッチスクリーンの読み取り、SDカードからBMP画像を表示する方法をステップごとに学びます。続いて、ESP32でTFT_eSPIを使ったはるかに速い描画、Raspberry Pi PicoでのMicroPythonの使い方、そしてほとんどの人が一度はぶつかる「白画面」問題のすべての原因を取り上げます。

ILI9341 TFTディスプレイとは?仕組みを解説
ILI9341は、ガラスの裏にあるドライバチップです。中には240×320ピクセルすべてを保持する約172 KBの表示メモリ(GRAM)があります。ArduinoはこのメモリにSPIでコマンドと色データを送り、チップは自分で画像を毎秒数十回リフレッシュします。そのため、画像をArduinoのRAMに置く必要がありません。RAMが2 KBしかないUnoでも、大きなカラー画面を駆動できます。何かを描くときは、まずウィンドウ(x、y、幅、高さ)を選び、そのウィンドウのピクセルを順番に送ります。DCピンは、送っているバイトがコマンド(LOW)かデータ(HIGH)かをチップに伝えます。色は通常RGB565形式、つまり1ピクセルあたり16ビットで送られます。内訳は赤が5ビット、緑が6ビット、青が5ビットです。SPIバスの仕組みはSPIとは?で詳しく説明しています。
| 項目 | 値 |
|---|---|
| ドライバチップ | ILI9341 |
| 解像度 | 240×320ピクセル(横向きでは320×240) |
| 色 | RGB565で65,536色(チップ自体は262K色に対応) |
| 画面サイズ | 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シールドの比較
| 項目 | 2.4インチSPIモジュール | 2.8インチSPIモジュール | 2.4インチUnoシールド |
|---|---|---|---|
| 解像度 | 240×320 | 240×320 | 240×320 |
| 接続 | SPI(ジャンパーワイヤーで配線) | SPI(ジャンパーワイヤーで配線) | 8ビットパラレル(Unoの上に差し込む) |
| 使用ピン | ディスプレイだけで5本、タッチとSD込みで8〜9本 | ディスプレイだけで5本、タッチとSD込みで8〜9本 | ほぼすべて(D2〜D9、A0〜A4、SD用にD10〜D13) |
| ドライバ | 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向けの設計 |
この記事では、ESP32やPicoでも使え、Unoのピンの大半を空けておけるSPIモジュールを扱います。2種類のSPIモジュールはピンもコードもまったく同じで、2.8インチの画面はピクセルが少し大きいため、タッチボタンを押しやすくなります。ディスプレイがシールドとしてUnoの上に直接差し込むタイプなら、SPIライブラリでは駆動できず、たいてい白画面になります。そのようなシールドにはMCUFRIEND_kbvライブラリをインストールし、tft.readID()でドライバIDを読み取り(ILI9341なら0x9341)、tft.begin(ID)で起動します。小さいけれど精細な代わりの選択肢はST7789 TFTディスプレイを、すべての選択肢を比べるならArduinoのディスプレイの種類を読んでください。
ILI9341のピン配置と配線表
モジュールの一辺には14ピンのメインヘッダーがあります。最初の9ピンはディスプレイ用で、T_で始まる最後の5ピンはタッチ用です。反対側の辺にある4ピン(SD_CS、SD_MOSI、SD_MISO、SD_SCK)はカードスロットにつながっています。3つのデバイスは同じ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 | 未接続 |
Arduino Unoでの3.3 Vレベル変換
ESP32とRaspberry Pi Picoはもともと3.3 Vで動くので、ディスプレイを直接つなげます。Uno、Nano、Megaのような5 Vのボードでは、Arduinoからディスプレイへ向かうすべてのラインを3.3 Vに下げる必要があります。対象はCS、DC、RESET、SCK、MOSI、さらにタッチとSDを使う場合はT_CLK、T_DIN、T_CS、SD_CS、SD_MOSI、SD_SCKです。ディスプレイからArduinoへ向かうSDO、T_DO、SD_MISO、T_IRQの各ラインには変換は不要です。Unoは3 Vを超える電圧をHIGHと読むので、3.3 Vの出力でその条件を満たせます。実際には4つの方法があります。
- 74HC4050バッファIC(最もきれいな方法):3.3 Vで動作し、入力に5 Vを受け付け、1個のチップで6本のラインを変換します。高速で、8 MHzのSPIでも問題なく動きます。古いCD4050も使えますが遅く、画像が乱れる場合はSPIの速度を下げる必要があります。ディスプレイだけなら5本で足りますが、タッチとSDを加えるとT_CSとSD_CSが増えて7本になるため、7本目には2個目のチップか分圧回路を使ってください。
- 抵抗による分圧回路:各ラインで、Arduinoのピンとディスプレイのピンの間に1 kΩ、ディスプレイのピンとGNDの間に2 kΩを入れます。これで5 V × 2 / 3 ≈ 3.3 Vになります。ほかの抵抗の組み合わせは分圧計算ツールで求められます。安上がりですが、5本のラインに10本の抵抗が必要になり、ブレッドボードが混み合います。
- 市販のレベル変換モジュール:BSS138 MOSFETを使った4チャネルや8チャネルのモジュールは配線が簡単ですが、搭載されたプルアップ抵抗のせいで、高速時に信号のエッジがなまります。画像が乱れる場合は、tft.begin(4000000)でSPIを4 MHzに下げてください。各方法の比較はレベル変換モジュールの記事にあります。
- 避けるべき方法:よく知られた「各ラインに10 kΩの直列抵抗を入れる」方法は、余分な電圧をチップ内部の保護ダイオードに流し込みます。しばらくは動くかもしれませんが、信頼性が低く、長い目で見るとディスプレイを傷めるおそれがあります。
ステップごとに:まずAdafruit_ILI9341でテスト
- Arduino IDEでライブラリマネージャを開き、Adafruit ILI9341を検索してインストールします。依存ライブラリについて聞かれたら全てをインストールを選び、Adafruit GFXとAdafruit BusIOも一緒にインストールします。初めてライブラリを入れる場合は、Arduinoにライブラリを追加する方法が役立ちます。
- タッチを使う場合は、Paul Stoffregen氏のXPT2046_Touchscreenライブラリもインストールします。SDライブラリはArduino IDEに最初から入っています。
- 上の表のとおりにディスプレイを配線します。Unoではレベル変換を経由させてください。最初のテストでは、タッチとSDのピンはつながなくてもかまいません。
- 下のスケッチを書き込み、シリアルモニタを9600ボーで開きます。ディスプレイに文字と図形が表示され、シリアルモニタにディスプレイの診断コードが出るはずです。
- 付属のスケッチ例も試してみてください。「ファイル」>「スケッチ例」>「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では8 MHz、ESP32では40 MHzのSPIを使用) |
| tft.setRotation(0〜3) | 向きを選択:0と2は縦向き(240×320)、1と3は横向き(320×240) |
| tft.fillScreen(色) | 画面全体を1色で塗りつぶす |
| 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(赤, 緑, 青) | 0〜255のRGB値を16ビットの色に変換 |
| tft.readcommand8(コマンド) | ディスプレイのレジスタを読み取る(MISOの接続が必要)。トラブルシューティング用 |
定番の色定数(ILI9341_RED、ILI9341_NAVY、ILI9341_ORANGEなど19個)はライブラリに入っています。独自の色にはcolor565()を使います。これは赤の上位5ビット、緑の上位6ビット、青の上位5ビットを並べて詰め込む関数で、計算式は「色 = (R >> 3) << 11 | (G >> 2) << 5 | (B >> 3)」です。たとえばオレンジ(255, 165, 0)の場合、31 << 11 = 0xF800と41 << 5 = 0x0520が合わさって0xFD20になり、これはライブラリのILI9341_ORANGEの値とぴったり一致します。緑だけ1ビット多いのは、人間の目が緑に対してより敏感だからです。
XPT2046タッチスクリーンの読み取り
タッチ付きモジュールでは、ガラスの上に薄い抵抗膜があります。指やタッチペンで押した場所で2枚の導電層が接触し、XPT2046チップがその点のXとYの位置を12ビットのADCで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の定数に入力します。点が指の位置と鏡写しに表示される場合は、map()でその軸の最小値と最大値を入れ替えます。XとYが完全に入れ替わっている場合は、ts.setRotation()に0〜3の値を試してください。本格的なメニューも同じ仕組みで、ボタンを描き、タッチした点がどの長方形に入るかを調べます。抵抗膜は指でも反応しますが、小さなボタンにはタッチペンのような先の細いもののほうが正確です。
SDカードからBMP画像を表示する
モジュール裏面のmicroSDスロットを使えば、ロゴや背景、写真をカードに保存して画面に表示できます。画像を用意するには、写真を320×240ピクセルにリサイズし、ペイントで24ビット ビットマップ(.bmp)として保存します。ArduinoのSDライブラリは短いファイル名で動作するため、RESIM.BMPのような8文字以内の名前を使い、カードはFAT32でフォーマットしてください。カード自体の詳細とinitialization failed(初期化失敗)エラーの直し方は、ArduinoのSDカードモジュールにあります。このスロットも3.3 Vであることを忘れないでください。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}BMPファイルの先頭54バイトはヘッダーです。そこには「BM」というシグネチャ、ピクセルデータの開始位置、幅、高さ、色深度が格納されています。ピクセルは青、緑、赤(BGR)の順で、通常は下から上へ格納され、各行は4バイトの倍数になるように埋め草(パディング)が入っています。そのためスケッチは、画面の行ごとにファイル内の正しい位置へジャンプし、一度に20ピクセル(60バイト)を読み込んでRGB565に変換し、drawRGBBitmap()で送り出します。小さな塊ごとに処理するので、すべてがUnoのRAMに収まり、SDカードとディスプレイが同じSPIバスを交代で使えます。描画時間はシリアルモニタに表示されます。Unoでは、全画面の画像が数秒かけて1行ずつ現れます。PNGやJPEGのファイルを表示したい場合や、はるかに速く描画したい場合は、TFT_eSPIとTJpg_Decoderライブラリを使ってESP32に移行してください。
ESP32とTFT_eSPIで高速描画

ESP32は3.3 Vで動作し、SPIを最大40 MHzで駆動できるため、レベル変換は不要で、画面全体の塗りつぶしは理論上約31ミリ秒まで短くなります。この速度を本当に活かすには、Bodmer氏のTFT_eSPIライブラリが定番です。ESP32のSPIハードウェアに直接アクセスし、アンチエイリアス(スムーズ)フォントと、何より重要なスプライトと呼ばれる画面外バッファに対応しています。RAM上のスプライトに好きなだけ描いてから、結果を一度に画面へ送るので、速く変化するグラフィックでもちらつきません。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)を表示します。16ビットカラーで300×100ピクセルのスプライトは、60 KBのRAMを使います。これはUnoの全メモリの約30倍ですが、ESP32なら問題ありません。タッチでは、T_CLK、T_DIN、T_DOピンをディスプレイのSCK、MOSI、MISOのラインに合流させ、T_CS(GPIO 21)だけを別にします。自分のキャリブレーション値を得るには、TFT_eSPIのGeneric > Touch_calibrateのスケッチ例を一度実行し、表示される角の矢印にタッチして、シリアルモニタに出力される5つの数値をcalData配列にコピーします。User_Setup_Select.hにある既成のSetup42_ILI9341_ESP32.hの行でも同じディスプレイのピンが選択されますが、TOUCH_CSの行は自分で追加する必要があります。ESP32が初めての場合は、まずESP32とは?を読んでください。
Raspberry Pi PicoとMicroPythonでILI9341を使う
Picoも3.3 Vのボードなので、ディスプレイを直接つなげます。最も広く使われているMicroPythonドライバは、rdagger氏のmicropython-ili9341リポジトリにあるili9341.pyとxpt2046.pyの組み合わせです。Thonnyで両方のファイルをPicoにコピーするだけで使えます。下のコードでは、ディスプレイがSPI0(40 MHz)、タッチコントローラーが別のSPI1バス(1 MHz)につながっているので、2つのチップの速度差は問題になりません。コードは文字と図形を描いた後、タッチした場所に黄色い点を置いていき、簡単なお絵かきボードになります。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です。画面を横向きで使うには、rotation=90と一緒にwidth=320とheight=240を渡します。タッチドライバのデフォルトのキャリブレーション(x_min=100、x_max=1962)が、すべてのモジュールに合うとは限りません。点がずれる場合は、raw_touch()の値を表示し、自分で求めたx_min、x_max、y_min、y_maxをTouch()に渡してください。画像を表示する場合、ドライバのdraw_image()関数は、RGB565に変換した.rawファイルを読み込みます。この変換は、リポジトリのutils/img2rgb565.pyスクリプトがパソコン上で行います。
白画面問題とその直し方
ILI9341で最も有名な問題は、バックライトは点いているのに画面が真っ白のままになる現象です。実は、白画面は手がかりになります。電源とバックライトは動いているものの、ドライバチップが一度も初期化されていないということだからです。パネルにコマンドが届かないと、ピクセルが光を通してしまい、画面が白く見えます。つまり、問題はほぼ必ず通信にあります。下の表を上から順に確認してください。
| 症状 | 考えられる原因 | 対策 |
|---|---|---|
| 白画面(Unoに直結している) | 5 Vの信号、レベル変換なし | 74HC4050か分圧回路を使う。チップが壊れていればモジュールを交換 |
| 白画面(配線はつないである) | CSとDCが逆、またはコード内のピン番号が配線と一致していない | TFT_CS、TFT_DC、TFT_RSTの定義を表と1つずつ照合 |
| 白画面(RESETが浮いている) | RESETが未接続、または間違ったピンにつながっている | RESETをコードで指定したピンか3.3 Vにつなぐ。絶対に浮かせたままにしない |
| ESP32で白画面(TFT_eSPI) | User_Setup.hが未編集、または更新で消えた | ESP8266の行を無効にし、ESP32のピンとILI9341_DRIVERを書き直す |
| ときどき白画面になる、画像がずれる・乱れる | ジャンパーワイヤーが長い、SPIが速すぎる、ブレッドボードの接触不良 | 配線を10〜15 cm未満にする。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_eSPIのTFT_RGB_ORDER設定を変更してください。色がネガのように見える場合は、tft.invertDisplay(true)を試してください。
- 画像が上下逆、または鏡写しになる:setRotation()の値を0〜3の間で変え、それに合わせてタッチの軸も調整します。
- ディスプレイは動くが、タッチやSDが動かない:T_CSとSD_CSがコード内のピンと一致しているか確認してください。一部のモジュールでは、ディスプレイのSDO出力がバスを解放しません。その場合はディスプレイのSDOピンを外してみてください。使えなくなるのは診断用の読み取りだけです。
- BMPが表示されない:カードがFAT32ではない、ファイル名が8文字より長い、または画像が24ビットではないことが原因です。ペイントで24ビットを選ぶのを忘れないでください。16ビットと32ビットのBMPは、このスケッチでは描画されません。
- 特殊文字が表示されない:GFXのデフォルトフォントには、éのようなアクセント付きの文字も、日本語のかなや漢字もありません。TFT_eSPIでは独自のスムーズフォントを作成できます。または、必要な文字を含むGFXフォントを使ってください。
- 表示が遅い:Unoでは、全画面の書き換えに少なくとも0.15秒かかります。アニメーションやゲームには、ESP32かPicoに移行し、変化する部分だけを描き直してください。
- モジュールが熱くなる:J1ジャンパーがショートされた状態でVCCに5 Vがかかっているか、VCCとGNDが逆になっている可能性があります。すぐに電源を外してください。
ILI9341を使ったプロジェクトのアイデア
- タッチ式スマートホームパネル:画面上の大きなボタンで照明やリレーを操作します。アイデアはArduinoのスマートホームプロジェクトを参考にしてください。
- カラーの天気表示:ESP32でインターネットから取得した天気を、アイコンと大きな数字で表示します(ESP32の天気ステーション)。
- センサーダッシュボード:BME280の温度、湿度、気圧をメーターとして描き、直近24時間のグラフも表示します。
- レトロゲーム機:スネークゲーム、ポン、テトリスをカラーで遊べます。操作用にジョイスティックモジュールを追加し、アイデアはArduinoのゲームプロジェクトを見てください。
- デジタルフォトフレーム:SDカードのBMP画像を数秒ごとに切り替えて表示します。
- ミニオシロスコープ:ESP32でスプライトを使い、analogReadの値を滑らかな波形として描きます。
- タッチ式テンキーロック:画面にテンキーを描き、正しいコードが入力されたらリレーやサーボで鍵を開けます。
ILI9341は、プロジェクトにカラーのタッチインターフェースを持たせる最も手頃な方法です。まずUnoで基本的な描画関数を覚え、速度や滑らかなアニメーションが必要になったら、TFT_eSPIを使ってESP32に移行しましょう。もっと小さな画面から始めたい場合は、OLEDディスプレイ(SSD1306)が良い出発点です。BlueGraysアプリでは、ESP32の天気表示、温度ロガー、駐車センサー、ディスプレイを使ったゲームのプロジェクトを、ステップごとの回路図とコード付きで見つけられます。
よくある質問
ILI9341ディスプレイは5 Vで動きますか?
モジュールのVCCピンは、基板上に3.3 Vレギュレーターがあるため3.3〜5 Vを受け付けます。ただし、CS、DC、SCK、MOSIなどの信号ピンは3.3 V専用です。Arduino Unoで使う場合は、これらのラインに74HC4050のようなレベル変換ICか分圧回路を入れてください。
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が好まれます。Unoに差し込む2.4インチのパラレルシールドには、代わりにMCUFRIEND_kbvが必要です。
ILI9341のタッチスクリーンはどうやってキャリブレーションしますか?
XPT2046タッチコントローラーは、0〜4095の生の値を返します。角にタッチしたときにシリアルモニタに表示される最小値と最大値をメモし、map()で画面の座標に変換します。TFT_eSPIを使う場合は、Touch_calibrateのスケッチ例がこれを自動で行い、そのまま使えるキャリブレーション配列を出力します。
2.4インチと2.8インチのILI9341の違いは何ですか?
どちらも解像度は240×320ピクセルで、ピンもコードも同じです。2.8インチの画面はピクセルが少し大きいので、文字が読みやすく、タッチボタンも押しやすくなります。2.4インチのモジュールは、より小さなケースに収まります。
ILI9341はArduino Nanoでも使えますか?
はい。NanoのSPIピンはUnoと同じで、D13がSCK、D11がMOSI、D12がMISOです。Nanoも5 Vロジックなので、レベル変換が必要です。2 KBのRAMはディスプレイを駆動するには十分ですが、大きな画像を保持するには足りないため、画像はSDカードから少しずつ読み込む必要があります。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32 HTTPClient+天気API(SSD1306 OLED付き)
このプロジェクトでは、ESP32でHTTPClient.hを使い、インターネット経由で天気API(wttr.in)をREST APIとして呼び出します。前のI2C LCD版とは異なり、SSD1306 OLED(128×64)ディスプレイを使うため、大きな文字やベクターアイコンで、1つの画面に豊かな表現ができます。天気を表す7種類のベクターアイコン(晴れ、晴れ時々曇り、曇り、雨、雪、嵐、霧)を実行時に描画するので、ビットマップファイルは不要です。気温は中央に3倍の大きさの文字で表示します。

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

ESP32の調色ライト(可変抵抗3個 + RGB LED + OLED)
ESP32と3個の可変抵抗で、好みの色を作れるスマート調色ライトです。赤・緑・青の可変抵抗を回すと、カソードコモンのRGB LEDの色がリアルタイムに変わります。SSD1306 OLEDディスプレイには、各色の0~255の値、色の強さを示すバー、#FF8040などのWeb用HEXカラーコードをリアルタイムで表示します。小さなプロ仕様のカラースタジオとして、RGBの色の仕組み、ADCによるアナログ値の読み取り、PWMによる明るさの制御、I2Cディスプレイの表示設計を1つのプロジェクトで学べます。

ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)
車の駐車センサーをESP32で作ります!HC-SR04超音波センサーで前方の物体までの距離を測ります。距離が縮まるほど5連LEDバーが緑 → 黄 → 赤の順に点灯し、ブザーの鳴る間隔が短くなります。10 cm未満まで近づくとアラームが鳴り続け、すべてのLEDが点灯します。SSD1306 OLEDには現在の距離をcm単位の大きな文字で表示し、状態メッセージ(落ち着いて / 注意 / 接近 / 停止! / 危険)とLEDバーのシミュレーションも表示します。HC-SR04のpulseIn、複数のデジタル出力、ブザーを学べる定番のプロジェクトです。

パドルボールゲーム(SSD1306)
ボール1個とパドル2本で遊ぶ、ビデオゲームの原点の1つである卓球ゲームを、小さなOLEDディスプレイで楽しめます。2つのボタンで右のパドルを上下に動かします。左のパドルはコンピューターがボールを追いながら操作し、ボールを打つたびにブザーが短く鳴ります。先に9点を取ると勝ちです。その後、新しいゲームが自動で始まります。

OLED T-Rexラン:Arduino恐竜ゲーム
インターネットが切れると現れる恐竜ゲームを、机の上で楽しめます。小さなOLEDディスプレイを恐竜が走り、迫るサボテンを1つのボタンでジャンプして避けます。横の4桁ディスプレイには得点がリアルタイムで表示され、100点ごとにブザーがお祝いの音を鳴らします。サボテンにぶつかるとゲーム終了です。ボタンを押すと、もう一度始められます。

ESP32 I2C LCDディスプレイ(16x2)
このプロジェクトでは、16x2文字のLCDをI2CでESP32に接続して文字を表示します。I2Cならデータ用の2本のピン(SDAとSCL)だけで制御でき、従来のパラレル接続のように6本以上のピンを使わずに済みます。コードでは1行目に固定メッセージ、2行目に毎秒増えるカウンターを表示します。また、起動時にI2Cスキャナーを実行し、ディスプレイのアドレス(通常は0x27または0x3F)をシリアルモニタに表示します。

Arduino LCDランナーゲーム
16x2文字のLCDディスプレイで動作する横スクロール型のエンドレスランナーゲームです。キャラクターは自動で走り、ボタンを押すとジャンプして障害物を飛び越えます。障害物は上段と下段にランダムに現れます。パッシブブザーで開始時のメロディー、ジャンプ音、50点獲得を祝う音、ゲーム終了時のメロディーを鳴らします。起動時には「BlueGrays — Arduino Projects」の画面を表示します。スコアは画面右上でリアルタイムに更新されます。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

