ArduinoでTFT LCDディスプレイ(ST7789)を使ってみる
ArduinoでTFT LCDディスプレイ(ST7789)を使ってみました。ご紹介するTFT LCDディスプレイはSPI通信ですが、CS(チップセレクト)のない製品でした。そのため複数のSPIデバイスを繋ぐことができません。単体で動かす場合でしたら問題なく表示できました。RGBフルカラーにも対応してます。この記事では、Arduino UnoでTFT LCDディスプレイに文字や図形を表示する方法や、注意点などまとめました。
ゴール
下記動画のように、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の配線
TFT LCDディスプレイとArduino Unoは次のとおり配線しました。BKLピンは使いません。
ただしST7789は3.3Vの信号電圧であるため、出力信号が5VピークのArduino Unoに直結して繋ぐのは故障の原因にもなるので推奨されません。 真似する場合は自己責任で行なってください。安全を考えるのであれば、ロジックレベル変換モジュールや分圧抵抗で信号電圧を3V3へ変換して接続することをおすすめします。
TFT LCDディスプレイ | Arduino | ESP32 | 説明 |
---|---|---|---|
BLK | 未接続 | 未接続 | バックライト制御 |
DC | 8 | 2 | データ/コマンド制御 |
RES | 9 | 4 | リセット信号入力 |
SDA | 11 | 23 | シリアルデータ入力 |
SCL | 13 | 18 | シリアルクロック |
VCC | 5V | 5V | 電源 |
GND | GND | GND | Ground |
ライブラリのインストール
ST7789搭載のTFT LCDディスプレイを動かすには、最低限 Adafruit_GFX Adafruit_SPITFT Adafruit_ST77xx Adafruit_ST7789 gfxfont Adafruit_I2CDevice Adafruit_SPIDevice が必要になります。下記GitHubリポジトリからインストールするなりしてください。
- GitHub - adafruit/Adafruit-GFX-Library: Adafruit GFX graphics core Arduino library, this is the 'core' class that all our other graphics libraries derive from
- GitHub - adafruit/Adafruit-ST7735-Library: This is a library for the Adafruit 1.8" SPI display http://www.adafruit.com/products/358 and http://www.adafruit.com/products/618
- GitHub - adafruit/Adafruit_BusIO: Arduino library for I2C & SPI abstractions
私は最近、VS CodeにPlatformIOを入れてArduino開発してます。そのため下記のような構成でライブラリを配置してます。基本ライブラリマネージャーは使っておらず、ソースコードをそのままダウンロードして配置させてます。ここでは display_test.ino にサンプルプログラムを記述しました。
.
├── 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 を次のように記述することで可能です。
サンプルのスケッチ(ソースコード)
下記URLのサンプルコードをそのまま動かしてみました。 tft240x240-spi/tft.ino at master · jumejume1/tft240x240-spi · GitHub
/**
* 下記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通信の制限により、これ以上フリッカーを抑えることは難しそうです。ちょっと残念です。
Arduino_ST7789_Fast というライブラリ を見つけまして、描画速度が速そうなので使ってみました。映像だと分かりづらいですが、若干フリッカーが減った感じがしますが。これでも満足のいく結果ではありませんね(´・_・`)
高速描画へ改善(フリッカーノイズ除去)
ここまでで、fillScreenで画面全体を更新させてしまうとどうしてもフリッカーノイズが発生してしまいます。
そこでスムーズに描画する方法はないかとさらに調べたところ、 ArduinoフォーラムにFast7Segment というソースコードが見つかりました。ソースを覗くと、fillScreen で画面更新するのではなく、文字の領域部分を drawFastHLine や drawFastVLine といった関数で更新されています。
ソースコードを少し改造してマイナスと小数点表現に対応し高速描画の実現に成功しました。動画はその実験の様子です。