由于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℃工作。
封装
引脚描述
引脚编号 | 引脚名称 | 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用户手册。
使用MCU硬件SPI时,需要选择合适的IO口,并使能AFIO外设。
通常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,数据在第二个时钟边沿被锁存。时钟相位需要搭配时钟极性使用,才能决定数据在哪个时钟边沿被锁存。
如上图所示,时钟相位为1,即第二个时钟边沿数据被锁存。
当时钟极性(CPOL)为1时,CLK空闲电平为高电平,第①个边沿为下降沿,第②个边沿为上升沿。数据在CLK上升沿是被锁存。主设备在时钟为低电平时可给从设备发送数据,在时钟为高电平时读取数据(模拟SPI思路)。
当时钟极性(CPOL)为0时,CLK空闲电平为低电平,第①个边沿为上升沿,第②个边沿为下降沿。数据在CLK下降沿是被锁存。主设备在时钟为高电平时可给从设备发送数据,在时钟为低电平时读取数据(模拟SPI思路)。
时钟相位为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