抽象層架構
如前一節所述,TouchGFX AL具有一套特定的職責。 Responsibilities are either implemented in the hardware part of the AL (HAL) or the part of the AL that synchronizes with TouchGFX Engine, typically through an RTOS (OSAL). The following table summarizes these responsibilities which were outlined in the previous section:
職責(Responsibility) | 作業系統或硬體 |
---|---|
將TouchGFX引擎主迴圈與顯示器的傳輸作同步 | 作業系統與硬體 |
回報觸摸銀幕與實體按鈕事件 | 硬體 |
同步影像緩衝區的存取 | 作業系統 |
回報下一個可用的影像緩衝區 | 硬體 |
執行渲染算圖操作 | 硬體 |
影像緩衝區到影示器的傳輸處理 | 硬體 |
以下每個小節對為實現上述職責應所採取的措施作重點介紹。 For custom hardware platforms the TouchGFX Generator, inside STM32CubeMX, can generate most of the AL and accompanying TouchGFX project. The remaining parts, that the AL developer must implement manually, are pointed out through code comments and notifications through the TouchGFX Generator. Read more about the TouchGFX Generator in the next section.
抽象層物件類別
The TouchGFX AL is accessed by the TouchGFX Engine through concrete sub-classes and through class implementation files (.cpp) where member functions of classes defined in the TouchGFX Engine are implemented. These sub-classes and class implementation files are generated by the TouchGFX Generator. The TouchGFX Generator can generate both the part of the HAL that reflects configurations from STM32CubeMX, as well as the OSAL for the used RTOS. Please read the section on TouchGFX Generator for further details. 通常,HAL的架構如下圖所示。
將TouchGFX引擎主迴圈與顯示器的傳輸作同步
The main idea behind this step is to block the TouchGFX Engine main loop when rendering is done, ensuring that no further frames are produced. 一旦顯示準備就緒,OSAL向被阻塞擱置的引擎主迴圈發出信號以繼續產生顯示圖框(frame)。
In order to fulfill this responsibility the typical way of a TouchGFX AL is to utilize the engine hook Rendering done and the interrupt Display Ready, as outlined in Responsibilities of the Abstraction Layer. The OSAL defines a function OSWrappers::signalVSync
in which developers can signal the semaphore that the engine waits upon when called.
Tip
渲染算圖完成
在渲染算圖完成之後, TouchGFX Engine會呼叫渲染算圖完成的鉤子OSWrappers::waitForVSync
。
When implementing this OSAL method, the Abstraction Layer must block the graphics engine until it is time to render the next frame. 實作該阻塞擱置的標準方法是阻止由訊息佇列(message queue)進行讀取。 The HAL developer is free to use any method to implement the block if this is not feasible.
Tip
When OSWrappers::signalVSync
is signaled (or the semaphore/queue used in OSWrappers::waitForVSync
is signaled) TouchGFX will start rendering the next application frame. The following code based on CMSIS V2 causes the TouchGFX engine to block until an element is added to the queue by another part of the system, typically an interrupt synchronized with the display.
OSWrappers.cpp (CMSIS V2)
static osMessageQueueId_t vsync_queue = NULL; //Queue identifier is assigned elsewhere
void OSWrappers::waitForVSync()
{
uint32_t dummyGet;
// First make sure the queue is empty, by trying to remove an element with 0 timeout.
osMessageQueueGet(vsync_queue, &dummyGet, 0, 0);
// 然後等待下一次VSYNC。
osMessageQueueGet(vsync_queue, &dummyGet, 0, osWaitForever);
}
如果不使用RTOS,TouchGFX Generator為waitForVSync
提供了以下使用揮發性易失變數(volatile variable)方式的實作。
OSWrappers.cpp (No OS)
static volatile uint32_t vsync_sem = 0;
void OSWrappers::waitForVSync()
{
if(vsync_sem)
{
vsync_sem = 0;
// Signal TouchGFX to start rendering next frame
...
}
}
Tip
顯示就緒
The Display ready signal to unblock the main loop should come from an interrupt from a display controller, from the display itself or even from a hardware timer. The source of the signal is dependent on the type of display.
OSWrappers
物件類別為此信號定義了一個函式:OSWrappers::signalVsync
。 The implementation of the function must unblock the main loop by satisfying the wait condition used in OSWrappers::waitForVSync
.
Continuing from the above CMSIS V2 example, the following code puts a message into the message queue vsync_queue
which unblocks the TouchGFX Engine.
OSWrappers.cpp (CMSIS V2)
void OSWrappers::signalVSync()
{
osMessageQueuePut(vsync_queue, &dummy, 0, 0);
}
This OSWrappers::signalVSync
method must be called at hardware level from an interrupt for e.g. an LTDC, an external signal from the display, or a hardware timer.
如果不使用RTOS,則使用變數,並分配一個非零值以打破while循環。
OSWrappers.cpp (No OS)
void OSWrappers::signalVSync()
{
vsync_sem = 1;
}
回報觸摸銀幕與實體按鈕事件
Before rendering a new frame, the TouchGFX Engine collects external input from the TouchController
and ButtonController
interfaces.
銀幕觸控座標
Coordinates from the touch controller are translated into click-, drag- and gesture events by the TouchGFX Engine and passed to the application.
The way touch coordinated from the touch controller is accessed by the TouchGFX Engine is by passing an implementation
The following code is generated by the TouchGFX Generator:
TouchGFXConfiguration.cpp
static STM32TouchController tc;
...
static TouchGFXHAL hal(dma, display, tc, 390, 390);
在TouchGFX Engine渲染算圖(render)的過程中,引擎呼叫tc物件的sampleTouch()
函式來收集輸入。
bool STM32TouchController::sampleTouch(int32_t& x, int32_t& y)
AL開發人員提供的實作應該讀取銀幕觸控座標的x和y值,並回傳是否偵測到銀幕觸控(真或偽)。
Tip
有多種實作此函式的方法:
- 在sampleTouch() 中輪詢(polling):透過發送請求並等待輪詢結果,可從硬體觸控控制器(通常為I2C)讀取螢幕觸摸狀態。 這會影響應用程式的總體渲染算圖時間,因為I2C的往返傳輸通常長達1ms,在此期間圖形引擎會被阻塞擱置(blocked)。
- 使用中斷:另一種可能是使用中斷。 I2C讀取的指令由定時器定期啟動,或者以回應觸控硬體控制器的外部中斷來啟動。 當I2C資料就緒時(可視為另一個中斷),此資料可透過訊息佇列(message queue)或全域變數將資料提供給
STM32TouchController
。 以下展示了由TouchGFX Generator所產生的STM32TouchController.cpp
程式碼sampleTouch
在RTOS系統中的可能樣貌:
STM32TouchController.cpp
bool STM32TouchController::sampleTouch(int32_t& x, int32_t& y)
{
if (osMessageQueueGet(mid_MsgQueue, &msg, NULL, 0U) == osOK)
{
x = msg.x;
y = msg.y;
return true;
}
return false;
}
此檔案的位置將在下一章的TouchGFX Generator中說明
其他的外部事件
按鈕控制器介面touchgfx::ButtonController
可用於將硬體訊號(按鈕或其他)映射到應用程式事件。 可在TouchGFX Designer中設定對這些事件的反應方式。
該介面的使用與上述觸控控制器類似,只是並不一定要具有ButtonController。 要使用此介面,請建立一個具有ButtonController
介面實作的實體物件,並將指向此實體物件的參照(reference)傳遞至HAL
MyButtonController.cpp
class MyButtonController : public touchgfx::ButtonController
{
bool sample(uint8_t& key)
{
... //Sample IO, set key, return true/false
}
};
TouchGFXConfiguration.cpp
static MyButtonController bc;
void touchgfx_init()
{
...
hal.initialize();
hal.setButtonController(&bc);
}
在每個圖框之前都會呼叫上述ButtonController物件類別當中的樣本方法。 當回傳真值時,鍵值(key value)將被傳遞至目前螢幕(screen)的handleKeyEvent事件處理程式。
Further reading
同步影像緩衝區的存取
多個執行單元(actors)可能都會對影像緩衝區進行存取。
1 | CPU | 在渲染算圖期間讀取和寫入像素 |
2 | DMA2D* | 在硬體輔助的渲染算圖期間讀取和寫入像素 |
3 | LTDC | 在傳輸到並列RGB顯示器期間讀取像素 |
4 | DMA | 在傳輸到SPI顯示器期間讀取像素 |
TouchGFX引擎透過OSWrappers
介面來同步影像緩衝區的存取,而對於同時也希望存取影像緩衝區的週邊(如DMA2D)也必須執行相同操作(透過OSWrapper對影像緩衝區進行存取)。 常見的設計是使用旗號(semaphore)來確保(guard)對影像緩衝區的存取,但也可以使用其它的同步機制。
下表顯示了OSWrappers
物件類別(OSWrappers.cpp)中的函式列表,包含了由TouchGFX Generator產生的函式或由使用者手動生成的函式。
物件方法 | 描述 |
---|---|
takeFrameBufferSemaphore | 由圖形引擎呼叫以獲取對影像緩衝區的獨佔存取。 這將阻塞擱置引擎直到DMA2D完成(若正在執行的話) |
tryTakeFrameBufferSemaphore | 確保已取得旗號鎖定(lock)。 此方法不會阻塞擱置,但下一次呼叫takeFrameBufferSemaphore時會阻塞擱置呼叫者 |
giveFrameBufferSemaphore | 釋放影像緩衝區的旗號鎖定(lock) |
giveFrameBufferSemaphoreFromISR | 在中斷程式當中釋放影像緩衝區的旗號鎖定(lock) |
Tip
回報下一個可用的影像緩衝區
無論採用哪種渲染算圖的策略,TouchGFX引擎在每個時標(tick)都必須知道應將像素渲染到哪個記憶體區域。 使用單一影像緩衝或雙影像緩衝策略時,TouchGFX引擎將根據影像緩衝區的寬度、高度和位元深度(bit depth)將像素資料寫入記憶體區域。 圖形引擎負責雙影像緩衝區配置當中兩個影像緩衝區之間的交換。
可以將影像緩衝區的存取局限在部分的影像緩衝區。 可在HAL物件子類別中重新實作HAL::getTFTCurrentLine()
物件方法(method)。 回傳上面用於圖形引擎繪製而保存的行號。
使用局部影像緩衝區策略時,開發人員需定義TouchGFX引擎在渲染算圖時使用的記憶體區塊(一個或多個)。 由此閱讀更多相關資訊。
Tip
執行渲染算圖
應用程式不是只需要作渲染算圖或是只作圖像顯示。 還有其他工作也需要使用CPU。 TouchGFX的目標之一是盡可能降低CPU資源的使用來繪製使用者介面。 HAL物件類別對許多STM32微控制器(或其他硬體功能)上的DMA2D功能進行抽象化以利圖形引擎的使用。
將圖資(如點陣圖)渲染演算至影像緩衝區時,TouchGFX引擎檢查HAL是否有能力對局部或者全部的點陣圖進行點陣疊圖(blit)至影像緩衝區。 如果有此功能,則將繪圖操作委託給HAL而不是由CPU直接處理。
引擎呼叫物件方法HAL::getBlitCaps()
以取得硬體能力的描述。 可在HAL物件子類別當中重新實作此呼叫以添加硬體能力的描述。
引擎在繪製使用者介面時呼叫HAL物件類別上的操作(如HAL::blitCopy
)將其排入DMA操作佇列。 如果HAL回報不具備所需的能力,則圖形引擎將退而使用軟體方式來渲染算圖。
Tip
影像緩衝區到影示器的傳輸處理
為將影像緩衝區的內容傳輸到顯示器,TouchGFX AL經常使用「區域渲染完畢(Rendering of area complete)」的掛鉤。 一旦部分影像緩衝區的渲染算圖完成後,引擎就會向AL發送信號。 AL可選擇如何將此影像緩衝區這部分的內容傳輸到顯示器。
區域渲染完畢
在程式碼當中的掛鉤(hook)為虛擬函式HAL::flushFrameBuffer(Rect& rect)
。
在帶有LTDC控制器的STM32微控制器上我們不需要在每次渲染算圖之後執行任何用於影像緩衝區傳輸的操作。 在完成LTDC初始化之後,該傳輸將以給定的頻率連續執行,因此我們可以在此物件方法的實作留白不作任何處理。
但對於其他類型的顯示器(如SPI或8080)來說就需要自行手動來實作影像緩衝區的傳輸。
這函式讓開發者可以發起該影像緩衝區域向顯示器GRAM的手動傳輸:
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& r)
{
HAL::flushFrameBuffer(rect); //call superclass
//start transfer if not running already!
if (!IsTransmittingData())
{
const uint8_t* pixels = ...; // Calculate pixel address
SendFrameBufferRect((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
else
{
... // Queue rect for later or wait here
}
}