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

FMCディスプレイ・インタフェース

このシナリオでは、FMCパラレル・インタフェースとGRAMを搭載したディスプレイを使用する場合に、 STM32 FMC(フレキシブル・メモリ・コントローラ)とTouchGFX Generatorを設定する方法について説明します。

Note
このシナリオでは、動作するディスプレイ・ドライバはボードの立ち上げフェーズで開発されたものとします。 ドライバは、ピクセルをディスプレイに転送し、ディスプレイのメモリ書込み位置を制御できる必要があります。 詳細については、ディスプレイのデータシートを参照してください。

設定

FMCの設定

STM32CubeMXカテゴリ・リストの[Connectivity]グループにある[FMC]を有効にします。 ここから、使用可能なFMCバンクのいずれかを「LCDインタフェース」に設定する必要があります。 このためには、STM32CubeMXで[Memory type]を[LCD Interface]に設定します。 ピクセルをディスプレイに転送するために使用するパラレル・データ・ラインの数もここで選択します(16ビットまたは8ビットのパラレル・インタフェース)。

FMCバンクの設定

使用するディスプレイに対応するタイミングを必ず正しく設定してください。 お使いのマイクロコントローラのFMCのベース・アドレスと、ディスプレイとのインタフェースに使用するバンクを確認します。

TouchGFX Generator

ディスプレイ・インタフェース: パラレルRGB(FMC)

16ビットのパラレル・インタフェースを使用する場合、TouchGFX GeneratorでHALを生成して、デフォルトの実装により、データとレジスタの読み出し/書き込みを行うシンプルなAPIを提供します。

TouchGFXGeneratedHAL.cpp
...
/**
* @brief Initialize the LCD IO.
* @param None
* @retval None
*/
__weak void LCD_IO_Init(void)
{
// Already Done by MX_FMC_Init()
}

/**
* @brief Writes data on LCD data register.
* @param Data: Data to be written
* @retval None
*/
__weak void LCD_IO_WriteData(uint16_t RegValue)
{
/* Write 16-bits Reg */
FMC_BANK1_WriteData(RegValue);
}
...

完全な生成されたコードについては、TouchGFXGeneratedHAL.cppをご確認ください。 これらの関数は __weakと定義されているので、開発者は必要に応じて独自のHAL実装内でこれをオーバーライドできます。

FMCバンクが正しく設定されていれば、TouchGFX Generatorでこれを選択できます。

ディスプレイ・インタフェース - パラレルRGB(FMC)

ディスプレイ・インタフェース: カスタム

8ビットのFMCインタフェース(またはSPI/DSIコマンド・モードなどの別のシリアル・インタフェース)を使用する場合、「カスタム」ディスプレイ・インタフェースをTouchGFX Generatorで選択する必要があります。 つまり、完全なHALは自動で生成できないので、ピクセルをフレームバッファ・メモリからディスプレイに手動で転送するための機能を、開発者が実装する必要があるのです。 これを実行するために必要なすべてのハンドルは、TouchGFX Generatorによって生成されます。 「カスタム」の選択方法を以下に示します。

ディスプレイ・インタフェース - カスタム

Tip
8ビットFMC、SPI、またはDSIコマンド・モード経由で接続されているディスプレイには、カスタム・ディスプレイ・インタフェースを選択する必要があります。

DMAの設定

CPUの負荷を最小化するために、フレームバッファからFMCにピクセル・データを転送するためのDMAチャネルを設定できます。 16ビットのパラレル・インタフェースを使用する場合、生成済みのLCD APIをオーバーライドして、DMA転送を代わりに実行することができます。 または、TouchGFXHAL.cppでAPIを拡張し、DMA転送機能をインクルードすることもできます。 FMCに対するGPDMAチャネル設定の例を以下に示します。

DMAの設定

フレームバッファをディスプレイに転送するためにHALレイヤ内のリクエストをプログラムする必要があるので、[Request]を[Software]に設定します。 [Source Address Increment]を有効にして、フレームバッファ・ポインタを移動し、[Destination Address Increment]を無効にして、FMCバンクのメモリ・アドレスに常に書き込みを行うようにします。

ユーザ・コード

一般的に、GRAMを内蔵したディスプレイの場合、生成されたTouchGFX HALハンドルのTouchGFXHAL.cppにおける実装では、以下の手順を実行して、ピクセルをディスプレイに転送し、ディスプレイとTouchGFX Engineを同期する必要があります。

  1. 「VSYNC」(ティアリング効果(TE)信号と呼ばれることもあります)を待ち、TouchGFX Engineに信号を送ります。
  2. 再描画されるフレームバッファの領域に基づいて、この領域に対応するGRAM内の場所に、「ディスプレイ・カーソル」および「アクティブ・ウィンドウ」(更新されるディスプレイ領域)を移動します。
  3. 送られてくるピクセル・データをGRAMに書き込む準備を整えます。 フレームバッファ戦略と使用するディスプレイ・インタフェースに応じて、これはフレームバッファのポインタのスワッピング、TouchGFX Engineの信号伝達、または前の転送の完了を待つことであったりします。
  4. ピクセル・データを送信します。

使用するディスプレイとフレームバッファ戦略によって、上記の手順の実装方法は異なります。 以下のセクションでは、FMCパラレル・インタフェースを持つGRAMディスプレイを使用する場合にこれらの手順を実装する方法を示します。

サポートされるフレームバッファ戦略

  • シングル
  • ダブル
  • パーシャル - GRAMディスプレイ
Further reading
TouchGFXのフレームバッファ戦略の概要については、「フレームバッファ戦略」の記事を参照してください。

シングル

FMCパラレル・インタフェースを使用してピクセル・データをディスプレイに転送すると、非常に広い帯域幅が提供されることがよくあります。 このため、多くのアプリケーションが「甘い考え」でフレームバッファを更新する場合があります。つまり、前のフレームがディスプレイに転送された後で次のフレームのレンダリングを開始するというやり方です。 もっと最適なアプローチとして、前のフレームを小さなチャンクに分けて転送し、TouchGFX Engineに信号を送って、それまでに転送済みのフレームバッファ領域で次のフレームのレンダリングを開始する方法があります。 この方法はLTDC駆動ディスプレイからの「スキャンライン」に続いてレンダリングする発想を模倣するものですが、今回に限っては「転送ライン」に続いてレンダリングします。

最適なディスプレイ転送とレンダリングを実現するには、正しいフレームバッファ戦略を使用するようにTouchGFX Engineを設定し、遅延関数を指定する必要があります。

TouchGFXHAL.cpp
void TouchGFXHAL::initialize()
{
// Other initialization code

registerTaskDelayFunction(&OSWrappers::taskDelay);
setFrameRefreshStrategy(REFRESH_STRATEGY_OPTIM_SINGLE_BUFFER_TFT_CTRL);

TouchGFXGeneratedHAL::initialize();
}

ディスプレイ転送を開始するには、ディスプレイのTE信号によって生成される外部割り込みを使用して、TouchGFX Engineに信号を送り、次のフレームのレンダリングを開始し、それまでにレンダリングしたフレームのディスプレイへの転送を開始するようにします。 通常、TE信号はパルス信号で、その立ち上がりエッジでディスプレイ・スキャンラインがアクティブ領域を離れ、立ち下がりエッジにで再びアクティブ領域に再び入ったことを示します。 ディスプレイ転送の帯域幅が、ディスプレイがピクセルをスキャンする帯域幅より大きい場合(FMCではよくある)は、TE信号の立ち上がりエッジで、ティアリングのリスクなしに安全に転送を開始できます。 その場合のコードを以下に示します。

TouchGFXHAL.cpp
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == LCD_TE_Pin)
{
// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();
GPIO::set(GPIO::VSYNC_FREQ);

if (refreshRequested)
{
refreshRequested = false;
nextSendToDisplayLine = refreshMinLine;
maxSendToDisplayLine = refreshMaxLine;
sendNextFrameBufferBlockToDisplay();
}
}
}
Note
ディスプレイ転送の帯域幅がディスプレイ・スキャンラインよりも高速であれば、TE信号の立ち上がりエッジで転送をティアリングのリスクなしで安全に開始できます。

上のコードでは、ヘルパ変数nextSendToDisplayLinemaxSendToDisplayLineが、前のフレームのディスプレイに転送される領域を示します。 これらの変数の値は、TouchGFX EngineによってTouchGFXHAL::flushFrameBufferで更新されます。これはフレームバッファの一部が更新されるたびに呼び出されます。 さらにフレームバッファは「dirty」とマークされ、次のTE信号でデータを転送する必要があるという信号になっています。

TouchGFXHAL.cpp
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
// Calling parent implementation of flushFrameBuffer(const touchgfx::Rect& rect).
TouchGFXGeneratedHAL::flushFrameBuffer(rect);

// In the below code, the refreshMinLine and refreshMaxLine variables are updated to span the
// smallest possible range, which covers all changes to the framebuffer.
int rectMin = rect.y;
int rectMax = rect.bottom();
refreshMinLine = MIN(rectMin, refreshMinLine);
refreshMaxLine = MAX(rectMax, refreshMaxLine);

refreshRequested = true; // Signal that the framebuffer has been updated this tick
}

フレームバッファ・ピクセルのチャンクをディスプレイに送信するヘルパ関数を、FMC関数を使用して実装する必要があります。

TouchGFXHAL.cpp
void sendNextFrameBufferBlockToDisplay()
{
maxDrawLine = nextSendToDisplayLine; // Update the area that TouchGFX framework is allow to draw to based on the progress of the transfer

const int32_t remainingLines = maxSendToDisplayLine - nextSendToDisplayLine;
if (remainingLines > 0)
{
// The display transfer is not done. Start transfer of the next block
const uint32_t sendBlockHeight = MIN(MAX_BLOCK_HEIGHT, remainingLines);
setDisplayWindow(0, nextSendToDisplayLine, DISPLAY_WIDTH, sendBlockHeight);
uint16_t* dataPtr = framebuffer + nextSendToDisplayLine * DISPLAY_WIDTH;
uint32_t dataSize = DISPLAY_WIDTH * sendBlockHeight * 2;
nextSendToDisplayLine += sendBlockHeight;
LCD_IO_SendDataDMA((uint8_t*)dataPtr, dataSize);
}
else
{
// The display transfer is done. Allow drawing to the entire framebuffer
maxDrawLine = DISPLAY_HEIGHT;
}
}

上のコードのremainingLines</CodeHeader>は、ディスプレイへ転送されていないライン数を示しています。 転送されていないラインがある場合は、<code>sendBlockHeightで転送するライン数を示します。 定数MAX_BLOCK_HEIGHTは転送サイズを限定します。 変数maxDrawLineは転送された領域の高ウォーターマークで、この値はTouchGFX Engineが次のフレームに描画する現在のラインを示します。 この値は転送の進捗に合わせてインクリメントされ、ダーティ・エリアの転送が完了するとDISPLAY_HEIGHTに設定され、TouchGFX Engineが次のフレームを完了できるようになります。 TouchGFX EngineはmaxDrawLineを照会して、更新可能なフレームバッファの量を確認します。

TouchGFXHAL.cpp
uint16_t TouchGFXHAL::getTFTCurrentLine()
{
return maxDrawLine;
}

1つのブロックが転送されると、転送完了のコールバックを使用して次の転送が開始されます。

TouchGFXHAL.cpp
static void DMA_TxCpltCallback(DMA_HandleTypeDef* hdma)
{
if (hdma == DISPLAY_DMA)
{
sendNextFrameBufferBlockToDisplay();
}
}

フレームバッファのすべてのダーティ・エリアがディスプレイに転送されるまで、これを繰り返します。

上記のサイクルは、次のTE信号を受信すると繰り返されます。

リファレンス実装

TouchGFX Board Setup NUCLEO-U575ZI + RVA35HIには、FMCを使用してレンダリング/転送が最適化されたシングル・フレームバッファ戦略のリファレンス実装が含まれています。

ダブル

シングル・フレームバッファと比べてダブル・バッファでは、1つのフレームバッファから前のフレームが転送される間に、TouchGFX Engineが次のフレームを他のフレームバッファに自由に書き込みできます。 これにより、ディスプレイ転送を待つ必要がなくなるため、レンダリング時間が最適化されます。

ディスプレイ転送を開始するには、ディスプレイのTE信号によって生成される外部割り込みを使用して、TouchGFX Engineに信号を送り、フレームバッファをスワップして次のフレームのレンダリングを開始します。 ディスプレイは、それまでにレンダリングしたフレームをディスプレイに転送するよう設定されます。

TouchGFXHAL.cpp
void LCD_SignalTearingEffectEvent(void)
{
// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();

if (refreshRequested)
{
// Swap frame buffers immediately instead of waiting for the task to be scheduled in.
// Note: task will also swap when it wakes up, but that operation is guarded and will not have
// any effect if already swapped.

touchgfx::HAL::getInstance()->swapFrameBuffers();

// Set window, enable display reading to GRAM, transmit buffer using DMA
setWindow(0, 0, 240, 240);
LCD_IO_WriteReg(ST7789H2_WRITE_RAM);
dmaCompleted = false;
LCD_IO_SendDataDMA((uint8_t*)TFTframebuffer, (240 * 240 * 2));
}
}

上のコードでは、ディスプレイの解像度は240x240ピクセル(16ビットRGB565フレームバッファ)なので、ウィンドウはディスプレイ領域全体に設定されます。 帯域幅に制約がある場合、ウィンドウを小さくしてダーティ・エリアのみを含むようにすることができます。 このためには、TouchGFX HAL関数TouchGFXHAL::flushFrameBufferを使用できます。

TouchGFX HAL関数TouchGFXHAL::beginFrame()TouchGFXHAL::endFrame()は、転送が必ず実行されるように要求するためのもので、つまりフレームバッファがダーティとマークされます。

TouchGFXHAL.cpp
bool TouchGFXHAL::beginFrame()
{
refreshRequested = false;

return TouchGFXGeneratedHAL::beginFrame();
}

void TouchGFXHAL::endFrame()
{
TouchGFXGeneratedHAL::endFrame();

if (frameBufferUpdatedThisFrame)
{
refreshRequested = true;
}
}

ディスプレイに転送されるフレームバッファのアドレスは、メソッドTouchGFXHAL::setTFTFrameBuffer()およびTouchGFXHAL::getTFTFrameBuffer()によって維持されます。

TouchGFXHAL.cpp
uint16_t* TouchGFXHAL::getTFTFrameBuffer() const
{
return TFTframebuffer;
}

void TouchGFXHAL::setTFTFrameBuffer(uint16_t* address)
{
TFTframebuffer = address;

TouchGFXGeneratedHAL::setTFTFrameBuffer(address);
}

TE割り込みハンドラ内のポインタTFTframebufferを使用して、ディスプレイに転送されるウィンドウのソース・アドレスを設定します。

リファレンス実装

TouchGFX Board Setup STM32H573-DK Int. Flashには、FMCを使用するダブル・フレームバッファ戦略のリファレンス実装が含まれています。

パーシャル - GRAMディスプレイ

TocuhGFX Generatorで「パーシャル - GRAMディスプレイ」フレームバッファ戦略を選択する場合、ユーザはパーシャル・フレームバッファ・ブロックの数とそれらのサイズを設定する必要があります。 TouchGFX Generatorによって生成されるコードでは、パーシャル・フレームバッファ・ブロックの状態を追跡し、フレームバッファ戦略を設定するために、FrameBufferAllocatorがインスタンス化されます。

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

void TouchGFXGeneratedHAL::initialize()
{
HAL::initialize();
registerEventListener(*(Application::getInstance()));
// Partial framebuffer strategy
setFrameBufferAllocator(&blockAllocator);
setFrameRefreshStrategy(HAL::REFRESH_STRATEGY_PARTIAL_FRAMEBUFFER);
}

もう1つのクラスのPartialFrameBufferManagerは、パーシャル・フレームバッファ・ブロック間の同期、ブロックの転送、ディスプレイのリフレッシュレートのタイミングを処理します。 生成されたコードは、PartialFrameBufferManagerで公開されたAPIを使用して、 TouchGFXGeneratedHAL::flushFrameBuffer内のフレームバッファ・ブロックを転送します。

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

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

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

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

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

上のコードでは、TouchGFXHAL.cppでディスプレイ・ドライバに実装できるC関数に呼び出しをフォワードします。

下の例の実装では、PartialFrameBufferManagerからのパラメータによってFMC API関数を呼び出しています。

TouchGFXHAL.cpp
extern "C" void touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
isTransmittingBlock = true;

setDisplayWindow(x, y, w, h);

LCD_IO_SendDataDMA((uint8_t*)pixels, w * h * 2);
}

関数touchgfxDisplayDriverTransmitActiveは、単純にグローバル変数isTransmittingBlockの値を返します。

TouchGFXHAL.cpp
extern "C" int touchgfxDisplayDriverTransmitActive()
{
return isTransmittingBlock ? 1 : 0;
}

この値は転送完了のコールバックでリセットされます。

関数touchgfxDisplayDriverShouldTransferBlockは、ディスプレイが新しいブロックを受信する準備ができたかどうかチェックします。 パーシャル・フレームバッファ・ブロックはディスプレイ・スキャンラインのバックグラウンドで送信されます。 たとえば、ディスプレイ・スキャンラインが50行目までスキャン済みで、転送するブロックの一番下が70行目である場合、TouchGFX Engineはディスプレイが70行目以下まで進むのを待ってからブロックの転送を開始する必要があります。 これにより、ディスプレイ上のティアリングが防止されます。

多くのディスプレイはこの機能をサポートしていないので、ハードウェアのタイマを使用して、ラインごとにカウンタをインクリメントすることでライン(何行目か)を推定します。 カウンタのインクリメントのペースは、理想的には、各フレームの持続時間(ms)/ ディスプレイの高さ(ピクセル)で求められます。たとえば、16.67 ms / 320ピクセル = 52.1 us(1ライン当たり)になります。 Line Timerを使用したtouchgfxDisplayDriverShouldTransferBlockの実装例を以下に示します。

TouchGFXHAL.cpp
extern "C" int touchgfxDisplayDriverShouldTransferBlock(uint16_t bottom)
{
// Only allow block transfer if the display has drawn past the bottom of the requested block (plus a margin of two lines)
// A timer is used to estimate how many lines have been drawn by setting the prescaler so the tick rate matches the line draw rate
uint16_t lastLineDrawn = LL_TIM_GetCounter(LINE_TIMER);
return bottom + 2 < lastLineDrawn || tearingEffectCount > 0;
}

上のコードでは、一番下(マージンを足して)のライン数がディスプレイのライン・カウンタよりも小さいか、 tearingEffectCountが0より大きい場合、関数はtrueを返します。 tearingEffectCountが0より大きい場合は、ディスプレイがTE信号を発行済みで、現時点で転送するブロックと同じフレーム(ただしディスプレイのY軸の背後)にいるということなので、ブロックを転送する必要があります。

Tip
「void waitUntilTransmitEnd()」および「void waitUntilCanTransferBlock(uint16_t bottom)」というフックを使用して、ディスプレイ転送の進行中、またはディスプレイが新しいブロックを受信する準備ができていないときの動作を定義できます。 デフォルトの動作は「OSWrappers::taskYield()」です。

tearingEffectCountがTE割り込みハンドラでインクリメントされ、TouchGFX Engineには次のフレームのレンダリングを開始する信号が送られます。

TouchGFXHAL.cpp
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == LCD_TE_Pin)
{
// Code to handle TE signal. Could be fine-tuning the display line timer frequency

tearingEffectCount++;

// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();

GPIO::set(GPIO::VSYNC_FREQ);
startRenderingImmediately = true;
}
}

次のフレームを開始するときに、tearingEffectCountをリセットします。

TouchGFXHAL.cpp
bool TouchGFXHAL::beginFrame()
{
tearingEffectCount = 0;
return TouchGFXGeneratedHAL::beginFrame();
}

リファレンス実装

TouchGFX Board Setup NUCLEO-H563ZI + RVA35HIには、FMCおよびアダプティブ・ディスプレイのLine Timer設定を使用するパーシャル・フレームバッファ戦略のリファレンス実装が含まれています。