NTU Racing Team
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Help
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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 的實現。

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully