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

目录

一 概述

二 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 Enable0x06写使能
Page Program0x02页写(最多 256 字节)
Read Data0x03读取数据
Sector Erase0x20擦除 4KB 扇区
Block Erase0xD8擦除 64KB 块
Chip Erase0xC7整片擦除
Read Status Register0x05

读取状态寄存器1

(目的是判断是否繁忙)

Manufacturer/Device ID0x90读取芯片 ID

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

三 代码部分

前言

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

W25Q64 引脚名称功能说明STM32 连接引脚( SPI1)
1CS片选(低电平有效)PA4
2DO数据输出(MISO)PA6
3GNDGND
4DI数据输入(MOSI)PA7
5CLK串行时钟PA5
6VCC电源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)具体内容如下:

位号名称含义说明
Bit7SRP状态寄存器保护位
Bit6SEC扇区保护指示
Bit5TB顶部/底部保护区域选择位
Bit4BP2块保护位 2
Bit3BP1块保护位 1
Bit2BP0块保护位 0
Bit1WEL写使能标志位(Write Enable Latch)
Bit0BUSYFlash 忙标志位(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 000x000000 ~ 0x0000FF
第 1 页0x0001 000x000100 ~ 0x0001FF
第 2 页0x0002 000x000200 ~ 0x0002FF
第 3 页0x0003 000x000300 ~ 0x0003FF
.........
第 255 页0x00FF 000x00FF00 ~ 0x00FFFF
第 256 页0x0100 000x010000 ~ 0x0100FF
.........

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

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

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

四 运行结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

关于 c、c#、c++ 三者区别

1. 起源与定位 语言起源时间开发者定位/特点C1972年Dennis Ritchie面向过程的编程语言&#xff0c;强调底层控制与高效性能C1983年Bjarne Stroustrup在 C 的基础上加入 面向对象编程&#xff08;OOP&#xff09;C#2000年微软&#xff08;Microsoft&#xff09;类似 Java&#…

7.7晚自习作业

实操作业02&#xff1a;Spark核心开发 作业说明 请严格按照步骤操作&#xff0c;并将最终结果文件&#xff08;命名为&#xff1a;sparkcore_result.txt&#xff09;于20点前上传。结果文件需包含每一步的关键命令执行结果文本输出。 一、数据读取与转换操作 上传账户数据$…

剑指offer第2版:动态规划+记忆化搜索

前三题是同一种模型&#xff0c;所以我分别用递推、记忆化、动归来做 一、p74-JZ10 斐波那契数列 斐波那契数列_牛客题霸_牛客网 class Solution { public:int Fibonacci(int n) {// write code hereif(n1||n2) return 1;int a1,b1,c1;while(n>2){cab;ab;bc;--n;}return c…

爬虫的笔记整理

网络爬虫首先要认识http和https协议 在浏览器中发送一个http请求&#xff1a; 1.输入一个URL地址之后&#xff0c;向http服务器发送请求&#xff0c;主要分为GET和POST两种方法 2.输入URL之后&#xff0c;发送一个request请求&#xff0c;这时候服务器把response文件对象发送…

Win11 安装 Visual Studio(保姆教程 - 更新至2025.07)

Visual Studio 安装&#xff08;保姆教程 - 更新至2025.07&#xff09; 前言安装须知安装过程1. 下载安装包2. 安装3. 注册4. 创建桌面快捷方式 前言 本教程针对 非计算机相关专业的小白用户 &#xff0c;手把手教你如何基于 win11 操作系统 安装 Visual Studio 2022。安装搭载…

【Linux网络编程】Socket - UDP

目录 V1版本 - Echo Server 初始化服务器 启动服务器 客户端 本地测试 网络测试 优化代码 V2版本 - Dict Server 服务器的调整 字典 网络模块与业务模块耦合 V3版本 - 简单聊天室 简单聊天室概述 消息转发模块 数据接收模块 重定向观察 补充细节 在这一篇文章…

【STM32】通用定时器PWM

STM32 通用定时器 PWM 输出完全解析&#xff08;以 TIM3_CH1 为例&#xff09; PWM 输出基本原理 PWM&#xff08;Pulse Width Modulation&#xff09;即脉冲宽度调制&#xff0c;是由定时器通过比较 CNT 与 CCR 寄存器实现的。 信号产生原理&#xff1a; ARR 决定周期&#…

重塑数学边界:人工智能如何引领数学研究的新纪元

目录 一、人工智能如何重新定义数学研究的边界 &#xff08;一&#xff09;数学与AI的关系&#xff1a;从基础理论到创新思维的回馈 &#xff08;二&#xff09;AI的创造力&#xff1a;突破传统推理的局限 &#xff08;三&#xff09;AI对数学研究的潜在贡献&#xff1a;创…

eNSP中实现vlan间路由通信(路由器)

eNSP中实现vlan间路由通信&#xff08;路由器&#xff09; 拓扑图 PC配置 pc1&#xff1a;192.168.10.1255.255.255.0192.168.10.254pc2&#xff1a;192.168.20.1255.255.255.0192.168.20.254pc3&#xff1a; 192.168.10.2255.255.255.0192.168.10.254pc4:192.168.20.2255.25…

Android PNG/JPG图ARGB_8888/RGB_565‌解码形成Bitmap在物理内存占用大小的简单计算

Android PNG/JPG图ARGB_8888/RGB_565‌解码形成Bitmap在物理内存占用大小的简单计算 Android的Bitmap 是一个用于表示图像数据的核心类&#xff0c;代表一张图片在内存中的存储&#xff0c;Bitmap存储了图像的像素信息数据。 Bitmap把图像理解为像素点组成的二维矩阵&#xff…

python实现简单的地图绘制与标记20250705

用python语言绘制显示范围不大于上海地区的地图 您的代码实现了一个 上海武馆地理信息系统&#xff0c;主要功能是通过可视化地图展示上海各区的传统武术馆信息。 通过和deeps对话一晚上实现的&#xff0c;我就是描述修改 高德的api key我搞了一会&#xff0c;平时很少接触密…

Kafka消息积压全面解决方案:从应急处理到系统优化

Kafka消息积压全面解决方案&#xff1a;从应急处理到系统优化 一、问题诊断与监控 1.1 确认积压情况 基础检查命令&#xff1a; # 查看消费者组滞后情况 kafka-consumer-groups.sh --bootstrap-server kafka:9092 \ --describe --group file-transcode-group# 查看主题详情…