# (志航) [STM32]Button Debouncing,SPI (Serial Peripheral Interface),W25Q32FV - Serial NOR Flash ## 按鍵抖動偵測 利用Timer寫出1ms傳送一個 "." 實現按鈕抖動的實際狀況,使用外部中斷偵測按鈕rising edge及falling edge的按下及放開。 偵測到高電位時發送"1",低電位時發送"0"。 *** ### 未加入預防抖動時間 rising edge 按鈕按下 ![pulldn_rising_edge按下偵測](https://hackmd.io/_uploads/ry7NCUwgyx.png) rising edge 按鈕放開 ![pullup_rising_edge放開偵測](https://hackmd.io/_uploads/S1m4CUve1x.png) falling edge 按鈕按下 ![pullup_falling_edge按下偵測](https://hackmd.io/_uploads/r1XE0LPeJx.png) falling edge 按鈕放開 ![pulldn_falling_edge放開偵測](https://hackmd.io/_uploads/Hy7NAIvxke.png) ### 加入預防抖動時間 falling edge 按鈕放開 ![image](https://hackmd.io/_uploads/r1ujmRnlye.png) #### 主程式 ```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訊號為低電位時,表示該從機被選擇。 ![image](https://hackmd.io/_uploads/r1VLhBogyx.png) ![image](https://hackmd.io/_uploads/S1CjkIsl1g.png) #### SPI(Serial Peripheral Interface)為主從式同步串列通訊,可分為單工/全雙工 單工:線路上的訊號只能做單向傳送 半雙工:線路上的訊號可以雙向傳送 , 但是不能同時傳送 全雙工:線路上的訊號可以同時雙向傳送 同步:傳送端和接收端共用同一個CLOCK 所有的傳輸都會根據一個共同的頻率訊號 , 此頻率訊號產生自”主控裝置(Master端)”, 從屬裝置(Slave端)會用此頻率訊號來對收到的位串流進行同步 如果有多個周邊晶片被連到同一個SPI介面 , 主控裝置能透過SS pin腳的電位高低來選擇接收資料的周邊裝置 ![image](https://hackmd.io/_uploads/S1W_aHsekx.png) 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.)取值。 ![image](https://hackmd.io/_uploads/HyVdbLigkx.png) ## W25Q32FV - Serial NOR Flash 快閃記憶體(NOR Flash)是一種非揮發性記憶體技術,廣泛應用於嵌入式系統和儲存設備中。 ### 基本特性 非揮發性:即使在沒有電力的情況下,存儲在 NOR Flash 中的數據也不會丟失。 可擦寫性:數據可以寫入、讀取和擦除,但在操作上一般需要擦除整個扇區或區塊,而不是單個字節。 Nor Flash主要應用在程式碼的儲存,容量較小、寫入速度慢,但因隨機讀取速度快,不適合朝大容量發展,主要用在手機上,目前以16Mb、32Mb為主。 ![image](https://hackmd.io/_uploads/H1u9UUslke.png) ![image](https://hackmd.io/_uploads/By6a5Usl1g.png) ### 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 常用指令集(非所有指令) ![image](https://hackmd.io/_uploads/S1akR2jxJx.png) #### 硬體規劃 ![螢幕擷取畫面 2024-10-28 141212](https://hackmd.io/_uploads/H1t_Aone1l.png) ### 程式邏輯圖 ![螢幕擷取畫面 2024-10-28 141704](https://hackmd.io/_uploads/r1wFns3g1l.png) #### 主程式 ```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 ) ,當線路閒置時,會保持在高電位,這是由於所有裝置都處於開放狀態,且上拉電阻將線路拉高。可以避免所有裝置短路或互相干擾。 ![image](https://hackmd.io/_uploads/Bk0VS2hl1x.png) *** ### I2C 總線在傳送數據過程中共有三種類型的信號: #### 開始信號: 當 SCL 為高電平時,SDA 由高電平跳變為低電平,開始傳送數據。 #### 結束信號: 當 SCL 為高電平時,SDA 由低電平跳變為高電平,結束傳送數據。 #### 應答信號: 接收數據的 IC 在接收到 8 位元數據後,向發送數據的 IC 發出特定的低電平脈衝,表示已接收到數據。CPU 向受控單元發出一個信號後,等待受控單元發出應答信號。當 CPU 接收到應答信號後,會根據實際情況決定是否繼續傳遞信號。若未收到應答信號,則判斷受控單元可能發生故障。 ![未命名33](https://hackmd.io/_uploads/r19UOT2gyg.png) *** 在MASTER傳送起始信號後,必須傳送一個SLAVE Address(7位),第 8 位為Write/Read Bit,用「0」表示MASTER發送資料,「1」表示MASTER接收資料。第 9 位為 ACK 應答位,緊接著為第一個資料字節,然後是一個應答位,後面繼續傳送第 2 個資料字節。 ![未命名22](https://hackmd.io/_uploads/SkQo2ahx1g.png) ![未命名](https://hackmd.io/_uploads/HyxP533g1g.png) ***