STM32之SPI详解
一、SPI和Flash存储
1. SPI基本概述
1.1 SPI 概述
SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线协议,由摩托罗拉 (现为 NXP)公司开发。它被广泛应用于微控制器(MCU)、传感器、存储器(如 Flash、SD 卡)、实时时钟(RTC)、数字信号处理器(DSP)等各种外围设备之间的短距离通信。 由于其简单性和高效率,SPI 是嵌入式系统中最常用的通信协议之一。
1.2 SPI的连接方式
- SPI 属于一主多从方式。Master 主机 Slave 从机
- SCLK 时钟线,要求主机必须提供主机和从机之间通信使用的时钟频率。用于控制当前 SPI 工作模式
- MOSI/sdo, Master Output, Slave Input,主机发送数据,从机接收数据,主机发送数据端口。
- MISO/sdi, Master Input, Slave Output,从机发送数据,主机接受数据,主机接受数据端口
- SS/cs, SPI Select/chip select, 片选线,根据当前 SS/cs 高电平选择,选择当前主机对应通信的对应从机是哪一个。
补充说明
- SPI 全双工实现是依赖于 MISO/sdi 和 MOSI/sdo,利用两根数据通信线,基于 SCLK 时钟完成数据通信,可以同时进行读写操作。
- SS/cs SPI 实现一主多从方式是依赖于片选线,多组设备连接一组 SPI 协议接口,需要提供多个 SS/cs 片选线控制通信外设选择。
1.3 SPI 的四种工作模式
-
CPOL (Clock Polarity):时钟极性
- 决定了 SCK 时钟信号在空闲时的状态。
- CPOL = 0:时钟空闲时为低电平。
- CPOL = 1:时钟空闲时为高电平。
-
CPHA (Clock Phase):时钟相位
- 决定了数据是在时钟的第几个边沿被采样 / 捕获 / 读取 ==> 对应 MISO/sdi。
- CPHA = 0:数据在时钟的第一个边沿(即第一个跳变沿)被采样。
- CPHA = 1:数据在时钟的第二个边沿被采样。
依据以上两个控制变量匹配,对应 SPI 有四种工作模式 通过组合 CPOL 和 CPHA,就构成了 SPI 的四种工作模式:
-
模式 0 (Mode 0):CPOL=0, CPHA=0
-
模式 1 (Mode 1):CPOL=0, CPHA=1
-
模式 2 (Mode 2):CPOL=1, CPHA=0
-
模式 3 (Mode 3):CPOL=1, CPHA=1
工作模式中,使用 SPI0 和 SPI3 较多。

1.4 SPI 数据形式
- 主要分析 SPI 数据 0 和 数据 1
- SPI 数据是依赖于 SCLK 时钟线,MISO/sdi 和 MOSI/sdo 数据线完成分析,同时需要考虑当前 SPI 的工作模式
- 按照 SPI0 工作模式 CPOL = 0(时钟空闲位低电平),CPHA = 0(第一个跳变沿采集数据)分析 SPI 数据 0 和 1

1.5 SPI 环形总线结构
2. STM32 SPI 支持
2.1 SPI 框图和技术特征
SPI 移位寄存器的核心工作机制是:以数据帧长度(通常 8 位)为容量,在每个时钟沿同步完成 “1 位发送” 和 “1 位接收”—— 按 MSB 优先(默认)或 LSB 优先的方向,待发数据从移位寄存器最高位(或最低位)“移出” 到 MOSI 总线,同时 MISO 总线上的外部数据从最低位(或最高位)“移入” 寄存器,通过连续多个时钟沿的移位动作,最终完成一整帧数据的全双工交换。
SPI 移位寄存器的核心工作机制是:以数据帧长度(通常 8 位)为容量,在每个时钟沿同步完成 “1 位发送” 和 “1 位接收”—— 按 MSB 优先(默认)或 LSB 优先的方向,待发数据从移位寄存器最高位(或最低位)“移出” 到 MOSI 总线,同时 MISO 总线上的外部数据从最低位(或最高位)“移入” 寄存器,通过连续多个时钟沿的移位动作,最终完成一整帧数据的全双工交换。
2.2 SPI 配置
2.2.1 MCU SPI 配置为主模式
2.2.2 2 SPI 对应引脚和时钟分析
- 根据当前原理图分析,时钟使能对应 GPIOB 和 SPI2
- GPIOB 对应使用控制寄存器是 APB2ENR
- SPI2 对应使用控制寄存器是 APB1ENR,注意 APB1 外设最大的时钟频率为 36 MHz
3. W25Q128 Flash 存储芯片
3.1 存储芯片基本内容
W25Q128 是华邦电子(Winbond)生产的一款非常流行和经典的 128M - bit 串行 Flash 存储器芯片。它采用 SPI(串行外设接口)进行通信,因其容量适中、接口简单、成本低廉、可靠性高,被广泛应用于各种电子设备中。
除 W25Q128 版本还有:
- W25Q128 ==> 实际容量是 16 MB
- W25Q64 ==> 实际容量是 8 MB
- W25Q32 ==> 实际容量是 4 MB
- W25Q16 ==> 实际容量是 2 MB
基于当前 STM32 单片机内部的 Flash 存储空间较少,可以利用外部的 Flash 芯片作为存储设备。可以用于车辆里程信息存储,指纹锁密码和指纹信息存储,RFID 读卡器数据存储。
3.2 W25Q128 芯片技术参考内容
- W25Q128 是一款高性能、低功耗、高可靠性的串行闪存芯片,适用于嵌入式、IoT、汽车电子、工业控制等多种应用场景。其 SPI/QSPI 接口、高速读写、工业级温度范围等特点,使其成为现代电子系统中的理想存储解决方案
- 存储容量:128Mbit(16MB),采用 16M×8 的组织结构。
- 接口类型:
- 标准 SPI(Serial Peripheral Interface)
- Dual SPI(双线 SPI)
- Quad SPI(四线 SPI)
- QPI(快速四线 SPI)
- 时钟频率:最高 104MHz(标准 SPI),在 Quad SPI 模式下可进一步提升数据传输速率。
- 写入速度:
- 字写周期 50μs
- 页写周期 3ms(256 字节 / 页)。
- 擦除单位:
- 扇区擦除(4KB)
- 块擦除(64KB)
- 整片擦除(16MB)。
- 工作电压:2.7V - 3.6V,适用于多种嵌入式系统。满足 3.3V 标准 MCU 工作电压需求。
- 温度范围:工业级 - 40°C ~ +85°C,适用于严苛环境。
- 封装形式:
- SOP - 8(小型封装)
- WSON - 8(更紧凑的封装)。
- 数据保持时间:20 年(非易失性存储)。
- 擦写寿命:100,000 次(每个存储单元可擦写 10 万次)。
- 电流数据:
- 工作电流低于 5 mA
- 掉电电流低于 1 μA
3.3 W25Q128 控制线和数据模式
- 片选 CS / SS
- CS 引脚用于启用或禁用设备操作:
- 当 CS 为高电平时:
- 设备处于未选中状态,串行数据输出引脚(DQ 或 IO0、IO1、IO2、IO3)呈高阻状态;
- 若设备未执行内部擦除、编程或状态寄存器写入操作,其功耗将降至待机水平,电流 1 μA
- 当 CS 拉低时:
- 设备被选中,功耗升至工作状态电流不大于 5 mA,此时可向设备写入指令或读取数据;
- 上电后,必须确保 CS 经历一次从高到低的跳变,设备才会接受新指令。
- 当 CS 为高电平时:
- CS 引脚用于启用或禁用设备操作:
- 输入输出 DI、DO
- 4.2 串行数据输入、输出及输入 / 输出端口(DI、DO 与 IO0、IO1、IO2、IO3)
W25Q128FV 支持标准 SPI、双线 SPI 和四线 SPI 操作。在标准 SPI 模式下:- 单向 DI(输入)引脚用于在串行时钟(CLK)输入引脚的上升沿,向设备串行写入指令、地址或数据;DI 连接的引脚是 MCU MOSI/sdo
- 单向 DO(输出)引脚则在 CLK 的下降沿从设备读取数据或状态。DO 链接引脚是 MCU 的 MISO/sdi
- 4.2 串行数据输入、输出及输入 / 输出端口(DI、DO 与 IO0、IO1、IO2、IO3)
- 写保护 WP
- WP 引脚用于防止状态寄存器被意外写入,其特性如下:
- 硬件写保护机制:
- 通过与状态寄存器的块保护位(CMP、SEC、TB、BP2、BP1、BP0)及状态寄存器保护位(SRP)配合,可实现从最小 4KB 扇区到整个存储阵列的硬件保护。
- 该引脚为低电平有效(Active Low),当拉低时激活保护功能。
- 四线模式下的功能切换:
- 若状态寄存器 - 2 中的 QE 位(Quad 使能位)被置 1,设备将进入四线 / I/O 模式,此时 I/WP 引脚功能失效,转而作为 IO2 信号线使用。
- 四线 / I/O 模式的引脚配置详见图 1a 至 1c。
- WP 引脚用于防止状态寄存器被意外写入,其特性如下:
- Hold 引脚
- 暂停与恢复控制:
- 当 CS 为低电平(设备选中)且 HOLD 被拉低时:
- DO 引脚将进入高阻态;
- DI 和 CLK 引脚上的信号将被忽略(无效状态)。
- 当 HOLD 恢复高电平后,设备操作将继续执行。
- 当 CS 为低电平(设备选中)且 HOLD 被拉低时:
- 多设备共享 SPI 总线时的应用:
- 该功能特别适用于多个设备共享同一组 SPI 信号线的场景,可通过 / HOLD 临时挂起当前设备以切换总线控制权。
- 引脚为低电平有效(Active Low)。
- 四线模式下的功能切换:
- 若状态寄存器 - 2 中的 QE 位(Quad 使能位)被置 1,设备进入四线 / I/O 模式,此时 / HOLD 引脚功能失效,转而作为 IO3 信号线使用。
- 四线 / I/O 模式的引脚配置详见
- 暂停与恢复控制:
3.4 地址规则【重点】
- 块 Block
- 块区域字节数是 64 KB,按照一块字节数数量,地址范围是 0x0000 ~ 0xFFFF
- 当前设备是 W25Q128 对应 16 MB 空间,当前设备一共有 256 块 (64KB),按照块进行编号 0x00 ~ 0xFF
- 整个 Flash 空间中每一个字节对应的地址是 0x000000 ~ 0xFFFFFF
- 扇区
- 每一个扇区占用 4 KB,一块 ==> 16 扇区
- 在当前块中,对应扇区的编号是 0x0 ~ 0xF (0 ~ 15)
- 页
- 每一页占用 256 字节,一个扇区 ==> 16 页
- 在当扇区中,对应页的编号是 0x0 ~ 0xF (0 ~ 15)
- 页内字节
- 每一页占用 256 字节
- 页内字节对应的地址凡是 0x00 ~ 0xFF (0 ~ 255)
需要在第 12 块中,第 10 个扇区,第 5 页中的页内 108 个字节写入一个字节数据
- 对应地址 0x0B946B
例如:
3.5 W25Q128 指令对照表
指令名称 | 操作码 (Hex) | 功能描述 | 时钟周期 (Clock Cycles) | 地址字节 (Address Bytes) | 哑字节 (Dummy Bytes) | 数据字节 (Data Bytes) |
---|---|---|---|---|---|---|
写使能 | 0x06 | 允许后续的写入或擦除操作 | 8 | 0 | 0 | 0 |
写禁止 | 0x04 | 禁止后续的写入或擦除操作 | 8 | 0 | 0 | 0 |
读状态寄存器 - 1 | 0x05 | 读取状态寄存器 1 的值(位 S7 到 S0) | 8+8 | 0 | 0 | 1+ (可连续读) |
读状态寄存器 - 2 | 0x35 | 读取状态寄存器 2 的值(位 S15 到 S8) | 8+8 | 0 | 0 | 1+ (可连续读) |
写状态寄存器 | 0x01 | 写入状态寄存器(同时配置 S7-S0 和 S15-S8) | 8+16 | 0 | 0 | 2 |
页编程 | 0x02 | 将数据写入芯片(最大 256 字节为一页) | 8+24 | 3 | 0 | 1-256 |
扇区擦除 (4KB) | 0x20 | 擦除一个 4KB 大小的扇区 | 8+24 | 3 | 0 | 0 |
块擦除 (32KB) | 0x52 | 擦除一个 32KB 大小的块 | 8+24 | 3 | 0 | 0 |
块擦除 (64KB) | 0xD8 | 擦除一个 64KB 大小的块 | 8+24 | 3 | 0 | 0 |
整片擦除 | 0xC7 / 0x60 | 擦除整个芯片的所有数据 | 8 | 0 | 0 | 0 |
读取数据 | 0x03 | 从指定地址开始读取数据 | 8+24 | 3 | 0 | 1+ (可连续读) |
快速读取 | 0x0B | 比标准读取更快的读取方式 | 8+24 | 3 | 1 | 1+ (可连续读) |
双线快速读取 | 0x3B | 使用两条数据线进行快速读取(提高速度) | 8+24 | 3 | 1 | 1+ (可连续读) |
四线快速读取 | 0x6B | 使用四条数据线进行快速读取(速度最快) | 8+24 | 3 | 1 | 1+ (可连续读) |
功率下降 | 0xB9 | 使芯片进入低功耗睡眠模式 | 8 | 0 | 0 | 0 |
释放功率下降 | 0xAB | 将芯片从低功耗睡眠模式中唤醒 | 8+24 | 3 (虚设地址) | 0 | 0 |
读 JEDEC ID | 0x9F | 读取制造商标识、内存类型和设备容量 ID | 8+24 | 0 |

4. 标准库 SPI Flash
4.1 标准库SPI源码移植到项目中

4.2 代码
main.c:
#include "stm32f10x.h"#include "led.h"
#include "key.h"
#include "delay.h"
#include "beep.h"
#include "usart1.h"
#include "adc.h"
#include "systick.h"
#include "tim6.h"
#include "tim3.h"
#include "sg90.h"
#include "myiic.h"
#include "spi.h"
#include "spi_flash.h"#include "stdio.h"
#include "stdlib.h"
#include "string.h"/*
根据 W25Q128 地址规则,提供对应读写数据地址对应地址 0x012300 ==> 块编号为 1 扇区编号为 2 页编号位 3,页内字节对应 00 PageSize == 0x100 ==> 256 字节
*/
#define WRITE_ADDR (0x012300)
#define READ_ADDR WRITE_ADDR/*
发送数据和接收数据的对应空间都是按照一页数据空间考虑。
*/
#define W25Q128_PAGE_SZIE (256)
u8 spi_send_buffer[W25Q128_PAGE_SZIE] = "ABCDEFG12345677654321GFEDCBA";
u8 spi_read_buffer[W25Q128_PAGE_SZIE] = {0};int main(void)
{Led_Init();USART1_Init(115200);/*利用 SPI 标准库提供的函数进行 SPI 初始化操作*/sFLASH_Init();/*优先将对应的 W25Q128 指定扇区空间进行擦除操作 删除 4KB*/sFLASH_EraseSector(WRITE_ADDR);/*利用 SPI 标准库提供的 writeBuffer 函数写入数据到 W25Q128 中*/sFLASH_WriteBuffer(spi_send_buffer, WRITE_ADDR, strlen((const char *)spi_send_buffer));/*用 SPI 标准库提供的 ReadBuffer 函数读取 W25Q128 中数据*/sFLASH_ReadBuffer(spi_read_buffer, READ_ADDR, strlen((const char *)spi_send_buffer));printf("spi_read_buffer : %s\n", spi_read_buffer);while (1){Led1_Ctrl(1);}
}
spi.c:
void SPI2_Init(void)
{/*PB12 --> CS/SS- GPIO 模式选择 推挽输出模式PB13 --> SCK- GPIO 模式选择 复用推挽输出模式PB14 --> MISO - Master Input, Slave Output- GPIO 模式选择 浮空输入模式PB15 --> MOSI- Master Output, Slave Input- GPIO 模式选择 复用推挽输出模式时钟使能需要SPI2 ==> RCC_APB1ENR_SPI2ENGPIOB ==> RCC_APB2ENR_IOPBEN*/// 1. 时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);// 2. 准备配置 GPIOB 相关的内容GPIO_InitTypeDef GPIO_InitStructure = {0};// PB12 GPIO 配置GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);// PB13 , PB15 GPIO 配置 都是复用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_Init(GPIOB, &GPIO_InitStructure);// PB14 GPIO 配置GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOB, &GPIO_InitStructure);/*3. GPIO 当前默认电平设置所有引脚全部拉高,作为准备阶段内容。尤其是 CS/SS ==> PB12 一旦处于低电平状态,相当于选择对应芯片。*/GPIO_SetBits(GPIOB, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);// 4. SPI 配置SPI_InitTypeDef SPI_InitStructure = {0};/*SPI 工作模式配置SPI0(00) SPI_CPOL_Low 空闲时钟为低电平 SPI_CPHA_1Edge 数据采样为第一个跳变沿SPI1(01) SPI_CPOL_Low 空闲时钟为低电平 SPI_CPHA_2Edge 数据采样为第二个跳变沿SPI2(10) SPI_CPOL_High 空闲时钟为高电平 SPI_CPHA_1Edge 数据采样为第一个跳变沿SPI3(11) SPI_CPOL_High 空闲时钟为高电平 SPI_CPHA_2Edge 数据采样为第二个跳变沿*/SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;// 设置 SPI 当前模式为主机模式 MasterSPI_InitStructure.SPI_Mode = SPI_Mode_Master;// 设置 SPI 数据发送高位先出SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;// 设置 SPI 工作数据方向,对应全双工 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;// 设置 SPI 字长为 8 bit ==> 1 byte SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;// 设置 SPI 当前波特率分频倍数,基于当前 APB1 时钟 36MHz// SPI_BaudRatePrescaler 选择 2 ,对应 SPI 工作波特率为 18MHzSPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;// 设置 SPI 触发方式为软件控制触发SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;// 对 SPI 进行初始化操作SPI_Init(SPI2, &SPI_InitStructure);// 开启 SPI2 SPI_Cmd(SPI2, ENABLE);
}
0voice · GitHub