# 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

### mech

### 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架構

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
### 新建專案

在Create中選469,命名專案,並記得儲存路徑或預設路徑。

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

* Screen欄中按Add Screen旁的加號建立第二個Screen,兩個Screen可選用不同顏色的box以區分。
***注意:Screen 可在右邊Property中改名,但請謹慎命名,因為之後進入後端操作時,Screen的函數名稱是跟著Property的命名改變的,更改Screen 會使我們能修改的 MVP 所有檔案被重新生成,變得跟新的一樣,所以東西會爛掉喔。其他元件更改名稱也會使操作元件的函數被改變名稱,而在 MVP 中我們添加的 code 則不會被更新。***
* 在兩個Screen分別添加一個Button,如上圖。

* 分別進入兩個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控制顯示資料的功能。
儀表專案中的時速數字的前端也是同樣的操作。

* 新增一個 TextArea

* 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:

上方選擇 Typographies :point_down:

在我們所選用的字型欄目中的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。

在畫面中新增一個gauge。

Angel 的 Start 跟 End可以調整指針行程外也可調整方向使其向左轉。
調整initial value看看效果如何。

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組成。

元件起點在畫面座標及元件寬度

背景圖選擇及背景圖在元件座標,背景圖可為 no image

圓心在元件座標

指針圖選擇及圓心在指針圖座標,座標可超過圖的大小。
***匯入png圖片時,無論指針或背景,電腦端顯示的畫面是會有偏移的,請務必相信像素位置計算的結果。***
### 從後面來
### widget 3 / progress bar
儀表專案中的engineer page中的溫度顯示即為此元件。

一如既往地在畫面中新增元件。
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

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 的實現。