フォト
2025年4月
    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      
無料ブログはココログ

« 次はSTM32ボードを積極的に使ていきたい(24) グラフィック液晶(5) | トップページ | 次はSTM32ボードを積極的に使ていきたい(26) グラフィック液晶(7) »

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カードからデータを読み込みながら表示でここまで高速に表示できるんですね。

« 次はSTM32ボードを積極的に使ていきたい(24) グラフィック液晶(5) | トップページ | 次はSTM32ボードを積極的に使ていきたい(26) グラフィック液晶(7) »

arduino」カテゴリの記事

表示器制御関連」カテゴリの記事

ARM」カテゴリの記事

STM32」カテゴリの記事

コメント

コメントを書く

(ウェブ上には掲載しません)

トラックバック

« 次はSTM32ボードを積極的に使ていきたい(24) グラフィック液晶(5) | トップページ | 次はSTM32ボードを積極的に使ていきたい(26) グラフィック液晶(7) »