深入浅出SPI通信协议与STM32实战应用(W25Q128驱动)(实战部分)
1. W25Q128简介
W25Q128 是Winbond推出的128M-bit(16MB)SPI接口Flash存储器,支持标准SPI、Dual-SPI和Quad-SPI模式。关键特性:
- 工作电压:2.7V~3.6V
- 分页结构:256页/块,每块16KB,共1024块
- 支持页编程(256字节/页)
- 擦除操作支持:扇区擦除(4KB)、块擦除(32/64KB)、全片擦除
- 最高104MHz时钟频率
- 超过10万次擦写周期

1.1 W25Q128存储架构
W25Q128存储器采用分级存储架构,总容量为16MB(128Mbit)。其物理存储结构按以下层级划分:
- 顶层划分:256个存储块(Block),每块容量64KB
- 块内结构:每个存储块包含16个扇区(Sector),每扇区容量4KB
- 扇区组成:每个扇区细分为16个存储页(Page),每页容量256字节
该存储器的擦除机制具有以下特性:
- 最小擦除单位为单个扇区(4KB)
- 执行擦除操作时必须完整处理整个扇区
- 系统需预置4KB对齐的缓冲区,以满足物理擦除粒度要求

1.2 W25Q128常用指令
W25Q128 有非常多的指令,我们介绍几个常用的指令。
| 指令 | (HEX) | 名称 作用 |
|---|---|---|
| 0x06 | 写使能 | 写入数据/擦除之前,必须先发送该指令 |
| 0x05 | 读 SR1 | 判定 FLASH 是否处于空闲状态,擦除用 |
| 0x03 | 读数据 | 读取数据 |
| 0x02 | 页写 | 写入数据,最多写256字节 |
| 0x20 | 扇区擦除 | 扇区擦除指令,最小擦除单位 |
1.3 W25Q128 操作时序详解
- 写使能(06h)- 指令功能:执行编程/擦除操作的必要使能信号
- 操作流程:
① CS引脚置低(通信起始)
② 发送1字节指令码06h(MSB先发)
③ CS引脚置高(通信终止) - 状态影响:WEL位(SR1)置1,有效时间约50ms
- 操作流程:
- 状态寄存器读取(05h)
- 指令格式:
┌──────┬─────────────┐
| 指令码 | 响应数据(持续可读) |
└──────┴─────────────┘ - 操作时序**:
① CS置低 → 发送05h → 连续读取SR1 → CS置高 - 技术要点:
BUSY位(SR1)监控:0=空闲,1=操作中
支持全双工SPI模式(MOSI/MISO同步传输)
- 指令格式:
- 数据读取(03h)
- 指令结构:
┌──────┬──────────┬─────────┐
| 03h | 24bit地址 | 数据流(≥1字节) |
└──────┴──────────┴─────────┘ - 执行步骤:
- 起始地址对齐(按页边界自动递增)
- 单次读取最大长度:256字节(单页容量)
- 跨页读取需重新发送地址指令
- 指令结构:
- 页编程(02h)
- 技术限制:
- 目标区域必须处于擦除状态(全FFh)
- 单次写入跨度≤256字节(禁止跨页写入)
- 操作序列:
[CS↓] → 02h → A23-A0 → Data_1~Data_n → [CS↑] - 物理特性:
实际编程时间约0.7-1.2ms(需BUSY状态轮询)
- 技术限制:
- 扇区擦除(20h)
- 约束条件:
- 擦除粒度:4KB(地址自动对齐到扇区起始)
- 擦除后状态:全FFh(电平=1)
- 时序流程:
[CS↓] → 20h → 目标地址(A23-A0) → [CS↑] - 耗时范围:
典型值400ms(温度25℃),最大600ms
- 约束条件:
2. 硬件连接
这篇文章,使用的是W25Q128模块,W25Q128 的模块各个厂家做的各有不同,只是长得不一样而已,使用方式、引脚都是一样的。
通过产品手册如下:
STM32F103C8T6 与 W25Q128 接线方式:
| STM32引脚 | W25Q128引脚 | 功能 |
|---|---|---|
| PA5 | CLK | SPI时钟 |
| PA6 | MISO | 主机输入 |
| PA7 | MOSI | 主机输出 |
| PA4 | CS | 片选信号 |
| 3.3V | VCC | 电源 |
| GND | GND | 地 |
注意:CS引脚需通过GPIO控制,不可固定接低电平
3. 代码实现

3.1 SPI及GPIO初始化
在进行初始化时, 我们需要查看参考手册:
按照手册和实际情况进行GPIO口的配置
SPI_HandleTypeDef spi_handle = {0};
void w25q128_spi_init(void)
{spi_handle.Instance = SPI1;spi_handle.Init.Mode = SPI_MODE_MASTER;spi_handle.Init.Direction = SPI_DIRECTION_2LINES;spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW; /* CPOL = 0 */spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE; /* CPHA = 0 */spi_handle.Init.NSS = SPI_NSS_SOFT;spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;spi_handle.Init.CRCPolynomial = 7;HAL_SPI_Init(&spi_handle);
}void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{if(hspi->Instance == SPI1){GPIO_InitTypeDef gpio_initstruct;//打开时钟__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOB时钟__HAL_RCC_SPI1_CLK_ENABLE();//调用GPIO初始化函数gpio_initstruct.Pin = GPIO_PIN_4; gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP; gpio_initstruct.Pull = GPIO_PULLUP; gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio_initstruct);gpio_initstruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; gpio_initstruct.Mode = GPIO_MODE_AF_PP; HAL_GPIO_Init(GPIOA, &gpio_initstruct);gpio_initstruct.Pin = GPIO_PIN_6; gpio_initstruct.Mode = GPIO_MODE_INPUT; HAL_GPIO_Init(GPIOA, &gpio_initstruct);}
}
3.2 W25Q128驱动函数
w251128.c
// 交换一个字节
uint8_t w25q128_spi_swap_byte(uint8_t data)
{uint8_t recv_data = 0;HAL_SPI_TransmitReceive(&spi_handle, &data, &recv_data, 1, 1000);return recv_data;
}// 初始化:
void w25q128_init(void)
{w25q128_spi_init();
}// 读取iD值
uint16_t w25q128_read_id(void)
{uint16_t device_id = 0;W25Q128_CS(0);w25q128_spi_swap_byte(FLASH_ManufactDeviceID);w25q128_spi_swap_byte(0x00);w25q128_spi_swap_byte(0x00);w25q128_spi_swap_byte(0x00);device_id = w25q128_spi_swap_byte(FLASH_DummyByte) << 8;device_id |= w25q128_spi_swap_byte(FLASH_DummyByte);W25Q128_CS(1);return device_id;
}// 写使能void w25q128_writ_enable(void)
{W25Q128_CS(0);w25q128_spi_swap_byte(FLASH_WriteEnable);W25Q128_CS(1);
}// 读取状态寄存器1
uint8_t w25q128_read_sr1(void)
{uint8_t recv_data = 0;W25Q128_CS(0);w25q128_spi_swap_byte(FLASH_ReadStatusReg1);recv_data = w25q128_spi_swap_byte(FLASH_DummyByte);W25Q128_CS(1);return recv_data;
}// 等待BUSY
void w25q128_wait_busy(void)
{while((w25q128_read_sr1() & 0x01) == 0x01);
}
// 发送地址
void w25q128_send_address(uint32_t address)
{w25q128_spi_swap_byte(address >> 16);w25q128_spi_swap_byte(address >> 8);w25q128_spi_swap_byte(address);
}
// 读取数据
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size)
{uint32_t i = 0;W25Q128_CS(0);w25q128_spi_swap_byte(FLASH_ReadData);w25q128_send_address(address);for(i = 0; i < size; i++)data[i] = w25q128_spi_swap_byte(FLASH_DummyByte);W25Q128_CS(1);
}// 页写
void w25q128_write_page(uint32_t address, uint8_t *data, uint16_t size)
{uint16_t i = 0;w25q128_writ_enable();W25Q128_CS(0);w25q128_spi_swap_byte(FLASH_PageProgram);w25q128_send_address(address);for(i = 0; i < size; i++)w25q128_spi_swap_byte(data[i]);W25Q128_CS(1);//等待空闲w25q128_wait_busy();
}// 擦除
void w25q128_erase_sector(uint32_t address)
{//写使能w25q128_writ_enable();//等待空闲w25q128_wait_busy();//拉低片选W25Q128_CS(0);//发送扇区擦除指令w25q128_spi_swap_byte(FLASH_SectorErase);//发送地址w25q128_send_address(address);//拉高片选W25Q128_CS(1);//等待空闲w25q128_wait_busy();
}
w25q128.h
#ifndef __W25Q128_H__
#define __W25Q128_H__#include "sys.h"#define W25Q128_CS(x) do{ x ? \HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET): \HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); \}while(0)/* 指令表 */
#define FLASH_ManufactDeviceID 0x90
#define FLASH_WriteEnable 0x06
#define FLASH_ReadStatusReg1 0x05
#define FLASH_ReadData 0x03
#define FLASH_PageProgram 0x02
#define FLASH_SectorErase 0x20
#define FLASH_DummyByte 0xFFvoid w25q128_init(void);
uint16_t w25q128_read_id(void);
void w25q128_read_data(uint32_t address, uint8_t *data, uint32_t size);
void w25q128_write_page(uint32_t address, uint8_t *data, uint16_t size);
void w25q128_erase_sector(uint32_t address);#endif
3.3 使用示例
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "uart1.h"
#include "w25q128.h"uint8_t data_write[4] = {0xAA, 0xBB, 0xCC, 0xDD};
uint8_t data_read[4] = {0};
int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */led_init(); /* 初始化LED灯 */uart1_init(115200);w25q128_init();printf("hello world!\r\n");uint16_t device_id = w25q128_read_id();printf("device id: %X\r\n", device_id);// w25q128_erase_sector(0x000000);
// w25q128_write_page(0x000000, data_write, 4);w25q128_read_data(0x000000, data_read, 4);printf("data read: %X, %X, %X, %X\r\n", data_read[0], data_read[1], data_read[2], data_read[3]);while(1){ }
}
4. 注意事项
- 擦除操作必需:Flash写入前必须擦除对应区域
- 页边界限制:单次写入不可跨页(每页256字节)
- 时钟配置:确保SPI时钟不超过芯片规格(建议初始使用较低频率)
- 状态检测:重要操作后需检查BUSY位
- 电源稳定:Flash操作期间需保持3.3V稳定
5. 常见问题排查
- 无法读取ID:检查SPI模式(CPOL=0/CPHA=0),确认CS信号时序
- 写入失败:确保已执行写使能命令,检查电源电压
- 数据错误:注意地址对齐,排除SPI时钟干扰
完整工程代码可在Gitee获取:https://gitee.com/bad-lemon/mcu-development-record.git
实现效果:通过上述代码可实现W25Q128的读写操作,实测写入速度可达600KB/s,读取速度1.2MB/s(SPI时钟18MHz)。该方案适用于需要大容量非易失存储的物联网设备、数据采集系统等场景。
相关文章:
深入浅出SPI通信协议与STM32实战应用(W25Q128驱动)(实战部分)
1. W25Q128简介 W25Q128 是Winbond推出的128M-bit(16MB)SPI接口Flash存储器,支持标准SPI、Dual-SPI和Quad-SPI模式。关键特性: 工作电压:2.7V~3.6V分页结构:256页/块,每块16KB,共1…...
前端知识点---闭包(javascript)
文章目录 1.怎么理解闭包?2.闭包的特点3.闭包的作用?4 闭包注意事项:5 形象理解6 闭包的应用 1.怎么理解闭包? 函数里面包着另一个函数,并且内部函数可以访问外部函数的变量。 <script> function box() {//周围状态(外部函数中定义的…...
Java 泛型的逆变与协变:深入理解类型安全与灵活性
泛型是 Java 中强大的特性之一,它提供了类型安全的集合操作。然而,泛型的类型关系(如逆变与协变)常常让人感到困惑。 本文将深入探讨 Java 泛型中的逆变与协变,帮助你更好地理解其原理和应用场景。 一、什么是协变与…...
Web框架 --- Web服务器和Web应用服务器
Web框架 --- Web服务器和Web应用服务器 什么是HTTP Web服务器Web框架与Web服务器的关系 --- 以SpringBoot 和 Tomcat为例Simple Web Server Example 在日常开发的时候不管是用什么样的Web框架,比如Srpingboot或者ASP.Net, 我们只要在IDE里点击Run,项目就…...
【SpringBoot】98、SpringBoot中整合springdoc-openapi-ui接口文档
1、引入依赖 引入依赖<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-ui...
多线程(进阶)(内涵面试题)
目录 一、常见的锁策略 1. 悲观锁 vs 乐观锁 2. 重量级锁 vs 轻量级锁 3. 挂起等待锁 vs 自旋锁 4. 普通互斥锁 vs 读写锁 5. 可重入锁 vs 不可重入锁 6. 公平锁 vs 非公平锁 7. 面试题 二、synchronized的原理 1. 基本特点 2. 加锁工作过程 1)偏向锁&am…...
蓝桥杯补题
方法技巧: 1.进行循环暴力骗分,然后每一层的初始进行判断,如果已经不满足题意了,那么久直接continue,后面的循环就不用浪费时间了。我们可以把题目所给的等式,比如说有四个未知量,那么我们可以用…...
【MySQL篇】mysqlpump和mysqldump参数区别总汇
💫《博主主页》:奈斯DB-CSDN博客 🔥《擅长领域》:擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控;并对SQLserver、NoSQL(MongoDB)有了解 💖如果觉得文章对你有所帮…...
C++17模板编程与if constexpr深度解析
一、原理深化 1.1 模板编程 1.1.1 编译器如何处理模板(补充) 模板的实例化机制存在两种模式: 隐式实例化:编译器在遇到模板具体使用时自动生成代码,可能导致多翻译单元重复实例化,增加编译时间。显式实…...
SQL:DDL(数据定义语言)和DML(数据操作语言)
目录 什么是SQL? 1. DDL(Data Definition Language,数据定义语言) 2. DML(Data Manipulation Language,数据操作语言) DDL和DML的区别 什么是SQL? SQL(Structured …...
神舟平板电脑怎么样?平板电脑能当电脑用吗?
在如今的数码产品市场上,神舟平板电脑会拥有独特的优势,其中比较受到大家关注的就是神舟PCpad为例,无论是设计还是规格也会有很多的亮点,那么是不是可以直接当成电脑一起来使用呢? 这款平板电脑就会配备10.1英寸显示屏…...
【力扣hot100题】(075)数据流的中位数
一开始只建立了一个优先队列,每次查询中位数时都要遍历一遍于是喜提时间超限,看了答案才恍然大悟原来还有这么聪明的办法。 方法是建立两个优先队列,一个大根堆一个小根堆,大根堆记录较小的数,小根堆记录较大的数。 …...
AI大模型从0到1记录学习 day15
14.3.5 互斥锁 1)线程安全问题 线程之间共享数据会存在线程安全的问题。 比如下面这段代码,3个线程,每个线程都将g_num 1 十次: import time import threading def func(): global g_num for _ in range(10): tmp g_num 1 # ti…...
43. Java switch 语句 null 选择器变量
文章目录 43. Java switch 语句 null 选择器变量null 选择器变量示例:处理 null 选择器变量程序输出:解释 📖 为什么需要这样做? 🤔更进一步:使用 Optional 避免 null 检查示例:使用 Optional 包…...
linux下MMC_TEST的使用
一:打开如下配置,将相关文件编译到内核里: CONFIG_MMC_TEST CONFIG_MMC_DEBUG CONFIG_DEBUG_FS二:将mmc设备和mmc_test驱动进行绑定 2.1查看mmc设备编号 ls /sys/bus/mmc/drivers/mmcblk/mmc0:aaaa2.2将mmc设备与原先驱动进行解绑 echo mmc0:aaaa >...
Java——pdf增加水印
文章目录 前言方式一 itextpdf项目依赖引入编写PDF添加水印工具类测试效果展示 方式二 pdfbox依赖引入编写实现类效果展示 扩展1、将inputstream流信息添加水印并导出zip2、部署出现找不到指定字体文件 资料参考 前言 近期为了知识库文件导出,文件数据安全处理&…...
leetcode_19. 删除链表的倒数第 N 个结点_java
19. 删除链表的倒数第 N 个结点https://leetcode.cn/problems/remove-nth-node-from-end-of-list/ 1、题目 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例 1: 输入:head [1,2,3,4,5], n 2 输出&#…...
41、web前端开发之Vue3保姆教程(五 实战案例)
一、项目简介和需求概述 1、项目目标 1.能够基于Vue3创建项目 2.能够基本Vue3相关的技术栈进行项目开发 3.能够使用Vue的第三方组件进行项目开发 4.能够理解前后端分离的开发模式 2、项目概述 使用Vue3结合ElementPlus,ECharts工具实现后台管理系统页面,包含登录功能,…...
zsh: command not found: hdc - 鸿蒙 HarmonyOS Next
终端中执行 hdc 命令抛出如下错误; zsh: command not found: hdc 解决办法 首先,查找到 DevEco-Studio 的 toolchains 目录路径; 其次,按照类似如下的文件夹层级结果推理到 toolchains 子级路径下,其中 sdk 后一级的路径可能会存在差异,以实际本地路径结构为主,直至找到 ope…...
ffpyplayer+Qt,制作一个视频播放器
ffpyplayerQt,制作一个视频播放器 项目地址FFmpegMediaPlayerVideoWidget 项目地址 https://gitee.com/chiyaun/QtFFMediaPlayer FFmpegMediaPlayer 按照 QMediaPlayer的方法重写一个ffpyplayer # coding:utf-8 import logging from typing import Unionfrom PySide…...
transformer 中编码器原理和部分实现
编码器部分实现 目标 了解编码器中各个组成部分的作用掌握编码器中各个组成部分的实现过程 编码器部分 由N个编码器堆叠组成每个编码器由两个子层连接结构组成第一个子连接结构包括一个多头注意力子层和规范化层以及一个残差连接第二个子层连接结构包括一个前馈全链接子层和…...
MySQL多表查询实战指南:从SQL到XML映射的完整实现(2W+字深度解析)
MySQL多表查询实战指南:从SQL到XML映射的完整实现(2W+字深度解析) 第一章 多表查询基础与核心原理 1.1 关系型数据库设计范式 以电商系统为例的三范式实践: -- 原始数据表(违反第三范式) CREATE TABLE orders (order_id INT PRIMARY KEY,customer_name VARCHAR(50),p…...
蓝桥杯--寻找整数
题解 public static void main(String[] args) {int[] mod {0, 0, 1, 2, 1, 4, 5, 4, 1, 2, 9, 0, 5, 10, 11, 14, 9, 0, 11, 18, 9, 11, 11, 15, 17, 9, 23, 20, 25, 16, 29, 27, 25, 11, 17, 4, 29, 22, 37, 23, 9, 1, 11, 11, 33, 29, 15, 5, 41, 46};long t lcm(2, 3);lo…...
Kafka 中,为什么同一个分区只能由消费者组中的一个消费者消费?
在 Kafka 中,同一个分区只能由消费者组中的一个消费者消费,这是 Kafka 的设计决策之一,目的是保证消息的顺序性和避免重复消费。这背后有几个关键的原因: 1. 保证消息顺序性 Kafka 中的每个 分区(Partitionÿ…...
自然语言处理入门6——RNN生成文本
一、文本生成 我们在前面的文章中介绍了LSTM,根据输入时序数据可以输出下一个可能性最高的数据,如果应用在文字上,就是根据输入的文字,可以预测下一个可能性最高的文字。利用这个特点,我们可以用LSTM来生成文本。输入…...
$R^n$超平面约束下的向量列
原向量: x → \overset{\rightarrow}{x} x→ 与 x → \overset{\rightarrow}{x} x→法向相同的法向量(与 x → \overset{\rightarrow}{x} x→同向) ( x → ⋅ n → ∣ n → ∣ 2 ) n → (\frac{\overset{\rightarrow}x\cdot\overset{\righta…...
FPGA_DDR错误总结
1otp 31-67 解决 端口没连接 必须赋值; 2.PLACE 30-58 TERM PLINITCALIBZ这里有问题 在顶层输出但是没有管脚约束报错 3.ERROR: [Place 30-675] 这是时钟不匹配IBUF不在同一个时钟域,时钟不在同一个时钟域里,推荐的不建议修改 问题 原本…...
k8s之Service类型详解
1.ClusterIP 类型 2.NodePort 类型 3.LoadBalancer 类型 4.ExternalName 类型 类型为 ExternalName 的 Service 将 Service 映射到 DNS 名称,而不是典型的选择算符, 例如 my-service 或者 cassandra。你可以使用 spec.externalName 参数指定这些服务…...
NOIP2011提高组.玛雅游戏
目录 题目算法标签: 模拟, 搜索, d f s dfs dfs, 剪枝优化思路*详细注释版代码精简注释版代码 题目 185. 玛雅游戏 算法标签: 模拟, 搜索, d f s dfs dfs, 剪枝优化 思路 可行性剪枝 如果某个颜色的格子数量少于 3 3 3一定无解因为要求字典序最小, 因此当一个格子左边有…...
网络安全应急响应之文件痕迹排查:从犯罪现场到数字狩猎的进化论
凌晨3点,某金融企业的服务器突然告警,核心数据库出现未知进程访问。安全团队紧急介入时,攻击者已抹去日志痕迹。在这场与黑客的时间赛跑中,文件痕迹排查成为破局关键。本文将带您深入数字取证的"案发现场",揭…...
