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

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

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

ゴール

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

SPI接続なのにCS端子がないTFTを買ってしまって...ようやく動かせました。5V供給しないと落ちちゃったり、SPI_MODE2じゃないとダメだったり、苦労しました😅

つかうもの

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

TFT LCDディスプレイ(ST7789)

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

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

項目
サイズ1.3インチ
表示モードノーマリーブラックIPS
入力データSPIインタフェース
ドライブICST7789VW
外形寸法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の配線

TFT LCDディスプレイとArduino Unoは次のとおり配線しました。BKLピンは使いません。

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

ただしST7789は3.3Vの信号電圧であるため、出力信号が5VピークのArduino Unoに直結して繋ぐのは故障の原因にもなるので推奨されません。 真似する場合は自己責任で行なってください。安全を考えるのであれば、ロジックレベル変換モジュールや分圧抵抗で信号電圧を3V3へ変換して接続することをおすすめします。

TFT LCDディスプレイArduinoESP32説明
BLK未接続未接続バックライト制御
DC82データ/コマンド制御
RES94リセット信号入力
SDA1123シリアルデータ入力
SCL1318シリアルクロック
VCC5V5V電源
GNDGNDGNDGround

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

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

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

project
.
├── lib
│   ├── README
│   └── TFT_ST7789
│       ├── Adafruit_GFX.cpp
│       ├── Adafruit_GFX.h
│       ├── Adafruit_I2CDevice.cpp
│       ├── Adafruit_I2CDevice.h
│       ├── Adafruit_SPIDevice.cpp
│       ├── Adafruit_SPIDevice.h
│       ├── Adafruit_SPITFT.cpp
│       ├── Adafruit_SPITFT.h
│       ├── Adafruit_SPITFT_Macros.h
│       ├── Adafruit_ST7789.cpp
│       ├── Adafruit_ST7789.h
│       ├── Adafruit_ST77xx.cpp
│       ├── Adafruit_ST77xx.h
│       ├── examples
│       │   └── demo
│       │       └── demo.ino
│       ├── gfxfont.h
│       └── glcdfont.c
├── platformio.ini
├── src
│   └── main.cpp

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

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

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

下記URLのサンプルコードをそのまま動かしてみました。 tft240x240-spi/tft.ino at master · jumejume1/tft240x240-spi · GitHub

display_test.ino
/**
 * 下記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 は、データを読み取るタイミングを決める設定で、クロックの立ち上がりや立ち下がりなどを決める要素になります。詳しくは こちら をご覧ください。

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

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

display_number.ino
/**
 * @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通信の制限により、これ以上フリッカーを抑えることは難しそうです。ちょっと残念です。

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

Arduino_ST7789_Fast
Arduino_ST7789_Fast

高速描画へ改善(フリッカーノイズ除去)

ここまでで、fillScreenで画面全体を更新させてしまうとどうしてもフリッカーノイズが発生してしまいます。

そこでスムーズに描画する方法はないかとさらに調べたところ、 ArduinoフォーラムにFast7Segment というソースコードが見つかりました。ソースを覗くと、fillScreen で画面更新するのではなく、文字の領域部分を drawFastHLinedrawFastVLine といった関数で更新されています。

ソースコードを少し改造してマイナスと小数点表現に対応し高速描画の実現に成功しました。動画はその実験の様子です。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

人気のArduino互換機
Arduinoで人気の周辺パーツ
あると便利な道具
Arduinoのオススメ参考書

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

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

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

関連記事