TFT_eSPIとAudioI2SでmediaPlayerを作る(3)

先ずはボタン

今回からLCDを使ったGUIの説明です。

画像データの作成

先ずは土台の画像と簡単なオンオフボタンを作って行きます。

  • 土台:
    • LCDの解像度(320x240)いっぱいにジュークボックス風の土台を作成しました。
    • 外側の余分な箇所をお絵描きソフトで切り取り透明扱いにします。
    • 中心ちょっと下に灰色の丸が3つ。これをボタンにします。
  • ボタン:
    • タッチした状態で赤。離すと灰色になるように画像は2枚用意します。
    • 土台からボタン部分を四角で切り取る。中心部を残し回りを切り取り透明扱にする。
    • ボタンにタッチした時にオン用の画像、離した時にオフ用の画像を土台に貼る。

画像表示

画像表示はPNG関係で説明している”配列に保存されたPNG画像データを表示する方法”を使います。

samp
 //
    xpos = 0;
    ypos = 0;
    rc = png.openFLASH((uint8_t *)xxxxxx, sizeof(xxxxxx), pngDraw);
    tft.startWrite();
    rc = png.decode(NULL, 0);
    tft.endWrite();


void pngDraw(PNGDRAW *pDraw) {
  uint16_t lineBuffer[MAX_IMAGE_WIDTH];
  png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
  tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer, tran_c);
}
  • 2,3行:  画像を表示する座標の指定
  • 4行:   xxxxxxが画像配列の名前。pngDrawは実行関数。
  • 5~7行:  お決まりにルーチン。
  • 10行: デコードの関数。
  • 13行: 最後の引数(tran_c)が透明色の指定です。

画像ファイルを配列に変換

下記はPNG形式の画像データを16進テキストファイルに変換するプログラムです。

change_txt.ino

#include "FS.h"
#include <SD_MMC.h>

//------------ SD_MMC 1-wire SD mode ----------------------------
#define mmc_CMD         21 
#define mmc_CLK         48  
#define mmc_D0          47  
#define SDMMC_FREQ   30000
#define MAX_FILE         3

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

    // Initialise the SD_MMC
    pinMode(mmc_D0, INPUT_PULLUP);
    SD_MMC.setPins(mmc_CLK, mmc_CMD, mmc_D0);
    if(!SD_MMC.begin("/sdmmc", true, false, SDMMC_FREQ, MAX_FILE)){
       Serial.println("Card Mount Failed");
       return;
    }
    else Serial.println("SD_MMC initialisation OK");
}

void loop() {
  change_txt("/xxxxxx");
  Serial.println("Comp!");
  while(1);

}

void change_txt(String str) {
  int a;
  String str1;
  uint8_t buf[10];

    str1= str;
    str += ".png";
    str1 += ".txt";

    File fp = SD_MMC.open(str, FILE_READ);
    File fp1 = SD_MMC.open(str1, FILE_WRITE);
    a = 0;
    sprintf((char*)buf,"0x%02x",fp.read());
    fp1.write(buf,4);
    while (fp.available()){
      sprintf((char*)buf,",0x%02x",fp.read());
      fp1.write(buf,5);
      a ++;
      if(a == 50){
        fp1.write('\n');
        a = 0;
      }
    }
    fp.close();
    fp1.close();
    Serial.println("OK");
}

プログラムの説明

  • 実行前にSDカードのルートにボタンが押された時の画像(赤丸の画像)を”btn_on.png”で保存
  • 25行の ”/xxxxxx” の箇所を”/btn_on”としてコンパイル、実行
  • SDカードに”btn_on.txt”という16進テキストファイルが作成されます。

テキストファイルの中身は以下の様になっています。

btn_on.txt

0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x21,0x00,0x00,0x00,0x1e,0x08,0x06,0x00,0x00,0x00,0xa2,0xc8,0x77,0x17,0x00,0x00,0x00,0x04,0x73,0x42,0x49,0x54,0x08,0x08,0x08,0x08,0x7c,0x08,0x64,0x88,0x00,0x00
,0x06,0x5c,0x49,0x44,0x41,0x54,0x48,0x89,0x9d,0x97,0x6b,0x88,0x5d,0x57,0x15,0xc7,0x7f,0x7b,0xed,0xbb,0xef,0x2b,0x73,0x7b,0xa7,0x37,0x33,0x93,0x4c,0x32,0x9d,0x50,0xec,0x83,0x32,0x2d,0x06,0xa3,0xc4,0x47,0x6b,0x45,0x09,0x14,0x1b,0x8b,0x8f,0x58,0x95,0xa2
,0x88,0xfd,0xa2,0x1f,0x04,0xf1,0xb3,0x58,0x5a,0x44,0xa9,0x45,0x51,0x41,0x11,0xa3,0x28,0x4a,0x10,0x23,0x7e,0xb0,0xda,0xb4,0x54,0x2c,0x31,0x95,0x2a,0x0a,0x05,0x1f,0x4d,0x74,0x4c,0x48,0x33,0x99,0xc9,0x63,0x26,0x93,0xce,0x23,0x33,0x73,0xcf,0x63,0xed,0xed
,0x87,0x73,0xce,0x7d,0xcd,0xa4,0x8d,0x2e,0x58,0x9c,0xc7,0x5e,0x67,0xff,0xd7,0xfe,0xaf,0xc7,0xde,0xc7,0x70,0x83,0xf2,0xc4,0x07,0x1e,0x0a,0xba,0xba,0x4e,0x43,0x2a,0x10,0x02,0xc4,0x09,0x12,0xa7,0x80,0x90,0xd8,0xcc,0xc6,0x07,0x4f,0x9b,0x12,0xcb,0xb5,0x3a
,0xdf,0x3a,0xfe,0x6b,0x73,0xa3,0x73,0xbf,0xa1,0xe1,0xd7,0x1e,0xfc,0x70,0x68,0xae ...........

これを使ってAriduinoで配列を定義します。定義は以下の様に行います。

static const uint8_t xxxx[] PROGMEM  = { };

xxxxが配列の名前です。下記は符号なし8ビットの配列xxxx[]を定義しています。この様に定義すると配列はプログラム領域に定義されます。データはサイズが大きいので通常の領域ではすぐにオーバーフローしてしまいます。

xxxxを btn_onとし、{ }の間に上記の “btn_on.txt” の中身をコピーすれば PNGファイルが配列 btn_on[] としてArduino に定義されます。

static const uint8_t btn_on[] PROGMEM  = {
  0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x......
}

PNG画像の表示

配列に保存されたPNG画像をLCDに表示するプログラムです。

gui0.ino

#include "FS.h"
#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h> 
#include <SD_MMC.h>
#include <LittleFS.h>

#include <PNGdec.h>
#include "pic.h"

//------------- TFT_eSPI ----------------------------------------
#define REPEAT_CAL      false
#define CALIBRATION_FILE "/TouchCalData1"
TFT_eSPI tft = TFT_eSPI();       
#define GFXFF           1

///------------- PNGdec ----------------------------------------
PNG png;
#define MAX_IMAGE_WIDTH 320 // Adjust for your images

int16_t xpos = 0;
int16_t ypos = 0;
int16_t tran_c = 0;

void setup()
{
    Serial.begin(115200);
    Serial.println("");

    // Initialise FS
    if (!LittleFS.begin()) {
      Serial.println("LittleFS initialisation failed!");
      while(1) yield(); 
    }
    else Serial.println("LittleFS OK");

    // Initialise TFT_eSPI 
    tft.init();
    tft.setRotation(1);
    touch_calibrate();
    tft.fillScreen(TFT_BROWN);
}

void loop()
{
  int16_t rc;

    xpos = 0;
    ypos = 0;
    rc = png.openFLASH((uint8_t *)skin, sizeof(skin), pngDraw);
    tft.startWrite();
    rc = png.decode(NULL, 0);
    tft.endWrite();

    xpos = 100;
    ypos = 80;
    rc = png.openFLASH((uint8_t *)btn_on, sizeof(btn_on), pngDraw);
    tft.startWrite();
    rc = png.decode(NULL, 0);
    tft.endWrite();

    xpos = 200;
    ypos = 80;
    rc = png.openFLASH((uint8_t *)btn_off, sizeof(btn_off), pngDraw);
    tft.startWrite();
    rc = png.decode(NULL, 0);
    tft.endWrite();
   
    Serial.println("OK");
    while(1) ;

}

//------------------------------------------------------------
// Here are the callback functions that the decPNG library
// will use to open files, fetch data and close the file.
//------------------------------------------------------------

void pngDraw(PNGDRAW *pDraw) {
  uint16_t lineBuffer[MAX_IMAGE_WIDTH];
  png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
  tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer, tran_c);
}

//---------------------------------------------------
//              LCD
//---------------------------------------------------
void touch_calibrate(){
  uint16_t calData[5];
  uint8_t calDataOK = 0;

  // check if calibration file exists and size is correct
  if (LittleFS.exists(CALIBRATION_FILE)) {
    if (REPEAT_CAL){
      // Delete if we want to re-calibrate
      LittleFS.remove(CALIBRATION_FILE);
    }
    else {
      File f = LittleFS.open(CALIBRATION_FILE, "r");
      if (f) {
        if (f.readBytes((char *)calData, 14) == 14)
          calDataOK = 1;
        f.close();
      }
    }
  }

  if (calDataOK && !REPEAT_CAL) {
    // calibration data valid
    tft.setTouch(calData);
  } 
  else {
    // data not valid so recalibrate
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(20, 0);
    tft.setTextFont(2);
    tft.setTextSize(1);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);

    tft.println("Touch corners as indicated");

    tft.setTextFont(1);
    tft.println();

    if (REPEAT_CAL) {
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.println("Set REPEAT_CAL to false to stop this running again!");
    }

    tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15);

    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.println("Calibration complete!");

    // store data
    File f = LittleFS.open(CALIBRATION_FILE, "w");
    if (f) {
      f.write((const unsigned char *)calData, 14);
      f.close();
    }
  }
}
  • 8行:   PNG画像処理用のヘッダーの読み込み
  • 9行:   土台を skin[], ボタンのオン・オフを btn_on[]、btn_off[] と配列定義し、”pic.h”というファイルに保存。
          その読み込み
  • 18〜23行: PNGdec関係のパラメタ設定。
    • xops/ypos : 画像の表示位置。
    • tran_c   : 透明色の指定。画像を切り取った透明部分の色は”0″の様です。
  • 42行:   LCD全面を茶色で塗りつぶし
  • 48~53行: 座標(0,0)にskin[]を表示
  • 55~60行: 座標(100,0)にbtn_on[]を表示
  • 62~67行: 座標(200,0)にbtn_off[]を表示

コンパイル実行するとLCDが以下の様になります

ちょっと余談

このスケッチですが、配列のポインタとx,y座標を引数にした関数 void load_flash(const uint8_t* buf, int16_t x, int16_t y)を新たに作って

gui0a.ino

void loop()
{
    load_flash(skin, 0, 0);
    load_flash(btn_on, 100, 80);
    load_flash(btn_off, 200, 80);

    Serial.println("OK");
    while(1) ;

}

void load_flash(const uint8_t* buf, int16_t x, int16_t y){
  int16_t rc;

    xpos = x;
    ypos = y;
    rc = png.openFLASH((uint8_t *)buf, sizeof(buf), pngDraw);
    tft.startWrite();
    rc = png.decode(NULL, 0);
    tft.endWrite();

}

とした方がスマートに見えますが、このスケッチでは画像が表示されません。理由が分からなかったのですが、

    rc = png.openFLASH((uint8_t *)buf, sizeof(buf), pngDraw);

に原因が有る事が分かりました。下記は配列 skin[] を、上段は直接配列のポインタで指定した場合、下段は引数で指定した場合のpng.openFLASH()の例です。

    rc = png.openFLASH((uint8_t *)skin, sizeof(skin), pngDraw);
    rc = png.openFLASH((uint8_t *)buf, sizeof(buf), pngDraw);

最初の引数、(uint8_t *)skin と (uint8_t *)buf は同じ値(配列のポインタ)ですが、次の sizeof(skin)と sizeof(buf)の値が違う事が分かりました。sizeof()は引数のサイズを返す関数で、sizeof(skin)は配列skin[]のサイズが、sizeof(buf)はポインタbufのサイズが帰って来るのです。プログラマーは sizeof(buf)でポインタbufが示す配列のサイズを返して欲しいのですが、コンパイラはポインタbuf自身のサイズ(ポインタのサイズは”4”でした)を返すのです。対策を探したのですが見つからず、直接配列の名前を引数に使うしか無い様です。

ボタンの作成

GUIボタンを元に下記のスケッチを書きました。

gui1.ino

#include "FS.h"
#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h> 
#include <SD_MMC.h>
#include <LittleFS.h>

#include <PNGdec.h>
#include "pic.h"

//------------- TFT_eSPI ----------------------------------------
#define REPEAT_CAL      false
#define CALIBRATION_FILE "/TouchCalData1"
TFT_eSPI tft = TFT_eSPI();       
#define GFXFF           1

///------------- PNGdec ----------------------------------------
PNG png;
#define MAX_IMAGE_WIDTH 320 // Adjust for your images

int16_t xpos = 0;
int16_t ypos = 0;
int16_t tran_c = 0;

//------------- Button parameter ----------------------------------------
#define btn0_x        82
#define btn0_y        172
#define btn1_x        145
#define btn1_y        150
#define btn2_x        206
#define btn2_y        172
#define btn_w         33

int btn_area[][4] = { 
      btn0_x, btn0_y, btn0_x + btn_w, btn0_y + btn_w,
      btn1_x, btn1_y, btn1_x + btn_w, btn1_y + btn_w,
      btn2_x, btn2_y, btn2_x + btn_w, btn2_y + btn_w
     };

bool btn_satate[3][2];
//btn_satate[][0]: currstate
//btn_satate[][1]: laststate

void setup()
{
    Serial.begin(115200);
    Serial.println("");

    // Initialise FS
    if (!LittleFS.begin()) {
      Serial.println("LittleFS initialisation failed!");
      while(1) yield(); 
    }
    else Serial.println("LittleFS OK");

    // Initialise TFT_eSPI 
    tft.init();
    tft.setRotation(1);
    touch_calibrate();
    tft.fillScreen(TFT_BROWN);
}

void loop()
{
  uint16_t t_x, t_y;
  bool pressed, flg_temp;
  int a,b,c;
  
  flash_png(-1, false);
  for(a = 0; a < 3; a++)
    for(b = 0; b < 2; b ++)
      btn_satate[a][b] = false;

  while(1){
    pressed = tft.getTouch(&t_x, &t_y);
    for (a = 0; a < 3; a ++) {
      flg_temp = false;
      if(pressed && btn_contains(a, t_x, t_y))     // tell the button it is pressed
        flg_temp = true;
      btn_press(a, flg_temp);          
    }

    for (a = 0; a < 3; a ++) {
      if(btn_justReleased(a)){
        flash_png(a, false);
      }

      if(btn_justPressed(a)){
        flash_png(a, true);
      }
    }
  }
}

//------------------------------------------------------------
// Here are the callback functions that the decPNG library
// will use to open files, fetch data and close the file.
//------------------------------------------------------------

void pngDraw(PNGDRAW *pDraw) {
  uint16_t lineBuffer[MAX_IMAGE_WIDTH];
  png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
  tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, lineBuffer, tran_c);
}

void flash_png(int b_no, bool flg){
  int16_t rc;

  switch(b_no){
    case -1:  rc = png.openFLASH((uint8_t *)skin, sizeof(skin), pngDraw);
              break;
    case 0:
    case 1:
    case 2:   rc = png.openFLASH((uint8_t *)btn_off, sizeof(btn_off), pngDraw);
              if(flg) rc = png.openFLASH((uint8_t *)btn_on, sizeof(btn_on), pngDraw);
              break;
  }

  if(b_no == -1) xpos = ypos = 0;
  else{
    xpos = btn_area[b_no][0];
    ypos = btn_area[b_no][1];
  }
  tft.startWrite();
  rc = png.decode(NULL, 0);
  tft.endWrite();
}

bool btn_isPressed(int a)    { return (btn_satate[a][0]); }
bool btn_justPressed(int a)  { return (btn_satate[a][0] && !btn_satate[a][1]); }
bool btn_justReleased(int a) { return (!btn_satate[a][0] && btn_satate[a][1]); }

bool btn_contains(int a, int16_t x, int16_t y) {
  return ((x >= btn_area[a][0]) && (x < btn_area[a][2]) &&
          (y >= btn_area[a][1]) && (y < btn_area[a][3]));
}

void btn_press(int b_no, bool stat){
  btn_satate[b_no][1] = btn_satate[b_no][0];
  btn_satate[b_no][0] = stat;
}

//---------------------------------------------------
//              LCD
//---------------------------------------------------
void touch_calibrate(){
  uint16_t calData[5];
  uint8_t calDataOK = 0;

  // check if calibration file exists and size is correct
  if (LittleFS.exists(CALIBRATION_FILE)) {
    if (REPEAT_CAL){
      // Delete if we want to re-calibrate
      LittleFS.remove(CALIBRATION_FILE);
    }
    else {
      File f = LittleFS.open(CALIBRATION_FILE, "r");
      if (f) {
        if (f.readBytes((char *)calData, 14) == 14)
          calDataOK = 1;
        f.close();
      }
    }
  }

  if (calDataOK && !REPEAT_CAL) {
    // calibration data valid
    tft.setTouch(calData);
  } 
  else {
    // data not valid so recalibrate
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(20, 0);
    tft.setTextFont(2);
    tft.setTextSize(1);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);

    tft.println("Touch corners as indicated");

    tft.setTextFont(1);
    tft.println();

    if (REPEAT_CAL) {
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.println("Set REPEAT_CAL to false to stop this running again!");
    }

    tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15);

    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.println("Calibration complete!");

    // store data
    File f = LittleFS.open(CALIBRATION_FILE, "w");
    if (f) {
      f.write((const unsigned char *)calData, 14);
      f.close();
    }
  }
}

  • 34行: int btn_area[][4] = {
    • ここでボタンの範囲を指定しています。ボタンの数は3個。
    • ボタンは丸ですが、範囲は下記の様に四角。(丸の指定は難しい)
    • LCDにタッチした時にその座標がこの範囲に入っていればボタンが押されたと判断
    • この範囲はボタン オンオフ画像と同じで 左上の座標は画像を読み込む時の基準になっている。
  • 40行: bool btn_satate[3][2];
    • 各ボタンの状態を示す配列。btn_satate[][0]が最新。btn_satate[][1]が一つ前の状態
    • ボタンが押されていれば、true。押されていなければ falseをセット。
  • 69行:土台の画像を表示。
  • 70行: ボタンの状態を全て falseにセット(初期状態)
  • 74行から:  画面のタッチ、ボタンの状況、その処理 ループ
    • 75行:  画面のタッチを確認
    • 76~81: タッチされた座標がボタンの中か確認して状態を保存
    • 83~91行:各ボタンに対して
      • 84行: 離された直後か確認
      • 85行: 直後であれば、オフの画像を描写
      • 88行: 押された直後か確認
      • 89行: 直後であれば、オンの画像を描写

スケッチ(画像用配列を含む)をここに添付します。

コンパイル、実行するとジュークボックスが表示されます。

3つのボタンで、ボタンにタッチすると中心が赤く、離すと中心が灰色に戻ります。

実際のプログラムでは

  • ボタンを押した直後に処理したい場合: スケッチの89行以降
  • ボタンを離した直後に処理したい場合: スケッチの85行以降

に処理したい内容を書く事になります。

次回は

今回はボタン3個のGUIでしたが、次回は同じ要領で他のGUIを追加して行きます。