メイン・コンテンツまでスキップ

パーシャル・フレームバッファによるメモリ使用量の軽減

このセクションでは、時計のアプリケーションを例に取り上げ、パーシャル・フレームバッファを設定および使用することで(一部の性能を犠牲にしながらも)メモリ要件を軽減する方法について説明します。

STM32L4R9Discovery評価キット上で実行されるアプリケーションに関するビデオを以下からご覧になれます。

フルサイズのフレームバッファ・メモリ

通常、フレームバッファは、ディスプレイで使用可能なすべてのピクセルを保持するのに十分なメモリを備えた大規模なメモリ・アレイになります。 解像度480x272の24bitディスプレイを使用している場合、フルサイズのフレームバッファは480 x 272 x 3バイト = 391,680バイトのデータを保持しています。

アプリケーションによっては、2つ(ダブル・バッファリング)または3つのフレームバッファを使用することもあります。 この場合の必要なメモリ容量の合計は、783,360バイトおよび1,175,040バイトになると考えられます。

TouchGFXは、UIのどの部分を描画するときにもピクセルのデータをフレームバッファに書き込みます。すべての描画処理が完了した後、フレームバッファがディスプレイに転送されます。 通常、UIの一部のみが更新された場合でも、フレームバッファ全体がディスプレイに転送されます。 一般的に、多くの小さいブロックに分けて更新された後、フレームバッファは転送されます。

更新1, 更新2, 更新3, ..., 更新Nのように更新した後、ディスプレイへ転送

一部のケースでは、特に外部RAMを使用しない低コストのソリューションでは、フレームバッファとアプリケーションが必要とするメモリ容量が、内部RAMに収まるようにフレームバッファのサイズを十分小さくする必要があります。 このような場合に、パーシャル・フレームバッファが役立ちます。

パーシャル・フレームバッファ・メモリ

パーシャル・フレームバッファを使用すると、フルサイズよりも小さいフレームバッファ上で、TouchGFXアプリケーションを実行可能です。 フレームバッファの数とサイズは設定可能です。 この手法により、アプリケーションのメモリ要件をかなりの量削減できますが、いくつか制限事項も発生します。

  • パーシャル・フレームバッファはメモリが内蔵されたディスプレイでのみ動作します。 これは通常はDSIディスプレイか、パラレル・バス接続(DBIタイプA/B、8080/6800)またはSPIバス接続を備えたディスプレイになります。
  • 複雑なアプリケーションではティアリングの可能性があります。

フレームバッファを使用してディスプレイ上のすべてのピクセルを表すのとは異なり、パーシャル・フレームバッファは通常は小さな部分を対象とします。 この記事で使用する時計のアプリケーションの例では、それぞれ11,700バイトの3つのフレームバッファが使用されています。 この結果、フレームバッファに必要なメモリ量は35,100バイトになります。

アプリケーションでUIの一部を更新する必要がある場合、必ずTouchGFXは設定済みのパーシャル・フレームバッファのいずれかを選択し、そのパーシャル・フレームバッファ内で描画処理を完了させ、その部分をディスプレイに転送します。 レンダリングの必要があるUIのすべてのエリアに対してこの操作が繰り返されます。これにより、描画データの更新と転送の式は以下のように変更されます。

更新1, 転送1, 更新2, 転送2, 更新3, 転送3, ..., 更新N, 転送N

場合によっては、1つのパーシャル・フレームバッファの転送を、次のバッファの更新中に実行することもできます。

ディスプレイのティアリング

フルサイズのフレームバッファの使用とは対照的に、TouchGFXでパーシャル・フレームバッファを使用するときには、UIの各部分が更新後すぐに転送されます。 ディスプレイの画面上には、受信した更新が最大でも16ms後(60fpsのディスプレイの場合)に表示されます。ディスプレイは定期的にリフレッシュする必要があるからです。 このため、すべての更新が転送される前に、ディスプレイへの最初の更新が表示され、ユーザが気づく可能性があります。

描画処理と転送のシーケンス全体が完了するまでに時間がかかる(> 16ms)場合、ユーザには前のフレームと新しい更新の一部が混在して表示される可能性が高くなります。 これはディスプレイのティアリングと呼ばれる現象で、望ましいものではありません。 このため、パーシャル・フレームバッファは、レンダリングに時間のかかる複雑なアニメーションを使用するアプリケーションには適さないといえます。

ディスプレイの更新例

アプリケーション内でパーシャル・フレームバッファを設定する方法について説明する前に、秒を表す円弧が移動するデジタル時計の具体例を見てみたいと思います。 この緑色の円弧は毎秒6度ずつ移動しており、1分で1周します。 下の画像に示すように、UIは次の4つのウィジェットから構築されています。

デジタル時計と円弧を更新するコードを次に示します。

MainView.cpp
void MainView::handleTickEvent()
{
ticks++;
if (ticks == 10)
{
ticks = 0;
secs += 1;
if (secs == 60) //increment minutes
{
secs = 0;
min += 1;
if (min == 60) //increment hours
{
min = 0;
hour += 1;
if (hour == 24)
{
hour = 0;
}
}
//Only update digital clock when minutes or hours change
digitalClock.setTime24Hour(hour, min, secs);
}
//Always update seconds
circleSeconds.updateArc(secs*6 - 20, secs*6);
}
}

以下の画像は、円弧が一番上に到達し、デジタル時計が更新された時点で、最初の数秒間に更新される領域を示しています(灰色の長方形)。 最初の2つのフレームでは、秒のみが変化しています(58秒と59秒)。 3つ目のフレームでは60秒に到達し、時間と分のテキストが更新されています。

上の3つ目の画像で更新された長方形は、154x60ピクセル、20x12ピクセル、33x8ピクセルです。 標準のフレームバッファを使用する場合、この3つの長方形はフレームバッファ全体に描画(以前のピクセルに上書き)され、後からディスプレイに転送されることになります。 パーシャル・フレームバッファを使用すると、この3つの長方形はそれぞれ独自の小さいフレームバッファに描画され、更新されると即座にディスプレイに転送され表示されることになります。

パーシャル・フレームバッファの設定

TouchGFXでパーシャル・フレームバッファを使用するために必要な手順は以下のとおりです。

  1. メモリ・バッファを含むフレームバッファ・アロケータ・オブジェクトを作成する
  2. そのアロケータを使用するためのTouchGFX HALクラスを設定する
  3. バッファをディスプレイに転送するためのコードを作成する

ステップ1と2は、TouchGFX GeneratorによってSTM32CubeMXを介して自動生成されますが、ステップ3はピクセルをディスプレイに転送するための独自のドライバが必要です。

上に示す設定によって、以下のコードが生成されます。

TouchGFXGeneratedHAL.cpp
// Block Allocator for Partial Framebuffer strategy
static ManyBlockAllocator<1920, /* block size */
3, /* number of blocks */
2 /* bytes per pixel */
> blockAllocator;

このフレームバッファ・アロケータは、それぞれ1,920バイトの3つのブロックを割り当てます。 TouchGFX HALは、パーシャル・フレームバッファ戦略およびブロック・アロケータを使用するように、自動的に構成されます。

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::initialize()
{
HAL::initialize();

registerEventListener(*(Application::getInstance()));
enableLCDControllerInterrupt();
enableInterrupts();
// Partial framebuffer strategy
setFrameBufferAllocator(&blockAllocator);
setFrameRefreshStrategy(HAL::REFRESH_STRATEGY_PARTIAL_FRAMEBUFFER);
}

この設定では、TouchGFXが小さいフレームバッファを割り当て、そこにUIを描画します。 最後のオペレーションは、その小さいフレームバッファをディスプレイに転送することです。

まずは、小さい円の更新を描画する(上の2つ目の画像)ために割り当てられた2つのフレームバッファの位置とサイズを確認してみましょう。

長方形xy高さピクセル
長方形1112562214308ピクセル = 924バイト
長方形2153422911319ピクセル = 957バイト

どちらの長方形も非常に小さいので、フレームバッファ・アロケータによって割り当てられたブロックに収まります。

上の3つ目の画像では、3つの長方形が更新されています。円への更新は少なく、大きな長方形がテキストを覆っています。

長方形xy高さピクセル
長方形1126512012240ピクセル = 720バイト
長方形216542338264ピクセル = 792バイト
長方形3118165154609,240ピクセル = 27,720バイト

繰り返しますが、長方形1と2は非常に小さいので、フレームバッファ・アロケータによって割り当てられたブロックに収まります。ただし、フレームバッファ3は大きすぎます。 この長方形3は大きすぎるので、それぞれがフレームバッファ(11,700バイト)に収まる複数の長方形に分割されます。

次に、3つの長方形を更新しますが、アロケータには2つのブロックしかありません。 こうした状況では、TouchGFXは最初のブロックが転送されるのを待ってから、ブロックを再利用します。

画面へのフレームバッファの転送

TouchGFXは、長方形を再描画する必要がある場合に、FrameBufferAllocatorからフレームバッファを割り当てます。 バッファへの描画の後、TouchGFXは次のメソッドを呼び出します。

void HAL::flushFrameBuffer(const Rect& rect);

この関数は、フレームバッファを画面に転送するために、HALサブクラス内でオーバーライドすることができます。 パーシャル・フレームバッファを機能させるために、この特殊な処理が必要になります。 以下のセクションでは、SPIディスプレイ搭載のSTM32G071評価キットと、DSIディスプレイ搭載のSTM32L4R9Discovery評価キット向けに、この設定を行う方法の実例を示します。

X-NUCLEO-GFX01M1 SPIディスプレイへのフレームバッファの転送

このセクションでは、X-Nucleo-GFX01M1拡張ボード付きSTM32G071 Nucleoボード向けの、TouchGFXボード設定について説明します。 この拡張ボードMB1642Bには、2.2インチの240x320 SPIディスプレイと64Mbit SPI NOR Flashが実装されています。

X-Nucleo-GFX01M1拡張ボード付きのNucleo-G071RB

このTouchGFXボード設定では、パーシャル・フレームバッファ・ブロックを管理するためにフレームワークのC++クラスを使用します。 これによりTouchGFXボード設定のコードが少しだけ短くなります。

TouchGFXボード設定はTouchGFX Generatorを使用して構築します。 詳細については、こちらを参照してください。

最も重要な部分は、次に示すflushFrameBuffer関数です。

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
HAL::flushFrameBuffer(rect);
// Try transmitting a block
PartialFrameBufferManager::tryTransmitBlock();
}

ここでは、PartialFrameBufferManagerフレームワーク・クラスを呼び出して、ブロックを転送しています。

TouchGFXGeneratedHAL::endFrame関数では、PartialFrameBufferManagerを呼び出して、残りのフレームバッファ・ブロックも転送しています。

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::endFrame()
{
// We must guard the next frame until we're done transferring all blocks over our display interface
// through either a semaphore if user is running an OS or a simple variable if not
PartialFrameBufferManager::transmitRemainingBlocks();

HAL::endFrame();
touchgfx::OSWrappers::signalRenderingDone();
}

PartialFrameBufferManagerは、3つの関数を使用してディスプレイ・ドライバ・コードを操作します。 これらはTouchGFXボード設定で実装する必要があります。

TouchGFXGeneratedHAL.cpp
/**
* Check if a Frame Buffer Block is beeing transmitted.
*/
__weak int transmitActive()
{
return touchgfxDisplayDriverTransmitActive();
}

/**
* Check if a Frame Buffer Block ending at bottom may be sent.
*/
__weak int shouldTransferBlock(uint16_t bottom)
{
return touchgfxDisplayDriverShouldTransferBlock(bottom);
}

/**
* Transmit a Frame Buffer Block.
*/
__weak void transmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
touchgfxDisplayDriverTransmitBlock(pixels, x, y, w, h);
}

上のコードでは、MB1642Bドライバ・コードのC関数に呼び出しを送っています。

MB1642BDisplayDriver.c
int touchgfxDisplayDriverTransmitActive(void)
{
return IsTransmittingBlock_;
}

void touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
Display_Bitmap((uint16_t*)pixels, x, y, w, h);
}

このドライバ・コードの実装は、使用するディスプレイによって大きく異なります。 MB1642Bモジュールでは、SPIチップの選択とデータ / コマンド・モードをコントロールするために、2つのGPIOが必要になります。

転送状態は、volatile uint8tの変数IsTransmittingBlockを使用して確認します。 この変数は、転送開始時に1に設定され、DMAコールバックでは0(ゼロ)に設定されます。

MB1642BDisplayDriver.c
void MB1642BDisplayDriver_DMACallback(void)
{
/* Transfer Complete Interrupt management ***********************************/
if ((0U != (DMA1->ISR & (DMA_FLAG_TC1))) && (0U != (hdma_spi1_tx.Instance->CCR & DMA_IT_TC)))
{
/* Disable the transfer complete and error interrupt */
__HAL_DMA_DISABLE_IT(&hdma_spi1_tx, DMA_IT_TE | DMA_IT_TC);

/* Clear the transfer complete flag */
__HAL_DMA_CLEAR_FLAG(&hdma_spi1_tx, DMA_FLAG_TC1);

IsTransmittingBlock_ = 0;

...

// Signal Transfer Complete to TouchGFX
DisplayDriver_TransferCompleteCallback();

上に示すように、DMAコールバックでは転送完了のコールバックも呼び出します。 この関数は、生成されたHALで実装されます。

TouchGFXGeneratedHAL.cpp
extern "C"
void DisplayDriver_TransferCompleteCallback()
{
// After completed transmission start new transfer if blocks are ready.
PartialFrameBufferManager::tryTransmitBlockFromIRQ();
}

このPartialFrameBufferManagerへの呼び出しにより、可能な場合は新たな転送が開始されます。

DSIディスプレイへのフレームバッファの転送

STM32L4R9Discovery評価キットはDSIディスプレイを使用します。 通常のHALクラスは、STM32HAL_DSI(STM32HAL_DSI.cpp内に配置)で呼び出されます。

ここではHAL::flushFrameBufferメソッドをオーバーライドして、ブロックが描画済みであることをFrameBufferAllocatorに通知します。

TouchGFXHAL.hpp
void TouchGFXHAL::flushFrameBuffer(const Rect& rect)
{
frameBufferAllocator->markBlockReadyForTransfer();
HAL::flushFrameBuffer(rect); //call normal implementation
}

ブロックの転送準備ができたら、 FrameBufferAllocatorサブクラスのManyBlockAllocatorが、グローバル関数FrameBufferAllocatorSignalBlockDrawn()を呼び出します。 このメソッドはBSPレイヤ内に実装する必要があります。

TouchGFXGeneratedHAL.cpp
void FrameBufferAllocatorSignalBlockDrawn()
{
if (!dsiIsTransferring)
{
sendBlock();
}
}

DSI上で転送がすでに進行中でなければ、この関数がsendBlock関数を呼び出します。 TouchGFXによって描画された最初のブロックについては、転送中ではないので、転送が開始されます。 DSI転送がまだ実行中なのに別のブロックの描画が完了した場合は、そのブロックは"ready to transfer state"のままになり、描画は別の空きブロックが存在すれば続行されます。

DSI転送が完了したら、まず転送済みのブロックを開放して、別の長方形用に再利用できるようにする必要があります。さらに、次のブロックの転送準備ができているかどうかを確認します。 これはすべてERインタラプトで実行されます。

TouchGFXHAL.cpp
__irq void DSI_IRQHandler(void) {
if (__HAL_DSI_GET_FLAG(&hdsi, DSI_IT_ER))
{
// End-of-refresh interrupt. Meaning last DSI transfer is complete
__HAL_DSI_CLEAR_FLAG(&hdsi, DSI_IT_ER);
if (dsiIsTransferring)
{
HAL::getInstance()->getFrameBufferAllocator()->freeBlockAfterTransfer();
dsiIsTransferring = 0;
}
sendBlock(); //transfer next block if availble
}

関数sendBlockはもっと複雑です。 ここではフレームバッファを転送するためにLTDCおよびDSIペリフェラルを設定します。 さらに、転送されたデータがディスプレイ・メモリ内に正しく配置するために、ディスプレイの設定も行います。 この部分のコードは、使用するディスプレイによって異なります。 ディスプレイのデータシートでコマンド仕様を確認してください。

TouchGFXHAL.cpp
static void sendBlock()
{
FrameBufferAllocator* fbAllocator = HAL::getInstance()->getFrameBufferAllocator();

//Is a block ready for transfer?
if (fbAllocator->hasBlockReadyForTransfer())
{
Rect transfer_rect;
const uint8_t* src = fbAllocator->getBlockForTransfer(transfer_rect);
dsiIsTransferring = 1;

//1. Setup LTDC and layer address and dimension
//2. Configure display active area
//3. Start DSI

__HAL_DSI_WRAPPER_DISABLE(&hdsi);

//1: Setup LTDC
LTDC_Layer1->CFBAR = (uint32_t)src;

const uint32_t width = transfer_rect.width;
const uint32_t height = transfer_rect.height;

LTDC->AWCR = ((width + 1) << 16) | (height + 1);
LTDC->TWCR = ((width + 1 + 1) << 16) | (height + 1 + 1);

const uint16_t layer_x0 = 2 + 0;
const uint16_t layer_x1 = 2 + width - 1;
LTDC_Layer1->WHPCR = (layer_x1 << 16) | layer_x0;

const uint16_t layer_y0 = 2 + 0;
const uint16_t layer_y1 = 2 + height - 1;
LTDC_Layer1->WVPCR = (layer_y1 << 16) | layer_y0;

LTDC_Layer1->CFBLR = ((width * 3) << 16) | (width * 3 + 3);
LTDC_Layer1->CFBLNR = height;

LTDC->SRCR = (uint32_t)LTDC_SRCR_IMR;

//2: Configure display
const int16_t x = transfer_rect.x + 4;
const int16_t x2 = transfer_rect.x + 4 + width - 1;
uint8_t InitParam1[4] = { (uint8_t)(x >> 8), (uint8_t)(x & 0xFF), (uint8_t)(x2 >> 8), (uint8_t)(x2 & 0xFF)};
HAL_DSI_LongWrite(&hdsi, 0, DSI_DCS_LONG_PKT_WRITE, 4, DSI_SET_COLUMN_ADDRESS, InitParam1);

const int16_t y = transfer_rect.y;
const int16_t y2 = transfer_rect.y + height - 1;
uint8_t InitParam2[4] = { (uint8_t)(y >> 8), (uint8_t)(y & 0xFF), (uint8_t)(y2 >> 8), (uint8_t)(y2 & 0xFF) };
HAL_DSI_LongWrite(&hdsi, 0, DSI_DCS_LONG_PKT_WRITE, 4, DSI_SET_PAGE_ADDRESS, InitParam2);

//3: Start DSI transfer
__HAL_DSI_WRAPPER_ENABLE(&hdsi);
HAL_DSI_Refresh(&hdsi);
}
}

SPIディスプレイへのフレームバッファの転送

SPIディスプレイへの長方形の転送方法は、原則的にDSIの場合と同じですが、一部の詳細が異なります。

まず、長方形が描画されたら、すでに進行中のものが何もなければ転送を開始します。

TouchGFXHAL.cpp
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
HAL::flushFrameBuffer(rect);
frameBufferAllocator->markBlockReadyForTransfer();
//start transfer if not running already!
if (!LCDManager_IsTransmittingData())
{
touchgfx::Rect r;
const uint8_t* pixels = frameBufferAllocator->getBlockForTransfer(r);
LCDManager_SendFrameBufferBlockWithPosition((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
}

関数LCDManager_SendFrameBufferBlockWithPositionは、DMAを使用してディスプレイへのSPI転送を開始します。

転送が完了したら、SPI転送の完了ハンドラが関数を呼び出します。

TouchGFXHAL.cpp
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
UNUSED(hspi);
LCD_CS_HIGH();
isTransmittingData = 0;

//Change to SPI datasize to 8 bit from 16 bit
heval_Spi.Instance->CR2 &= ~(SPI_DATASIZE_16BIT - SPI_DATASIZE_8BIT);

//signal transfer complete
LCDManager_TransferComplete();
}

LCDManager_TransferComplete関数が新しい転送を開始します。

TouchGFXHAL.cpp
void LCDManager_TransferComplete()
{
touchgfx::startNewTransfer();
}

void startNewTransfer()
{
FrameBufferAllocator* fba = HAL::getInstance()->getFrameBufferAllocator();
fba->freeBlockAfterTransfer();
blockIsTransferred = true;

if (fba->hasBlockReadyForTransfer())
{
touchgfx::Rect r;
const uint8_t* pixels = fba->getBlockForTransfer(r);
LCDManager_SendFrameBufferBlockWithPosition((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
}

まとめ

この記事では、パーシャル・フレームバッファ戦略によって、フレームバッファ・メモリが内蔵されたディスプレイを実装したプラットフォームのメモリ要件を、軽減する方法について説明しました。

パーシャル・フレームバッファの構成および設定方法は、すべてのプラットフォームで同じですが、ブロックのコンテンツをディスプレイに転送する方法はさまざまです。 ここで説明した方法によると、LTDC/DSIベースのプラットフォーム(STM32L4R9-DISCO)では、DSIへの転送準備ができた次のブロックに合うようにLTDCレイヤを再設定することができ、LCDコントローラ(STM32G071)のないプラットフォームでは、SPIを使用してディスプレイにブロックを転送することができます。