【STM32教程】使用硬件SPI和模拟SPI驱动W25Q64芯片的SPI时序

        由于MCU的FLASH空间有限,在特殊使用场所中会存在FLASH存储不够使用的情况。例如上篇中驱动LCD屏,需要将一个中文字库保存到MCU的FLASH中是不太现实的(STM32F103ZET6内部FLASH大小512KB),为此可使用外部FLASH作为拓展。

1. W25Q64简介

        W25Q64(64Mbit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行FLASH存储器。25Q系列比普通的串行FLASH存储器更灵活,性能更优越。基于双倍/四倍的SPI,它们能够可以立即完成提供数据给RAM,包括声音、文本和数据。

        W25Q64由每页256字节组成,每页的256字节用一次页编程指令即可完成。每次可以擦除16页(一个扇区),128页(32KB块),256页(64KB块)和全片擦除。

        W25Q64的内存空间结构:一页256字节,4k(4096字节)为一个扇区,16个扇区为一块,总容量为8M字节,共有128个块即2048个扇区。SPI最高支持80MHZ,当使用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHZ,四倍输出时最高速率320MHZ。

特点:

  • 标准、双倍和四倍SPI​​​​​​
  • 高性能串行FLASH存储器
  • 灵活的4KB扇区结构
  •        统一的扇区擦除和块擦除, 一次编程256字节,至少100000写/擦除周期,数据保存20年。

  • 高级的安全特点
  • 低功耗、宽温度范围
  •         单电源2.7~3.6V,工作电流4mA,-40℃~85℃工作。

    封装

    8Pin SOIC 208-mil

     引脚描述

    引脚编号 引脚名称 I/O 功能
    1 /CS I 片选端输入
    2 DO(IO1) I/O 数据输出
    3 /WP(IO2) I/O 写保护输入
    4 GND
    5 DI(IO0) I/O 数据输入
    6 CLK I 串行时钟输入
    7 /HOLD(IO3) I/O 保持端输入
    8 VCC 电源

    1.1 片选端(/CS)

            SPI片选(/CS)引脚使能和禁止芯片操作。当/CS为高电平时,芯片未被选择,串行数据输出(IOx)引脚为高阻态,芯片处于待机状态下的低功耗,除非芯片内部在擦除。当/CS变成低电平,芯片功耗将增长到正常工作,能够从芯片上读写数据。上电后,在接收新的指令前,/CS必须由高电平变成低电平。

    1.2 串行数据输入输出(DI、DO)

           标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。

    1.3 写保护(/WP)

            写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用,被用作了 IO2。

    1.4 保持端(/HOLD)

            当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD功能用在当有多个设备共享同一 SPI 总线时。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用,被用作了 IO3。

    1.5 串行时钟(CLK)

            串行时钟输入引脚为串行输入和输出操作提供时序。设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从上升沿开始。

    1.6  结构框图

    1.7 SPI操作

            W25Q64/16/32 兼容的 SPI 总线包含四个信号:串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)。标准的 SPI 用 DI 输入引脚在 CLK 的上升沿连续的写命令、地址或数据到芯片内。DO 输出在 CLK 的下降沿从芯片内读出数据或状态。
            支持 SPI 总线的工作模式 0(0,0)和 3(1,1)。模式 0 和模式 3 的主要区别在于常态时的 CLK信号,当 SPI 主机已准备好数据还没传输到串行 Flash 中,对于模式 0 CLK 信号常态为低。
            设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲
    状态时,时钟线CLK 为高电平。

    1.8 状态寄存器

             W25Q64的状态寄存器支持读写操作,读状态寄存器(指令:05H),写状态寄存器(指令:01H)。

            S0:忙位(BUSY)。BUSY位是个只读位。当器件在执行“页编程”“扇区擦除”“块区擦除”“芯片擦除”“写状态寄存器”指令时,该位自动置一。这时,除了“读状态寄存器”指令,其他指令都忽略。当编程、擦除和写状态寄存器指令执行完毕之后,该位自动变为0,表示该芯片可以接收其他指令了。

            S1:写保护位(WEL)。WEL位是个只读位,位于状态寄存器中的S1。执行完“写使能”指令后,该位置一。当芯片处于“写保护状态”下,该位为0。下面两种情况下,会进入“写保护状态”:

  •         掉电后。
  •         执行完以下指令后:写禁能,页编程,扇区擦除,块区擦除,芯片擦除和写状态寄器。
  • 其他状态位:略。

    2. 引脚连接

     注: 该原理图为野火霸道开发板上的原理图。

    PA4 /CS
    PA5 CLK
    PA6 DO/IO1
    PA7 DI/IO0

    3. SPI

    3.1 硬件SPI

            本次使用的MCU为STM32F103ZET6,硬件SPI详细介绍请仔细查看STM32F10x用户手册。

    SPI1 复用功能重映射

             使用MCU硬件SPI时,需要选择合适的IO口,并使能AFIO外设。

    SPI 框图

     通常SPI通过4个引脚与外部器件相连:

  • MISO:主设备输入/从设备输出。在从模式下发送数据,主模式下接收数据。
  • MOSI:主设备输出/从设备输入。在从模式下接收收据,主模式下发送数据。
  • SCK:  串口时钟,作为主设备的输出,从设备的输入。
  • NSS:从设备选择(片选引脚)。
  • 在本次使用过程中,应该将SPI1设为主设备,用于控制W25Q64。

    3.1.1 时钟极性和时钟相位

            SPI_CR寄存器的CPOL和CPHA位,能够组合成4种可能的时序关系。CPOL(时钟极性)控制在没有数据传输时时钟的空闲状态电平:CPOL=0时,空闲时钟电平为低电平;CPOL=1时,空闲时钟电平为高电平。CPHA(时钟相位)用于控制数据在第几个边沿被锁存:CPHA=0,数据在第一个时钟边沿被锁存;CPHA=1,数据在第二个时钟边沿被锁存。时钟相位需要搭配时钟极性使用,才能决定数据在哪个时钟边沿被锁存。

    CPHA=1

     如上图所示,时钟相位为1,即第二个时钟边沿数据被锁存。 

    当时钟极性(CPOL)为1时,CLK空闲电平为高电平,第①个边沿为下降沿,第②个边沿为上升沿。数据在CLK上升沿是被锁存。主设备在时钟为低电平时可给从设备发送数据,在时钟为高电平时读取数据(模拟SPI思路)。

    当时钟极性(CPOL)为0时,CLK空闲电平为低电平,第①个边沿为上升沿,第②个边沿为下降沿。数据在CLK下降沿是被锁存。主设备在时钟为高电平时可给从设备发送数据,在时钟为低电平时读取数据(模拟SPI思路)。

    CPHA=0

     时钟相位为0时,数据在第一个时钟边沿被锁存。

    当时钟极性(CPOL)为1时,CLK空闲电平为高电平,第①个边沿为下降沿。数据在CLK下降沿是被锁存。主设备在时钟为高电平时可给从设备发送数据,在时钟为低电平时读取数据(模拟SPI思路)。

    当时钟极性(CPOL)为0时,CLK空闲电平为低电平,第①个边沿为上升沿。数据在CLK上升沿是被锁存。主设备在时钟为低电平时可给从设备发送数据,在时钟为高电平时读取数据(模拟SPI思路)。

    3.1.2 数据帧格式

    根据SPI_CR1寄存器中的LSBFIRST位,输出数据位时可以MSB(高)在先也可以LSB在先。
    根据SPI_CR1寄存器的DFF位,每个数据帧可以是8位或是16位。所选择的数据帧格式对发送和
    /或接收都有效。

    3.1.3配置为SPI主模式

    寄存器开发模式

     如上图所示,为官方寄存器配置方式,主要是配置波特率用于配置SPI数据传输速率;时钟极性和时钟相位;帧格式(8位或16位,先发高位或先发低位);片选引脚工作模式等。为了减少翻手册查看寄存器时间,本次使用固件库配置SPI。

    API:void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)

    初始化SPI外设。

    SPI_InitTypeDef        初始化结构体

    typedef struct
    {
      uint16_t SPI_Direction;          //设置SPI单向或者双向数据模式
      uint16_t SPI_Mode;               //SPI工作模式
      uint16_t SPI_DataSize;           //SPI数据大小
      uint16_t SPI_CPOL;               //时钟极性
      uint16_t SPI_CPHA;               //时钟相位
      uint16_t SPI_NSS;                //N片选引脚管理方式
      uint16_t SPI_BaudRatePrescaler;  //波特率预分频值
      uint16_t SPI_FirstBit;           //高位先发或者低位先发
      uint16_t SPI_CRCPolynomial;      //CRC计算
    }SPI_InitTypeDef;

    (1)SPI_Direction设置了 SPI 单向或者双向的数据模式。

    SPI_Direction 描述
    SPI_Direction_2Lines_FullDuplex SPI 设置为双线双向全双工
    SPI_Direction_2Lines_RxOnly SPI 设置为双线单向接收
    SPI_Direction_1Line_Rx SPI 设置为单线双向接收
    SPI_Direction_1Line_Tx SPI 设置为单线双向发送

    (2)SPI_Mode 设置了 SPI 工作模式。主SPI(SPI_Mode_Master ),从SPI(SPI_Mode_Slave)。

    (3)SPI_DataSize 设置了 SPI 的数据大小。16位数据帧结构(SPI_DataSize_16b),8位数据帧结构(SPI 发送接收 8 位帧结构)。

    (4)SPI_CPOL设置了空闲时钟电平。空闲时钟高电平(SPI_CPOL_High),空闲时钟低电平(SPI_CPOL_Low)。

    (5)SPI_CPHA设置了捕获的时钟边沿。数据在第一个边沿被捕获(SPI_CPHA_1Edge),数据在第二个边沿被捕获(SPI_CPHA_2Edge)。

    (6)SPI_NSS指定了 NSS 信号由硬件(NSS 管脚)还是软件(使用 SSI 位)管理。NSS由硬件管理(SPI_NSS_Hard),内部 NSS 信号由 软件控制(SPI_NSS_Soft)。

    (7)SPI_BaudRatePrescaler 用来定义波特率预分频的值,这个值用以设置发送和接收的 SCK 时钟。

    (8)SPI_FirstBit 指定了数据传输从 MSB 位还是 LSB 位开始。数据传输从 MSB 位开始(SPI_FisrtBit_MSB),数据传输从 LSB 位开始(SPI_FisrtBit_LSB)。

    (9)SPI_CRCPolynomial 定义了用于 CRC 值计算的多项式。

    3.1.4 SPI初始化配置

    /*
    	引脚连接:
    	PA4	- #CS
    	PA5 - SCK
    	PA6 - MISO
    	PA7 - MOSI
    */
    void W25Q64_InitConfig(void)
    {
    	//1.配置GPIO
    	GPIO_InitTypeDef w25q64_GPIO;
    	w25q64_GPIO.GPIO_Pin = GPIO_Pin_5| GPIO_Pin_6 | GPIO_Pin_7;
    	w25q64_GPIO.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
    	w25q64_GPIO.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&w25q64_GPIO);
    	
    	w25q64_GPIO.GPIO_Pin = GPIO_Pin_4;
    	w25q64_GPIO.GPIO_Mode = GPIO_Mode_Out_PP;//通用推挽输出
    	w25q64_GPIO.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&w25q64_GPIO);
    	
    	W25Q64_CS_H();//取消选择状态
    	
    	//2.SPI配置
    	SPI_I2S_DeInit(SPI1);
    	SPI_InitTypeDef w25q64_SPI;
    	w25q64_SPI.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线双向全双工
    	w25q64_SPI.SPI_Mode = SPI_Mode_Master;		//主机模式
    	w25q64_SPI.SPI_DataSize = SPI_DataSize_8b;	//8位数据模式
    	w25q64_SPI.SPI_CPOL = SPI_CPOL_High;		//时钟极性高
    	w25q64_SPI.SPI_CPHA = SPI_CPHA_2Edge;		//第二个边沿
    	w25q64_SPI.SPI_NSS = SPI_NSS_Soft;			//由软件控制
    	w25q64_SPI.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
    	w25q64_SPI.SPI_FirstBit = SPI_FirstBit_MSB;//高位先发
    	w25q64_SPI.SPI_CRCPolynomial = 7;
    	SPI_Init(SPI1,&w25q64_SPI);
    	//3.配置更新中断
    	//SPI_I2S_ITConfig(SPI1,SPI_I2S_IT_RXNE,ENABLE);
    	//4.使能SPI1
    	SPI_Cmd(SPI1,ENABLE);
    }

    (1)配置GPIO工作模式。CS由IO管脚自行控制,配置为通用推挽输出模式;其他3个管脚由SPI1控制,配置为复用推挽输出模式。

    (2)配置SPI。在配置SPI前,可将片选引脚拉高。

            由于需要读写数据,所以配置为主机模式,双线双向全双工数据模式。

            数据帧格式配置为8为数据高位先发。

            时钟极性和时钟相位可配置为模式(0,3),这里配置为模式3,时钟极性为高,时钟相位为第二边沿捕获。

            片选引脚由软件控制。(硬件控制:SPI控制)

            时钟分频这里选择4分频。其他分频值也可。

    (3)使能SPI1。

    注:外设时钟在main.c中配置。

    3.15 读写数据函数

    uint8_t W25Q64_ReadWriteByte(uint8_t bety)
    {
    	//uint16_t count=4096;
    	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);//等待发送缓存区空
    	SPI_I2S_SendData(SPI1,bety);
    	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);//等待接收缓存区非空
    	return SPI_I2S_ReceiveData(SPI1);
    }
    

     (1)等待发送存储器为空,这里可加延时等待,避免出现程序卡死现象。只有发送寄存器中没有数据了,才发送数据。

    (2)等待接收缓存器中有数据,再接收数据。

    3.2 模拟SPI

            模拟SPI相较于硬件SPI,模拟SPI不需要MCU由SPI这个外设,使用普通IO口就可以模拟SPI时序完成通信。

    /*
    	引脚连接:
    	PA4	- #CS
    	PA5 - SCK
    	PA6 - MISO
    	PA7 - MOSI
    */
    #define W25Q64_SCK_H() GPIO_SetBits(GPIOA,GPIO_Pin_5)
    #define W25Q64_SCK_L() GPIO_ResetBits(GPIOA,GPIO_Pin_5)
    #define W25Q64_OUT_H() GPIO_SetBits(GPIOA,GPIO_Pin_7)
    #define W25Q64_OUT_L() GPIO_ResetBits(GPIOA,GPIO_Pin_7)
    #define W25Q64_IN()	   GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)
    
    uint8_t W25Q64_ReadWriteByte(uint8_t bety)
    {
    	uint8_t i;
    	uint8_t data=0;
    	W25Q64_SCK_L();
    	for(i=0;i<8;i++)
    	{
    		bety & 0x80? W25Q64_OUT_H():W25Q64_OUT_L();
    		bety <<=1;
    		W25Q64_SCK_H();
    		if(W25Q64_IN())
    		{
    			data |= (1<<(7-i));
    		}
    		W25Q64_SCK_L();
    	}
    	return data;
    }
    
    void W25Q64_InitConfig(void)
    {
    	//1.配置GPIO
    	GPIO_InitTypeDef w25q64_GPIO;
    	w25q64_GPIO.GPIO_Pin = GPIO_Pin_4| GPIO_Pin_5 | GPIO_Pin_7;
    	w25q64_GPIO.GPIO_Mode = GPIO_Mode_Out_PP;//复用推挽输出
    	w25q64_GPIO.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&w25q64_GPIO);
    	
    	w25q64_GPIO.GPIO_Pin = GPIO_Pin_6;
    	w25q64_GPIO.GPIO_Mode = GPIO_Mode_IPU;//浮空输入
    	w25q64_GPIO.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA,&w25q64_GPIO);
    	
    	W25Q64_CS_H();//取消选择状态
    }

    (1)配置GPIO工作模式,/CS,CLK,MOSI引脚配置为输出模式,MISO配置为输入模式。

    (2)编写模拟SPI函数。在使用模拟SPI时,选择了模式0,时钟极性为0,时钟相位为0。时钟空闲电平为低电平,上升沿捕获数据。

  • 定义一个无符号字符类型的变量data,用于保存接收到的数据。
  • 拉低时钟管脚。
  • for循环中循环8次,读取或发送一个字节。
  • 上升沿时捕获数据,所以在时钟管脚低电平时输出高电平或低电平,使其在上升沿到来之前保持稳定。这里使用三目运算符获取要发送字节的最高位。并将低以为左移至最高位,便于下一位传输。
  • 拉高时钟管脚。
  • 在时钟管脚为高电平时读取数据。并移位至对应位。
  • 拉低时钟为下一次上升沿到来做准备,也可以在最后以为数据传输完成后保持时钟为高电平。
  • 4. W25Q64

    4.1 读取W25Q64制造/器件号(90H)

            在读写FLASH前,可用读取W25Q64的“制造/器件号”检验SPI读写函数是否可行。

            先把/CS引脚拉低,然后把指令90H通过函数W25Q64_ReadWriteByte发送到芯片,接着把24位地址000000H发送到芯片,然后芯片会把“制造ID”和“器件ID”在DO引脚上升沿发送出去,使用W25Q64_ReadWriteByte函数读取即可。

    读取制造/器件号时序
    uint16_t W25Q64_GetID(void)
    {
    	uint16_t id=0;
    	//发送读制造/器件号指令 0x90
    	W25Q64_CS_L();
    	W25Q64_ReadWriteByte(0x90);
    	//发送24位地址
    	W25Q64_ReadWriteByte(0x00);
    	W25Q64_ReadWriteByte(0x00);
    	W25Q64_ReadWriteByte(0x00);
    	id = W25Q64_ReadWriteByte(0xFF)<<8;	//生产ID
    	id |= W25Q64_ReadWriteByte(0xFF);		//器件ID
    	W25Q64_CS_H();
    	return id;
    }

    (1)拉低片选线。

    (2)发送读取 “制造/器件号”指令90H,发送24位地址000000H。

    (3)读取制造ID,读取器件ID。

    (4)读取完毕,拉高片选。

    制造ID=0xEF,器件ID=0x16。

     错误:在使用硬件SPI时,模式(0,3)读取ID以及收发数据和模拟SPI模式0相同。在使用模拟SPI模式3时,收发数据正常,读取ID就会存在ID号不同的问题。观看本篇文章学习的友友们可以试试模拟SPI模式3看看会不会出现该问题。

    4.2写使能(06H)

            写使能指令将会使状态寄存器的WEL位置位。在执行每个“页编程”“扇区擦除”“块区擦除”“芯片擦除”和“写状态寄存器”指令之前,都要先置位WEL。/CS先拉低,向芯片发送06H指令,然后再拉高/CS引脚。

    //写使能
    void W25Q64_WriteENABLE(void)
    {
    	W25Q64_CS_L();
    	W25Q64_ReadWriteByte(0x06);//写使能
    	W25Q64_CS_H();
    }

    4.2 擦除指令

            在向FLASH中写入数据前必须保证内存空H,而擦除后的扇区位都为1,扇区字节都为FFH。

    4.2.1 扇区擦除(20H)

            扇区擦除指令将一个扇区(16页4096字节)擦除,在执行扇区擦除指令前,需要先执行“写使能”指令,保证WEL位为1。

            先拉低/CS引脚,然后向芯片发送20H指令,接着把24位扇区地址发送到芯片,然后再拉高/CS。如果没有及时拉高/CS,指令将不起作用。在执行指令期间,BUSY位为1。在执行完指令后,BUSY为将复位,WEL位也会复位。

    扇区擦除时序
    /*
    	\brief:	扇区擦除
    	\param:	addr 24位扇区地址
    	\retval:	none
    */
    void W25Q64_SectorErase(uint32_t addr)
    {
    	while(W25Q64_ReadState() & W25Q64_BUSY);//等待忙结束
    	W25Q64_WriteENABLE();//写使能
    	W25Q64_CS_L();
    	W25Q64_ReadWriteByte(0x20);//扇区擦除
    	W25Q64_ReadWriteByte((addr&0xFF0000)>>16);//发送24位地址
    	W25Q64_ReadWriteByte((addr&0xFF00)>>8);
    	W25Q64_ReadWriteByte((addr&0xFF));
    	W25Q64_CS_H();
    }

    (1)在连续擦除扇区时,会存在上一个扇区代码执行结束了但擦除没有结束,所以要等待擦除结束。这里我用来死等,可加入超时时间。

    (2)写使能,确保在擦除扇区前WEL位为1。

    (3)拉低片选。

    (4)发送扇区擦除指令,发送24为扇区地址。

    (5)拉高片选。

    4.2.2 块区擦除(DBH)

    略。(详情请查看W25Q64数据手册,代码请查看附件)

    4.3 读数据(03H)

            “读数据”指令允许读出一个字节或一个以上的字节被读出。先把/CS 引脚拉低,然后把 03h 通过 DIO 引脚送到芯片,之后再送入 24位的地址,这些数据在 CLK 的上升沿被芯片采集。芯片接收完 24 位地址之后,就会把相应地址的数据在 CLK 引脚的下降沿从 DO 引脚送出去,高位在前当读完这个地址的数据之后,地址自动增加,然后通过 DO 引脚把下一个地址的数据送出去,形成一个数据流。也就是说,只要时钟在工作,通过一条读指令,就可以把整个芯片存储区的数据读出来。把/CS 引脚拉高,“读数据”指令结束。当芯片在执行编程、擦除和读状态寄存器指令的周期内,“读数提”指令不起作用。

    读数据时序
    /*
    	\brief:	读数据
    	\param:	addr:要读取的地址
    				data:保存读取的数据
    				size:读取的字节数(数据长度)
    	\retval:	none
    */
    void W25Q64_ReadData(uint32_t addr,uint8_t *data,uint16_t size)
    {
    	if(addr+size > W25Q64_END_ADDR) 
    	{
    		return ;
    	}
    	uint8_t *pData=data;
    	while(W25Q64_ReadState() & W25Q64_BUSY);//等待忙结束
    	W25Q64_CS_L();
    	W25Q64_ReadWriteByte(0x03);//读数据指令
    	W25Q64_ReadWriteByte((addr&0xFF0000)>>16);//发送24位地址
    	W25Q64_ReadWriteByte((addr&0xFF00)>>8);
    	W25Q64_ReadWriteByte((addr&0xFF));
    	while(size--)
    	{
    		*pData=W25Q64_ReadWriteByte(0xFF);//保存数据
    		pData++;
    	}
    	W25Q64_CS_H();
    }

    (1)判断地址是否操作FLASH最大地址范围。

    (2)等待忙结束。

    (3)拉低片选。

    (4)发送读数据指令03H,发送要读取数据的起始地址。

    (5)连续读取数据。这里设置data的大小为一个扇区,只能读取一个扇区数据。

    (6)拉高片选。

    4.4 页编程(02H)

            执行“页编程”指令之前,需要先执行“写使能”指令,而且要求待写入的区域位都为1,也就是需要先把待写入的区域擦除。先把/CS 引脚拉低,然后把代码 2h 通过 DIO 引送到芯片,然后再把 24 位地址送到芯片,然后接着送要写的字节到芯片。在写完数据之后,把/CS 引脚拉高。
            写完一页 (256 个字节)之后,必须把地址改为 0,不然的话,如果时钟还在继续,地址将自动变为页的开始地址。在某些时候,需要写入的字节不足 256 个字节的话,其它写入的字节都是无意义的。如果写入的字节大于了 256 个字节,多余的字节将会加上无用的字节覆盖刚刚写入的 256 个字节。所以需要保证写入的字节小于等于 256 个字节
            在指令执行过程中,用“读状态寄存器”可以发现 BUSY 位为 1,当指令执行完毕,BUSY位自动变为 0。如果需要写入的地址处于“写保护”状态,“页编程”指令无效。

    页编程时序

             在编程时,一次最多只能写一页的数据,发送编程指令在发送要写入的地址后,直接发送要写入的数据,地址会自动偏移。在编写页编程函数时,不考虑写入的数据是否超过一页而发生数据覆盖,就默认写入的字节在一页以内。

    //页编程
    void W25Q64_PageWrite(uint32_t addr,uint8_t *data,uint16_t size)
    {
    	if(addr+size > W25Q64_END_ADDR) 
    		return ;
    	uint8_t *pData = data;
    	while(W25Q64_ReadState() & W25Q64_BUSY);//等待忙结束
    	W25Q64_WriteENABLE();//写使能
    	W25Q64_CS_L();
    	//while(!(W25Q64_ReadState() & W25Q64_WEL));//等待写使能完成
    	W25Q64_ReadWriteByte(0x02);//页写指令
    	W25Q64_ReadWriteByte((addr&0xFF0000)>>16);//发送24位地址
    	W25Q64_ReadWriteByte((addr&0xFF00)>>8);
    	W25Q64_ReadWriteByte((addr&0xFF));
    	while(size--)
    	{
    		W25Q64_ReadWriteByte(*pData);
    		pData++;
    	}
    	W25Q64_CS_H();
    }

    (1)判断要写入数据的地址是否超过FLASH地址范围。

    (2)等待忙结束,这个函数没有“擦除”,但具体调用函数前可能调用了“擦除函数”,所以要等待擦除完成。否则以下命令皆无效。

    (3)写使能。在执行页编程前先执行写使能。

    (4)拉低片选。

    (5)发送页编程指令,接着发送24位FLASH地址。

    (6)依次写入要写入的数据。

    (7)拉高片选。页编程结束。

    4.4.1 跨页写函数(不考虑擦除)

            在向W25Q64写入数据时,只支持页写操作,如果要写入更多的数据只能分一页一页依次写到FLASH中,不方便操作。W25Q64没有跨页写功能,但是可以一页一页依次写入,根据这个思路,可封装跨页写函数。

    /*
    	\brief:	可跨页写数据(不考虑擦除,认为写入的地址都为0xFF)
    	\param:	addr:要写入的地址
    				data:写入的数据
    				size:数据的数量(字节数)
    	\retval:	none
    */
    void W25Q64_StepOverPageWrite(uint32_t addr,uint8_t *data,uint32_t size)
    {
    	uint32_t addr_remain= 256 - addr%256;//当前页地址剩余
    	uint8_t *pData=data;
    	if(size <= addr_remain)
    	{
    		addr_remain = size;
    	}
    	while(1)
    	{
    		W25Q64_PageWrite(addr,pData,addr_remain);
    		if(addr_remain == size) break;		//数据全部写入
    		pData += addr_remain;	//数据地址偏移
    		addr += addr_remain;	//地址偏移
    		size -= addr_remain;	//计算剩余数据
    		addr_remain = 256;//写入一页数据
    		if(size <= addr_remain)	//计算当前页是否够写入剩余数据
    		{
    			addr_remain = size;
    		}
    	}
    }

    (1)定义一个变量基于记录当前页剩余地址,计算当前页剩余地址。例如:0x00000F为要写入数据的起始地址。0x000000~0x0000FF为第一页(256字节),地址从0x00000F开始到0x0000FF,还有241个字节地址空间。256-15%256=241。

    (2)判断要写入的数据是否超过当前页剩余空间,没有超过就意味着当前页够写要写入的数据量,就另addr_remain等于要写入的数据量。addr_remain变量在当前函数中作用很大,如果要写书的数据等于addr_remain,就意味着数据全部写入完成。

    (3)在当前页地址写入addr_remain个字节数据。addr_remain最大256字节,不会造成数据覆盖,所以在页编程函数中没有加判断。

    (4)如果addr_remain等于要写入的数据表示数据已经写入完成。因为在第(2)步做了判断,只有要写入的数据小于当前页剩余量,才会相等。不相等的情况是要写入的数据当前页剩余地址空间写不满,需要跨页。

    (5)数据未全部写入,已经写了addr_remain字节,数据指针偏移addr_remain,FLASH地址偏移addr_remain(到了下一页起始地址),要写入的数据量减少addr_remain。

    (6)计算当前页剩余空间(其实就是一整页256字节)。0x00000F+241=0x000100(0x100=256),256%256=0,256-0=256。

    (7)判断当前页是否够写剩下的数据,如果剩余数据量超过一页,那么就只能写入一页数据,addr_remain = 256。当前页够写,则写下剩余数据。

    (8)以此反复直至数据写完。

    4.4.2 跨页写(考虑擦除和原有数据)

            在跨页写时,都没有考虑地址是否可写入数据,接下来考虑擦除问题。用的擦除函数为扇区擦除(4096字节),流程和跨页写相似。扇区擦除会把当前扇区的存储位都置一,会把原有数据都给清掉,所以在擦除函数前应该做好数据备份。

    第二个扇区

             如图所示为第二个扇区的地址,共有16页,在写数据时,如果当前扇区的前2页都含有有用数据,现要在当前扇区的第3页写入一页数据。在擦除扇区时,该页将被全部擦除(包括有效数据),所以需要将该页的数据保存起来,最后一并写入。当然,如果不需要擦除,就直接写到第二页中。

    /*
    	\brief:	可跨页写数据(考虑擦除和原有数据)
    	\param:	addr:要写入的地址
    				data:写入的数据
    				size:数据的数量(字节数)
    	\retval:	none
    */
    
    uint8_t sector_data[W25Q64_SECTOR_SIZE];
    void W25Q64_WriteData(uint32_t addr,uint8_t *data,uint32_t size)
    {
    	uint16_t sector_offset = addr%4096;	//计算当前扇区的地址偏移
    	uint16_t sector_remain = 4096 - sector_offset;	//计算当前扇区剩余
    	uint32_t sector_addr = addr - sector_offset;	//计算当前扇区的起始地址
    	uint8_t *pData=data;
    	uint32_t i;
    	if(size <= sector_remain)
    	{
    		sector_remain=(uint16_t )size;
    	}
    	while(1)
    	{
    		W25Q64_ReadData(addr,sector_data,sector_remain);//读取要写入地址的数据
    		for(i=0;i<sector_remain;i++)
    		{
    			if(sector_data[i]!=0xFF) break;
    		}
    		if(i!=sector_remain)//判断是否需要擦除扇区
    		{
    			//擦除前保存当前扇区前一段数据
    			W25Q64_ReadData(sector_addr,sector_data,sector_offset);
    			//擦除前保存当前扇区后一段数据
    			W25Q64_ReadData(addr + sector_remain,sector_data+(sector_offset+sector_remain),W25Q64_SECTOR_SIZE - (sector_offset+sector_remain));
    			W25Q64_SectorErase(sector_addr);//擦除扇区
    			//将要写入的数据插入缓冲区
    			for(i=0;i<sector_remain;i++)
    			{
    				sector_data[sector_offset+i]= pData[i];
    			}
    			W25Q64_StepOverPageWrite(sector_addr,sector_data,W25Q64_SECTOR_SIZE);
    			sector_offset = 0;
    		}
    		else
    		{
    			W25Q64_StepOverPageWrite(addr,pData,sector_remain);//向当前扇区写入数据
    		}
    		if(sector_remain == size) break;//全部数据完全写入
    		
    		pData += sector_remain;	//数据地址偏移
    		addr += sector_remain;	//flash地址偏移
    		sector_addr = addr;		//当前扇区起始地址
    		size -= sector_remain;	//数据量减少
    		sector_remain = W25Q64_SECTOR_SIZE;//当前扇区剩余
    		if(size <= W25Q64_SECTOR_SIZE)//计算当前扇区是否够写入剩余数据
    		{
    			sector_remain = size;
    		}
    	}
    }
    

    例:从地址0x001300写入4096字节数据。

    (1)计算要写入的起始地址在当前上去中的偏移量(768),扇区剩余空间(3328)和当前扇区的起始地址(0x001000)。

    (2)判断当前扇区是否够写入4096字节数据。显然不够的。

    (3)读取0x001300~0x001FFF的(3328字节)数据并判断含有不为0xFF的。

    (4)不需要擦除,直接在0x001300地址依次写入3328字节数据。

    (5)需要擦除,读取前半段地址(0x001000~0x000FFF)数据并保持至缓存区,以及后半段数据(因为这里需要写满该扇区,所以后半段没有数据需要保存)。再将3328字节数保存至缓存区前半段数据后。

    (6)数据没有写完,还有768字节(4096-3328)。

    (7)偏移数据地址和flash地址,当前扇区地址(变为0x002000),数据量减少3328字节,当前页剩余4096字节空间。

    (8)当前页够写剩下的数据,另rector_remain=size。再回到(2)。

            调用该函数,就可以直接在flash中跨页写入数据且自带擦除功能,可以在多次写数据时不会因为扇区擦除而导致数据丢失。

    5. 附件

    链接:https://pan.baidu.com/s/1GWecV_hZuDM6DFUpf77Yxg?pwd=1234 
    提取码:1234

    6. 实际测试

            这里选择在0x2000000地址两侧分别写入数据,因为0x200000是下一个扇区的起始地址(也可以选择其他的两个扇区),0x200000-10是上一个扇区的末端。在上一个扇区0x200000-10地址处连续写入5字节数据,再从0x200000-5地址处跨扇区写入25字节数据,共写入30字节数据。

            可看到读取的前五的字节数据“abcde”和第一次写入的数据相同,并没有因为第二次写数据而被清除掉,第二次写数据也能实现跨页写数据。 

    运用:

    使用W25Q64保存字库,并在LCD中调用。

    【STM32篇】LCD显示汉字(从W25Q64中读取GBK字库)

    2023/07/19

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【STM32教程】使用硬件SPI和模拟SPI驱动W25Q64芯片的SPI时序

    发表评论