概要
前回は全く関係ない画面キャプチャの話題でした。今回はいろいろな描画の時間を計測して、どんな処理になっているのかを確認してきたいと思います。
スケッチ例1(通常描画)
ちょっと長いので、スケッチは別の場所に置きます。
drawPixel() : 451027 drawPixel() + setColor() : 437125 drawPixel() + startWrite(): 126832 drawPixel() + startWrite() + setColor() : 118255 fillRect() : 16074 fillRect() + startWrite() : 16074 sprite drawPixel() : 29290 sprite drawPixel() + setColor() : 13288 sprite drawPixel() + startWrite(): 29923 sprite drawPixel() + startWrite() + setColor() : 13963 sprite fillRect() + startWrite() : 240 sprite fillRect() : 212 sprite pushSprite() : 16159 spritePSRAM drawPixel() : 40610 spritePSRAM drawPixel() + setColor() : 24207 spritePSRAM drawPixel() + startWrite(): 40003 spritePSRAM drawPixel() + startWrite() + setColor() : 24017 spritePSRAM fillRect() + startWrite() : 10655 spritePSRAM fillRect() : 10644 spritePSRAM pushSprite() : 16725 img pushImage() : 16746 imgOnMem pushImage() : 16908 imgPSRAM pushImage() : 16836 img 0x3f404680 imgOnMem 0x3ffe4374 imgPSRAM 0x3f8138b8
上記が実行結果です。内容を個別に解説していきたいと思います。
Lcd.drawPixel()
200×200の4万ピクセルを塗りつぶすサンプルです。
条件 | 経過時間(us) |
drawPixel() | 451,027 |
drawPixel() + setColor() | 437,125 |
drawPixel() + startWrite() | 126,832 |
drawPixel() + startWrite() + setColor() | 118,255 |
for (int y = 0; y < 200; y++) { for (int x = 0; x < 200; x++) { M5Lite.Lcd.drawPixel(x, y, TFT_RED); } }
Lcdに対して4万回描画した場合451ミリ秒も時間がかかっています。これだと秒間2フレームしか描画できませんね。これはLcdはdrawPixel()で実際に画面に転送されてしまうので、転送待ち時間が非常に長くなっています。
M5Lite.Lcd.setColor(TFT_BLUE); for (int y = 0; y < 200; y++) { for (int x = 0; x < 200; x++) { M5Lite.Lcd.drawPixel(x, y); } }
2行目はsetColor()をしています。drawPixel()関数で色指定をした場合には、内部でsetColor()を呼んでから描画をしています。同じ色を塗る場合にはsetColor()を省略することができますので、若干早くなっています。
M5Lite.Lcd.startWrite(); for (int y = 0; y < 200; y++) { for (int x = 0; x < 200; x++) { M5Lite.Lcd.drawPixel(x, y, TFT_RED); } } M5Lite.Lcd.endWrite();
上記は描画処理をstartWrite()とendWrite()で囲んであります。データベースのトランザクションのように、SPI通信を専有することを宣言しています。宣言していない場合には各drawPixel()でSPI通信が利用可能かを確認する必要があります。今回は4万回もSPI通信をしていますので非常に差がでています。基本的にはstartWrite()とendWrite()は利用するようにしたほうがよいと思います。
Lcd.fillRect()
条件 | 経過時間(us) |
fillRect() | 16,074 |
fillRect() + startWrite() | 16,074 |
200×200の4万ピクセルを塗りつぶすサンプルです。あまり時間が変わっていません。
M5Lite.Lcd.startWrite(); M5Lite.Lcd.fillRect(0, 0, 200, 200, TFT_BLUE); M5Lite.Lcd.endWrite();
こちらの場合には一回しかSPI通信をしないんで、色指定やstartWrite()などの影響をほとんど受けません。
sprite.drawPixel()
条件 | 経過時間(us) |
sprite drawPixel() | 29,290 |
sprite drawPixel() + setColor() | 13,288 |
sprite drawPixel() + startWrite() | 29,923 |
sprite drawPixel() + startWrite() + setColor() | 13,963 |
処理はLcdと同じなのですが、オンメモリ上に描画しているのでSPI通信は発生していません。そのためstartWrite()の影響は受けません。setColor()はLcdと同じ程度の時間が短縮されています。
sprite.fillRect()
条件 | 経過時間(us) |
sprite fillRect() + startWrite() | 240 |
sprite fillRect() | 212 |
こちらもあまり差がありません。誤差の範囲ですが実際はstartWrite()の処理がある分だけ遅くなっているはずです。
sprite.pushSprite()
条件 | 経過時間(us) |
sprite pushSprite() | 16,159 |
オンメモリで描画してたスプライトをLcdに転送する処理です。
sprite.pushSprite(0, 0);
fillRect()で同色で塗り潰したときと同じぐらいの時間がかかっています。
spritePSRAM.drawPixel()
条件 | 経過時間(us) |
spritePSRAM drawPixel() | 40,610 |
spritePSRAM drawPixel() + setColor() | 24,207 |
spritePSRAM drawPixel() + startWrite() | 40,003 |
spritePSRAM drawPixel() + startWrite() + setColor() | 24,017 |
オンメモリではなく、SPI接続のPSRAMにスプライトを確保した場合の時間です。こちらもsetColor()は早くなっていますが、スプライトなのでstartWrite()は影響を受けません。全般的にオンメモリのスプライトと比べると遅くなっているのがわかると思います。
spritePSRAM.fillRect()
条件 | 経過時間(us) |
spritePSRAM fillRect() + startWrite() | 10,655 |
spritePSRAM fillRect() | 10,644 |
こちらも同様の傾向です。
spritePSRAM.pushSprite()
条件 | 経過時間(us) |
spritePSRAM pushSprite() | 16,725 |
Lcdに転送した結果です。実はこの時間はオンメモリからの時間とあまり変わりません。Lcdの転送時間が支配的なんだと思われます。
pushImage()
条件 | 経過時間(us) |
img pushImage() | 16,746 |
imgOnMem pushImage() | 16,908 |
imgPSRAM pushImage() | 16,836 |
こちらもびっくりなんですが、200×200の画像ファイルをLcdに描画した場合の時間です。オンメモリからだろうと、PSRAMからだろとフラッシュからだろうとあまり変わりませんでした。
スケッチ例2(画面全体描画)
先程は通常の描画でしたが、M5Stack Core2の320×240の画面を3分割して、320×80を3回描画して画面全体を更新するサンプルです。
clear()
条件 | 経過時間(us) |
clear() | 30,842 |
clear() + startWrite() | 30,839 |
画面全体を同じ色で塗りつぶす関数です。fillScreen()と同等の処理でfillRect()で画面全体を指定しても同じ処理です。こちらも一回のSPI通信なのでstartWrite()の影響は受けていません。
pushSprite()
条件 | 経過時間(us) |
pushSprite() | 31,391 |
pushSprite() + startWrite() | 30,903 |
pushPixels() | 31,470 |
pushPixels() + startWrite() | 31,078 |
pushPixelsDMA() | 31,188 |
pushPixelsDMA() + startWrite() | 30,883 |
pushSprite() + delay() | 45,044 |
pushSprite() + delay() + startWrite() | 30,902 |
スプライトの転送ですが、pushSprite()の他にも各種条件チェックをせずに高速に転送をするpushPixels()関数と、DMA転送専用のpushPixelsDMA()があります。
sprite[0].pushSprite(0, 0); M5Lite.Lcd.pushPixels((uint16_t*)sprite[0].getBuffer(), 320 * 80); M5Lite.Lcd.pushPixelsDMA((uint16_t*)sprite[0].getBuffer(), 320 * 80);
上記のように呼び出しがちょっと異なります。結果を見てみるとどの関数でもあまり時間は変わっていませんでした。
sprite[0].fillScreen(TFT_RED); sprite[0].pushSprite(0, 0); delay(5); sprite[1].fillScreen(TFT_BLUE); sprite[1].pushSprite(0, 80); delay(5); sprite[0].fillScreen(TFT_RED); sprite[0].pushSprite(0, 160); delay(5);
唯一遅いのは上記のpushSprite()のあとにdelay(5)と5ミリ描画3回で15ミリ秒の遅延を入れた場合です。この場合は描画が約30ミリ秒、遅延が15ミリ秒と他の結果と同じ時間になります。
M5Lite.Lcd.startWrite(); sprite[0].fillScreen(TFT_RED); sprite[0].pushSprite(0, 0); delay(5); sprite[1].fillScreen(TFT_BLUE); sprite[1].pushSprite(0, 80); delay(5); sprite[0].fillScreen(TFT_RED); sprite[0].pushSprite(0, 160); delay(5); M5Lite.Lcd.endWrite();
同じような処理なのですが、startWrite()とendWrite()で囲むとなんと約30ミリ秒と描画だけの時間になり、delay()の影響を受けなくなります。これは実はpushSprite()は一瞬で完了しており、バックグラウンドでDMA転送をされています。そのためdelay(5)で遅延しても総描画時間には影響がなく、次のpushSprite()やendWrite()の先頭でDMA転送が終わるまで待機しているのです。
ここはちょっとわかりにくいので、次のスケッチ例で解説をしたいと思います。
spritePSRAM.pushSprite()
条件 | 経過時間(us) |
spritePSRAM pushSprite() | 48,584 |
spritePSRAM pushSprite() + startWrite() | 46,493 |
spritePSRAM pushPixels() | 45,937 |
spritePSRAM pushPixels() + startWrite() | 45,134 |
spritePSRAM pushPixelsDMA() | 49,106 |
spritePSRAM pushPixelsDMA() + startWrite() | 37,525 |
spritePSRAM pushSprite() + delay() | 62,739 |
spritePSRAM pushSprite() + delay() + startWrite() | 60,136 |
同じ用に今度はオンメモリではなくPSRAMに確保したスプライトで実験します。全般的にメモリの速度が違うので遅くなっています。
そして、delay()が入った例ではstartWrite()を使っても早くなっていません。これはPSRAMからはDMA転送ができないので、delay()の分がそのまま遅延されてしまっています。
また、pushPixelsDMA()ですが、PSRAMからはDMA転送ができないので描画時間的には問題ないのですが実際に描画されている画像は崩れています。つまり明示的にDMA転送をさせる関数なのですが、最速のはずですがDMA転送ができるかのチェックもされていないので、PSRAMから実行するとデータが正しく描画されません。若干チェックのオーバーヘッドがありますが、pushSprite()を利用することで、DMA転送が可能な場合にはDMA転送を利用すると判定してくれるので便利だと思います。
スケッチ例3(処理別処理時間)
上記に個別の関数単位での経過時間を取得するスケッチがあります。
コマンド | 開始時間 | 実行時間 |
M5Lite.Lcd.startWrite() | 2776504 | 12 |
sprite[0].fillScreen(TFT_RED) | 2776516 | 172 |
sprite[0].pushSprite(0, 0) | 2776688 | 168 |
M5Lite.Lcd.waitDMA() | 2776856 | 10,238 |
sprite[1].fillScreen(TFT_BLUE) | 2787094 | 155 |
sprite[1].pushSprite(0, 80) | 2787249 | 14 |
M5Lite.Lcd.waitDMA() | 2787263 | 10,240 |
sprite[0].fillScreen(TFT_RED) | 2797503 | 142 |
sprite[0].pushSprite(0, 160) | 2797645 | 16 |
M5Lite.Lcd.waitDMA() | 2797661 | 10,240 |
M5Lite.Lcd.endWrite() | 2807901 | 5 |
全体 | 2807906 | 31,402 |
全体では31ミリ秒かかっています。個別に時間を見ていくとpushSprite()関数は一瞬で終わっています。そしてwaitDMA()でDMA転送が終わるまで待機している時間が約10ミリ秒ですね。
320×80の画像を転送する時間は約10ミリ秒で、DMA転送なのでバックグラウンド転送されています。つまりCPUは自由に使える状況ですので、その間に転送しているのとは別のスプライトに描画をすることができます。今回は塗り潰しだけなのであまり意味がないですが、LovyanGFXの公式スケッチ例にあるMovingIconsなどはこのDMA転送待ちの間に、次に転送するスプライトを作成することで高速描画が可能です。
これは実験のため、waitDMA()を入れていますが実際には入れないほうが早いです。入れない場合には次回のpushSprite()かendWrite()で自動的にwaitDMA()が実行されます。そうすることでfillScreen()などの描画している時間がDMA転送と並行になり時間短縮されます。
PSRAMの場合
sprite[0].setPsram(true); sprite[0].createSprite(320, 80); sprite[1].setPsram(true); sprite[1].createSprite(320, 80);
スケッチ例のスプライト作成時にPSRAMを有効にして作成してみます。この状況で同じ処理を実行してみました。
コマンド | 開始時間 | 実行時間 |
M5Lite.Lcd.startWrite() | 2787929 | 12 |
sprite[0].fillScreen(TFT_RED) | 2787941 | 6,839 |
sprite[0].pushSprite(0, 0) | 2794780 | 10,845 |
M5Lite.Lcd.waitDMA() | 2805625 | 4 |
sprite[1].fillScreen(TFT_BLUE) | 2805629 | 4,791 |
sprite[1].pushSprite(0, 80) | 2810420 | 10,724 |
M5Lite.Lcd.waitDMA() | 2821144 | 6 |
sprite[0].fillScreen(TFT_RED) | 2821150 | 4,774 |
sprite[0].pushSprite(0, 160) | 2825924 | 10,738 |
M5Lite.Lcd.waitDMA() | 2836662 | 7 |
M5Lite.Lcd.endWrite() | 2836669 | 4 |
全体 | 2836673 | 48,744 |
全体の実行時間が31ミリ秒から、48ミリ秒に伸びました。個別に見るとpushSprite()に時間がかかっており、waitDMA()は一瞬で終わっています。つまりDMA転送がされていないので、pushSprite()はCPU時間を使って転送されています。
スケッチ例4(関数別処理時間例)
処理時間(us) | コマンド |
91 | sprite.clear(); |
523 | sprite.drawArc (x, y, r0, r1, angle0, angle1); // 円弧の外周 |
705 | sprite.drawArc (x, y, r0, r1, angle0, angle1, color); // 円弧の外周 |
176 | sprite.drawBezier (x0, y0, x1, y1, x2, y2); // 3点間のベジエ曲線 |
238 | sprite.drawBezier (x0, y0, x1, y1, x2, y2, color); // 3点間のベジエ曲線 |
425 | sprite.drawBezier (x0, y0, x1, y1, x2, y2, x3, y3); // 4点間のベジエ曲線 |
528 | sprite.drawBezier (x0, y0, x1, y1, x2, y2, x3, y3, color); // 4点間のベジエ曲線 |
26 | sprite.drawCircle (x, y , r); // 円の外周 |
54 | sprite.drawCircle (x, y , r, color); // 円の外周 |
33 | sprite.drawEllipse (x, y, rx, ry ); // 楕円の外周 |
64 | sprite.drawEllipse (x, y, rx, ry , color); // 楕円の外周 |
7 | sprite.drawFastHLine(x, y, w ); // 水平線 |
21 | sprite.drawFastHLine(x, y, w , color); // 水平線 |
3 | sprite.drawFastVLine(x, y , h ); // 垂直線 |
18 | sprite.drawFastVLine(x, y , h , color); // 垂直線 |
22 | sprite.drawLine (x0, y0, x1, y1 ); // 2点間の直線 |
50 | sprite.drawLine (x0, y0, x1, y1 , color); // 2点間の直線 |
5 | sprite.drawPixel (x, y ); // 点 |
12 | sprite.drawPixel (x, y , color); // 点 |
16 | sprite.drawRect (x, y, w, h ); // 矩形の外周 |
25 | sprite.drawRect (x, y, w, h , color); // 矩形の外周 |
64 | sprite.drawRoundRect(x, y, w, h, r); // 角丸の矩形の外周 |
100 | sprite.drawRoundRect(x, y, w, h, r, color); // 角丸の矩形の外周 |
117 | sprite.drawTriangle (x0, y0, x1, y1, x2, y2); // 3点間の三角形の外周 |
125 | sprite.drawTriangle (x0, y0, x1, y1, x2, y2, color); // 3点間の三角形の外周 |
137 | sprite.fillArc (x, y, r0, r1, angle0, angle1); // 円弧の塗り |
158 | sprite.fillArc (x, y, r0, r1, angle0, angle1, color); // 円弧の塗り |
63 | sprite.fillCircle (x, y , r); // 円の塗り |
80 | sprite.fillCircle (x, y , r, color); // 円の塗り |
61 | sprite.fillEllipse (x, y, rx, ry ); // 楕円の塗り |
85 | sprite.fillEllipse (x, y, rx, ry , color); // 楕円の塗り |
137 | sprite.fillRect (x, y, w, h ); // 矩形の塗り |
139 | sprite.fillRect (x, y, w, h , color); // 矩形の塗り |
244 | sprite.fillRoundRect(x, y, w, h, r); // 角丸の矩形の塗り |
277 | sprite.fillRoundRect(x, y, w, h, r, color); // 角丸の矩形の塗り |
250 | sprite.fillTriangle (x0, y0, x1, y1, x2, y2); // 3点間の三角形の塗り |
292 | sprite.fillTriangle (x0, y0, x1, y1, x2, y2, color); // 3点間の三角形の塗り |
1 | sprite.setColor (color); // 色指定 |
代表的な関数群の速度をためしてみました。こちらは描画するサイズによってかなり差がでるので、参考程度にしてください。
ざっくりした指針として、M5Stackの320×240の画面を秒間30フレームで描画する場合には、1フレームあたり33ミリ秒で描画する必要があります。オンメモリに320×240のスプライトは確保できないので320×80のスプライトを2枚確保して、交互に描画をしながら上、中、下と3回にわけて転送をします。320×80のDMA転送時間が約10ミリ秒なので、次の描画を10ミリ秒(10,000マイクロ秒)で準備できれば秒間30フレームで描画可能です。

例えばfillTriangle()の色指定無しが250マイクロ秒なので、320×240に40個ぐらい、画面全体で120個相当であれば10ミリ秒で間に合うかもしれません。fillCircle()だともっと早いのでたくさん描画できそうですね。ただこの時間は描画する大きさによってかなり変わるので、この計算通りには行かないと思います。
まとめ
かなり長々としてしまいましたが、Lcd.startWrite()とLcd.endWrite()は描画の前後で呼び出すようにしてください。スプライトや画像から数回の転送であってもDMA転送が使えるようになります。
DMA転送を利用する場合にはオンメモリのメモリである必要があります。PSRAMを利用した場合にはDMA転送ではなくなるため、バックグラウンド転送中に他の処理をすることはできません。秒間30フレーム必要でない場合には、PSRAMを使って画面サイズとおなじスプライトを作成して描画したほうが処理は楽になると思います。
DMA転送を利用して高速描画をする場合には、オンメモリにスプライトを確保する必要があります。ESP32で一度に確保できるサイズが100KB程度が上限のため、320×80などを2枚確保する必要があります。そして描画範囲を移動しながら3回描画をして画面全体を更新します。DMA転送をするため、ちょっと面倒な処理になるので注意が必要です。
スプライトの描画関数は、pushSprite()関数、pushPixels()関数、pushPixelsDMA()関数の3種類がありますが、通常はpushSprite()関数を使えば問題ないと思います。さらに高速動作させたい場合にはpushPixelsDMA()などにチャレンジしてみてください。
また、今回紹介した処理時間は一回のみの時間であり、誤差がかなり含まれると思います。とくにキャッシュの影響などで処理順番によって速度が変わることがあると思います。参考程度にして、実際の環境でいろいろ試しながらチューニングをしてみてください。
コメント