はじめに

240×240ドットのフルカラーTFT液晶が数百円で買えるようになりました。今回使ったのはST7789というドライバICを積んだ1.3インチのモジュールで、SPIで手軽につなげるのが売りです。ところが実際に配線してサンプルを流しても、最初はうんともすんとも言いませんでした。安い代わりに、この製品にはハマりどころがいくつか隠れています。

この記事では、まずその「動かない」の正体を先に片付けてから、配線・ライブラリ・サンプルコードへと進みます。後半では、数字をカウントアップ表示させたときに出るフリッカー(画面のチラつき)をどう抑えたか、というところまで扱います。

つまずくのはこの3か所

先に結論を書いておきます。私が買ったST7789モジュールが動かなかった原因は、次の3点に集約されました。ここさえ押さえれば、あとは素直に表示できます。

一つ目は、このモジュールにCS端子が無いことです。SPIというのは、1本のクロックとデータ線を複数の機器で共有し、CS(チップセレクト、どの相手と話すかを選ぶ信号)で通信相手を1台だけ指名する仕組みです。ところがCSが無いと相手を選び分けられないので、このディスプレイはSPIバスを独り占めする前提でしか使えません。試しにSDカードモジュールを同じSPIに相乗りさせたら、案の定ディスプレイは表示しなくなりました。ほかのSPI機器と一緒に使いたいなら、CS端子のある製品(ST7735 系など)を選んだほうが無難です。ST7735チップでもST7789と似た感覚でプログラミングできます。

二つ目は電源で、VCCは5Vに入れる必要がありました。3.3Vを与えると起動時に電圧が足りず、画面が一瞬ついてはすぐ落ちてしまいます。ただし紛らわしいのですが、電源が5Vでも信号線は3.3Vロジックです。Arduino Unoの出力は5Vなので、データ線を直結するのは本来おすすめできません(この点は配線のところで補足します)。

三つ目がSPI_MODE2です。SPIにはクロックの極性と位相の組み合わせでMODE0〜3の4種類があり、これが送受信側で食い違うと1ドットも表示されません。私の個体はMODE2でようやく通りました。初期化のところで1行指定するだけなので、表示されないときはまずここを疑うと早いです。

用意するもの

ディスプレイのスペックは次のとおりです。IPSパネルなので視野角が広く、発色も素直でした。

項目
サイズ1.3インチ
表示モードノーマリーブラックIPS
インタフェースSPI
ドライバICST7789VW
外形寸法27.78(W)× 39.22(H)× 3.0 ±0.1(T)mm
解像度240×240ドット(RGB各8bit)
表示エリア23.4(W)× 23.4(H)mm
使用温度-20〜70℃
重量6.1g

同じ1.3インチのST7789モジュールは、ST7789TFTディスプレイ で検索すると見つかります。本体側はArduino Unoを使いました。

Arduino Unoとの配線

配線は次の写真のとおりです。バックライト制御のBLK(BKL)ピンは今回使いませんでした。CS端子は基板に出ていないので、当然どこにもつなぎません。

Arduino UnoとST7789 TFT LCDの配線

ピン割り当ては、Arduino UnoとESP32のそれぞれで次のようにしました。SDA・SCLはSPIのデータとクロックで、DCはデータとコマンドを切り替える線です。

ディスプレイArduino UnoESP32役割
BLK未接続未接続バックライト制御
DC82データ/コマンド切替
RES94リセット
SDA1123SPIデータ入力
SCL1318SPIクロック
VCC5V5V電源
GNDGNDGNDGND

先ほど触れたとおり、電源は5Vですが信号は3.3Vロジックです。Arduino Unoの5V出力を各データ線に直結すると、ディスプレイ側の定格を超えて故障の原因になり得ます。とりあえず動作を試すだけなら直結でも表示はできてしまいますが、常用するならロジックレベル変換モジュールや分圧抵抗で3.3Vに落として接続するのが安全です。ESP32はもともと3.3Vロジックなので、この心配はありません。

ライブラリと初期化

描画にはAdafruitのグラフィックスライブラリを使います。最低限、次のリポジトリのソースが必要です。

私はVS CodeのPlatformIOで、ライブラリマネージャーを使わずソースを直接 lib/ に置いて開発しています。ライブラリさえ通ってしまえば、初期化で大事なのは次の数行だけです。CS端子が無いので、コンストラクタに渡すCSピン番号は形だけの指定で、実際には配線しません。そして先ほどの SPI_MODE2init() で指定します。

#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>

#define TFT_CS 10  // CS端子は無いので、この番号は使われない(形式上の指定)
#define TFT_RST 9
#define TFT_DC 8

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

void setup() {
    tft.init(240, 240, SPI_MODE2);  // ← MODE2を指定しないと何も表示されない
    tft.fillScreen(ST77XX_BLACK);
    tft.setCursor(0, 30);
    tft.setTextColor(ST77XX_WHITE);
    tft.setTextSize(2);
    tft.println("Hello World!");
}

void loop() {}

これで “Hello World!” が出れば、3つの落とし穴はすべてクリアできています。線や円、四角の描画をひととおり試したいなら、有志が公開しているグラフィックステスト(jumejume1/tft240x240-spi 、サードパーティ製のサンプル)をそのまま書き込むのが手っ取り早いです。実際に流すと、次の動画のように図形や文字が次々に描かれていきます。

なお SPI_MODE は、クロックの立ち上がり/立ち下がりのどちらでデータを読むかを決める設定です。仕組みを詳しく知りたい方はこちらの解説 が分かりやすいです。

数字を表示する — そしてフリッカーとの戦い

このディスプレイでやりたかったのは、センサーの値などをリアルタイムに大きく表示することでした。そこで、数字をカウントアップして表示するだけの短いコードを書きました。

#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>

#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8

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

int number = 0;

void setup() {
    tft.init(240, 240, SPI_MODE2);
    tft.fillScreen(ST77XX_BLACK);
    tft.setRotation(3);        // 横向きに回転
    tft.setTextWrap(false);    // 桁が増えても折り返さない(大きな文字では必須)
}

void loop() {
    char buff[16];
    sprintf(buff, "%2d", number);  // 桁を右寄せで揃える

    tft.fillScreen(ST77XX_BLACK);   // ← 毎フレーム画面全体を黒で消してから描く
    tft.setCursor(20, 50);
    tft.setTextColor(ST77XX_WHITE);
    tft.setTextSize(18);
    tft.println(buff);
    delay(500);

    number++;
}

前の数字が残ると重なって読めなくなるので、毎回 fillScreen で画面を真っ黒に塗ってから描き直しています。動くには動くのですが、実際に見ると数字が切り替わる瞬間にチラつきが出ます。

数字が切り替わる瞬間に出るフリッカー

原因はこの fillScreen そのものです。全体を黒で塗る→数字を描く、という順番なので、そのあいだにほんの一瞬だけ画面が真っ黒なフレームが挟まります。これが目にはチラつきとして見えます。描画範囲が広いほど塗りつぶしに時間がかかるので、SPIの転送速度が上限のArduino Unoでは、この黒フレームを消しきるのが難しいというわけです。

最初に試したのは、描画そのものを速くする作戦でした。Arduino_ST7789_Fast という高速描画ライブラリに差し替えてみたところ、下のように多少はなめらかになります。ただ、速く描けても全消しをやめたわけではないので、黒フレーム自体は残り、チラつきは完全には消えませんでした。

高速描画ライブラリに変えても黒フレームは残る

そこで発想を変えます。チラつきを消す鍵は、画面全体を消さないことです。変わったのは数字だけなのに、毎回240×240ドット全部を塗り直しているのが無駄で、そこに黒フレームが生まれています。数字が入る領域だけ、しかも値が変わったときだけ塗り替えれば、真っ黒になる瞬間そのものを減らせます。

全消しだと数字の合間に黒フレームが挟まってチラつく。変わった数字だけ描けば黒フレームは生まれない

考え方を最小限のコードにすると、こうなります。fillScreen の代わりに数字が入る矩形だけを fillRect で消し、さらに値が変わったフレームだけ描き直します。

int number = 0;
int prev = -1;

void loop() {
    if (number != prev) {  // 値が変わったときだけ描く
        char buff[16];
        sprintf(buff, "%2d", number);

        tft.fillRect(20, 50, 200, 130, ST77XX_BLACK);  // 数字の領域だけ消す
        tft.setCursor(20, 50);
        tft.setTextColor(ST77XX_WHITE);
        tft.setTextSize(18);
        tft.println(buff);

        prev = number;
    }
    number++;
    delay(500);
}

これだけでもチラつきはかなり軽くなります。黒フレームを完全に無くすには、桁を7セグメントに分解して、変化したセグメントだけを塗り替えるところまで踏み込みます。ArduinoフォーラムのFast7Segment (サードパーティのコード)がまさにそれで、fillScreen を使わず drawFastHLine / drawFastVLine で必要な線だけを描き換えています。これを小数点とマイナス表示に対応させて動かした結果が次の動画です。冒頭のサンプル動画とは別物で、こちらがフリッカーを抑えた最終形になります。

手元のST7789でも、まずはこの最小コードから試してみてください。

関連記事

ディスプレイに出すネタや、Arduinoまわりをもっと触りたい方はこちらもどうぞ。センサーで測った値をこのST7789に表示する、といった組み合わせが定番です。