---
# System prepended metadata

title: TouchGFX on STM32F469-Disco
tags: [NTURT, electrical_system, tutorial]

---

# TouchGFX on STM32F469-Disco
###### tags: `tutorial` `electrical_system` `NTURT`
## 硬體
STM32F469-Disco
[User Manual](https://www.st.com/resource/en/user_manual/um1932-discovery-kit-with-stm32f469ni-mcu-stmicroelectronics.pdf)
[儀表專案 github](https://github.com/mich9075/dashboard-F469)


### pinout
![](https://i.imgur.com/AYTJNQ5.png =500x)
### mech
![](https://i.imgur.com/fBFfRKF.png =500x)

### prospect
custmized board 以解決F469-Disco I2C1與CAN占用問題。
可選用有LTDC的MCU如F469/479/429/439、F7配合SDRAM by FMC作為frame buffer
或ILI9341等集成控制器與RAM的螢幕。

<iframe width="600" height="315"
src="https://www.youtube.com/embed/suMytEyQTP4?autoplay=0&mute=0">
</iframe>



## GUI Library
TouchGFX：STM家的親兒子
### MVP架構
![](https://i.imgur.com/6SWISDN.jpg =400x)

model : 所有GUI相關的最底層，跟main溝通或處理跨Screen的函數或資料


|           | 功能             | construct/destruct 時機 |
| -----     | ----            | -------------------------- |
|   model   | 所有GUI相關的最底層，跟main溝通或處理跨Screen的函數或資料|GUI開始/GUI結束|
| presenter | 一個Screen一次的功能  | Screen切入/Screen切出 |
|   view    | 最上層widget的顯示 | 同 presenter |

### prospect
LVGL 可能也值得一試?


## 軟體(電腦端)
1. CubeMX (記得要安裝TouchGFX相關的Pack)
2. CubeIDE / VS Code
3. TouchGFX Designer
https://www.st.com/en/development-tools/touchgfxdesigner.html#overview

官方安裝教學: [TouchGFX-Introduction-Installation](https://support.touchgfx.com/4.20/docs/introduction/installation#installing-touchgfx-generator-in-stm32cubemx)


## Start Your Journey
### 新建專案
![](https://i.imgur.com/mOGRrOE.png =500x)
在Create中選469，命名專案，並記得儲存路徑或預設路徑。

![](https://i.imgur.com/rZ4cWku.png =500x)
在上方工具欄第五格中選擇box，調整尺寸，作為各別Screen的底色。
右下方桃紅按鈕由左依序為 generate code,模擬器,燒錄。
至此可以模擬或燒錄來判斷專案是否成功建立。
### 純粹前端
### widget 0 / software button change screen 
此段所有button類widget皆適用，如 buttonWithLabel。

![](https://i.imgur.com/fLqSIP6.png =500x)
* Screen欄中按Add Screen旁的加號建立第二個Screen，兩個Screen可選用不同顏色的box以區分。

***注意：Screen 可在右邊Property中改名，但請謹慎命名，因為之後進入後端操作時，Screen的函數名稱是跟著Property的命名改變的，更改Screen 會使我們能修改的 MVP 所有檔案被重新生成，變得跟新的一樣，所以東西會爛掉喔。其他元件更改名稱也會使操作元件的函數被改變名稱，而在 MVP 中我們添加的 code 則不會被更新。***

* 在兩個Screen分別添加一個Button，如上圖。

![](https://i.imgur.com/1AZyjGQ.png)
* 分別進入兩個Button的Interaction欄，按加號以新增 Interaction，點擊剛新增出的 Interaction，各欄位依序選Button is clicked、當前的Button、Change Screen、另一個Screen

至此可以模擬或燒錄來看看Screen能否正確切換。

##### 踹踹看
1. 請在Interaction中的Action使用 Execute C++ Code來控制開發版正面的LED燈
2. 請在Interaction中的Action使用 Call new virtual function，在專案文件中找到你新增的函數，並思考一下繼承關係以及該如何使用函數。
 提示：依照前面的MVP架構，函數應該在當前的Screen的View中

### widget 1 / TextArea Wildcard with TickEvent
TickEvent 是當每次頁面刷新的時候會被執行的函式(應該吧)，但因為頻率無法確定，所以實際沒什麼屁用...? 除非你想做一些小動畫之類的。不過我們在此用作TextArea的練習，做一個能讓我們從code控制顯示資料的功能。
儀表專案中的時速數字的前端也是同樣的操作。

![](https://i.imgur.com/yHzuMoi.png =500x)
* 新增一個 TextArea

![](https://i.imgur.com/xtxJrQy.png =500x)
* TextArea 的 Property 中的 Text 填入<blah>，中間的字將不會被顯示。點擊Wildcard 1，**務必勾選use wildcard buffer**，此時填入initial value會被顯示在畫面上。當一個TextArea中有多個變數，可使用多個Wildcard。
    
接下來的操作要進入編輯器，個人是用VS code開啟整個專案的資料夾，用 CubeIDE 的話文件的 hierarchy 似乎會被亂動喔。
找到 ~\\<project_name>\TouchGFX\gui\include\gui\screen1_screen\Screen1View.hpp
也就是此Screen的View。
在此添加一個函數 (下方第15行)
```C++=
#ifndef SCREEN1VIEW_HPP
#define SCREEN1VIEW_HPP

#include <gui_generated/screen1_screen/Screen1ViewBase.hpp>
#include <gui/screen1_screen/Screen1Presenter.hpp>

class Screen1View : public Screen1ViewBase
{
public:
    Screen1View();
    virtual ~Screen1View() {}
    virtual void setupScreen();
    virtual void tearDownScreen();
    
    virtual void handleTickEvent(); //add this
    
protected:
};

#endif // SCREEN1VIEW_HPP
```
接下來進入cpp implement 函數
    ~\\<project name>\TouchGFX\gui\src\screen1_screen\Screen1View.cpp
    
```C++=
#include <gui/screen1_screen/Screen1View.hpp>

Screen1View::Screen1View(){}

void Screen1View::setupScreen(){
    Screen1ViewBase::setupScreen();
}
void Screen1View::tearDownScreen(){
    Screen1ViewBase::tearDownScreen();
}
// added start
void Screen1View::handleTickEvent(){
}
// added end
```
此時畫面更新時函數就會被呼叫。
接下來讓函數改變TextArea的內容。
我們進入這個 screen 的 view 的 parent, viewbase, 尋找TextArea顯示的函數。
***viewbase 是你的好朋友，所有Widget的操作函數以及所需的include, 如RGB, 都可以來這裡找使用範例***，除了TextArea需要一些額外步驟。
    Viewbase 位於GUI_generated下，所以文件式鎖住的，更改也會被gernate cade覆寫掉。
~\\<project name>\TouchGFX\generated\gui_generated\src\screen1_screen\Screen1ViewBase.cpp
    
找到 textArea1(或你對該textArea的取名)的段落
```C++=
    textArea1.setXY(320, 228);
    textArea1.setColor(touchgfx::Color::getColorFromRGB(0, 0, 0));
    textArea1.setLinespacing(0);
    
    //this one
    Unicode::snprintf(textArea1Buffer, TEXTAREA1_SIZE, "%s", touchgfx::TypedText(T_SINGLEUSEID2).getText());
    //
    
    textArea1.setWildcard(textArea1Buffer);
    textArea1.resizeToCurrentText();
    textArea1.setTypedText(touchgfx::TypedText(T_SINGLEUSEID1));
```
    
將上方的第六行的函數複製到 View 的 handleTickEvent函數中，並做相應的修改。
我們在view.hpp的class中添加一個 int 成員 counter。
然後再加一些Code。
    
*    view.hpp
```C++=
class Screen1View : public Screen1ViewBase
{
public:
    Screen1View();
    virtual ~Screen1View() {}
    virtual void setupScreen();
    virtual void tearDownScreen();

    virtual void handleTickEvent(); //add this
    int counter; //added

protected:
};
```
* view.cpp
```C++=
void Screen1View::handleTickEvent()
{
    //added start
    Unicode::snprintf(textArea1Buffer, TEXTAREA1_SIZE, "%d", counter++);
    textArea1.resizeToCurrentText();
    textArea1.invalidate();
    // added end
}
```
至此編譯燒錄或使用模擬器來看看效果如何。
應該會看到?，不過位數倒是對的。原因是我們尚未添加字型。
    
在 TextArea 的 Property 中的 typography 中可找到字型
。接著進入畫面右邊Texts的頁面。
:point_down: 
![](https://i.imgur.com/gwtwTdy.png)

上方選擇 Typographies :point_down: 
![](https://i.imgur.com/KmKpYu9.png)
    
在我們所選用的字型欄目中的Wildcard Range中填入1-9，若有需要顯示字母或符號則填入Wildcard Characters。
接下來應該就能正常顯示了。
要注意改變大小就是不同字型，得重填Ranges或Characters。
    
官方對於Wildcard ranges填法的描述
    *This is similar to Wildcard Characters, but ranges can easily be specified, e.g. "0-9,A-F" will be the same as putting "0123456789ABCDEF" in the Wildcard Characters column. Ranges can also be specified as numbers, so for example "0-9" can also be specified as "48-57" or "0x30-0x39". Please note that the quotes should not be entered.*
    
麻煩還沒結束，TouchGFX在渲染時，會選擇有改變的地方渲染，但他笨笨的，當我們的TextArea顯示長度變短時，即便調用了 resizeToCurrentText 函數，變短的區域有時候就不會被渲染到，但觸發條件跟解決辦法我也不清楚。
此現象可用counter /= 100或10、counter++ if counter >= 100 counter = 0 復現。
儀表專案中是依照數值位數在前後添加空白使長度大致相同，順便解決resize函數無法置中的問題。
在之後的progress bar也有類似的問題。
    
##### 踹踹看
當counter在累加時，如果我們切換Screen再切回來，應該要發現數字重新計算了，請以MVP架構思考該如何解決以保留資料。
    


### widget 2 / gauge
此 Widget 能依數值顯示指針與Arc，儀表專案中的速度Arc即為此Widget。
    
    
    
    

    
![](https://i.imgur.com/Y2B7jQG.png =500x)
在畫面中新增一個gauge。
    
![](https://i.imgur.com/S4rpEBg.png =500x)
Angel 的 Start 跟 End可以調整指針行程外也可調整方向使其向左轉。   
調整initial value看看效果如何。
    
![](https://i.imgur.com/3YrHWQQ.png =500x)
Properties中下拉找到Arc並打勾啟用，調整Radius與Line Width。
    Line Width 為0時為填滿，Radius 是以 Arc 的中線計算。
   
然後generate code。
接續前面的遺產，在 ViewBase 中找到 gauge 的 setvalue 函數填入 TickEvent 函數再稍作修改。

```C++=
void Screen1View::handleTickEvent()
{
    Unicode::snprintf(textArea1Buffer, TEXTAREA1_SIZE, "%d", counter);
    // added start
    gauge1.setValue(counter);
    if(counter == 100){  counter = 0; }
    else{  counter++;  }
    // added end
    textArea1.resizeToCurrentText();
    textArea1.invalidate();
}
```
至此應該能看到指針與Arc隨數值改變。
下一步是將gauge的樣式做成我們想要的樣子，Style有一些系統現成的可選，或者我們能自己 customized 。
gauge主要由元件外框、背景圖、指針圖、圓心位置跟Arc組成。

![](https://i.imgur.com/Z8xwCRS.png =200x)
元件起點在畫面座標及元件寬度
    
![](https://i.imgur.com/LvQmlfK.png =200x)
背景圖選擇及背景圖在元件座標，背景圖可為 no image

![](https://i.imgur.com/n4k6LdI.png =200x)
圓心在元件座標
    
![](https://i.imgur.com/FnUR21i.png =200x)
指針圖選擇及圓心在指針圖座標，座標可超過圖的大小。
    
***匯入png圖片時，無論指針或背景，電腦端顯示的畫面是會有偏移的，請務必相信像素位置計算的結果。***

### 從後面來
### widget 3 / progress bar
    
儀表專案中的engineer page中的溫度顯示即為此元件。

![](https://i.imgur.com/oPIjwOg.png)
一如既往地在畫面中新增元件。
    
Generate code後在ViewBase中尋找操作函數，除了setVale外，還有setColor函數。
```C++=    
boxProgress1.setXY(455, 136);
boxProgress1.setProgressIndicatorPosition(2, 2, 180, 16);
boxProgress1.setRange(0, 100);
boxProgress1.setDirection(touchgfx::AbstractDirectionProgress::RIGHT);
boxProgress1.setBackground(touchgfx::Bitmap(BITMAP_BLUE_PROGRESSINDICATORS_BG_MEDIUM_PROGRESS_INDICATOR_BG_SQUARE_0_DEGREES_ID));
boxProgress1.setColor(touchgfx::Color::getColorFromRGB(0, 151, 255));
boxProgress1.setValue(60);
```
要注意的是setColor函數的參數需要上方的include
```C++=
#include <touchgfx/Color.hpp>
```
踹踹看
請嘗試看看輸入值為 0~100，輸出 RGB 藍道綠到黃到紅
    
至此你可以如同之前的方式在TickEvent嘗試，但這裡想介紹呼叫View更有用的方法，也就是基於MVP架構的操作。
    
以儀表專案中的溫度顯示為例，peripheral 在 main.c 中我們執行接收的 Task 的 EnteryFunction中拿到我們要顯示的值。
    
首先得跨 Task 讓 TouchGFX 所在的 DefaultTask 收到，所以會在 model 中被接收，不過如何把 DefaultTask 的 EntryFunction 轉接到 model.c 我沒搞明白。
    
Queue的事情先放一邊，我們先解決資料從 model 傳到 presenter 再到 view。
    
由於 model 只有一個，而 Screen 會實時的切換，model 的函數在呼叫 presenter 時不知道是哪個 screen 的 presenter，不知道有那些函數可 call，因此有一個中介層負責處理實時的 binding ，叫做 ModelListener。
    
ModelListener 會跟 model bind，並且為 presenter 的 parent。
    
model 再往前call函數時是去 call modellistener 的函數，各個 Screen 的 presenter 再繼承去 implement，因此 modellistener 只有 vitural 沒有 implement，所以只有 .hpp
    
~\<project name>\TouchGFX\gui\include\gui\model\ModelListener.hpp
    
model 中添加一個 int成員 counter, constructor 中將其初始化，modelListener 中添加一個 virtual function, 例如這裡取名 MtoML。
***以下要宣告的部分就不贅述了，請自行判斷。***
* ModelListener.hpp
    
```C++=
#ifndef MODELLISTENER_HPP
#define MODELLISTENER_HPP

#include <gui/model/Model.hpp>

class ModelListener{
public:
    ModelListener() : model(0) {}
    virtual ~ModelListener() {}
    void bind(Model* m){ model = m; }

    // added start  
    virtual void MtoML( int );
    // added  end
    
protected:
    Model* model;
};
#endif // MODELLISTENER_HPP
```
* Model.cpp
```C++=
#include <gui/model/Model.hpp>
#include <gui/model/ModelListener.hpp>

Model::Model() : modelListener(0)
{
    // added start
    counter = 0;
    // added  end
}

void Model::tick()
{
    // added start
    modelListener->MtoML( counter++ );
    counter = (counter >= 100) ? 0 : counter ;
    // added  end
}
```
在 View 中函數函數，這裡取名PtoV

* Screen1Presenter.cpp
```C++=
#include <gui/screen1_screen/Screen1View.hpp>
#include <gui/screen1_screen/Screen1Presenter.hpp>

Screen1Presenter::Screen1Presenter(Screen1View& v): view(v){}
void Screen1Presenter::activate(){}
void Screen1Presenter::deactivate(){}

// added start
void Screen1Presenter::MtoML( int num )
{
    view.PtoV( num );
}
// added end
```
* Screen1View.cpp
```C++=
#include <gui/screen1_screen/Screen1View.hpp>

// added start
#include <touchgfx/Color.hpp>
// added end

Screen1View::Screen1View():counter(0){}

void Screen1View::setupScreen(){ Screen1ViewBase::setupScreen(); }
void Screen1View::tearDownScreen(){ Screen1ViewBase::tearDownScreen(); }
void Screen1View::handleTickEvent(){}
void Screen1View::function1(){}

// added start
void Screen1View::PtoV( int num ){
    boxProgress1.setValue(0);  // try to remove this
    if( num > 50 )
    {   //                                                           notice int or float
        boxProgress1.setColor(touchgfx::Color::getColorFromRGB(255, 255-(num-50)/50.0*255, 0));
    }
    else
    {
        boxProgress1.setColor(touchgfx::Color::getColorFromRGB(num/50.0*255, 255, 0));
    }
    boxProgress1.setValue(num);
}
// added end
```
至此TouchGFX Designer的模擬器應該就不再能用了，或至少我是如此，請燒錄至開發版上驗證。
    
踹踹看
1. 註解掉 Screen1View.cpp 中的 boxProgress1.setValue(0) 看看會如何。
2. 試著反過來做，將 View 的的資料傳回 Model，此向將不再經過ModelListener。
### Task and Queue
利用cubeMX生成Task請參考freeRTOS的教學，但這裡不使用CubeMX生成的CMSIS-RTOS的queue生成函數，我們手動添加原始 freeRTOS 的 queue 的 header 及 queue 生成函數，然後用 model 的 tick 中定時檢查 queue 的接收通道裡有沒有東西。
* Model.cpp
```C++=
#include <gui/model/Model.hpp>
#include <gui/model/ModelListener.hpp>

#include <FreeRTOS.h>
#include <queue.h>
#include <task.h>

unsigned char new_speed;

extern "C"
{
    xQueueHandle msg_speed;
}

Model::Model() : modelListener(0)
{
    msg_speed = xQueueGenericCreate(1, 4, 0);
}

void Model::tick()
{
    if (xQueueReceive(msg_speed, &new_speed, 0) == pdTRUE && current_screen == 0){
        modelListener->setNewSpeed(new_speed);
    }
}
```
main.c 中在 task 的 entry function 使用 xQueueSend 往通道丟資料。
* main.c
```C++=
void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN 5 */
  /* Infinite loop */

  uint8_t Rx_buffer[3] = {'r', 94, '\n'};

  for (;;)
  {
    HAL_UART_Receive_DMA(&huart6, Rx_buffer, 3);
    
    switch (Rx_buffer[0])
    {
    case 's':
      xQueueSend(msg_speed, &Rx_buffer[1], 0);
      break;

    case 'r':
      break;

    default:
      break;
    } 
    Rx_buffer[0] = 'r';
    osDelay(1);
  }
  /* USER CODE END 5 */
}
```
### hardware button
在 designer 中添加一個 interaction，triger 選haedware botton，選擇一個key
    
![](https://i.imgur.com/QfKevCF.png)
    
generate code後，在viewbase中可以找到 handleKeyEvent 函數
* viewbase
```C++=
//Handles when a key is pressed
void EngineerPageViewBase::handleKeyEvent(uint8_t key)
{
    if(0 == key)
    {
        //Interaction_HW1
        //When hardware button 0 clicked change screen to DriverPage
        //Go to DriverPage with no screen transition
        application().gotoDriverPageScreenNoTransition();
    }
}
```
call 函數時依 parameter( key ) 觸發 designer 中 interaction 設定的功能。
    
由於函數位於view的父輩，我們需要完成從main到MPV的一連串函數呼叫。
    

model_listener.hpp 中宣告 butt_0 virtural函數，各 presenter 繼承並實現 butt_0，在 model.cpp 中在 queue recieve 後呼叫 butt_0 函數。
* model.cpp
```C++=
void Model::tick()
{
    if (xQueueReceive(msg_butt, &new_sec, 0) == pdTRUE){
        modelListener->butt_0();
    }
}
```
presenter.hpp宣告後presenter.cpp 在 butt_0 中實現呼叫 handleKeyEvent
```C++=
void EngineerPagePresenter::butt_0()
{
	view.handleKeyEvent(0);
}
```
    
NVIC設定EXTI的步驟在此不贅述，在main.c找到EXTI的callback function，丟出queue send函數。請參考上節建立queue，要注意的是在interrupt中需使用特別的send函數 xQueueSendFromISR。
* main.c
```C++=
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  /* Prevent unused argument(s) compilation warning */
  if (GPIO_Pin == GPIO_PIN_0 || GPIO_Pin == GPIO_PIN_13)
  {
    xQueueSendFromISR(msg_butt, &butt_state, 0);
  }
  /* USER CODE END Callback01 */
}
```
至此完成從 EXTI 一路到 interaction 的實現。