フォト
2018年11月
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  
無料ブログはココログ

表示器制御関連

2018年11月 7日 (水)

豊四季Tiny BASIC for Arduino STM32で日本語フォント利用対応中

先日作成した「Arduino用SJIS漢字フォントライブラリ SDカード版」を
豊四季Tiny BASIC for Arduino STM32に組み込んで日本語表示対応を試みています。

(現時点の修正案は下記のリンク先にまとめています。
  ttbasic_arduino_stm32のプロジェクトのissues#58にまとめています^^)

日本語表示が出来れば、簡単なアドベンチャーゲームが作成出来ると思います。


日本語表示表示の雰囲気は次のような感じです。

OLED版の表示デモ


プログラムソース

10 'OLED日本語表示デモ
20 @(0)=8,10,12,14,16,20,24
30 S="吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。"
40 L=LEN(S)
50 FOR J=0 TO 6
60 Z=@(J):X0=0:X=X0:Y=0
70 CLS 1
80 FOR I=1 TO L
90 R=KFONT(MEM,ASC(S,I),Z)
100 BITMAP X,Y,MEM,0,Z,Z,1
110 IF (X+Z*2+1)>(GW-1) X=0:Y=Y+Z ELSE X=X+Z+1
120 IF Y+Z>GH-1 I=L
130 NEXT I
140 WAIT 500
150 NEXT J
160 GOTO 160

プログラムはKFONT関数で1文字ずつフォントデータを取得して表示しています。
フォントをSDカードから逐次参照して表示しているため、表示は遅いです。

表示専用のKANJIコマンドを追加予定です。
これを使えばもう少しプログラムが短くなり、表示も早くなると思います。

OLEDの他に、NTSC画面、TFT画面にも表示出来ます。


NTSCビデオ出力画面での表示デモ

プログラムソース

5 CONSOLE 0
10 CLS
20 S="日本語表示にほんご"
30 L=LEN(S):X0=0:X=X0:Y=30:@(0)=8,10,12,14,16,20,24
35 FOR J=0 TO 6
40 FOR I=1 TO L
50 R=KFONT(MEM,ASC(S,I),@(J))
60 BITMAP X,Y,MEM,0,@(J)/R,@(J),1
70 X=X+@(J)/R+0
80 NEXT I
90 X=X0:Y=Y+@(J)+10
100 NEXT J
110 GOTO 110

プログラムの日本語入力は、NTSC、OLED、TFT画面では入力出来ないため、
シリアル接続したTeraTermにて行います。

01


TFT版ではカラー表示に対応しています。

Dscn8965

2018年9月25日 (火)

3Dプリンターでの試行錯誤 その2

3Dプリンタ利用の練習として、
以前製作した下の写真 「I2C接続 16x8ドット LEDマトリックス」のケースを製作してみました。

Dscn4264_2

Dscn4266_2

3D図形の作成はTinkercadにて行いました。
形状は単純なので、割と簡単に作成出来ました。

03

ここで、寸法は実際の寸法よりも、縦横0.4mmほど大きくしています。
試行錯誤の結果、ノズル幅分の約0.4mm小さくなることが分かり、その調整をしています。

データを、STL形式でエクスポートして3Dプリンタに読み込ませて出力開始、
1時間ほどで出力出来ました。

Dscn8857

寸法はドンピシャ、ジャストフィットで収まりました。
いい感じにできました。

Dscn8864

とりあえずIchigoJamで動作、問題無く動作しました。
基板剥き出しから一転して、ちょっとおしゃれな感じになりました。

Dscn8877

動作はこんな感じです。



3Dプリンタ、色々と活用出来そうです。

2018年8月 6日 (月)

アクティブマトリクス蛍光表示管の実験用表示モジュールの試用 (3)

前回のアクティブマトリクス蛍光表示管の実験用表示モジュールの改訂版、
MW25616L実験表示モジュールver.2の利用の続きです。

改訂版では、I2C接続用端子が新たに追加さました。

01 03

下からGNDVCCA4(SDA)A5(SCL)D2が割り付けられており、
付属のRTCモジュール(DS3231利用)が直接接続出来ます。

02

前回に引き続き、豊四季タイニーBASIC for Arduino機能拡張版(以降豊四季Tiny BASIC拡張版と略称)にて
動作確認をしてみます。

DATEコマンドで時刻を参照すると、当然正しく無いですが参照できました。

01_2

SETDATEコマンドで正しい時刻を設定します。

02_2


次に、下記のプログラムにてVFDに時刻を表示してみます。
10 VCLS:VBRIGHT 100
20 GETTIME H,M,S
30 VMSG 0,#-2;"現在の時刻は";H;"時";M;"分";S;"秒です"
40 WAIT 1000
50 VSCROLL 256,-1
60 GOTO 20

動かして見ると、表示内容を更新する毎にノイズが乗るような感じで乱れたりします。

04

05

旧バージョンでも試してみましたが、同様の現象が発生しました。
う~ん、BASICインタプリタのバグか?

雰囲気的に「データ転送時のデータ化け」っぽく、
調査していくと、VFDふぁんさんが公開しているサンプルソースのコメントに
ヒントがありました。

どうも、輝度設定(VBRIGHTコマンド)に影響するようです。
輝度を最大輝度の設定(VBRIGHT 255)にすると正常に表示出来ました。

VFDの輝度はディスプレイイネーブル(EN)をPWMを使ってオン・オフ(表示のオンオフ)して
調整しているのですが、これがスイッチングノイズを発生させて影響を及ぼすようです。

VFDの供給元のノリタケの技術サポート公開資料の
「アクティブマトリクス型CL-VFD アプリケーションノート - インターフェースインターフェースについて」

  スイッチングノイズによる誤動作(データ誤取込)を防止するため、
  データ書き込み中はENをH→L、あるいはL→Hに変えないでください。

との記載がありました。

ということで、データ転送中は輝度設定を255(最大輝度)にしなければならないことが
分かりました。利用の際の制約事項ですね。以後気を付けることにします。


問題解決後の実際の動作の様子




プログラムの自動起動の設定

利用の都度、プログラムをRUNコマンドで実行させるのはちょっと面倒です。
電源ON後、時刻表示プログラムをロードして実行するようにしたいですね。

豊四季Tiny BASIC拡張版には保存したプログラムを電源ON時に自動起動する機能があります。
デフォルトでは、自動起動判定用ピンD7HIGHの場合、内部EEPROMに保存されているプログラムを実行します。

RTCモジュール端子のD2RTCモジュールSQW(割り込み通知用の入力)用ですが、
この信号はアクティブLOWなので、RTCモジュールを接続した状態で起動すると
D2は常にHIGHとなります。これは利用出来ますね。

D2を、豊四季Tiny BASIC拡張版の自動起動判定用ピンに利用し、
RTCモジュールが接続されていれば、電源ON時はD2HIGHなので自動起動出来ます。

そこで、スケッチのttconfig.hAutoPinの設定を7から2に変更して書き込みます。

08

書き込み後、現在時刻表示プログラムをコピペしてSAVEコマンドで保存します。
VBRIGHT 255の修正は忘れずに)

09_2

ダイソーの300円 モバイルバッテリーをつなげて動かしてみました。

06

保存してプログラムが自動起動して、時刻を表示してくれました。
いい感じで動いてくれました。
単体で動作出来るのは良いですね。

07

折角ここまで来たので、何か実用的なものを作ってみましょう。
次に、I2C接続の湿度・温度センサーもつなげて時刻・湿度・温度を表示出来るように
したいと思います。


2018年8月 5日 (日)

Arduino STM32用のTV出力ライブラリを更新しました

Arduino STM32用のTV出力ライブラリを更新しました。

01

更新したライブラリ
  ・Arduino STM32 TVoutライブラリ
   https://github.com/Tamakichi/ArduinoSTM32_TVout

   修正内容
     ・横解像度448ドット指定時、ビット・バンディングを使ったドット描画位置がずれる
       不具合の対応を行いました。
       これによりdraw系の関数の描画不具合を解消しました。

      ・draw_rect()、draw_circle()の塗りつぶし指定時に塗りつぶし正しく行えない不具合
       の対応を行いました。

      ・adjust()関数に映像の横表示開始位置補正、縦表示開始位置補正を行うための
       引数を追加し、映像表示位置の調整を行えるようにしました。

  ・Arduino STM32 NTSCビデオ出力ラブラリ
   https://github.com/Tamakichi/ArduinoSTM32_TNTSC

   修正内容
      ・adjust()関数に映像の横表示開始位置補正、縦表示開始位置補正を行うための
       引数を追加し、映像位置の調整を行えるようにしました。

   ※ NTSCビデオ出力ラブラリは、TVoutライブラリの下位レイアのライブラリです。
       NTSCビデオ出力のみを行います。
       TVoutライブラリは、描画等のAPIを提供するライブラリです。

   
従来は利用するモニターによっては、表示する映像が上や左に寄り過ぎて欠けたり
した場合、どうすることも出来なかったのですが、今回の修正で補正を行えるようにしました。

こんな感じで、右側が欠けてしまった場合、

02

関数 adjust(0,8,0) にて、映像を左にずらすことが出来ます。

03

さて、Arduino STM32 TVoutライブラリは、ArduinoのTVoutの描画処理を
ベースにしているのですが、オリジナル版に不具合があり、意外とハマりました。

使い勝手も今一よろしく無いので、ライブラリを2つに分けていたり利点を生かし、
Adafruit_GFXを利用したバージョンを別途用意してそちらを利用しようと考えています。



2018年7月28日 (土)

アクティブマトリクス蛍光表示管の実験用表示モジュールver.2の動作確認

VFDふぁんさんより、アクティブマトリクス蛍光表示管の実験用表示モジュール
改訂版 MW25616L実験表示モジュールver.2(VF1711) を頂きました。

最近、電子工作のモチベーションが下降気味でしたが、一気にテンションが上がりました!
VFDふぁんさん、ありがとうございます。

こんな感じのモジュールです。

MW25616L_1

初版からは、いくつか改良が施されているようですが、
変更点については、おって確認したいと思います。

MW25616L_2

改訂版は基板の上下にアクリル版が標準装備になったみたいです。
これは有難いです。ショートの防止になります。

実は、初版の利用において、私の不注意で基板の裏に何かが接触してショートし、
破損させてしまいました(その後、VFDふぁんさんに修理して頂き、再稼働出来ました)。

早速、電源を入れてみるとデモスケッチが動きました。

MW25616L_3

コントラスト・輝度が強くで、私のデジカメでは絞らないと綺麗に撮影出来ませんでした。
表示は鮮明で読みやすいです。

豊四季タイニーBASIC for Arduino機能拡張版(+ VFD MW25616L対応)を動かす

早速、「豊四季タイニーBASIC for Arduino機能拡張版(+ VFD MW25616L対応)」
動作確認をしてます。

折角なので、利用出来るまでの手順を復習がてらまとめてみました。

まずは、スケッチの書込みを行います。
プロジェクトの一式を、下記からダウンロードして解凍します。
   ・ttbasic_MW25616L-master.zip

解凍すると次のファイルが得られます。

02

次に、Arduino IDEを起動して上記のフォルダtbasic 内のttbasic.inoを開きます。

03

デフォルトの設定では、アクティブマトリクス蛍光表示管の利用が出来ません。

ttconfig.hの設定の一部を下記のように変更(赤字の項目)します。
// ** 機能利用オプション設定 *************************************************
#define USE_CMD_PLAY   1  // PLAYコマンドの利用(0:利用しない 1:利用する デフォルト:1)
#define USE_CMD_I2C    1  // I2Cコマンドの利用(0:利用しない 1:利用する デフォルト:1)
#define USE_CMD_VFD    1  // VFDモジュールコマンドの利用(0:利用しない 1:利用する デフォルト:0)
#define USE_RTC_DS3231 1  // I2C接続RTC DS3231の利用(0:利用しない 1:利用する デフォルト:1)
#define USE_I2CEEPROM  1  // I2C EEPROM対応(0:利用しない 1:利用する デフォルト:1)
#define USE_SYSINFO    0  // SYSINFOコマンド(0:利用しない 1:利用する デフォルト:0)
#define USE_GRADE      0  // GRADE関数(0:利用しない 1:利用する デフォルト:1)
#define USE_DMP        0  // DMP$関数(0:利用しない 1:利用する デフォルト:1)
#define USE_IR         0  // IR関数(0:利用しない 1:利用する デフォルト:1)
#define USE_ANADEF     1  // アナログピン定数A0~A7orA15(0:利用しない 1:利用する デフォルト:1)
#define USE_SO1602AWWB 0  // 有機ELキャラクタディスプレイ SO1602AWWB(0:利用しない 1:利用する デフォルト:0)

アクティブマトリクス蛍光表示管の利用にはフラッシュメモリ利用域を消費するため、
他の機能を無効にする必要があります。
今回は、利用頻度の少ない、GRADEコマンド、DMPコマンド、IR関数を無効にしました。

スケッチを書き込みます。

01


次にTeraTarmを起動して、MW25616L実験表示モジュールに接続します。

通信条件

   05

端末の設定

   改行コード、漢字-受信、漢字-送信を下記の設定にします。

   06

接続します。何も表示されない場合は、F5キーを押して再表示させてます。

    04

試しに次のプログラムをコピー&ペーストして実行してみます。
10 VCLS
20 VMSG 10,"CL-VFD実験用表示モジュールVer2登場!"
30 VSCROLL 256,5
40 WAIT 300
50 GOTO 20

07

問題なく動作しました。

Dscn8487

実際に動いている様子です。



問題無く動いているようです。
実際は、もっと鮮明で綺麗な表示なのですが、
私のデジカメの動画撮影機能では、綺麗に撮影出来ませんでした。

もうすこし、動作確認を続けてみます。

2018年7月25日 (水)

プロペラでVT100端末の実装

SNSにてシリアル・ターミナル コンソールをマイコンで実装する話題があり、
「そういえば、プロペラマイコンでVGA+PS/2キーボード構成が簡単に実装出来るなぁ」
と思い出し、やってみました。
久しぶりのプロペラマイコン利用です。

Dscn8443

ゼロからの自作は無理なので、mikrocontroller.netVT100-Terminal (VGA+PS2)
にて紹介されている、「RETROCOMPUTING VT100 VIDEO TERMIAL」を
利用して実装しました。

ソースや回路図等は上記フォーラムに掲載されているリンク先から入手しました。
  ・https://www.mikrocontroller.net/svnbrowser/avr-cp-m/trunk/VT100/


回路図を見ると、キーボードやVGA周りの実装は、
プロペラのデータシートに掲載されているデモボードと同じようです。
比較的少ない部品で実装出来ました。

Dscn8440

一応、これで640x480ドット 80x25文字、 64色同時表示のVGA表示が出来ます。
キーボードはPS/2インタフェースとなります。
シリアル接続のターミナル コンソール VT100相当として利用出来ます。

動作確認に、Arduino Uno互換機にて「豊四季タイニーBASIC for Arduino機能拡張版」
動かしてみました。

本来はパソコン上でTeraTarmの利用を想定しているのですが、
果たして使い物になるでしょうか?


動作確認の様子


予想以上にまともに動作しました。
表示もNTSCとは異なり、非常に綺麗です。

カラー表示も出来ました。
意外と使えそうです。

他にもプロペラを使った類似のものが公開されているので、もう少し調べてみます。

2018/07/28 補足

開発環境はPropeller IDEを利用しています。
こんな感じのツールです。言語としてはSPINというインタプリタ言語が標準です。

Ide

出回っているプログラムは、旧開発環境のPropeller Toolを使っている場合があります。
その場合、Propeller IDEではコンパイルに失敗する場合があります。
Propeller Toolもインストールして、Propeller IDEのライブラリパスの設定にて、
Propeller Toolの付属ライブラリにパスを通す必要があります。


関連記事
Propeller始めました(8) スピーカーで音を出してみる (2015.11.30)
Propeller始めました(3) VGA出力を試してみる (2015.11.14)
Propeller始めました(2) TV出力を試してみる (2015.11.12)

2018年7月14日 (土)

Arduino用漢字フォントライブラリ SPI フラッシュメモリ版の更新

先日の「Arduino用漢字フォントライブラリ SDカード版のSdfat対応」繋がりで、
「Arduino用漢字フォントライブラリ SPI フラッシュメモリ版」を更新しました。
ベースとなっている「Arduino用 SPI接続フラッシュメモリW25Q64ライブラリ」も更新しました。

 

更新ライブラリ
    ・Arduino用漢字フォントライブラリ SPI フラッシュメモリ版
      https://github.com/Tamakichi/Arduino_exfonts

      修正点
          - SPIバスの共有対応
          - Arduino STM32環境での複数のSPIポート利用に対応
          - SPIクロック速度の任意設定対応

    ・Arduino用 SPI接続フラッシュメモリW25Q64ライブラリ
      https://github.com/Tamakichi/Arduino-W25Q64    
          - SPIバスの共有対応
          - Arduino STM32環境での複数のSPIポート利用に対応
          - SPIクロック速度の任意設定対応

    上記2つのライブラリはArduino(Atmega328)、Arduino STM32で利用可能です。

早速、先日の「吾輩は猫である」の漢字フォントを
SDカードからSPI接続フラッシュメモリ(W25Q64)に置き換えてみました。

置き換後の様子

 

スクロールが発生しない文字サイズでは、SDカード版よりもかなり早くなっています。
(表示速度の違いについては、追って追記します^^; )

今回の動作チェックでは、BluePillボードの代わりにRobotDyn製の互換ボードを利用しました。

Dscn8422

BluePillボード(右)と比べるとRobotDyn製互換ボードは、幅が狭いので、
ブレッドボードを有効活用出来ます。

SPIバスの共有動作チェックする際、BluePillボードでは片側が1列しか確保出来ないため、
仕方なく置き換えました。

Dscn8421

RobotDyn製互換ボードは、BluePillボードよりも品質が良く、
ブートローダ付きの製品もあります。
ただし、お値段がBluePillボードよりもちょっと高いです。
(単品購入だと、送料込みでBluePillボードの3倍!!)

STM32F103C8T6, STM32 bootloader compatible for Arduino IDE or STM firmware,
ARM Cortex-M3 Minimum System Development Board

02

送料がかかるので、RobotDyn製の他のパーツとセットで購入が良いと思います。

 

2018年7月11日 (水)

Arduino用漢字フォントライブラリ SDカード版のSdfat対応

以前作成した「Arduino用漢字フォントライブラリ SDカード版」をSdfatライブラリ対応しました。
Arduino標準のSDライブラリを使うよりもパフォーマンスが向上します。

 
  ・Arduino用漢字フォントライブラリ SDカード版
   https://github.com/Tamakichi/Arduino-KanjiFont-Library-SD

Arduino STM32でも利用可能です。BluePillボードでグラフィック液晶モジュールで
日本語表示をやってみました。

      Dscn8411


動いている様子


タッチスクリーンの競合等の動作チェックのため、簡単なお絵描き機能を付けています。
フォントを逐次、SDカードから読んでいるのでさすがに表示は遅いです。

スクロールはグラフィック液晶モジュールから1ラインづつ読み込んだデータを
上にフォント幅分、ずらした位置に描画するここで実装しています。
フォントデータをメモリ上に乗せられないため、先日の高速処理は行えません。
もし、フレームバッファ150kバイト確保できるマイコンボードなら、
マイコンボード上でスクロールして転送するなどして高速に出来るのですが、
BluePillでは、これが限界ですね。

スケッチ

//
// フォントライブラリ利用サンプル
// 作成 2018/07/10 by たま吉さん
//

#include <sdfonts.h>
#include <Adafruit_GFX_AS.h>     
#include <Adafruit_ILI9341_STM.h>
#include <XPT2046_touch.h>

// TFT制御用ピン
#define TFT_CS  PA0
#define TFT_RST PA1
#define TFT_DC  PA2

// タッチスクリーンCSピン
#define TS_CS  PA3 

// タッチスクリーン領域
#define TS_MINX 600
#define TS_MINY 440
#define TS_MAXX 3460
#define TS_MAXY 3460

#define MY_SPIPORT  2   // SPIポートの指定 1:SPI ,2:SPI2

//
// ※SdFatを使う場合は、sdfontsConfig.hのSDFONTS_USE_SDFATに1を設定し、
//   SdFatまたは、SdFatEX型のグローバルオブジェクトSDを用意すること
//

// 利用するSDオブジェクトの定義
#if SDFONTS_USE_SDFAT == 1
  #include <SdFat.h>
  #if ENABLE_EXTENDED_TRANSFER_CLASS == 1
    SdFatEX  SD(MY_SPIPORT);
  #else
    SdFat    SD(MY_SPIPORT);  
  #endif
#else
  #include <SD.h>
#endif

SPIClass  SPI_2(2); // タッチスクリーン用SPI

// タッチスクリーン制御用
XPT2046_touch ts(TS_CS, SPI_2); // Chip Select pin, SPI port

// TFT制御用
SPIClass  SPI_1(1);
Adafruit_ILI9341_STM tft = Adafruit_ILI9341_STM(TFT_CS, TFT_DC, TFT_RST,SPI_1);

// スクロールアップ
void scrollUp(uint16_t y0,uint16_t h,uint16_t bg) {
  uint16_t sc_w = tft.width();
  uint16_t sc_h = tft.height();
  uint16_t buf[sc_w];

  for (uint16_t y=h+y0; y < sc_h; y++) {  
    tft.readPixels(0, y, sc_w-1, y, buf);
    tft.setAddrWindow(0, y-h, sc_w-1, y-h);
    tft.pushColors(buf, sc_w, 0);
  }
  tft.fillRect(0, sc_h-h, sc_w-1, h, bg);
}

// 指定位置に1文字表示
void mputc(uint16_t x, uint16_t y, uint8_t* buf, uint16_t fg, uint16_t bg) {   
  uint16_t w = SDfonts.getWidth();
  uint16_t h = SDfonts.getHeight();
  int16_t byteWidth = (w + 7)>>3;
  uint8_t byte = 0;

  // フォントの描画
  tft.setAddrWindow(x, y, x+w-1, y+h-1); // 描画領域の設定
  for(int16_t j=0; j<h; j++, y++) {
    for(int16_t i=0; i<w; i++) {
      byte = (i & 7) ? byte<<1 : buf[j * byteWidth + (i>>3)];
      tft.pushColor((byte & 0x80) ? fg : bg);
    }
  }
}

// 指定位置に文字列表示
void mprint(uint16_t x, uint16_t y, char* str, uint16_t fg, uint16_t bg) { 
  uint8_t buf[MAXFONTLEN]; 
  int16_t   len,x0 = x, y0 = y;
  char* pUTF8 = str;

  SDfonts.open();   // フォントのオープン
  while ( pUTF8 = SDfonts.getFontData(buf, pUTF8) ) {  // フォントの取得
    mputc(x, y, buf ,fg, bg);
    if (x + SDfonts.getWidth()*2 < tft.width()) {
      x += SDfonts.getWidth()+1;  
    } else {  
      x = x0;
      if (y + SDfonts.getHeight()*2 < tft.height()) {
        y += SDfonts.getHeight()+2;
      } else {  
        scrollUp(y0, SDfonts.getHeight()+2, bg);
      }
    }
  }      
  SDfonts.close();   // フォントのクローズ
}

void setup() {
  Serial.begin(115200);
  delay(1000);
#if SDFONTS_USE_SDFAT == 0
  SPI.setModule(2);
#endif

  // フォント管理の初期化
  if(!SDfonts.init(PB0)) {                
    Serial.println(F("sdfonts init error"));
    exit(1);
  }
  Serial.println(F("test sdfonts liblary"));
  
  ts.begin();
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(ILI9341_BLACK); 
}

static const char* text =
   "吾輩は猫である。名前はまだ無い。"
   "どこで生れたかとんと見当がつかぬ。"
   "何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。"
   "吾輩はここで始めて人間というものを見た。"
   "しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。"
   "というのは時々我々を捕つかまえて煮にて食うという話である。"
   "しかしその当時は何という考もなかったから別段恐しいとも思わなかった。"
   "ただ彼の掌のひらに載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。"
   "掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始めであろう。"
 ;

void loop() {
  //7種類のフォントサイズで文字列表示
  for (uint8_t i =0 ; i <7; i++) {
    tft.fillScreen(ILI9341_BLACK);
    SDfonts.setFontSizeAsIndex(i);
    mprint(2, 2, (char*)text, ILI9341_WHITE, ILI9341_BLACK);

    // テキスト表示後、3秒間タッチスクリーンを使って描画出来る
    // (SDfontsとタッチスクリーンのSPIバス共有利用の動作確認)
    uint32_t t = millis() + 3000;
    while(millis() < t) {
      TS_Point p = ts.getPoint();
      p.x = tft.width() - map(p.x, TS_MINX, TS_MAXX, 0, tft.width());
      p.y = tft.height() - map(p.y, TS_MINY, TS_MAXY, 0, tft.height());

      if (p.x >=0 && p.x < tft.width() && p.y >=0 &&  p.y < tft.height()) {
         tft.fillCircle(p.x, p.y, 3, ILI9341_RED);
      }       
    }
  }
}

表示速度の改善方法としては、
Arduino用漢字フォントライブラリ SPI フラッシュメモリ版」への置き換えで
かなり改善出来ると思います。これについてもちょっとやってみます。
Arduino Unoでも同じこと出来るかも試したいと思います。

2018年7月 9日 (月)

次はSTM32ボードを積極的に使ていきたい(26) グラフィック液晶(7)

フォント描画処理の改善

SPI接続のグラフィック液晶モジュール(コントローラー ILI9341)利用の調査を継続中です。

Dscn8386

利用しているAdafruit_ILI9341_STMライブラリのフォント表示、ビットマップ表示の
描画処理の遅さが気になり何とかならないかと試行錯誤してみました。

特に横向きに利用した場合、ハードウェア的にスクロール機能が無いため、
表示内容を上にスクロールするには、全画面再表示をする必要があり、
描画速度が遅いのは致命的です。

遅さの原因を調べるためライブラリのソースを見ると、
文字表示、ビットマップ表示は1点毎にdrawPixel()関数で描画しているようです。
これはかなり効率の悪い処理です。1点毎にグラフィック液晶モジュールに
コマンドとデータの送信を行っています。

そこで処理を前回の画像表示で高速化に貢献したDMAを利用する方式を適用してみました。
なるべくコマンド送信回数を減らす作戦です。


実装する画面構成

  ・グラフィック画面解像度 320x240ドット
  ・文字表示 53x30文字
  ・フォントサイズ 6x8ドット(半角256文字、自作IchigoJam互換コード採用フォント )

動作テストとしては、画面いっぱいに文字を表示し(53x30文字)、その表示処理時間の
改善を図ります。

この処理が高速化されれば、スクロール表示処理も当然改善されます。
実際に、改善を施したビフォー&アフターは次のような感じです。
前半が改善前、後半が改善後です。


かなり改善出来た雰囲気が伝わると思います。
数値的には、53x30文字表示全表示に かかる時間は、

  改善前 808.099 ms
  改善後 35.149 ms
と大幅に処理速度が改善出来ました。
方式的には、1ライン(320ドット)用のバッファを用意し、1ライン毎にDMA転送を行いまいた。
1280バイト(320ドットx16ビットx2)ほどバッファ用のメモリを消費しますが、効果は絶大です。

さて、理論的に最大何処まで改善可能なのか見積もってみると、
今回のSPIの転送速度はクロックが36MHzなので、データ送信能力は
    36,000,000ビット/秒 
です。

一方、グラフィック画面の1画面のデータは、
    320x240x16ビット= 1,228,800 ビット
です。

1画面の更新に必要とする時間は、
   1,228,800 ビット ÷   36,000,000ビット/秒   = 0.034133333 秒 = 34.13 ms

理論的な最大パフォーマンスに近い値が出せていることが分かります。


検証用スケッチ
//
// BluePill(Arduino STM32) グラフィック液晶(ILI9341) フォント表示改善デモ
// 2018/07/09 by たま吉さん

#include <Adafruit_GFX_AS.h>      // Core graphics library
#include <Adafruit_ILI9341_STM.h> // Hardware-specific library
#include <font6x8tt.h>            // 6x8ドットフォント

// TFT制御用ピン
#define TFT_CS  PA0
#define TFT_RST PA1
#define TFT_DC  PA2

// TFT制御用
Adafruit_ILI9341_STM tft = Adafruit_ILI9341_STM(TFT_CS, TFT_DC, TFT_RST);

// フォント管理用
uint8_t* fontTop;
#define CH_W     6            // フォント横サイズ
#define CH_H     8            // フォント縦サイズ

// スクリーン管理用
#define SC_W     53           // キャラクタスクリーン横サイズ
#define SC_H     30           // キャラクタスクリーン縦サイズ
#define SCSIZE   (SC_W*SC_H)  // スクリーンサイズ
uint8_t screen[SCSIZE];       // スクリーンバッファ

// 指定座標に文字の表示
void drawChar(uint16_t x, uint16_t y, uint8_t c) {
  uint8_t* ptr = &fontTop[c*8];       // フォントデータ先頭アドレス
  for (uint8_t i=0; i <8; i++) {
    for (uint8_t j=0; j < 6; j++) {
      if ( (*ptr) & (0x80>>j)) {
        tft.drawPixel(x+j,y+i,ILI9341_WHITE);
      } else {
        tft.drawPixel(x+j,y+i,ILI9341_BLACK);
      }
    }
    ptr++;
  }
}

// 従来の画面更新表示
void sc_updateOld() {
  for (uint16_t y=0; y < SC_H; y++) {
    for (uint16_t x=0; x < SC_W; x++) {
      uint8_t c = screen[SC_W*y+x];  // キャラクタの取得
      drawChar(x*CH_W, y*CH_H, c);
    }
  }
}

// 指定位置の文字の更新表示
void sc_updateChar(uint16_t x, uint16_t y) {
  uint8_t c    = screen[SC_W*y+x];    // キャラクタの取得
  uint8_t* ptr = &fontTop[c*8];       // フォントデータ先頭アドレス
  tft.setAddrWindow(x*CH_W, y*CH_H, x*CH_W+CH_W-1, y*CH_H+CH_H-1);
  for (uint8_t i=0; i <8; i++) {
    for (uint8_t j=0; j < 6; j++) {
      if ( (*ptr) & (0x80>>j)) {
        tft.pushColor(ILI9341_WHITE);
        //SPI.write(ILI9341_WHITE);
      } else {
        tft.pushColor(ILI9341_BLACK);
        //SPI.write(ILI9341_BLACK);
      }
    }
    ptr++;
  }
}


// 指定行をTFT画面に反映
// 引数
//  ln:行番号(0~29)
void sc_updateLine(uint16_t ln) {
  uint8_t c;
  uint8_t dt;
  uint16_t buf[2][SC_W*CH_W];
  uint16_t index;

  for (uint16_t i=0; i < CH_H; i++) { // 1文字高さ分ループ
    index = 0;
    for (uint16_t clm = 0; clm < SC_W; clm++) { // 横文字数分ループ
      c  = screen[SC_W*ln+clm]; // キャラクタの取得
      dt = fontTop[c*8+i];      // 文字内i行データの取得
      for (uint16_t j=0; j < 6; j++) {
        if ( dt & (0x80>>j)) {
          buf[i&1][index] = ILI9341_WHITE;
        } else {
          buf[i&1][index] = ILI9341_BLACK;
        }
        index++;
      }
    }
    tft.pushColors(buf[i&1], SC_W*CH_W, 1);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  // TFTの初期化
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(ILI9341_BLACK);
  fontTop = (uint8_t*)font6x8tt+3;
  memset(screen, 0, SCSIZE);

  // ダミーデータのセット
  for (uint16_t i=0; i < SCSIZE; i++)
    screen[i] = i % 256;
}

void loop() {
  Serial.print("Hit any key to start");
  while(Serial.read() <0);
  Serial.println();

  // 従来方式
  uint32_t t1, t2;
  uint8_t c = 0;
  for (uint16_t i=0; i < 15; i++) {
    t1 = micros();
    sc_updateOld();
    t2 = micros();
    Serial.print("update Time =");
    Serial.println(t2-t1,DEC);
    memmove(&screen[0], &screen[SC_W*1], SCSIZE-SC_W);
    for(uint8_t j=0;j < SC_W; j++)
      screen[SC_W*29+j] = c++;
    //delay(10);
  }

  delay(3000);

  // 高速対応
  for (uint16_t i=0; i < 300; i++) {
    t1 = micros();
    tft.setAddrWindow(0, 0, SC_W*CH_W-1, 29*CH_H+CH_H-1);
    for (uint8_t y=0; y < 30; y++) {
       sc_updateLine(y);
    }    
    t2 = micros();
    Serial.print("update Time =");
    Serial.println(t2-t1,DEC);
    memmove(&screen[0], &screen[SC_W*1], SCSIZE-SC_W);
    for(uint8_t j=0;j < SC_W; j++)
      screen[SC_W*29+j] = c++;
    //delay(10);
  }
}

pushColors()関数の利用が今回の改善の立役者です。
内部的にDMA転送をバックグランドで行ってくれます。
setAddrWindow()、pushColors()、pushColor()関数は使い方によってかなり描画処理の
改善を行うことが出来ます。

いずれは、豊四季Tiny BASIC for Arduino STM32にこのテキスト描画を採用しようと思います。

2018年7月 4日 (水)

次はSTM32ボードを積極的に使ていきたい(25) グラフィック液晶(6)

Sdfatライブラリを使った画像ロード&表示時間の改善

前回からの続きです。

2018/07/07 内容を大幅修正しました

今回はTFT、SDカード、タッチスクリーンを同時利用しました。

04

テスト用スケッチの起動時間を短縮するために色々と試行錯誤してみました。
(テスト用スケッチは起動後、SDカードから猫画像をロードして表示し、お絵描きが出来ます)

改善策 その1 ・・・・ 2つのSPIバスの割り振りの最適化

TFTにSPI1、SDカード・タッチスクリーンにSPI2を割り当てた方が、
描画のパフォーマンスが向上します。

BluePillボードの仕様として
SPI1ではクロック上限が36MHz、、SPI2ではクロック上限が18MHzとなります。
TFTの表示制御ではSPI1 クロック 36MHzを利用するとパファーマンスが2倍に向上します。

単純な画面塗りつぶしの速度の比較
   fillScreen(ILI9341_BLUE);

  SPI1 システムクロック上限 36MHz設定時: 34.2 ms
  SPI2 システムクロック上限 18MHz設定時: 68.3 ms

一方、SDカードでは18MHz以上では、動作しないため、SPI1を使うメリットがありません。

そこで、グラフィック液晶モジュールのSPIバスの割り当て・結線は次のようにしました。

06



改善策 その2 SDライブりからSdfatライブラリへの変更


Sdfatライブラリは、DMAの利用が出来るため、SDライブラリからSdfatライブラリに
変更することで、パフォーマンスの改善が期待出来ます。

実際にどれくらい、パフォーマンスが改善するかを、確認してみました。
写真の猫画像 320x240ドット(24ビット色 230,454バイト)のロード&表示時間を比較してみました。

なお、Sdfatライブラリは下記から入手しました。
  ・greiman/SdFat -  Arduino FAT16/FAT32 Library
    https://github.com/greiman/SdFat

比較表

05_3
Adafruit_ILI9341_STMライブラリは、前回の不具合対応したものを利用しています。

SDライブラリから、SdFatライブラリに単純に置き換えると、
2,187msかかっていた表示が512msと劇的に改善しました。

また、SdFatのコンフィグレーション定義のSdFatConfig.hのうち、
使用するバッファ容量、拡張機能利用有無に関する設定をいじって性能と
メモリ消費について調べてみました。

変更操作をしたのは、下記の3つの設定です。
  1) ENABLE_EXTENDED_TRANSFER_CLASS
  2) USE_SEPARATE_FAT_CACHE
  3) USE_MULTI_BLOCK_IO

1)の設定を1にすると拡張版のSdfatExクラスを利用することが出来ます。
デフォルトでは無効になっています。
有効にすると、ノーマルの512msから440msに改善しました。
この設定において、SRAM消費量の増加は大したことないので、有効にした方が良いようです。

2)3)はキャッシュ、ブロック数に関する設定です。
デフォルトでは有効になっています。
無効にした場合、512msから520msと若干遅くなりましたが、
その分SRAMの使用量が528バイト減りました。
今回のデモアプリでは、この設定は、無効でも良いかもしれません。

次に、1)を有効にしつつ、2)3)を無効にした場合も測定してみました。
ノーマルの512msから451msと早くなり、SRAM使用量は256バイト増加となりました。
この設定がベストかもしれません。

最後の色のついた測定値は、画像ファイルと表示処理に修正したものです。
DMA転送を利用出来るよう、画像ファイルに16ビットカラーのビットマップ画像を利用しました。
読み込んだファイルは無加工で済むためDMAを使った転送が利用出来ます。
224msと更に改善出来ました。
16ビットカラーのビットマップ画像はWindows的にはサポートされていないので、
gimp2で変換して作成しました。
(標準のペイントツールやビュアーでも閲覧は可能です)

以上の測定から、
SDライブラリからSdfatライブラリへの変更はかなり有益であると思います。
プログラムサイズ、SRAMの使用量の増加もさほど大きくないです。
16ビットカラーのビットマップ画像の利用も効果ありです。

利用したスケッチ
Adafruit_ILI9341_STMライブラリのspitftbitmapとtouchpaintを元に作成しました。

/***************************************************
  This is our Bitmap drawing example for the Adafruit ILI9341 Breakout and Shield
  ----> http://www.adafruit.com/products/1651

  Check out the links above for our tutorials and wiring diagrams
  These displays use SPI to communicate, 4 or 5 pins are required to
  interface (RST is optional)
  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.
  MIT license, all text above must be included in any redistribution
 ****************************************************/

// SDカード用ライブラリの選択
#define USE_SDFAT 1 // 0:SDライブラリ利用, 1:SdFatライブラリ利用

// SdFatライブラリ利用時は下記の設定を好みで行う
// SdFatConfig.h:
//   ENABLE_EXTENDED_TRANSFER_CLASS (1の場合、SdFatEXを使う) 
//   USE_SEPARATE_FAT_CACHE
//   USE_MULTI_BLOCK_IO

#include <SPI.h>
#if USE_SDFAT == 1
  #include <SdFat.h>
  #if ENABLE_EXTENDED_TRANSFER_CLASS == 1
    SdFatEX  SD(2);
  #else
    SdFat  SD(2);  
  #endif
  #define SPI_SPEED SD_SCK_MHZ(18)
#else
  #include <SD.h>
#endif

#include <Adafruit_GFX_AS.h>      // Core graphics library
#include <Adafruit_ILI9341_STM.h> // Hardware-specific library
#include <XPT2046_touch.h>

// TFT display and SD card will share the hardware SPI interface.
// Hardware SPI pins are specific to the Arduino board type and
// cannot be remapped to alternate pins.  For Arduino Uno,
// Duemilanove, etc., pin 11 = MOSI, pin 12 = MISO, pin 13 = SCK.

// タッチスクリーン領域
#define TS_MINX 600
#define TS_MINY 440
#define TS_MAXX 3460
#define TS_MAXY 3460

// タッチスクリーンCSピン
#define TS_CS  PA3 

// TFT制御用ピン
#define TFT_CS  PA0
#define TFT_RST PA1
#define TFT_DC  PA2

SPIClass  SPI_2(2); // タッチスクリーンで共有

// タッチスクリーン制御用
XPT2046_touch ts(TS_CS, SPI_2); // Chip Select pin, SPI port

// TFT制御用
Adafruit_ILI9341_STM tft = Adafruit_ILI9341_STM(TFT_CS, TFT_DC, TFT_RST);
//SPISettings TFT_SPISet(SAFE_FREQ, MSBFIRST, SPI_MODE0, DATA_SIZE_16BIT);

// SDカード選択
#define SD_CS PB0

// Size of the color selection boxes and the paintbrush size
#define BOXSIZE 40
#define PENRADIUS 3
int oldcolor, currentcolor;

// システム情報の表示
void iinfo() {
  char top = 't';
  uint32_t adr = (uint32_t)&top;
  uint8_t* tmp = (uint8_t*)malloc(1);
  uint32_t hadr = (uint32_t)tmp;
  free(tmp);

  // スタック領域先頭アドレスの表示
  Serial.print("Stack Top:"); Serial.println(adr,HEX);
  
  // ヒープ領域先頭アドレスの表示
  Serial.print("Heap Top :"); Serial.println(hadr,HEX);

  // SRAM未使用領域の表示
  Serial.print("SRAM Free:"); Serial.println(adr-hadr,DEC);
}

void setup(void) {
  Serial.begin(115200);
  delay(1000);
  Serial.println((uint32_t)SPI.dev(),HEX);

#if USE_SDFAT == 1
  if (!SD.begin(SD_CS,SPI_SPEED)) {
#else
  if (!SD.begin(SD_CS)) {
#endif
    Serial.println("failed!");
  }

  ts.begin();
  tft.begin();  
  Serial.println((uint32_t)SPI.dev(),HEX);
  //Serial.println(micros(),DEC);
  tft.fillScreen(ILI9341_BLUE);
  //Serial.println(micros(),DEC);
  Serial.println("OK!");
  bmpDraw("CAT.BMP", 0, 0);
  iinfo();
  // make the color selection boxes
  //SPI_2.beginTransaction(TFT_SPISet);
  tft.fillRect(0, 0, BOXSIZE, BOXSIZE, ILI9341_RED);
  tft.fillRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, ILI9341_YELLOW);
  tft.fillRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, ILI9341_GREEN);
  tft.fillRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, ILI9341_CYAN);
  tft.fillRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, ILI9341_BLUE);
  tft.fillRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, ILI9341_MAGENTA);

  // select the current color 'red'
  tft.drawRect(0, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
  currentcolor = ILI9341_RED;
  //SPI_2.endTransaction();

}

void loop() {
  // Retrieve a point
  TS_Point p = ts.getPoint();

  if ( (p.z <=800) || (p.z > 3000))
    return;
   
 int16_t tmp = p.x;
  p.x = p.y;
  p.y = tmp;
  
  Serial.print("Z = "); Serial.print(p.z);
  Serial.print(" X = "); Serial.print(p.x);
  Serial.print(" Y = "); Serial.print(p.y);

  //SPI_2.beginTransaction(TFT_SPISet);

  // Scale from ~0 ~ TS_MAXX to tft.width using the calibration #'s
  p.x = tft.width() - map(p.x, TS_MINX, TS_MAXX, 0, tft.width());
  p.y = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());

  Serial.print("("); Serial.print(p.x);
  Serial.print(", "); Serial.print(p.y);
  Serial.println(")");

  if (p.y < BOXSIZE) {
    oldcolor = currentcolor;

    if (p.x < BOXSIZE) {
      currentcolor = ILI9341_RED;
      tft.drawRect(0, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    } else if (p.x < BOXSIZE * 2) {
      currentcolor = ILI9341_YELLOW;
      tft.drawRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    } else if (p.x < BOXSIZE * 3) {
      currentcolor = ILI9341_GREEN;
      tft.drawRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    } else if (p.x < BOXSIZE * 4) {
      currentcolor = ILI9341_CYAN;
      tft.drawRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    } else if (p.x < BOXSIZE * 5) {
      currentcolor = ILI9341_BLUE;
      tft.drawRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    } else if (p.x < BOXSIZE * 6) {
      currentcolor = ILI9341_MAGENTA;
      tft.drawRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, ILI9341_WHITE);
    }

    if (oldcolor != currentcolor) {
      if (oldcolor == ILI9341_RED)
        tft.fillRect(0, 0, BOXSIZE, BOXSIZE, ILI9341_RED);
      if (oldcolor == ILI9341_YELLOW)
        tft.fillRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, ILI9341_YELLOW);
      if (oldcolor == ILI9341_GREEN)
        tft.fillRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, ILI9341_GREEN);
      if (oldcolor == ILI9341_CYAN)
        tft.fillRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, ILI9341_CYAN);
      if (oldcolor == ILI9341_BLUE)
        tft.fillRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, ILI9341_BLUE);
      if (oldcolor == ILI9341_MAGENTA)
        tft.fillRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, ILI9341_MAGENTA);
    }
  }
  if (((p.y - PENRADIUS) > BOXSIZE) && ((p.y + PENRADIUS) < tft.height())) {
    tft.fillCircle(p.x, p.y, PENRADIUS, currentcolor);
  }
  //SPI_2.endTransaction();
}

// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates.  It's sped up
// by reading many pixels worth of data at a time
// (rather than pixel by pixel).  Increasing the buffer
// size takes more of the Arduino's precious RAM but
// makes loading a little faster.  20 pixels seems a
// good balance.

#define BUFFPIXEL 20

void bmpDraw(char *filename, uint8_t x, uint16_t y) {

  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  uint8_t  sdbuffer[3*BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip    = true;        // BMP is stored bottom-to-top
  int      w, h, row, col;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();

  if((x >= tft.width()) || (y >= tft.height())) return;

  Serial.println();
  Serial.print(F("Loading image '"));
  Serial.print(filename);
  Serial.println('\'');

  // Open requested file on SD card
  if ((bmpFile = SD.open(filename)) == NULL) {
    Serial.print(F("File not found"));
    return;
  }

  // Parse BMP header
  if(read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print(F("File size: ")); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageoffset = read32(bmpFile); // Start of image data
    Serial.print(F("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    Serial.print(F("Header size: ")); Serial.println(read32(bmpFile));
    bmpWidth  = read32(bmpFile);
    bmpHeight = read32(bmpFile);
    if(read16(bmpFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(bmpFile); // bits per pixel
      Serial.print(F("Bit Depth: ")); Serial.println(bmpDepth);
      if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print(F("Image size: "));
        Serial.print(bmpWidth);
        Serial.print('x');
        Serial.println(bmpHeight);

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * 3 + 3) & ~3;

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if(bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip      = false;
        }

        // Crop area to be loaded
        w = bmpWidth;
        h = bmpHeight;
        if((x+w-1) >= tft.width())  w = tft.width()  - x;
        if((y+h-1) >= tft.height()) h = tft.height() - y;

        // Set TFT address window to clipped image bounds
        tft.setAddrWindow(x, y, x+w-1, y+h-1);

        for (row=0; row<h; row++) { // For each scanline...
          // Seek to start of scan line.  It might seem labor-
          // intensive to be doing this on every line, but this
          // method covers a lot of gritty details like cropping
          // and scanline padding.  Also, the seek only takes
          // place if the file position actually needs to change
          // (avoids a lot of cluster math in SD library).
          if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
            pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
          else     // Bitmap is stored top-to-bottom
            pos = bmpImageoffset + row * rowSize;
          if(bmpFile.position() != pos) { // Need seek?
            bmpFile.seek(pos);
            buffidx = sizeof(sdbuffer); // Force buffer reload
          }

          for (col=0; col<w; col++) { // For each pixel...
            // Time to read more pixel data?
            if (buffidx >= sizeof(sdbuffer)) { // Indeed
              bmpFile.read(sdbuffer, sizeof(sdbuffer));
              buffidx = 0; // Set index to beginning
            }

            // Convert pixel from BMP to TFT format, push to display
            b = sdbuffer[buffidx++];
            g = sdbuffer[buffidx++];
            r = sdbuffer[buffidx++];
            tft.pushColor(tft.color565(r,g,b));                  
          } // end pixel
        } // end scanline
        Serial.print(F("Loaded in "));
        Serial.print(millis() - startTime);
        Serial.println(" ms");
      } // end goodBmp
    }
  }

  bmpFile.close();
  if(!goodBmp) Serial.println(F("BMP format not recognized."));
}

// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

uint16_t read16(File &f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(File &f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}

16ビットカラーのビットマップ画像対応ではbmpDraw()関数を次のように修正しています。

void bmpDraw(char *filename, uint8_t x, uint16_t y) {

  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  uint8_t  sdbuffer[320*2];       // pixel buffer (R5+G6+B5 per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip    = true;        // BMP is stored bottom-to-top
  int      w, h, row, col;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();

  if((x >= tft.width()) || (y >= tft.height())) return;

  Serial.println();
  Serial.print(F("Loading image '"));
  Serial.print(filename);
  Serial.println('\'');

  // Open requested file on SD card
  if ((bmpFile = SD.open(filename)) == NULL) {
    Serial.print(F("File not found"));
    return;
  }

  // Parse BMP header
  if(read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print(F("File size: ")); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageoffset = read32(bmpFile); // Start of image data
    Serial.print(F("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    Serial.print(F("Header size: ")); Serial.println(read32(bmpFile));
    bmpWidth  = read32(bmpFile);
    bmpHeight = read32(bmpFile);
    if(read16(bmpFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(bmpFile); // bits per pixel
      Serial.print(F("Bit Depth: ")); Serial.println(bmpDepth);
      //if((bmpDepth == 16) && (read32(bmpFile) == 0)) { // 0 = uncompressed
      if((bmpDepth == 16)) { // 0 = uncompressed
        read32(bmpFile);
        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print(F("Image size: "));
        Serial.print(bmpWidth);
        Serial.print('x');
        Serial.println(bmpHeight);

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * 2 + 2) & ~3;

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if(bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip      = false;
        }

        // Crop area to be loaded
        w = bmpWidth;
        h = bmpHeight;
        if((x+w-1) >= tft.width())  w = tft.width()  - x;
        if((y+h-1) >= tft.height()) h = tft.height() - y;

        // Set TFT address window to clipped image bounds
        tft.setAddrWindow(x, y, x+w-1, y+h-1);

        for (row=0; row<h; row++) { // For each scanline...
          if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
            pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
          else     // Bitmap is stored top-to-bottom
            pos = bmpImageoffset + row * rowSize;
          if(bmpFile.position() != pos) { // Need seek?
            bmpFile.seek(pos);
            buffidx = sizeof(sdbuffer); // Force buffer reload
          }

          bmpFile.read(sdbuffer, w*2);
          tft.pushColors((&sdbuffer[0]), w,1);
                              
        } // end scanline
        Serial.print(F("Loaded in "));
        Serial.print(millis() - startTime);
        Serial.println(" ms");
      } // end goodBmp
    }
  }

  bmpFile.close();
  if(!goodBmp) Serial.println(F("BMP format not recognized."));
}

描画の高速化の過程で、SDカードの初期化(begin())が失敗してしまうことに悩まされました。
特にノートPCにUSB接続して利用すると頻発しました。
GNDとVCC間にパスコンを入れたら安定しました。

さて、TFTの描画に関しての高速化については、もう少し何とかしたいと思うのですが、
Blue Pillボードを使った描画において、次のデモ動画を見つけました。


グラフィック液晶のインタフェースはSPIではなく8ビットパラレル接続のようです。
SDカードからデータを読み込みながら表示でここまで高速に表示できるんですね。

より以前の記事一覧