今回も、TFTシールドを使って遊んでみます。今回のテーマは文字(列)の描画、です。
入手して最初に動かしてみたサンプルコードがありましたが、この時に使用したライブラリ(Adafruit_GFX)には、文字列を描画する関数が用意されています。……たぶん(1^^)っ。ヘッダファイルを眺めてみただけで使ってないのでわかりませんが。
ヘッダファイルを見る限り、線を引いたり、円を描いたり、一通りの関数が揃っている様ですし、何より、ライブラリとして完成していますので、いつでもインクルードして使うことが出来ます。こちらのライブラリを使う、と言う選択をする皆様は、このページを読み飛ばしていただいて宜しいと思います。
ちょっと脱線して、本HPでのDIYに対する取り組み方についてお話しておこうと思います。
DIY=Do It Yourselfの略で、日本語意訳で「日曜大工」とされています。英語の直訳では、「それをあなた自身でやりなさい」ですね。本HPは、電子工作のDIYをテーマとしているわけですが、少し、日曜大工の例を引き合いにさせていただきます。
日曜大工で机上に置く様な本立てを作るとします。
ざっとこんな感じでしょうか?この例では、材料から自分で買い集めて、ゼロから自分のイメージ通りの本立てを作っています。
一方、日曜大工そのものよりも、「本立てを作りたい」事に主眼が有って、既製品では見つからなかったけど組み立てタイプのキットの中にイメージに近い物があった場合には、キットを買って来て自分で組み立てるかもしれませんね。
同様に、電子工作にもいろいろなオプションがあるのです。本HPは「電子工作でこれを作りたい」と、言うよりは「電子工作その物」に面白さを感じているので、「可能な限り自分でやりたいな〜」と、思ってしまいます。
今回のTFTシールドへの文字列描画にしても、既にどなたかが作って提供してくれるライブラリがあるのなら、それを使ってしまえば一発、なのだけど、何か物足りなさを感じてしまいます。なので、今回もちゃんと、自分で描画ルーチンを起こしてみたいと思います。
とは言えども、どなたかの作ったSDライブラリとか、UIPEthernetライブラリ、Serialライブラリ等々、使わせてもらっています。さすがに自分で作るには知識や技能が足りなかったり、手が付けられない場合には、既製品(ライブラリ)を使わせてもらっています。これは、日曜大工に当てはめると、「ねじ・釘は既製品を買って使う」ってことに相当すると考えています。
勿論、自分でタップを使ってねじすら自作して使う強者もいらっしゃるでしょうが……。
色々なアプローチがあって良いと思いますので、皆様のニーズに合わせて、読み進んでいただけたら幸いです。
蛇足ですが本HPの場合、あまり「最後の仕上げ」まで実施しないケースが大半です。そもそも、色々な電子工作をしていると、次々と「こうしたらもっと良くなる、使い易くなる」などのアイディアが湧いてくるので、「完成することは無い」のも事実です。逆に「完成」させてしまうと、そこから先の進歩や対策の余地を潰してしまうことになりかねないので、あえて完成させない(避けている)という面もあります。
外から見ると「いい加減に作るだけ作って途中で放り出している」様に見えて、「飽きっぽいのか?」と思われているかもしれませんが、あくまで「電子工作すること」を楽しんでいる趣味の世界ですのでご理解をv(1^−^)v。
脱線終わり。
何はともあれ、フォントデータが無いと始まりません。フォントを作りましょう。
今やベクタフォントが主流です。高精細なPCやスマホなどに、ある程度の大きさでベクタフォントを表示すると活字のようにきれいなのですが、逆に8×8[ドット]くらいの小さな文字を表示しようとすると汚くて見られたものではありません。
筆者が初めて触れた8ビットパソコンは、当時640ドット×200ドットの画面に8×8[ドット]のフォントで80桁×25行の表示ができました。当時のフォントはベクタフォントではなくて、ビットマップフォントと呼ばれるものでした。今回使用するTFTシールド(下図)は240ドット×320ドットと、昔の8ビットパソコン並みの表示領域なので、同様に8×8[ドット]のビットマップフォントを使おうと思います。
当時の8ビット/16ビットパソコンで、日本語(漢字)を表示するのに使われていたのは16×16[ドット]フォントだったのですが、さすがに数の多い漢字フォントをArduinoに持たせる訳にはいきませんし、8×8[ドット]で7ビットASCIIコード文字に限定しようと思います。
いつもの様に、Excelの機能と数式を使ってフォントを作成しました。
以下、デザインする時のコツです。
文字の形が固まったら、横方向に8ビット2進数のデータと見て、1バイトの16進数データに変換します。これを上から順に並べると、以下のデータ列が文字'A'のフォントデータとなります。
0x10,0x38,0x64,0xC2,0xFE,0xC2,0xC2,0x00, // 'A'
8×8[ドット]くらいまでなら、フォントを作るのって楽しいです(1^^)。それが実際に画面に表示されると更に楽しい。機会があれば、ご自身のフォントを作って見てください。
7ビットASCII文字の印刷可能文字は、コード0x20〜0x7eまでの95文字です。前節の要領で95文字分のフォントデータを作ったら、C言語の配列変数に変換します(↓)。
const uint8 PROGMEM tft_prn_font[ ]= // tft: font data (0x20~0x7e)
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ' '
0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x00, // '!'
0x66,0x66,0x88,0x00,0x00,0x00,0x00,0x00, // '"'
0x44,0xFE,0x44,0x44,0x44,0xFE,0x44,0x00, // '#'
0x28,0x7E,0xA8,0x7C,0x2A,0xFC,0x28,0x00, // '$'
:
: (中略)
};
95文字×8バイト=760バイトです。普通に配列宣言してしまうと、AVR(ATmega328)の場合RAM(2Kバイト)に領域が確保されてしまって760バイトもRAMを圧迫してしまうので、PROGMEM宣言をしてROM(Flashメモリ)上に配置します。
「必要な文字だけを作って、その文字だけをフォントデータに持っておく」と言う方法で、メモリを節約はできますが、描画する時、文字コードからフォントデータへのポインタを求める時に余計な処理が必要になって、速度を落とすことになります。これを避けるため、文字を飛ばさずに0x20〜0x7eまで95文字分の全フォントデータを作って、単純な直交テーブルにしておきました。
この場合、文字コード(ASCIIコード)からフォントデータのポインタへの変換は以下の様になります。
int fnt;
fnt=(*str) - 0x20;
if (fnt<0 || 0x7e - 0x20<fnt)
fnt=0;
else
fnt*=8;
tft_putchr (fnt);
7ビットASCIIコードの印刷可能文字以外は、フォント番号0番(fnt=0)を表示します。今回のフォントデータtft_prn_font[ ]では、フォント番号0番は0x20空白(' ')の番号なので、空白が描画されます。
「#10-9 QVGA TFT液晶を使う」にて、TFTシールドの表示ドライバチップILI9328にアクセスするためのtft_write()関数を作成済みです。これを使うと、任意座標(ix,iy)にドット(pix)を表示するには、以下の様に実行するだけです。
tft_write (0x20, ix); // horizontal position
tft_write (0x21, iy); // vertical position
tft_write (0x22, pix); // write to gram
8×8[ドット]なので、上の処理を64回繰り返せば、1文字分の描画が完成します。ですが、それって相当遅い、です。毎回座標を指定してピクセルデータを書き込むので、3ワードの書き込みが必要となっていますから、3ワード×64回の書き込みアクセスが必要となってしまいます。
しかも下図の様にアドレス/データマルチプレクスI/Oバスとなっているので、毎回レジスタアドレスを指定するプロトコルが必要となるのも、速度を劣化させる原因となります。、
※ILI9328データシートから引用
実は、「#10-9 QVGA TFT液晶を使う」で提供したプログラム(test_tft.ino)でも実施していたのですが、ILI9328にはGRAMのアドレスカウンタを自動更新する機能が有って、これを使うと上図の様にデータを連続して書き込むことが可能となっています。
アドレスカウンタの自動更新は、単にx座標を+1しながら次々と点を打っていく、のに留まらず、以下図の様に矩形領域を指定しておくと、範囲内で自動的に左から右、上から下へアドレスカウンタを自動更新してくれる優れものです。
矩形領域(hsa,vsa)−(hea,vea)は、ILI9328のレジスタアドレス0x50〜0x53(Horizontal and Vertical RAM Address Position)で指定します(↓)。
// character address -> dot address
ix=tft_prn_lx*8;
iy=tft_prn_ly*8;
// set rectangle
tft_write (0x50, ix); // 矩形領域指定
tft_write (0x51, ix + 7);
tft_write (0x52, iy);
tft_write (0x53, iy + 7);
tft_write (0x20, ix); // ドットの書き込み座標指定
tft_write (0x21, iy);
上の例では、キャラクタ座標(tft_prn_lx,tft_prn_ly)からドット座標(ix,iy)に変換して、文字を描画する範囲(ix,iy)−(ix+7,iy+7)を指定しています。ドットを描画する最初のアドレスをレジスタアドレス0x20,0x21にセットするのは同じですが、このアドレスが0x50〜0x53で指定した矩形領域の範囲内になる必要があります。
後は、GRAMの書き込みレジスタ0x22に対して、必要数のピクセルデータを連続書き込むと、8×8[ドット]の矩形領域に任意の図形を描画することが可能です(↓)。
tft_write (0x22, pbuf, 64);
矩形領域を指定するため、0x50〜0x53へのレジスタ書き込み4回分が増加するのですが、その代わりに8×8[ドット]のデータ書き込みが圧倒的に高速化できるので、全体としては高速に描画することが出来ます。
「文字を描画してみたは良いけれど、タラタラと描画しているのが目で追える」みたいな残念なのは嫌ですものね……。
では、文字列を描画する関数群を低レベル側から順に解説して行きます。
現在のGRAMアドレスカウンタ位置にドットpixを書き込みます(opt=0の時)。
本関数は内部にバッファpix_buf[ ]を持っていて、呼び出されるとピクセルデータpixをバッファに一旦蓄積します(↓)。
static int pix_ptr;
static uint16 pix_buf[tft_PIX_MAX];
// store pixel
pix_buf[pix_ptr]=pix;
pix_ptr++;
バッファ(ArduinoのRAM)に蓄えるだけなので、ILI9328へのアクセスは発生しません。
何度かtft_putpix()関数の呼び出しが有って、バッファpix_buf[ ]がオーバフロウ(tft_PIX_MAX)すると、その時点で初めて、蓄積したデータをILI9328のGRAMへ一気に書き込みます(↓)。
if (pix_ptr>=tft_PIX_MAX)
{
// flush
tft_write (0x22, pix_buf, tft_PIX_MAX);
pix_ptr=0;
}
上コードの様にtft_PIX_MAX個のバーストライトとなるので、バスサイクルのオーバヘッドが軽減され、高速に書き込むことが出来ます。
最初に使う時に内部のバッファを初期化する必要があり、opt=1(tft_OPT_INIT)を指定してコールすると、バッファの初期化が実行されます。この時のpixはdo not careですので、適切な数値(0などで可)を指定しておいてください。
書き込むデータ数がバッファサイズの整数倍であれば良いのですが、そうではないケースもあるはずなので、opt=2(tft_OPT_FLUSH)を指定してコールすると、その時点でバッファに蓄積していたデータをILI9328に書き込みます。この時のpixもdo not careです。
8×8[ドット]で1文字なので、バッファサイズtft_PIX_MAX=64に設定するのが一番高効率なのですが、関数のデバッグも兼ねて、半分のtft_PIX_MAX=32に設定してあります。
現在のカーソル位置(tft_prn_lx,tft_prn_ly)に、fntが指し示す位置から8バイト分のフォントデータを描画します。つまり、1文字描画ルーチンです。
カーソルとは、30桁×40行のキャラクタ表示座標で、次の1文字を描画する座標を指しています。
フォントデータはPROGMEMでuint8の一次元配列になっています。8バイトで1文字分のフォントデータとなっているので、引数fntに指定するのは、<文字番号>×8のインデックス値です。フォントデータはPROGMEMなので、pgm_read_byte()関数を使ってアクセスします。以下、該当部分の抜粋です。
// draw character
tft_putpix (0, tft_OPT_INIT);
for (iy=0; iy<8; iy++)
{
dat=pgm_read_byte (tft_prn_font + fnt);
fnt++;
for (ix=0; ix<8; ix++)
{
if (dat&0x80)
tft_putpix (tft_prn_fg);
else
tft_putpix (tft_prn_bg);
dat <<= 1;
}
}
tft_putpix (0, tft_OPT_FLUSH);
フォアグラウンド(前景)色(tft_prn_fg)とバックグラウンド(背景)色(tft_prn_bg)を使って描画します。デフォルトではtft_prn_fg=0xffff(白)、tft_prn_bg=0x0000(黒)に設定されています(黒地に白文字🅐)。
現在のカーソル位置から文字列strを描画します。
カーソル座標(tft_prn_lx,tft_prn_ly)は、描画色tft_prn_fg、tft_prn_bgと共にグローバル変数にしてあります。よって、tft_print()関数を呼ぶ前にグローバル変数を設定して描画位置や描画色を設定する事も可能ですが、文字列中に制御コードで指定する方法を提供します。制御コードについては後ほど説明します。
strに格納された文字列を描画して行きます。
まず、(*str)の1文字をtft_putchr()関数で描画します。続いてstr++して、カーソルをtft_prn_lx++します。
この時、tft_prn_lx≧tft_SIZH/8 (画面横幅の右端に到達)となったら、tft_prn_lx=0,tft_prn_ly++とします。さらにこの時、tft_prn_ly≧tft_SIZV/8 (画面高さの下端に到達)となったら、tft_prn_ly=0とします。これが、カーソル座標の更新作業です。
この処理を(*str)='\0'となるまで継続すると、文字列strを描画しきることが出来ます。
この処理の中で、(*str)が以下の制御コードだった場合は特殊な動作となります。
例えば、キャラクタ座標(11,5)に、文字の色0x001f (赤色)で文字列"ABCD"を描画したい場合には、以下の様にします。
"\x10\x0b\x11\x05\x12\x00\x1fABCD"
先頭文字から追いかけて行くと:
制御コードで指定可能とするこの方法は、以下の様にして一部分だけ別な色で描画したり:
"\x12\xff\xffThis is \x12\x00\x1fnot\x12\xff\xff an apple."
↓ 描画
This is not an apple. …… "not"だけが赤色
行をまたいで2×2文字領域に文字を描画したりする時(以下例)等に便利です。
"Ne\b\b\nXT"
↓ 描画
Ne …… "Ne"を描画後、2文字左に移動("\b\b")して、
XT …… 更に1行下に移動("\n")後、"XT"を描画
なお、改行コードが本来の意味の通りに実装されているので、tft_print()関数で文字列の中に改行を指定したい場合には、Windowsなどで使われている、CR+LFのコードを使う必要があります。
"I am Arduino\r\n" …… 改行コード: 復帰(CR,'\r')+改行(LF,'\n')
制御コードによる表示の制御は、昔懐かしい8ビットパソコンのBASIC言語で一部採用されていた方式です。
また、座標値やピクセル値の指定をする時、16進数のデータとして'\x00'を使ってしまうと、C言語の文字列としては終端コード('\0')扱いとなってしまって、文字列操作関数(strcpy()、strcmp()関数等)での操作ができなくなってしまうので注意してください。
小さいとは言え、30桁×40行の文字表示が可能なQVGA TFTですので、大量な文字列をドヒャッと描画したいところです。そこで、PCからシリアルモニタ経由で送ったテキストデータをそのままTFTシールドに描画するプログラムを作ってテストしようと思います。
いつもの様にシリアルモニタからデータを受信すると、以下特定の文字列で始まる行について、コマンドとして認識します。
コマンド仕様:
コマンド以外の文字を受信すると、そのままtft_print()関数を使って、TFTシールドに描画して行きます。改行しながらどんどん下の行にずれて行って、画面下端に到達したらスクロールさせて、更に描画し続けます。
実験プログラムを以下にリンクしておきます。
実験プログラム:tft_print.ino
コンパイルしてTFTシールドを装着したUNOに書き込んでください。
※下の動画で使ったASCIIアートを表示したところ。濃淡がASCII文字で表現されています。
実験サンプル:【 】内はシリアルモニタからの送信データ
【rgb,63,0,0】
OK: rgb(63,0,0)=\x12\x00\x1f
【prn,\x12\xff\xffThis is \x12\x00\x1fnot\x12\xff\xff an apple.】
OK: print
前節でサンプルとして示した制御コードを使ってみました。ちゃんと、"not"だけが赤い文字で描画されたはずです。他にも、制御コードの動作を確かめてみてください。
ここでシリアルモニタを閉じて、Tera Termなど、非同期シリアル通信で任意のファイルを転送可能な通信ソフトウェアを起動します。UNOのシリアルポートと接続したら、「Fileメニュー ⇒ File Transfer...」を選択します。ファイル名を問い合わせてくるので、適切なテキストファイルを指定してください。
7ビットASCII文字しか描画できませんので、日本語などの文字を含まないテキストデータが適切です。また、TABコードが含まれるソースコードなども、避けた方が良いでしょう。∵TABコードはカーソル移動の制御コードに割り当たっているため。
実際に描画してみたところ(↑)。こんな感じです。動画内で使ったASCIIアートのデータをリンクしておきます。
サッカーボールのASCIIアート:a111_soccer.zip
元データ:「サッカーボールを作る」
元画像:
解凍するとテキストファイルの拡張子(a111_soccer.txt)になっていますが、tft_print()用の描画色指定制御コード(バイナリ)が含まれているので、テキストエディタでは開けません。先ほどのTera Termの「Fileメニュー ⇒ File Transfer...」にて、「Binary」チェックボックスにチェックした上で、Arduino+tft_print.ino+TFTシールドに送信して見てください。※「.zip」のまま送信してもダメですよ(1^^;)。
最後に、文字列の描画速度を確認しておきましょう。
実験サンプル:【 】内はTera Termからの送信データ
【who】
DBG: print 18415396[us]/41685[chr]=441[us/chr]
OK: tft_print ver.1.00
描画しながら計測していた描画速度が表示されました。約440μs/文字でした。逆数を取ると約2,200文字/sですね。10ビット/フレームの非同期シリアル通信では、約22,000baudの通信速度に相当します。上でご覧に入れた動画では、シリアルの通信速度を9,600baudに設定したので、表示速度自体は非同期シリアル通信の速度で律速されています。
頑張って非同期シリアル通信の速度を上げるともう少し表示速度も速くなりますが、描画速度は変わらないので最終的には描画速度:2,200文字/sで頭打ちとなります。
※また、Arduinoのハードウェアシリアルユニットには、ハードウェアフロウ制御(RTS,CTS)が付いていないので、描画速度を超える通信速度でデータを送信してしまうと、どこかで取りこぼしが発生すると思います。
う〜ん、どうなんでしょうね?PC等の高速表示に慣れてしまっているせいで、遅いのか速いのか、良く解りません。勿論、PCに比べたらうんと遅いのは当たり前なのですが、Arduinoクラス(8ビットMCU @16MHz)で考えた時に、速いのやら遅いのやら?って意味です。
1画面30桁×40文字=1,200文字ですから、1秒間に1.5回強、全画面を書き換えることが出来る描画速度です。やっぱり、ちょっと遅いかな?まあ、使い方次第ですけどね。後は皆様でご判断ください。
やっぱりちょっと、TFT液晶画面の接写に難があります。実際に目で見ている映像みたいに綺麗に撮影できません(1;_;)。TFTって、それ自体が光っている(バックライト)ので、カメラで撮影するのって難しいですよね。露出やシャッタ速度を調整できるカメラならまだしも、スマホじゃ難しいかな〜?