W25Q128存储器详解
可能有很多小伙伴对 W25Q128 感到陌生,说白了它就是一个存储芯片。它是一款高性能、容量较大的闪存存储器芯片,通过 SPI 接口进行通信,适用于各种需要高速、大容量数据存储的场合。常用于嵌入式系统中,作为程序代码存储器或配置数据存储器,如微控制器、单板计算机等。
SPI 是一种通信协议,今天学习 W25Q128 的同时会讲解一下 SPI 通信协议,不懂 SPI 的小伙伴也可以接着看。
1. 源码下载及前置阅读
本文首发良许嵌入式网,https://www.lxlinux.net/e/ ,欢迎关注!
本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):
https://www.lxlinux.net/e/stm32/w25q128-tutorial.html
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
- 通俗易懂的 GPIO 介绍与实践:如何快速成为点灯大师?
- 从零开始轻松掌握STM32开发的必备指南:零基础快速上手STM32开发(手把手保姆级教程)
- 使用接收中断+超时判断完成不定长数据的接收:STM32串口接收不定长数据(空闲中断+DMA)
往期教程,有兴趣的小伙伴可以看看。
- ESP8266详解,助你成为物联网应用的专家:手把手教你玩转ESP8266(原理+驱动)
- 实现物联网数据采集与远程监控:小项目:使用MQTT上传温湿度到Onenet服务器
- 深入浅出,帮助您理解和应用MQTT协议:万字猛文:MQTT原理及案例
作者简介 |
---|
大家好,我是良许,博客里所有的文章皆为我的原创。 下面是我的一些个人介绍,欢迎交个朋友: · 211工科硕士,国家奖学金获得者; · 深耕嵌入式11年,前世界500强外企高级嵌入式工程师; · 书籍《速学Linux作者》,机械工业出版社专家委员会成员; · 全网60W粉丝,博客分享大量原创成体系文章,全网阅读量累计超4000万; · 靠自媒体连续年入百万,靠自己买房买车。 |
我本科及硕士都是学机械,通过自学成功进入世界500强外企。我已经将自己的学习经验写成了一本电子书,超千人通过此书学习并转行成功。现在将这本电子书免费分享给大家,希望对你们有帮助:
电子书链接:https://www.lxlinux.net/1024.html
2. W25Q128介绍
2.1 W25Q128型号介绍
W25Q128是华邦公司推出的一款容量为 128M-bit(相当于 16M-byte)的 SPI 接口的 NOR Flash 芯片。
给大家解释一下新单词:
- NOR Flash:一种非易失性存储器,它可以在断电或掉电后仍然保持存储的数据,因此被广泛应用于长期数据存储。它具有容量大,可重复擦写、按“扇区/块”擦除的特性。Flash 是有一个物理特性:只能写 0 ,不能写 1 ,写 1 靠擦除。
它还有很多不同容量的好兄弟:
型号 | 容量 |
---|---|
W25Q256 | 256M bits = 32M bytes |
W25Q128 | 128M bits = 16M bytes |
W25Q64 | 64M bits = 8M bytes |
W25Q32 | 32M bits = 4M bytes |
W25Q16 | 16M bits = 2M bytes |
W25Q80 | 8M bits = 1M bytes |
2.2 W25Q128模块参数及引脚介绍
W25Q128 的模块各个厂家做的各有不同,只是长得不一样而已,使用方式、引脚都是一样的。下面我介绍的是我们自绘的 W25Q128 模块。
W25Q128参数:
- 产品容量:128M-bit(16M-byte)
- 时钟频率:<=104MHz
- 工作电压:2.7V ~ 3.6V
- 工作温度:-40℃ ~ +85℃
- 支持 SPI 接口
参考接线如下:
W25Q128 | STM32 | 备注 |
---|---|---|
VCC | 3.3 | 电源正极 |
CS | A4/B12 | 片选信号 |
DO | A6/B14 | 输出 |
GND | G | 电源负极 |
CLK | A5/B13 | 时钟信号 |
DI | A7/B15 | 输入 |
如果你对引脚介绍有点懵,没关系,看看下面的 SPI 介绍你就明白了。
2.3 W25Q128存储架构
W25Q128 将 16M 的容量分为 256 个块(block),每块 64K 字节;每块分为 16 个扇区(sector),一扇区 4K 字节;每扇区分为 16 个页(page),一页 256 字节。
W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。
2.4 W25Q128常用指令
W25Q128 有非常多的指令,这里我们只介绍几个指令。
指令(HEX) | 名称 | 作用 |
---|---|---|
0x06 | 写使能 | 写入数据/擦除之前,必须先发送该指令 |
0x05 | 读 SR1 | 判定 FLASH 是否处于空闲状态,擦除用 |
0x03 | 读数据 | 读取数据 |
0x02 | 页写 | 写入数据,最多写256字节 |
0x20 | 扇区擦除 | 扇区擦除指令,最小擦除单位 |
具体工作时序如下:
写使能 (06H)
执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。
拉低 CS 片选 → 发送 06H → 拉高 CS 片选
读SR1(05H)
拉低 CS 片选 → 发送 05H → 返回SR1的值 → 拉高 CS 片选
读数据(03H)
拉低 CS 片选 → 发送 03H → 发送24位地址 → 读取数据(1~n)→ 拉高 CS 片选
页写 (02H)
页写命令最多可以向FLASH传输256个字节的数据。
拉低 CS 片选 → 发送 02H → 发送24位地址 → 发送数据(1~n)→ 拉高 CS 片选
扇区擦除(20H)
写入数据前,检查内存空间是否全部都是 0xFF ,不满足需擦除。
拉低 CS 片选 → 发送 20H→ 发送24位地址 → 拉高 CS 片选
2.5 W25Q128状态寄存器
W25Q128 一共有 3 个状态寄存器,它们的作用是跟踪芯片的状态。
这里我们只介绍常用的状态寄存器 1:
我不过多介绍了,感兴趣的小伙伴可以去看芯片手册。
我们需要记住的是在状态寄存器 1 中:
BUSY:指示当前的状态,0 表示空闲;1 表示忙碌。
WEL:写使能锁定,为 1 时,可以操作页/扇区/块;为 0 时,写禁止。
3. SPI介绍
SPI(Serial Peripheral Interface)串行外设接口,是一种高速、全双工、同步的通信总线,仅使用四根线来连接芯片的管脚,节省了管脚和PCB布局空间。由于其简单易用的特性,越来越多的芯片集成了SPI通信协议。
3.1 SPI物理架构
SPI 工作模式:
SPI 通信分为主设备(Master)和从设备(Slave)。一个完整的 SPI 通信系统需要包含一个主设备和一个或多个从设备。主设备提供时钟信号,从设备接收时钟信号。所有的读写操作都由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。
SPI 是全双工,并且没有定义速度限制,一般的实现通常能达到甚至超过 10Mbps。
SPI 信号线:
SPI 一般使用四条信号线通信:
- SCLK(Serial Clock):时钟信号线,由主设备提供并驱动整个通信过程。
- MOSI(Master Output,Slave Input):主设备输出、从设备输入线,主设备向从设备发送数据。
- MISO(Master Input,Slave Output):主设备输入、从设备输出线,从设备向主设备发送数据。
- SS/CS(Slave Select / Chip Select):片选信号线,由主设备控制从设备的选中状态。拉低表示选中。
示意图如下:
3.2 SPI工作原理
SPI 通信中,主机和从机都有一个串行移位寄存器。主机通过向自己的 SPI 串行寄存器写入一个字节来发起传输。
- 首先,拉低相应的 SS 信号线,表示与特定的从机进行通信。
- 主机通过发送 SCLK 时钟信号告诉从机进行数据的读写操作。
- 注意,SCLK 时钟信号可以是低电平有效或高电平有效,因为SPI有不同的模式(下文将介绍)。
- 主机将要发送的数据写入发送数据缓冲区,然后通过移位寄存器逐位地将数据传输给从机的串行移位寄存器,使用 MOSI 信号线进行传输。同时,从机的 MISO 接口接收到的数据也经过移位寄存器一位一位地移到接收缓冲区。
- 从机也通过 MISO 信号线将自己串行移位寄存器中的内容返回给主机。同时,从机通过 MOSI 信号线接收主机发送的数据。这样,两个移位寄存器中的内容就被交换。
SPI通信只有主模式和从模式,没有明确的读和写操作之分。实际上,外设的写操作和读操作是同步完成的。在SPI通信中,发送一个数据必然会收到一个数据;如果要接收一个数据,就必须先发送一个数据。
如果只进行写操作,主机可以忽略从设备传输过来的字节,因为主机不需要接收数据。
如果主机要读取从设备的一个字节,那么主机必须发送一个空字节来引发从设备的传输。
3.3 SPI工作模式
SPI 有4种不同的工作模式。
从设备的 SPI 模式是厂家设定的,不可变。但主从设备必须在同一工作模式下才能正常工作。所以我们可以设置主设备的 SPI 模式。
那怎么设置呢?通过 CPOL(时钟极性)和 CPHA(时钟相位)来控制,具体如下:
CPOL(时钟极性)定义了时钟空闲状态电平:
- CPOL=0,表示当 SCLK=0 时处于空闲态,所以有效状态就是 SCLK 处于高电平时。
- CPOL=1,表示当 SCLK=1 时处于空闲态,所以有效状态就是 SCLK 处于低电平时。
CPHA(时钟相位)定义数据的采集时间:
- CPHA=0,SCLK 的第一个(奇数)边沿进行数据位采样。数据在第一个时钟边沿被锁存,在第二个边沿发送数据。
- CPHA=1,SCLK 的第二个(偶数)边沿进行数据位采样。数据在第二个时钟边沿被锁存,在第一个边沿发送数据。
总结如下表:
SPI 模式 | CPOL | CPHA | 空闲时 SCK 时钟 | 采样边沿 | 采样时刻 |
---|---|---|---|---|---|
0 | 0 | 0 | 低电平 | 上升沿 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 下降沿 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 下降沿 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 上升沿 | 偶数边沿 |
四个模式的时序图如下,方便大家理解。绿线表示开始与结束,黄线表示数据采样,蓝线表示数据发送。
1.模式0(常用)CPOL = 0,CPHA = 0。
空闲时 SCLK 为低电平,采样时刻为第一个边沿,即上升沿。
2.模式1CPOL = 0,CPHA = 1。
空闲时 SCLK 为低电平,采样时刻为第二个边沿,即下降沿。
3.模式2,CPOL = 1,CPHA = 0。
空闲时 SCLK 为高电平,采样时刻为第一个边沿,即上升沿。
4.模式3(常用),CPOL = 1,CPHA = 1。
空闲时 SCLK 为高电平,采样时刻为第二个边沿,即上升沿。
4. 编程实战
实战目标:使用 SPI 通讯读写 W25Q128 模块。
4.1 硬件接线
本教程使用的硬件如下:
- W25Q128 模块
- 单片机:STM32F103C8T6
- 串口:USB 转 TTL
- 烧录器:ST-LINK V2
W25Q128 | STM32 | USB 转 TTL |
---|---|---|
VCC | 3.3 | |
CS | A4 | |
CLK | A5 | |
DO | A6 | |
DI | A7 | |
A10 | TX | |
A9 | RX | |
G | GND |
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
接好如下图。开发板使用的是我们自绘的板子。大家也可以用自己的板子,只要是 STM32F103C8T6 主控芯片就行。
4.2 SPI初始化
SPI 的工作模式我们配置为 0,即 CPOL = 0,CPHA = 0。
STM32F1系列的 SPI 接口有两个,SPI1 和 SPI2,这里我们选择 SPI1,引脚对应关系如下:
void SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; /* CPOL = 0 */
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; /* CPHA = 0 */
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(spiHandle->Instance==SPI1)
{
__HAL_RCC_SPI1_CLK_ENABLE(); /* SPI1时钟使能 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*
PA4 ------> SPI1_CS
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
GPIO_InitStruct.Pin = W25Q128_CS_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(W25Q128_CS_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
4.3 SPI读写一个字节
我们利用 HAL 库的 SPI 数据发送和接收函数 HAL_SPI_TransmitReceive
来读写一个字节。
函数原型:HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)
。
参数说明:
- hspi:指向SPI外设的句柄(handle)。
- pTxData:要发送的数据缓冲区指针。
- pRxData:接收数据的缓冲区指针。
- Size:要发送/接收的数据字节数。
- Timeout:超时时间,以毫秒为单位。
根据 SPI 的工作原理,我们发送一个字节的 data
,得到一个字节的 rec_data
。后续如果我们只需要读取一个字节,就发送一个无意义的 0xFF。
uint8_t read_write_one_byte(uint8_t data)
{
uint8_t rec_data = 0;
HAL_SPI_TransmitReceive(&hspi1, &data, &rec_data, 1, 1000);
return rec_data;
}
4.4 W25Q128初始化
初始化我们做个小检测,确保这个芯片是 W25Q128,而不是 W25Q64 或者 W25Q32。W25Q128 的芯片号是 0XEF17,从哪来的呢,当然是芯片手册啦。
void w25q128_init(void)
{
uint16_t flash_type;
read_write_one_byte(0xFF); /* 清除DR的作用 */
W25Q128_CS(1); /* 拉高片选 */
flash_type = w25q128_read_id(); /* 读取FLASH ID. */
if (flash_type == 0XEF17) /* FLASH芯片号0XEF17 */
printf("检测到W25Q128芯片\r\n");
}
uint16_t w25q128_read_id(void)
{
uint16_t deviceid;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ManufactDeviceID); /* 发送读 ID 命令 0x90 */
read_write_one_byte(0); /* 写入三个0 */
read_write_one_byte(0);
read_write_one_byte(0);
deviceid = read_write_one_byte(0xFF) << 8; /* 读取高8位字节 */
deviceid |= read_write_one_byte(0xFF); /* 读取低8位字节 */
W25Q128_CS(1); /* 拉高片选 */
return deviceid;
}
4.5 W25Q128等待空闲
前面我们提到状态寄存器 1 中 BUSY 是指示当前的状态,0 表示空闲;1 表示忙碌。
所以我们读取 W25Q128 的状态寄存器 1 的值,
static void w25q128_wait_busy(void)
{
while ((w25q128_rd_sr1() & 0x01) == 0x01); /* 等待BUSY位为0 */
}
uint8_t w25q128_rd_sr1(void)
{
uint8_t rec_data = 0;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ReadStatusReg1); /* 读状态寄存器1 0x05 */
rec_data = read_write_one_byte(0xFF);
W25Q128_CS(1); /* 拉高片选 */
return rec_data;
}
4.6 W25Q128写使能
写入数据/擦除之前必须写使能。
按照 W25Q128 写使能的工作时序:拉低 CS 片选 → 发送 06H → 拉高 CS 片选,编写代码。
void w25q128_write_enable(void)
{
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_WriteEnable); /* 发送写使能 0x06 */
W25Q128_CS(1); /* 拉高片选 */
}
4.7 W25Q128发送地址
read_write_one_byte
一次发送一字节数据,而 W25Q128 的地址有三字节,所以我们分三次发送。
static void w25q128_send_address(uint32_t address)
{
read_write_one_byte((uint8_t)((address)>>16)); /* 发送 bit23 ~ bit16 地址 */
read_write_one_byte((uint8_t)((address)>>8)); /* 发送 bit15 ~ bit8 地址 */
read_write_one_byte((uint8_t)address); /* 发送 bit7 ~ bit0 地址 */
}
4.8 W25Q128擦除一个扇区
传参 saddr
表示要擦除第几扇区,注意我们计算机是从0开始数数哦。剩下就是按工作时序写理论,注释写的很清楚啦,不多讲。
void w25q128_erase_sector(uint32_t saddr)
{
saddr *= 4096; /* 一扇区4096字节 */
w25q128_write_enable(); /* 写使能 */
w25q128_wait_busy(); /* 等待空闲 */
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_SectorErase); /* 发送扇区擦除命令 0x20 */
w25q128_send_address(saddr); /* 发送地址 */
W25Q128_CS(1); /* 拉高片选 */
w25q128_wait_busy(); /* 等待扇区擦除完成 */
}
4.9 W25Q128页写和读数据
传参 pbuf
:要写入/读取的数据,addr
:开始写入的地址,datalen
:字节数。剩下就是按工作时序写理论,注释写的很清楚啦,不多讲。
void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
w25q128_write_enable(); /* 写使能 */
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_PageProgram); /* 发送页写命令 0x02*/
w25q128_send_address(addr); /* 发送地址 */
for(i=0;i<datalen;i++)
{
read_write_one_byte(pbuf[i]); /* 循环写入 */
}
W25Q128_CS(1); /* 拉高片选 */
w25q128_wait_busy(); /* 等待写入结束 */
}
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
W25Q128_CS(0); /* 拉低片选 */
read_write_one_byte(FLASH_ReadData); /* 发送读取命令 0x03 */
w25q128_send_address(addr); /* 发送地址 */
for(i=0;i<datalen;i++)
{
pbuf[i] = read_write_one_byte(0XFF); /* 循环读取 */
}
W25Q128_CS(1); /* 拉高片选 */
}
4.10 主函数
我们向 W25Q128 写入一句“良许 嵌入式”,然后读出。
int main(void)
{
uint8_t datatemp[TEXT_SIZE];
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
uart1_init(115200); /* 串口初始化,波特率115200 */
printf("SPI通讯读写W25Q128模块...\r\n");
SPI1_Init();
w25q128_init();
/* 写入数据 */
sprintf((char *)datatemp, "良许 嵌入式");
w25q128_erase_sector(0); /* 擦除第一个扇区 */
w25q128_write_page(datatemp, 0x00000, TEXT_SIZE); /* 从第0位开始写 */
printf("数据写入完成!\r\n");
/* 读出数据 */
memset(datatemp, 0, TEXT_SIZE);
w25q128_read(datatemp, 0x00000, TEXT_SIZE); /* 从第0位开始读 */
printf("读出数据:%s\r\n", datatemp);
while(1)
{
}
}
4.11 最终效果
串口输出如下:
5. 小结
细心的小伙伴会发现我只是简单的写页、读数据、擦扇区。一页有256字节,那如果我第一页只写了50字节,又去第二页写100字节,这不是很浪费存储空间吗。不是我不会更完善的代码,源码我都藏着呢,只是作为入门教程这样的程度刚刚好,剩下的进阶优化就留作课后作业吧。
感谢各位看官,peace and love!
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
有收获?希望老铁们来个三连击,给更多的人看到这篇文章
推荐阅读:
- 程序员必备编程资料大全
- 程序员必备软件资源
欢迎关注我的博客:良许嵌入式教程网,满满都是干货!