1. 实验目的
读取W25Q128Flash的设备ID、厂商ID,以及读写数据操作。
2. 实验流程
初始化SPI;
编写SPI发送并接收一个字节函数;
编写读取设备ID函数;
编写Flash使能函数;
编写擦除Flash指定扇区函数;
编写等待Flash内部时序操作完成函数;
编写读取Flash的内容函数;
编写向Flash中写入内容的函数;
编写main函数。
2.1 准备工作
如图可以看到,PB3、PB4、PB5分别和SPI1 SCK、SPI1 MISO、SPI1 MOSI相连,F_CS片选引脚与PB14相连。要把PB3、PB4、PB5引脚复用为SPI1的三个引脚。
2.2 初始化SPI
![在这里插入图片描述](https://img-blog.csdnimg.cn/e2d9cb3d369842d381496a0623ab60e7.png
可以看到SPI1挂载到APB2总线上面,GPIOG、GPIOB挂载到AHB1总线上。
#define W25QXX_CS PBout(14) //W25QXX的片选信号,W25QXX_CS = 0; PB14引脚输出低电平。W25QXX_CS = 1; PB14引脚输出高电平
void SPI1_Init(void){
GPIO_InitTypeDef GPIO_InitStructure; //GPIO结构体
SPI_InitTypeDef SPI_InitStructure; //SPI结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //使能SPI时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE); //使能GPIOB时钟,PB14引脚是片选的引脚
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE); //使能GPIOG时钟
//GPIOFB3,4,5初始化设置: 复用功能输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100MHZ高速
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化GPIO的引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; //PB14,CS引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //输出
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化GPIO的引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PG7(参考正点原子实验手册)
GPIO_Init(GPIOG, &GPIO_InitStructure); //初始化
GPIO_SetBits(GPIOG,GPIO_Pin_7); //PG7输出1,防止NRF干扰SPI FLASH的通信
W25QXX_CS = 1; //SPI FLASH不选中,开始状态,不发数据就是1
//配置引脚复用映射
GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3复用为SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB3复用为SPI1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB3复用为SPI1
//SPI口初始化
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小: 8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //数据捕获于第二个时钟沿
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由软件管理
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从MSB(高数据在前)位开始,配置数据先行,发送数据就需要先发送高位,再发送低位
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式,使用CRC校验,校验位,一般不使用,这里也没有使用
SPI_Init(SPI1, &SPI_InitStructure); //根据指定的参数初始化外设SPIx寄存器,写入配置到寄存器
SPI_Cmd(SPI1, ENABLE); //使能 SPI1
}
2.3 编写SPI发送并接收一个字节函数
//下面调用了这个函数
static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorCode){
printf("SPI 等待超时!errorCode =%d",errorCode);
return 0;
}
//发送并接收一个字节
uint8_t Spi_Flash_Send_Byte(uint8_t data){
uint16_t SPITimeout = 1000; //超时时间
//检测TXE标志()发送缓冲区是空的,才能发送数据,如果为空的话就会置1的
//SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE); //@arg SPI_I2S_FLAG_TXE: Transmit buffer empty flag.
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET){ //RESET = 0; //检测并等待至发送缓冲区为空
if((SPITimeout--) == 0){
return SPI_TIMEOUT_UserCallback(0);
}
};
//程序执行到此处,发送缓冲区置为空
SPI_I2S_SendData(SPI1,data); //发送数据
//检查并等待至RX缓冲区为非空 ,非空的话就代表有数据
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET){ //RESET = 0; //检测并等待至接收缓冲区为非空,置1的话代表非空
if((SPITimeout--) == 0){
return SPI_TIMEOUT_UserCallback(0);
}
}
return SPI_I2S_ReceiveData(SPI1); //程序执行到此处,说明数据发送完毕,并接收到了一个字节数据
}
//接收一个字节数据
uint8_t SPI_FLASH_Read_Byte(void){
return Spi_Flash_Send_Byte(0X00); //FLASH会忽略这个数据
}
2.4 编写读取设备ID函数
由上图可知制造商ID是0xEF,设备ID是0x17。
由上面的时序图可以看出,先要发送一个0X90,再发送一个任意字节,再发送一个任意字节,再发送一个0X00,然后接收字节是制造商ID,再接收字节是设备ID。
uint32_t SPI_Read_ID(void){
uint16_t flash_id;
//片选使能
W25QXX_CS = 0; //CS引脚拉低
Spi_Flash_Send_Byte(0X90); //发送命令
SPI_FLASH_Read_Byte();
SPI_FLASH_Read_Byte();
SPI_FLASH_Read_Byte(); //这里就是发送的0X00
flash_id |= SPI_FLASH_Read_Byte(); //随便发送一个字节,收到厂商id:0XEF
flash_id <<= 8; //左移8位
flash_id |= SPI_FLASH_Read_Byte(); //随便发送一个字节,收到厂商id:0XEF
W25QXX_CS = 1; //CS引脚拉高
return flash_id;
}
2.5 编写Flash使能函数
发送0X06,不用接收。
//FLASH写入使能 ,在擦除,读写函数中都要调用这个使能
void SPI_Writer_Enable(void){
W25QXX_CS = 0; //片选使能,CS引脚拉低
Spi_Flash_Send_Byte(0X06); //flash写入使能,查手册得到的
W25QXX_CS = 1; //CS引脚拉高
}
2.6 编写擦除Flash指定扇区函数
发送0X20,发送扇区地址,分三次发送扇区地址。
//擦除FLASH指定扇区
void SPI_ERASER_SECTOR(uint32_t addr){
SPI_Writer_Enable(); //FLASH写入使能
//uint8_t flash_8 = (addr>>16)&0XFF; //发的23,所以移动的是16
W25QXX_CS = 0; //CS引脚拉低 ,片选使能
Spi_Flash_Send_Byte(0X20); //发送命令
Spi_Flash_Send_Byte((uint8_t)((addr>>16)&0XFF)); //分三次发送扇区地址
Spi_Flash_Send_Byte((uint8_t)((addr>>8)&0XFF));
Spi_Flash_Send_Byte((uint8_t)(addr&0XFF));
W25QXX_CS = 1; //CS引脚拉高
}
2.7 编写等待Flash内部时序操作完成函数
发送0X05,等待返回的值是否为0,这是为1就是忙碌,0就是不忙碌。
//等待FLASH内部时序操作完成
void SPI_WaitForWriterEnd(void){
uint8_t status_reg = 0; //状态标志位
W25QXX_CS = 0; //CS引脚拉低 ,片选使能
Spi_Flash_Send_Byte(0X05); //发送命令,读状态寄存器
status_reg = Spi_Flash_Send_Byte(0X00); //FLASH会忽略这个数据
while(status_reg&0X01 == 1){ //如果为1就是忙碌,0就是不忙碌
status_reg = Spi_Flash_Send_Byte(0X00); //一直读取状态
}
W25QXX_CS = 1; //CS引脚拉高
}
2.8 编写读取Flash的内容函数
发送0X03,发送扇区地址,分三次发送扇区地址,发送任意字节,返回数据。
//读取FLASH的内容
void SPI_Read_Data(uint32_t addr, uint8_t *Read_Buff,uint32_t numByteToRead){
SPI_Writer_Enable(); //FLASH写入使能
W25QXX_CS = 0; //CS引脚拉低,片选使能
Spi_Flash_Send_Byte(0X03); //发送命令,读状态寄存器
Spi_Flash_Send_Byte((addr>>16)&0XFF); //分三次发送扇区地址
Spi_Flash_Send_Byte((addr>>8)&0XFF);
Spi_Flash_Send_Byte(addr&0XFF);
while(numByteToRead--){
*Read_Buff = Spi_Flash_Send_Byte(0X00); //返回数据
Read_Buff++; //地址+1
}
W25QXX_CS = 1;
}
2.9 编写向Flash中写入内容的函数
发送0X02,发送扇区地址,分三次发送扇区地址,发送内容字节,进行数据存储。
//写入内容
void SPI_Write_Data(uint32_t addr, uint8_t *Write_Buff,uint32_t numByteToWrite){
SPI_Writer_Enable(); //FLASH写入使能
W25QXX_CS = 0; //CS引脚拉低 ,片选使能
Spi_Flash_Send_Byte(0X02); //发送命令,写数据的指令
Spi_Flash_Send_Byte((addr>>16)&0XFF); //分三次发送扇区地址
Spi_Flash_Send_Byte((addr>>8)&0XFF);
Spi_Flash_Send_Byte(addr&0XFF);
while(numByteToWrite--){
Spi_Flash_Send_Byte(*Write_Buff); //写入数据
Write_Buff++; //地址+1
}
W25QXX_CS = 1; //CS引脚拉高
}
2.10 编写main函数
uint8_t read_buff[4096];
uint8_t write_buff[4096];
int main(void)
{
uint32_t value;
uint16_t i;
uint16_t j;
LED_GPIO_Config();
USART_Config();
SPI1_Init();
value = SPI_Read_ID();
printf("ID = 0X%x\r\n",value);
SPI_ERASER_SECTOR(0); //擦除扇区,0是代表地址
SPI_WaitForWriterEnd(); //等待擦除扇区结束
for(i = 0;i < 20; i++){
write_buff[i] = 9; //写进去的数字不能大于256
}
SPI_Write_Data(0, write_buff,20); //只写20个字节的数据
SPI_WaitForWriterEnd(); //等待写入扇区结束
SPI_Read_Data(0,read_buff,4096); //读地址0,把读到的数据放到buff中,读4096个字节
for(j = 0;j<4096;j++){
printf("0X%x",read_buff[j]);
if(j%10 == 0){ //每10个数据,回车换行
printf("\r\n");
}
}
while(1){}
}
3. 实验结果
可以看到制造商ID:0XEF,设备ID:0X17。
可以看到前20个字节的数据内容是0X9,后面的字节内容都是0XFF,证明写入和读取数据正确。