M5StickCのDisplay周り解析 その3 描画ノウハウ

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

前回はざっくりとスプライトの説明をしました。今回はスプライトの使い方を研究してみました。

画像について

M5StickCではTFT_eSPIというライブラリが内部で使われています。このライブラリで標準的にサポートしているは16ビットカラーの画像です。RGB565と呼ばれる形式で、古くからある形式ですが最近だとUnityとかでも使われている形式です。

一般的なツールだと保存できないので、変換ツールを作ってみました。

このブログの上のメニューのToolsから「画像データ変換」のリンクでもいけると思います。

まずは必要な画像を準備します。今回のサンプルは「いらすとや」さんの猫の画像を使わせてもらいました。

ちょっとM5StickCで表示するのには大きいので、まわりの白い余白を削り、大きさも縮小しました。

この画像を、先程のツールで変換してみたいと思います。画像ファイルを選択して、データ名の設定とデータ形式を選択するだけで変換可能です。

データ名はC言語の変数名になります。通常はRGB565のフォーマットで問題ないと思います。送信すると下にテキストエリアが表示され、そこに変換された画像データが表示されます。

この画像は左端が白ですので、データの最初も白の0xFFFFになっています。データ形式はBMPとJpeg、PNGに対応していますが通常はPNGあたりで編集するのが無難だと思います。

通常描画(M5_Image_BMP16bit)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)
}

void loop() {
  // 描画開始(明示的に宣言すると早くなる)
  M5.Lcd.startWrite();

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  M5.Lcd.pushImage(x, y, imgWidth, imgHeight, img);

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

  // 文字表示
  char str[256];
  sprintf(str, "M5ImageRGB565検証%3d", fps);
  printEfont(str, 0, 0);

  // 描画終了
  M5.Lcd.endWrite();

  // Wait
  delay(1);
}

画像をランダムに表示するサンプルです。

日本語フォントが利用できる状態で、右上が秒間描画回数です。

スケッチの構造としては、メインのスケッチの他にdata.hという画像用のファイルを読み込んでいます。

上記のページに使われるスケッチはすべて保存してあるので、参考にしてください。

日本語フォントを利用しているので、あらかじめArduino IDEのライブラリマネージャーよりefontをインストールしておいてください。

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

上記が基本的なM5StickCの初期化と、日本語フォントを使うための準備です。efontEnableJaMini.hを指定しているので、第2水準相当の日本語フォントが利用できます。efontEnableAll.hを読み込むと中国語、韓国語も含めた文字が使えるようになります。

上記にefontの利用方法がまとまっています。

// イメージデータ
#include "data.h"                   // 画像データの読み込み

ここで画像データを読み込んでいます。複数画像データがある場合には、同じファイルに入れることもできますが、わかりにくいのでなるべく別ファイルに分割したほうがよいと思います。

画像データもそれなりに容量が大きいので、たくさん入れすぎるとフラッシュの容量が足りなくなってしまうと思います。

M5StickCは4MBのフラッシュメモリを内蔵していますので、ストレージの割当を変えることで標準の1.3MBのアプリ容量を増やすことができます。

上記に変更の仕方を説明していますが、2MBまではメニューで選択するだけで拡張可能です。自分で設定ファイルを編集することで3MBまで拡張できます。

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

1秒間に何回表示できたかのFPS(frame per second)を計算するための変数です。処理にはあまり関係ありません。

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)
}

初期化関数です。setSwapBytes()の設定をしていますが、画像を読み込むときにはtrueに設定してください。falseの場合には色がおかしな状態で表示されると思います。

void loop() {
  // 描画開始(明示的に宣言すると早くなる)
  M5.Lcd.startWrite();

描画処理の頭の部分です。M5.Lcd.startWrite()関数を呼び出すことで、LCDで利用しているSPIの利用を専有します。この開始がない場合には、各関数の中で開始と完了を自動で呼び出します。頭で呼び出すことで、複数回関数を呼び出した場合に若干高速化します。

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  M5.Lcd.pushImage(x, y, imgWidth, imgHeight, img);

画像描画部分です。pushImage()関数で座標と画像を指定して描画します。

  M5.Lcd.pushImage(x, y, imgWidth, imgHeight, img, WHITE);

最後に透過色の指定ができます。この画像の場合には白(WHITE, 0xFFFF)を指定することで、背景無しの猫だけが描画されます。

肉眼だともっとしっかり見えますが、上記のように猫だけが描画されています。ただ、白などの背景は画像の中に含まれている場合があるので、ピンクやマゼンタなどの通常利用しない色を背景色に指定したほうが安全だとは思います。

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

FPSの計算をしています。描画回数をカウントしていって、秒が変わったところで保存しています。

  // 文字表示
  char str[256];
  sprintf(str, "M5ImageRGB565検証%3d", fps);
  printEfont(str, 0, 0);

文字を描画しています。printEfont()関数で日本語対応の文字が可能ですが、printf()関数が使えないので、事前にsprintf()関数で文字列を用意しています。

  // 描画終了
  M5.Lcd.endWrite();

ここで描画完了のため完了を呼びます。この処理ではあまり描画物が多くないので開始と完了は呼ばなくても処理時間はそれほど変わらないと思います。

  // Wait
  delay(1);

個人的には省電力のためloop()関数の最後でdelay()関数を呼び出すことを推奨します。1以上の数値で意味があるので、最小値の1を設定しています。

こちらのスケッチは、画像変換ページを使えば他の画像にすぐに変更が可能です。ぜひ変更してみて試してもらいたいと思います。

さて、このスケッチですが、若干文字のところがチラつくと思います。これでは描画を実行するたびにLCDにデータを転送しています。そのため文字の下に画像を描画し、その後に文字を描画となるタイミングでチラついています。

次の例ではより極端な例になります。

画像移動(M5_ImageMove_BMP16bit)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

// 描画座標
int x = 0;              // X座標
int y = 0;              // Y座標
int xd = 4;             // X座標移動量
int yd = 3;             // Y座標移動量

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)
}

void loop() {
  // 描画開始(明示的に宣言すると早くなる)
  M5.Lcd.startWrite();

  // 黒で塗りつぶし
  M5.Lcd.fillScreen(BLACK);

  // 画像を移動して描画
  x += xd;
  if ( x <= 0) {
    x = 0;
    xd = 4;
  } else if ( M5.Lcd.width() <= x ) {
    x = M5.Lcd.width();
    xd = -4;
  }
  y += yd;
  if ( y <= 0) {
    y = 0;
    yd = 3;
  } else if ( M5.Lcd.height() <= y ) {
    y = M5.Lcd.height();
    yd = -3;
  }
  M5.Lcd.pushImage(x, y, imgWidth, imgHeight, img, WHITE);

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

  // 文字表示
  char str[256];
  sprintf(str, "M5ImageMove検証  %3d", fps);
  printEfont(str, 0, 0);

  // 描画終了
  M5.Lcd.endWrite();

  // Wait
  delay(100);
}

最初に黒で塗りつぶしたあとに画像を移動させるスケッチです。

実行してみればわかるのですが、非常にチラつくとおもいます。画面を黒で塗りつぶしたのが見えて、その後に画像と文字が描画されています。

このように直接M5.Lcdに描画するとどうしてもチラついてしまいます。

ダブルバッファ(M5_DB_Image_BMP16bit)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// 画面ダブルバッファ用スプライト
TFT_eSprite canvas = TFT_eSprite(&M5.Lcd);

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)

  // 画面ダブルバッファ用スプライト作成
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
  canvas.setSwapBytes(false);
}

void loop() {
  // 描画開始(明示的に宣言すると早くなる)
  M5.Lcd.startWrite();

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  canvas.pushImage(x, y, imgWidth, imgHeight, img);

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

  // 文字表示
  char str[256];
  sprintf(str, "M5DBImgRGB565検証%3d", fps);
  printEfont(&canvas, str, 0, 0);

  // 描画
  canvas.pushSprite(0, 0);

  // 描画終了
  M5.Lcd.endWrite();

  // Wait
  delay(1);
}

ダブルバッファと呼ばれる描画用のキャンバスを作成し、そこに描画したあとにLCDに一括描画するスケッチになります。

  // 画面ダブルバッファ用スプライト作成
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
  canvas.setSwapBytes(false);

スプライトを使うことで、画面ダブルバッファ用のキャンバスを作成しています。setSwapBytes(false)で実行していますが、ライブラリが間違っていて本来はtrueを指定すべきみたいです。今後バージョンアップしたらtrueを指定しないと色がおかしくなる可能性があります。色がおかしかったらsetSwapBytes()の設定を変更してみてください。

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  canvas.pushImage(x, y, imgWidth, imgHeight, img);

描画先はcanvasになります。

  // 文字表示
  char str[256];
  sprintf(str, "M5DBImgRGB565検証%3d", fps);
  printEfont(&canvas, str, 0, 0);

文字を描画する先もキャンバスを指定して描画しています。

  // 描画
  canvas.pushSprite(0, 0);

最後にLCDに一括転送をします。実際にLCDに転送するのはこの関数だけですので、startWrite()関数とendWrite()関数は実際には呼び出す必要はないと思います。

このスケッチはランダム描画のサンプルですが、最初の通常描画と比べるとチラつきがなく、しかもFPSも上がっています。M5StickCの場合には、ダブルバッファが非常に有効です。M5Stackなどは画面が大きいのでダブルバッファを確保するとメモリが厳しいですが、M5StickCの場合には画面がそれほど大きくはないので、なるべくならばダブルバッファを利用したほうがきれいな描画になると思います。

ダブルバッファ回転(M5_DB_ImageSpriteRotated_BMP16bit)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// 画面ダブルバッファ用スプライト
TFT_eSprite canvas = TFT_eSprite(&M5.Lcd);

// 画像用スプライト
TFT_eSprite sprite = TFT_eSprite(&M5.Lcd);

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)

  // 画面ダブルバッファ用スプライト作成
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
  canvas.setSwapBytes(false);

  // 画像用スプライト作成
  sprite.createSprite(imgWidth, imgHeight);
  sprite.setSwapBytes(false);
  sprite.pushImage(0, 0, imgWidth, imgHeight, img);
}

void loop() {
  // 描画開始(明示的に宣言すると早くなる)
  M5.Lcd.startWrite();

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  int angle = random(360);
  canvas.setPivot(x, y);
  sprite.pushRotated(&canvas, angle);

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

  // 文字表示
  char str[256];
  sprintf(str, "M5DBImgRot565検証%3d", fps);
  printEfont(&canvas, str, 0, 0);

  // 描画
  canvas.pushSprite(0, 0);

  // 描画終了
  M5.Lcd.endWrite();

  // Wait
  delay(1);
}

ランダムに回転する画像を描画するサンプルです。画像データのままだと回転できないので、画像用のスプライトを作っています。

  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  int angle = random(360);
  canvas.setPivot(x, y);
  sprite.pushRotated(&canvas, angle);

回転しながら描画している部分です。文字などを90度回転させて表示する場合などには便利だと思います。

FPSは回転をしているため、100前後だったものが65ぐらいまで落ちてしまっています。標準ライブラリでの回転は非常に重い処理になると思いますので、注意してください。

高速描画(M5_ImageFast)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)

  // 描画開始(SPIを専有)
  M5.Lcd.startWrite();
}

void loop() {
  // 画像をランダムに表示
  int x = random(M5.Lcd.width());
  int y = random(M5.Lcd.height());
  M5.Lcd.pushImage(x, y, imgWidth, imgHeight, img);

  // FPS更新
  ++frame_count;
  sec = millis() / 1000;
  if (psec != sec) {
    psec = sec;
    fps = frame_count;
    frame_count = 0;
  }

  // 文字表示
  char str[256];
  sprintf(str, "M5ImageFast検証  %3d", fps);
  printEfont(str, 0, 0);
}

今回のランダム描画で最速になるようにしたスケッチです。

  // 描画開始(SPIを専有)
  M5.Lcd.startWrite();

setup()関数の中でstartWrite()関数を呼び出しています。実際のところSPIを使うのはLCDぐらいですので、setup()関数の中で専有しっぱなしでも通常は問題ありません。

あとはloop()関数の最後にあるdelay(1)を呼び出していません。このロジックではここまでの変更をしてFPSが110ぐらいと10%程度しか早くなりません。

ダブルバッファを使って、普通に描画するほうがいいとは思います。

30フレーム描画(M5_DB_30frame)

#include <M5StickC.h>               // M5StickCの読み込み

// フォント関連
#include <efontEnableJaMini.h>      // 第2水準相当の日本語フォントを読み込み 4千文字139KB
#include <efont.h>                  // 実際のフォントデータの読み込み
#include "efontM5StickC.h"          // efontテキスト描画関数

// イメージデータ
#include "data.h"                   // 画像データの読み込み

// 画面ダブルバッファ用スプライト
TFT_eSprite canvas = TFT_eSprite(&M5.Lcd);

// FPS計算
static uint32_t sec;
static uint32_t psec;
static size_t fps = 0;
static size_t frame_count = 0;

// タイマー
hw_timer_t * timer = NULL;

// 画面描画タスクハンドル
TaskHandle_t taskHandle;

// 画面描画タスク
void dispTask(void *pvParameters) {
  int x = 0;              // X座標
  int y = 0;              // Y座標
  int xd = 4;             // X座標移動量
  int yd = 3;             // Y座標移動量

  while (1) {
    // タイマーが発生するまで待つ
    xTaskNotifyWait(0, 0, NULL, portMAX_DELAY);

    // まずは前フレームを描画してから次フレームを作る
    canvas.pushSprite(0, 0);

    // 黒で塗りつぶし
    canvas.fillSprite(BLACK);

    // 画像を移動して描画
    x += xd;
    if ( x <= 0) {
      x = 0;
      xd = 4;
    } else if ( M5.Lcd.width() <= x ) {
      x = M5.Lcd.width();
      xd = -4;
    }
    y += yd;
    if ( y <= 0) {
      y = 0;
      yd = 3;
    } else if ( M5.Lcd.height() <= y ) {
      y = M5.Lcd.height();
      yd = -3;
    }
    canvas.pushImage(x, y, imgWidth, imgHeight, img);

    // FPS更新
    ++frame_count;
    sec = millis() / 1000;
    if (psec != sec) {
      psec = sec;
      fps = frame_count;
      frame_count = 0;
    }

    // 文字表示
    char str[256];
    sprintf(str, "M5 30フレーム検証%3d", fps);
    printEfont(&canvas, str, 0, 0);
  }
}

// タイマー割り込み
void IRAM_ATTR onTimer() {
  xTaskNotifyFromISR(taskHandle, 0, eIncrement, NULL);
}

void setup() {
  M5.begin();
  M5.Axp.ScreenBreath(12);          // 7-12で明るさ設定
  M5.Lcd.setRotation(3);            // 0-3で画面の向き
  M5.Lcd.setSwapBytes(true);        // スワップON(色がおかしい場合には変更する)

  // 画面ダブルバッファ用スプライト作成
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
  canvas.setSwapBytes(false);

  // タイマー作成(33.333ms)
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 33333, true);
  timerAlarmEnable(timer);

  // 描画用タスク作成
  xTaskCreateUniversal(
    dispTask,
    "dispTask",
    8192,
    NULL,
    1,
    &taskHandle,
    APP_CPU_NUM
  );
}

void loop() {
  // Wait
  delay(1);
}

タイマーを使って、33.333ミリ秒間隔で画面描画をするサンプルです。タイマーと通知を使って実現しています。

上記の記事にて解説していますので、気になる人は見てみてください。

FPS比較

スケッチFPS
M5_Image_BMP16bit96
M5_DB_Image_BMP16bit100
M5_DB_ImageSpriteRotated_BMP16bit65
M5_ImageFast111
M5_DB_ImageFast105

画像を回転するとものすごく遅くなっています。通常よりダブルバッファの方が早いので、メモリに余裕がある場合にはダブルバッファで描画をしたほうが良さそうです。

Fastとついているのがdelay()やSPI確保をしたバージョンです。それほど性能向上はしないようです。

まとめ

描画まわりを調査してみました。思ったよりM5StickCは高速描画できる気がします。やはり画面サイズに比例するので、小さい分早いですしダブルバッファのメモリ量などもそれほど気になりません。

ダブルバッファは全画面分を確保する必要はなく、描画が更新される範囲分だけ確保するなどの最適化が可能です。転送サイズを減らした分だけ高速になるので、いろいろ試してみてください。

上記などに関数リファレンスがありますので、細かい関数は上記を参考にしてください。

次回は、高速描画に対応したライブラリの紹介をしたいと思います。

続編

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。