LV16-20-SPI-02-读写外部SPI-FLASH

本文主要是使用STM32的SPI通信协议读写外部SPI FLASH W25Q128的一些相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

一、W25Q128简介

1. 简介

EEPROM和Flash的本质上是一样的, 都用于保存数据,Flash包括MCU内部的Flash和外部扩展的Flash,我使用的开发板的W25Q128就是一个SPI接口的外部NOR Flash。从功能上, Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。 从结构上, Flash按扇区操作, EEPROM通常按字节操作。

W25Q128是华邦的一款SPI接口 NOR FLASH 芯片,我看我使用的开发板上使用的型号是W25Q128JV,我们可以在这里找到它的芯片手册:W25Q128JV-datasheet

2. 结构组成

Flash类型众多,其中比较常见是W25Qxx系列, 从命名上看, W25Qxx中xx的单位是M Bit,如W25Q16,其存储容量为16M Bit。本开发板上的Flash型号为W25Q128, 其存储容量为128M Bit。W25Q12将16MB的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。每个扇区又可以分为16页,每一页256字节。这样划分主要是因为FLASH的存储特性。

image-20230507192414828

W25Qxx的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。这样我们需要给W25Qxx开辟一个至少4K的缓存区,这样对SRAM要求比较高,要求芯片必须有4K以上SRAM才能很好的操作。

3. 存储特性

Flash有个物理特性:只能写0,不能写1。如果把Flash的每个Bit,都看作一张纸, bit=1表示纸没有内容,bit=0表示纸写入了内容。 当纸为白纸时(bit=1),这时往纸上写东西是可以的,写完后纸的状态变为bit=0。当纸有内容时( bit=0 ),这时往纸上写东西只能让数据越乱,也就无法正常写数据。此时需要橡皮檫,进行擦除操作,将有内容的纸( bit=0 )变为白纸( bit=1 ),使得以后可以重新写入数据。 所以总的来说,FLASH存储特性有以下几点:

(1)在写入数据前必须擦除;

(2)擦除时会把所有的数据位置1;

(3)写入数据时只能把数据从1改成0;

(4)擦除时必须按照最小单位来擦除,对于FLASH我们没法一个字节一个字节擦除,最小单位擦除单位是一个扇区,对于此芯片来说就是4KB。一般来讲FLASH擦除的最小单位都是扇区。写入的时候没有限制,可以一个字节一个字节的写入。

【说明】

  • NOR FLASH 可以按字节写入数据。

  • NAND FLASH必须按照扇区或者块进行读写数据,SD卡、SSD硬盘等。

4. 引脚说明

我们可以看芯片手册的W25Q128JV-datasheet的 3.3 Pin Description SOIC 208-mil, WSON 6x5-mm / 8x6-mm 和 4. PIN DESCRIPTIONS两节的内容

image-20230507192941513

【注意】带有斜杠的引脚名表示低电平有效。

5. 通信速度

我们看芯片手册的开头,有这么几句:

SPI clock frequencies of W25Q128JV of up to 133MHz are supported allowing equivalent clock rates of 266MHz (133MHz x 2) for Dual I/O and 532MHz (133MHz x 4) for Quad I/O when using the Fast Read Dual/Quad I/O. These transfer rates can outperform standard Asynchronous 8 and 16-bit Parallel Flash memories

也就是说,这个SPI FLASH通信时支持的最高速率可以达到133MHZ,这就意味着,我们使用STM32F103ZET6来控制这个芯片时,SPI的时钟直接配置到最高36M也是没有任何问题的。

6. 状态和控制寄存器

状态和控制寄存器的相关描述我们可以看 W25Q128JV-datasheet的7.1 Status Registers,需要注意的是不仅仅是下边这一小部分,还挺长的,这里只是说明一下去哪找资料。

image-20230507200233319

当我们擦除扇区或者写入数据的时候我们怎么知道已经完成相关操作了呢?W25Q128为我们提供了一个状态寄存器来表示各个状态,我们可以通过读取相关标志来判断内部操作是否已经完成了。

7. 支持的命令

W25Q128的寄存器是在芯片内部,那我们的STM32怎么读取呢?我们可以通过SPI总线,向W25Q128芯片发送一些特定的命令来读取对应的寄存器的数据。命令在哪里?我们可以参考 W25Q128JV-datasheet 的 8.1 Device ID and Instruction Set Tables一节:

image-20230507201400133

比如说这里就有读取状态寄存器的命令。注意这张表里边,Byte1表示命令的编码,Byte2~Byte7中,带有 () 的,表示芯片返回的数据,不带()的,表示STM32写命令的时候向W25Q128传输的数据。这些在表格的最下方都有说明。比如说上边的读取状态寄存器的命令,Byte1的05h表示要读取状态寄存器,一般都是由STM32发送给W25Q128,然后W25Q128收到命令代码之后就知道是STM32主机想要读取状态寄存器的值,于是便将状态寄存器数据放在Byte2通过SPI协议返回给STM32。

8. 地址位数?

这个芯片分了这么多的块,扇区,页,我们想要将数据写入到某个地址中的话,应该给多少位地址?这个嘛,我们其实可以来看一下框图:

image-20230507204248563

左边是每个块内的地址,右边是每个块内部的每个扇区的地址范围,我们发现地址都是24位的,为什么是24位?我们一共是128Mbit,一共就是16M的内存空间,24位地址最多能表示多少空间?

1
2^24 = 2^14 * 2^10 = 2^10 * 2^10 * 2^4 = 2^4MB = 16MB

刚好够表示16M的地址空间,所以后边我们发送地址的时候只需要24位就可以啦。

二、读写数据

1. 读取状态寄存器

参考 W25Q128JV-datasheet 的 8.2.4 Read Status Register-1 (05h), Status Register-2 (35h) & Status Register-3 (15h):

image-20230507202117478

2. 擦除扇区

image-20230507202629656

其中Byte2~Byte4一共24位(3个字节),表示要擦除的地址。时序可以参考 W25Q128JV-datasheet 的8.2.15 Sector Erase (20h) ,这条命令将会每次擦除一个扇区的数据,也就是4KB数据。

image-20230507202413239

3. 写入数据

image-20230507203050335

其中Byte2Byte4一共24位(3个字节),表示要写入的地址。Byte5Byte6表示要写入的数据。时序可以参考 W25Q128JV-datasheet 的 8.2.13 Page Program (02h) :

image-20230507203139132

4. 读取数据

image-20230507203432851

其中Byte2~Byte4一共24位(3个字节),表示要读取的地址。Byte5表示W25Q128返回的的数据。时序可以参考 W25Q128JV-datasheet 的8.2.6 Read Data (03h):

image-20230507203540042

三、读写实例

通过SPI读写外部FLASH的一般步骤如下:

(1)初始化通讯使用的目标引脚及端口时钟;

(2)使能 SPI 外设的时钟;

(3)配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;

(4)编写基本 SPI 按字节收发的函数;

(5)编写对 FLASH 擦除及读写操作的的函数;

(6)编写测试程序,对读写数据进行校验。

1. 硬件设计

image-20230507205517724

这里我们选择了PB12作为NSS引脚,它刚好就是SPI2外设的SPI2_NSS。

2. 物理SPI读写FLASH

2.1 STM32CubeMX配置

2.1.1 打开SPI2

image-20230507210048082

2.1.2 NSS引脚

image-20230507210419381

我们前边已经说过了,这个NSS引脚可以是随便一个IO,由于这里刚好接在了SPI2的PB12引脚上,所以这里我们可以选择使用外设硬件控制NSS信号,还是使用软件控制,官方建议是自己使用软件控制,自己控制的话只需要将引脚配置成推挽输出,然后自己决定高低电平即可,所以我们这里还是使用软件控制,注意记得将引脚初始化。

我们进入GPIO配置界面,将PB12进行初始化:

image-20230507210922311

注意当开始传输数据时,引脚输出低电平,所以平时就输出高电平就可以了,需要使用的时候拉低即可。

2.1.3 SPI配置

image-20230507212600204

基本保持默认就可以了,关注一下SCK的频率,注意不要超过SPI FLASH的最大频率。

2.2 宏定义

我们需要定义几个宏,,方便使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define W25Q128	0XEF17                   // 器件ID
#define W25QXX_CS PBout(12) // W25QXX的片选信号

//指令表
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg1 0x05
#define W25X_ReadStatusReg2 0x35
#define W25X_ReadStatusReg3 0x15
#define W25X_WriteStatusReg1 0x01
#define W25X_WriteStatusReg2 0x31
#define W25X_WriteStatusReg3 0x11
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define W25X_Enable4ByteAddr 0xB7
#define W25X_Exit4ByteAddr 0xE9

2.3 SPI读写一个字节数据

1
2
3
4
5
6
u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 Rxdata;
HAL_SPI_TransmitReceive(&SPI2_Handler, &TxData, &Rxdata, 1, 1000);
return Rxdata; //返回收到的数据
}

SPI通信的时候可以一边读一边写,所以这里我们可以直接使用 HAL_SPI_TransmitReceive 函数将 TxData 数据写入SPI FLASH,同时获取到返回的数据并存放于 Rxdata 中。

2.4 写使能与禁止

在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能

2.4.1 写使能

1
2
3
4
5
6
7
8
9
//W25QXX写使能	
//将WEL置位
void W25QXX_Write_Enable(void)
{
W25QXX_CS=0; //使能器件
SPI2_ReadWriteByte(W25X_WriteEnable); //发送写使能
W25QXX_CS=1; //取消片选
}

2.4.2 写禁止

1
2
3
4
5
6
7
8
//W25QXX写禁止	
//将WEL清零
void W25QXX_Write_Disable(void)
{
W25QXX_CS=0; //使能器件
SPI2_ReadWriteByte(W25X_WriteDisable); //发送写禁止指令
W25QXX_CS=1; //取消片选
}

2.5 读写状态寄存器

2.5.1 状态寄存器

与EEPROM 一样,由于 FLASH 芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲”时才能进行再次写入。为了表示自己的工作状态, FLASH 芯片定义了几个状态寄存器 :

(1)状态寄存器1

1
2
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

(2)状态寄存器2

1
2
BIT7   6   5   4   3   2   1    0
SUS CMP LB3 LB2 LB1 (R) QE SRP1

(3)状态寄存器3:

1
2
BIT7      6     5    4   3   2   1   0
HOLD/RST DRV1 DRV0 (R) (R) WPS ADP ADS

读取状态寄存器的时序如下:

image-20230508231720539

2.5.2 读状态寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//读取W25QXX的状态寄存器,W25QXX一共有3个状态寄存器
//regno:状态寄存器号,范:1~3
//返回值:状态寄存器值
uint8_t W25QXX_ReadSR(uint8_t regno)
{
uint8_t byte = 0, command = 0;
switch(regno)
{
case 1:
command = W25X_ReadStatusReg1; //读状态寄存器1指令
break;
case 2:
command = W25X_ReadStatusReg2; //读状态寄存器2指令
break;
case 3:
command = W25X_ReadStatusReg3; //读状态寄存器3指令
break;
default:
command = W25X_ReadStatusReg1;
break;
}
W25QXX_CS = 0; //使能器件
SPI2_ReadWriteByte(command); //发送读取状态寄存器命令
byte = SPI2_ReadWriteByte(0Xff); //读取一个字节
W25QXX_CS = 1; //取消片选
return byte;
}

只要向 FLASH 芯片发送了读状态寄存器的指令, FLASH 芯片就会向主机返回最新的状态寄存器内容。

2.5.2 写状态寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//写W25QXX状态寄存器
void W25QXX_Write_SR(uint8_t regno, uint8_t sr)
{
uint8_t command=0;
switch(regno)
{
case 1:
command=W25X_WriteStatusReg1; //写状态寄存器1指令
break;
case 2:
command=W25X_WriteStatusReg2; //写状态寄存器2指令
break;
case 3:
command=W25X_WriteStatusReg3; //写状态寄存器3指令
break;
default:
command=W25X_WriteStatusReg1;
break;
}
W25QXX_CS=0; //使能器件
SPI2_ReadWriteByte(command); //发送写取状态寄存器命令
SPI2_ReadWriteByte(sr); //写入一个字节
W25QXX_CS=1; //取消片选
}

2.5.3 等待空闲

1
2
3
4
5
//等待空闲
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}

2.6 读取FLASH芯片ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//读取芯片ID
//返回值如下:
//0XEF17,表示芯片型号为W25Q128
uint16_t W25QXX_ReadID(void)
{
uint16_t 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;
}

注意,这一步可以用来判断我们的SPI FLASH是否是功能正常,也可以确定型号。

2.7 扇区擦除

由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵,在要存储数据“0”时,才更改该位。

通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如这里使用的SPI FLASH 芯片W25Q128支持“扇区擦除”、“块擦除”以及“整片擦除”:

擦除单位 大小
扇区擦除 Sector Erase 4KB
块擦除 Block Erase 64KB
整片擦除 Chip Erase 整个芯片完全擦除

FLASH 芯片的最小擦除单位为扇区 (Sector),而一个块 (Block) 包含 16 个扇区。使用扇区擦除指令“Sector Erase”可控制 FLASH 芯片开始擦写 。扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕 。

扇区擦除时序如下:

image-20230507202413239

扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//擦除一个扇区
//Dst_Addr:扇区地址 根据实际容量设置
//擦除一个扇区的最少时间:150ms
void W25QXX_Erase_Sector(uint32_t Dst_Addr)
{
//监视falsh擦除情况,测试用
//printf("fe:%x\r\n",Dst_Addr);
Dst_Addr *= 4096;
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
W25QXX_CS = 0; //使能器件
SPI2_ReadWriteByte(W25X_SectorErase); //发送扇区擦除指令
if(W25QXX_TYPE == W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((Dst_Addr) >> 24));
}
SPI2_ReadWriteByte((uint8_t)((Dst_Addr) >> 16)); //发送24bit地址
SPI2_ReadWriteByte((uint8_t)((Dst_Addr) >> 8));
SPI2_ReadWriteByte((uint8_t)Dst_Addr);
W25QXX_CS = 1; //取消片选
W25QXX_Wait_Busy(); //等待擦除完成
}

调用扇区擦除指令时注意输入的地址要对齐到 4KB

2.8 写入数据

2.8.1 按页写入

目标扇区被擦除完毕后,就可以向它写入数据了。与 EEPROM 类似, FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256 个字节的数据,我们把这个单位为页大小。 页写入时序如下图:

image-20230508231904323

从时序图可知,第 1 个字节为“页写入指令”编码, 2-4 字节为要写入的“地址 A”,接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始,按顺序写入到FLASH 的存储矩阵。若发送的数据超出 256 个,则会覆盖前面发送的数据。与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储单元是擦除状态即可 (即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后,发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址 (x+200)”开始写入 200 个字节也是没有问题的 (小于 256 均可)。只是在实际应用中由于基本擦除单元是 4KB,一般都以扇区为单位进行读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t i;
W25QXX_Write_Enable(); //SET WEL
W25QXX_CS = 0; //使能器件
SPI2_ReadWriteByte(W25X_PageProgram); //发送写页命令
if(W25QXX_TYPE == W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((WriteAddr) >> 24));
}
SPI2_ReadWriteByte((uint8_t)((WriteAddr) >> 16)); //发送24bit地址
SPI2_ReadWriteByte((uint8_t)((WriteAddr) >> 8));
SPI2_ReadWriteByte((uint8_t)WriteAddr);
for(i = 0; i < NumByteToWrite; i++)SPI2_ReadWriteByte(pBuffer[i]); //循环写入
W25QXX_CS = 1; //取消片选
W25QXX_Wait_Busy(); //等待写入结束
}

2.8.2 无校验写入数据

必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!具有自动换页功能,在指定地址开始写入指定长度的数据,但是要确保地址不越界!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//无检验写SPI FLASH
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t pageremain;
pageremain = 256 - WriteAddr % 256; //单页剩余的字节数
if(NumByteToWrite <= pageremain)pageremain = NumByteToWrite; //不大于256个字节
while(1)
{
W25QXX_Write_Page(pBuffer, WriteAddr, pageremain);
if(NumByteToWrite == pageremain)break; //写入结束了
else //NumByteToWrite>pageremain
{
pBuffer += pageremain;
WriteAddr += pageremain;

NumByteToWrite -= pageremain; //减去已经写入了的字节数
if(NumByteToWrite > 256)pageremain = 256; //一次可以写入256个字节
else pageremain = NumByteToWrite; //不够256个字节了
}
};
}

2.8.3 写入指定字节数据

该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。

(1)先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移。

(2)判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不需要,则直接写入数据即可,如果要擦除则读出整个扇区,在对应的偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。

(3)当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。 这里我们定义了一个 W25QXX_BUFFER 的全局变量,用于擦除时缓存扇区内的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//写SPI FLASH
//在指定地址开始写入指定长度的数据,该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
uint8_t W25QXX_BUFFER[4096];
void W25QXX_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t * W25QXX_BUF;
W25QXX_BUF = W25QXX_BUFFER;
secpos = WriteAddr / W25QXX_SECTOR_SIZE; // 扇区地址
secoff = WriteAddr % W25QXX_SECTOR_SIZE; // 在扇区内的偏移
secremain = W25QXX_SECTOR_SIZE - secoff; // 扇区剩余空间大小
//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite <= secremain)
secremain = NumByteToWrite; //不大于4096个字节
while(1)
{
W25QXX_Read(W25QXX_BUF, secpos * W25QXX_SECTOR_SIZE, W25QXX_SECTOR_SIZE); //读出整个扇区的内容
for(i = 0; i < secremain; i++) //校验数据
{
if(W25QXX_BUF[secoff + i] != 0XFF)break; //需要擦除
}
if(i < secremain) //需要擦除
{
W25QXX_Erase_Sector(secpos);//擦除这个扇区
for(i = 0; i < secremain; i++) //复制
{
W25QXX_BUF[i + secoff] = pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF, secpos * W25QXX_SECTOR_SIZE, W25QXX_SECTOR_SIZE); //写入整个扇区

}
else
W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain); //写已经擦除了的,直接写入扇区剩余区间.
if(NumByteToWrite == secremain)
break; //写入结束了
else//写入未结束
{
secpos++;//扇区地址增1
secoff = 0; //偏移位置为0

pBuffer += secremain; //指针偏移
WriteAddr += secremain; //写地址偏移
NumByteToWrite -= secremain; //字节数递减
if(NumByteToWrite > W25QXX_SECTOR_SIZE)
secremain = W25QXX_SECTOR_SIZE; //下一个扇区还是写不完
else
secremain = NumByteToWrite; //下一个扇区可以写完了
}
}
}

2.9 读取数据

相对于写入, FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可 ,对应的时序图如下:

image-20230509070328475
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
uint16_t i;
W25QXX_CS = 0; //使能器件
SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令
if(W25QXX_TYPE == W25Q256) //如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 24));
}
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 16)); //发送24bit地址
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 8));
SPI2_ReadWriteByte((uint8_t)ReadAddr);
for(i = 0; i < NumByteToRead; i++)
{
pBuffer[i] = SPI2_ReadWriteByte(0XFF); //循环读数
}
W25QXX_CS = 1;
}

发送了指令编码及要读的起始地址后, FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯, FLASH 芯片就会一直返回数据。 由于读取的数据量没有限制,所以发送读命令后一直接收 NumByteToRead个数据到结束即可。

2.10 测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const uint8_t TEXT_Buffer[]={"WarShipSTM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)

void W25QXX_Test(void)
{
uint32_t FLASH_SIZE = 0;
uint8_t datatemp[SIZE] = {0};

W25QXX_TYPE = W25QXX_ReadID();
printf("SPI FLASH ID=%#x\r\n", W25QXX_TYPE);
FLASH_SIZE= 16 * 1024 * 1024;
printf("write to SPI FLASH data=%s\r\n", TEXT_Buffer);
W25QXX_Write((uint8_t*)TEXT_Buffer, FLASH_SIZE - 100, SIZE); // 从倒数第100个地址处开始,写入SIZE长度的数据
W25QXX_Read(datatemp, FLASH_SIZE - 100, SIZE); // 从倒数第100个地址处开始,读出SIZE个字节
printf("read from SPI FLASH data=%s\r\n\r\n", datatemp);
}

3. 模拟SPI读写FLASH

这个这里我就没有去测试了,模拟的过程可以看《通信协议-04-SPI通信》这一节的笔记,在笔记中实现了SPI的通信协议,并且实现了读写字节的函数,再配合上边的读写函数就可以对SPI FLASH进行读写