【STM32F407開發板使用者手冊】第32章 STM32F407的SPI匯流排應用之驅動W25QXX(支援查詢,中斷和DMA)
最新教程下載:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第32章 STM32F407的SPI匯流排應用之驅動W25QXX(支援查詢,中斷和DMA)
本章節為大家講解標準SPI接線方式驅動W25QXX,實現了查詢,中斷和DMA三種方式。
32.1 初學者重要提示
32.2 W25QXX硬體設計
32.4 W25QXX關鍵知識點整理(重要)
32.5 W25QXX驅動設計
32.6 SPI匯流排板級支援包(bsp_spi_bus.c)
32.7 W25QXX板級支援包(bsp_spi_flash.c)
32.8 使用例程設計框架
32.9 實驗例程說明(MDK)
32.10 實驗例程說明(IAR)
32.11 總結
32.1 初學者重要提示
- 學習本章節前,務必優先學習第31章。
- W25Q64FV屬於NOR型Flash儲存晶片。
- W25Q64FV手冊下載地址:連結 (這是一個超連結),當前章節配套例子的Doc檔案件裡面也有存放。
- 本章第3小節整理的知識點比較重要,務必要了解下,特別是頁程式設計和頁回捲。
- 對SPI Flash W25QXX的不同接線方式(1線,2線或者4線,這裡的線是指的資料線),程式設計命令是不同的。
- W25Q64FV最高支援104MHz,但最高讀命令03H速度是50MHz。
- 檔案bsp_spi_bus.c檔案公共的匯流排驅動檔案,支援序列FLASH、TSC2046、VS1053、AD7705、ADS1256等SPI裝置的配置。
- 函式sf_WriteBuffer不需要使用者做擦除,會自動執行擦除功能,支援任意大小,任意地址,不超過晶片容量即可。
32.2 W25QXX硬體設計
STM32F4驅動W25QXX的硬體設計如下:
關於這個原理圖,要了解到以下幾個知識:
- 當前V5開發板實際外接的晶片是W25Q64FV。
- CS片選最好接上拉電阻,防止意外操作。
- 這裡的PB3,PB4和PB5引腳可以複用SPI1,SPI3。實際應用中是複用的SPI1。
- W25Q64的WP引腳用於防寫,低電平有效性,當前是直接高電平。
- HOLD引腳也是低電平有效,當前是將其接到高電平。此引腳的作用是CS片選低電平時,DO引腳輸出高阻,忽略CLK和DI引腳上的訊號。
32.3 W25QXX關鍵知識點整理(重要)
驅動W25QXX前要先了解下這個晶片的相關資訊。
32.3.1 W25QXX基礎資訊
- W25Q64FV的容量是8MB(256Mbit)。
- W25Q64FV支援標準SPI(單線SPI),用到引腳CLK、CS,DI和DO引腳。
支援兩線SPI,用到引腳CLK、CS、IO0、IO1 。
支援四線SPI,用到引腳CLK、CS、IO0、IO1,IO2、IO3。
(注:這裡幾線的意思是幾個資料線)。
- W25Q64FV支援的最高時鐘是133MHz。
- 每個扇區最少支援10萬次擦寫,可以儲存20年資料。
- 頁大小是256位元組,支援頁程式設計,也就是一次編寫256個位元組,也可以一個一個編寫。
- 支援4KB為單位的扇區擦除,也可以32KB或者64KB為單位的擦除。
整體框圖如下:
W25Q64FV:
- 有128個Block,每個Block大小64KB。
- 每個Block有16個Sector,每個Sector大小4KB。
- 每個Sector有16個Page,每個Page大小是256位元組。
32.3.2 W25QXX命令
使用W25Q的接線方式不同,使用的命令也有所不同,使用的時候務必要注意,當前我們使用的標準SPI,即單線SPI,使用的命令如下:
當前主要用到如下幾個命令:
#define CMD_EWRSR 0x50 /* 允許寫狀態暫存器的命令 */ #define CMD_WRSR 0x01 /* 寫狀態暫存器命令 */ #define CMD_WREN 0x06 /* 寫使能命令 */ #define CMD_READ 0x03 /* 讀資料區命令 */ #define CMD_RDSR 0x05 /* 讀狀態暫存器命令 */ #define CMD_RDID 0x9F /* 讀器件ID命令 */ #define CMD_SE 0x20 /* 擦除扇區命令 */ #define CMD_BE 0xC7 /* 批量擦除命令 */ #define WIP_FLAG 0x01 /* 狀態暫存器中的正在程式設計標誌(WIP) */
32.3.3 W25QXX頁程式設計和頁回捲
SPI Flash僅支援頁程式設計(頁大小256位元組),所有其它大批量資料的寫入都是以頁為單位。這裡注意所說的頁程式設計含義,頁程式設計分為以下三步(虛擬碼):
bsp_spiWrite1(0x02); ----------第1步傳送頁程式設計命令 bsp_spiWrite1((_uiWriteAddr & 0xFF0000) >> 16); ----------第2步傳送地址 bsp_spiWrite1((_uiWriteAddr & 0xFF00) >> 8); bsp_spiWrite1(_uiWriteAddr & 0xFF); for (i = 0; i < _usSize; i++) { bsp_spiWrite1(*_pBuf++); ----------第3步寫資料,此時就可以連續寫入資料了, 不需要再重新設定地址,地址會自增。這樣可以大大加快寫入速度。 }
頁程式設計的含義恰恰就體現在第3步了,如果使用者設定的“起始地址+資料長度”所確定的地址範圍超過了此起始地址所在的頁,地址自增不會超過頁範圍,而是重新回到了此頁的首地進行編寫。這一點要特別的注意。如果使用者不需要使用地址自增效果,那麼直接指定地址進行編寫即可。可以任意指定地址進行編寫,編寫前一定要進行擦除。
比如下面就是頁內操作(使用前已經進行了扇區擦除,每次擦除最少擦除一個扇區4KB):
uint8_t tempbuf[10] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x00}; uint8_t temp1 = 0x10, temp2 = 0x29, temp3 = 0x48;
- 從250地址開始寫入10個位元組資料 PageWrite(tempbuf, 250, 10);(因為一旦寫入超過地址255,就會從0地址開始重新寫)。
- 向地址20寫入1個位元組資料:PageWrite(&temp1, 20, 1);
- 向地址30寫入1個位元組資料:PageWrite(&temp2, 30, 1);
- 向地址510寫入1個位元組資料:PageWrite(&temp3, 510, 1) (這裡已經是寫到下一頁了)
下面是將從0地址到511地址讀取出來的512個位元組資料,一行32位元組。
32.3.4 W25QXX扇區擦除
SPI Flash的擦除支援扇區擦除(4KB),塊擦除(32KB或者64KB)以及整個晶片擦除。對於扇區擦除和塊擦除,使用的時候要注意一點,一般情況下,只需使用者給出扇區或者塊的首地址即可。
如果給的不是扇區或者塊的首地址也沒有關係的,只要此地址是在扇區或者塊的範圍內,此扇區或者塊也可以被正確擦除。不過建議使用時給首地址,方便管理。
32.3.5 W25QXX規格引數
這裡我們主要了解擦寫耗時和支援的時鐘速度,下面是擦寫時間引數:
- 頁程式設計時間:典型值0.45ms,最大值3ms。
- 扇區擦除時間(4KB):典型值45-60ms,最大值400ms。
- 塊擦除時間(32KB):典型值120ms,最大值1600ms。
- 塊擦除時間(64KB):典型值150ms,最大值2000ms。
- 整個晶片擦除時間:典型值20s,最大值100s。
支援的速度引數如下:
可以看到最高支援的讀時鐘(使用命令03H)速度是50MHz,其它命令速度可以做到104MHz。
32.4 W25QXX驅動設計
W25QXX的程式驅動框架設計如下:
有了這個框圖,程式設計就比較好理解了。
32.4.1 第1步:SPI匯流排配置
spi匯流排配置通過如下兩個函式實現:
/* ********************************************************************************************************* * 函 數 名: bsp_InitSPIBus * 功能說明: 配置SPI匯流排。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_InitSPIBus(void) { g_spi_busy = 0; bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_8, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); } /* ********************************************************************************************************* * 函 數 名: bsp_InitSPIParam * 功能說明: 配置SPI匯流排引數,時鐘分頻,時鐘相位和時鐘極性。 * 形 參: _BaudRatePrescaler SPI匯流排時鐘分頻設定,支援的引數如下: * SPI_BAUDRATEPRESCALER_2 2分頻 * SPI_BAUDRATEPRESCALER_4 4分頻 * SPI_BAUDRATEPRESCALER_8 8分頻 * SPI_BAUDRATEPRESCALER_16 16分頻 * SPI_BAUDRATEPRESCALER_32 32分頻 * SPI_BAUDRATEPRESCALER_64 64分頻 * SPI_BAUDRATEPRESCALER_128 128分頻 * SPI_BAUDRATEPRESCALER_256 256分頻 * * _CLKPhase 時鐘相位,支援的引數如下: * SPI_PHASE_1EDGE SCK引腳的第1個邊沿捕獲傳輸的第1個數據 * SPI_PHASE_2EDGE SCK引腳的第2個邊沿捕獲傳輸的第1個數據 * * _CLKPolarity 時鐘極性,支援的引數如下: * SPI_POLARITY_LOW SCK引腳在空閒狀態處於低電平 * SPI_POLARITY_HIGH SCK引腳在空閒狀態處於高電平 * * 返 回 值: 無 ********************************************************************************************************* */ void bsp_InitSPIParam(uint32_t _BaudRatePrescaler, uint32_t _CLKPhase, uint32_t _CLKPolarity) { /* 提高執行效率,只有在SPI硬體引數發生變化時,才執行HAL_Init */ if (s_BaudRatePrescaler == _BaudRatePrescaler && s_CLKPhase == _CLKPhase && s_CLKPolarity == _CLKPolarity) { return; } s_BaudRatePrescaler = _BaudRatePrescaler; s_CLKPhase = _CLKPhase; s_CLKPolarity = _CLKPolarity; /* 設定SPI引數 */ hspi.Instance = SPIx; /* 例化SPI */ hspi.Init.BaudRatePrescaler = _BaudRatePrescaler; /* 設定波特率 */ hspi.Init.Direction = SPI_DIRECTION_2LINES; /* 全雙工 */ hspi.Init.CLKPhase = _CLKPhase; /* 配置時鐘相位 */ hspi.Init.CLKPolarity = _CLKPolarity; /* 配置時鐘極性 */ hspi.Init.DataSize = SPI_DATASIZE_8BIT; /* 設定資料寬度 */ hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; /* 資料傳輸先傳高位 */ hspi.Init.TIMode = SPI_TIMODE_DISABLE; /* 禁止TI模式 */ hspi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /* 禁止CRC */ hspi.Init.CRCPolynomial = 7; /* 禁止CRC後,此位無效 */ hspi.Init.NSS = SPI_NSS_SOFT; /* 使用軟體方式管理片選引腳 */ hspi.Init.Mode = SPI_MODE_MASTER; /* SPI工作在主控模式 */ /* 復位SPI */ if(HAL_SPI_DeInit(&hspi) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } if (HAL_SPI_Init(&hspi) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } }
關於這兩個函式有以下兩點要做個說明:
- 函式bsp_InitSPIBus裡面的配置是個初始設定。實際驅動晶片時,會通過函式bsp_InitSPIParam做再配置。
- 函式bsp_InitSPIParam提供了時鐘分頻,時鐘相位和時鐘極性配置。驅動不同外設晶片時,基本上調整這三個引數就夠。當SPI介面上接了多個不同型別的晶片時,通過此函式可以方便的切換配置。
32.4.2 第2步:SPI匯流排的查詢,中斷和DMA方式設定
SPI驅動的查詢,中斷和DMA方式主要通過函式bsp_spiTransfer實現資料傳輸:
/* ********************************************************************************************************* * 選擇DMA,中斷或者查詢方式 ********************************************************************************************************* */ //#define USE_SPI_DMA /* DMA方式 */ //#define USE_SPI_INT /* 中斷方式 */ #define USE_SPI_POLL /* 查詢方式 */ uint8_t g_spiTxBuf[SPI_BUFFER_SIZE]; uint8_t g_spiRxBuf[SPI_BUFFER_SIZE]; /* ********************************************************************************************************* * 函 數 名: bsp_spiTransfer * 功能說明: 啟動資料傳輸 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_spiTransfer(void) { if (g_spiLen > SPI_BUFFER_SIZE) { return; } /* DMA方式傳輸 */ #ifdef USE_SPI_DMA wTransferState = TRANSFER_WAIT; if(HAL_SPI_TransmitReceive_DMA(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } while (wTransferState == TRANSFER_WAIT) { ; } #endif /* 中斷方式傳輸 */ #ifdef USE_SPI_INT wTransferState = TRANSFER_WAIT; if(HAL_SPI_TransmitReceive_IT(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } while (wTransferState == TRANSFER_WAIT) { ; } #endif /* 查詢方式傳輸 */ #ifdef USE_SPI_POLL if(HAL_SPI_TransmitReceive(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen, 1000000) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } #endif }
通過開頭巨集定義可以方便的切換中斷,查詢和DMA方式。
32.4.3 第3步:W25QXX的時鐘極性和時鐘相位配置
首先回憶下STM32F4支援的4種時序配置。
- 當CPOL = 1, CPHA = 1時
SCK引腳在空閒狀態處於高電平,SCK引腳的第2個邊沿捕獲傳輸的第1個數據。
- 當CPOL = 0, CPHA = 1時
SCK引腳在空閒狀態處於低電平,SCK引腳的第2個邊沿捕獲傳輸的第1個數據。
- 當CPOL = 1, CPHA = 0時
SCK引腳在空閒狀態處於高電平,SCK引腳的第1個邊沿捕獲傳輸的第1個數據。
- 當CPOL = 0, CPHA = 0時
SCK引腳在空閒狀態處於低電平,SCK引腳的第1個邊沿捕獲傳輸的第1個數據。
有了F4支援的時序配置,再來看下W25Q的時序圖:
Mode0 : 空閒狀態的sck是低電平。
Mode1 : 空閒狀態的sck是高電平。
首先W25Q是上升沿做資料採集,所以STM32F4的可選的配置就是:
CHOL = 1, CPHA = 1
CHOL = 0, CPHA = 0
對於這兩種情況,具體選擇哪種,繼續往下看。W25Q有兩種SCK模式,分別是Mode0和Mode3,也就是空閒狀態下,SCK既可以是高電平也可以是低電平。這樣的話,這兩種情況都可以使用,經過實際測試,STM32F4使用這兩個配置均可以配置驅動W25Q。
32.4.4 第4步:單SPI介面管理多個SPI裝置的切換機制
單SPI介面管理多個SPI裝置最麻煩的地方是不同裝置的時鐘分配,時鐘極性和時鐘相位並不相同。對此的解決解決辦法是在片選階段配置切換,比如SPI Flash的片選:
/* ********************************************************************************************************* * 函 數 名: sf_SetCS * 功能說明: 序列FALSH片選控制函式 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void sf_SetCS(uint8_t _Level) { if (_Level == 0) { bsp_SpiBusEnter(); bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_2, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); SF_CS_0(); } else { SF_CS_1(); bsp_SpiBusExit(); } }
通過這種方式就有效的解決了單SPI介面管理多裝置的問題。因為給每個裝置都配了一個獨立的片選引腳,這樣就可以為每個裝置都配置這麼一個片選配置。
但是頻繁配置也比較繁瑣,所以函式bsp_InitSPIParam裡面做了特別處理。當前配置與之前配置相同的情況下無需重複配置。
32.4.5 第5步:W25QXX的讀取實現
W25QXX的讀取功能比較好實現,傳送03H命令後,設定任意地址都可以讀取資料,只要不超過晶片容量即可。
/* ********************************************************************************************************* * 函 數 名: sf_ReadBuffer * 功能說明: 連續讀取若干位元組,位元組個數不能超出晶片容量。 * 形 參: _pBuf : 資料來源緩衝區; * _uiReadAddr :首地址 * _usSize :資料個數, 不能超出晶片總容量 * 返 回 值: 無 ********************************************************************************************************* */ void sf_ReadBuffer(uint8_t * _pBuf, uint32_t _uiReadAddr, uint32_t _uiSize) { uint16_t rem; uint16_t i; /* 如果讀取的資料長度為0或者超出序列Flash地址空間,則直接返回 */ if ((_uiSize == 0) ||(_uiReadAddr + _uiSize) > g_tSF.TotalSize) { return; } /* 擦除扇區操作 */ sf_SetCS(0); /* 使能片選 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = (CMD_READ); /* 傳送讀命令 */ g_spiTxBuf[g_spiLen++] = ((_uiReadAddr & 0xFF0000) >> 16); /* 傳送扇區地址的高8bit */ g_spiTxBuf[g_spiLen++] = ((_uiReadAddr & 0xFF00) >> 8); /* 傳送扇區地址中間8bit */ g_spiTxBuf[g_spiLen++] = (_uiReadAddr & 0xFF); /* 傳送扇區地址低8bit */ bsp_spiTransfer(); /* 開始讀資料,因為底層DMA緩衝區有限,必須分包讀 */ for (i = 0; i < _uiSize / SPI_BUFFER_SIZE; i++) { g_spiLen = SPI_BUFFER_SIZE; bsp_spiTransfer(); memcpy(_pBuf, g_spiRxBuf, SPI_BUFFER_SIZE); _pBuf += SPI_BUFFER_SIZE; } rem = _uiSize % SPI_BUFFER_SIZE; /* 剩餘位元組 */ if (rem > 0) { g_spiLen = rem; bsp_spiTransfer(); memcpy(_pBuf, g_spiRxBuf, rem); } sf_SetCS(1); /* 禁能片選 */ }
這個函式對DMA傳輸做了特別處理,方便分包進行。
32.4.6 第6步:W25QXX的扇區擦除實現
扇區擦除的實現也比較簡單,傳送“扇區擦除命令+扇區地址”即可完成相應扇區的擦除。擦除的扇區大小是4KB。
/* ********************************************************************************************************* * 函 數 名: sf_EraseSector * 功能說明: 擦除指定的扇區 * 形 參: _uiSectorAddr : 扇區地址 * 返 回 值: 無 ********************************************************************************************************* */ void sf_EraseSector(uint32_t _uiSectorAddr) { sf_WriteEnable(); /* 傳送寫使能命令 */ /* 擦除扇區操作 */ sf_SetCS(0); /* 使能片選 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = CMD_SE; /* 傳送擦除命令 */ g_spiTxBuf[g_spiLen++] = ((_uiSectorAddr & 0xFF0000) >> 16); /* 傳送扇區地址的高8bit */ g_spiTxBuf[g_spiLen++] = ((_uiSectorAddr & 0xFF00) >> 8); /* 傳送扇區地址中間8bit */ g_spiTxBuf[g_spiLen++] = (_uiSectorAddr & 0xFF); /* 傳送扇區地址低8bit */ bsp_spiTransfer(); sf_SetCS(1); /* 禁能片選 */ sf_WaitForWriteEnd(); /* 等待序列Flash內部寫操作完成 */ }
整個晶片的擦除更省事些,僅傳送整個晶片擦除命令即可:
/* ********************************************************************************************************* * 函 數 名: sf_EraseChip * 功能說明: 擦除整個晶片 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void sf_EraseChip(void) { sf_WriteEnable(); /* 傳送寫使能命令 */ /* 擦除扇區操作 */ sf_SetCS(0); /* 使能片選 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = CMD_BE; /* 傳送整片擦除命令 */ bsp_spiTransfer(); sf_SetCS(1); /* 禁能片選 */ sf_WaitForWriteEnd(); /* 等待序列Flash內部寫操作完成 */ }
32.4.7 第7步:W25QXX的程式設計實現
W25QXX的程式設計實現略複雜,因為做了自動擦除支援,大家可以在任意地址,寫任意大小的資料,只要不超過晶片容量即可。我們這裡就不做展開討論了,大家有興趣可以研究下:
/* ********************************************************************************************************* * 函 數 名: sf_WriteBuffer * 功能說明: 寫1個扇區並校驗,如果不正確則再重寫兩次,本函式自動完成擦除操作。 * 形 參: _pBuf : 資料來源緩衝區; * _uiWrAddr :目標區域首地址 * _usSize :資料個數,任意大小,但不能超過晶片容量。 * 返 回 值: 1 : 成功, 0 : 失敗 ********************************************************************************************************* */ uint8_t sf_WriteBuffer(uint8_t* _pBuf, uint32_t _uiWriteAddr, uint32_t _usWriteSize) { uint32_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0; Addr = _uiWriteAddr % g_tSF.SectorSize; count = g_tSF.SectorSize - Addr; NumOfPage = _usWriteSize / g_tSF.SectorSize; NumOfSingle = _usWriteSize % g_tSF.SectorSize; if (Addr == 0) /* 起始地址是扇區首地址 */ { if (NumOfPage == 0) /* 資料長度小於扇區大小 */ { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0) { return 0; } } else /* 資料長度大於等於扇區大小 */ { while (NumOfPage--) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0) { return 0; } _uiWriteAddr += g_tSF.SectorSize; _pBuf += g_tSF.SectorSize; } if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0) { return 0; } } } else /* 起始地址不是扇區首地址 */ { if (NumOfPage == 0) /* 資料長度小於扇區大小 */ { if (NumOfSingle > count) /* (_usWriteSize + _uiWriteAddr) > SPI_FLASH_PAGESIZE */ { temp = NumOfSingle - count; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0) { return 0; } _uiWriteAddr += count; _pBuf += count; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, temp) == 0) { return 0; } } else { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0) { return 0; } } } else /* 資料長度大於等於扇區大小 */ { _usWriteSize -= count; NumOfPage = _usWriteSize / g_tSF.SectorSize; NumOfSingle = _usWriteSize % g_tSF.SectorSize; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0) { return 0; } _uiWriteAddr += count; _pBuf += count; while (NumOfPage--) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0) { return 0; } _uiWriteAddr += g_tSF.SectorSize; _pBuf += g_tSF.SectorSize; } if (NumOfSingle != 0) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0) { return 0; } } } } return 1; /* 成功 */ }
32.5 SPI匯流排板級支援包(bsp_spi_bus.c)
SPI匯流排驅動檔案bsp_spi_bus.c主要實現瞭如下幾個API供使用者呼叫:
- bsp_InitSPIBus
- bsp_InitSPIParam
- bsp_spiTransfer
32.5.1 函式bsp_InitSPIBus
函式原型:
void bsp_InitSPIBus(void)
函式描述:
此函式主要用於SPI匯流排的初始化,在bsp.c檔案呼叫一次即可。
32.5.2 函式bsp_InitSPIParam
函式原型:
void bsp_InitSPIParam(uint32_t _BaudRatePrescaler, uint32_t _CLKPhase, uint32_t _CLKPolarity)
函式描述:
此函式用於SPI匯流排的配置。
函式引數:
- 第1個引數SPI匯流排的分頻設定,支援的引數如下:
SPI_BAUDRATEPRESCALER_2 2分頻
SPI_BAUDRATEPRESCALER_4 4分頻
SPI_BAUDRATEPRESCALER_8 8分頻
SPI_BAUDRATEPRESCALER_16 16分頻
SPI_BAUDRATEPRESCALER_32 32分頻
SPI_BAUDRATEPRESCALER_64 64分頻
SPI_BAUDRATEPRESCALER_128 128分頻
SPI_BAUDRATEPRESCALER_256 256分頻
- 第2個引數用於時鐘相位配置,支援的引數如下:
SPI_PHASE_1EDGE SCK引腳的第1個邊沿捕獲傳輸的第1個數據
SPI_PHASE_2EDGE SCK引腳的第2個邊沿捕獲傳輸的第1個數據
- 第3個引數是時鐘極性配置,支援的引數如下:
SPI_POLARITY_LOW SCK引腳在空閒狀態處於低電平
SPI_POLARITY_HIGH SCK引腳在空閒狀態處於高電平
32.5.3 函式bsp_spiTransfer
函式原型:
void bsp_spiTransfer(void)
函式描述:
此函式用於啟動SPI資料傳輸,支援查詢,中斷和DMA方式傳輸。
32.6 W25QXX板級支援包(bsp_spi_flash.c)
W25QXX驅動檔案bsp_spi_flash.c主要實現瞭如下幾個API供使用者呼叫:
- sf_ReadBuffer
- sf_WriteBuffer
- sf_EraseSector
- sf_EraseChip
- sf_EraseSector
32.6.1 函式sf_ReadBuffer
函式原型:
void sf_ReadBuffer(uint8_t * _pBuf, uint32_t _uiReadAddr, uint32_t _uiSize)
函式描述:
此函式主要用於從SPI Flash讀取資料,支援任意大小,任意地址,不超過晶片容量即可。
函式引數:
- 第1個引數用於儲存從SPI Flash讀取的資料。
- 第2個引數是讀取地址,不可以超過晶片容量。
- 第3個引數是讀取的資料大小,讀取範圍不可以超過晶片容量。
32.6.2 函式sf_WriteBuffer(自動執行擦除)
函式原型:
uint8_t sf_WriteBuffer(uint8_t* _pBuf, uint32_t _uiWriteAddr, uint32_t _usWriteSize)
函式描述:
此函式主要用於SPI Flash讀取資料,支援任意大小,任意地址,不超過晶片容量即可。特別注意,此函式會自動執行擦除,無需使用者處理。
函式引數:
- 第1個引數是源資料緩衝區。
- 第2個引數是目標區域首地址。
- 第3個引數是資料個數,支援任意大小,但不能超過晶片容量。單位位元組個數。
- 返回值,返回1表示成功,返回0表示失敗。
32.6.3 函式sf_EraseSector
函式原型:
void sf_EraseSector(uint32_t _uiSectorAddr)
函式描述:
此函式主要用於扇區擦除,一個扇區大小是4KB。
函式引數:
- 第1個引數是扇區地址,比如擦除扇區0,此處填0x0000,擦除扇區1,此處填0x1000,擦除扇區2,此處填0x2000,以此類推。
32.6.4 函式sf_EraseChip
函式原型:
void sf_EraseChip(void)
函式描述:
此函式主要用於整個晶片擦除。
32.6.5 函式sf_PageWrite(不推薦)
函式原型:
void sf_PageWrite(uint8_t * _pBuf, uint32_t _uiWriteAddr, uint16_t _usSize)
函式描述:
此函式主要用於頁程式設計,一次可以程式設計多個頁,只要不超過晶片容量即可。不推薦大家呼叫此函式,因為呼叫這個函式前,需要大家呼叫函式sf_EraseSector進行扇區擦除。
函式引數:
- 第1個引數是資料來源緩衝區。
- 第2個引數目標區域首地址,比如程式設計頁0,此處填0x0000,程式設計頁1,此處填0x0100,程式設計頁2,此處填0x0200,以此類推。
- 第3個引數是程式設計的資料大小,務必是256位元組的整數倍,單位位元組個數。
32.7 W25QXX驅動移植和使用
W25QXX移植步驟如下:
- 第1步:複製bsp_spi_bus.c,bsp_spi_bus.h,bsp_spi_flash.c,bsp_spi_flash.h到自己的工程目錄,並新增到工程裡面。
- 第2步:根據使用的第幾個SPI,SPI時鐘,SPI引腳和DMA通道等,修改bsp_spi_bus.c檔案開頭的巨集定義
/* ********************************************************************************************************* * 時鐘,引腳,DMA,中斷等巨集定義 ********************************************************************************************************* */ #define SPIx SPI1 #define SPIx_CLK_ENABLE() __HAL_RCC_SPI1_CLK_ENABLE() #define DMAx_CLK_ENABLE() __HAL_RCC_DMA2_CLK_ENABLE() #define SPIx_FORCE_RESET() __HAL_RCC_SPI1_FORCE_RESET() #define SPIx_RELEASE_RESET() __HAL_RCC_SPI1_RELEASE_RESET() #define SPIx_SCK_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_SCK_GPIO GPIOB #define SPIx_SCK_PIN GPIO_PIN_3 #define SPIx_SCK_AF GPIO_AF5_SPI1 #define SPIx_MISO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_MISO_GPIO GPIOB #define SPIx_MISO_PIN GPIO_PIN_4 #define SPIx_MISO_AF GPIO_AF5_SPI1 #define SPIx_MOSI_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_MOSI_GPIO GPIOB #define SPIx_MOSI_PIN GPIO_PIN_5 #define SPIx_MOSI_AF GPIO_AF5_SPI1 #define SPIx_TX_DMA_CHANNEL DMA_CHANNEL_3 #define SPIx_TX_DMA_STREAM DMA2_Stream3 #define SPIx_RX_DMA_CHANNEL DMA_CHANNEL_3 #define SPIx_RX_DMA_STREAM DMA2_Stream0 #define SPIx_IRQn SPI1_IRQn #define SPIx_IRQHandler SPI1_IRQHandler #define SPIx_DMA_TX_IRQn DMA2_Stream3_IRQn #define SPIx_DMA_RX_IRQn DMA2_Stream0_IRQn #define SPIx_DMA_TX_IRQHandler DMA2_Stream3_IRQHandler #define SPIx_DMA_RX_IRQHandler DMA2_Stream0_IRQHandler
- 第3步:根據使用的SPI ID,新增定義到檔案bsp_spi_flash.h。
/* 定義序列Flash ID */ enum { SST25VF016B_ID = 0xBF2541, MX25L1606E_ID = 0xC22015, W25Q64BV_ID = 0xEF4017, /* BV, JV, FV */ W25Q128_ID = 0xEF4018 };
- 第4步:新增相應型號到bsp_spi_flash.c檔案的函式sf_ReadInfo裡面。
/* ********************************************************************************************************* * 函 數 名: sf_ReadInfo * 功能說明: 讀取器件ID,並填充器件引數 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void sf_ReadInfo(void) { /* 自動識別序列Flash型號 */ { g_tSF.ChipID = sf_ReadID(); /* 晶片ID */ switch (g_tSF.ChipID) { case SST25VF016B_ID: strcpy(g_tSF.ChipName, "SST25VF016B"); g_tSF.TotalSize = 2 * 1024 * 1024; /* 總容量 = 2M */ g_tSF.SectorSize = 4 * 1024; /* 扇區大小 = 4K */ break; case MX25L1606E_ID: strcpy(g_tSF.ChipName, "MX25L1606E"); g_tSF.TotalSize = 2 * 1024 * 1024; /* 總容量 = 2M */ g_tSF.SectorSize = 4 * 1024; /* 扇區大小 = 4K */ break; case W25Q64BV_ID: strcpy(g_tSF.ChipName, "W25Q64"); g_tSF.TotalSize = 8 * 1024 * 1024; /* 總容量 = 8M */ g_tSF.SectorSize = 4 * 1024; /* 扇區大小 = 4K */ break; case W25Q128_ID: strcpy(g_tSF.ChipName, "W25Q128"); g_tSF.TotalSize = 16 * 1024 * 1024; /* 總容量 = 8M */ g_tSF.SectorSize = 4 * 1024; /* 扇區大小 = 4K */ break; default: strcpy(g_tSF.ChipName, "Unknow Flash"); g_tSF.TotalSize = 2 * 1024 * 1024; g_tSF.SectorSize = 4 * 1024; break; } } }
- 第5步:根據晶片支援的時鐘速度,時鐘相位和時鐘極性配置函式sf_SetCS。
/* ********************************************************************************************************* * 函 數 名: sf_SetCS * 功能說明: 序列FALSH片選控制函式 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void sf_SetCS(uint8_t _Level) { if (_Level == 0) { bsp_SpiBusEnter(); bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_2, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); SF_CS_0(); } else { SF_CS_1(); bsp_SpiBusExit(); } }
- 第6步:根據使用的SPI Flash片選引腳修改bsp_spi_bus.c檔案開頭的巨集定義。
/* 序列Flash的片選GPIO埠, PD13 */ #define SF_CS_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE() #define SF_CS_GPIO GPIOD #define SF_CS_PIN GPIO_PIN_13 #define SF_CS_0() SF_CS_GPIO->BSRR = ((uint32_t)SF_CS_PIN << 16U) #define SF_CS_1() SF_CS_GPIO->BSRR = SF_CS_PIN
- 第7步:初始化SPI。
/* 針對不同的應用程式,新增需要的底層驅動模組初始化函式 */ bsp_InitSPIBus(); /* 配置SPI匯流排 */ bsp_InitSFlash(); /* 初始化SPI 序列Flash */
- 第8步:SPI Flash驅動主要用到HAL庫的SPI驅動檔案,簡單省事些可以新增所有HAL庫C原始檔進來。
- 第9步:應用方法看本章節配套例子即可。
32.8 實驗例程設計框架
通過程式設計框架,讓大家先對配套例程有一個全面的認識,然後再理解細節,本次實驗例程的設計框架如下:
第1階段,上電啟動階段:
- 這部分在第14章進行了詳細說明。
第2階段,進入main函式:
- 第1部分,硬體初始化,主要是MPU,Cache,HAL庫,系統時鐘,滴答定時器和LED。
- 第2部分,應用程式設計部分,實現SPI Flash的中斷,查詢和DMA方式操作。
32.9 實驗例程說明(MDK)
配套例子:
V5-011_序列SPI Flash W25QXX讀寫例程(查詢方式)
V5-012_序列SPI Flash W25QXX讀寫例程(中斷方式)
V5-013_序列SPI Flash W25QXX讀寫例程(DMA方式)
實驗目的:
- 學習SPI Flash的讀寫實現,支援查詢,中斷和DMA方式。
實驗操作:
- 支援以下7個功能,使用者通過電腦端串列埠軟體傳送命令給開發板即可
- printf("請選擇操作命令:\r\n");
- printf("【1 - 讀序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【2 - 寫序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【3 - 擦除整個序列Flash】\r\n");
- printf("【4 - 寫整個序列Flash, 全0x55】\r\n");
- printf("【5 - 讀整個序列Flash, 測試讀速度】\r\n");
- printf("【Z - 讀取前1K,地址自動減少】\r\n");
- printf("【X - 讀取後1K,地址自動增加】\r\n");
- printf("其他任意鍵 - 顯示命令提示\r\n");
上電後串列埠列印的資訊:
波特率 115200,資料位 8,奇偶校驗位無,停止位 1。
程式設計:
系統棧大小分配:
硬體外設初始化
硬體外設的初始化是在 bsp.c 檔案實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬體裝置。該函式配置CPU暫存器和外設的暫存器並初始化一些全域性變數。只需要呼叫一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32F407 HAL 庫初始化,此時系統用的還是F407自帶的16MHz,HSI時鐘: - 呼叫函式HAL_InitTick,初始化滴答時鐘中斷1ms。 - 設定NVIV優先順序分組為4。 */ HAL_Init(); /* 配置系統時鐘到168MHz - 切換使用HSE。 - 此函式會更新全域性變數SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於程式碼執行時間測量,MDK5.25及其以上版本才支援,IAR不支援。 - 預設不開啟,如果要使能此選項,務必看V5開發板使用者手冊第8章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串列埠 */ bsp_InitExtIO(); /* 初始化擴充套件IO */ bsp_InitLed(); /* 初始化LED */ BEEP_InitHard(); /* 初始化蜂鳴器 */ /* 針對不同的應用程式,新增需要的底層驅動模組初始化函式 */ bsp_InitSPIBus(); /* 配置SPI匯流排 */ bsp_InitSFlash(); /* 初始化SPI 序列Flash */ }
主功能:
主程式實現如下操作:
- 啟動一個自動重灌軟體定時器,每100ms翻轉一次LED2。
- 支援以下7個功能,使用者通過電腦端串列埠軟體傳送命令給開發板即可
- 請選擇操作命令:
- 1 - 讀序列Flash
- 2 - 寫序列Flash
- 3 - 擦除整個序列Flash
- 4 - 寫整個序列Flash
- 5 - 讀整個序列Flash
- Z - 讀取前1K
- X - 讀取後1K
/* ********************************************************************************************************* * 函 數 名: DemoSpiFlash * 功能說明: 序列EEPROM讀寫例程 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void DemoSpiFlash(void) { uint8_t cmd; uint32_t uiReadPageNo = 0; /* 檢測序列Flash OK */ printf("檢測到序列Flash, ID = %08X, 型號: %s \r\n", g_tSF.ChipID , g_tSF.ChipName); printf(" 容量 : %dM位元組, 扇區大小 : %d位元組\r\n", g_tSF.TotalSize/(1024*1024), g_tSF.SectorSize); sfDispMenu(); /* 列印命令提示 */ bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重灌的定時器 */ while(1) { bsp_Idle(); /* 這個函式在bsp.c檔案。使用者可以修改這個函式實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 進來一次 */ bsp_LedToggle(2); } if (comGetChar(COM1, &cmd)) /* 從串列埠讀入一個字元(非阻塞方式) */ { switch (cmd) { case '1': printf("\r\n【1 - 讀序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE); sfReadTest(); /* 讀序列Flash資料,並打印出來資料內容 */ break; case '2': printf("\r\n【2 - 寫序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE); sfWriteTest(); /* 寫序列Flash資料,並列印寫入速度 */ break; case '3': printf("\r\n【3 - 擦除整個序列Flash】\r\n"); printf("整個Flash擦除完畢大概需要20秒左右,請耐心等待"); sfErase(); /* 擦除序列Flash資料,實際上就是寫入全0xFF */ break; case '4': printf("\r\n【4 - 寫整個序列Flash, 全0x55】\r\n"); printf("整個Flash寫入完畢大概需要20秒左右,請耐心等待"); sfWriteAll(0x55);/* 擦除序列Flash資料,實際上就是寫入全0xFF */ break; case '5': printf("\r\n【5 - 讀整個序列Flash, %dM位元組】\r\n", g_tSF.TotalSize/(1024*1024)); sfTestReadSpeed(); /* 讀整個序列Flash資料,測試速度 */ break; case 'z': case 'Z': /* 讀取前1K */ if (uiReadPageNo > 0) { uiReadPageNo--; } else { printf("已經是最前\r\n"); } sfViewData(uiReadPageNo * 1024); break; case 'x': case 'X': /* 讀取後1K */ if (uiReadPageNo < g_tSF.TotalSize / 1024 - 1) { uiReadPageNo++; } else { printf("已經是最後\r\n"); } sfViewData(uiReadPageNo * 1024); break; default: sfDispMenu(); /* 無效命令,重新列印命令提示 */ break; } } } }
32.10 實驗例程說明(IAR)
配套例子:
V5-011_序列SPI Flash W25QXX讀寫例程(查詢方式)
V5-012_序列SPI Flash W25QXX讀寫例程(中斷方式)
V5-013_序列SPI Flash W25QXX讀寫例程(DMA方式)
實驗目的:
- 學習SPI Flash的讀寫實現,支援查詢,中斷和DMA方式。
實驗操作:
- 支援以下7個功能,使用者通過電腦端串列埠軟體傳送命令給開發板即可
- printf("請選擇操作命令:\r\n");
- printf("【1 - 讀序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【2 - 寫序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【3 - 擦除整個序列Flash】\r\n");
- printf("【4 - 寫整個序列Flash, 全0x55】\r\n");
- printf("【5 - 讀整個序列Flash, 測試讀速度】\r\n");
- printf("【Z - 讀取前1K,地址自動減少】\r\n");
- printf("【X - 讀取後1K,地址自動增加】\r\n");
- printf("其他任意鍵 - 顯示命令提示\r\n");
上電後串列埠列印的資訊:
波特率 115200,資料位 8,奇偶校驗位無,停止位 1。
程式設計:
系統棧大小分配:
硬體外設初始化
硬體外設的初始化是在 bsp.c 檔案實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬體裝置。該函式配置CPU暫存器和外設的暫存器並初始化一些全域性變數。只需要呼叫一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32F407 HAL 庫初始化,此時系統用的還是F407自帶的16MHz,HSI時鐘: - 呼叫函式HAL_InitTick,初始化滴答時鐘中斷1ms。 - 設定NVIV優先順序分組為4。 */ HAL_Init(); /* 配置系統時鐘到168MHz - 切換使用HSE。 - 此函式會更新全域性變數SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於程式碼執行時間測量,MDK5.25及其以上版本才支援,IAR不支援。 - 預設不開啟,如果要使能此選項,務必看V5開發板使用者手冊第8章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串列埠 */ bsp_InitExtIO(); /* 初始化擴充套件IO */ bsp_InitLed(); /* 初始化LED */ BEEP_InitHard(); /* 初始化蜂鳴器 */ /* 針對不同的應用程式,新增需要的底層驅動模組初始化函式 */ bsp_InitSPIBus(); /* 配置SPI匯流排 */ bsp_InitSFlash(); /* 初始化SPI 序列Flash */ }
主功能:
主程式實現如下操作:
- 啟動一個自動重灌軟體定時器,每100ms翻轉一次LED2。
- 支援以下7個功能,使用者通過電腦端串列埠軟體傳送命令給開發板即可
- 請選擇操作命令:
- 1 - 讀序列Flash
- 2 - 寫序列Flash
- 3 - 擦除整個序列Flash
- 4 - 寫整個序列Flash
- 5 - 讀整個序列Flash
- Z - 讀取前1K
- X - 讀取後1K
/* ********************************************************************************************************* * 函 數 名: DemoSpiFlash * 功能說明: 序列EEPROM讀寫例程 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void DemoSpiFlash(void) { uint8_t cmd; uint32_t uiReadPageNo = 0; /* 檢測序列Flash OK */ printf("檢測到序列Flash, ID = %08X, 型號: %s \r\n", g_tSF.ChipID , g_tSF.ChipName); printf(" 容量 : %dM位元組, 扇區大小 : %d位元組\r\n", g_tSF.TotalSize/(1024*1024), g_tSF.SectorSize); sfDispMenu(); /* 列印命令提示 */ bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重灌的定時器 */ while(1) { bsp_Idle(); /* 這個函式在bsp.c檔案。使用者可以修改這個函式實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 進來一次 */ bsp_LedToggle(2); } if (comGetChar(COM1, &cmd)) /* 從串列埠讀入一個字元(非阻塞方式) */ { switch (cmd) { case '1': printf("\r\n【1 - 讀序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE); sfReadTest(); /* 讀序列Flash資料,並打印出來資料內容 */ break; case '2': printf("\r\n【2 - 寫序列Flash, 地址:0x%X,長度:%d位元組】\r\n", TEST_ADDR, TEST_SIZE); sfWriteTest(); /* 寫序列Flash資料,並列印寫入速度 */ break; case '3': printf("\r\n【3 - 擦除整個序列Flash】\r\n"); printf("整個Flash擦除完畢大概需要20秒左右,請耐心等待"); sfErase(); /* 擦除序列Flash資料,實際上就是寫入全0xFF */ break; case '4': printf("\r\n【4 - 寫整個序列Flash, 全0x55】\r\n"); printf("整個Flash寫入完畢大概需要20秒左右,請耐心等待"); sfWriteAll(0x55);/* 擦除序列Flash資料,實際上就是寫入全0xFF */ break; case '5': printf("\r\n【5 - 讀整個序列Flash, %dM位元組】\r\n", g_tSF.TotalSize/(1024*1024)); sfTestReadSpeed(); /* 讀整個序列Flash資料,測試速度 */ break; case 'z': case 'Z': /* 讀取前1K */ if (uiReadPageNo > 0) { uiReadPageNo--; } else { printf("已經是最前\r\n"); } sfViewData(uiReadPageNo * 1024); break; case 'x': case 'X': /* 讀取後1K */ if (uiReadPageNo < g_tSF.TotalSize / 1024 - 1) { uiReadPageNo++; } else { printf("已經是最後\r\n"); } sfViewData(uiReadPageNo * 1024); break; default: sfDispMenu(); /* 無效命令,重新列印命令提示 */ break; } } } }
32.11 總結
本章節就為大家講解這麼多,實際應用中根據需要選擇DMA,中斷和查詢方式。