# 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 可以看到:

- User 可以使用的 LED 有:
- 橘色 LED,是 PD13
- 綠色 LED,是 PD12
- 紅色 LED,是 PD14
- 藍色 LED,是 PD15
- 這次 lab 會用到的藍色按鈕,則是 PA0
## Prerequisites and Configuration Setup

- 對 `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):

另外,在 `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;
}
}
}
```
結果:

可以看到要達到能被 `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. */
}
``` -->