最新教程下載:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第32章 STM32F429的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的硬件設計如下:
關於這個原理圖,要了解到以下幾個知識:
- 當前V6開發板實際外接的芯片是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)
配套例子:
V6-011_串行SPI Flash W25QXX讀寫例程(查詢方式)
V6-012_串行SPI Flash W25QXX讀寫例程(中斷方式)
V6-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寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無0 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32H429 HAL 庫初始化,此時系統用的還是F429自帶的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)
配套例子:
V6-011_串行SPI Flash W25QXX讀寫例程(查詢方式)
V6-012_串行SPI Flash W25QXX讀寫例程(中斷方式)
V6-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寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無0 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32H429 HAL 庫初始化,此時系統用的還是F429自帶的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(); /* 初始化蜂鳴器 */ }
主功能:
主程序實現如下操作:
- 啟動一個自動重裝軟件定時器,每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,中斷和查詢方式。