後端通信
在大多數應用中,UI需以某種方式連接到系統的其餘部分,並發送和接收資料。 這可能會連接硬體外設(感測器資料、類比/數位轉換和串列通信等)或其他軟體模組。
本文將介紹實現此類連接的推薦解決方案。
第一種方法是一種“快而不精”的方法,主要用於原型開發,而第二種方法是一種在架構上較完善的方法,可正確地連接UI與現實世界中的剩餘元件。
在本文末,我們將連結使用兩種方法的範例。
Model類
所有TouchGFX應用都有Model類,除了存儲UI狀態資訊,還用作面向周圍系統的介面。 這裡我們指的是兩種硬體外設,但也與系統中的其他OS任務進行通信。 好的設計通常不會在各自的View類中訪問其他軟體模組或硬體。
Further reading
Model類非常適合放置任何此類介面程式碼,原因在於:
- Model類有一個
tick()
函數,會在每一幀自動呼叫,並且可實現用於查找來自其他子模組的事件或對事件作出反應。 - Model類具有指向當前活動Presenter的指標,為的是能夠將傳入事件通知UI。
系統介面
與周圍系統連接的方式有兩種:一種是從GUI任務直接採樣,另一種是從二級任務採樣
從GUI任務採樣
連接周圍系統的最佳方式取決於您需要的採樣頻率、時間消耗和時間嚴格性的要求。
如果這些方面的要求並不嚴格,那麼最簡單的方法就是在Model::tick
函數中與周圍系統進行交互通訊。
如果採樣頻率低於幀率(通常約為60Hz),您可以添加計數器,並且只在每次的第N個計數點採樣。 如果這樣做,則採樣操作必須稍微快一些(通常為1ms或更短),否則會影響幀率,因為採樣是在GUI任務的背景下執行的並且會延遲幀繪製。
從二級任務採樣
或者,如果不方便將與周圍系統的交互直接放在GUI任務的背景下,可以新建負責執行採樣的OS任務。
您可以根據特定場景的需要,將該任務配置為以準確的時間間隔運行。 此外,根據您的需求,此新任務的優先順序可以低於或高於GUI任務。
如果優先順序更高,則可以確保它會準確地在指定時間運行,無論GUI任務在執行什麼操作。 有一個缺點是,如果是CPU占用量大的進程,可能會影響UI的幀率。
另一方面,如果採樣對時間的要求不嚴格,則可以分配低於GUI任務的優先順序,這樣UI幀率將永遠不受周圍系統採樣的影響。 在渲染時,GUI任務將休眠很長時間(如在等待基於DMA的像素傳輸完成時),允許優先順序較低的任務頻繁運行,這對絕大多數應用而言足夠了。
如果您使用二級任務法,建議您使用RTOS提供的任務間消息傳送系統。 大多數(如果不是全部)RTOSes具有佇列/郵件機制,可從一個任務向另一個任務發送資料(通常為使用者定義的C語言結構體、位元組陣列或簡單的整數)。 為了將新資料傳遞給GUI任務,為UI任務設置郵箱或訊息佇列,並使用此消息傳送系統將資料發送給GUI任務。 然後您可以使用Mode::tick
函數來輪詢GUI任務裡的郵箱去查看是否有新的資料到來。 如果有,讀取資料並相應地更新UI。
向UI傳播資料
無論您是從GUI任務採樣還是從二級任務採樣,Model::tick
函數都是GUI任務發現要在UI中顯示的新資料的地方。 除了充當周圍系統的介面,Model類還負責保存狀態資料(如前文所述),因此可能有些狀態變數也需要更新。
我們來考慮一個簡單的範例:溫度感測器連接到系統,當前溫度將顯示在UI上。 在準備中,我們將增加Model類以便支援:
Model.hpp
class Model
{
public:
// Function that allow your Presenters to read current temperature.
int getCurrentTemperature() const { return currentTemperature; }
// Called automatically by framework every tick.
void tick();
...
private:
// Variable storing last received temperature;
int currentTemperature;
...
};
在上述情況下,Presenter
能夠詢問Model當前溫度,以便在進入顯示溫度的螢幕時Presenter在UI(View)中設置此值。 現在,需要能夠在接收到新的溫度資訊時再次更新UI。 為此,我們利用Model有指向當前活動Presenter指標的特性。 該指標的類型是介面(ModelListener
),為了反映合適的應用特定的事件,您可以進行修改:
ModelListener.hpp
class ModelListener
{
public:
// Call this function to notify that temperature has changed.
// Per default, use an empty implementation so that only those
// Presenters interested in this specific event need to
// override this function.
virtual void notifyTemperatureChanged(int newTemperature) {}
};
現在,我們已經連接了此介面,剩餘的工作是執行傳入“新溫度”事件的實際採樣 Model::tick
Model.cpp
void Model::tick()
{
// Pseudo-code for sampling data
if (OS_Poll(GuiTaskMBox))
{
// Here we assume that you have defined a "Message" struct containing type and data,
// along with some event definitions.
struct Message msg = OS_Read(GuiTaskMBox);
if (msg.eventType == EVT_TEMP_CHANGED)
{
// We received information that temperature has changed.
// First, update Model state variable
currentTemperature = msg.data;
// Second, notify the currently active Presenter that temperature has changed.
// The modelListener pointer points to the currently active Presenter.
if (modelListener != 0)
{
modelListener->notifyTemperatureChanged(currentTemperature);
}
}
}
}
以上方法可確保兩點:
currentTemperature
變數總是最新值,因此Presenter可隨時獲取當前溫度。Presenter
立即通知溫度變化,並能採取合適措施。
MVP模式的一大優勢是根據當前所在螢幕實現單獨的通知處理。 例如,假設在顯示某些設置功能表(例如MainMenuPresenter/MainMenuView是活動的)時發生了一個溫度變化的事件,而當前溫度與此無關。
因為notifyTemperatureChanged
函數具有預設的空實現,此通知會被 MainMenuPresenter
忽略。 另一方面,如果有TemperatureControlPresenter
,您可以在該Presenter中重寫notifyTemperatureChanged
函數,並通知View它應顯示更新後的溫度:
TemperatureControlPresenter.hpp
class TemperatureControlPresenter : public ModelListener
{
public:
// override the empty function.
virtual void notifyTemperatureChanged(int newTemperature) {
view.setTemp(newTemperature);
}
};
當然,View類TemperatureControlView
必須實現setTemp
方法。
從UI向周圍系統發送資料
反方向從UI向周圍系統傳輸資料/事件時,將通過Model以大致上相同的方式來執行。 繼續前面的例子,如果我們需要增加配置新的目標溫度的能力,我們將向Model添加以下內容:
Model.hpp
void setNewTargetTemperature(int newTargetTemp)
{
// Pseudo-code for sending an event to a task responsible for controlling temperature.
struct Message msg;
msg.eventType = EVT_SET_TARGET_TEMP;
msg.data = newTargetTemp;
OS_Send(SystemTaskMBox, &msg);
}
如果使用者在UI中設置新的目標溫度,View可通知保有指向Model物件指標的Presenter,從而能夠呼叫setNewTargetTemperature
函數。
範例
下面的範例是為特定演示板配置的完整演示,但演示的許多程式碼可重複用於其他演示板和自訂硬體。
來自GUI任務
STM32F746的工作範例展示了如何對按鈕採樣並直接在Model類中控制LED。 該範例使用MVP架構在兩個視圖和Model類之間傳輸值和事件。 Model類對按鈕採樣,並更新LED以與應用狀態相匹配。
STM32F429的工作範例展示了如何在Model類中對按鈕採樣。 該範例使用MVP架構將按鈕事件傳輸到View。
來自其他任務
STM32F469的工作範例展示了如何在單獨執行緒中對模擬輸入採樣。 該範例使用MVP架構將模擬值傳輸到View。
工作範例展示了任務間通信和與UI之間的傳播。 在您自己進行設置時,它也許能激發您的靈感。 該範例在用C程式碼實現的後端系統與C++ TouchGFX GUI之間進行通信。 該範例在FreeRTOS之上的STM32F746G-DISCO板上運行。
來自多個任務
2018年5月28日舉行的TouchGFX網路研討會“與硬體集成”上演示了該工作範例。
該應用專為STM32F769-DISCO開發板而設計,它與LED和使用者按鈕交互,以便展示如何將C程式碼和硬體外設集成到TouchGFX應用中。
應用以GPIO模式配置按鈕。 其行為是在btntask.c中對按鈕狀態進行採樣,並在按下按鈕時通過GUI訊息佇列傳遞消息。 因此,我們可以通過按住按鈕來推進應用中的動畫。
該應用使用三個FreeRTOS任務。 一個用於GUI,另外兩個分別用於兩個外設(LED和使用者按鈕)。
來自任務和外部中斷線路
2018年5月28日舉行的TouchGFX網路研討會“與硬體集成”上演示了該工作範例。
該應用專為STM32F769-DISCO開發板而設計,它與LED和使用者按鈕交互,以便展示如何將C程式碼和硬體外設集成到TouchGFX應用中。
該應用以EXTI模式配置按鈕(外部中斷線路0)。 其行為是在按下按鈕時接收中斷,此後中斷清零。 這不允許出現與GPIO模式下相同的行為,而會是單步動畫,原因在於只在接收到中斷時通過GUI訊息佇列發送消息。
該應用使用兩個FreeRTOS任務。 一個用於GUI,一個用於LED。 (多工演示中的Button任務在該應用中仍處於活動狀態,用於舉例說明外設交互程式碼已移至中斷處理函數中)。