摘要: 本文从 SPI 的物理连线与移位寄存器交换模型讲起,系统梳理 SPI 通信协议的核心知识点:主从架构、四根信号线、全双工同步收发、CPOL/CPHA 时钟极性与相位及由此衍生的四种工作模式。随后以 STM32F411RET6 为例,对照 HAL 库讲清 SPI 外设句柄、初始化结构体与收发函数,并走通 CubeMX 工程配置,把理论落地到一个真实项目——通过 SPI2 总线驱动华邦 W25Q256(32MB NOR Flash),从零封装写使能、忙等待、JEDEC ID 读取、扇区擦除、页写入、缓冲区读取等驱动函数,最终通过串口回读验证读写正确性。文末附完整可编译源码与常见问题排查,帮助读者打通 SPI「从原理到代码」的完整开发链路。
一、SPI 是什么
SPI(Serial Peripheral Interface,串行外设接口)最早由摩托罗拉(Motorola)提出,是一种高速、全双工、同步的串行通信总线,没有固定的速度上限,实际中通常能达到甚至超过 10 Mbps。
和 I2C 相比,SPI 最鲜明的标签是全双工:发送和接收各走一条独立的线,可以在同一时刻既发又收。代价是引脚更多——至少 4 根线。
1.1 四根信号线
| 信号线 | 全称 | 方向 | 作用 |
|---|---|---|---|
| MOSI | Master Output Slave Input | 主 → 从 | 主设备数据输出,从设备数据输入 |
| MISO | Master Input Slave Output | 从 → 主 | 主设备数据输入,从设备数据输出 |
| SCLK | Serial Clock | 主 → 从 | 时钟信号,由主设备产生 |
| CS/SS | Chip Select / Slave Select | 主 → 从 | 片选,低电平有效,决定此刻和哪个从机通信 |
1.2 主从架构
- SPI 采用主从式通信,且只支持单主机(不支持多主机仲裁,这是它和 I2C 的一大区别);
- 提供时钟的是主设备(Master),接收时钟的是从设备(Slave),所有的读写操作都由主设备发起;
- 一主多从时,每个从机有自己独立的片选线(CS/SS),主设备通过拉低对应片选来选中目标从机。
接线方式: ① 一主一从;② 一主多从(SCLK/MOSI/MISO 并联到总线,每个从机单独一根片选线)。

② 一主多从

二、通信原理:移位寄存器「交换」模型
SPI 的本质,可以理解成主设备和从设备之间首尾相连的两个串行移位寄存器,拼成一个环形移位寄存器。
数据通信流程:
- 主设备拉低 CS/SS,启动一次通信,选中从机;
- 主设备产生时钟脉冲,告诉从机在哪个边沿采样/输出(对应后面要讲的四种模式);
- 主机把待发送数据写入发送缓冲区 → 经移位寄存器,通过 MOSI 一位一位移出送给从机;同时 MISO 上收到的数据经移位寄存器一位一位移入接收缓冲区;
- 从机同时把自己的数据通过 MISO 移回主机。一个时钟周期交换 1 bit,8 个时钟周期就交换 1 个字节。
示例: 主机拉低 NSS,产生时钟(上升沿触发),在 MOSI 上逐位发出 0x53,同时在 MISO 上逐位收到 0x46。一次移位寄存器交换完成,两个字节就被「对调」了。
2.1 一个关键认知:SPI 没有「读/写」,只有「交换」
SPI 只有主模式和从模式,严格意义上没有「读」和「写」之分——发送与接收是同步完成的。
- 若只想「写」一个字节给从机:主机正常发数据,忽略 MISO 同时收回来的那个字节(虚拟数据)即可;
- 若只想「读」从机一个字节:主机必须先发一个空字节(通常是
0xFF)来制造时钟,引发从机把它的数据移出来。
一句话:发一个数据必然会收到一个数据;要收一个数据,也必须先发一个数据。 理解了这一点,后面看 W25Q256 的驱动代码就不会困惑「为什么读操作也要 Transmit」。
三、通信特性:时钟速率 / 极性 / 相位与四种模式
3.1 设备选择
SPI 是单主设备协议,只有主机能发起通信。当主机想读/写某从机时,先把该从机对应的 SS 线拉低(SS 低电平有效),然后在 SCLK 上发送工作脉冲,在相应边沿上通过 MOSI「写」、通过 MISO「读」。
3.2 时钟三要素
- 时钟速率: 由主机在通信开始时配置并产生,理论上任意速率,实际受系统时钟频率和 SPI 最大传输速率限制;
- 时钟极性 CPOL(CKP): 决定时钟线空闲时的电平。CPOL=0 空闲为低电平;CPOL=1 空闲为高电平;
- 时钟相位 CPHA(CKE): 决定在时钟的第几个跳变沿采样数据。CPHA=0 在第一个边沿采样;CPHA=1 在第二个边沿采样。
3.3 四种工作模式(CPOL + CPHA 组合)
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿(第1个/第2个) |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿采样,下降沿发送 |
| Mode 1 | 0 | 1 | 低 | 下降沿采样,上升沿发送 |
| Mode 2 | 1 | 0 | 高 | 下降沿采样,上升沿发送 |
| Mode 3 | 1 | 1 | 高 | 上升沿采样,下降沿发送 |
务必让主机与从机工作在同一模式。 W25Q256 支持 Mode 0 和 Mode 3,本项目的 STM32 SPI2 就配置为与之匹配的模式,否则采样边沿对不上,读出来全是乱码。
四、STM32 的 SPI 编程接口(HAL 库)
参照:DS_stm32f411re.pdf 第 108–110 页;HAL 库函数说明(SPI 章节)。
4.1 三个核心数据结构
① SPI_HandleTypeDef —— 外设句柄(总入口) 封装寄存器基地址、初始化参数、收发缓冲区与状态:Instance(指向 SPI1/SPI2 等寄存器)、Init(指向下面的配置结构体)、pTxBuffPtr/pRxBuffPtr(收发缓冲指针)、TxXferSize/Count、RxXferSize/Count、hdmatx/hdmarx(DMA 句柄)、State(复位/就绪/忙/错误)、ErrorCode,以及 TxCpltCallback、RxCpltCallback 等回调。
② SPI_InitTypeDef —— 配置参数结构体 决定「怎么通信」,关键字段:
spi.Init.Mode // 主/从模式:SPI_MODE_MASTER / SPI_MODE_SLAVE
spi.Init.Direction // 传输方向:全双工 / 只收 / 单线
spi.Init.DataSize // 数据帧大小:8 位 / 16 位
spi.Init.CLKPolarity // CPOL:时钟极性(空闲高/低)
spi.Init.CLKPhase // CPHA:采样边沿
spi.Init.NSS // 片选管理:硬件 NSS / 软件 NSS
spi.Init.BaudRatePrescaler // 波特率预分频系数
spi.Init.FirstBit // MSB 优先 / LSB 优先
③ SPI_TypeDef —— 底层寄存器映射 CR1/CR2(控制)、SR(状态:忙/发送空/接收满)、DR(数据寄存器,读写它完成收发)、CRC 相关寄存器、I2S 复用寄存器。
4.2 常用 HAL 函数
| 函数 | 作用 |
|---|---|
HAL_SPI_Init(hspi) | 按 hspi->Init 配置初始化 SPI,保证正常工作的关键函数 |
HAL_SPI_MspInit(hspi) | 在 HAL_SPI_Init 内被回调,通常在此配置 SPI 关联的 GPIO |
HAL_SPI_DeInit / MspDeInit | 复位 SPI,撤销初始化 |
HAL_SPI_Transmit(hspi, pData, Size, Timeout) | 阻塞式发送 Size 个字节 |
HAL_SPI_Receive(hspi, pData, Size, Timeout) | 阻塞式接收 Size 个字节 |
五、CubeMX 工程配置
步骤 0:新建工程并选择芯片
- 打开 STM32CubeMX →
FILE → New Project,在芯片选型框搜索并选中 STM32F411RET6。 - 芯片选型搜索框 + 选中 STM32F411RETx 的界面。
步骤 1:系统设置(调试 / 时钟源)
System Core → SYS:Debug选 Serial Wire(否则烧录一次后芯片"锁死"无法再下载)。System Core → RCC:High Speed Clock (HSE)选 Crystal/Ceramic Resonator(用板载无源晶振)。- SYS 页 Debug=Serial Wire;

- RCC 页 HSE=Crystal/Ceramic Resonator。

步骤 2:使能 SPI2 并配置
- 在
Connectivity列表点开 SPI2(如果与硬件配置引脚不一,可以先行左键单击对应引脚选择SPI,最后再点开SPI2进行配置),Mode选 Full-Duplex Master(全双工主模式)。(下方图片配置NSS有瑕疵,根据下方的文字说明配置即可)
- 展开
SPI2 → Parameter Settings,按下表配置(CPOL/CPHA 必须与 W25Q256 匹配,W25Q256 支持 Mode0 与 Mode3,本项目用 Mode0):
| 参数 | 取值 | 说明 |
|---|---|---|
| Direction | Full-Duplex | 全双工 |
| Data Size | 8 Bits | 8 位数据帧 |
| Type | Master | 主模式 |
| Frame Format | Motorola | 标准 SPI 帧 |
| First Bit | MSB First | 高位先发 |
| CPOL | low | 空闲时 SCK 为低电平 |
| CPHA | 1 Edge | 第一个跳变沿采样(配合 CPOL=High 即 Mode0) |
| NSS | Disable | 软件片选(GPIO 手动控制 CS) |
| Prescaler | 4 | SPI 速率 = APB1 时钟 / Prescaler;先低速跑通再提 |
本项目SPI2 引脚:SCK=PB13、MISO=PB14、MOSI=PB15。
- SPI2 Mode=Full-Duplex Master;
- SPI2 Parameter Settings

步骤 3:配置片选 GPIO(PB12)
- 在引脚图左键点击 PB12 → 选 GPIO_Output(软件片选 CS)。
System Core → GPIO → PB12:
| 参数 | 取值 | 说明 |
|---|---|---|
| GPIO output level | High | 上电默认拉高 = 未选中 Flash(CS 低有效) |
| Output pull-up/Pull-down | Pull-up(可选) | 更稳 |
| Maximum output speed | High | 跟随 SPI 速率 |
| User Label | CS | 生成 CS_PIN / CS_PORT 宏 |
- PB12 的 GPIO 参数面板。

步骤 4:使能 USART(回读打印验证)
Connectivity → USART1:Mode选 Asynchronous(115200、8-N-1,用于printf)。- USART1 Mode=Asynchronous 与 Parameter Settings。
步骤 5:时钟树(Clock Configuration)
- HSE=25MHz,PLL 倍频后
SYSCLK=HCLK=100MHz(F411 上限),APB1(SPI2 在此总线)配 50MHz。 - 点 Resolve Clock Issues 消除报错(常需把 APB1 Prescaler 设为 2)。
- Clock Configuration 全树,绿色无报错,标注 SYSCLK=100MHz、APB1=50MHz。

步骤 6:工程生成设置(Project Manager)
Project:选Toolchain/IDE= MDK-ARM (Keil) 或 STM32IDE。Advanced Settings:勾选 Generate peripheral initialization as a pair of '.c/.h' files per peripheral。- Project Manager 页。


步骤 7:生成代码
- 右上角 GENERATE CODE。在 Keil 工程新建分组
Peripherals,把w25q256.c加入编译,把w25q256.h所在目录加入 Include Path。



- Keil 工程目录树(含新建的
Peripherals/w25q256.c分组)。
六、项目实战:STM32F411 + W25Q256 读写 NOR Flash
6.1 功能需求与硬件
需求: 编写程序对板载 W25Q256FV(华邦 W25Q256) NOR Flash 进行读写,并通过串口打印回读内容验证。
- 主控: STM32F411RET6,使用 SPI2 接口;
- 从机: W25Q256,容量 256 Mbit = 32MB;
- 片选 CS: 由 GPIOB 的 GPIO_PIN_12 软件控制(用 GPIO 模拟片选,而非硬件 NSS);
- 注意: 需把开发板上 1–4 号拨码开关拨到 ON 一侧,接通 SPI Flash 电路。
6.2 W25Q256 的关键特性
- JEDEC ID:
0x9F命令读出 3 字节,值为0xEF 0x40 0x190xEF= 制造商华邦(Winbond);0x40= W25Q 系列;0x19= 容量 256 Mbit(32MB);
- 地址模式坑: W25Q256 默认是 3 字节地址模式,只能访问低 16MB。要操作整个 32MB,必须先发送
0xB7进入 4 字节地址模式,否则地址被截断、读写错位; - 容量/页/扇区: 页 256 字节,扇区 4KB;Flash 只能把位从 1 写成 0,要恢复 1 必须擦除(擦除后整扇区全
0xFF)。
6.3 命令清单(写进宏定义)
#define SPI_FLASH_PageSize 256 // 一页 256 字节
#define W25Q256_WriteEnable 0x06 // 写使能
#define W25Q256_WriteDisable 0x04 // 写屏蔽
#define W25Q256_ReadStatusReg1 0x05 // 读状态寄存器1
#define W25Q256_ReadData 0x13 // 4字节地址模式 读数据
#define W25Q256_PageProgram_4byte 0x12 // 4字节地址模式 页写入
#define W25Q256_SectorErase 0x20 // 扇区擦除(4KB)
#define W25Q256_ReadJEDECID 0x9F // 读 JEDEC ID
#define W25Q256_Enter4ByteAddrMode 0xB7 // 进入4字节地址模式
6.4 核心驱动函数(思路版)
① SPI 收发封装: 基于 HAL_SPI_Transmit/Receive,注意 SPI 全双工「收必发」,HAL 在 Receive 时内部自动发 dummy 制造时钟。
② 忙等待: 拉低片选 → 0x05 读状态寄存器1 → 循环读回直到 bit0(BUSY)=0 → 拉高片选。
③ 写使能 / 4字节模式: 各发一条单字节命令(0x06 / 0xB7),操作后忙等待。
④ 读 JEDEC ID: 0x9F 后接收 3 字节,验证 SPI 链路是否打通(期望 EF 40 19)。
⑤ 扇区擦除: 写使能 → 0x20 + 4 字节地址(高字节在前)→ 忙等待。地址必须是 4096 整数倍。
⑥ 页写入: 写使能 → 0x12 + 4 字节地址 + 数据 → 忙等待。一次最多 256 字节,不能跨页。
⑦ 缓冲区读取: 拉低片选 → 0x13 + 4 字节地址 → 接收数据 → 拉高片选(读无需擦除与写使能)。
6.5 验证主流程:写入 0–255,回读串口打印
void outputW25xData(void){
uint8_t w25qx_wr_data[256], w25qx_rd_data[256];
for(int i=0;i<256;i++) w25qx_wr_data[i] = i; // 写入数据按顺序 0-255
SPI_FLASH_FOUR_MODE(); // 进入4字节地址模式
SPI_FLASH_SectorErase(0x00000000); // 先擦除目标扇区
SPI_FLASH_PageWrite(w25qx_wr_data, 0x00000000, 256); // 写 256 字节到地址0
SPI_FLASH_BufferRead(w25qx_rd_data, 0x00000000, 256); // 回读
for(int i=0;i<256;i++) // 串口按16进制打印
printf("%02X ", w25qx_rd_data[i]);
}
预期现象: 串口打印 00 01 02 … FE FF,与写入数据一一对应,说明 SPI 链路、擦除、写入、回读全部正确。若开头就读出全 FF 或乱码,优先排查:① 片选 PB12 是否在通信期间正确拉低;② SPI 模式是否匹配(Mode0/Mode3);③ 是否忘记擦除或忘记写使能;④ 是否进入了 4 字节地址模式。
七、完整可编译源码清单
基于 STM32F4 HAL(CubeMX 生成工程)。
w25q256.c放自定义Peripherals分组;main.c改动放USER CODE区,避免重新生成时被覆盖。
文件一:w25q256.h
#ifndef __W25Q256_H
#define __W25Q256_H
#include "main.h" /* 含 stm32f4xx_hal.h 与 CS_PIN/CS_PORT 宏 */
#include "spi.h" /* 含 extern SPI_HandleTypeDef hspi2 */
/* ---------------- W25Q256 指令表 ---------------- */
#define W25Q256_WriteEnable 0x06 /* 写使能 */
#define W25Q256_WriteDisable 0x04 /* 写屏蔽 */
#define W25Q256_ReadStatusReg1 0x05 /* 读状态寄存器1 */
#define W25Q256_ReadStatusReg2 0x35 /* 读状态寄存器2 */
#define W25Q256_ReadData 0x13 /* 4字节地址模式 读数据 */
#define W25Q256_PageProgram_4byte 0x12 /* 4字节地址模式 页写入 */
#define W25Q256_SectorErase 0x20 /* 扇区擦除(4KB) */
#define W25Q256_BlockErase_32K 0x52 /* 32K 块擦除 */
#define W25Q256_BlockErase_64K 0xD8 /* 64K 块擦除 */
#define W25Q256_ChipErase 0xC7 /* 全片擦除 */
#define W25Q256_ReadJEDECID 0x9F /* 读 JEDEC ID */
#define W25Q256_Enter4ByteAddrMode 0xB7 /* 进入4字节地址模式 */
#define W25Q256_Exit4ByteAddrMode 0xE9 /* 退出4字节地址模式 */
#define W25Q256_PowerDown 0xB9 /* 掉电 */
#define W25Q256_ReleasePowerDown 0xAB /* 释放掉电 */
/* ---------------- 容量/页/扇区参数 ---------------- */
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_SectorSize 4096
#define SPI_FLASH_ChipSize (32 * 1024 * 1024) /* 32MB */
/* ---------------- JEDEC ID 期望值 ---------------- */
#define W25Q256_MANUFACTURER_ID 0xEF /* Winbond 华邦 */
#define W25Q256_MEMORY_TYPE 0x40 /* W25Q 系列 */
#define W25Q256_CAPACITY_256M 0x19 /* 256Mbit=32MB */
/* ---------------- 函数声明 ---------------- */
uint8_t SPI_FLASH_SendnByte(uint8_t *pData, uint32_t number);
uint8_t SPI_FLASH_ReadnByte(uint8_t *pData, uint32_t number);
void SPI_FLASH_busy_block(void);
uint8_t SPI_FLASH_WriteEnable(void);
uint8_t SPI_FLASH_WriteDisable(void);
uint8_t SPI_FLASH_FOUR_MODE(void);
uint8_t getW25Q_ID(uint8_t *pID);
uint8_t SPI_FLASH_SectorErase(uint32_t SectorAddr);
uint8_t SPI_FLASH_ChipErase(void);
uint8_t SPI_FLASH_PageWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
uint8_t SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
uint8_t SPI_FLASH_BufferRead(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead);
#endif /* __W25Q256_H */
文件二:w25q256.c
#include "w25q256.h"
/* 片选宏:CS 低有效。若无 User Label,可换成
* HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET/SET) */
#define FLASH_CS_LOW() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET)
#define FLASH_CS_HIGH() HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET)
/* ① SPI 收发封装(HAL) */
uint8_t SPI_FLASH_SendnByte(uint8_t *pData, uint32_t number)
{
return (HAL_SPI_Transmit(&hspi2, pData, (uint16_t)number, 0xFFFF) == HAL_OK)
? HAL_OK : HAL_ERROR;
}
uint8_t SPI_FLASH_ReadnByte(uint8_t *pData, uint32_t number)
{
/* SPI 全双工:收的同时 HAL 内部自动发 dummy 字节制造时钟 */
return (HAL_SPI_Receive(&hspi2, pData, (uint16_t)number, 0xFFFF) == HAL_OK)
? HAL_OK : HAL_ERROR;
}
/* ② 忙等待:轮询状态寄存器1 BUSY(bit0) */
void SPI_FLASH_busy_block(void)
{
uint8_t status;
FLASH_CS_LOW();
status = W25Q256_ReadStatusReg1; /* 0x05 */
SPI_FLASH_SendnByte(&status, 1);
do {
SPI_FLASH_ReadnByte(&status, 1);
} while (status & 0x01);
FLASH_CS_HIGH();
}
/* ③ 写使能 / 写屏蔽 */
uint8_t SPI_FLASH_WriteEnable(void)
{
uint8_t cmd = W25Q256_WriteEnable; /* 0x06 */
FLASH_CS_LOW();
uint8_t r = SPI_FLASH_SendnByte(&cmd, 1);
FLASH_CS_HIGH();
return r;
}
uint8_t SPI_FLASH_WriteDisable(void)
{
uint8_t cmd = W25Q256_WriteDisable; /* 0x04 */
FLASH_CS_LOW();
uint8_t r = SPI_FLASH_SendnByte(&cmd, 1);
FLASH_CS_HIGH();
return r;
}
/* ④ 进入 4 字节地址模式(访问 >16MB 必须) */
uint8_t SPI_FLASH_FOUR_MODE(void)
{
uint8_t cmd = W25Q256_Enter4ByteAddrMode; /* 0xB7 */
FLASH_CS_LOW();
uint8_t r = SPI_FLASH_SendnByte(&cmd, 1);
FLASH_CS_HIGH();
SPI_FLASH_busy_block();
return r;
}
/* ⑤ 读 JEDEC ID(验证 SPI 链路是否打通) */
uint8_t getW25Q_ID(uint8_t *pID)
{
uint8_t cmd = W25Q256_ReadJEDECID; /* 0x9F */
FLASH_CS_LOW();
SPI_FLASH_SendnByte(&cmd, 1);
SPI_FLASH_ReadnByte(pID, 3); /* 期望 EF 40 19 */
FLASH_CS_HIGH();
return HAL_OK;
}
/* 32 位地址拆 4 字节(高字节在前,MSB First) */
static inline void split_addr_4byte(uint32_t addr, uint8_t out[4])
{
out[0] = (uint8_t)((addr >> 24) & 0xFF);
out[1] = (uint8_t)((addr >> 16) & 0xFF);
out[2] = (uint8_t)((addr >> 8) & 0xFF);
out[3] = (uint8_t)( addr & 0xFF);
}
/* ⑥ 扇区擦除(写之前必须先擦!) */
uint8_t SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
SPI_FLASH_WriteEnable(); /* 擦除前必须写使能 */
FLASH_CS_LOW();
uint8_t cmd = W25Q256_SectorErase; /* 0x20 */
SPI_FLASH_SendnByte(&cmd, 1);
uint8_t addr[4]; split_addr_4byte(SectorAddr, addr);
SPI_FLASH_SendnByte(addr, 4);
FLASH_CS_HIGH();
SPI_FLASH_busy_block();
return HAL_OK;
}
/* ⑦ 全片擦除(慎用,耗时) */
uint8_t SPI_FLASH_ChipErase(void)
{
SPI_FLASH_WriteEnable();
FLASH_CS_LOW();
uint8_t cmd = W25Q256_ChipErase; /* 0xC7 */
SPI_FLASH_SendnByte(&cmd, 1);
FLASH_CS_HIGH();
SPI_FLASH_busy_block();
return HAL_OK;
}
/* ⑧ 页写入(一次最多 256 字节,不能跨页) */
uint8_t SPI_FLASH_PageWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
if (NumByteToWrite == 0 || NumByteToWrite > SPI_FLASH_PageSize)
NumByteToWrite = SPI_FLASH_PageSize;
SPI_FLASH_WriteEnable(); /* 编程前必须写使能 */
FLASH_CS_LOW();
uint8_t cmd = W25Q256_PageProgram_4byte; /* 0x12 */
SPI_FLASH_SendnByte(&cmd, 1);
uint8_t addr[4]; split_addr_4byte(WriteAddr, addr);
SPI_FLASH_SendnByte(addr, 4);
SPI_FLASH_SendnByte(pBuffer, NumByteToWrite);
FLASH_CS_HIGH();
SPI_FLASH_busy_block();
return HAL_OK;
}
/* ⑨ 缓冲区写入(自动拆页,可跨页/跨扇区) */
uint8_t SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint32_t addr = WriteAddr;
uint16_t remain = NumByteToWrite;
uint16_t page_space, this_page;
while (remain > 0)
{
page_space = SPI_FLASH_PageSize - (addr % SPI_FLASH_PageSize);
this_page = (remain < page_space) ? remain : page_space;
SPI_FLASH_SectorErase(addr - (addr % SPI_FLASH_SectorSize)); /* 先擦目标扇区 */
SPI_FLASH_PageWrite(pBuffer, addr, this_page);
addr += this_page; pBuffer += this_page; remain -= this_page;
}
return HAL_OK;
}
/* ⑩ 缓冲区读取 */
uint8_t SPI_FLASH_BufferRead(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
FLASH_CS_LOW();
uint8_t cmd = W25Q256_ReadData; /* 0x13 */
SPI_FLASH_SendnByte(&cmd, 1);
uint8_t addr[4]; split_addr_4byte(ReadAddr, addr);
SPI_FLASH_SendnByte(addr, 4);
SPI_FLASH_ReadnByte(pBuffer, NumByteToRead);
FLASH_CS_HIGH();
return HAL_OK;
}
文件三:main.c 关键改动(放在 USER CODE 区)
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "w25q256.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 0 */
/* printf 重定向到 USART1 */
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 0xFFFF);
return len;
}
#else
struct __FILE { int handle; };
FILE __stdout;
void _sys_exit(int x) { (void)x; }
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
#endif
void outputW25xData(void)
{
uint8_t wr[256], rd[256];
for (int i = 0; i < 256; i++) wr[i] = (uint8_t)i; /* 写 0~255 */
SPI_FLASH_FOUR_MODE(); /* 4字节地址模式 */
SPI_FLASH_BufferWrite(wr, 0x00000000, 256);
SPI_FLASH_BufferRead(rd, 0x00000000, 256);
printf("\r\n--- W25Q256 read-back (addr 0x00) ---\r\n");
for (int i = 0; i < 256; i++) {
printf("%02X ", rd[i]);
if ((i + 1) % 16 == 0) printf("\r\n");
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 2 */(while 之前)
uint8_t id[3];
SPI_FLASH_FOUR_MODE();
getW25Q_ID(id);
printf("\r\nW25Q256 JEDEC ID: %02X %02X %02X\r\n", id[0], id[1], id[2]);
/* 期望: EF 40 19 */
outputW25xData();
/* USER CODE END 2 */
八、SPI 读写 Flash 的「三板斧」总结
W25Q256(乃至几乎所有 SPI NOR Flash)的写操作都遵循同一套铁律:
- 写使能先行——擦除/编程前必须先发
0x06(Write Enable),否则指令无效; - 写之前必须擦除——Flash 位只能 1→0,要恢复 1 只能整扇区擦除为
0xFF; - 操作后忙等待——用
0x05轮询状态寄存器 BUSY 位,等到为 0 才算完成,期间不能发起下一次操作。
读操作相对简单:片选拉低 → 发读命令(+地址)→ 接收数据 → 片选拉高,无需擦除与写使能。
九、SPI vs I2C 对比(呼应上一篇 I2C 笔记)
| 维度 | SPI | I2C |
|---|---|---|
| 信号线 | 4 根(MOSI/MISO/SCLK/CS) | 2 根(SDA/SCL) |
| 双工方式 | 全双工(收发独立线) | 半双工 |
| 主从 | 仅单主机 | 支持多主机(带仲裁) |
| 寻址 | 靠片选线选中从机 | 靠7 位地址寻址 |
| 速率 | 高(常 10Mbps+) | 较低(标准 100k / 快速 400k) |
| 应答机制 | 无 ACK | 每字节带 ACK/NACK |
| IO 占用 | 多(每个从机加一根片选) | 少(共用两根线) |
| 典型场景 | Flash、屏幕、高速 ADC | 低速传感器、EEPROM、RTC |
一句话选型: 要快、要全双工、引脚不紧张 → SPI;要省引脚、挂很多低速器件 → I2C。
十、总结特点
- 软件 / 硬件 SPI: 本项目的硬件 SPI(HAL)由外设自动生成时序,CPU 只管搬数据,速度快、稳定,代价是引脚固定;若用 GPIO 模拟(软件 SPI)则引脚随意,但要手写每个时钟沿,速度受限——这与 I2C 的软件/硬件之争同理。
- SPI 的灵魂是「交换」:没有读写的分别,发即收、收必发,理解这一点是读懂 Flash 驱动代码的钥匙。
- 模式必须匹配: CPOL/CPHA 四个组合,主从双方要约定一致,否则数据错位、读全乱码。
- Flash 写三板斧: 写使能 → 擦除 → 忙等待,缺一不可。
至此,我们从 SPI 的四根线、移位寄存器交换模型出发,弄懂了四种时钟模式,对照 HAL 库走通了 STM32 的 SPI 编程接口,再用 CubeMX 配好工程、亲手驱动 W25Q256 完成「擦除—页写—回读—串口验证」的完整实战,把 SPI 从原理真正落到了代码上。
附:常见问题排查(FAQ)
| 现象 | 优先排查点 |
|---|---|
JEDEC ID 读出来全是 00 | 片选 PB12 通信期间是否正确拉低;MISO(PB14) 接线;SPI 模式 Mode0/Mode3 是否匹配 |
| JEDEC ID 读出乱码 | CPOL/CPHA 与 W25Q256 不匹配;波特率过快(先加大 Prescaler 降速) |
回读全 FF | 忘记擦除(Flash 只能 1→0,未擦的新内容写不进);忘记 SPI_FLASH_WriteEnable() |
| 高地址读写错位 | 忘记 SPI_FLASH_FOUR_MODE()(默认 3 字节只访问低 16MB) |
| 写到第 257 字节数据丢失 | 用 SPI_FLASH_BufferWrite(自动拆页),别用 PageWrite 跨页 |
| printf 无输出 | 未勾选 Options→Target→Use MicroLIB,或fputc 重定向未写、USART1 未使能 |
389



被折叠的 条评论
为什么被折叠?



