STM32标准库SPI通信协议与W25Q64-2

简介: STM32标准库SPI通信协议与W25Q64

STM32标准库SPI通信协议与W25Q64-1

https://developer.aliyun.com/article/1508404


2.硬件电路


  • 首先看一下引脚定义,VCC、GND是电源供电,引脚供电电压是2.7~3.6V,是一个典型的3.3V供电设备不能直接接入5V电压,然后1号脚cs,这个cs左边画了个斜杠代表是低电平有效,或者这边cs上面画了个横线也是低电平有效,那这里cs对应之前我们讲spi的名称就是SS,意思是spi的片选引脚,6号脚clk对应就是sck,是spi的时钟线,然后5号引脚di对应mosi,是spi主机输出从机输入,2号do对应miso,是spi主机输入从机输出,这四个引脚就是spi通信的四个引脚。
  • 3号引脚wp,他的意思是写保护配合内部的寄存器器配置,可以实现保硬件的写保护,写保护低电平有效,wp接低电平保护住不让写,wp接高电平不保护可以,最后7号hold意思就是数据保持哈,低电平有效,这个用的不多了解一下,就是如果你在进行正常读写时突然产生中断,然后想用spi通信线去操控其他器件,这时如果把cs置回高电平,那时序就终止,但如果你又不想终止总线,又想操作其他器件,这就可以hold引脚置低电平,这样芯片就hold住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来哈,hold置回高电平,然后继续hold之前的时序,相当于spi总线进来一次中断,并且在中断里还可以用spi干别的事情,这就是hold的功能。
  • di、do、wp、和HOLD,旁边都有括号,写了lO0、lO1、lO2、lO3 ,这个就对应我们刚才这里说的双重spi和四重spi,如果是普通的spi模式,那括号里的都不用看,如果是双重spi,那di和do就变成lO0和lO1,也就是数据同时收和同时发的两个数据位,如果是四重spi,那就再加上wp当做lO2 ,HOLD当做lO3 ,这四个引脚都作为数据收发引脚,一个时钟四个数据位。


3.框图


  • 首先右边这一整个矩形空间里是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址,这样说了w25q64的地址宽度是24位3个字节,所以可以看到左下角第一个字节,它的地址是00 00 00h,h代表16进制,之后的空间地址依次自增,直到最后一个字节,地址是7F FF FF h,那最后一个字节为啥是7f开头,不是f f开头呢,因为24位地址最大寻址范围是16MB,我们这个芯片只有8MB,所以地址空间我们只用了一半,8MB排到最后一个字节,就是7F FF FF h,那这是整个地址空间,从000000~7F FF FF,然后在这整个空间里,我们以64kb为一个基本单元,把它划分为若干的块block,从前往后依次是块0块1块2等等,一直分到最后一块,那整块蛋糕是8MB,以64kb为一块进行划分,最后分得的快数就是8MB除以64kB,这里可以分得128块(8*1025/64=128),那块序号就是块0一直到最后一个是块127,然后观察一下块内地址值的变化规律,比如块0的起始地址是000000,结束地址是00f f f f,之后块31起始是1f0000 ,结束是1f f f f f,之后的都观察一下,可以发现在每一块内,它的地址变化范围就是最低的两个字节,每个块的起始地址是XX0000,结束是XXf f f f,这是块内地址的变化规律,到这里这一块大蛋糕我们就分好块了,64kb为一块总共128块,之后看一下左边这个示意图,我们还要再对每一块进行更细的划分,分为多个扇区sector,这里的虚线指向了右边的各个块,也就是告诉你每一块里面都是这个样子的,那在每个块里,它的起始地址是XX0000,结束地址是XXf f f f,在一块里我们再以4kb为一个单元进行切分,一块是64kb,我4kb一切总共16份,所以在每一块里都可以分为扇区0一直到扇区15,观察一下地址规律,可以发现每个扇区内的地址范围是XXX000到XXXf f f,地址划分啊到扇区就结束了,但是当我们在写入数据时啊,还会有个更细的划分,就是页Page,页是对整个存储空间划分的,当然你也可以把它看作在扇区里再进行划分都一样,那页的大小是256个字节,一个扇区是4kb,所以一个扇区里可以分为16页(4*1025/256=16),然后页的地址规律呢我们也看一下,在这里每一行就是一页,左边这里指了个箭头,写的是页地址的开始,右边这里也指了个箭头,写的是页地址的结束,在一页中,地址变化范围是XXXX00到XXXXFF,一页内的地址变化,仅限于地址的最低一个字节,这就是页的划分,那这个存储器的地址划分啊我就讲完了,我们需要记住的是一整个存储空间,首先划分为若干块,对于每一块又划分为若干扇区,然后对于整个空间会划分为很多很多页,每页256字节,这个我们需要记住。        
  • 控制逻辑左边就是spi的通信引脚,有wp、HOLD、CLK、CS、DI和DO,这些引脚就和我们的主控芯片相连,主控芯片通过spi协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能,然后去看控制逻辑上面有个状态寄存器器,这个状态寄存器是比较重要的,比如芯片是否处于忙状态,是否写使能,是否写保护,都可以在这个状态寄存器里体现,可以通过看手册分析,然后上面是写控制逻辑和外部的wp引脚相连,显然这个是配合wp引脚实现硬件写保护的,然后继续右边这里是一个高电压生成器,这个是配合flash进行编程的,因为flash是掉电不丢失的,如何实现掉电不丢失呢,比如你点亮一个led表示1,熄灭led表示0,但如果整个系统电都没有,那1和0就无从说起了,所以要想掉电不丢失,就要我们在存储器里产生一些刻骨铭心的变化,比如一个led我给他加很高的电压,那led就烧坏了,我们用烧坏的led表示1没烧坏的led表示0然后再断电,烧坏的led还是烧坏的,有电没电它都是坏的,这个烧没烧坏的状态,不受有电还是没电的影响,所以它就是掉电不丢失,那对于我们的非易失性存储器来说也是一样,我们要让它产生即使断电也不会消失的状态,一般都需要一个比较高的电压去刺激它,所以这种掉电不丢失的存储器,一般都需要一个高压源,那这里芯片内部集成了高电压发生器,所以就不需要我们在外接高电压了,比较方便哈,当然我这里只是举例简单描述一下掉电不丢失的存储原理。
  • 然后继续看下面,这里是页地址锁存计数器,然后下面还有一个字节地址锁存计数器,这两个地址锁存和记数器就是用来指定地址的,我们通过spi总共发过来三个字节的地址,因为一页是256字节,所以一页内的字节地址就取决于最低一个字节,而高位的两个字节就对应的是页体质,所以在这里我们发的三个字节地址,前两个字节会进到这个页地址锁存计数器里,最后一个字节会进到这个字节地址锁存计数器里,然后页地址通过这个写保护和行解码来选择我要操作哪一页,字节地址通过这个列解码和256字节页缓存,来进行指定地址的读写操作,那就因为我们这个地址锁存,都是有个计数器的,所以这个地址指针在读写之后可以自动加1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了,那最后右边这里有个256字节的页缓存区,它其实是一个256字节的ram存储器,这个稍微留个印象,等会儿还会提到,然后我们数据读写,就是通过这个ram缓冲区域来进行的,我们写入数据会先放到缓存区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的flash里,进行永久保存,那为啥要弄个缓冲区呢,我们直接往flash里写不好吗,那这是因为我们的spi写入的频率是非常高的,而flash的写入由于需要掉电不丢失,留下刻骨铭心的印象,他就比较慢,所以这个芯片的设计思路就是你写入的数据,我先放在缓存区里存着,因为缓存区是ram,所以它的速度非常快啊,可以跟得上spi总线的速度,但这里有个小问题,就这个缓冲区只有256字节,所以写入的时序有个限制条件,就是写入了一个时序,连续写入的数据量不能超过256字节,然后等你写完了,我芯片再慢慢的把数据从缓冲区转移到flash存储器里,那么数据从缓存区转到flash里,需要一定的时间哈,所以在写入时序结束后,芯片会进入一段忙的状态,在这里它就会有一条线哈,通往状态寄存器给状态接容器的busy位置1表示芯片当前正忙,那在忙的时候,芯片就不会响应新的读写时序了哈,就是写入的执行流程,然后我们读取数据,虽然这里画的话应该也是会通过缓冲区来读句,但是由于读取只看一下电路的状态就行了,它基本不花时间,所以读取的限制就很少了,速度也非常快。


4.Flash操作注意事项


  • 第一点写入操作前必须先进行写使能,这个是一种保护措施,防止你误操作的,就像我们使用手机一样,先解锁再操作,这样可以防止手机在你裤兜里到处点点点对吧,写使能的话我们就使用spi发送一个写使能的指令,就可以完成了。
  • 每个数据位只能由1改写为0,不能由0改写为1,这个意思就是说,flash并没有像ram那样的直接完全覆盖改写的能力,比如在某一个字节的存储单元里面,存储了0xAA这个数据,对应的二进制位就是10101010,如果我直接再次在这个存储单元写入一个新的数据,比如我再次写入一个0x55 ,那写完之后这个存储单元里存的是x55,实际上并不是,因为0x55的二进制是01010101,当这个01010101要覆盖原来的10101010时,就会受到这里第二条规定的限制,每个数据位只能由一改写为零,不能由零改写为一,你要问为啥会有这个限制,那只能说是成本原因或者技术原因,所以这里写入01010101之后,依次来看啊,最高位由原来的1改写为0是可以的,所以写出之后新的最高位就是零,但是第二位原来是零,现在我要改写成1,这是不行的,所以写入之后,新的第二位仍然是零,之后第三位要改写为零,可以,结果为零,第四位零改写为1,不可以,结果仍然是零,那以这个规律进行下去,0xaa在覆盖写入0x55 之后,这个存储单元最终的数据是什么啊,0x00也就是八位全为零,这就出现问题了对吧,所以为了弥补这个只能1改0,不能0改1的缺陷,我们就引出了第三条规定,就是写入数据前必须先擦除,擦除后所有数据位变为一,在这里flash是有一个擦除的概念的,擦除会有专门的擦除电路进行,我们只要给他发送擦除的指令就行了,那通过擦除电路擦除之后,所有的数据位都变成一,这样我们是不是就可以弥补第二条限制的缺陷了,当我们写出一个数据之前,无论原来存的是什么,我直接给它擦除掉,擦除之后所有的位变成1,也就是16进制的f f,这样我无论再写入什么样的数据,就都可以正确的写入了。
  • 擦除必须按最小拆除单元进行,这个应该也是为了成本而做出的妥协,就是说你写入前要进行擦除,这我知道,所以如果我想在00这个地址下写入数据,那我就先把00地址擦除,再写入数据到00地址不就行了吗,但是这个方案有个问题啊,flash的擦除有最小擦除单元的限制,你不能指定某一个直接去擦除,要擦就得一大片一起擦,那在我们这个芯片里,你可以选择整个芯片擦除,也可以选择按块擦除或者按扇区擦除,然后再小就没有了,所以最小的擦除单元就是一个扇区,刚才我们看了一个扇区是4kb就是4096个字节,所以你擦除最少就得4096个字节一起擦,我只想查出某一个字节怎么办呢,这没办法你只能把那个字节所在扇区的4096个字节全都擦掉,那你又说这个扇区其他的地方我还存的有数据怎么办呢,这也没办法,要想不丢失数据,你只能先把4096个字节都读出来,再把4096个字节的扇区擦掉,改写完读出来的数据后,再把4096个字节全都写回去。
  • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入,这个意思就是说你在写入的时候,一次性不能写太多了,一个写入时序最多只能写一页的数据也就是256字节,为什么有这个限制呢,这是因为在这里有一个页缓冲区,它只有256字节,为什么有缓冲区呢,这是因为flash的写入太慢了,跟不上spi的频率,所以写入的数据会先放在ram里暂存,等时序结束后,芯片再慢慢的把数据写入到flash里,所以这里会有个限制,每个时序最多写入一页的数据,你再写多缓冲区存不下了,如果你非要写,那超过页尾位置的数据会回到页首覆盖写入,另外我们这个页缓存区是和flash的页对应的,你必须得从页起始位置开始写,才能最大写入256字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱哈,所以我们在进行多字节写入时,一定要注意这个地址范围不能跨越页的边缘,否则会地址错乱。
  • 然后写入操作结束后,芯片进入忙状态,不响应新的读写操作,我们的写入操作都是对缓存区进行的,等时序结束后芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下不要进行新的读写操作,否则芯片是不会响应我们的,要想知道芯片什么时候结束盲状态,我们可以使用读状态寄存器的指令,看一下状态寄存器的busy位是否为1,为0时芯片就不忙了,我们再进行操作,另外注意这个写入操作,包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等忙状态结束后才能进行后续操作。


四、软件SPI读写W25Q64

面包板接线:

MySPI.c

#include "stm32f10x.h"                  // Device header
 
/*引脚配置层*/
 
/**
  * 函    数:SPI写SS引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
  GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);    //根据BitValue,设置SS引脚的电平
}
 
/**
  * 函    数:SPI写SCK引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
  */
void MySPI_W_SCK(uint8_t BitValue)
{
  GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);    //根据BitValue,设置SCK引脚的电平
}
 
/**
  * 函    数:SPI写MOSI引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
  */
void MySPI_W_MOSI(uint8_t BitValue)
{
  GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);    //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}
 
/**
  * 函    数:I2C读MISO引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
  */
uint8_t MySPI_R_MISO(void)
{
  return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);      //读取MISO电平并返回
}
 
/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
  */
void MySPI_Init(void)
{
  /*开启时钟*/
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
  
  /*GPIO初始化*/
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);          //将PA4、PA5和PA7引脚初始化为推挽输出
  
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);          //将PA6引脚初始化为上拉输入
  
  /*设置默认电平*/
  MySPI_W_SS(1);                      //SS默认高电平
  MySPI_W_SCK(0);                     //SCK默认低电平
}
 
/*协议层*/
 
/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
  MySPI_W_SS(0);        //拉低SS,开始时序
}
 
/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
  MySPI_W_SS(1);        //拉高SS,终止时序
}
 
/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
  uint8_t i, ByteReceive = 0x00;          //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
  
  for (i = 0; i < 8; i ++)            //循环8次,依次交换每一位数据
  {
    MySPI_W_MOSI(ByteSend & (0x80 >> i));   //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
    MySPI_W_SCK(1);               //拉高SCK,上升沿移出数据
    if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量
                                //当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
    MySPI_W_SCK(0);               //拉低SCK,下降沿移入数据
  }
  
  return ByteReceive;               //返回接收到的一个字节数据
}


MySPI.h

#ifndef __MYSPI_H
#define __MYSPI_H
 
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
 
#endif

W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
 
/**
  * 函    数:W25Q64初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
  MySPI_Init();         //先初始化底层的SPI
}
 
/**
  * 函    数:MPU6050读取ID号
  * 参    数:MID 工厂ID,使用输出参数的形式返回
  * 参    数:DID 设备ID,使用输出参数的形式返回
  * 返 回 值:无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_JEDEC_ID);      //交换发送读取ID的指令
  *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回
  *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位
  *DID <<= 8;                 //高8位移到高位
  *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);  //或上交换接收DID的低8位,通过输出参数返回
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64写使能
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WriteEnable(void)
{
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_WRITE_ENABLE);    //交换发送写使能的指令
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64等待忙
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WaitBusy(void)
{
  uint32_t Timeout;
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);        //交换发送读状态寄存器1的指令
  Timeout = 100000;             //给定超时计数时间
  while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)  //循环等待忙标志位
  {
    Timeout --;               //等待时,计数值自减
    if (Timeout == 0)           //自减到0后,等待超时
    {
      /*超时的错误处理代码,可以添加到此处*/
      break;                //跳出等待,不等了
    }
  }
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64页编程
  * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray  用于写入数据的数组
  * 参    数:Count 要写入数据的数量,范围:0~256
  * 返 回 值:无
  * 注意事项:写入的地址范围不能跨页
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
  uint16_t i;
  
  W25Q64_WriteEnable();           //写使能
  
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_PAGE_PROGRAM);    //交换发送页编程的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  for (i = 0; i < Count; i ++)        //循环Count次
  {
    MySPI_SwapByte(DataArray[i]);     //依次在起始地址后写入数据
  }
  MySPI_Stop();               //SPI终止
  
  W25Q64_WaitBusy();              //等待忙
}
 
/**
  * 函    数:W25Q64扇区擦除(4KB)
  * 参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
  * 返 回 值:无
  */
void W25Q64_SectorErase(uint32_t Address)
{
  W25Q64_WriteEnable();           //写使能
  
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);  //交换发送扇区擦除的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  MySPI_Stop();               //SPI终止
  
  W25Q64_WaitBusy();              //等待忙
}
 
/**
  * 函    数:W25Q64读取数据
  * 参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
  * 参    数:Count 要读取数据的数量,范围:0~0x800000
  * 返 回 值:无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
  uint32_t i;
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_READ_DATA);     //交换发送读取数据的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  for (i = 0; i < Count; i ++)        //循环Count次
  {
    DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据
  }
  MySPI_Stop();               //SPI终止
}

W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H
 
void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);
 
#endif

W25Q64_Ins.h

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
 
#define W25Q64_WRITE_ENABLE             0x06
#define W25Q64_WRITE_DISABLE            0x04
#define W25Q64_READ_STATUS_REGISTER_1       0x05
#define W25Q64_READ_STATUS_REGISTER_2       0x35
#define W25Q64_WRITE_STATUS_REGISTER        0x01
#define W25Q64_PAGE_PROGRAM             0x02
#define W25Q64_QUAD_PAGE_PROGRAM          0x32
#define W25Q64_BLOCK_ERASE_64KB           0xD8
#define W25Q64_BLOCK_ERASE_32KB           0x52
#define W25Q64_SECTOR_ERASE_4KB           0x20
#define W25Q64_CHIP_ERASE             0xC7
#define W25Q64_ERASE_SUSPEND            0x75
#define W25Q64_ERASE_RESUME             0x7A
#define W25Q64_POWER_DOWN             0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE        0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET     0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID   0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID       0x90
#define W25Q64_READ_UNIQUE_ID           0x4B
#define W25Q64_JEDEC_ID               0x9F
#define W25Q64_READ_DATA              0x03
#define W25Q64_FAST_READ              0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT        0x3B
#define W25Q64_FAST_READ_DUAL_IO          0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT        0x6B
#define W25Q64_FAST_READ_QUAD_IO          0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO        0xE3
 
#define W25Q64_DUMMY_BYTE             0xFF
 
#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
 
uint8_t MID;              //定义用于存放MID号的变量
uint16_t DID;             //定义用于存放DID号的变量
 
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};  //定义要写入数据的测试数组
uint8_t ArrayRead[4];               //定义要读取数据的测试数组
 
int main(void)
{
  /*模块初始化*/
  OLED_Init();            //OLED初始化
  W25Q64_Init();            //W25Q64初始化
  
  /*显示静态字符串*/
  OLED_ShowString(1, 1, "MID:   DID:");
  OLED_ShowString(2, 1, "W:");
  OLED_ShowString(3, 1, "R:");
  
  /*显示ID号*/
  W25Q64_ReadID(&MID, &DID);      //获取W25Q64的ID号
  OLED_ShowHexNum(1, 5, MID, 2);    //显示MID
  OLED_ShowHexNum(1, 12, DID, 4);   //显示DID
  
  /*W25Q64功能函数测试*/
  W25Q64_SectorErase(0x000000);         //扇区擦除
  W25Q64_PageProgram(0x000000, ArrayWrite, 4);  //将写入数据的测试数组写入到W25Q64中
  
  W25Q64_ReadData(0x000000, ArrayRead, 4);    //读取刚写入的测试数据到读取数据的测试数组中
  
  /*显示数据*/
  OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);    //显示写入数据的测试数组
  OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
  OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
  OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
  
  OLED_ShowHexNum(3, 3, ArrayRead[0], 2);     //显示读取数据的测试数组
  OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
  OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
  OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
  
  while (1)
  {
    
  }
}


五、硬件SPI读写W25Q64

硬件SPI读写W25Q64有固定引脚如下:

MySPI.c

#include "stm32f10x.h"                  // Device header
 
/**
  * 函    数:SPI写SS引脚电平,SS仍由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
  GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);    //根据BitValue,设置SS引脚的电平
}
 
/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
  /*开启时钟*/
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);  //开启SPI1的时钟
  
  /*GPIO初始化*/
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);          //将PA4引脚初始化为推挽输出
  
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);          //将PA5和PA7引脚初始化为复用推挽输出
  
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);          //将PA6引脚初始化为上拉输入
  
  /*SPI初始化*/
  SPI_InitTypeDef SPI_InitStructure;            //定义结构体变量
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;     //模式,选择为SPI主模式
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //方向,选择2线全双工
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;   //数据宽度,选择为8位
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    //先行位,选择高位先行
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;  //波特率分频,选择128分频
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;        //SPI极性,选择低极性
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;      //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;       //NSS,选择由软件控制
  SPI_InitStructure.SPI_CRCPolynomial = 7;        //CRC多项式,暂时用不到,给默认值7
  SPI_Init(SPI1, &SPI_InitStructure);           //将结构体变量交给SPI_Init,配置SPI1
  
  /*SPI使能*/
  SPI_Cmd(SPI1, ENABLE);                  //使能SPI1,开始运行
  
  /*设置默认电平*/
  MySPI_W_SS(1);                      //SS默认高电平
}
 
/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
  MySPI_W_SS(0);        //拉低SS,开始时序
}
 
/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
  MySPI_W_SS(1);        //拉高SS,终止时序
}
 
/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
  while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空
  
  SPI_I2S_SendData(SPI1, ByteSend);               //写入数据到发送数据寄存器,开始产生时序
  
  while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);  //等待接收数据寄存器非空
  
  return SPI_I2S_ReceiveData(SPI1);               //读取接收到的数据并返回
}


MySPI.h

#ifndef __MYSPI_H
#define __MYSPI_H
 
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
 
#endif

W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
 
/**
  * 函    数:W25Q64初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
  MySPI_Init();         //先初始化底层的SPI
}
 
/**
  * 函    数:MPU6050读取ID号
  * 参    数:MID 工厂ID,使用输出参数的形式返回
  * 参    数:DID 设备ID,使用输出参数的形式返回
  * 返 回 值:无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_JEDEC_ID);      //交换发送读取ID的指令
  *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回
  *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位
  *DID <<= 8;                 //高8位移到高位
  *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);  //或上交换接收DID的低8位,通过输出参数返回
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64写使能
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WriteEnable(void)
{
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_WRITE_ENABLE);    //交换发送写使能的指令
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64等待忙
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_WaitBusy(void)
{
  uint32_t Timeout;
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);        //交换发送读状态寄存器1的指令
  Timeout = 100000;             //给定超时计数时间
  while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)  //循环等待忙标志位
  {
    Timeout --;               //等待时,计数值自减
    if (Timeout == 0)           //自减到0后,等待超时
    {
      /*超时的错误处理代码,可以添加到此处*/
      break;                //跳出等待,不等了
    }
  }
  MySPI_Stop();               //SPI终止
}
 
/**
  * 函    数:W25Q64页编程
  * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray  用于写入数据的数组
  * 参    数:Count 要写入数据的数量,范围:0~256
  * 返 回 值:无
  * 注意事项:写入的地址范围不能跨页
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
  uint16_t i;
  
  W25Q64_WriteEnable();           //写使能
  
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_PAGE_PROGRAM);    //交换发送页编程的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  for (i = 0; i < Count; i ++)        //循环Count次
  {
    MySPI_SwapByte(DataArray[i]);     //依次在起始地址后写入数据
  }
  MySPI_Stop();               //SPI终止
  
  W25Q64_WaitBusy();              //等待忙
}
 
/**
  * 函    数:W25Q64扇区擦除(4KB)
  * 参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
  * 返 回 值:无
  */
void W25Q64_SectorErase(uint32_t Address)
{
  W25Q64_WriteEnable();           //写使能
  
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);  //交换发送扇区擦除的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  MySPI_Stop();               //SPI终止
  
  W25Q64_WaitBusy();              //等待忙
}
 
/**
  * 函    数:W25Q64读取数据
  * 参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
  * 参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
  * 参    数:Count 要读取数据的数量,范围:0~0x800000
  * 返 回 值:无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
  uint32_t i;
  MySPI_Start();                //SPI起始
  MySPI_SwapByte(W25Q64_READ_DATA);     //交换发送读取数据的指令
  MySPI_SwapByte(Address >> 16);        //交换发送地址23~16位
  MySPI_SwapByte(Address >> 8);       //交换发送地址15~8位
  MySPI_SwapByte(Address);          //交换发送地址7~0位
  for (i = 0; i < Count; i ++)        //循环Count次
  {
    DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据
  }
  MySPI_Stop();               //SPI终止
}


W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H
 
void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);
 
#endif

W25Q64_Ins.h

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H
 
#define W25Q64_WRITE_ENABLE             0x06
#define W25Q64_WRITE_DISABLE            0x04
#define W25Q64_READ_STATUS_REGISTER_1       0x05
#define W25Q64_READ_STATUS_REGISTER_2       0x35
#define W25Q64_WRITE_STATUS_REGISTER        0x01
#define W25Q64_PAGE_PROGRAM             0x02
#define W25Q64_QUAD_PAGE_PROGRAM          0x32
#define W25Q64_BLOCK_ERASE_64KB           0xD8
#define W25Q64_BLOCK_ERASE_32KB           0x52
#define W25Q64_SECTOR_ERASE_4KB           0x20
#define W25Q64_CHIP_ERASE             0xC7
#define W25Q64_ERASE_SUSPEND            0x75
#define W25Q64_ERASE_RESUME             0x7A
#define W25Q64_POWER_DOWN             0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE        0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET     0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID   0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID       0x90
#define W25Q64_READ_UNIQUE_ID           0x4B
#define W25Q64_JEDEC_ID               0x9F
#define W25Q64_READ_DATA              0x03
#define W25Q64_FAST_READ              0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT        0x3B
#define W25Q64_FAST_READ_DUAL_IO          0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT        0x6B
#define W25Q64_FAST_READ_QUAD_IO          0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO        0xE3
 
#define W25Q64_DUMMY_BYTE             0xFF
 
#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
 
uint8_t MID;              //定义用于存放MID号的变量
uint16_t DID;             //定义用于存放DID号的变量
 
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};  //定义要写入数据的测试数组
uint8_t ArrayRead[4];               //定义要读取数据的测试数组
 
int main(void)
{
  /*模块初始化*/
  OLED_Init();            //OLED初始化
  W25Q64_Init();            //W25Q64初始化
  
  /*显示静态字符串*/
  OLED_ShowString(1, 1, "MID:   DID:");
  OLED_ShowString(2, 1, "W:");
  OLED_ShowString(3, 1, "R:");
  
  /*显示ID号*/
  W25Q64_ReadID(&MID, &DID);      //获取W25Q64的ID号
  OLED_ShowHexNum(1, 5, MID, 2);    //显示MID
  OLED_ShowHexNum(1, 12, DID, 4);   //显示DID
  
  /*W25Q64功能函数测试*/
  W25Q64_SectorErase(0x000000);         //扇区擦除
  W25Q64_PageProgram(0x000000, ArrayWrite, 4);  //将写入数据的测试数组写入到W25Q64中
  
  W25Q64_ReadData(0x000000, ArrayRead, 4);    //读取刚写入的测试数据到读取数据的测试数组中
  
  /*显示数据*/
  OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);    //显示写入数据的测试数组
  OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
  OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
  OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
  
  OLED_ShowHexNum(3, 3, ArrayRead[0], 2);     //显示读取数据的测试数组
  OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
  OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
  OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
  
  while (1)
  {
    
  }
}
相关文章
|
6月前
使用STM32F103标准库实现定时器控制LED点亮和关闭
通过这篇博客,我们学习了如何使用STM32F103标准库,通过定时器来控制LED的点亮和关闭。我们配置了定时器中断,并在中断处理函数中实现了LED状态的切换。这是一个基础且实用的例子,适合初学者了解STM32定时器和中断的使用。 希望这篇博客对你有所帮助。如果有任何问题或建议,欢迎在评论区留言。
481 2
|
5月前
stm32f407探索者开发板(十七)——串口寄存器库函数配置方法
stm32f407探索者开发板(十七)——串口寄存器库函数配置方法
802 0
|
6月前
|
IDE 开发工具
使用STM32F103标准库实现自定义键盘
通过本文,我们学习了如何使用STM32F103标准库实现一个简单的自定义键盘。我们首先初始化了GPIO引脚,然后实现了一个扫描函数来检测按键状态。这个项目不仅能够帮助我们理解STM32的GPIO配置和按键扫描原理,还可以作为进一步学习中断处理和低功耗设计的基础。希望本文对你有所帮助,祝你在嵌入式开发的道路上不断进步!
539 4
|
6月前
|
传感器
【经典案例】STM32F407使用HAL库配置I2C详解
STM32F407是一个强大的微控制器,广泛应用于嵌入式系统中。在许多应用中,我们需要使用I2C总线来与传感器、EEPROM、显示屏等外设进行通信。本文将详细介绍如何使用STM32 HAL库来配置和使用I2C接口。
802 2
|
6月前
|
存储 数据采集 数据安全/隐私保护
使用STM32F103读取TF卡并模拟U盘:使用标准库实现
通过以上步骤,你可以实现用STM32F103将TF卡内容变成U盘进行读取。这种功能在数据采集、便携式存储设备等应用中非常有用。如果你有更多的需求,可以进一步扩展此项目,例如添加文件管理功能、加密存储等。希望这篇博客能帮到你,如果有任何问题,欢迎在评论区留言讨论!
274 1
|
6月前
|
开发者
【经典案例】使用HAL库配置STM32F407的SPI外设
在嵌入式系统开发中,STM32F407是一款广泛应用的微控制器,而SPI(Serial Peripheral Interface)是一种常用的通信接口。本文将详细介绍如何使用STM32的硬件抽象层(HAL)库配置STM32F407的SPI外设,并提供完整的代码示例。
653 1
|
5月前
|
传感器 编解码 API
【STM32开发入门】温湿度监测系统实战:SPI LCD显示、HAL库应用、GPIO配置、UART中断接收、ADC采集与串口通信全解析
SPI(Serial Peripheral Interface)是一种同步串行通信接口,常用于微控制器与外围设备间的数据传输。SPI LCD是指使用SPI接口与微控制器通信的液晶显示屏。这类LCD通常具有较少的引脚(通常4个:MISO、MOSI、SCK和SS),因此在引脚资源有限的系统中非常有用。通过SPI协议,微控制器可以向LCD发送命令和数据,控制显示内容和模式。
210 0
|
7月前
|
传感器
STM32标准库ADC和DMA知识点总结-1
STM32标准库ADC和DMA知识点总结
|
7月前
|
传感器 存储 缓存