stm32--SPI原理应用W25Q64(二)

发布于:2025-07-08 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

一 概述

二 W25Q64的介绍

简介

硬件样子

主要特性

常用 SPI 指令

三 代码部分

前言

SPI.c

新加代码第一部分:

新加代码第二部分:

新加代码第三部分:

 新加代码第四部分:

新加代码第五部分(读):

新加代码第六部分(写):

新加代码第七部分(获取地址ID):

main.c


一 概述

我已经在前面一章《stm32-掌握SPI原理(一)》,已经讲解完了spi的一些底层架构的内容,本篇文章,我们会通过W25Q64这个芯片去给大家展示如何利用SPI原理去往W25Q64这个芯片里烧写内容,并且通过串口打印出来相应的内容。本篇文章他会更加强调于应用,其实会用底层函数就行,大家不需要独立写出相应的SPI.c文件里的代码。

二 W25Q64的介绍

简介

W25Q64 是 Winbond 出品的一款 串行 SPI Flash 存储器,全名为 W25Q64JV,其中“64”表示其容量为 64 Mbit = 8MB。它采用 SPI 通信协议,适用于嵌入式系统中需要大容量非易失性存储的场景,Flash 是非易失性存储器断电后数据不会丢失,可以多次擦写,但寿命是有限的。如:

物联网设备、数据记录器、音频/图像缓存、配置文件存储等。

硬件样子

主要特性

特性项 描述
接口协议 SPI(最高支持 104MHz)
工作电压 2.7V ~ 3.6V(所以后续接线的时候一定要接3.3V
容量 64 Mbit(= 8MB)
最小可写单位 页(Page) = 256 字节
最小可擦除单位 扇区(Sector) = 4KB
较大擦除单位 块(Block)= 64KB 或 32KB
擦除整片 支持
ID 读取 支持读取厂商 ID 和设备 ID
状态寄存器 三个(SR1, SR2, SR3)

我们写数据时,通常以“页”为单位写,以“扇区”或“块”为单位擦除,而我们今天的w25q64用的是“扇区”来擦除。

常用 SPI 指令

指令名称 指令码 功能
Write Enable 0x06 写使能
Page Program 0x02 页写(最多 256 字节)
Read Data 0x03 读取数据
Sector Erase 0x20 擦除 4KB 扇区
Block Erase 0xD8 擦除 64KB 块
Chip Erase 0xC7 整片擦除
Read Status Register 0x05

读取状态寄存器1

(目的是判断是否繁忙)

Manufacturer/Device ID 0x90 读取芯片 ID

我们后面结合代码去讲解,就有许多指令,大家有个印象就行,不需要我们背下来,以上是常用的一些指令,如果有需要,去翻阅w25q64的芯片手册即可。

三 代码部分

前言

在正式进入代码学习之前,我们要进行硬件的连接

W25Q64 引脚 名称 功能说明 STM32 连接引脚( SPI1)
1 CS 片选(低电平有效) PA4
2 DO 数据输出(MISO) PA6
3 GND GND
4 DI 数据输入(MOSI) PA7
5 CLK 串行时钟 PA5
6 VCC 电源 3.3V

SPI.c

void W25Q64_SPI_Init(void)
{
	
	GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    // CS 管脚(PA4)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_4);  // 拉高片选

    // SCK (PA5), MOSI (PA7)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
	/*
	  为什么要把cs配置成推挽输出,而把SCK和MOSI配置成复用推挽呢?
		  答:这些引脚(如 PA5、PA7)是由 SPI1 外设控制的。
		你不能用 GPIO_SetBits/GPIO_ResetBits 控制它,而是通过 SPI_SendData 控制。
		而对于CS来说,他只是作为普通的IO口
	*/

    // MISO (PA6)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // SPI 配置
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;		// CPOL=0,空闲为低
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;	// CPHA=0,第一个边沿采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStructure);

    SPI_Cmd(SPI1, ENABLE);
	
	
	
}

//发送/接收一个字节
uint8_t w25q64_spi_swap_byte(uint8_t data)
{
	//TXE 为 1 表示可以写入数据了
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, data);
	//RXNE 为 1 表示已经有数据被接收到,可以读取数据
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
	
	//再将SPI1读取的数据,再返还给这个函数的返回值
    return SPI_I2S_ReceiveData(SPI1);
}

 以上代码是在上一篇文章《stm32-掌握SPI原理(一)》中已经写好的代码,如果以上代码没有看明白,请看上一篇文章,接下来,我们进行其他代码的讲解。

新加代码第一部分:

void W25Q64_CS_LOW(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); }
void W25Q64_CS_HIGH(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); }

因为SPI通信是需要先把片选信号拉低再进行数据通信,如果想关闭SPI通信则拉高片选。

新加代码第二部分:

void W25Q64_WriteEnable(void)
{
    W25Q64_CS_LOW();
    w25q64_spi_swap_byte(0x06);  // 写使能指令
    W25Q64_CS_HIGH();
}

 发送 0x06 指令,让 Flash 进入“写允许”状态(必须的前置步骤)

uint8_t W25Q64_ReadSR1(void)
{
    uint8_t status;
    W25Q64_CS_LOW();
	w25q64_spi_swap_byte(0x05);
/*
	发送指令 0x05 给 W25Q64 芯片,表示我要读取状态寄存器1
	通过 SPI 发送 0x05 指令,这是 W25Q64 固定的“读取状态寄存器1”命令。
	目的要判断是否繁忙
*/
    status = w25q64_spi_swap_byte(0xFF);
/*
    这一步“其实是为了接收”,发什么无所谓,0xFF 只是占位
    这是因为 SPI 协议的本质是“全双工同步通信”,
    所以你在 接收数据时,必须同时“发送”一个字节,不然 SPI 时钟不会运行
    Slave 就不会把数据发送出来。
*/
    W25Q64_CS_HIGH();
    return status;
}

该函数的作用是读取状态寄存器1,判断 Flash 当前状态,常用于判断忙不忙(BSY 位),那么W25Q64里面的状态寄存器1到底有什么呢,W25Q64 中的状态寄存器1(SR1)具体内容如下:

位号 名称 含义说明
Bit7 SRP 状态寄存器保护位
Bit6 SEC 扇区保护指示
Bit5 TB 顶部/底部保护区域选择位
Bit4 BP2 块保护位 2
Bit3 BP1 块保护位 1
Bit2 BP0 块保护位 0
Bit1 WEL 写使能标志位(Write Enable Latch)
Bit0 BUSY Flash 忙标志位(1 = 正在编程/擦除,0 = 空闲)

而我们要判断flash忙不忙,就只需要看Bit0这一位即可,如果他忙了,就不让SPI通信,如果不忙就进行通信。

新加代码第三部分:

void W25Q64_WaitBusy(void)
{
    while((W25Q64_ReadSR1() & 0x01) == 0x01);
	//这里用的是 &(按位与),而不是 &&(逻辑与)
}

在这里给大家简单的总结一下&和&&的区别:

运算符 名称 用于 示例
& 按位与 对两个数的二进制位逐位比较 0x05 & 0x010x01
&& 逻辑与 用于判断两个条件都为真 a && b → 如果都非 0,则为真

 而我们在这里,只需要判断第一位Bit0位是不是1,如果是1,就会卡在W25Q64_WaitBusy这个函数里,不往下运行。如果是0,代表空闲。

 新加代码第四部分:

void W25Q64_SendAddress(uint32_t addr)
{
    w25q64_spi_swap_byte((addr >> 16) & 0xFF);
    w25q64_spi_swap_byte((addr >> 8) & 0xFF);
    w25q64_spi_swap_byte(addr & 0xFF);
	/*
	将一个24位地址拆成3个字节,从高到低依次通过 SPI 发送给 Flash 芯片,是读、写、擦操作的前置步骤
	*/
}

新加代码第五部分(读):

void W25Q64_ReadData(uint32_t addr, uint8_t* buf, uint32_t size)
{
    uint32_t i;
    W25Q64_CS_LOW();
    w25q64_spi_swap_byte(0x03);
    W25Q64_SendAddress(addr);
    for(i = 0; i < size; i++)
        buf[i] = w25q64_spi_swap_byte(0xFF);
    W25Q64_CS_HIGH();
}

为什么我们这里是先发送0x03(读数据的命令),而不是像I2C一样先发地址呢?这就是SPI独特的地方,举个通俗的比喻:你要去图书馆借书,流程是这样的:

你说:“我要看书” —— 这一步就像 w25q64_spi_swap_byte(0x03); 发读取指令。

图书馆说:“你要看哪本?” —— 你就给它地址:0x000123

图书馆才从书架上帮你取数据 —— 开始读数据阶段。

这是因为 SPI 是“流式指令式通信”,不像 I2C 那样有“设备地址 + 内部寄存器地址”的概念。W25Q64 是 SPI Flash,它规定:

你要读取数据,必须先发一个读命令(0x03),再告诉我地址,我才知道你要读哪里

新加代码第六部分(写):

void W25Q64_WritePage(uint32_t addr, uint8_t* data, uint16_t size)
{
    uint16_t i;
    W25Q64_WriteEnable();
    W25Q64_CS_LOW();
    w25q64_spi_swap_byte(0x02);
    W25Q64_SendAddress(addr);
    for(i = 0; i < size; i++)
        w25q64_spi_swap_byte(data[i]);
    W25Q64_CS_HIGH();
    W25Q64_WaitBusy();
}

如果大家观察细心的话会发现:写比读多了两个函数W25Q64_WriteEnable()和W25Q64_WaitBusy();这是因为在 W25Q64 中,所有会更改数据的操作(写入/擦除)都必须经过“写使能”授权,并等待芯片完成操作;而读取操作是非破坏性的,因此不需要这些前置步骤。

 新加代码第七部分(擦):

void W25Q64_EraseSector(uint32_t addr)
{
    W25Q64_WriteEnable();
    W25Q64_WaitBusy();
    W25Q64_CS_LOW();
    w25q64_spi_swap_byte(0x20);
    W25Q64_SendAddress(addr);
    W25Q64_CS_HIGH();
    W25Q64_WaitBusy();
}

擦除 Flash 中以“扇区”为单位的 4KB 区域

新加代码第七部分(获取地址ID):

uint16_t W25Q64_ReadID(void)
{
    uint16_t device_id = 0;
    W25Q64_CS_LOW();
    w25q64_spi_swap_byte(0x90);
    w25q64_spi_swap_byte(0x00);
    w25q64_spi_swap_byte(0x00);
    w25q64_spi_swap_byte(0x00);
    device_id = w25q64_spi_swap_byte(0xFF) << 8;
    device_id |= w25q64_spi_swap_byte(0xFF);
    W25Q64_CS_HIGH();
    return device_id;
}

获取芯片的制造商 ID 和设备 ID,这个并不是必须要用的,大家选择性运用

main.c

#include "stm32f10x.h"
#include "spi.h"
#include "usart.h"

uint8_t tx_data[16] = "Hello STM32!";
uint8_t rx_data[16] = {0};

int main(void)
{
    my_usart_Config();       // 串口初始化
    W25Q64_SPI_Init();       // SPI 初始化

    printf("Start test...\r\n");

    uint16_t id = W25Q64_ReadID();
    printf("Flash ID = 0x%04X\r\n", id);

    if(id == 0xEF16)  // W25Q64 正常 ID;w25q128 的 ID  0xEF17
    {
        printf("W25Q64 detected.\r\n");

        // 擦除扇区
        W25Q64_EraseSector(0x000000);
        printf("Sector erased.\r\n");

        // 写入一页,sizeof(rx_data) 的作用是:告诉函数要读取多少个字节的数据
        W25Q64_WritePage(0x000000, tx_data, sizeof(tx_data));
        printf("Page written.\r\n");

        // 读取数据
        W25Q64_ReadData(0x000000, rx_data, sizeof(rx_data));

        // 打印读取的数据
        printf("Read data: %s\r\n", rx_data);
    }
    else
    {
        printf("W25Q64 not found.\r\n");
    }

    while(1);
}

 如上是我利用串口打印出来的w25q64里面的内容,当然,我会给大家讲的明明白白的,第零页是0x000 000,那第二页呢?如果我想写到别的页上怎么办呢?

页编号 起始地址(十六进制) 范围
第 0 页 0x0000 00 0x000000 ~ 0x0000FF
第 1 页 0x0001 00 0x000100 ~ 0x0001FF
第 2 页 0x0002 00 0x000200 ~ 0x0002FF
第 3 页 0x0003 00 0x000300 ~ 0x0003FF
... ... ...
第 255 页 0x00FF 00 0x00FF00 ~ 0x00FFFF
第 256 页 0x0100 00 0x010000 ~ 0x0100FF
... ... ...

 so,我们如果想写到别的页上内容的话,大家明白了吧,我们只需要改写前四个位置的数值

  •  比如你想写第 3 页,就把地址写成 3 × 256 = 0x000300
  • 想写第 10 页?那就 10 × 256 = 0x000A00
  • 总之:页号 × 256 = 你要写入的起始地址。

 我再多说一句:W25Q64正常ID是0xEF16;w25q128的ID是0xEF17

四 运行结果


网站公告

今日签到

点亮在社区的每一天
去签到