英数字やカタカナなどの文字を表示できる液晶ディスプレイを ESP-WROOM-02(トンペイーノ)に接続します.下図は i2c 接続の液晶モジュールの例(秋月電子 AE-AQM1602A)で,16文字 × 2行の文字表示ができます.型番から推察できますね.
接続方法は i2c です.電源のほかは,クロック(SCL)と信号線(SDA)のみの結線となります.SCL と SDA には 1k〜10kΩ 程度のプルアップ抵抗が必要です.ここでは 10kΩ にしてあります.(複数の i2c デバイスを SCL/SDA 上に並列接続する場合は,プルアップ抵抗は1組だけにしてください.)
あくまで参考例ですが,トンペイーノとの接続は下図のようになります.もともと IO0 に繋がれた黄色いジャンパ線(run/prog 切替)は,ブレッドボード上側の "+" または "−" ラインに接続しています.
i2c 接続されたデバイスとの通信には,Arduino 標準の Wire ライブラリを使います.ESP-WROOM-02 では,IO5 がクロック(SCL)の出力に,IO4 がデータ(SDA)の送受信に割り当てられ,つねに ESP-WROOM-02 が i2c 通信のマスターとなり,SCL/SDA に接続されたスレーブ(個々の i2c デバイス)と通信することができます.複数のスレーブが SCL/SDA に「ぶら下がる」ことができますが,スレーブは個別のアドレスを持つためマスターから区別することができます.
Wire ライブラリの使い方は,つぎのプログラム(意味のある動作はしません)を参考にしてください.アドレス 0x3e のスレーブに対して {0x00, 0x01} という2バイトのデータを1秒ごとに送信するというものです.
#include <Wire.h> #define LCDaddr 0x3e void setup() { Wire.begin(); } void loop() { Wire.beginTransmission(LCDaddr); Wire.write(0x00); Wire.write(0x01); Wire.endTransmission(); delay(1000); }
液晶モジュールへの通信は,(a) 指令 (instruction) と (b) データ (data) のいずれかの形式をとります.今回使用する液晶モジュールのアドレスは 0x3e です.
指令: 1バイトの指令本体に,それが指令であることを明示するためにもう1バイト(定数 0x00)を先行させます.たとえば,{0x00, 0x01} であれば,最初の 0x00 はこれが指令であることを表し,つぎの 0x01 が「画面クリア」の指令本体となります.指令本体については,液晶ディスプレイの説明書を参照してください.指令を送信する関数 LCDsendInst() は,つぎのように定義できます.
void LCDsendInst(byte inst) { // instruction write = {LCDaddr, 0x00, inst} Wire.beginTransmission(LCDaddr); Wire.write(0x00); Wire.write(inst); Wire.endTransmission(); // delay (1.08ms max) delay(2); }
データ: 1バイトのデータ本体に,それがデータであることを明示するためにもう1バイト(定数 0x40)を先行させます.たとえば,{0x40, 0x41} であれば,最初の 0x40 はこれがデータであることを表し,つぎの 0x41 がデータ本体(文字 "A")となります.通常,データ本体は,カーソル位置に表示すべき文字データとなります.1バイトのデータを送信する関数 LCDsendData() は,つぎのように定義できます.
void LCDsendData(byte data) { // data write = {LCDaddr, 0x40, data} Wire.beginTransmission(LCDaddr); Wire.write(0x40); Wire.write(data); Wire.endTransmission(); // delay (may be omitted) delay(1); }
液晶モジュールは,電源投入後,適切な手順で初期化する必要があります.詳しい説明は省きますが,およそつぎの手順で初期化すればよいでしょう.これを setup() から呼び出すことになります.
void LCDinit() { // wait for VDD gets stable delay(40); // Function set 0x39=B00111001 (extension-mode) LCDsendInst(0x39); // Internal OSC 0x14=B00010100 (BS=0, FR=4) LCDsendInst(0x14); // Contrast set 0x72=B01110010 (level=2) LCDsendInst(0x72); // Power etc 0x56=B01010110 (Boost, const=B10) LCDsendInst(0x56); // Follower 0x6c=B01101100 (on, amp=B100) LCDsendInst(0x6c); // Function set 0x38=B00111001 (normal-mode) LCDsendInst(0x38); // clear display LCDsendInst(0x01); // display on 0x0c=B00001100 (On, no-cursor, no-blink) LCDsendInst(0x0c); }
この初期化関数 LCDinit() や上述の LCDsendInst(), LCDsendData() を組み合わせれば,液晶モジュールに文字を表示させるプログラムが簡単につくれます.つぎの例を参考にしてください.
// LCD モジュール #include <Wire.h> #define LCDaddr 0x3e // = 0x7C >> 1 void LCDsendInst(byte inst) { ... 変更なし ... } void LCDsendData(byte data) { ... 変更なし ... } void LCDinit() { ... 変更なし ... } // 描画関数 void LCDclear() { // 画面クリア LCDsendInst(0x01); } void LCDlocate(int x, int y) { // カーソルを x 行 y 字目に移動 if (y == 0) { LCDsendInst(0x80 + x); } else { LCDsendInst(0xc0 + x); } } void LCDprint(char *str) { // カーソル位置に文字列を表示(表示後にカーソルは移動) for (int i = 0; i < strlen(str); i++) { LCDsendData(str[i]); } } void LCDprintXY(int x, int y, char *str) { // カーソルを (x, y) に移動して文字列を表示(表示後にカーソルは移動) LCDlocate(x, y); LCDprint(str); } // メインプログラム char kanaMessage[] = { // "コシ゛ケン ト゛ウシ゛ョウ"(末尾のNULL文字 0x00 を忘れずに) 0xba, 0xbc, 0xf2, 0xb9, 0xdd, 0x20, 0xc4, 0xf2, 0xb3, 0xbc, 0xf2, 0xae, 0xb3, 0x00 }; void setup() { // init LCD Wire.begin(); LCDinit(); } void loop() { LCDclear(); LCDprintXY(0, 0, "Hello, world!"); LCDprintXY(3, 1, kanaMessage); delay(2000); LCDclear(); LCDprintXY(0, 0, kanaMessage); LCDprintXY(3, 1, "Hideki Kozima"); delay(2000); }
英数字については,そのまま文字配列を LCDprint(), LCDprintXY() に与えれば表示されます.(String 型のオブジェクト STR を与えたい場合は STR.c_str() のように,文字配列を取り出してから使ってください.)
カナ文字については,文字コード表(液晶モジュールの説明書にあります)を参照して,文字コードに手動で変換してから,文字配列をつくってください.たとえば「ア」は,B10110001 となり,16進数なら 0xb1,10進数なら 177 となります.
文字だけでなくグラフィック表示もできると楽しいですね.ここではカラー TFT 液晶ディスプレイを Dongbeino に接続し,グラフィック表示を試しましょう.使用するディスプレイは,aitendo から調達できる 1.44inch のもの(M-Z144SPI-2P)で,解像度は 128x128xRGB となっています.モジュール(Z144SN005)と基板のセットで 750円でした.
まずは,モジュールを基板にハンダづけしてください.かなり細かい作業になると思います.また,ブレッドボードに差すための8本のピンヘッダを裏向きにハンダづけしてください.ここでは 100Ω の抵抗器を取り付け,3.3V で十分なバックライト光量が得られるようにします.
Dongbeino との接続(通信)には,SPI (serial peripheral interface) という方式をとります.下図のように配線してください.ここで利用する SPI 通信は,ESP-WROOM-02 から Z144SN005 への単方向のバイト列伝送です.双方向の通信(バイト列の交換)については「ESP センシング編」の後半を参照してください.
実際の配線については,下図を参考に進めてください.
SPI はマスター(ここでは ESP-WROOM-02)とスレーブ(ここでは Z144SN005)の間をつなぐ4線(CS, CLK, MOSI, MISO)で双方向の通信をするものです.スレーブが複数となる場合もあり,共通の SCLK, MOSI, MISO に「ぶら下がる」ように接続します.ESP-WROOM-02 では,SCLK は IO14,MOSI は IO13,MISO は IO12 のピンに配置されています.CS(Chip Select;または CE: Chip Enable, SS: Slave Select)は,マスターから各スレーブに1本ずつ接続します.CS には余っているディジタル出力端子を使えばよいでしょう.CS はアクティブ LOW です.
マスターは,CS を LOW にしたスレーブにバイト列を送り出し,それと同時に同じ長さのバイト列をそのスレーブから受け取ります.つまり,同じ長さのバイト列をマスターとスレーブが同時交換するわけです.交換されるデータの長さ(ビット数)は CS が LOW の間のクロック数と同じになります.ひとかたまりのバイト列(パケットなど)の送受信が終われば,CS を HIGH に戻します.
ここで使用するカラー TFT 液晶ディスプレイ(Z144SN005)は,マスターからの MOSI 接続のみによる単方向通信で動作します.したがって,MISO は結線不要です.グラフィック描画のために伝送される情報(描画指令)は,およそ次のとおりです.
《コマンド》《データ》《データ》...《データ》
各要素(《...》)は1バイト(8bit)のデータです.先頭は《コマンド》で,後続する数バイトが《データ》です.《データ》を伴わない《コマンド》もあります.《コマンド》と《データ》を区別するために,IO16 から A0 への接続を使います.IO16 が LOW のときに SPI 伝送されたデータは《コマンド》,HIGH のときは《データ》として取り込まれます.この信号線は DC (Data/Command) あるいは RS (Register Select) と呼ばれることが多いようです.
つぎにあげる部分プログラム(これだけでは動作しません)は,描画指令の伝送に関する部分です.SPI を使うには SPI ライブラリを組み込みます.Sketch > Include Library > SPI を選択してください.スケッチ冒頭に "#include <SPI.h>" が挿入されます.プログラム本体では,最初に SPIinit() を一度呼び出してから,描画指令を SPIsendCommN() で伝送します.SPIsendDataN() はバイト列(ピクセル列など)をデータとして一括伝送する関数です.
#include <SPI.h> #define CS 15 // as a master #define DC 16 // DC -> A0 (H:data, L:command) #define MOSI 13 // MISO is not used #define SCK 14 // SPI 初期化 (SS=10, MOSI=11, (MISO=12), SCK=13) void SPIinit() { pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); pinMode(DC, OUTPUT); digitalWrite(DC, LOW); SPI.begin(); SPI.setBitOrder(MSBFIRST); SPI.setDataMode(SPI_MODE0); SPI.setFrequency(15000000ULL); // クロック 15MHz } // SPI から描画指令(cmd[0]がコマンド;それ以降はデータ)を送信 void SPIsendCommN(unsigned char *cmd, int length) { digitalWrite(CS, LOW); digitalWrite(DC, LOW); // command SPI.write(cmd[0]); // parameters (data) digitalWrite(DC, HIGH); for (int i = 1; i < length; i++) { SPI.write(cmd[i]); } digitalWrite(CS, HIGH); } // SPI からデータ送信(複数バイト) void SPIsendDataN(unsigned char *data, int length) { { digitalWrite(CS, LOW); digitalWrite(DC, HIGH); SPI.writeBytes(data, length); digitalWrite(CS, HIGH); }
TFT 液晶ディスプレイを使用する上で,複雑なのは初期化,つまり電源投入(あるいはリセット)時に適切な初期設定を行うことが必要な点で,これはディスプレイに内臓されたコントローラ IC(たとえば ST7735S)ごとに方法が異なります.ここでは,初期化の方法についての説明は割愛します.詳しくは,ライブラリ内のファイル esp_ST7735.cpp をご覧ください.
一方,描画そのものの方法はいたってシンプルです.画面上の矩形領域(左上端の位置 x, y と大きさ w, h)を指定し,その領域内のピクセル(w × h 個)の RGB データを送信するだけです.ライブラリ(esp_ST7735.cpp)の内部では,およそ次のようなコードで実装されています.(このコードはそのままでは実行できません.)
// 塗りつぶしピクセルデータの生成 unsigned int pixel[w * h]; for (int k = 0; k < w * h; k++) { pixel[k] = 0xffff; // 白 } // (x, y) を左上端とする大きさ (w, h) の矩形領域を設定する LCDsetWindow(x, y, x+w-1, y+h-1); // その矩形領域にピクセルデータを流し込む LCDsendDataN(pixel, w * h);
直線を描画する場合も,水平線であれば h = 1 となる領域を埋めるようにします.点を描画する場合は,w = 1, h = 1 の「領域」を埋めることになります.斜めの直線については,太さ0の理想的な直線が貫通するピクセルを求め,それらを点(または水平線・垂直線)で描画していきます.
ピクセルの状態(RGB ごとの光量;つまり色)は 16bit の整数で表現されます.B:G:R の順番に 5bit:6bit:5bit となっています.たとえば,RGB の各要素を 8bit とした {r8, g8, b8} の形式で表現された色については,この 16bit 表現は (b & 0xf8) << 8) | ((g & 0xfc) << 3) | ((r & 0xf8) >> 3 として求めることができます.0xffff ならば白,0x0000 ならば黒です.赤は 0x001f で,青は 0xf800 です.
小嶋先生お手製のライブラリ esp_ST7735.zip をダウンロードして,Arduino IDE に登録してください.ライブラリへの登録は,ZIP ファイルのまま可能です.Sketch > Include Library > Add .ZIP Library... でダウンロードしたファイルを指定します.
まずはテストプログラムを動かしてみましょう.新規スケッチを開き,Sketch > Include Library から "esp_ST7735" を選択します.スケッチ冒頭に "#include ..." が2行挿入されるはずです.つぎのプログラムを打ち込み,動かしてみてください.白地の中央に赤い四角,赤字の中央に白い四角が,1秒ごとに交互に描画されるはずです.
#include <esp_ST7735.h> #include <font6x8.h> void setup() { // 初期化(最初に1回だけ呼び出す) LCDinit(); } void loop() { // 白でクリア LCDclear(0xffff); // 中央に赤い四角 LCDrect(32, 32, 64, 64, 0x001f, 1); delay(1000); // 赤でクリア LCDclear(0x001f); // 中央に白い四角 LCDrect(32, 32, 64, 64, 0xffff, 1); delay(1000); }
このテストプログラムが動いたら,つぎに esp_ST7735_ex1.zip を動かしてみてください.ZIP ファイルをデスクトップ等で解凍し,フォルダ内部にある esp_ST7735_ex1.ino を Arduino IDE から開いてください.かなり高速に描画されるのがわかると思います.各描画関数の簡単な説明を以下にあげます.
- void LCDinit()
TFT 液晶ディスプレイを初期化する.最初に1回だけ実行すること. - int LCDcolor(int r, int g, int b)
RGB 24bit の色データを BGR 16bit(B:G:R = 5:6:5)に変換する.たとえば「赤」を得るには int bgr16 = LCDcolor(255, 0, 0) とする.以下にあげる描画関数の color にはこの BGR 16bit の色データを与えること.たとえば LCDpoint(64, 64, LCDcolor(255, 192, 64)) のように使う. - void LCDclear(int color)
色 color で全画面を埋める.画面クリアとして使う.
- void LCDpoint(int x, int y, int color)
位置 x, y に色 color の点を描画する. - void LCDline(int x1, int y1, int x2, int y2, int color)
位置 x1, y1 から x2, y2 への直線を色 color で描画する. - void LCDrect(int x, int y, int w, int h, int color, int fill)
位置 x, y を左上端する大きさ w × h の長方形を,色 color で描画する.fill が 0 ならば輪郭(太さ 1 ピクセル)のみを描画し,fill が 0 以外であれば塗りつぶす. - void LCDcircle(int x, int y, int r, int color, int fill)
位置 x, y を中心する半径 r の円を,色 color で描画する.fill が 0 ならば輪郭(太さ 1 ピクセル)のみを描画し,fill が 0 以外であれば塗りつぶす. - void LCDellipse(int x, int y, int a, int b, int color, int fill)
位置 x, y を中心し,横方向の半径を a,縦方向の半径を b とする楕円を,色 color で描画する.fill が 0 ならば輪郭(太さ 1 ピクセル)のみを描画し,fill が 0 以外であれば塗りつぶす. - void LCDtext(int x, int y, char *string, int color)
文字列 string を,位置 x, y が左上端となるように,色 color で描画する.文字は 6x8 ドットの等幅フォントで,使える文字は ASCII 文字(文字コード 0x20〜0x7E)のみ. - void LCDtext(int x, int y, char *string, int colorF, int colorB)
文字列 string を,位置 x, y が左上端となるように,文字色 colorF・背景色 colorB で描画する.文字は 6x8 ドット(文字本体は 5x7 ドット)の等幅フォントで,使える文字は ASCII 文字(文字コード 0x20〜0x7E)のみ. - void LCDimage(int x, int y, int w, int h, int *image16)
位置 x, y を左上端する大きさ w × h の矩形領域にピクセルデータ image16 を描画する.image16 は w × h 要素の整数配列として与える.要素形式については LCDcolor() を参照のこと. - void LCDimage(int x, int y, int w, int h, unsigned char *image24)
位置 x, y を左上端する大きさ w × h の矩形領域にピクセルデータ image24 を描画する.image24 は w × h × 3 要素の unsigned char からなる配列(つまり {r, g, b, r, g, b, ...} という形式)としてあたえる.
描画アルゴリズムに興味のある人は,ライブラリのソースコード(esp_ST7735.h と esp_ST7735.cpp)を参照してください.
画面が暗いようでしたら,VLED を +5V(USB シリアル変換器の USB 端子 = 3端子レギュレータの IN)に接続してください.少し明るくなるはずです.
トンペイーノのリセットボタンを押せば,ディスプレイもリセットがかかり,正しく初期化が始まります.一方,IO0 を "run"("+" 側)に接続した状態で,電源供給を開始(USB ケーブルをつなぐ,電池をつなぐなど)した場合,ディスプレイが先にリセットされてしまい,うまく初期化が始まらない場合があります.その場合は,ESP-WROOM-02 とディスプレイの RST どうしの接続を,ESP-WROOM-02 の空き端子(ここでは IO2 を想定)からディスプレイの RST への接続に変更した上で,つぎのコードを参考に,LCDinit() を実行する直前に LOW パルスをディスプレイの RST に送るようにしてください.
void setup() {
// LCD リセット
pinMode(2, OUTPUT);
digitalWrite(2, LOW);
delay(100);
digitalWrite(2, HIGH);
delay(100);
// LCD 初期化
LCDinit();
}