目录
SPI控制逻辑部分:SPICommand &Control Logic
SPI高电压生成器High Voltage Generators
(11.2.1)Manufacturer and Device Identification 制造商和设备标识
(11.2.2) Instruction Set Table SPI 指令集 (常用标记)
4.最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入
SPI通信详解可以看我的这篇博客:SPI通信详解-CSDN博客
STM32硬件SPI通信详解可以看我的这篇博客:STM32通过SPI硬件读写W25Q64-CSDN博客
STM32通过SPI软件读写W25Q64
1. W25Q64简介
-
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
-
存储介质:Nor Flash(闪存)
-
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI) 可以在只发或者只收的时候,把不用的引脚(比如写保护、或者不用的发送、接收引脚)暂时当做数据传输用的。(这里只做了解)
-
支持SPI模式0和模式3
-
存储容量(24位地址):
- W25Q40: 4Mbit / 512KByte
- W25Q80: 8Mbit / 1MByte
- W25Q16: 16Mbit / 2MByte
- W25Q32: 32Mbit / 4MByte
- W25Q64: 64Mbit / 8MByte
- W25Q128: 128Mbit / 16MByte
- W25Q256: 256Mbit / 32MByte
2. W25Q64硬件电路图
这里的小括号IO1、2、3、4就是介绍时所说的双重SPI和四重SPI
这个电路图没啥好说的
引脚 | 功能 |
---|---|
VCC、GND | 电源(2.7~3.6V) |
CS(SS) | SPI片选 |
CLK(SCK) | SPI时钟 |
DI(MOSI) | SPI主机输出从机输入 |
DO(MISO) | SPI主机输入从机输出 |
WP | 写保护 |
HOLD | 数据保持 |
3. W25Q64芯片框图
先看右边:存储部分
- W25Q64的地址宽度是24位。也就是三个字节。
- 右边这一整块是所有的存储器。存储器以字节为单位。每个字节都有一个对应的地址,左下角为起始地址,右上角为结束地址
- 右上角的最大地址为7FFFFFh(因为W25Q64的芯片最大容量为8M,只用了一半的地址空间)
- 整个存储器分为128块(块0~块7F),以64KB为一块。
- 两个例子↓
- 块0 起始地址:000000 结束地址:00FFFF
- 块15 起始地址:0F0000 结束地址:0FFFFF
- 一个块又分为16个扇区(扇区0~扇区F),以4KB为一扇区。
- 两个例子↓
- 块0 ,扇区2 起始地址:002000 结束地址:002FFF
- 块15 , 扇区14 起始地址:0FE000 结束地址:0FEFFF
- 一个扇区又分为16页(页0~页F),以256字节为一页
- 两个例子↓
- 块0 ,扇区2 ,页12 起始地址:002C00 结束地址:002CFF
- 块15 , 扇区14 ,页3 起始地址:0FE300 结束地址:0FE3FF
SPI控制逻辑部分:SPICommand &Control Logic
芯片内部可以进行地址锁存、数据读写等操作。引出的引脚是我们用来操作的。
SPI状态寄存器 StatusRegister
检测芯片是否处于忙状态、是否写使能、是否写保护等等。都是在状态寄存器中体现
SPI写控制逻辑 Write Control Logic
与外部WP引脚相连。可以用来进行写保护
SPI高电压生成器High Voltage Generators
配合Flash进行编程用的。为了使芯片掉电不丢失
SPI页地址锁存/计数器、字节地址锁存/计数器
这两个寄存器就是用来指定地址的。
我们发的前两个字节会进入页地址锁存计数器中。最后一个字节会进入字节锁存计数器中。
- 页地址通过写保护和行解码决定操作那一页。
- 字节地址通过列解码和256页缓存决定指定地址的读写操作
- 页缓存区是一个256字节的RAM存储器
- 数据写入就是通过这个RAM缓存区来进行的。
- 因为Flash存储信息较慢,而RAM读写信息很快。
- 所以需要RAM先临时保存,Flash根据RAM存储器来进行存储操作。
- 所以也有一个规定:一次写的字节数不能超过256字节
- 在在写入时序之后,芯片会进入一段忙的状态(忙着把RAM中的信息存储到Flash中)这个忙的状态会通过一条线,连接SPI状态寄存器。给状态寄存器的BUSY位置1。
- 因为读取只是看一眼状态,不需要改变,所以读取时很快的。基本不会受到限制
- 并且因为这两个寄存器都有计数器,所以在读写之后会指针+1
4. Flash操作注意事项
写入操作时:
-
写入操作前,必须先进行写使能
-
每个数据位只能由1改写为0,不能由0改写为1
所以写入数据前必须先擦除,擦除后,所有数据位变为1
-
擦除必须按最小擦除单元进行(整个芯片、按块、按扇区)
-
连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
5. W25Q64手册精简
这里只精简出一般需要的。其他位请详看手册。括号内是对应手册的章节
(11.1 )STATUS REGISTER 状态寄存器
- BUSY位
- 忙碌(BUSY)是状态寄存器(S0)中的一个只读位,
- 当设备正在执行页编程、扇区擦除、块擦除、芯片擦除或写状态寄存器指令时,该位被设置为 1 状态。
- 在此期间,设备将忽略其他指令,除了读状态寄存器和擦除暂停指令(请参阅交流特性中的 tW、tPP、tSE、tBE 和 tCE)。
- 当编程、擦除或写状态寄存器指令完成时,BUSY位将被清除为 0 状态,表示设备已准备好接受进一步的指令。
- 写使能锁存器(WEL)
- 写使能锁存器(WEL)是状态寄存器(S1)中的一个只读位,
- 在执行写使能指令后被设置为 1。
- 当设备被写禁止时,WEL 状态位被清除为 0。
- 在上电或执行以下任何指令后会出现写禁止状态:写禁止、页编程、扇区擦除、块擦除、芯片擦除和写状态寄存器。 这表明我们在每次写入操作时都要写使能一下
- 这两位在寄存器中是这样的。我们要掌握的在最低的两位
(11.2.1)Manufacturer and Device Identification 制造商和设备标识
(11.2.2) Instruction Set Table SPI 指令集 (常用标记)
6. W25Q64读写存储器编写
7.1编写时需要注意的点
一定要先看懂时序图再去编写存储器 的 读写时序
需要注意Flash的操作注意事项
写入操作时:
-
写入操作前,必须先进行写使能
-
每个数据位只能由1改写为0,不能由0改写为1
所以写入数据前必须先擦除,擦除后,所有数据位变为1
-
擦除必须按最小擦除单元进行(整个芯片、按块、按扇区)
-
连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
7.2程序文件简要说明:
- MySPI:为了防止与stm32函数重名,所以添加了My前缀。 主要是为了完成SPI的三个基本时序
- MySPI.h:函数声明
- W25Q64.c:初始化W25Q64存储寄存器。完成读写、擦除存储器的时序
- W25Q64.h:函数声明、数据结构体声明
- W25Q64_Ins.h:控制存储器时需要用到的指令集
- main.c:测试SPI读取存储器结果。
MySPI.c
#include "stm32f10x.h" // Device header
/*所用引脚列表*/
#define RCCPeriph RCC_APB2Periph_GPIOA
#define SCK_Port GPIOA
#define SCK_Pin GPIO_Pin_6
#define SS_Port GPIOA
#define SS_Pin GPIO_Pin_5
#define MOSI_Port GPIOA
#define MOSI_Pin GPIO_Pin_4
#define MISO_Port GPIOA
#define MISO_Pin GPIO_Pin_3
/**
* 函 数:写片选信号SS
* 参 数:BitValue:输入1片选信号SS为高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_SS (uint8_t BitValue)
{
GPIO_WriteBit(SS_Port,SS_Pin,(BitAction)BitValue);
}
/**
* 函 数:写时钟信号SCK
* 参 数:BitValue:输入1时钟信号SCK为高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_SCK (uint8_t BitValue)
{
GPIO_WriteBit(SCK_Port,SCK_Pin,(BitAction)BitValue);
}
/**
* 函 数:写 主机输出,从机输入信号MOSI
* 参 数:BitValue:输入1时主机输出高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_MOSI (uint8_t BitValue)
{
GPIO_WriteBit(MOSI_Port,MOSI_Pin,(BitAction)BitValue);
}
/**
* 函 数:读 主机输入,从机输出信号MISO
* 参 数:无
* 返 回 值:BitValue
* 注意事项:无
*/
uint8_t MySPI_R_MISO (void)
{
return GPIO_ReadInputDataBit(MISO_Port,MISO_Pin);
}
/**
* 函 数:MySPI初始化
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Init(void)
{
/*配置时钟与引脚*/
RCC_APB2PeriphClockCmd(RCCPeriph,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = SCK_Pin | SS_Pin | MOSI_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOSI_Port,&GPIO_InitStructure); //时钟、片选、MOSI都是推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = MISO_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MISO_Port,&GPIO_InitStructure); //MISO 为上拉输入
MySPI_W_SS(1); //片选默认为高
MySPI_W_SCK(0); //时钟默认为低
}
/*******************/
/*SPI的三个时序单元*/
/*******************/
/**
* 函 数:起始信号
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
/**
* 函 数:终止条件
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/**
* 函 数:交换一个字节(模式0)(方法1)
* 参 数:SendByte 待发送的字节
* 返 回 值:ReceiveByte 接收到的字节
* 注意事项:这是使用掩码依次提取数据中的每一位保存或发送
好处是不会改变传入参数本身,但是效率不高
如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
*/
//uint8_t MySPI_WarpByte(uint8_t SendByte)
//{
// uint8_t ReceiveByte = 0x00;
//
// uint8_t i = 0;
// for(i = 0; i < 8; i++)
// {
// MySPI_W_MOSI(SendByte & (0x80 >> i)); //发送第一个bit
// MySPI_W_SCK(1);//第上升沿来临
// if (MySPI_R_MISO() == 1)
// {
// ReceiveByte |= (0x80 >> i); //按照从高往低接收数据
// }
// MySPI_W_SCK(0); //下降沿来临
// }
// return ReceiveByte;
//}
/**
* 函 数:交换一个字节(模式0)(方法2)
* 参 数:SendByte 待发送的字节
* 返 回 值:ReceiveByte 接收到的字节
* 注意事项:这是使用了移位模型的方式。效率更快
如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
*/
uint8_t MySPI_WarpByte(uint8_t SendByte)
{
uint8_t i = 0;
for(i = 0; i < 8; i++)
{
MySPI_W_MOSI(SendByte & 0x80); //发送第一个bit
SendByte <<= 1; //发送数据左移一位
MySPI_W_SCK(1); //第上升沿来临
if (MySPI_R_MISO() == 1)
{
SendByte |= 0x01; //保存收到的数据到发送寄存器的最低位
}
MySPI_W_SCK(0); //下降沿来临
}
return SendByte;
}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H
//初始化
void MySPI_Init(void);
//起始
void MySPI_Start(void);
//终止
void MySPI_Stop(void);
//交换
uint8_t MySPI_WarpByte(uint8_t SendByte);
#endif
W25Q64.c
#include "stm32f10x.h" // Device header
#include "W25Q64.h"
#include "W25Q64_Ins.h"
#include "MySPI.h"
/**
* 函 数:初始化W25Q64
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_Init(void)
{
MySPI_Init();
}
/********************/
/*拼接完整的通信时序*/
/********************/
/**
* 函 数:查看W25Q64的厂商号和设备号
* 参 数:ID* Str 存放了ID结构体的指针
* 返 回 值:无
* 注意事项:接收第八位时是|=
*/
ID W25Q64_ID;//存放设备ID号的结构体
void W25Q64_ReadID(ID* Str)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_JEDEC_ID);//发送读取设备号指令。返回值不要
Str->MID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收厂商ID 从机发来的设备号。发送值随便
Str->DID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID高八位
Str->DID <<= 8;//把接收到的数据放到高八位
Str->DID |= MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID低八位
MySPI_Stop();//停止
}
/**
* 函 数:写使能
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_WRITE_ENABLE);//发送
MySPI_Stop();//停止
}
/**
* 函 数:写失能
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WriteDisable(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_WRITE_DISABLE);//发送
MySPI_Stop();//停止
}
/**
* 函 数:等待忙函数
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WaitBusy(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_READ_STATUS_REGISTER_1); //发送
uint32_t TimeOut = 100000;
while((MySPI_WarpByte(W25Q64_DUMMY_BYTE)&0x01) == 0x01) //读取状态寄存器1的Busy位是否为1,为1则等待
{
TimeOut--;
if(TimeOut == 0)
{
break; //超时退出
}
}
MySPI_Stop(); //停止
}
/**
* 函 数:页编程
* 参 数:Address 要写入那个页地址
*DataArray 存储字节所用的数组
Count 一次写入多少字节
* 返 回 值:无
* 注意事项:一次只能写入最多0-256个字节
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count)//(0-256,所以要16位)
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_PAGE_PROGRAM);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
MySPI_WarpByte(Address >> 0);//发送页地址
uint16_t i = 0;
for(i = 0; i < Count; i++)
{
MySPI_WarpByte(DataArray[i]);//发送Count个数组的第i位
}
MySPI_Stop();//停止
}
/**
* 函 数:页擦除
* 参 数:Address 要擦除那一页
* 返 回 值:无
* 注意事项:最小的擦除单位。4kb 1扇区
*/
void W25Q64_PageErase(uint32_t Address)
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_SECTOR_ERASE_4KB);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//发送地址
MySPI_WarpByte(Address >> 0);
MySPI_Stop();//停止
}
/**
* 函 数:读取数据
* 参 数:Address 要读取个地址
*DataArray 存储字节所用的数组
Count 一次读取多少字节
* 返 回 值:无
* 注意事项:读取可以无限制读取
*/
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count)//读取没有限制
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_READ_DATA);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
MySPI_WarpByte(Address >> 0);//发送页地址
uint32_t i = 0;
for(i = 0; i < Count; i++)
{
DataArray[i] = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收Count个字节,放到数组的第i位
}
MySPI_Stop();//停止
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H
//初始化W25Q64
void W25Q64_Init(void);
/*厂商和设备ID号*/
typedef struct ID
{
uint8_t MID;//8位厂商ID
uint16_t DID;//16位设备ID
}ID;
extern ID W25Q64_ID;
//获取厂商和设备号ID
void W25Q64_ReadID(ID* Str);
//页编程
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count);
//页擦除
void W25Q64_PageErase(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 "oled.h"
#include "Delay.h"
#include "key.h"
#include "W25Q64.h"
/**
* 函 数:验证SPI控制W25Q64存储器
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
uint8_t ArrWrite[] = {0x01,0x02,0x03,0x04};
uint8_t ArrRead[4];
int main()
{
Delay_Init();//初始化演示
OLED_Init();//初始化OLED;
W25Q64_Init();//初始化W25Q64存储器
OLED_ShowString(1,1,"MID: ,DID: ");
W25Q64_ReadID(&W25Q64_ID);//读ID放到这个结构体中
OLED_ShowHexNum(1,5,W25Q64_ID.MID,2);
OLED_ShowHexNum(1,12,W25Q64_ID.DID,4);//显示MID DID
OLED_ShowString(2,1,"W:");
OLED_ShowString(3,1,"R:");
W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
OLED_ShowHexNum(2, 3, ArrWrite[0], 2);
OLED_ShowHexNum(2 ,6, ArrWrite[1], 2);
OLED_ShowHexNum(2, 9, ArrWrite[2], 2);
OLED_ShowHexNum(2, 12, ArrWrite[3], 2);
OLED_ShowHexNum(3, 3, ArrRead[0], 2);
OLED_ShowHexNum(3 ,6, ArrRead[1], 2);
OLED_ShowHexNum(3, 9, ArrRead[2], 2);
OLED_ShowHexNum(3, 12, ArrRead[3], 2);
while(1)
{
}
}
7. 验证Flash注意事项
1. Flash操作注意事项
写入操作时:
-
写入操作前,必须先进行写使能
-
每个数据位只能由1改写为0,不能由0改写为1
所以写入数据前必须先擦除,擦除后,所有数据位变为1
-
擦除必须按最小擦除单元进行(整个芯片、按块、按扇区)
-
连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
2. 验证Flash擦除之后变为全1
只擦除,不写入,直接读取
W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
// W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
3. 验证每个数据位只能由1改写为0,不能由0改写为1
当前0x000000
地址存储为 0xAA 0xBB 0xCC 0xDD
不擦除直接写入
//W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
-
直接写入
0xFF 0xFF 0xFF 0xFF
结果。字节中0并未改变
-
直接写入
0x00 0x00 0x00 0x00
结果。字节中1全部改变为0
4.最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入
一页的范围是xxxx00到xxxxFF。因此他能存放(0-255)256个字节
现在指定写入的地址为0x0000FF
。写入0xAA 0xBB 0xCC 0xDD
那么按照规则来说,就会在页尾写入0xAA、页首写入 0xBB 0xCC 0xDD
而另一页并不会写入数据。而读取可以跨页
从页尾开始读取结果如下:
从页首开始读取结果如下: