ArduinoでTFT LCDディスプレイ(ST7789)を使ってみる

ArduinoでTFT LCDディスプレイ(ST7789)を使ってみる

ArduinoでTFT LCDディスプレイ(ST7789)を使ってみました。ご紹介するTFT LCDディスプレイはSPI通信ですが、CS(チップセレクト)のない製品でした。そのため複数のSPIデバイスを繋ぐことができません。単体で動かす場合でしたら問題なく表示できました。RGBフルカラーにも対応してます。この記事では、Arduino UnoでTFT LCDディスプレイに文字や図形を表示する方法や、注意点などまとめました。ぜひ、ご参考いただければと思います。

ゴール

Twitterにアップした動画のように、ArduinoとTFT LCDディスプレイを繋いでサンプルプログラムを実行するまでをゴールとします。

つかうもの

この記事で使うものをご紹介いたします。

TFT LCDディスプレイ(ST7789)

こちらの、ST7789チップ搭載のTFT LCDディスプレイを使用しました。ピン端子を各自で半田付けする必要があります。

スペックは次のとおりです。

項目
サイズ 1.3インチ
表示モード ノーマリーブラックIPS
入力データ SPIインタフェース
ドライブIC ST7789VW
外形寸法 27.78(W)* 39.22(H)* 3.0 +/- 0.1(T)mm
解像度 240RGB×240ドット
液晶表示エリア 23.4(W)* 23.4(H)
ドットピッチ 0.0975(H)×0.0975(V)mm
使用温度 -20〜70℃
重量 6.1g

注意点としまして、この製品はSPI接続ですが、CS端子がないのです。そのため複数のSPIデバイスを接続したい場合に問題になります。実際、こちらの記事で紹介したSPI接続するSDカードモジュールを追加したところ、TFT LCDディスプレイは正常に表示しませんでした。

ですから、複数のSPIデバイスを接続したい場合は、チップセレクトのある商品を選びましょう。下記のディスプレイはST7735チップですが、ST7789と似た感じでプログラミングできます。ST7735の使い方はESP32でST7735 TFT LCD液晶ディスプレイを使ってみるにまとめましたのでご参考ください。

Arduino Uno

ここではArduino Unoを使ってTFT LCDディスプレイのテストを行いました。もちろんESP32や他のArduinoでも動かせると思いますが、その際はデバイスと接続する端子を変更する必要があると思うのでご了承ください。

▼ Arduinoがいっぱいありすぎてよく分からないという方は、こちらの記事で初心者向けの解説をしていますのでご参考になさってみてください。

その他

また、ブレッドボードやジャンプワイヤもあると便利です。

TFT LCDディスプレイとArduino Unoの配線

Amazonの製品説明では3.3Vの電源に繋ぐと書かれていますが、実際に試したところ3.3Vでは途中で落ちてしまい、5V電源に繋いだところ正常に動作しました。TFT LCDディスプレイとArduino Unoは次のとおり配線しました。BKLピンは使いません。

Arduino UnoとTFT LCDディスプレイの配線
Arduino UnoとTFT LCDディスプレイの配線

TFT LCDディスプレイ Arduino 説明
BLK 未接続 バックライト制御
DC 8 データ/コマンド制御
RES 9 リセット信号入力
SDA 11 シリアルデータ入力
SCL 13 シリアルクロック
VCC 5V 電源
GND GND Ground

ライブラリのインストール

ST7789搭載のTFT LCDディスプレイを動かすには、最低限 Adafruit_GFX Adafruit_SPITFT Adafruit_ST77xx Adafruit_ST7789 gfxfont が必要になります。下記GitHubリポジトリからインストールするなりしてください。

私は最近、VS CodeにPlatformIOを入れてArduino開発を行っています。そのため下記のような構成でライブラリを配置しています。基本ライブラリマネージャーは使っておらず、ソースコードをそのままダウンロードして配置させています。ここでは display_test.ino にサンプルプログラムを記述しました。

.
├── include
│   └── README
├── lib
│   ├── README
│   └── TFT_ST7789
│       ├── Adafruit_GFX.cpp
│       ├── Adafruit_GFX.h
│       ├── Adafruit_SPITFT.cpp
│       ├── Adafruit_SPITFT.h
│       ├── Adafruit_ST7789.cpp
│       ├── Adafruit_ST7789.h
│       ├── Adafruit_ST77xx.cpp
│       ├── Adafruit_ST77xx.h
│       ├── examples
│       │   └── display_test
│       │       └── display_test.ino
│       ├── gfxfont.h
│       └── glcdfont.c
├── platformio.ini
├── src
│   └── main.cpp
└── test
    └── README

デフォルトではmain.cppのソースコードがビルドされますが、上記のようなツリー構造の中の display_test.ino を対象としてビルドしたい場合は、platformio.ini を次のように記述することで可能です。

platformio.iniの設定
platformio.iniの設定

サンプルのスケッチ(ソースコード)

下記URLのサンプルコードをそのまま動かしてみました。

/**
 * 下記URLのサンプルコードを元に注釈を入れています。
 * https://github.com/jumejume1/tft240x240-spi/blob/master/tft.ino
 * 
 * @date 2022-11-21
 * @brief Modified by https://101010.fun
*/
#include <Adafruit_GFX.h>     // Core graphics library
#include <Adafruit_ST7789.h>  // Hardware-specific library for ST7789
#include <Arduino.h>
#include <SPI.h>

// TFTの電源電圧は5Vに繋ぐ(3.3Vだと落ちる)
// TFT SCL -> 13
// TFT SDA -> 11
#define TFT_CS 10  // CS端子がないので実際には接続しないし使わない
#define TFT_RST 9
#define TFT_DC 8

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

float p = 3.1415926;

void setup() {
    Serial.begin(115200);
    delay(500);
    tft.init(240, 240, SPI_MODE2); // SPI_MODE2指定しないと動かない
    delay(1000);
    Serial.println(F("Initialized"));

    uint16_t time = millis();
    tft.fillScreen(ST77XX_BLACK);
    time = millis() - time;

    Serial.println(time, DEC);
    delay(500);

    // large block of text
    tft.fillScreen(ST77XX_BLACK);
    testdrawtext(
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur "
        "adipiscing ante sed nibh tincidunt feugiat. Maecenas enim massa, "
        "fringilla sed malesuada et, malesuada sit amet turpis. Sed porttitor "
        "neque ut ante pretium vitae malesuada nunc bibendum. Nullam aliquet "
        "ultrices massa eu hendrerit. Ut sed nisi lorem. In vestibulum purus a "
        "tortor imperdiet posuere. ",
        ST77XX_WHITE);
    delay(1000);

    // tft print function!
    tftPrintTest();
    delay(4000);

    // a single pixel
    tft.drawPixel(tft.width() / 2, tft.height() / 2, ST77XX_GREEN);
    delay(500);

    // line draw test
    testlines(ST77XX_YELLOW);
    delay(500);

    // optimized lines
    testfastlines(ST77XX_RED, ST77XX_BLUE);
    delay(500);

    testdrawrects(ST77XX_GREEN);
    delay(500);

    testfillrects(ST77XX_YELLOW, ST77XX_MAGENTA);
    delay(500);

    tft.fillScreen(ST77XX_BLACK);
    testfillcircles(10, ST77XX_BLUE);
    testdrawcircles(10, ST77XX_WHITE);
    delay(500);

    testroundrects();
    delay(500);

    testtriangles();
    delay(500);

    mediabuttons();
    delay(500);

    Serial.println("done");
    delay(1000);
}

void loop() {
    tft.invertDisplay(true);
    delay(500);
    tft.invertDisplay(false);
    delay(500);
}

void testlines(uint16_t color) {
    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = 0; x < tft.width(); x += 6) {
        tft.drawLine(0, 0, x, tft.height() - 1, color);
        delay(0);
    }
    for (int16_t y = 0; y < tft.height(); y += 6) {
        tft.drawLine(0, 0, tft.width() - 1, y, color);
        delay(0);
    }

    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = 0; x < tft.width(); x += 6) {
        tft.drawLine(tft.width() - 1, 0, x, tft.height() - 1, color);
        delay(0);
    }
    for (int16_t y = 0; y < tft.height(); y += 6) {
        tft.drawLine(tft.width() - 1, 0, 0, y, color);
        delay(0);
    }

    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = 0; x < tft.width(); x += 6) {
        tft.drawLine(0, tft.height() - 1, x, 0, color);
        delay(0);
    }
    for (int16_t y = 0; y < tft.height(); y += 6) {
        tft.drawLine(0, tft.height() - 1, tft.width() - 1, y, color);
        delay(0);
    }

    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = 0; x < tft.width(); x += 6) {
        tft.drawLine(tft.width() - 1, tft.height() - 1, x, 0, color);
        delay(0);
    }
    for (int16_t y = 0; y < tft.height(); y += 6) {
        tft.drawLine(tft.width() - 1, tft.height() - 1, 0, y, color);
        delay(0);
    }
}

void testdrawtext(char *text, uint16_t color) {
    tft.setCursor(0, 0);
    tft.setTextColor(color);
    tft.setTextWrap(true);
    tft.print(text);
}

void testfastlines(uint16_t color1, uint16_t color2) {
    tft.fillScreen(ST77XX_BLACK);
    for (int16_t y = 0; y < tft.height(); y += 5) {
        tft.drawFastHLine(0, y, tft.width(), color1);
    }
    for (int16_t x = 0; x < tft.width(); x += 5) {
        tft.drawFastVLine(x, 0, tft.height(), color2);
    }
}

void testdrawrects(uint16_t color) {
    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = 0; x < tft.width(); x += 6) {
        tft.drawRect(tft.width() / 2 - x / 2, tft.height() / 2 - x / 2, x, x,
                     color);
    }
}

void testfillrects(uint16_t color1, uint16_t color2) {
    tft.fillScreen(ST77XX_BLACK);
    for (int16_t x = tft.width() - 1; x > 6; x -= 6) {
        tft.fillRect(tft.width() / 2 - x / 2, tft.height() / 2 - x / 2, x, x,
                     color1);
        tft.drawRect(tft.width() / 2 - x / 2, tft.height() / 2 - x / 2, x, x,
                     color2);
    }
}

void testfillcircles(uint8_t radius, uint16_t color) {
    for (int16_t x = radius; x < tft.width(); x += radius * 2) {
        for (int16_t y = radius; y < tft.height(); y += radius * 2) {
            tft.fillCircle(x, y, radius, color);
        }
    }
}

void testdrawcircles(uint8_t radius, uint16_t color) {
    for (int16_t x = 0; x < tft.width() + radius; x += radius * 2) {
        for (int16_t y = 0; y < tft.height() + radius; y += radius * 2) {
            tft.drawCircle(x, y, radius, color);
        }
    }
}

void testtriangles() {
    tft.fillScreen(ST77XX_BLACK);
    uint16_t color = 0xF800;
    int t;
    int w = tft.width() / 2;
    int x = tft.height() - 1;
    int y = 0;
    int z = tft.width();
    for (t = 0; t <= 15; t++) {
        tft.drawTriangle(w, y, y, x, z, x, color);
        x -= 4;
        y += 4;
        z -= 4;
        color += 100;
    }
}

void testroundrects() {
    tft.fillScreen(ST77XX_BLACK);
    uint16_t color = 100;
    int i;
    int t;
    for (t = 0; t <= 4; t += 1) {
        int x = 0;
        int y = 0;
        int w = tft.width() - 2;
        int h = tft.height() - 2;
        for (i = 0; i <= 16; i += 1) {
            tft.drawRoundRect(x, y, w, h, 5, color);
            x += 2;
            y += 3;
            w -= 4;
            h -= 6;
            color += 1100;
        }
        color += 100;
    }
}

void tftPrintTest() {
    tft.setTextWrap(false);
    tft.fillScreen(ST77XX_BLACK);
    tft.setCursor(0, 30);
    tft.setTextColor(ST77XX_RED);
    tft.setTextSize(1);
    tft.println("Hello World!");
    tft.setTextColor(ST77XX_YELLOW);
    tft.setTextSize(2);
    tft.println("Hello World!");
    tft.setTextColor(ST77XX_GREEN);
    tft.setTextSize(3);
    tft.println("Hello World!");
    tft.setTextColor(ST77XX_BLUE);
    tft.setTextSize(4);
    tft.print(1234.567);
    delay(1500);
    tft.setCursor(0, 0);
    tft.fillScreen(ST77XX_BLACK);
    tft.setTextColor(ST77XX_WHITE);
    tft.setTextSize(0);
    tft.println("Hello World!");
    tft.setTextSize(1);
    tft.setTextColor(ST77XX_GREEN);
    tft.print(p, 6);
    tft.println(" Want pi?");
    tft.println(" ");
    tft.print(8675309, HEX);  // print 8,675,309 out in HEX!
    tft.println(" Print HEX!");
    tft.println(" ");
    tft.setTextColor(ST77XX_WHITE);
    tft.println("Sketch has been");
    tft.println("running for: ");
    tft.setTextColor(ST77XX_MAGENTA);
    tft.print(millis() / 1000);
    tft.setTextColor(ST77XX_WHITE);
    tft.print(" seconds.");
}

void mediabuttons() {
    // play
    tft.fillScreen(ST77XX_BLACK);
    tft.fillRoundRect(25, 10, 78, 60, 8, ST77XX_WHITE);
    tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_RED);
    delay(500);
    // pause
    tft.fillRoundRect(25, 90, 78, 60, 8, ST77XX_WHITE);
    tft.fillRoundRect(39, 98, 20, 45, 5, ST77XX_GREEN);
    tft.fillRoundRect(69, 98, 20, 45, 5, ST77XX_GREEN);
    delay(500);
    // play color
    tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_BLUE);
    delay(50);
    // pause color
    tft.fillRoundRect(39, 98, 20, 45, 5, ST77XX_RED);
    tft.fillRoundRect(69, 98, 20, 45, 5, ST77XX_RED);
    // play color
    tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_GREEN);
}

私が購入したTFT LCDディスプレイですと、SPI_MODE2 で通信しないと動作しませんでした。SPI_MODE は、データを読み取るタイミングを決める設定で、クロックの立ち上がりや立ち下がりなどを決める要素になります。詳しくは こちら をご覧ください。

数字をカウントアップして表示させる(ソースコード)

実用的に使うであろう数値などのデータだけ表示させるサンプルプログラムを書いてみました。

/**
 * @date 2022-11-21
 * @copyright https://101010.fun
 * @author Toshihiko Arai
 */
#include <Adafruit_GFX.h>     // Core graphics library
#include <Adafruit_ST7789.h>  // Hardware-specific library for ST7789
#include <Arduino.h>
#include <SPI.h>

// TFTの電源電圧は5Vに繋ぐ(3.3Vだと落ちる)
#define TFT_CS 10  // CS端子がないので実際には接続しないし使わない
#define TFT_RST 9
#define TFT_DC 8

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

int number = 0;

void setup() {
    Serial.begin(115200);
    delay(500);
    tft.init(240, 240, SPI_MODE2);  // SPI_MODE2指定しないと動かない
    delay(1000);
    tft.fillScreen(ST77XX_BLACK);
    tft.setRotation(3);  // 画面を回転させる
}

void loop() {
    
    char buff[4];
    sprintf(buff, "%2d", number);  // 数字を右寄せで表示させるため
    tft.setTextWrap(false);

    tft.fillScreen(ST77XX_BLACK);
    tft.setCursor(20, 50);
    tft.setTextColor(ST77XX_WHITE);
    tft.setTextSize(18);
    tft.println(buff);
    delay(500);

    number++;
}

数字が一桁の場合、sprintfを使って右寄せに配置させています。画面をクリアしないと文字がどんどん重なってしまいますので、fillScreen(ST77XX_BLACK)で画面を真っ黒に埋めています。fillScreen の中身は fillRect で、四角形で画面を塗りつぶしているだけです。ところで実際このように数字をカウントアップさせて描画すると、下の映像のようになります。

気になるフリッカー
気になるフリッカー

画面クリアする際のチラつき(フリッカー)が気になります。ArduinoのスペックやTFTモジュール、SPI通信の制限により、これ以上フリッカーを抑えることは難しそうです。。。ちょっと残念ですね。解決策ありましたらぜひ、教えてくださいm(_ _)m

Arduino_ST7789_Fast というライブラリ を見つけまして、描画速度が速そうなので使ってみました。映像だと分かりづらいですが、若干フリッカーが減った感じがしますが。これでも満足のいく結果ではありませんね。。(´・_・`)

Arduino_ST7789_Fast
Arduino_ST7789_Fast

「キッチンノート.fun」という料理サイトを立ち上げました!このサイトで紹介していた料理記事は、そちらへ移動しました。
記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
人気のArduino互換機
Arduinoで人気の周辺パーツ
あると便利な道具
Arduinoのオススメ参考書

▼ Arduino初心者向きの内容となっています。ほかのArduino書籍と比べて図や説明がとてもていねいで、読みやすかったです。Arduinoで一通りのセンサーが扱えるようになります。

▼ 外国人が書いた本を翻訳したものです。この手の書籍は、目からうろこな発見をすることが多いです。

▼ Arduinoの入門書を既に読んでいる方で、次のステップを目指したい人向きの本です。C言語のプログラミングの内容が中心です。ESP32だけでなく、ふつうのArduinoにも役立つ内容でした。

関連記事