# (志航) [STM32]Button Debouncing,SPI (Serial Peripheral Interface),W25Q32FV - Serial NOR Flash
## 按鍵抖動偵測
利用Timer寫出1ms傳送一個 "." 實現按鈕抖動的實際狀況,使用外部中斷偵測按鈕rising edge及falling edge的按下及放開。
偵測到高電位時發送"1",低電位時發送"0"。
***
### 未加入預防抖動時間
rising edge 按鈕按下

rising edge 按鈕放開

falling edge 按鈕按下

falling edge 按鈕放開

### 加入預防抖動時間
falling edge 按鈕放開

#### 主程式
```c=
//c
int main(void)
{
while (1) {
// 如果按鈕被按下 (檢查 GPIOB 的 PBD_Pin 引腳)
if (PB_ON(GPIOB, PBD_Pin)) { // 使用 PB_ON 函數檢查是否按下按鈕 PBD_Pin (通常為 GPIOB 的某個引腳)
// 檢查按鈕是否未設置為高電位 (通常用於消抖或確保按鈕按下狀態)
if (HAL_GPIO_ReadPin(GPIOB, PBD_Pin) != SET) { // 如果 PBD_Pin 沒有高電位
HAL_UART_Transmit(&huart1, (uint8_t *)"1", 1, 1000); // 透過 UART 傳送字串 "1",指示按鈕按下
}
// 切換 GPIOD 的 LD4_Pin LED 狀態 (開關切換)
HAL_GPIO_TogglePin(GPIOD, LD4_Pin); // 切換 GPIOD 的 LD4_Pin 引腳的狀態,以控制 LED 的開關
}
}
}
```
```c=
//c
void TIM2_IRQHandler(void)
{
FL += 1; // 每次進入中斷時,將變數 FL 增加 1,用於計算中斷觸發的次數
HAL_UART_Transmit(&huart1, (uint8_t *)".", 1, 1000); // 通過 UART1 傳送一個點(".")字元,顯示中斷已觸發
if (FL > 1000) // 當 FL 的值超過 1000 時
{
FL = 0; // 將 FL 重置為 0,避免數值過大
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == PBU_Pin) // 檢查是否是 PBU_Pin (假設是 PB3) 引腳觸發中斷
{
// 檢查該引腳的電位狀態
if (HAL_GPIO_ReadPin(GPIOD, PBU_Pin) == SET) // 如果 PBU_Pin 引腳的狀態為高電位
{
HAL_UART_Transmit(&huart1, (uint8_t *)"1", 1, 1000); // 通過 UART1 發送字元 "1" 表示高電位
HAL_GPIO_TogglePin(GPIOD, LD5_Pin); // 切換 LED5 的狀態
}
else // 如果 PBU_Pin 引腳的狀態為低電位
{
HAL_UART_Transmit(&huart1, (uint8_t *)"0", 1, 1000); // 通過 UART1 發送字元 "0" 表示低電位
HAL_GPIO_TogglePin(GPIOD, LD4_Pin); // 切換 LED4 的狀態
}
}
}
uint8_t PB_ON (GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) {
uint8_t PB = 0; // 定義一個變數 PB,初始值為 0,用於表示按鈕狀態
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin)) // 如果 GPIO 引腳的輸入為高電平 (按鈕被按下)
{
my_Delay(20); // 延遲 20 毫秒,用於消除按鈕去彈跳的影響
PB = 1; // 設置 PB 為 1,表示按鈕被按下
}
while(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin)) // 當按鈕仍保持被按下的狀態時
{
// 等待按鈕釋放
}
my_Delay(20); // 再次延遲 20 毫秒,用於去除按鈕釋放的彈跳影響
return PB; // 返回按鈕狀態,若按鈕被按下過,則返回 1,否則返回 0
}
```
___
## SPI (Serial Peripheral Interface)
SPI(Serial Peripheral Interface)是一種常見的串列通訊協議,用於在MCU和外部設備之間進行高速數據傳輸。SPI通訊通常用於連接MCU與各種外部設備,如感測器、螢幕、儲存裝置、無線模組等。
### 基本原理
#### SPI通訊是一種同步通訊協議,它基於主從(Master-Slave)架構。以下是SPI通訊的基本原理:
**主機(Master):**MCU當主機,生成時鐘訊號並控制數據傳輸。
**從機(Slave):**外部設備(如感測器或儲存裝置)當從機,按照主機的時鐘訊號進行數據傳輸。
#### SPI通訊使用四條訊號線進行通訊
SCLK(Serial Clock):主機生成的時鐘訊號,用於同步數據傳輸。
MISO(Master In Slave Out):從機到主機的數據傳輸線,用於將數據從從機傳輸到主機。
MOSI(Master Out Slave In):主機到從機的數據傳輸線,用於將數據從主機傳輸到從機。
CS/SS(Chip Select/Slave Select):選擇要與主機通訊的從機的信號線。當CS/SS訊號為低電位時,表示該從機被選擇。


#### SPI(Serial Peripheral Interface)為主從式同步串列通訊,可分為單工/全雙工
單工:線路上的訊號只能做單向傳送
半雙工:線路上的訊號可以雙向傳送 , 但是不能同時傳送
全雙工:線路上的訊號可以同時雙向傳送
同步:傳送端和接收端共用同一個CLOCK
所有的傳輸都會根據一個共同的頻率訊號 , 此頻率訊號產生自”主控裝置(Master端)”, 從屬裝置(Slave端)會用此頻率訊號來對收到的位串流進行同步
如果有多個周邊晶片被連到同一個SPI介面 , 主控裝置能透過SS pin腳的電位高低來選擇接收資料的周邊裝置

SPI_CR1 中有兩個 bits 時鐘極性CPOL 和 時鐘相位CPHA 控制取值的時間關係,總共有4種組合。
CPOL(clock polarity) 決定閒置時 clock 的電位。
CPOL = 0 表閒置時為低電位。
CPOL = 1 表閒置時為高電位。
CPHA(clock phase) 決定在 clock 的哪個 edge 取值。
CPHA = 0 表示在第一個 edge (Rising,when CPOL=0.Falling,when CPOL=1.)取值。
CPHA = 1 表示在第二個 edge (Falling,when CPOL=1.Rising,when CPOL=0.)取值。

## W25Q32FV - Serial NOR Flash
快閃記憶體(NOR Flash)是一種非揮發性記憶體技術,廣泛應用於嵌入式系統和儲存設備中。
### 基本特性
非揮發性:即使在沒有電力的情況下,存儲在 NOR Flash 中的數據也不會丟失。
可擦寫性:數據可以寫入、讀取和擦除,但在操作上一般需要擦除整個扇區或區塊,而不是單個字節。
Nor Flash主要應用在程式碼的儲存,容量較小、寫入速度慢,但因隨機讀取速度快,不適合朝大容量發展,主要用在手機上,目前以16Mb、32Mb為主。


### W25Q32 記憶體結構:
W25Q32 共有 64 個block,因此總容量為 4MB。
區塊 (block):
每個block的大小為 64KB。 1 block = 16 sector。
扇區 (sector):
每個sector的大小為 4KB。1 sector = 16 page。
頁 (page):
1 page = 256byte。
字節 (byte):
1 byte = 8 bit。每個byte有唯一的address。
位 (bit):
最小的單位是bit,每個bit的值是 1 或 0。每 8 bit 構成 1 個byte。
### W25Q32 常用指令集(非所有指令)

#### 硬體規劃

### 程式邏輯圖

#### 主程式
```c=
//c
uint32_t FLASH_SIZE = 4 * 1024 * 1024; // FLASH 大小為 4MB (4 * 1024 * 1024 位元組)
uint32_t Data_Address = 4090; // 設置為 0x0FFA,表示跨扇區和跨頁的邊界
// 要寫入的數據
uint8_t Write_data[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
#define Write_data_SIZE sizeof(Write_data) // 定義要寫入數據的大小
// 用於存放讀取數據的緩衝區
uint8_t Read_data[100] = {0}; // 用於讀取操作的數據緩衝區
#define Read_data_SIZE sizeof(Read_data) // 定義讀取緩衝區的大小
int main(void)
{
read_W25Q128_ID();
W25Q128_test();
}
```
#### 顯示製造商和裝置ID
```c=
//c
void read_W25Q128_ID()
{
uint8_t _RxData[2] = {0x00};
W25Q128_Enable(); // 啟用裝置(將 /CS 拉低)
// 發送指令
spi2_Transmit_one_byte(0x90); // 發送 0x90 指令,用於讀取製造商和裝置 ID
spi2_Transmit_one_byte(0x00); // 傳送第一個 8 位地址(預設為 0x00)
spi2_Transmit_one_byte(0x00); // 傳送第二個 8 位地址(預設為 0x00)
spi2_Transmit_one_byte(0x00); // 傳送第三個 8 位地址(預設為 0x00)
// 接收數據
_RxData[0] = spi2_Receive_one_byte(); // 接收製造商 ID
_RxData[1] = spi2_Receive_one_byte(); // 接收裝置 ID
W25Q128_Disable(); // 停用裝置(將 /CS 拉高)
// 以十六進位格式顯示讀取到的 ID
printf("Manufacturer ID: 0x%02X, Device ID: 0x%02X\r\n", _RxData[0], _RxData[1]);
}
void W25Q128_Enable()
{
HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, RESET); // 啟用晶片選擇 (將 /CS 拉低)
}
void spi2_Transmit_one_byte(uint8_t _dataTx)
{
HAL_SPI_Transmit(&hspi1, (uint8_t*) &_dataTx, 1, HAL_MAX_DELAY); // 傳送一個位元組的數據
}
uint8_t spi2_Receive_one_byte()
{
uint16_t _dataRx;
HAL_SPI_Receive(&hspi1, (uint8_t*) &_dataRx, 1, HAL_MAX_DELAY); // 接收一個位元組的數據
return _dataRx;
}
void W25Q128_Disable()
{
HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, SET); // 停用晶片選擇 (將 /CS 拉高)
}
```
#### 讀取並顯示原始數據、清除扇區、顯示清除後的數據、寫入新數據、最後讀取並顯示寫入後的數據
```c=
//c
void W25Q128_test()
{
// 讀取並顯示原始數據
Read_W25Q128_data(Read_data, Data_Address, Read_data_SIZE);
printf("Original data:\n");
for (uint8_t i = 0; i < Write_data_SIZE; i++)
printf("0x%02X ", Read_data[i]); // 使用十六進位顯示數據
printf("\r\n");
// 擦除需要寫入數據的扇區
Erase_Write_data_Sector(Data_Address, Write_data_SIZE);
// 再次讀取並顯示擦除後的數據
Read_W25Q128_data(Read_data, Data_Address, Read_data_SIZE);
printf("After erase:\n");
for (uint8_t i = 0; i < Write_data_SIZE; i++)
printf("0x%02X ", Read_data[i]);
printf("\r\n");
// 寫入數據
Write_Page(Write_data, Data_Address, Write_data_SIZE);
// 讀取並顯示寫入後的數據
Read_W25Q128_data(Read_data, Data_Address, Read_data_SIZE);
printf("After write:\n");
for (uint8_t i = 0; i < Write_data_SIZE; i++)
printf("0x%02X ", Read_data[i]);
printf("\r\n");
}
```
讀取資料
```c=
void Read_W25Q128_data(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
uint16_t i = 0;
W25Q128_Enable(); // 啟用裝置 (將 /CS 拉低,使能器件)
spi2_Transmit_one_byte(0x03); // 發送 0x03 指令,表示讀取數據
spi2_Transmit_one_byte((uint8_t)((ReadAddr) >> 16)); // 發送 24 位地址的高位元組 (A23–A16)
spi2_Transmit_one_byte((uint8_t)((ReadAddr) >> 8)); // 發送 24 位地址的中位元組 (A15–A8)
spi2_Transmit_one_byte((uint8_t)ReadAddr); // 發送 24 位地址的低位元組 (A7–A0)
for (; i < NumByteToRead; i++)
{
pBuffer[i] = spi2_Receive_one_byte(); // 循環接收數據,將接收到的位元組存入緩衝區 pBuffer
}
W25Q128_Disable(); // 停用裝置 (將 /CS 拉高,取消選擇)
}
```
清除扇區資料
```c=
void Erase_Write_data_Sector(uint32_t Address, uint32_t Write_data_NUM)
{
// 總共 4096 個扇區
// 計算從寫入數據的開始地址到寫入數據的結尾地址跨越的扇區數量
uint16_t Start_Sector, End_Sector, Num_Sector;
Start_Sector = Address / 4096; // 計算數據寫入開始的扇區位置
End_Sector = (Address + Write_data_NUM) / 4096; // 計算數據寫入結束的扇區位置
Num_Sector = End_Sector - Start_Sector; // 計算寫入操作跨越的扇區數量
// 開始逐個擦除所需的扇區
for (uint16_t i = 0; i <= Num_Sector; i++)
{
Erase_one_Sector(Address); // 擦除指定的扇區
Address += 4096; // 將地址更新到下一個扇區的開始位置
}
}
void Erase_one_Sector(uint32_t Address)
{
W25Q128_Write_Enable(); // 設置寫入使能 (允許執行擦除操作)
W25Q128_Wait_Busy(); // 確認 Flash 處於空閒狀態,確保可以執行新操作
W25Q128_Enable(); // 啟用裝置 (將 /CS 拉低)
spi2_Transmit_one_byte(0x20); // 發送 0x20 指令,表示執行扇區擦除操作
spi2_Transmit_one_byte((Address >> 16) & 0xFF); // 傳送 24 位元地址的高位元組 (A23–A16)
spi2_Transmit_one_byte((Address >> 8) & 0xFF); // 傳送 24 位元地址的中位元組 (A15–A8)
spi2_Transmit_one_byte(Address & 0xFF); // 傳送 24 位元地址的低位元組 (A7–A0)
W25Q128_Disable(); // 停用裝置 (將 /CS 拉高,結束通訊)
W25Q128_Wait_Busy(); // 等待擦除操作完成,確保 Flash 準備好接受新的操作
}
void W25Q128_Wait_Busy()
{
while ((W25Q128_ReadSR() & 0x01) == 0x01); // 讀取狀態寄存器 BUSY 位,等待 BUSY 位清空
}
uint8_t W25Q128_ReadSR(void)
{
uint8_t byte = 0;
W25Q128_Enable(); // 啟用裝置 (將 /CS 拉低)
spi2_Transmit_one_byte(0x05); // 發送 0x05 指令,讀取狀態寄存器
byte = spi2_Receive_one_byte(); // 接收並儲存狀態寄存器的內容
W25Q128_Disable(); // 停用裝置 (將 /CS 拉高)
return byte; // 返回狀態寄存器內容
}
```
寫入資料
```c=
void Write_Page(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t Word_remain = 256 - (WriteAddr % 256); // 計算當前頁剩餘可寫入的字節數
if (NumByteToWrite <= Word_remain)
Word_remain = NumByteToWrite; // 如果數據可在當前頁寫完,則更新 Word_remain 為數據大小
while (1)
{
Write_Word(pBuffer, WriteAddr, Word_remain); // 將 Word_remain 字節的數據寫入當前頁
W25Q128_Wait_Busy(); // 等待寫入操作完成
if (NumByteToWrite == Word_remain)
break; // 如果所有數據已經寫入,則結束
else // 如果還有剩餘數據,進行翻頁繼續寫入
{
pBuffer += Word_remain; // 更新緩衝區指標,指向未寫入的剩餘數據
WriteAddr += Word_remain; // 更新寫入地址
NumByteToWrite -= Word_remain; // 減去已寫入的數據數量
if (NumByteToWrite > 256)
Word_remain = 256; // 若剩餘數據超過 256 字節,則可在下一頁寫滿 256 字節
else
Word_remain = NumByteToWrite; // 若剩餘數據不足 256 字節,則將 Word_remain 更新為剩餘數據大小
}
}
}
void Write_Word(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t i;
W25Q128_Write_Enable(); // 啟用寫入使能 (設置 WEL)
W25Q128_Enable(); // 啟用裝置 (將 /CS 拉低,選擇晶片)
spi2_Transmit_one_byte(0x02); // 發送 0x02 指令,用於執行寫入操作
spi2_Transmit_one_byte((uint8_t)((WriteAddr) >> 16)); // 傳送 24 位目標地址的高位元組 (A23–A16)
spi2_Transmit_one_byte((uint8_t)((WriteAddr) >> 8)); // 傳送 24 位目標地址的中位元組 (A15–A8)
spi2_Transmit_one_byte((uint8_t)WriteAddr); // 傳送 24 位目標地址的低位元組 (A7–A0)
for (i = 0; i < NumByteToWrite; i++)
spi2_Transmit_one_byte(pBuffer[i]); // 循環寫入每個位元組的數據
W25Q128_Disable(); // 停用裝置 (將 /CS 拉高,結束通訊)
W25Q128_Wait_Busy(); // 等待寫入操作完成,確保 Flash 準備好接受新指令
}
void W25Q128_Write_Enable()
{
W25Q128_Enable(); // 啟用裝置 (將 /CS 拉低,使能裝置)
spi2_Transmit_one_byte(0x06); // 發送 0x06 指令,設置寫入使能 (WEL 位設為 1)
W25Q128_Disable(); // 停用裝置 (將 /CS 拉高,取消裝置選擇)
}
```
#### 成果
{%youtube XqZ81sQxapM%}
## IIC
Inter-Integrated Circuit, IIC 或稱為 I2C ( I Square C ) ,是飛利浦公司於 1980 年代發表的通訊界面,主要用在電路板之間的短距離通訊。其介面簡單只有兩條線路分別是 SCL 時脈線與 SDA 資料線,在標準模式下的傳輸速度為 100 Kbps,快速模式下為 400 Kbps 也有一些裝置能提供最高 5 Mbps 的傳輸速度。
I2C 在硬體上採用開集極 ( open collect ) 或開洩極 ( open drain ) ,當線路閒置時,會保持在高電位,這是由於所有裝置都處於開放狀態,且上拉電阻將線路拉高。可以避免所有裝置短路或互相干擾。

***
### I2C 總線在傳送數據過程中共有三種類型的信號:
#### 開始信號:
當 SCL 為高電平時,SDA 由高電平跳變為低電平,開始傳送數據。
#### 結束信號:
當 SCL 為高電平時,SDA 由低電平跳變為高電平,結束傳送數據。
#### 應答信號:
接收數據的 IC 在接收到 8 位元數據後,向發送數據的 IC 發出特定的低電平脈衝,表示已接收到數據。CPU 向受控單元發出一個信號後,等待受控單元發出應答信號。當 CPU 接收到應答信號後,會根據實際情況決定是否繼續傳遞信號。若未收到應答信號,則判斷受控單元可能發生故障。

***
在MASTER傳送起始信號後,必須傳送一個SLAVE Address(7位),第 8 位為Write/Read Bit,用「0」表示MASTER發送資料,「1」表示MASTER接收資料。第 9 位為 ACK 應答位,緊接著為第一個資料字節,然後是一個應答位,後面繼續傳送第 2 個資料字節。


***