当前位置: 首页 > article >正文

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 靠擦除。

它还有很多不同容量的好兄弟:

型号容量
W25Q256256M bits = 32M bytes
W25Q128128M bits = 16M bytes
W25Q6464M bits = 8M bytes
W25Q3232M bits = 4M bytes
W25Q1616M bits = 2M bytes
W25Q808M bits = 1M bytes

2.2 W25Q128模块参数及引脚介绍

W25Q128 的模块各个厂家做的各有不同,只是长得不一样而已,使用方式、引脚都是一样的。下面我介绍的是我们自绘的 W25Q128 模块。

W25Q128参数:

  • 产品容量:128M-bit(16M-byte)
  • 时钟频率:<=104MHz
  • 工作电压:2.7V ~ 3.6V
  • 工作温度:-40℃ ~ +85℃
  • 支持 SPI 接口

参考接线如下:

W25Q128STM32备注
VCC3.3电源正极
CSA4/B12片选信号
DOA6/B14输出
GNDG电源负极
CLKA5/B13时钟信号
DIA7/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 串行寄存器写入一个字节来发起传输。

  1. 首先,拉低相应的 SS 信号线,表示与特定的从机进行通信。
  2. 主机通过发送 SCLK 时钟信号告诉从机进行数据的读写操作。
  3. 注意,SCLK 时钟信号可以是低电平有效或高电平有效,因为SPI有不同的模式(下文将介绍)。
  4. 主机将要发送的数据写入发送数据缓冲区,然后通过移位寄存器逐位地将数据传输给从机的串行移位寄存器,使用 MOSI 信号线进行传输。同时,从机的 MISO 接口接收到的数据也经过移位寄存器一位一位地移到接收缓冲区。
  5. 从机也通过 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 模式CPOLCPHA空闲时 SCK 时钟采样边沿采样时刻
000低电平上升沿奇数边沿
101低电平下降沿偶数边沿
210高电平下降沿奇数边沿
311高电平上升沿偶数边沿

四个模式的时序图如下,方便大家理解。绿线表示开始与结束,黄线表示数据采样,蓝线表示数据发送。

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
W25Q128STM32USB 转 TTL
VCC3.3
CSA4
CLKA5
DOA6
DIA7
A10TX
A9RX
GGND

烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。

ST-Link V2STM32
SWCLKSWCLK
SWDIOSWDIO
GNDGND
3.3V3V3

接好如下图。开发板使用的是我们自绘的板子。大家也可以用自己的板子,只要是 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 随你挑!

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

  • 程序员必备编程资料大全
  • 程序员必备软件资源

欢迎关注我的博客:良许嵌入式教程网,满满都是干货!


http://www.kler.cn/a/450161.html

相关文章:

  • 探索AI代理在《我的世界》中的奇妙之旅:代理IP的角色与影响
  • springboot vue 会员营销系统
  • Redis篇--常见问题篇6--缓存一致性1(Mysql和Redis缓存一致,更新数据库删除缓存策略)
  • 牛客网 SQL36查找后排序
  • linux中docker命令大全
  • Set集合进行!contains判断IDEA提示Unnecessary ‘contains()‘ check
  • 微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
  • 如何详细地遵循RustDesk的步骤来搭建远程访问和自定义服务器?
  • 如何使用nvm来管理node版本
  • 线程的安全、volatile、synchronized
  • 【运维笔记】向日葵远程:输入法大写无法切换至小写
  • 【React中最优雅的异步请求】
  • vue3+vite一个IP对站点名称的前端curd更新-会议系统优化
  • Spark-Streaming receiver模式源码解析
  • Redis实现延迟任务 + RedisUtil升级
  • 音频接口:PDM TDM128 TDM256
  • QT-简单视觉框架代码
  • Spring Security 自动踢掉前一个登录用户,一个配置搞定!,网易前端社招面经
  • 前端框架Vue的路由机制
  • 【已解决】黑马点评项目Redis版本替换过程中误删数据库后前端显示出现的问题
  • 基于 SOME/IP 的动态服务发现与调用:原理、实现与示例全解析
  • selenium学习笔记(一)
  • 软件测试之非功能测试设计
  • 自然语言编写的prompt为啥比不上编程语言prompt高效?
  • LeetCode 209. 长度最小的子数组 (C++实现)
  • 编译libtorch时报错:NvToolsExt Could not open input file ***nvToolsExt64_1.lib