cpt1020
    • Create new note
    • Create a note from template
      • 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
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me 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
    • Transfer ownership
    • Delete this note
    • Save as template
    • 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 Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control 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
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me 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
    # Lab 1 <!-- {%hackmd hackmd-dark-theme %} --> > 國立成功大學 資訊工程學系 嵌入式作業系統分析與實作 Analysis and Implementation of Embedded Operating Systems [CSIE7618] 2022 Spring > GitHub: https://github.com/cpt1020/EmbeddedOS-Lab1 ## Objective - 理解Button Bounce為何,並實作Software Debounce的方法 - 理解並使用FreeRTOS的Inter-Task Communication APIs ## Requirement :::info - MultiTasking - Two tasks: one for LED controlling, the other for Button handling - Using Inter-Task Communication (ITC) mechanism - LED-task has 2 states (S1, S2) - S1 (紅綠LED輪流): - First, only Green LED lights up for 2 seconds,<br>and then only Red LED lights up for 2 seconds,<br>and then switches back to the Green LED, then RED, and so on - S2 (橘色LED): - Only Orange LED is blinking (1 second ON, 1 second OFF, ...) - Button-task: If the button is pressed, the LED-task will switch to the other state (And execute from the start point of that state) - Debounce handling - Edge-detection handling ::: ### Lab1 Grading - LED至少一個state的亮暗模式正確(25%) - 按按鈕後LED會有變化(25%) - 按按鈕後LED的亮暗模式與lab要求相同(50%) - Lab report(一定要交) ## Bounce & Debounce ### Introduction 在嵌入式系統和電子設計中,"bounce" 和 "debounce" 是與按鈕或開關相關的兩個重要概念。 1. Button Bounce:當你按下或釋放一個物理按鈕時,按鈕的電子接觸可能不會立即穩定在其最終狀態。這是由於按鈕的機械性質,包括彈簧等因素,導致在按下或釋放按鈕的瞬間,電子接觸會迅速開啟和關閉多次。這種短暫的多次連接和斷開會導致在電子系統中產生多個開關信號,稱為 "Button Bounce" 。 Button Bounce 可能會導致錯誤或不穩定的操作,因為系統可能誤解這些瞬間的信號變化。<br>一個電子產品若有彈跳現象的話,最常見到的「症狀」是按下一個開關,結果數字跳好幾下。 2. Button Debounce:為了解決 Button Bounce 的問題,需要採取 Debounce 措施。Button Debounce 是一個過程,它確保在按鈕的狀態穩定後,系統只會生成一個信號變化。這通常涉及到在電路中添加適當的電子元件或在軟體中實施延時或計數器,以確保穩定的按鈕狀態被正確識別。通過 Debounce,系統可以避免錯誤地處理 Button Bounce 所引起的多次信號變化,從而實現可靠的按鈕操作。 Button Bounce 和 Debounce 是在電子設計和嵌入式系統開發中需要考慮的重要問題,特別是在需要按鈕輸入的應用中,如控制系統、嵌入式設備和用戶界面。適當的 Debounce 可以確保按鈕操作的可靠性和穩定性。 ### Solutions Debounce有硬體和軟體的方法,以下是一些常見的做法 Hardware Debounce: - 使用電容器:將電容器連接到按鈕的引腳。當按鈕被按下或釋放時,電容器充電或放電,從而緩和按鈕的突變。這種方法需要精心選擇電容值以達到所需的去彈跳效果。 - 使用 RC 電路:在按鈕引腳和地之間放置一個電阻(R)和一個電容器(C)的串聯電路。這種電路的常數(RC時間常數)可以調整以控制去彈跳時間。 - 使用機械去彈跳器:某些按鈕本身具有機械去彈跳設計,這可以減少硬體層面上的去彈跳問題。 Software Debounce: - 延遲計數:在軟體中實施一個計數器或計時器,當按鈕被按下或釋放時開始計數。只有在計數達到一個特定值之後,才被認為按鈕操作是穩定的。這種方法需要選擇適當的計數值和計數速度。 - 狀態機:實現一個有限狀態機,追蹤按鈕的狀態。當按鈕發生變化時,狀態機轉換到不同的狀態,並等待按鈕狀態穩定後再執行操作。這種方法可以更靈活地處理不同情況下的去彈跳。 - 中斷觸發:使用硬體中斷觸發,當按鈕狀態穩定時觸發一個中斷,然後在中斷服務程序中處理按鈕操作。這個方法可以實現即時的去彈跳處理。 每種方法都有其優點和限制,選擇哪種方法取決於具體需求和應用。硬體debounce通常更有效,但可能需要更多的電路設計工作。軟體debounce提供了更大的靈活性,但可能需要更多的計算資源。選擇最適合的應用的方法,取決於項目的要求和資源可用性。 $References$ - [探討:Button Debouncing (軟體作法)](http://andrew-workshop.blogspot.com/2015/05/lab-button-debouncing.html) - [[YouTube] STM32 programming part 7 - Button Debounce](https://www.youtube.com/watch?v=yTsjfXsW25A) - [STM32按键消抖的几种实现方式-STM32 Button Debouncing](https://www.cnblogs.com/xyw-blog/p/16655450.html) ## LEDs & Blue Button Pins 在 [UM1472 Discovery kit with STM32F407VG MCU](https://drive.google.com/file/d/1g46_RRT_xp9N05Mal4vouiHfJFGbltRt/view?usp=sharing) p.18 可以看到: ![image](https://hackmd.io/_uploads/HJApVec4T.png) - User 可以使用的 LED 有: - 橘色 LED,是 PD13 - 綠色 LED,是 PD12 - 紅色 LED,是 PD14 - 藍色 LED,是 PD15 - 這次 lab 會用到的藍色按鈕,則是 PA0 ## Prerequisites and Configuration Setup ![image.png](https://hackmd.io/_uploads/HyhjZD4Q6.png) - 對 `PD12` ~ `PD15` 點右鍵,選 `Enter User Label` ,分別輸入 `Green_LED` 到 `Blue_LED` - 對 `PA0` 做一樣的設定,只是 `Enter User Label` 輸入 `Blue_Button` - `Save` and `Generate Code` - Label name可自行設定,但注意不要有空白 Code generate出來後,可在 `lab1/Core/Inc/main.h` 看到其產生相對應的macro (line 74-75, 88-95): ![image.png](https://hackmd.io/_uploads/HyxxQ7UX6.png) 另外,在 `Drivers/STM32Fxx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h` 內可以看到 `GPIO_PIN_0` ~ `GPIO_PIN_15` 的定義: ```cpp /** @defgroup GPIO_pins_define GPIO pins define * @{ */ #define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */ #define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */ #define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */ #define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */ #define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */ #define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */ #define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */ #define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */ #define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */ #define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */ #define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */ #define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */ #define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */ #define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */ #define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */ #define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */ #define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */ #define GPIO_PIN_MASK 0x0000FFFFU /* PIN mask for assert test */ ``` ## HAL Library - Official document: [[UM1725 Description of STM32F4 HAL and low-layer drivers]](https://drive.google.com/file/d/1YbZT-6qF25z9Frmt4rmDQ8ANFWGAj36g/view?usp=sharing) 以下介紹3個 STMicroelectronics 提供的 HAL (Hardware Abstraction Layer) Library 常用到的 API,他們都是用在 STM32 微控制器的 GPIO (通用輸出/輸入) 引腳操作。他們分別是 `HAL_GPIO_ReadPin()`、`HAL_GPIO_WritePin()`、以及 `HAL_GPIO_TogglePin()`。 在介紹三個 API 之前,先介紹 `GPIO_PIN_RESET` 和 `GPIO_PIN_SET` ### ++GPIO_PIN_RESET & GPIO_PIN_SET++ 在 `Drivers/STM32Fxx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h` 內可以看到: ```cpp /** * @brief GPIO Bit SET and Bit RESET enumeration */ typedef enum { GPIO_PIN_RESET = 0, GPIO_PIN_SET } GPIO_PinState; ``` - 可以看到 `GPIO_PinState` 是一個enum,用來表示GPIO引腳的狀態,他有兩個成員: - `GPIO_PIN_RESET`,是 `0`,用來表示低電位 - 若LED燈沒在發光,那他的 `GPIO_PinState` 就是 `GPIO_PIN_RESET` - `GPIO_PIN_SET` 是 `1`,用來表示高電位 - 若LED燈在發光,那他的 `GPIO_PinState` 就是 `GPIO_PIN_SET` ### ++HAL_GPIO_ReadPin()++ 此 API 的目的是讀取特定 GPIO 引腳的 `GPIO_PinState`,即檢查引腳是處於高電位還是低電位。 ++Function Prototype++ ```cpp GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); ``` - `GPIOx`: - 這是一個指向 `GPIO_TypeDef` 型別的 pointer,表示要讀取的 GPIO 端口。例如,如果要讀取 `PA0` 引腳的狀態,則 `GPIOx` 可以設置為 `GPIOA`。 - 我們用的開發板,`x` 可以是 `A` ~ `I` - `GPIO_Pin`: - 這是一個表示要讀取的 `GPIO` 引腳的 bit mask。可以使用它來指定要讀取哪個引腳的狀態。例如,如果要讀取 `PA0` 引腳的狀態,則 `GPIO_Pin` 要設置為 `GPIO_PIN_0`。 - Return value: - 此 function 返回一個 `GPIO_PinState` 型別的值,它是一個enum,可以是以下兩個值之一: - `GPIO_PIN_RESET`:表示引腳處於低電位狀態,相當於 `0`。 - `GPIO_PIN_SET`:表示引腳處於高電位狀態,相當於 `1`。 例如: - 若LED正在發光,則 `HAL_GPIO_ReadPin` return `GPIO_PIN_SET` - 若LED沒發光,則 `HAL_GPIO_ReadPin` return `GPIO_PIN_RESET` - 若按鈕是在被按下的狀態,則 `HAL_GPIO_ReadPin` return `GPIO_PIN_SET` - 若按鈕不是被按下的狀態,則 `HAL_GPIO_ReadPin` return `GPIO_PIN_RESET` 例子: ```cpp! HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); // 或 HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0); ``` 以上的code會return藍色按鈕 (pin 是 PA0) 的狀態。如果按鈕是按下的狀態,則這個function會返回 `GPIO_PIN_SET` 。若按鈕未按下,則會return `GPIO_PIN_RESET` 。 ```cpp! HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_12); // 或 HAL_GPIO_ReadPin(Green_LED_GPIO_Port, GPIO_PIN_12); ``` 以上的code則會返回Green LED (pin 是 PD12) 的狀態。如果LED燈處於發光狀態,即LED亮起,那麼這個函數將返回 `GPIO_PIN_SET`,表示引腳處於高電位狀態。如果LED燈未發光,即LED熄滅,那麼它將返回 `GPIO_PIN_RESET`,表示引腳處於低電位狀態。 ++Function Definition++ 在 `Drivers/STM32Fxx_HAL_Driver/Src/stm32f4xx_hal_gpio.c` 可看到此 function 的 definition: ```cpp /** * @brief Reads the specified input port pin. * @param GPIOx where x can be (A..K) to select the GPIO peripheral for STM32F429X device or * x can be (A..I) to select the GPIO peripheral for STM32F40XX and STM32F427X devices. * @param GPIO_Pin specifies the port bit to read. * This parameter can be GPIO_PIN_x where x can be (0..15). * @retval The input port pin value. */ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIO_PinState bitstatus; /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); if((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET) { bitstatus = GPIO_PIN_SET; } else { bitstatus = GPIO_PIN_RESET; } return bitstatus; } ``` ### ++HAL_GPIO_WritePin()++ 此 API 的目的是設定特定 GPIO 引腳的輸出狀態,也就是將該引腳設為高電位或低電位。 Function Prototype: ```cpp void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); ``` - `GPIOx`: - 這是一個指向 `GPIO_TypeDef` 型別的 pointer,表示要設定的 GPIO 端口。例如,如果要設定 PA0 引腳的狀態,則 `GPIOx` 可以設置為`GPIOA`。 - `GPIO_Pin`: - 這是一個表示要設定的 GPIO 引腳的 bit mask。可以使用它來指定要設定哪個引腳的狀態。例如,如果要設定 PA0 引腳的狀態,則 `GPIO_Pin` 可以設置為 `GPIO_PIN_0`。 - `PinState`: - 這是一個 `GPIO_PinState` 型別的參數,它是一個enum,可以是以下兩個值之一: - `GPIO_PIN_RESET`:表示將引腳設為低電位狀態。 - `GPIO_PIN_SET`:表示將引腳設為高電位狀態。 可以藉此 function 來設定 GPIO 引腳的輸出狀態,而控制LED、馬達、繼電器或其他需要控制的外部設備。例如,若想將一個 LED 燈設為亮,可以使用 `HAL_GPIO_WritePin()` 將相應的 GPIO 引腳設為高電位。 例子: ```cpp! HAL_GPIO_WritePin(Green_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_SET); ``` 以上程式碼會讓綠色 LED 燈一直處於發光的狀態,若要讓它熄滅則可用以下code: ```cpp! HAL_GPIO_WritePin(Green_LED_GPIO_Port, GPIO_PIN_12, GPIO_PIN_RESET); ``` ++Function Definition++ 在 `Drivers/STM32Fxx_HAL_Driver/Src/stm32f4xx_hal_gpio.c` 可看到此 function 的 definition: ```cpp /** * @brief Sets or clears the selected data port bit. * * @note This function uses GPIOx_BSRR register to allow atomic read/modify * accesses. In this way, there is no risk of an IRQ occurring between * the read and the modify access. * * @param GPIOx where x can be (A..K) to select the GPIO peripheral for STM32F429X device or * x can be (A..I) to select the GPIO peripheral for STM32F40XX and STM32F427X devices. * @param GPIO_Pin specifies the port bit to be written. * This parameter can be one of GPIO_PIN_x where x can be (0..15). * @param PinState specifies the value to be written to the selected bit. * This parameter can be one of the GPIO_PinState enum values: * @arg GPIO_PIN_RESET: to clear the port pin * @arg GPIO_PIN_SET: to set the port pin * @retval None */ void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) { /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); assert_param(IS_GPIO_PIN_ACTION(PinState)); if(PinState != GPIO_PIN_RESET) { GPIOx->BSRR = GPIO_Pin; } else { GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U; } } ``` ### ++HAL_GPIO_TogglePin()++ 此 API 的目的是切換特定 GPIO 引腳的輸出狀態,也就是將引腳的電位狀態由高切換到低,或由低切換到高。 Function Prototype: ```cpp! void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); ``` - `GPIOx`: - 這是一個指向 `GPIO_TypeDef` 型別的 pointer,表示要切換的 GPIO 端口。例如,如果要切換 PA0 引腳的狀態,則 `GPIOx` 可以設置為 `GPIOA`。 - `GPIO_Pin`: - 這是一個表示要切換的 GPIO 引腳的 bit mask。可以使用它來指定要切換哪個引腳的狀態。例如,如果要切換 PA0 引腳的狀態,則 `GPIO_Pin` 可以設置為 `GPIO_PIN_0`。 `HAL_GPIO_TogglePin()` 的作用是將引腳的電位狀態切換,如果引腳是高,則切換為低,如果引腳是低,則切換為高。此 function 常用於控制 LED 的閃爍,或者切換其他需要交替狀態的設備。 舉例來說,如果想要在每次呼叫某個 function 的時候就切換一個 LED 的狀態(即閃爍),則可在該 function 內使用 `HAL_GPIO_TogglePin()`。這樣就不需要紀錄 LED 的當前狀態,只需呼叫該 function,則 LED 的狀態就會切換。 ```cpp! HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); ``` ++Function Definition++ 在 `Drivers/STM32Fxx_HAL_Driver/Src/stm32f4xx_hal_gpio.c` 可看到此 function 的 definition: ```cpp /** * @brief Toggles the specified GPIO pins. * @param GPIOx Where x can be (A..K) to select the GPIO peripheral for STM32F429X device or * x can be (A..I) to select the GPIO peripheral for STM32F40XX and STM32F427X devices. * @param GPIO_Pin Specifies the pins to be toggled. * @retval None */ void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { uint32_t odr; /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); /* get current Output Data Register value */ odr = GPIOx->ODR; /* Set selected pins that were at low level, and reset ones that were high */ GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin); } ``` ## USART 如果有想要顯示東西到console的話,需要設定USART,USART的設定我另外寫在這篇 https://hackmd.io/@cpt/embeddedOS_USART ## 觀察Button Bounce的狀況 寫一個小task來觀察開發板bounce的狀況,以下是以按鈕狀態從 `GPIO_PIN_SET` 跳到 `GPIO_PIN_RESET` 當作一次bounce,並記錄我每按一次按鈕,會有幾次bounce (也就是從按下按鈕到debounce期間,有多少bounce)。 ```cpp= void vBounceObserver(void *pvParameters) { static uint16_t buttonState = 0; static char msg1 [] = "Bounce Detected\n\r"; static uint16_t prevState = 0; static uint16_t curState = 0; static unsigned int bounceCount = 0; while (1) { curState = HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0); buttonState = (buttonState << 1) | curState | 0xFE00; if (prevState == GPIO_PIN_SET && curState == GPIO_PIN_RESET) { bounceCount += 1; HAL_UART_Transmit(&huart2, (uint8_t *) msg1, strlen(msg1), 0xffff); } prevState = curState; if (buttonState == 0xFF00) { char msg2 [150]; memset(msg2, '\0', sizeof(msg2)); strcat(msg2, "Total Bounce Detected: "); char bounce_count [10]; itoa(bounceCount, bounce_count, 10); strcat(msg2, bounce_count); strcat(msg2, "\n\r"); HAL_UART_Transmit(&huart2, (uint8_t *) msg2, strlen(msg2), 0xffff); bounceCount = 0; } } } ``` 結果: ![ezgif-5-9e0b97e7e9.gif](https://hackmd.io/_uploads/HyruXRjmT.gif) 可以看到要達到能被 `HAL_GPIO_ReadPin` 偵測到有差異的bounce似乎並不多。 ## vButtonHandler 讀取按鈕狀態的task,也就是要做debounce的task~並將狀態傳送到msgQueue ### Version 1 以下寫法改寫自這篇(https://www.e-tinkers.com/2021/05/the-simplest-button-debounce-solution/)的David Mellis 2006的Arduino版本 ```cpp! void vButtonHandler(void *pvParameters) { unsigned long lastDebounceTime = 0; unsigned long debounceDelay = 50; static unsigned int state = 0; // LED燈狀態 BaseType_t debounceInProgress = pdFALSE; // 紀錄是否正在debounce while (1) { // 取得button目前的狀態 int buttonState = HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0); // 若button狀態是GPIO_PIN_SET,就代表button被按下 if (buttonState == GPIO_PIN_SET) { // 記錄目前的時間 lastDebounceTime = xTaskGetTickCount(); // 開始進入debounce的過程 debounceInProgress = pdTRUE; } // 若是在debounce的狀態,且已超過debounceDelay if (debounceInProgress && xTaskGetTickCount() - lastDebounceTime > debounceDelay) { // 若button狀態不是GPIO_PIN_SET,也就是button已經被放掉 // 這就代表button已經被按了一次,且被放掉 if (buttonState != GPIO_PIN_SET) { // 更改LED燈的狀態 state ^= 1; // 把LED燈的狀態送到msgQueue xQueueSend(xQueue, (int *) &state, 1); // debounce結束 debounceInProgress = pdFALSE; } } vTaskDelay(10); } } ``` :::info `debounceDelay` 的設置: `debounceDelay` 的值通常會根據具體的硬體和按鈕的特性而有所不同。一般來說,`50 ms` 是一個相對常見的初始值,但具體的選擇可能會受到以下因素的影響: - 硬體特性:不同的按鈕和硬體配置可能需要不同的 debounce 時間。有些按鈕可能在按下或放開時會有更多的抖動,因此可能需要較長的 debounce 時間。 - 應用需求:具體應用的需求可能會影響 debounce 時間的選擇。例如,如果應用需要快速的按鈕反應,可能需要較短的 debounce 時間。 - 實際測試:最好的方法是進行實際測試。通過觀察實際硬體中的按鈕行為,可以調整 debounce 時間,以確保它適應特定的情況。 總而言之,`50 ms` 是一個常見的初始值,但可能需要根據具體硬體和應用需求進行調整。在開發過程中,通過需要透過實際測試按鈕來找到最適合的 debounce 時間。 ::: :::info `xTaskGetTickCount() - lastDebounceTime > debounceDelay` 的目的是確保 debounce 過程已經持續了足夠的時間,以確保按鈕的狀態穩定下來。 - `xTaskGetTickCount() - lastDebounceTime`:這部分計算自上次按鈕狀態改變以來的時間,以milli-second為單位。`xTaskGetTickCount()` 會return自 FreeRTOS 啟動以來的 tick 數,`lastDebounceTime` 是上次按鈕狀態改變的時間。 - `debounceDelay`:這是 debounce 過程所需的最小時間(milli-second)。當經過了這個時間後,就認為按鈕的狀態已經穩定。 因此,整個判斷式確保了 debounce 過程已經持續了足夠的時間,使得在此期間內的任何按鈕狀態變化都被視為噪聲或抖動,而不是實際的按下或放開動作。 這樣有助於過濾掉按鈕的抖動,確保只在按鈕狀態穩定下來後進行最終的狀態切換,而避免錯誤的觸發。 ::: :::info `vTaskDelay(10)`的用意: `vTaskDelay(10);` 的目的是在每次迭代之後引入一個小的延遲。這樣做的主要原因是為了減少在高速執行的 while 迴圈中消耗的 CPU 資源。如果沒有這個延遲,該迴圈將盡可能快速地重複執行,可能導致高 CPU 使用率,對其他任務造成影響。 這個延遲的大小(這裡是 10 個 tick)可以根據實際的應用需求和硬體特性進行調整。主要考慮以下因素: - CPU 資源:如果沒有延遲,while 迴圈可能會在很短的時間內迭代數千次,佔用大量 CPU 資源。 - 任務間切換:如果其他任務需要執行,這個小的延遲有助於讓 FreeRTOS 進行任務間切換。 - 功耗:如果在應用中考慮功耗,可能會需要調整延遲,以確保在適當的反應時間的同時降低功耗。 在實際應用中,可能需要根據實際效果調整這個延遲的大小。如果不需要這麼高的迭代速率,增加延遲可能有助於節省能源。然而,如果需要更高的real time,可能需要減少延遲。 ::: ### Version 2 (更簡短的寫法 :+1:) 以下寫法改寫自這篇(https://www.e-tinkers.com/2021/05/the-simplest-button-debounce-solution/)的The simplest debounce function的Arduino版本 ```cpp= void vButtonHandler(void *pvParameters) { static uint16_t buttonState = 0; // 用16個bit來記錄按鈕的歷史狀態 static unsigned int LEDstate = 0; while (1) { buttonState = (buttonState << 1) | HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0) | 0xFE00; if (buttonState == 0xFF00) { // 按鈕穩定了,可以更新LED燈的state了 LEDstate ^= 1; xQueueSend(xQueue, (int *) &LEDstate, 1); } vTaskDelay(10); } } ``` :::info `buttonState = (buttonState << 1) | HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0) | 0xFE00` 說明: - `(buttonState << 1)` - 把 `buttonState` 都往左移一個bit - `HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0)` - 讀取當前iteration的按鈕的狀態,若是按下按鈕的狀態,則會return `GPIO_PIN_SET`,也就是 `1`;若是放開的狀態,則return `GPIO_PIN_RESET`,也就是 `0` - `0xFE` 是bit mask (`1111 1110 0000 0000`),和其做 `OR` 相當於只保留最右側9個bit的狀態,也就是在包含目前這個iteration的狀況下的最近9個iteration的狀態 - 所以while loop的每個iteration,會先把 `buttonState` 往左移一個bit,因為最右邊的那個bit要拿來放現在的按鈕的狀態。再來,`| HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0)` 則會把目前的狀態放到 `buttonState` 最後一個bit。最後,`| 0xFE00` 則是只保留最後9個bit的狀態 再來,按鈕被按下(按下的過程也會bounce)並且釋放後,按鈕就會開始bounce,所以這段時間 `HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0)` 可能會得到 `0` 或 `1`;等到按鈕真的穩定了,`HAL_GPIO_ReadPin(Blue_Button_GPIO_Port, GPIO_PIN_0)` 就一定會得到 `0`。 假設 iteration = 1 時,按下按鈕得到 `1` ,這時`buttonState`是`0000 0000 0000 0001` 假設 iteration = 2 ~ 10,按鈕bounce,每個iteration可能得到`0`或`1`,假設iteration = 10時,`buttonState`是 `0000 0010 1101 0011` iteration = 11,開始穩定,`buttonState` == `0000 0101 1010 0110` iteration = 12,`buttonState` == `0000 1011 0100 1100` iteration = 13,`buttonState` == `0001 0110 1001 1000` iteration = 14,`buttonState` == `0010 1101 0011 0000` iteration = 15,`buttonState` == `0101 1010 0110 0000` iteration = 16,`buttonState` == `1011 0100 1100 0000` iteration = 17,`buttonState` == `0110 1001 1000 0000` iteration = 18,`buttonState` == `1101 0011 0000 0000` 注意~當iteration = 18,`buttonState` (`1101 0011 0000 0000`) 做 `| 0xFE00` 會得到 `1111 1111 0000 0000`,這時 `if (buttonState == 0xFF00)` 才會成立!才會代表說已經穩定了,這時才可以更改LED的狀態,並發送訊息到msgQueue。 所以也就是當最後一個記錄到 `1` 的bit,他被左移到由左而右第8個bit的位置,並且右邊是連續8個`0`,那才當作按鈕穩定了,而`if (buttonState == 0xFF00)` 才會成立。 當到了下個iteration,`buttonState` 跟`0xFE00`做完 `OR` ,則會變成 `1111 1110 0000 0000`,則 `if (buttonState == 0xFF00)` 就不會成立。 所以其他時間點做 `| 0xFE00` 都不可能得到 `0xFF00`。 我一開始不太能理解為什麼bit mask也不設定為 `0xFF00` ,但從這裡的觀察就可以知道為什麼bit mask要選 `0xFE00` ,而if判斷式是判斷有沒有等於 `0xFF`。因為若bit mask也是 `0xFF` ,那只要按鈕在沒被按下的狀態,`buttonState` 做`OR 0xFE00`就會一直得到 `0xFF`,那 `if (buttonState == 0xFF00)` 就會一直成立,就會一直送msg到msgQueue。 ::: ### $Reference$ - [:+1: The simplest button debounce solution](https://www.e-tinkers.com/2021/05/the-simplest-button-debounce-solution/) ## vLEDHandler 讀取msgQueue以取得LED應該要是哪個狀態 ```cpp= void vLEDHandler(void *pvParameters){ static uint8_t receivedMsg = 0; // receivedMsg就是LED燈的狀態 while (1) { xQueueReceive(xQueue, &receivedMsg, 1); if (receivedMsg == 0) { STATE0: HAL_GPIO_TogglePin(Green_LED_GPIO_Port, GPIO_PIN_12); xQueueReceive(xQueue, &receivedMsg, 2000); HAL_GPIO_TogglePin(Green_LED_GPIO_Port, GPIO_PIN_12); if (receivedMsg == 1) { goto STATE1; } HAL_GPIO_TogglePin(Red_LED_GPIO_Port, GPIO_PIN_14); xQueueReceive(xQueue, &receivedMsg, 2000); HAL_GPIO_TogglePin(Red_LED_GPIO_Port, GPIO_PIN_14); if (receivedMsg == 1) { goto STATE1; } } if (receivedMsg == 1) { STATE1: HAL_GPIO_TogglePin(Orange_LED_GPIO_Port, GPIO_PIN_13); xQueueReceive(xQueue, &receivedMsg, 1000); HAL_GPIO_TogglePin(Orange_LED_GPIO_Port, GPIO_PIN_13); xQueueReceive(xQueue, &receivedMsg, 1000); if (receivedMsg == 0) { goto STATE0; } } } } ``` :::info 需要注意的是,line 8, 16, 26, 28必須要用 `xQueueReceive(xQueue, &receivedMsg, 2000)` 來控制LED燈亮的時間,這幾個地方不能用 `vTaskDelay(2000)` 來控制LED燈亮的時間。因為假如LED燈剛亮,就按了按鈕,那必須要等到 `vTaskDelay(2000)` 結束才會更換LED燈的狀態,這樣的反應太慢了,無法達到需求。 ::: <!-- ```cpp! void vLEDHandler(void *pvParameters){ uint8_t receivedMsg = 0; while (1) { while (receivedMsg == 0) { HAL_GPIO_TogglePin(Green_LED_GPIO_Port, GPIO_PIN_12); vTaskDelay(2000); HAL_GPIO_TogglePin(Green_LED_GPIO_Port, GPIO_PIN_12); xQueueReceive(xQueue, &receivedMsg, 1); if (receivedMsg == 1) { break; } HAL_GPIO_TogglePin(Red_LED_GPIO_Port, GPIO_PIN_14); vTaskDelay(2000); HAL_GPIO_TogglePin(Red_LED_GPIO_Port, GPIO_PIN_14); xQueueReceive(xQueue, &receivedMsg, 1); if (receivedMsg == 1) { break; } } while (receivedMsg == 1) { HAL_GPIO_TogglePin(Orange_LED_GPIO_Port, GPIO_PIN_13); vTaskDelay(1000); HAL_GPIO_TogglePin(Orange_LED_GPIO_Port, GPIO_PIN_13); vTaskDelay(1000); xQueueReceive(xQueue, &receivedMsg, 1); if (receivedMsg == 1) { break; } } } } ``` --> <!-- 用這方法的話,由於是用 `vTaskDelay()` ,若按了按鈕,必須等到當前 `vTaskDelay()` 跑完,到了 `if (receivedMsg == 1)` 這個判斷式才能切換,所以無法及時反應 --> ## main.c 其他部分 ```cpp! /* Private variables ---------------------------------------------------------*/ UART_HandleTypeDef huart2; // ... /* USER CODE BEGIN PFP */ void vButtonHandler(void *pvParameters); void vLEDHandler(void *pvParameters); /* USER CODE END PFP */ // ... /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ QueueHandle_t xQueue; /* USER CODE END 0 */ // ... int main(void) { // ... /* USER CODE BEGIN 2 */ xQueue = xQueueCreate(1, sizeof(int)); xTaskCreate(vLEDHandler, "LED", 128, NULL, 1, NULL); xTaskCreate(vButtonHandler, "BUTTON", 128, NULL, 1, NULL); vTaskStartScheduler(); /* USER CODE END 2 */ // ... } // ... /* USER CODE BEGIN 4 */ void vButtonHandler(void *pvParameters) { // ... } void vLEDHandler(void *pvParameters) { // ... } /* USER CODE END 4 */ ``` <!-- ## Appendix - Message Queue ### [xQueueCreate](https://www.freertos.org/a00116.html) ```cpp! QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); ``` #### Parameters - `uxQueueLength` - The maximum number of items the queue can hold at any one time. - `uxItemSize` - The size, in ++bytes++, required to hold each item in the queue. - Items are queued by ++copy++, not by reference, so this is the number of bytes that will be copied for each queued item. Each item in the queue must be the same size. #### Returns If the queue is created successfully then a handle to the created queue is returned. If the memory required to create the queue could not be allocated then `NULL` is returned. #### Example ```cpp! struct AMessage { char ucMessageID; char ucData[ 20 ]; }; void vATask( void *pvParameters ) { QueueHandle_t xQueue1, xQueue2; /* Create a queue capable of containing 10 unsigned long values. */ xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) ); if( xQueue1 == NULL ) { /* Queue was not created and must not be used. */ } /* Create a queue capable of containing 10 pointers to AMessage structures. These are to be queued by pointers as they are relatively large structures. */ xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) ); if( xQueue2 == NULL ) { /* Queue was not created and must not be used. */ } /* ... Rest of task code. */ } ``` ### [xQueueSend](https://www.freertos.org/a00117.html) ```cpp! BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait); ``` - This is a macro that calls `xQueueGenericSend()`. - Post an item on a queue. The item is queued by copy, not by reference. - This function must not be called from an interrupt service routine. #### Parameters - `xQueue` - The handle to the queue on which the item is to be posted. - `pvItemToQueue` - A pointer to the item that is to be placed on the queue. The size of the items the queue will hold was defined when the queue was created, so this many bytes will be copied from pvItemToQueue into the queue storage area. - `xTicksToWait` - The maximum amount of time the task should block waiting for space to become available on the queue, should it already be full. The call will return immediately if the queue is full and `xTicksToWait` is set to 0. The time is defined in tick periods so the constant `portTICK_PERIOD_MS` should be used to convert to real time if this is required. - If `INCLUDE_vTaskSuspend` is set to '1' then specifying the block time as `portMAX_DELAY` will cause the task to block indefinitely (without a timeout). #### Returns `pdTRUE` if the item was successfully posted, otherwise `errQUEUE_FULL`. #### Example ```cpp! struct AMessage { char ucMessageID; char ucData[ 20 ]; } xMessage; unsigned long ulVar = 10UL; void vATask( void *pvParameters ) { QueueHandle_t xQueue1, xQueue2; struct AMessage *pxMessage; /* Create a queue capable of containing 10 unsigned long values. */ xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) ); /* Create a queue capable of containing 10 pointers to AMessage structures. These should be passed by pointer as they contain a lot of data. */ xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) ); /* ... */ if( xQueue1 != 0 ) { /* Send an unsigned long. Wait for 10 ticks for space to become available if necessary. */ if( xQueueSend( xQueue1, ( void * ) &ulVar, ( TickType_t ) 10 ) != pdPASS ) { /* Failed to post the message, even after 10 ticks. */ } } if( xQueue2 != 0 ) { /* Send a pointer to a struct AMessage object. Don't block if the queue is already full. */ pxMessage = & xMessage; xQueueSend( xQueue2, ( void * ) &pxMessage, ( TickType_t ) 0 ); } /* ... Rest of task code. */ } ``` ### [xQueueReceive](https://www.freertos.org/a00118.html) ```cpp! BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait); ``` - This is a macro that calls the `xQueueGenericReceive()` function. - Receive an item from a queue. The item is received by copy so a buffer of adequate size must be provided.The number of bytes copied into the buffer was defined when the queue was created. - This function must not be used in an interrupt service routine. #### Parameters - `xQueue` - The handle to the queue from which the item is to be received. - `pvBuffer` - Pointer to the buffer into which the received item will be copied. - `xTicksToWait` - The maximum amount of time the task should block waiting for an item to receive should the queue be empty at the time of the call. Setting `xTicksToWait` to 0 will cause the function to return immediately if the queue is empty. The time is defined in tick periods so the constant `portTICK_PERIOD_MS` should be used to convert to real time if this is required. - If `INCLUDE_vTaskSuspend` is set to '1' then specifying the block time as `portMAX_DELAY` will cause the task to block indefinitely (without a timeout). #### Returns `pdTRUE` if an item was successfully received from the queue, otherwise `pdFALSE`. #### Example ```cpp! /* Define a variable of type struct AMMessage. The examples below demonstrate how to pass the whole variable through the queue, and as the structure is moderately large, also how to pass a reference to the variable through a queue. */ struct AMessage { char ucMessageID; char ucData[ 20 ]; } xMessage; /* Queue used to send and receive complete struct AMessage structures. */ QueueHandle_t xStructQueue = NULL; /* Queue used to send and receive pointers to struct AMessage structures. */ QueueHandle_t xPointerQueue = NULL; void vCreateQueues( void ) { xMessage.ucMessageID = 0xab; memset( &( xMessage.ucData ), 0x12, 20 ); /* Create the queue used to send complete struct AMessage structures. This can also be created after the schedule starts, but care must be task to ensure nothing uses the queue until after it has been created. */ xStructQueue = xQueueCreate( /* The number of items the queue can hold. */ 10, /* Size of each item is big enough to hold the whole structure. */ sizeof( xMessage ) ); /* Create the queue used to send pointers to struct AMessage structures. */ xPointerQueue = xQueueCreate( /* The number of items the queue can hold. */ 10, /* Size of each item is big enough to hold only a pointer. */ sizeof( &xMessage ) ); if( ( xStructQueue == NULL ) || ( xPointerQueue == NULL ) ) { /* One or more queues were not created successfully as there was not enough heap memory available. Handle the error here. Queues can also be created statically. */ } } /* Task that writes to the queues. */ void vATask( void *pvParameters ) { struct AMessage *pxPointerToxMessage; /* Send the entire structure to the queue created to hold 10 structures. */ xQueueSend( /* The handle of the queue. */ xStructQueue, /* The address of the xMessage variable. sizeof( struct AMessage ) bytes are copied from here into the queue. */ ( void * ) &xMessage, /* Block time of 0 says don't block if the queue is already full. Check the value returned by xQueueSend() to know if the message was sent to the queue successfully. */ ( TickType_t ) 0 ); /* Store the address of the xMessage variable in a pointer variable. */ pxPointerToxMessage = &xMessage; /* Send the address of xMessage to the queue created to hold 10 pointers. */ xQueueSend( /* The handle of the queue. */ xPointerQueue, /* The address of the variable that holds the address of xMessage. sizeof( &xMessage ) bytes are copied from here into the queue. As the variable holds the address of xMessage it is the address of xMessage that is copied into the queue. */ ( void * ) &pxPointerToxMessage, ( TickType_t ) 0 ); /* ... Rest of task code goes here. */ } /* Task that reads from the queues. */ void vADifferentTask( void *pvParameters ) { struct AMessage xRxedStructure, *pxRxedPointer; if( xStructQueue != NULL ) { /* Receive a message from the created queue to hold complex struct AMessage structure. Block for 10 ticks if a message is not immediately available. The value is read into a struct AMessage variable, so after calling xQueueReceive() xRxedStructure will hold a copy of xMessage. */ if( xQueueReceive( xStructQueue, &( xRxedStructure ), ( TickType_t ) 10 ) == pdPASS ) { /* xRxedStructure now contains a copy of xMessage. */ } } if( xPointerQueue != NULL ) { /* Receive a message from the created queue to hold pointers. Block for 10 ticks if a message is not immediately available. The value is read into a pointer variable, and as the value received is the address of the xMessage variable, after this call pxRxedPointer will point to xMessage. */ if( xQueueReceive( xPointerQueue, &( pxRxedPointer ), ( TickType_t ) 10 ) == pdPASS ) { /* *pxRxedPointer now points to xMessage. */ } } /* ... Rest of task code goes here. */ } ``` -->

    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