タッチパネル付TFT液晶モジュールを使ってみる

概要

以前にamazonで安く買った2.4インチのTFT液晶モジュール(タッチパネル付)をArduino PRO MINIと組み合わせて使ってみた。前からたまに作っている赤外線リモコンの操作部を、メカニカルスイッチではなくてGUI (!!) にできないものか、と考えたのがきっかけだった。液晶の上に対象に応じた操作ボタンを表示できるようにすれば、1台で複数の家電製品に対応できるわけである。

ESP-WROOM-02などのWiFi機能のあるマイコンに赤外線送出機能を作り込み、スマホの画面に操作パネルを表示してhttp経由でコマンドを送るやり方は以前にやった。残念ながら、WROOM-02側はWiFiアクセスポイントとして常時待ち受ける都合上、ACアダプタを使ったが、できれば電池で動かしたいしそれなりに電池を長持ちさせたい。TFT液晶は電力消費が多いことは分かっているので、そのあたりがポイントになるだろうなとか思いながら作り始めた。

今回は、試作した回路、タッチパネル用のライブラリおよびテスト用スケッチについてで、リモコンにはなっていない。

構成

マイコンにはArduino PRO MINIの3.3V版(The Simple, 8MHz)を使うことにした。以前の赤外線リモコンと同様に、省電力のために基板上のPON LED(の直列抵抗)と+3.3V出力の定電圧レギュレータは除去して使うことにした。

Arduino PRO MINI
Arduino PRO MINI

レギュレータと抵抗を外したところ。数mAの節約にすぎないが電池駆動のために余計なものは外した。

タッチパネル付TFT液晶モジュール

タッチパネル付のTFT液晶モジュールとして、SPI接続タッチパネル付2.4インチTFT液晶モジュールの TJCTM24024-SPI という製品を使うことにした(以下、TFTモジュールと記述する)。amazonで購入したときは700円程度だったと思う。いま検索してみたら、商品自体が消えていた。Aliexpressでは売っているようだが。

TJCTM24024
TJCTM24024
TJCTM24024
TJCTM24024

例によってちゃんとした資料がないのだが、電源電圧は+3.3Vが標準のようである。+5V電源のArduinoと接続するときにはレベル変換が必要になることに注意。
裏面右側のJ2コネクタから、液晶表示とタッチパネルに用いるSPI接続用の信号がでている。制御用のICは、液晶がILI9341でタッチパネルがHR2046(TIのTSC2046互換)のようである。裏面にはSDカードスロットもついているが、今回は使わない。

各制御用ICのデータシートは以下から入手した。

液晶への描画用にはAdafruit社のライブラリ ( Adafruit_ILI9341 および Adafruit_GFX) を用いた (リンク先はいずれもGitHub)。

PRO MINIとの接続はSPIなので、MISO, MOSI, SCK, SSが必須になる。これら4本に加えて、D/C (Data or Command)とRESETの2本が必要。さらにバックライト用のLED端子を電源に接続する(後述するが、バックライトはスケッチから消灯/点灯できるような回路とした)。

タッチパネル側もSPIのため、MISO, MOSI, SCKは液晶側と共通とし、タッチパネルを選択するためのSS(Slave Select) 信号をTFTモジュールとは別のI/O端子に接続する。そして、タッチされたことの検出用に、TFTモジュール側のT_IRQをPRO MINIの割込み0端子(D2)に接続した。

したがって、PRO MINI側の8つ(バックライト制御を加えると9つ)のI/O端子を使うことになる。

基本回路図

まずは、以下のような回路構成でブレッドボード上に試作し、TFTモジュールとタッチパネルを使えるようにした。

A_PRO_MINI_ILI9341_TFT1
A_PRO_MINI_ILI9341_TFT1
A_PRO_MINI_ILI9341_TFT1

ブレッドボードの上に組んだところ。TFTモジュールのコネクタの都合でこんな姿になってしまった。左側の電源モジュールから3.0Vを与えている。Arudino PRO MINIは、秋月電子のシリアルUSB変換アダプタ AE-FT231Xを介してPCに接続している。

PRO MINI側のI/Oポートについて

PRO MINIの各ポートとTFTモジュールは以下のように接続している。

ILI9341(TFTLCD用)
D4 LED
(回路経由)
LOWのときバックライトをオフ、HIGHのときオン
D5 CS ILI9341をSPIの通信相手として選択するときLOWとする
D6 RST ILI9341をリセットする
D7 D/C HIGHのときデータ、LOWのときコマンドの送信を示す
MOSI(D11) MOSI SPI Master Out Slave In
MISO(D12) MISO SPI Master In Slave Out
SCK(D13) SCK SPIクロック
HR2046(タッチパネル用)
INT0 (D2) T_IRQ タッチを検出したときLOWとなる。
D8 T_CS HR2046をSPIの通信相手として選択するとき、LOWとする
MOSI(D11) T_DIN(MOSI) SPI Master Out Slave In (共通)
MISO(D12) T_DO(MISO) SPI Master In Slave Out (共通)
SCK(D13) T_CLK SPIクロック (共通)

バックライト制御回路

VCC=+3.3Vのとき、TFTモジュールのLED端子をVCCに接続し、このラインを流れる電流を測ってみると約48mAだった(VCC=+3.0Vの場合は約30mA)。バックライトを点けたままでは一晩で電池があがってしまうので、トランジスタを組合せたスイッチング回路でオン/オフできるようにした。
この回路のように、負荷側に流す電流を制御するときにはPNPトランジスタを使うのが定番で、なおかつPNPトランジスタのベースを、エミッタ接地したNPNトランジスタのコレクタに接続しておくことで、PNPのベースからNPNのコレクタに流れる電流をオン/オフできるようにしておく。PNPのベースから電流が流れなければ、負荷側にも電流は流れない。

VCC=+3.0Vで、TFTモジュール側(表示、タッチパネル)もマイコン側も問題なさそうだったので、省電力のために電源電圧は+3.0Vを使うことにした。なお、PRO MINIのBOD FUSEは変更していないので電源電圧が2.7V未満になるとリセットがかかるだろう。

トランジスタのベース抵抗R1は、2SA1015のベース電流を1mAとするために2.2KΩとし、R2は2SC1815のベース電流を0.5mAとするために4.7KΩとした。PNPのエミッタ-コレクタ間には、負荷(液晶のバックライト)が決まっているので30mA以上の電流が流れることはない。各トランジスタのコレクタ-エミッタ間に生じる電位差(VCE)を小さくするため、計算値もより大きなベース電流を与えた。
今回のスイッチング回路でも、ベース-エミッタ間の抵抗(RBE)を省略したがPRO MINIのD4を digitalWrite(4, LOW); としてやれば、まず影響はない。電源投入直後の未初期化時に負荷側に一切の電流を流せない(リレーを接続するような)場合は、RBEによってICBOの影響を排除したほうがよいと思う。

そういえば、LED端子に与える電流をPWM制御でもって絞ってやれば、スケッチで明るさも調整できるかもしれない。

スケッチ

全体としては、ちょっと前に作ったシャワートイレ用リモコンと同じような構造になる予定。タクトスイッチのオン操作ではなく、タッチパネルへの押圧が、スリープを解除しつつタッチ位置を判定するためのきっかけ(割込み)になる。あとは液晶にボタン状の矩形を描画してやり、タッチされた矩形を識別して赤外線を発射すればよいだろう。

液晶表示についてはAdafruit社のライブラリのおかげでほとんど何も考えずに文字も図形も表示できたのだが、タッチパネルについてはなかなかピンとくるものが見つからなかったので、自前で用意することにした。

今回は、タッチパネル用ライブラリが意図したとおりに動くことの確認用のスケッチを掲載する。

tsc2046_spi.h

以下のコードを tsc2046_spi.h という名前で保存し、スケッチファイルと同じフォルダに置くことでスケッチにタッチパネルからの読み取り機能を追加した。

TSC2046 は抵抗性タッチパネルの制御用デバイスで、逐次比較型のA/Dコンバータおよびマルチプレクサを内蔵している。マイコン側からSPIを経由して測定対象と測定方式および動作を指定する8ビットのコマンドバイトを送信し、次の16クロックで測定結果を得る。コマンドバイト送信時の5番目のクロックの立下りで抵抗値がホールドされ、8番目のクロックの立下りからA/D変換が開始する。そして、次のクロックの立下りから変換結果が送信されてくる(MSBが先頭)。コマンドバイトは以下のような構成になっている。

制御バイトの構成
名称 ビット 説明
スタートビット bit7 (MSB) 制御バイトの開始を示す。常に1
測定対象 bit6~4 マルチプレクサのアドレスを指定。
001b : Y位置
101b : X位置
011b : Z1位置
100b : Z2位置
変換モード bit3 A/D変換の分解能指定。0のとき12ビット、1のとき8ビット
リファレンス電圧モードの指定 bit2 タッチ位置測定時には、0の差動モードを選択。1のときは単エンドモードとなる
パワーダウンモードビット bit1~0 タッチ位置の読み取り時は11bとし、読み取り終了時には00bとする。

 各部の説明

コンストラクタ

メインのスケッチから、スレーブセレクト用のI/Oポート番号、TFT液晶の幅および高さ、TFT液晶ライブラリと同じ座標回転方向(どの隅を 0,0 とするか、長手方向をXとするかYとするか)を与えてインスタンスを作成する。TFT液晶の情報が必要なのかは、タッチパネルから得られた座標情報をTFT液晶の表示座標系と合わせるためである。

なお、画面やパネルの回転については、1と3のみに対応しており、常に長手方向をxとしている。

void read_adc(bool pd_only = false) ;

TSC2046を選択してSPIで通信を行い、タッチ位置の検出を行う。pd_only = true とした場合、読み取りは行わず、パワーダウンモードビットとして 00bを指定し、パワーダウンすると共にタッチによる割込み動作(タッチされるとT_IRQが”L”となる動作)を許可している。

A/D変換結果は12ビットなのに、 z1 += SPI.transfer16(B11000011) >> 3;  のように3回しかシフトしておらず、まるで13ビットのデータを得ているように見えるが、これは変換結果の最初のビット( MSB = ビット11)が出てくるのが9番目のクロックの立下りエッジであるため(コマンドバイトのスタートビット時に1番目のクロックが出ている)。つまり、SPI MODE0なのでマイコン側はクロックの立上りエッジにおいてデータビットをラッチするから、MSBをラッチできるのは10番目のクロックにおいて、ということになる。言い換えれば、MSBが常に0の13ビットのデータを得ているということもできる。詳しくは、TSC2046のデータシートにある変換タイミング図を参照。

実際の読み取りは、x,y,z1,z2のおのおのについて4回読出しを行い、4で割って平均値を得ている。TSC2046用のスレーブセレクト(CS端子)をLOWにしている期間は、ロジアナで見たところ274μsecだった。人間が操作する内容を得るものなので十分に高速といえるだろう。

Z軸方向(押圧)は、データシートにある圧力測定用の式を適当なコードで置き換えたものを使って得ている。このような計算を行わないと、タッチした位置が液晶パネルの中央部か端寄りかによってZ1やZ2の値を大きく変わってしまう。

 const bool read_pos(int& x, int& y)

read_adc() を呼び出すことで得た生データ(rx, ry, rz) に基いて、液晶側の座標に変換して返す。なお、押圧(rz)がある程度以上の大きさでなければ、タッチされていないと見なしてfalseを返す。

TSC2046が返す座標値(rx, ry)は、ぎりぎり端をタッチしても0や4095にはならず、若干のマージンが含まれているようだ。そのため、実際にタッチして得た端の座標値を定数としてコードに書いてある。おそらく、製品ごとのばらつきもあるだろう。それらの定数と、Arduino の map() 関数により生座標を液晶座標に変換している。

void touch_loop()

arduinoの void loop(); から呼び出されることを前提としており、タッチ検出用のループを回す。ループ内でread_pos() を呼び出し、十分な押圧でタッチされているならば、その座標を関数ポインタ touch_handler にアドレスがセットされているコールバック関数に渡す。

タッチされていない状態が一定の期間続くか、ループの上限回数に達したら終了する。コールバック関数側が true を返した場合、連続したタッチ検出が許可されたことになり、ループは上限回数に達しない。コールバック関数でのGUIボタンの操作制御のためにそのようにした。

なお、タッチパネルが出力するデータを検証する場合はメインスケッチの void loop() を以下のようにすればよいだろう。

こうすることで、シリアルモニタに20msecごとに変換結果(の平均値)が表示され続ける。

Adafruit_ILI9341.hへの追加項目

ILI9341自体には、表示のオン/オフやスリープモードを開始するための機能があるのだが、残念ながら表記のライブラリには組み込まれていない。ちょっとずるい気もするが以下の短いコードを adafruit_ili9341.h の public: セクションに追加した。

実のところ、通常の状態でtft_sleep() しても6mA程度(@+3.0V) しか節約できないし液晶には何も表示されなくなってしまう。あまり効果はないのかなと思っていたが、tft_sleep()とマイコンのスリープおよびバックライトオフとを組合せることで、今回の回路に流れる電流を20μA程度まで減らすことができた。これなら電池駆動で作ってみてもいいだろう。

メインスケッチ(A_PROMINI_XPT2046.ino)

メインのスケッチには液晶の表示のオン/オフ、タッチによるスリープ状態 (SLEEP_MODE_PWR_DOWN) からの復帰、タッチ動作の確認などを行うためのコードを入れた。PRO MINIの省電力のためのコードなどは以前と同様。

初期設定が終わると液晶の表示もバックライトも消してスリープ状態に入いる。タッチを検出すると、タッチし続けている間は検出した座標を液晶上に表示し続ける。

 各部の説明

#defineで、液晶制御用のポートを定義し、Adafruit_ILI9341クラスのインスタンス tft を作成。さらに、タッチパネル読み取り用のインスタンス TS を作成している。タッチパネル用のスレーブセレクトはD8に割り当てた。

void getTextMetrics(char* cpText, uint16_t& tw, uint16_t& th)

液晶上の同じ位置にタッチパネルからの読み取り値を連続的に表示するにあたり、表示対象の矩形の大きさを得るために作った関数。この関数を呼ぶ前に、tft.setTextSize() によってフォントサイズを指定しておく必要がある。

bool onTouch(int x, int y)

タッチパネルライブラリからコールバックされる関数。引数x, yには液晶ライブラリと同様の座標系で位置が与えられる。最初に表示位置矩形の幅と高さを使って矩形を塗りつぶし、同じ位置に引数として得られた座標と、タッチパネルライブラリが保持する生座標を表示する。

void setup()

ATmega328PのADCやアナログコンパレータの動作を禁止し、液晶のバックライトをオフにする。そしてILI9341に対して明示的にリセットをかけ、液晶表示のための初期化を行い、タッチパネルライブラリにコールバック関数のポインタを与える。

void isr()

スリープからの復帰用なので、中身は何も書く必要がない。

void loop()

明示的にバックライトを消し、液晶表示もオフにしてからスリープモード (SLEEP_MODE_PWR_DOWN) に入る。スリープ中のBODも禁止している。
タッチパネルに押圧が掛かると割込みが発生(D2がLOWになる)し、スリープから復帰してくるので、バックライトの点灯後に液晶の表示を再開させ、タッチパネルライブラリの touch_loop() を呼び出す。

タッチが続いている間はonTouch() が連続して呼び出される。タッチされなくなるとtouch_loop() から帰ってくるので、loop() の先頭に戻ってスリープ状態に入る。

動作

TFTモジュールをペンでタッチしている模様を動画にしてみた。

液晶にカメラだのレンズだのが映ってしまうので、部屋を暗くして撮影した。ペンを動かすごとに変換した座標と生座標を表示し、ペンを離すと液晶が消え(バックライトオフ)、戻すとすぐに復帰する。

A_PRO_MINI_ILI9341_TFT1
A_PRO_MINI_ILI9341_TFT1

タッチするごとにTFTモジュールが動いてしまうので、長いブレッドボードからはみ出した下に適当な台をおいてしのいだ。ちゃんとしたハコに入れてやった方がいいのだろう。

きょうのまとめ

安い製品だが、表示もタッチパネルもちゃんと動いているようで安心した。動画ではタッチペンを使っているが、最初に買った製品には付属していなかった。別のTFTモジュールに付いてきたものを使っている。専用のタッチペンがなくても、ボールペンでも箸でもいいとは思うのだが。

次回は、今回のタッチパネルライブラリと連携するような操作ボタン機能をこしらえて、その使用例としてテレビだけを操作できるシンプルなリモコンを試作してみる予定。テレビにもたくさん機能があり、そのリモコンにも数多くのボタンがあるが、よく使うものだけを用意する。

問題は、2.4インチの液晶や、そこに表示できる文字がとても小さいこと。読書用のメガネがないと文字が読めない恐れがある。ILI9341とTSC2046ベースのTFTモジュールならば同じソフトウェアが動くだろうから、もうちょっと大きなTFTモジュールを使いたい。ただそうなると、余計に電気を食ってしまうだろうことが問題か。