跳轉到主要內容

作業系統

引言

本節將討論圖形化使用者介面應用中作業系統的使用。

嵌入式設備越來越先進。 系統的大部分設備不僅處理圖形化使用者介面,通常還處理複雜的控制演算法和任務。

舉例來說,這些任務可以是電機控制、資料獲取或安全相關任務。 許多先進設備包含用來與資料中心通信的通信協議棧(如TCP/IP)或用來與其他本地設備通信的射頻協定棧(如藍牙)。

其他任務與使用者介面的交替

在具有圖形化使用者介面並只支援幾項簡單任務的簡單設備中,可圍繞使用者介面程式碼建構整個應用。 除了常規的使用者介面升級,應用執行的任務非常少,因此可將其他任務的執行相當成功地嵌入使用者介面程式碼。

當設備包含具有單獨時序要求的更高級的“後臺運行”功能(如馬達調控)時,將難以在滿足要求的同時將兩項任務合二為一。

正如我們在之前的文章中討論的那樣,圖形引擎必須持續繪製新幀,才能支援流暢的使用者介面。 如果在運行其他任務時圖形引擎暫停此過程,幀率將會下降。 同樣地,如果其他任務只在幀之間、閒置時間運行,那麼在使用者介面渲染複雜場景時,由於閒置時間較少,這些任務會受到影響。 這些影響使得UI任務與其他複雜任務的手動交替變得困難。

範例

在本節剩餘部分,我們將建構一個具有顯示器的藍牙揚聲器。 我們有3個主要任務:運行圖形化使用者介面,將音樂輸入揚聲器,以及處理藍牙棧以便與其他設備通信。

不難看出,以使用者介面為中心的應用架構並不是好的選擇:假設我們將音樂程式碼與使用者介面混合,並將啟動重播的程式碼放在使用者介面上某個按鈕的事件處理器中。 現在,需要一點時間才能開始播放音樂,期間使用者介面被鎖定。 與此同時,運行的任何動畫都將停止。

一般情況下,使用者介面的回應性開始依賴於音樂任務的執行時間(開始、停止和下一首等)。 這是一個一般的問題,我們稍後再做討論。

如果還想從藍牙播放音樂,會發生什麼? 使用者介面是否應以某種方式介入其中?

我們如何為音樂任務分配優先順序,以避免音樂暫停? 與此同時,我們還希望在沒有音樂任務運行時使用者介面以最高性能運行。

作業系統可通過任務、通信手段和同步來解決所有這些問題。

RTOS

即時作業系統是一個小軟體,它通過各種服務為應用提供支援,並為應用中的任務分配計算資源。

RTOS幫助您在許多獨立但相互協作的任務中構建應用。 然後,在要用到這些任務時,RTOS會根據任務的優先順序執行這些任務。

我們甚至可以將一項作業分割成一個高優先順序任務和一個低優先順序任務。 假設我們必須在藍牙資料到達時非常快速地從緩衝區讀取資料,並將它們放入較大的應用緩衝區。 資料處理可能會稍微延遲。 這樣一來,我們將有兩個藍牙任務。

在本例中,我們從主函數開始4項任務:

int main() {
...
os_start_task(gui_task, medium_priority);
os_start_task(music_task, low_priority);
os_start_task(bt_comm_task, high_priority);
os_start_task(bt_appl_task, low_priority);
os_start_scheduler();
}

對音樂任務進行類似分割:一個將資料輸送到揚聲器的高優先順序任務,一個控制播放哪首歌曲並向使用者介面發送通知的低優先順序任務。

使用上述不同優先順序的結果是:當有資料要處理時,運行bt_comm_task;否則,運行使用者介面任務。 當使用者介面任務等待顯示器時,兩個低優先順序任務可以運行。 操作系統調度程式將為我們處理此類時間分配。

在典型的TouchGFX應用中,使用者介面在每一幀中等待顯示器,它還定期等待圖形加速器ChromArt,以便完成繪製元素。 這意味著會有許多短暫的暫停,優先順序較低的任務可以在暫停期間運行。 作業系統調度程式將自動更改MCU,以便在優先順序較高的任務等待時運行這些任務。

Task communication

當我們使用多個任務時,還需要一種安全的通信方式用於任務間的通信。 舉個簡單的例子,從使用者介面到音樂任務。 除其他情況外,這裡我們需要音樂任務進行等待,直至gui_task要求其開始播放歌曲。 一種簡單的實現方式是使用訊息佇列(queue)。 在佇列中出現訊息之前,音樂任務休眠。 當佇列中出現消息時以及優先順序較高的任務不忙碌時,調度程式 喚醒任務。

   ...
music_task_input_queue = os_create_queue(10); //10 element queue
...

在使用者介面中,當按下“播放”時,我們向音樂任務的佇列發送一條訊息:

void ScreenMusic::handlePlayPressed()
{
os_send_message(music_task_input_queue, play_message);
}

音樂任務能夠以讀取佇列的方式等待消息。 這會在有消息到達前阻止任務:

...
Message message;
os_receive_message(music_task_input_queue, &message);

在將訊息放入音樂任務的佇列後,使用者介面繼續運行並儘快渲染幀。 我們不將時間浪費在立即處理播放訊息上。 但是,在渲染完成後,在渲染下一幀之前UI任務處於等候狀態,調度程式改為執行音樂任務,此任務將處理傳入的訊息。

同樣地,我們還可以給予使用者介面一個輸入佇列。 然後,音樂任務可以發送通知消息,例如在歌曲結束時。 使用者介面任務不應等待訊息,而應快速檢查是否有訊息(無攔截)並讀取它(如果有)。

此設置在系統中的任務之間提供了一種十分鬆散的連接。 我們實際上無需使用使用者介面即可測試音樂任務,我們還可以輕鬆地從藍牙任務播放音樂。

處理中斷

一些任務需要作為對中斷的回應來運行。 在我們的範例中,藍牙通信任務就是這樣一個例子。 我們希望任務在藍牙晶片有新的資料包時運行。 假設我們能夠在那種情況下實現中斷,我們可以從中斷處理器發送一條消息:

void BT_DataAvailable_Handler(void)
{
os_send_message(bt_data_queue, data_available_message);
}

除了佇列,還有其他同步基元可用。 例如,許多作業系統中都有Semaphore和mutexe。

FreeRTOS

在開發期間,用FreeRTOS作業系統測試了TouchGFX。 TouchGFX的要求非常低並能在許多其他作業系統上運行,但FreeRTOS是一個好的起點,除非您有許多特殊的要求。

FreeRTOS是一種簡單的作業系統,在商業應用中可免費使用。 它以原始程式碼的形式隨STM32Cube韌體的提供,為所有STM32微控制器提供了可直接使用的範例。

請參考freertos.org瞭解FreeRTOS的更多資訊和許可條款。

TouchGFX OS Wrappers

具有預設配置的TouchGFX在FreeRTOS上運行,使用一個訊息佇列實現與顯示器控制器的同步,並使用semaphore保護對影像緩衝的存取。

這是通過在touchgfx/os/OSWrappers.cpp中定義的OSWrappers類來處理的。 此類具有下列方法:

物件方法描述
signalVSync()應在顯示器為下一幀做好準備時從顯示器驅動呼叫此方法。
waitForVSync()由圖形引擎呼叫用於等待。 在signalVSync被呼叫前不應返回。
isVSyncAvailable()(可選)如果發生了VSync,則返回true。 在waitForVSync中可用於避免攔截。
signalRenderingDone()(可選)刪除任何未處理的VSync信號。
takeFrameBufferSemaphore()由圖形引擎和加速器呼叫,用來獲取對影像緩衝的直接存取
giveFrameBufferSemaphore()被呼叫用於再次釋放直接存取。

默認實現使用訊息佇列實現VSync(幀)同步。 在下一個VSync到達前,圖形引擎任務休眠。

此OSWrapper類由TouchGFX Generator生成。 請在此進一步瞭解Generator。

無RTOS

TouchGFX還可以在沒有作業系統的情況下運行。 在這種情況下,必須在主函數中直接開始圖形引擎主迴圈:

int main()
{
...
touchgfx::HAL::getInstance()->taskEntry();

//never returns
}

不使用RTOS並不會降低TouchGFX的性能。 可能會增加MCU負載,並增加與TouchGFX一起運行的其他任務的難度。

如上文所述,現在您需要在主函數中使用者介面運行時手動驅動其他的任務。

Model::tick

一種方式是在每一幀執行一次檢查Model類的任務:

Model.cpp
void Model::tick()
{
//run other tasks here
music_task_tick();
bluetooth_task_tick();
}

使用此方法可在每一幀將所有任務執行一次。 任務消耗的時間會被添加到使用者介面的渲染時間中。 對於所有任務均可快速結束的簡單系統而言,這是一種簡單且可接受的解決方案。

OSWrappers

另一種方法是在OSWrappers類中使用鉤子函數。 如上文所述,圖形引擎在需要等待事件時呼叫此類上的方法。 您可以在等待所述事件時使用此方法執行其他工作:

OSWrappers.cpp
static volatile uint8_t vsync_sem = 0;

void OSWrappers::signalVSync()
{
vsync_sem = 1;
}

void OSWrappers::waitForVSync()
{
vsync_sem = 0; //clear the flag, so we wait for the next vsync
do {
// Perform other work while waiting
music_task_tick();
bluetooth_task_tick();
} while(!vsync_sem);
}

使用此方法時,其他任務可以充分使用幀間的空閒任務,但任務得到的時間量會有變化。

另一種解決方案是使用OSWrappers::isVSyncAvailable和OSWrappers::signalRenderingDone函數。 這將幫助應用避免擁有多個while迴圈。 當選擇無作業系統配置時,TouchGFXGenerator將使用這些函數。

任務必須能夠將其工作分割成時長大概1毫秒的小步驟。 否則,將影響使用者介面性能。