目錄
前言
技術實現?
接線圖?
代碼實現?
技術要點?
引腳操作
SPI初始化
SPI起始信號
SPI終止信號
SPI字節交換
宏替換命令?
W25Q64寫使能?
忙等待
讀取設備ID號和制造商ID
頁寫入
數據讀取
實驗結果
問題記錄?
前言
SPI(Serial Peripheral Interface,串行外設接口)是一種同步串行通信接口規范,主要用于短距離通信,廣泛應用于嵌入式系統中。它使一個主設備能夠與一個或多個從設備進行通信。SPI使用四條主要信號線:MOSI(主機輸出/從機輸入)、MISO(主機輸入/從機輸出)、SCK(串行時鐘)和SS/CS(從選/片選)來實現數據的雙向傳輸。這種接口方式支持全雙工通信,具有傳輸速率高、延遲低的優點,但相比其他一些接口協議,使用的信號線較多。SPI常用于連接傳感器、存儲器、ADC(模數轉換器)等外圍設備。
SPI硬件電路
- 所有SPI設備的SCK,MISO,MOSI分別連在一起。
- 主機另外引出多條CS控制線分別連接到各從機的CS引腳。
- 輸出引腳配置為推挽輸出,輸入引腳配置為浮空輸入或上拉輸入。
W25Q64是華邦公司(Winbond)推出的一款基于SPI通信的大容量閃存產品,其存儲容量為64Mb(即8MB)。該芯片支持2.7~3.6V的工作電壓范圍,并且具備至少10萬次的擦寫周期和長達20年的數據保存時間。W25Q64以其靈活性和出色的性能著稱,非常適合用于存儲聲音、文本和數據等應用。它被組織為32768個可編程頁面,每個頁面包含256字節的數據。此外,W25Q64還支持多種擦除命令,包括4KB扇區擦除、32KB塊擦除、64KB塊擦除以及全片擦除,并且兼容標準SPI模式0和模式3,最高支持133MHz的時鐘頻率。通過SPI接口,W25Q64可以輕松地與各種微控制器進行集成,實現高效的數據讀寫操作。
W25Q64由8MB的存儲容量,存儲區域被劃分成了128個塊,每個塊又被劃分成了16個扇區,每個扇區又被劃分成了?16個頁。
W25Q64寫入操作時:
- ?在寫入操作之前,必須先進行寫使能。
- 每個數據為只能有1改寫成0,不能由0改寫成1。
- 寫入數據之前必須先擦除,擦除后,所有的數據位都變為1。
- 擦除必須以最小單元擦除(這里最小擦除單元時4kb扇區)。
- 連續寫入多個字節時最多寫入一頁的數據(256Byte),超過頁尾的數據會回到頁首覆蓋寫入。
- 寫入操作完成后,芯片進入忙狀態,不響應新的寫操作。
W25Q64讀操作時:
- 直接調取讀操作時序,無需寫使能,無需其他操作,沒有頁限制,讀取操作完成后芯片不會進入忙狀態,但是芯片忙狀態時不能進行讀操作。
技術實現?
原理圖
?
接線圖?
代碼實現?
main.c
/**********************************************************
1.實驗名稱:軟件SPI讀寫W25Q64
2.實驗環境:STM32F103C8T6最小系統板
3.實驗內容:使用軟件模擬SPI讀取W25Q64
4.作者;abai
5.實驗時間:2025-5-6
**********************************************************/
#include "stm32f10x.h" // Device header
#include "Delay.h" //延時函數
#include "OLED.h"
#include "W25Q64.h"uint8_t MF;
uint16_t ID;
uint8_t SendArry[] = {0x05,0x06,0x07,0x08};
uint8_t ReceiveArry[4];int main(void)
{/*OLED初始化*/OLED_Init();W25Q64_Init();OLED_ShowString(1,1,"MF:");OLED_ShowString(2,1,"ID:");OLED_ShowString(3,1,"W:");OLED_ShowString(4,1,"R:");W25Q64_SectorErase(0x000000); //寫入數據之前必須頁擦除W25Q64_PageProgram(0x000000,SendArry,4);W25Q64_ReadData(0x000000,ReceiveArry,4);W25Q64_ReadID(&MF,&ID);OLED_ShowHexNum(3,3,SendArry[0],2);OLED_ShowHexNum(3,5,SendArry[1],2);OLED_ShowHexNum(3,7,SendArry[2],2);OLED_ShowHexNum(3,9,SendArry[3],2);OLED_ShowHexNum(4,3,ReceiveArry[0],2);OLED_ShowHexNum(4,5,ReceiveArry[1],2);OLED_ShowHexNum(4,7,ReceiveArry[2],2);OLED_ShowHexNum(4,9,ReceiveArry[3],2);OLED_ShowHexNum(1,4,MF,2);OLED_ShowHexNum(2,4,ID,4);while(1){}
}
MySPI.h?
#ifndef MYSPI_H
#define MYSPI_H#include "stm32f10x.h" // Device headervoid MySPI_Init(void);
void MySPI_CS_W(uint8_t BitValue);
void MySPI_SCK_W(uint8_t BitValue);
void MySPI_MOSI_W(uint8_t BitValue);
uint8_t MySPI_MISO_R(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteValue);#endif
?MySPI.c
#include "MySPI.h"/***@brief SPI初始化*@param None*@retval None**/
void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIOGPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //推挽輸出GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //上拉輸入GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);//引腳初始化MySPI_CS_W(1); //片選信號默認為高電平MySPI_SCK_W(0); //SPI模式0,時鐘信號默認為低電平}/***@brief SPI CS控制*@param BitValue 要寫入CS的位數據*@retval None**/
void MySPI_CS_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}/***@brief SPI SCK控制*@param BitValue 要寫入SCK的位數據*@retval None**/
void MySPI_SCK_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}/***@brief SPI MOSI寫操作*@param BitValue 要寫入MOSI的位數據*@retval None**/
void MySPI_MOSI_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}/***@brief SPI MISO讀*@param None*@retval MISO數據線接受到的數據**/
uint8_t MySPI_MISO_R(void)
{return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}/***@brief SPI起始信號*@param None*@retval None**/
void MySPI_Start(void)
{MySPI_CS_W(0); //將片選信號拉低發送起始信號
}/***@brief SPI終止信號*@param None*@retval None**/
void MySPI_Stop(void)
{MySPI_CS_W(1); //將片選信號拉高發送終止信號
}/***@brief SPI交換字節*@param None*@retval 交換的字節**/
uint8_t MySPI_SwapByte(uint8_t ByteValue)
{//掩碼操作uint8_t ByteReceive = 0x00;uint8_t i;for(i=0;i<8;i++){MySPI_MOSI_W((ByteValue<<i) & 0x80);MySPI_SCK_W(1);if(MySPI_MISO_R() == 1)ByteReceive |= (0x80>>i);MySPI_SCK_W(0);}return ByteReceive;//移位操作
// uint8_t i;
// for(i=0;i<8;i++)
// {
// MySPI_MOSI_W(ByteValue& 0x80);
// ByteValue <<= 1;
// MySPI_SCK_W(1);
// if(MySPI_MISO_R() == 1)
// ByteValue |= 0x01;
// MySPI_SCK_W(0);
// }
//
// return ByteValue;
}
W25Q64_Ins.h?
#ifndef W25Q64_INS_H
#define W25Q64_INS_H#define W25Q64_WRITEENABLE 0x06
#define W25Q64_READSTATUSREGISTER1 0x05
#define W25Q64_PAGEPROGRAM 0x02
#define W25Q64_SECTORERASE4KB 0x20
#define W25Q64_JEDECID 0X9F
#define W25Q64_READDATA 0x03
#define W25Q64_DUMMY_BYTE 0xFF#endif
W25Q64.h?
#ifndef W25Q64_H
#define W25Q64_H#include "stm32f10x.h" // Device header
#include "MySPI.h"void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t* MF, uint16_t*ID);
void W25Q64_WriteEnable(void);
void W25Q64_WaitBusy(void);
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArry, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address,uint8_t* DataArry, uint32_t Count);#endif
W25Q64.c?
#include "W25Q64.h"
#include "W25Q64_Ins.h"/***@brief W25Q64初始化*@param None*@retval None**/
void W25Q64_Init(void)
{MySPI_Init();
}/***@brief W25Q64ID讀取*@param None*@retval None**/
void W25Q64_ReadID(uint8_t* MF, uint16_t*ID)
{MySPI_Start();//發送讀取ID指令MySPI_SwapByte(W25Q64_JEDECID);//發送空指令交換數據*MF = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //隨便發送一個數據用于與從機交換數據,一般使用0xFF或0x00*ID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);*ID <<= 8;*ID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);MySPI_Stop();
}/***@brief W25Q64ID寫使能*@param None*@retval None**/
void W25Q64_WriteEnable(void)
{MySPI_Start();//發送寫使能指令MySPI_SwapByte(W25Q64_WRITEENABLE);MySPI_Stop();
}/***@brief W25Q64ID忙等待*@param None*@retval None**/
void W25Q64_WaitBusy(void)
{MySPI_Start();//發送讀狀態寄存器指令MySPI_SwapByte(W25Q64_READSTATUSREGISTER1);//等待BUSY位清零,則從設備退出忙狀態while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0X01) == 0X01);MySPI_Stop();
}/***@brief W25Q64ID頁寫入*@param None*@retval None**/
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArry, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //寫使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGEPROGRAM);//發送24位地址MySPI_SwapByte(Address>>16);MySPI_SwapByte(Address>>8);MySPI_SwapByte(Address);//發送指定數量的數據for(i=0;i<Count;i++){MySPI_SwapByte(DataArry[i]);}MySPI_Stop();W25Q64_WaitBusy(); //事后等待忙狀態
}/***@brief W25Q64ID指定扇區擦除*@param None*@retval None**/
void W25Q64_SectorErase(uint32_t Address)
{ W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_SECTORERASE4KB);//發送24位地址MySPI_SwapByte(Address>>16);MySPI_SwapByte(Address>>8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();
}/***@brief W25Q64ID讀取數據*@param None*@retval None*@note 數據接收的數量也要指定,不然無法停止數據讀取操作**/
void W25Q64_ReadData(uint32_t Address,uint8_t* DataArry, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READDATA);//發送24位地址MySPI_SwapByte(Address>>16);MySPI_SwapByte(Address>>8);MySPI_SwapByte(Address);for(i=0;i<Count;i++){DataArry[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
?OLED部分代碼參照文章《STM32基礎教程——OLED顯示》
技術要點?
引腳操作
/***@brief SPI CS控制*@param BitValue 要寫入CS的位數據*@retval None**/
void MySPI_CS_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}/***@brief SPI SCK控制*@param BitValue 要寫入SCK的位數據*@retval None**/
void MySPI_SCK_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}/***@brief SPI MOSI寫操作*@param BitValue 要寫入MOSI的位數據*@retval None**/
void MySPI_MOSI_W(uint8_t BitValue)
{GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}/***@brief SPI MISO讀*@param None*@retval MISO數據線接受到的數據**/
uint8_t MySPI_MISO_R(void)
{return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
對單個引腳的操作進行封裝,用于模擬SPI引腳電平翻轉或引腳電平讀取。?
SPI初始化
/***@brief SPI初始化*@param None*@retval None**/
void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIOGPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //推挽輸出GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //上拉輸入GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);//引腳初始化MySPI_CS_W(1); //片選信號默認為高電平MySPI_SCK_W(0); //SPI模式0,時鐘信號默認為低電平}
實驗使用軟件模擬SPI,PA4引腳模擬SPI的CS引腳,PA5模擬SPI的SCK引腳,PA6模擬MISO引腳,PA7模擬MOSI引腳。根據輸出引腳配置為推挽輸出,輸入引腳配置為上拉輸入的要求初始化GPIO。初始化完成后,對CS引腳和SCK引腳初始化。片選信號CS默認為高電平,表示未選中從設備。模擬SPI模式0,SCK初始默認電平為低電平。
SPI起始信號
SPI的起始時序是CS(SS)從高電平切換為低電平 。
/***@brief SPI起始信號*@param None*@retval None**/
void MySPI_Start(void)
{MySPI_CS_W(0); //將片選信號拉低發送起始信號
}
SPI終止信號
?
?SPI的終止時序是CS(SS)從低電平切換為高電平 。
/***@brief SPI終止信號*@param None*@retval None**/
void MySPI_Stop(void)
{MySPI_CS_W(1); //將片選信號拉高發送終止信號
}
SPI字節交換
SPI為同步全雙工通信,每一次通信都會進行數據交換。
/***@brief SPI交換字節*@param None*@retval 交換的字節**/
uint8_t MySPI_SwapByte(uint8_t ByteValue)
{//掩碼操作uint8_t ByteReceive = 0x00;uint8_t i;for(i=0;i<8;i++){MySPI_MOSI_W((ByteValue<<i) & 0x80);MySPI_SCK_W(1);if(MySPI_MISO_R() == 1)ByteReceive |= (0x80>>i);MySPI_SCK_W(0);}return ByteReceive;
}
SPI模式0下,SCK第一個邊沿移入數據,所以當第一個邊沿到來之前數據已經移出。?
MySPI_MOSI_W((ByteValue<<i) & 0x80);
?所以通過MOSI發送一個數據字節。然后將SCK拉高,SCK的第一個邊沿將數據移入。
if(MySPI_MISO_R() == 1)ByteReceive |= (0x80>>i);
?ByteValue默認為0x00,只有當移入的數據為1時將對應位置1,為0時不執行操作。
MySPI_SCK_W(0);
?然后將SCK拉低,發生數據移出操作。
使用for循環完成一個字節的數據交換,最后將數據返回。
宏替換命令?
#ifndef W25Q64_INS_H
#define W25Q64_INS_H#define W25Q64_WRITEENABLE 0x06
#define W25Q64_READSTATUSREGISTER1 0x05
#define W25Q64_PAGEPROGRAM 0x02
#define W25Q64_SECTORERASE4KB 0x20
#define W25Q64_JEDECID 0X9F
#define W25Q64_READDATA 0x03
#define W25Q64_DUMMY_BYTE 0xFF#endif
將對W25Q64操作的命令使用宏代替, 提高代碼的便捷性和可讀性。
W25Q64寫使能?
/***@brief W25Q64ID寫使能*@param None*@retval None**/
void W25Q64_WriteEnable(void)
{MySPI_Start();//發送寫使能指令MySPI_SwapByte(W25Q64_WRITEENABLE);MySPI_Stop();
}
將寫使能操作封裝,方便使用。主機與從機交換數據,發送寫使能命令,對從機進行寫使能操作。
忙等待
/***@brief W25Q64ID忙等待*@param None*@retval None**/
void W25Q64_WaitBusy(void)
{MySPI_Start();//發送讀狀態寄存器指令MySPI_SwapByte(W25Q64_READSTATUSREGISTER1);//等待BUSY位清零,則從設備退出忙狀態while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0X01) == 0X01);MySPI_Stop();
}
?發送讀取狀態寄存器1的命令,判斷BUSY位的狀態,使用while循環等待芯片退出忙狀態將BUSY位清零。
讀取設備ID號和制造商ID
/***@brief W25Q64ID讀取*@param None*@retval None**/
void W25Q64_ReadID(uint8_t* MF, uint16_t*ID)
{MySPI_Start();//發送讀取ID指令MySPI_SwapByte(W25Q64_JEDECID);//發送空指令交換數據*MF = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //隨便發送一個數據用于與從機交換數據,一般使用0xFF或0x00*ID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);*ID <<= 8;*ID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);MySPI_Stop();
}
?主機發送讀取JEDEC ID的命令,從機向主機發送數據,數據格式為第一個字節為制造商ID,第二個和第三個字節為設備ID。這里使用指針進行數據操作。
頁寫入
/***@brief W25Q64ID頁寫入*@param None*@retval None**/
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArry, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //寫使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGEPROGRAM);//發送24位地址MySPI_SwapByte(Address>>16);MySPI_SwapByte(Address>>8);MySPI_SwapByte(Address);//發送指定數量的數據for(i=0;i<Count;i++){MySPI_SwapByte(DataArry[i]);}MySPI_Stop();W25Q64_WaitBusy(); //事后等待忙狀態
}
頁寫入操作,先進行寫使能,然后發送起始信號,發送頁寫入命令。然后發送24位地址,高16位為頁地址,低八位為字節地址。使用for循環寫入指定數量的數據,數據的數量不應超過256字節。最后發送停止信號,然后進行芯片忙等待操作(忙等待如果加在函數開始,那讀取操作也要加忙等待函數調用,這樣會使操作更復雜。)。所有寫操作函數與本函數類似,不再贅述。
數據讀取
/***@brief W25Q64ID讀取數據*@param None*@retval None*@note 數據接收的數量也要指定,不然無法停止數據讀取操作**/
void W25Q64_ReadData(uint32_t Address,uint8_t* DataArry, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READDATA);//發送24位地址MySPI_SwapByte(Address>>16);MySPI_SwapByte(Address>>8);MySPI_SwapByte(Address);for(i=0;i<Count;i++){DataArry[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
數據讀取操作無需寫使能,無需進行其他操作。發送讀數據命令,然后發送24位地址,同樣高16位地址為頁地址,低八位為字節地址。讀取操作不受頁限制。
實驗結果
問題記錄?
暫無