跳轉到主要內容

FMC 顯示器介面

本情境說明如何在使用具有 FMC 平行介面和 GRAM 的顯示器時,設定 STM32 FMC (彈性記憶體控制器) 以及 TouchGFX Generator

Note
此情境假設在開發板初次啟動階段,已經開發出可用的顯示器驅動程式。 驅動程式必須能夠將像素傳輸到顯示器,並可以控制顯示器的記憶體寫入位置。 如需進一步的詳細資訊,請查看顯示器的資料手冊。

設定

FMC 設定

在 STM32CubeMX 類別清單中從連線群組啟用 FMC。 從這裡開始,必須將其中一個可用的 FMC 庫設定為 LCD 介面。 操作方法是在 STM32CubeMX 中將記憶體類型設定為 LCD 介面。 在這裡,也會選擇將像素傳輸到顯示器的平行資料線的數量,即 16 位元或 8 位元平行介面。

FMC Bank配置

務必正確設定所用顯示器的時間。 驗證 MCU 的 FMC 基底位址,以及用於與顯示器互動的庫。

TouchGFX Generator

顯示器介面:平行 RGB (FMC)

如果使用 16 位元平行介面,TouchGFX Generator 可以產生一個 HAL,它提供一個簡單的 API,其中包含預設實作,用於讀取/寫入資料和暫存器到 LCD:

TouchGFXGeneratedHAL.cpp
...
/**
* @brief 將 LCD IO 初始化。
* @param None
* @retval None
*/
__weak void LCD_IO_Init(void)
{
// Already Done by MX_FMC_Init()
}

/**
* @brief 將資料寫入 LCD 資料暫存器。
* @param Data: 待寫入資料
* @retval None
*/
__weak void LCD_IO_WriteData(uint16_t RegValue)
{
/* 寫入 16-bits 暫存器 */
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 負載,可以設定 DMA 通道以將像素資料從影像緩衝區傳輸到 FMC。 如果使用 16 位元平行介面,則可以覆蓋產生的 LCD API 來執行 DMA 傳輸。 另外,API 可以在 TouchGFXHAL.cpp 中擴充以包括 DMA 傳輸函數。 以下是 FMC 的 GPDMA 通道設定範例:

DMA 設定

請求設定為軟體,因為我們需要在 HAL 層編寫請求,以將影像緩衝區傳輸到顯示器。 我們啟用來源位址遞增以移動影像緩衝區指標,並停用目標位址遞增以始終寫入 FMC 庫記憶體位址。

使用者程式碼

一般而言,對於具有嵌入式 GRAM 的顯示器,實作 TouchGFXHAL.cpp 中產生的 TouchGFX HAL 控制指標應執行以下步驟,以將像素傳輸到顯示器並使顯示器與 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 更新,每當影像緩衝區的一部分被更新時就會呼叫它。 影像緩衝區也被標記,表示我們需要在下一個 TE 訊號上傳輸資料。

TouchGFXHAL.cpp
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
// 呼叫 flushFrameBuffer(const touchgfx::Rect& rect) 的父層實作。
TouchGFXGeneratedHAL::flushFrameBuffer (rect);

// 以下的程式碼中,refreshMinLine 和 refreshMaxLine 變數經過更新以涵蓋
// 最小可能範圍,涵蓋對影像緩衝區的所有變更。
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; // 更新 TouchGFX 框架可依據傳輸速度繪製的區域

const int32_t remainingLines = maxSendToDisplayLine - nextSendToDisplayLine;
if (remainingLines > 0)
{
// 顯示傳輸未結束。 開始傳輸下個區塊
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
{
// 顯示傳輸結束。 Allow drawing to the entire framebuffer
maxDrawLine = DISPLAY_HEIGHT;
}
}

在上列程式碼中,remainingLines 是指剩餘要傳輸到顯示器的行數。 如果還有剩餘的行,我們會將 sendBlockHeight 定義為正在傳輸的行數。 常數 MAX_BLOCK_HEIGHT 限制了傳輸的大小。 變數 maxDrawLine 是已傳輸區域的高水位線,其決定了 TouchGFX Engine 在下一個影格中允許繪製的目前線。 此值隨著傳輸進行而遞增,並在髒區域的傳輸完成時設定為 DISPLAY_HEIGHT,以允許 TouchGFX Engine 完成下一個影格。 TouchGFX Engine 將查詢 maxDrawLine 以瞭解 允許更新多少影像緩衝區:

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

一旦區塊被傳輸,會利用傳輸完成回呼啟動下一次傳輸:

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

此操作會重複,直到影像緩衝區的所有髒區域均傳輸到顯示器。

當接收到下一個 TE 訊號時,上述循環將重複。

參考實作

TouchGFX 開發板設定 NUCLEO-U575ZI + RVA35HI 包含使用 FMC 最佳化渲染/傳輸的單一影像緩衝區策略參考實作:

雙重

與單一影像緩衝區相比,雙重緩衝允許 TouchGFX Engine 自由地將下一影格寫入一個影像緩衝區,同時從另一個影像緩衝區傳輸前一影格。 這提供了最佳的渲染時間,因為不需要等待顯示器傳輸。

為了啟動顯示器傳輸,會利用顯示器 TE 訊號產生的外部中斷來向 TouchGFX Engine 發出訊號,以交換影像緩衝區並開始渲染下一個影格。 顯示器設定為將先前渲染的影格傳輸到顯示器:

TouchGFXHAL.cpp
void LCD_SignalTearingEffectEvent(void)
{
// VSync 已發生,遞增 TouchGFX 引擎的 vsync 計數器
HAL::getInstance()->vSync();
// VSync 已發生,通知 TouchGFX 引擎
OSWrappers::signalVSync();

if (refreshRequested)
{
// 立即交換影像緩衝區而不等待任務加入排程。
// 註:任務在喚醒時也會交換,但操作受到保護,
// 如果已經交換,則不會產生任何影響。

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);
}

指標 TFTframebuffer 在 TE 中斷處理程序中用於設定傳輸到顯示器的視窗的來源位址。

參考實作

TouchGFX Board Setup STM32H573-DK Int. Flash 包含使用 FMC 的雙重影像緩衝區策略的參考實作:

局部-GRAM 顯示器

選擇 TouchGFX Generator 中的局部-GRAM 顯示器影像緩衝區策略時,使用者必須設定局部影像緩衝區塊的數量及其大小。 TouchGFX Generator 產生的程式碼將會實例化 FrameBufferAllocator 來追蹤局部影像緩衝區塊的狀態並設定影像緩衝區策略:

TouchGFXGeneratedHAL.cpp
// 局部影像緩衝區策略用的影像緩衝區分配器
static ManyBlockAllocator<2048, /* block size */
3, /* number of blocks */
2 /* bytes per pixel */
> blockAllocator;

void TouchGFXGeneratedHAL::initialize()
{
HAL::initialize();
registerEventListener(*(Application::getInstance()));
// 局部影像緩衝區策略
setFrameBufferAllocator(&blockAllocator);
setFrameRefreshStrategy(HAL::REFRESH_STRATEGY_PARTIAL_FRAMEBUFFER);
}

另一個 PartialFrameBufferManager 類別處理局部影像緩衝區塊之間的同步、區塊的傳輸以及顯示器更新率的時間。 產生的程式碼利用 PartialFrameBufferManager 公開的 API 來傳輸 TouchGFXGeneratedHAL::flushFrameBuffer 中的影像緩衝區塊:

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

PartialFrameBufferManager 使用三個函數與顯示器驅動程式程式碼互動。 這些必須在 TouchGFX 開發板設定中實作:

TouchGFXGeneratedHAL.cpp
/**
* 檢查影像緩衝區區塊是否在傳輸。
*/
int transmitActive()
{
return touchgfxDisplayDriverTransmitActive();
}

/**
* 檢查影像緩衝區區塊結尾是否可傳輸。
*/
int shouldTransferBlock(uint16_t bottom)
{
return touchgfxDisplayDriverShouldTransferBlock(bottom);
}

/**
* 傳輸影像緩衝區區塊。
*/
void transmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
touchgfxDisplayDriverTransmitBlock(pixels, x, y, w, h);
}

上述程式碼將呼叫轉發到 C 函數,其可在 TouchGFXHAL.cpp 內實作於顯示器驅動器。

在下列範例中,實作使用來自 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。 使用行計時器touchgfxDisplayDriverShouldTransferBlock 實作範例如下所示:

TouchGFXHAL.cpp
extern "C" int touchgfxDisplayDriverShouldTransferBlock(uint16_t bottom)
{
// 如果顯示已超過所要求區塊的底部 (加上兩行的邊界),僅允許區塊傳輸
// 使用計時器預估已繪製幾行,方法為設定預除器,讓 tick 速度配合行繪製速度
uint16_t lastLineDrawn = LL_TIM_GetCounter(LINE_TIMER);
return bottom + 2 < lastLineDrawn || tearingEffectCount > 0;
}

在上列程式碼中,如果底部 (加上一些邊距) 小於顯示器行計數器,或者 tearingEffectCount 大於 0,函數會回傳 true。 如果 tearingEffectCount 大於 0,表示顯示器已經發出 TE 訊號,並且現在與待傳輸的區塊位於同一影格 (但在顯示器 Y 軸之後),亦即必須傳輸該區塊。

Tip
Hook「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)
{
// 處理 TE 訊號的程式碼。 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 開發板設定 NUCLEO-H563ZI + RVA35HI 包含使用 FMC 和自適應顯示器行計時器設定的局部影像緩衝區策略參考實作: