一、EN25Q128简介
EN25Q128 是大容量 SPI FLASH 产品,EN25Q128 的容量为 128Mb(16M 字节)。学习这个芯片可以参考华邦公司的 W25Q128 芯片,因为它们是完全兼容的。
FLASH 是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的 FLASH 主要有 NOR FLASH 和 NAND FLASH 两种类型。
NOR FLASH 的地址线和数据线分开,它可以按“字节”读写数据,符合 CPU 的指令译码执行要求,所以假如 NOR FLASH 上存储了代码指令,CPU 给 NOR FLASH 一个地址,NOR FLASH 就能向 CPU 返回一个数据让 CPU 执行,中间不需要额外的处理操作。因此可以用 NOR FLASH 直接作为嵌入式 MCU 的程序存储空间。
NAND FLASH 的数据和地址线共用,只能按“块”来读写数据,假如 NAND FLASH 上存储了代码指令,CPU 给 NAND FLASH 地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。若代码存储在 NAND FLASH 上,可以把它先加载到 RAM 存储器上,再由 CPU 执行。所以在功能上可以认为 NOR FLASH 是一种断电后数据不丢失的 RAM,但它的擦除单位与 RAM 有区别,且读写速度比 RAM 要慢得多。
NOR FLASH与 NAND FLASH 在数据写入前都需要有擦除操作,但实际上 NOR Flash 的一个 bit 可以从 1变成 0,而要从 0 变 1 就要擦除后再写入,NAND Flash 这两种情况都需要擦除。擦除操作的最小单位为 “扇区/块” ,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前的数据都可能会被擦除。
FLASH 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题:一是 FLASH 的使用寿命,另一个是可能的位反转。
使用寿命体现在:读写上是 FLASH 的擦除次数都是有限的(NOR FLASH 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。由于 NAND FLASH 通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用 NAND FLASH 最好通过算法扫描介质找出坏块并标记为不可用,因为坏块上的数据是不准确的。
位反转是数据位写入时为 1,但经过一定时间的环境变化后可能实际变为 0 的情况,反之亦然。位反转的原因很多,可能是器件特性也可能与环境、干扰有关,由于位反转的问题可能存在,所以 FLASH 存储器需要“探测/错误更正(EDC/ECC)”算法来确保数据的正确性。
W25Q128 将 16M 的容量分为 256 个块( Block),每个块大小为 64K 字节,每个块又分为 16 个扇区( Sector),每个扇区 4K 个字节。 W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。
W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。
二、EN25Q128常用指令
NOR FLASH 的指令非常多,一般我们值需要 5 条指令就可以完成对 NOR FLASH 的基本操作。
指令 | 名称 | 作用 |
---|---|---|
0x06 | 写使能 | 写入数据/擦除之前,必须先发送写使能 |
0x05 | 读 SR1 | 判定 FLASH 是否处于空闲状态,擦除用 |
0x03 | 读数据 | 用于读取 NOR FLASH 数据 |
0x02 | 页写 | 用于写入 NOR FLAS 数据,最多写 246 字节 |
0x20 | 扇区擦除 | 扇区擦除指令,最小擦除单位(4096 字节) |
#define W25Q128_WIRITE_ENABLE 0x06
#define W25Q128_READ_STATTUS_REGISTER_1 0x05
#define W25Q128_READ_DATA 0x03
#define W25Q128_PAGE_PROGRAM 0x02
#define W25Q128_SECTOR_ERASE 0x20
#define W25Q128_JEDEC_ID 0x9F
#define W25Q128_DUMMY_BYTE 0xFF
2.1、写使能指令(0x06)
写使能指令将状态寄存器中的写使能锁存器(WEL)位设置为 1。WEL 位必须设置在写数据、扇区擦除、块擦除、芯片擦除、写状态寄存器和擦除/程序安全寄存器指令之前。
写使能指令的输入方式是拉低片选线,然后发送指令 "0x06",最后在拉高片选线。
2.2、读状态寄存器1指令(0x05)
读状态寄存器指令允许读取 8 位状态寄存器。
首先拉低片选线,然后发送指令 "0x05",返回状态寄存器 1 的数据,最后在拉高片选线。
2.3、读数据指令(0x03)
读取数据指令允许从内存中按顺序读取一个或多个数据字节。在每个字节的数据被移出后,地址自动增加到下一个更高的地址,允许连续的数据流。这意味着只要时钟继续,就可以用一条指令访问整个内存。读状态寄存器指令允许读取 8 位状态寄存器。
首先拉低片选线,然后发送指令 "0x03",然后在发送 24 位地址,返回读取的数据,最后在拉高片选线。
2.4、页写指令(0x02)
页写指令允许在先前擦除(FFh)内存位置对 1 到 256 字节(一页)的数据进行写入。在设备接受页写指令(状态寄存器位WEL=1)之前,必须执行写使能指令。
首先拉低片选线,然后发送指令代码 "02h",接着发送一个 24 位地址和至少一个数据字节。当数据被发送到设备时,片选线必须在指令的整个长度内保持低电平。
2.5、扇区擦除指令(0x20)
由于 FLASH 的特性决定它只能把原来为 "1" 的数据位改写为 "0",而原来为 "0" 的数据位不能直接改写为 "1"。因此,我们需要扇区擦除指令,将指定扇区的内容全部擦除为 "1"。在设备接受扇区擦除之前,必须执行写使能指令指令(状态寄存器位 WEL必须等于 1)。
首先拉低片选线,然后发送指令代码 "20h",接着发送一个 24 位地址,FLASH 会把当前地址所在的扇区中的数据全部擦除为 "1",最后在拉高片选,等待擦除完成。
三、EN25Q128常用寄存器
状态寄存器 1 的 位 0 BUSY 指示当前状态,0:空闲状态(硬件自动完成);1:当前处于忙碌状态;
状态寄存器 1 的 位 1 WEL 写使能位,0:写禁止,不能页编程/扇区、块、片擦除/写状态寄存器;1:写使能;
四、W25Q128操作步骤
4.1、W25Q128读操作命令
- 发送读命令(03H)。
- 发送 24 位地址,分 3 次发送。
- 发送空字节(0xFF),读取数据,支持连续读。
void W25Q128_ReadData(uint32_t address, uint8_t *data, uint16_t length)
{
// 拉低片选
W25Q128_CS(0);
// 发送读数据命令
SPI_SwapOneByte(W25Q128_READ_DATA);
// 发送扇区地址
SPI_SwapOneByte((address >> 16));
SPI_SwapOneByte((address >> 8));
SPI_SwapOneByte(address);
// 读取数据
for (uint32_t i = 0; i < length; i++)
{
data[i] = SPI_SwapOneByte(W25Q128_DUMMY_BYTE);
}
// 拉高片选
W25Q128_CS(1);
}
4.2、W25Q128擦除命令
- 发送写使能命令(06H)。
- 发送擦除删除命令(02H)。
- 发送要擦除扇区的 24 位地址,分 3 次发送,会自动清除该地址所在的扇区。
- 等待擦除完成。
/**
* @brief W25Q128写使能函数
*
*/
void W25Q128_WriteEnable(void)
{
// 拉低片选
W25Q128_CS(0);
// 发送写使能命令
SPI_SwapOneByte(W25Q128_WIRITE_ENABLE);
// 拉高片选
W25Q128_CS(1);
}
/**
* @brief W25Q64等待完成函数
*
*/
void W25Q128_WaitBusy(void)
{
uint32_t time = 0xFFFF;
// 拉低片选
W25Q128_CS(0);
// 发送读状态寄存器1命令
SPI_SwapOneByte(W25Q128_READ_STATTUS_REGISTER_1);
// 等待写使能完成
while((SPI_SwapOneByte(W25Q128_DUMMY_BYTE) & 0x01) || time--);
// 拉高片选
W25Q128_CS(1);
}
/**
* @brief W25Q128扇区擦除函数
*
* @param address 待删除的扇区的内存地址
*/
void W25Q128_SectorErase(uint32_t address)
{
// 写使能
W25Q128_WriteEnable();
// 拉低片选
W25Q128_CS(0);
// 发送扇区擦除命令
SPI_SwapOneByte(W25Q128_SECTOR_ERASE);
// 发送扇区地址
SPI_SwapOneByte((address >> 16));
SPI_SwapOneByte((address >> 8));
SPI_SwapOneByte(address);
// 拉高片选
W25Q128_CS(1);
// 等待擦除完成
W25Q128_WaitBusy();
}
4.3、W25Q128写操作命令
- 发送写使能命令(06H)。
- 发送页写命令(02H),一次最多写入 256 字节。
- 发送要写入数据的 24 位内存地址,分 3 次发送。
- 发送要写入的数据,一次最多写入 256 字节。
- 等待写入完成。
/**
* @brief W25Q128页写入函数
*
* @param address 待写入数据的内存地址
* @param data 待写入的数据
* @param length 待写入数据的长度
*/
void W25Q128_PageProgram(uint32_t address, uint8_t *data, uint16_t length)
{
// 写使能
W25Q128_WriteEnable();
// 拉低片选
W25Q128_CS(0);
// 发送页写入命令
SPI_SwapOneByte(W25Q128_PAGE_PROGRAM);
// 发送扇区地址
SPI_SwapOneByte((address >> 16));
SPI_SwapOneByte((address >> 8));
SPI_SwapOneByte(address);
// 发送数据
for (uint16_t i = 0; i < length; i++)
{
SPI_SwapOneByte(data[i]);
}
// 拉高片选
W25Q128_CS(1);
// 等待写使能完成
W25Q128_WaitBusy();
}
在向 FLASH 写入前,我们最后先擦除数据,否则,容易造成数据错乱。这是因为 FLASH 的只能把原来为 "1" 的数据位改写为 "0",而原来为 "0" 的数据位不能直接改写为 "1"。
五、源码实现
5.1、原理图
通过原理图,可知 FLASH 使用 SPI1,它的片选引脚接着 PB14 引脚,使用软件管理的方式。
5.2、程序源码
SPI1 初始化函数内容如下:
SPI_HandleTypeDef g_spi1_handle;
/**
* @brief SPI1初始化
*
*/
void SPI1_Init(void)
{
g_spi1_handle.Instance = SPI1; // SPI基地址
g_spi1_handle.Init.Mode = SPI_MODE_MASTER; // SPI主机模式
g_spi1_handle.Init.Direction = SPI_DIRECTION_2LINES; // SPI全双工模式
g_spi1_handle.Init.DataSize = SPI_DATASIZE_8BIT; // SPI帧格式
g_spi1_handle.Init.CLKPolarity = SPI_POLARITY_LOW; // SPI时钟极性
g_spi1_handle.Init.CLKPhase = SPI_PHASE_1EDGE; // SPI时钟相位
g_spi1_handle.Init.NSS = SPI_NSS_SOFT; // SPI软件NSS控制
g_spi1_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // SPI时钟分频因子
g_spi1_handle.Init.FirstBit = SPI_FIRSTBIT_MSB; // SPI数据高位先发送
g_spi1_handle.Init.TIMode = SPI_TIMODE_DISABLE; // SPI不使用TI模式
g_spi1_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; // SPI不使用CRC校验
g_spi1_handle.Init.CRCPolynomial = 7; // 设置CRC校验多项式
HAL_SPI_Init(&g_spi1_handle);
}
SPI 底层初始化函数内容如下:
/**
* @brief SPI底层初始化函数
*
* @param hspi SPI句柄
*/
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hspi->Instance == SPI1)
{
__HAL_RCC_SPI1_CLK_ENABLE(); // 使能SPI1时钟
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能SPI1对应的GPIO时钟
GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 ; // SPI1的SCK引脚、MISO引脚和MOSI引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推完输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; // 复用功能
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
}
SPI 发送接收一个字节函数内容如下:
/**
* @brief SPI发送接收一个字节函数
*
* @param hspi SPI句柄
* @param data 要发送的数据
* @return uint8_t 接收的数据
*/
uint8_t SPI_TransmitReceiveOneByte(SPI_HandleTypeDef *hspi, uint8_t data)
{
uint8_t receive = 0;
HAL_SPI_TransmitReceive(hspi, &data, &receive, 1, 1000);
return receive;
}
W25Q128 初始化函数内容如下:
/**
* @brief W25Q128初始化函数
*
*/
void W25Q128_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能W25Q128 CS对应GPIO引脚的时钟
RCC_W25Q128_CS_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = W25Q128_CS_GPIO_PIN; // W25Q128的CS引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
HAL_GPIO_Init(W25Q128_CS_GPIO_PORT, &GPIO_InitStruct);
W25Q128_CS(1); // 片选引脚默认为高电平,不选中从机
}
W25Q128 读取 ID 函数内容如下:
/**
* @brief W25Q128读取ID函数
*
* @param hspi SPI句柄
* @return uint32_t W25Q128的ID
*/
uint32_t W25Q128_ReadId(SPI_HandleTypeDef *hspi)
{
uint8_t temp[3] = {0};
uint32_t id = 0;
// 拉低片选
W25Q128_CS(0);
// 发送读ID命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_JEDEC_ID);
HAL_SPI_Receive(hspi, temp, 3, 1000);
// 读取ID
for (uint8_t i = 0; i < 3; i++)
{
id <<= 8;
id |= temp[i];
}
// 拉高片选
W25Q128_CS(1);
// 返回ID
return id;
}
W25Q128 写使能函数内容如下:
/**
* @brief W25Q128写使能函数
*
* @param hspi
*/
void W25Q128_WriteEnable(SPI_HandleTypeDef *hspi)
{
// 拉低片选
W25Q128_CS(0);
// 发送写使能命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_WIRITE_ENABLE);
// 拉高片选
W25Q128_CS(1);
}
W25Q128 等待完成函数内容如下:
/**
* @brief W25Q64等待完成函数
*
*/
void W25Q128_WaitBusy(SPI_HandleTypeDef *hspi)
{
uint32_t time = 0xFFFF;
uint8_t status = 1;
// 拉低片选
W25Q128_CS(0);
// 发送读状态寄存器1命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_READ_STATTUS_REGISTER_1);
// 等待写使能完成
while ((status & 0x01) && time--)
{
HAL_SPI_Receive(hspi, &status, 1, 1000);
}
// 拉高片选
W25Q128_CS(1);
}
WW25Q128 页写入函数内容如下:
/**
* @brief W25Q128页写入函数
*
* @param address 待写入数据的内存地址
* @param data 待写入的数据
* @param length 待写入数据的长度
*/
void W25Q128_PageProgram(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
// 写使能
W25Q128_WriteEnable(hspi);
// 拉低片选
W25Q128_CS(0);
// 发送页写入命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_PAGE_PROGRAM);
// 发送扇区地址
SPI_TransmitReceiveOneByte(hspi, address >> 16);
SPI_TransmitReceiveOneByte(hspi, address >> 8);
SPI_TransmitReceiveOneByte(hspi, address);
// 发送数据
HAL_SPI_Transmit(hspi, data, length, 1000);
// 拉高片选
W25Q128_CS(1);
// 等待写使能完成
W25Q128_WaitBusy(hspi);
}
W25Q128 扇区擦除函数内容如下:
/**
* @brief W25Q128扇区擦除函数
*
* @param address 待删除的扇区的内存地址
*/
void W25Q128_SectorErase(SPI_HandleTypeDef *hspi, uint32_t address)
{
// 写使能
W25Q128_WriteEnable(hspi);
// 拉低片选
W25Q128_CS(0);
// 发送扇区擦除命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_SECTOR_ERASE);
// 发送扇区地址
SPI_TransmitReceiveOneByte(hspi, address >> 16);
SPI_TransmitReceiveOneByte(hspi, address >> 8);
SPI_TransmitReceiveOneByte(hspi, address);
// 拉高片选
W25Q128_CS(1);
// 等待写使能完成
W25Q128_WaitBusy(hspi);
}
W25Q128 读取数据函数内容如下:
/**
* @brief W25Q128读取数据函数
*
* @param address 待读取数据的内存地址
* @param data 保存读取的数据
* @param length 读取数据的长度
*/
void W25Q128_ReadData(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
// 拉低片选
W25Q128_CS(0);
// 发送读数据命令
SPI_TransmitReceiveOneByte(hspi, W25Q128_READ_DATA);
// 发送扇区地址
SPI_TransmitReceiveOneByte(hspi, address >> 16);
SPI_TransmitReceiveOneByte(hspi, address >> 8);
SPI_TransmitReceiveOneByte(hspi, address);
// 读取数据
HAL_SPI_Receive(hspi, data, length, 1000);
// 拉高片选
W25Q128_CS(1);
}
无校验写 W25Q128 函数内容如下:
/**
* @brief 无校验写W25Q128函数
*
* @param hspi SPI句柄
* @param address 待写入数据的内存地址
* @param data 待写入的数据
* @param length 待写入数据的个数
*
* @note
* 确保所写地址范围的数据全为0xFF,否则在非0xFF处写入失败。
* 该函数具有自动换页的功能
*
*/
void W25Q128_WriteData_NoCheck(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
uint16_t pageRemain = 256 - address % 256; // 单页剩余的字节数,得到地址在某页的位置
// 当写入的数据小于单页剩余的字节数,将写入的字节数赋值给单页剩余的字节数
pageRemain = (length <= pageRemain ? length : pageRemain);
while (1)
{
// 当写入字节比页内剩余地址还少的时候,一次性写完
// 当写入字节比页内剩余地址多的时候,先写完页内剩余地址,然后根据剩余长度进行不同的处理
W25Q128_PageProgram(hspi, address, data, pageRemain);
if (length == pageRemain) // 写入完成
{
break;
}
else
{
address += pageRemain; // 写地址偏移
data += pageRemain; // 写数据指针偏移
length -= pageRemain; // 写入总长度减去已经写入的个数
// 当剩余长度大于一页256时,可以一次写一页
// 当剩余数据小于一页,可以一次写完
pageRemain = (length > 256 ? 256 : length);
}
}
}
该函数通过判断传参中的写入字节的长度与单页剩余的字节数,来决定是否是需要在新页写入剩下的字节。
W25Q128 写数据函数内容如下:
/**
* @brief W25Q128写数据函数
*
* @param hspi SPI句柄
* @param address 待写入数据的内存地址
* @param data 待写入的数据
* @param length 待写入数据的个数
*/
void W25Q128_WriteData(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
uint32_t sectorPosition = address / 4096; // 扇区地址
uint16_t sectorOffset = address % 4096; // 在扇区中的偏移地址
uint16_t sectorRemain = 4096 - sectorOffset; // 扇区剩余空间大小
uint8_t *pBuff = g_w25q128_buffer;
uint16_t i = 0;
// 当写入的数据小扇区剩余空间大小,将写入的字节数赋值给扇区剩余空间大小
sectorRemain = (length <= sectorRemain ? length : sectorRemain);
while (1)
{
// 读出整个扇区的内容
W25Q128_ReadData(hspi, sectorPosition * 4096, pBuff, 4096);
// 校验数据是否需要擦除
for (i = 0; i < sectorRemain; i++)
{
if (pBuff[sectorOffset + i] != 0xFF)
{
break; // 需要擦除,直接退出for循环
}
}
// 需要擦除
if (i < sectorRemain)
{
// 擦除这个扇区
W25Q128_SectorErase(hspi, sectorPosition * 4096);
// 将待写入的数据拷贝到缓冲区
for (i = 0; i < sectorRemain; i++)
{
pBuff[i + sectorOffset] = data[i];
}
//写入整个扇区
W25Q128_WriteData_NoCheck(hspi, sectorPosition * 4096, pBuff, 4096);
}
else
{
// 对于已经擦除的,直接写入扇区剩余空间
W25Q128_WriteData_NoCheck(hspi, address, data, sectorRemain);
}
// 写入完成
if (length == sectorRemain)
{
break;
}
// 写入未完成
else
{
sectorPosition++; // 扇区加1,使用下一个扇区
sectorOffset = 0; // 扇区偏移地址为0
address += sectorRemain; // 写地址偏移
data += sectorRemain; // 写数据指针偏移
length -= sectorRemain; // 写入总长度减去已经写入的个数
// 当剩余长度大于扇区长度4096时,可以一次写一整个扇区
// 当剩余长度小于扇区长度4096时,下一个扇区可以写完
sectorRemain = (length > 4096 ? 4096 : length);
}
}
}
该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。首先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个 g_w25q128_buff 的全局变量,用于擦除时缓存扇区内的数据。
有关时钟配置函数请在 STM32 的时钟系统 篇章查看。
有关 USART1 的配置请在 串口通信 篇章查看。
int main(void)
{
uint32_t id = 0;
uint8_t writeDataArray[] = {0x11, 0x22, 0x33, 0x44, 0x55};
uint8_t readDataArray[100] = {0};
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); // 设置中断优先级分组
USART1_Init(115200);
SPI1_Init();
W25Q128_Init();
id = W25Q128_ReadId(&g_spi1_handle);
printf("id:%#X\r\n", id);
W25Q128_WriteData(&g_spi1_handle, 4096, writeDataArray, 5);
W25Q128_WriteData(&g_spi1_handle, 4096 + 5, writeDataArray, 5);
W25Q128_ReadData(&g_spi1_handle, 4096, readDataArray, 11);
for (uint8_t i = 0; i < 11; i++)
{
printf("%d:%#x\r\n", i, readDataArray[i]);
}
while (1)
{
}
return 0;
}
标签:16,FLASH,扇区,SPI,W25Q128,擦除,address,hspi
From: https://www.cnblogs.com/kurome/p/18083785