小嶋秀樹 | 研究室
日本語 | English
ESP にキャラクタ液晶ディスプレイを i2c 接続する

英数字やカタカナなどの文字を表示できる液晶ディスプレイを ESP-WROOM-02(トンペイーノ)に接続します.下図は i2c 接続の液晶モジュールの例(秋月電子 AE-AQM1602A)で,16文字 × 2行の文字表示ができます.型番から推察できますね.

【ESP から i2c で接続する】

接続方法は i2c です.電源のほかは,クロック(SCL)と信号線(SDA)のみの結線となります.SCL と SDA には 1k〜10kΩ 程度のプルアップ抵抗が必要です.ここでは 10kΩ にしてあります.(複数の i2c デバイスを SCL/SDA 上に並列接続する場合は,プルアップ抵抗は1組だけにしてください.)

あくまで参考例ですが,トンペイーノとの接続は下図のようになります.もともと IO0 に繋がれた黄色いジャンパ線(run/prog 切替)は,ブレッドボード上側の "+" または "−" ラインに接続しています.

【Wire ライブラリによる液晶モジュールとの通信】

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 となります.

ESP にカラー TFT 液晶ディスプレイを SPI 接続する

文字だけでなくグラフィック表示もできると楽しいですね.ここではカラー TFT 液晶ディスプレイを Dongbeino に接続し,グラフィック表示を試しましょう.使用するディスプレイは,aitendo から調達できる 1.44inch のもの(M-Z144SPI-2P)で,解像度は 128x128xRGB となっています.モジュール(Z144SN005)と基板のセットで 750円でした.

まずは,モジュールを基板にハンダづけしてください.かなり細かい作業になると思います.また,ブレッドボードに差すための8本のピンヘッダを裏向きにハンダづけしてください.ここでは 100Ω の抵抗器を取り付け,3.3V で十分なバックライト光量が得られるようにします.

【Dongbeino との SPI 接続】

Dongbeino との接続(通信)には,SPI (serial peripheral interface) という方式をとります.下図のように配線してください.ここで利用する SPI 通信は,ESP-WROOM-02 から Z144SN005 への単方向のバイト列伝送です.双方向の通信(バイト列の交換)については「ESP センシング編」の後半を参照してください.

実際の配線については,下図を参考に進めてください.

【SPI 接続による描画指令の伝送】

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 から開いてください.かなり高速に描画されるのがわかると思います.各描画関数の簡単な説明を以下にあげます.

描画アルゴリズムに興味のある人は,ライブラリのソースコード(esp_ST7735.hesp_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();
}