次はSTM32ボードを積極的に使ていきたい(25) グラフィック液晶(6)
Sdfatライブラリを使った画像ロード&表示時間の改善
前回からの続きです。
2018/07/07 内容を大幅修正しました
今回はTFT、SDカード、タッチスクリーンを同時利用しました。
テスト用スケッチの起動時間を短縮するために色々と試行錯誤してみました。
(テスト用スケッチは起動後、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バスの割り当て・結線は次のようにしました。
改善策 その2 SDライブりからSdfatライブラリへの変更
Sdfatライブラリは、DMAの利用が出来るため、SDライブラリからSdfatライブラリに
変更することで、パフォーマンスの改善が期待出来ます。
実際にどれくらい、パフォーマンスが改善するかを、確認してみました。
写真の猫画像 320x240ドット(24ビット色 230,454バイト)のロード&表示時間を比較してみました。
なお、Sdfatライブラリは下記から入手しました。
・greiman/SdFat - Arduino FAT16/FAT32 Library
https://github.com/greiman/SdFat
比較表
※Adafruit_ILI9341_STMライブラリは、前回の不具合対応したものを利用しています。
SDライブラリから、SdFatライブラリに単純に置き換えると、
2,187msかかっていた表示が512msと劇的に改善しました。
また、SdFatのコンフィグレーション定義のSdFatConfig.hのうち、
使用するバッファ容量、拡張機能利用有無に関する設定をいじって性能と
メモリ消費について調べてみました。
1) ENABLE_EXTENDED_TRANSFER_CLASS
デフォルトでは無効になっています。
有効にすると、ノーマルの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)⊤ 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」カテゴリの記事
- Arduino IDE+Arduino STM32環境で指定と異なるgccが使われてしまう(2025.01.23)
- Zorin OSでArduino Uno互換機(CH340)が認識しない(2025.01.19)
- Arduino IDE 2.3.4でArduino STM32を利用する(2025.01.12)
- Arduino用 SKK日本語変換ライブラリの開発 その1(2024.12.28)
- NeoPixel(WS2812B)の制御 その5(2024.09.15)
「表示器制御関連」カテゴリの記事
- NeoPixel(WS2812B)の制御 その5(2024.09.15)
- Arduino用 美咲フォントライブラリを更新しました(2024.03.21)
- Raspberry Pi Pico(MicroPython)でLEDドットマトリックスを使ってみる(2024.03.14)
- Raspberry Pi Pico MicroPython用のマルチフォントライブラリ(2023.02.09)
- MicroPython(Raspberry Pi pico)で8x8ドットNeoPixcel文字表示(2023.02.08)
「ARM」カテゴリの記事
- Arduino IDE+Arduino STM32環境で指定と異なるgccが使われてしまう(2025.01.23)
- Arduino IDE 2.3.4でArduino STM32を利用する(2025.01.12)
- PocketGoで遊んでみる(1)(2020.03.24)
- Arduino用 MML文演奏ライブラリの作成 その1(2019.04.01)
- BluePillボードで4桁7セグLEDの制御(2019.03.21)
「STM32」カテゴリの記事
- Arduino IDE+Arduino STM32環境で指定と異なるgccが使われてしまう(2025.01.23)
- Arduino IDE 2.3.4でArduino STM32を利用する(2025.01.12)
- 「Arduino STM32 リファレンス 日本語版」が2万アクセス突破!(2021.03.26)
- SPI接続フラッシュメモリモジュールを入手しました(2020.05.13)
- Arduino STM32でキャラクタ液晶ディスプレイを使う(2019.06.01)
« 次はSTM32ボードを積極的に使ていきたい(24) グラフィック液晶(5) | トップページ | 次はSTM32ボードを積極的に使ていきたい(26) グラフィック液晶(7) »
コメント