前言
本次我们学习一下STM32F103关于SPI对存储芯片的读写,介绍W25QXX芯片和对芯片内部讲解和代码解读,学习W25QXX芯片的各种读写指令,操作芯片读写,认识底层驱动,本篇内容主要目的是教会大家看手册写代码,读代码。
本篇博客大部分是自己收集和整理,如有侵权请联系我删除。
本次实验板子使用的是正点原子精英版,芯片是STM32F103ZET6,需要资料可以@我拿取。
交流群:717237739
如果觉得有用点赞关注收藏三连,多谢支持
本博客内容原创,创作不易,转载请注明
没有初步认识SPI协议的,可以先看看我之前的博客:SPI协议讲解
一 . W25QXX 存储芯片介绍
应用:需要存储较多数据的时候,比如字库,图片,各种模块数据等等
总述:W25Q16,W25Q32 ,W25Q64 系列的FLASH存储器可以为用户提供存储解决方案。
存储芯片大小:这是其中几个,其他都差不多,具体要查一下
W25Q16 |
16M比特 (bit) / 2M字节(byte)
|
W25Q32 | 32M比特 (bit) / 4M字节(byte) |
W25Q64 | 64M比特 (bit) / 8M字节(byte) |
W25Q128 | 128M比特 (bit) / 16M字节(byte) |
本篇文章我们用W25Q16来介绍,内存容量大小为:2*1024*1024->内存地址
W25Q16,W25Q32 ,W25Q64 系列的FLASH存储器分别有8192,16384,32768可编程页,每页256个字节。芯片内部分为了块,扇区,页的编程指令和擦除指令,下面来看看他们之间的关系。
内部区分是从 块 -> 扇区 -> 页
块 | 1 块 = 16个扇区 = 64 K |
扇区 | 1 扇区= 16页 = 4096 字节 = 4 K |
页 | 1 页 = 256 个字节 |
字节 | 1 字节 = 8 位 |
1.用“页编程指令”每次就可以编程256个字节,意思就是每次都能一次性最多写入256个字节,因为芯片规定了不能跨页来写入数据,所以需要我们自己修改程序代码。
2.用 扇区擦除指令 每次可以擦除16页 ,不能自己删除指定的页区,每次使用这个指令都是直接擦除16页
3.用 块擦除指令 每次可以擦除256页 , 用 整片擦除指令 可擦除整个芯片。
标准的SPI接口:
地址和区域分布(在英文手册有描述):
二 . 引脚说明和SPI初始化
通过手册我们可以看到SPI的配置模式选择:
SPI 配置我们就不多说了,不懂去看看我博客的SPI协议讲解,现在直接上代码:
#include "spi.h"
//SPI口初始化
void spi1_init(void)
{
RCC->APB2ENR|=1<<2; //PORTA时钟使能
RCC->APB2ENR|=1<<12; //SPI1时钟使能
GPIOA->CRL&=0X000FFFFF;
GPIOA->CRL|=0XB8B00000; //PA5 PA7复用推挽输出
GPIOA->ODR|=0X07<<5; //PA6 上拉输入
SPI1->CR1|=0<<11; //8bit数据格式
SPI1->CR1|=0<<10; //全双工模式
SPI1->CR1|=1<<9; //软件nss管理
SPI1->CR1|=1<<8;
SPI1->CR1|=0<<7; //MSB first
SPI1->CR1|=3<<3; //Fsck=Fpclk1/16,对SPI1属于APB2的外设.时钟频率最大为72M.
SPI1->CR1|=1<<2; //SPI主机
SPI1->CR1|=1<<1; //空闲模式下SCK为1 CPOL=1
SPI1->CR1|=1<<0; //数据采样从第二个时间边沿开始,CPHA=1
SPI1->CR1|=1<<6; //SPI设备使能
}
//spi1发送数据
void spi1_send_byte(u8 data)
{
//等待前面的发送完成
while((SPI1->SR&(1<<1))==0); //等待发送区空
//给数据DR寄存器
SPI1->DR = data; //这一步执行完成后并不会马上传输完成
//spi是一个数据交换协议,发送一个字节,必然可以收到一个字节
while((SPI1->SR&(1<<0))==0); //等待接收完成(无用的数据)
data = SPI1->DR; //读取接收到无用的数据
}
/********************************************************************
* Function: spi2_receive_byte
* Description: spi2接收数据
* Return : 接收到的数据
*********************************************************************/
u8 spi1_receive_byte(void)
{
u8 data;
//等待前面的发送完成
while(!(SPI1->SR&(1<<1)));
//给数据DR寄存器
SPI1->DR = 0xaa; // 读的时候传的值,这个值不重要,什么值都可以(0x01等)给这个值的目的主要是维持有时钟(时序图可以看出)
//等待接收完成
while(!(SPI1->SR&(1<<0)));
data = SPI1->DR; //读取数据
return data; //返回的是我们需要的数据。
}
//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 spi1_read_write_byte(u8 data)
{
//写功能
while((SPI1->SR&1<<1)==0); //等待发送区空 --等待发送缓冲为空
//这一步执行完成后并不会马上传输完成
SPI1->DR=data; //发送一个byte ,写进去之后会自动的发出去
//到这里其实发送函数已经发送完成了
//SPI是一个数据交换协议,发送一个字节,必然可以接收到一个字节
while((SPI1->SR&1<<0)==0) ; //等待接收完一个byte
return SPI1->DR; //返回收到的数据
}
三 . W25Q16读写指令时序讲解
芯片本身自带有一个控制和状态寄存器,要控制他读写,我们就需要控制操作寄存器来编写代码。
1. 状态寄存器位了解
这个第0位 和 第1位,都是只读位,在执行操作的时候才会置1,所以平常默认为 0;
这两个为是可读可写位,根据自己想要执行的操作,给上对应的位,就可以通过SPI写入读出。
通过以上的寄存器了解,我们就可以读出状态数据知道芯片的存储器阵列是否可写或者不可写,会是否处于写保护状态。
2.W25QXX 的指令了解和代码讲解
在学习这个指令之前,我们要先了解清楚SPI的工作模式,以及使用到SPI的读写函数,这个不清楚可以看我往期博客,这里就不细说了;
函数的基本执行流程:一般来说就是拉低片选,然后发送指令,从机会随机返回一个数据,这个数据可以由你自己决定,执行相应操作后,拉高片选,这个函数就实现了对应的指令功能了,
1.读状态寄存器指令(05h)
1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把0X05的指令发送过去
3.接着从机返回一个随机数据,这里我们读取一个0xff
4.发送完成后,拉高片选,这样我们就完成了读状态寄存器的指令函数的编写了
因为这次是读取从机的数据,所以需要返回值
//读取W25QXX的状态寄存器
//BIT7 6 5 4 3 2 1 0
//SPR RV TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
//W25QXX_ReadStatusReg 0
u8 W25QXX_ReadSR(void)
{
u8 byte=0;
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ReadStatusReg); //发送读取状态寄存器命令
byte=spi1_read_write_byte(0Xff); //读取一个字节
W25QXX_CS=1; //取消片选
return byte;
}
2.写状态寄存器命令(01h )
1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把 0X01 的指令发送过去
3.然后写好我们需要发送的字节
4.发送完成后,拉高片选,这样我们就完成了写状态寄存器的指令函数的编写了
//写W25QXX状态寄存器
//只有SPR,TB,BP2,BP1,BP0(bit 7,5,4,3,2)可以写!!!
// W25X_WriteStatusReg 0x01
void W25QXX_Write_SR(u8 sr)
{
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25X_WriteStatusReg);//发送写取状态寄存器命令
spi1_read_write_byte(sr); //写入一个字节
W25QXX_CS=1; //取消片选
}
3.写使能状态寄存器 (06h)
1.通过介绍,我们首先就是先拉低片选引脚
2.然后通过SPI协议编写的读写函数,把 0X06 的指令发送过去
3.发送完成后,拉高片选,这样我们就完成了写状态寄存器的指令函数的编写了
//W25QXX写使能
//将WEL置位
//W25QXX_WriteEnable 0x06
void W25QXX_Write_Enable(void)
{
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_WriteEnable); //发送写使能
W25QXX_CS=1; //取消片选
}
4.页编程指令(02h)
我们要通过写入数据进去flash,就需要用到这个页编程指令,在页内写入我们的数据,写这个函数都要用到上面的读写函数,所以我们要先写好其他的函数,才能执行其他的指令。
为什么要发送24位地址,这个24位地址又是什么东西?
1.首先我们要先知道,SPI每次只能发送8位地址,所以这个24位地址我们需要分三次发
2.这个24位的地址,本质上就是数据存放的位置,为什么是24位呢,因为这个地址就是24位的,我们看手册就能知道。
3.比如我们要发送0X123456怎么发呢,SPI是高位先发的,所以我们要用上点位运算就可以了
4.发完24位地址之后,我们就可以发送我们的字节了,最大只能发送256个字节,超过就不行了
下面直接上代码讲解一下实现过程:
//SPI在一页(0~32768)内写入少于256个字节的数据,按照你的芯片规格计算大小
//在指定地址开始写入最大256字节的数据 (不能跨扇区写)
//调用这个函数必须确保写入的区域已经擦除了
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
//W25QXX_PageProgram 0
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 i;
W25QXX_Write_Enable(); //SET WEL 发送写使能,WEL位自动置1
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_PageProgram); //发送写页命令
spi1_read_write_byte((u8)((WriteAddr)>>16)); //发送24bit地址
spi1_read_write_byte((u8)((WriteAddr)>>8));
spi1_read_write_byte((u8)WriteAddr);
for(i=0;i<NumByteToWrite;i++)
spi1_read_write_byte(pBuffer[i]); //循环写数
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待写入结束
}
1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,保证区域内没有数据,如果不确定就需要先擦除
3.发送页编程的指令0x02
4.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
5.使用一个for循环,写入数组或者指针内的数据,可以连续写入,最多可以写256个
6.拉高片选,写入数据结束
7.根据手册,最后还要一个等待写入结束,这时候我们写一个函数判断一下就行,后面也会用到这个函数的
u8 texttowrite[]="12345ABCDE"; //要发送的数据
#define SIZE sizeof(texttowrite) .
W25QXX_Write(texttowrite,88,SIZE);
//等待空闲
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR()&0x01)==0x01); // 等待BUSY位清空
}
5.读数据寄存器(03h)
1.根据手册描述,首先我们先拉低片选
2.发送页编程的指令0x03
4.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
5.使用一个for循环,读取数组或者指针内的数据,同时返回随机数据
6.拉高片选,读取数据结束
本质上和页编程差不多,对着时序和手册都能简单的写出来了
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大32768)
//W25QXX_ReadData 0
void W25QXX_Read(u8* buffer,u32 read_address,u16 NumByteToRead)
{
u16 i;
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ReadData); //发送读取命令
spi1_read_write_byte((u8)((read_address)>>16)); //发送24bit地址
spi1_read_write_byte((u8)((read_address)>>8));
spi1_read_write_byte((u8)read_address);
for(i=0;i<NumByteToRead;i++)
{
buffer[i]=spi1_read_write_byte(0XFF); //循环读数 ,发送空字节
}
W25QXX_CS=1;
}
6.扇区擦除指令(20h)
1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,发送扇区擦除的指令0x20,等待空闲
3.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
4.拉高片选,扇区擦除完成,等待空闲
扇区擦除每次擦除4K=4096;在程序里面我们可自己设置擦除哪一个扇区的地址,也可以不设置,直接从写入地址开始擦除,下面看程序:
//擦除一个扇区
//Dst_Addr: 扇区编号,是第几个扇区,不是绝对地址
//擦除一个扇区的最少时间:150ms
// W25QXX_SectorErase 0x20
void W25QXX_Erase_Sector(u32 Dst_Addr) //1
{
Dst_Addr*=4096;
W25QXX_Write_Enable(); //SET WEL 写使能
W25QXX_Wait_Busy(); //等待空闲
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_SectorErase); //发送扇区擦除指令
spi1_read_write_byte((u8)((Dst_Addr)>>16)); //发送24bit地址 ,先写高位
spi1_read_write_byte((u8)((Dst_Addr)>>8));
spi1_read_write_byte((u8)Dst_Addr);
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待擦除完成
}
7.块擦除指令(D8h)
1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,发送扇区擦除的指令0xD8,等待空闲
3.发送24 bit的地址,右移16先发高位,然后右移8位,最后不用右移了
4.拉高片选,扇区擦除完成,等待空闲
程序代码和扇区擦除差不多,因为我懒所以这块没做,留着给大家自己尝试一下
8.芯片擦除指令(C7h)
1.根据手册描述,首先我们先写使能,使用到刚才的写使能函数
2.拉低片选,发送扇区擦除的指令0xc7,等待空闲
3.拉高片选,扇区擦除完成,等待空闲
//擦除整个芯片
//等待时间超长...
//W25QXX_ChipErase 0xc7
void W25QXX_Erase_Chip(void)
{
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
W25QXX_CS=0; //使能器件
spi1_read_write_byte(W25QXX_ChipErase); //发送片擦除命令
W25QXX_CS=1; //取消片选
W25QXX_Wait_Busy(); //等待芯片擦除结束
}
9.掉电指令(B9h)
这个指令实在适合电池供电的情况下使用,我们根据手册要求使用就可以
//进入掉电模式
//W25X_PowerDown 0XB9
void W25QXX_PowerDown(void)
{
W25QXX_CS=0; //使能器件
SPI2_ReadWriteByte(W25X_PowerDown); //发送掉电命令
W25QXX_CS=1; //取消片选
delay_us(3); //等待TPD
}
10. 读ID指令(90h)
因为我们这个W25QXX,这个程序大部分都适用,所以当我们使用不同大小的flsah芯片的时候,我们就可以通过读取ID,选到自己对应的芯片了。
//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q32
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
u16 W25QXX_ReadID(void)
{
u16 Temp = 0;
W25QXX_CS=0;
SPI2_ReadWriteByte(0x90);//发送读取ID命令
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp|=SPI2_ReadWriteByte(0xFF)<<8;
Temp|=SPI2_ReadWriteByte(0xFF);
W25QXX_CS=1;
return Temp;
}
以上就是这个flash芯片的大部分指令函数以及使用了,相信很多人对这个擦除的方式不太了解,为什么每次都整片擦除,那之前写的数据怎么办,又要重新写入吗?
针对这个问题,那这种方法肯定是我们自己通过程序来实现,随时随地擦除和写入,由于本次内容已经很多了,所以这个问题留到下一个博客,下一个博客讲解一下如何实现随意的擦除和写入,还有对字库以及图片写入数据的讲解,
总结:
以上就是这篇博客的全部内容了,内容比较多,感觉大家耐心看完,代码其实都是大同小异,大家可以学会使用看手册和时序来编写自己想要实现的函数。
大家如果对我的博客有疑问或者错误,可以@我修改,大家相互交流。
交流群:717237739
如果觉得有用点赞关注收藏三连,多谢支持
本博客内容原创,创作不易,转载请注明
点赞收藏关注博主,不定期分享单片机知识,互相学习交流。