16. 深入浅出 SPI 通信协议:从移位寄存器到 W25Q256 Flash 读写实战

SPI学习(二):SPI工作机制与协议解析 写在前面,SPI工作机制无非就是想介绍SPI模块内置的移位寄存器,这一部分可以看下面博客,里面有时序图与动图,可以很方便理解移位 https://blog.csdn.net/qq_25814297/article/details/103934794 SPI协议部分重点向介绍CPOL和CPHA,这一部分可以看下面博客 https://www.cnblogs.com/shangdawei/p/4752476.html 一.SPI工作机制 1.SPI内部原理图 SPI内部原理图如下图所示,这里参考《S 阅读详情

摘要: 本文从 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 四根信号线

信号线全称方向作用
MOSIMaster Output Slave Input主 → 从主设备数据输出,从设备数据输入
MISOMaster Input Slave Output从 → 主主设备数据输入,从设备数据输出
SCLKSerial Clock主 → 从时钟信号,由主设备产生
CS/SSChip Select / Slave Select主 → 从片选,低电平有效,决定此刻和哪个从机通信

1.2 主从架构

  • SPI 采用主从式通信,且只支持单主机(不支持多主机仲裁,这是它和 I2C 的一大区别);
  • 提供时钟的是主设备(Master),接收时钟的是从设备(Slave),所有的读写操作都由主设备发起;
  • 一主多从时,每个从机有自己独立的片选线(CS/SS),主设备通过拉低对应片选来选中目标从机。

接线方式: ① 一主一从;② 一主多从(SCLK/MOSI/MISO 并联到总线,每个从机单独一根片选线)。

② 一主多从


二、通信原理:移位寄存器「交换」模型

SPI 的本质,可以理解成主设备和从设备之间首尾相连的两个串行移位寄存器,拼成一个环形移位寄存器。

数据通信流程:

  1. 主设备拉低 CS/SS,启动一次通信,选中从机;
  2. 主设备产生时钟脉冲,告诉从机在哪个边沿采样/输出(对应后面要讲的四种模式);
  3. 主机把待发送数据写入发送缓冲区 → 经移位寄存器,通过 MOSI 一位一位移出送给从机;同时 MISO 上收到的数据经移位寄存器一位一位移入接收缓冲区;
  4. 从机同时把自己的数据通过 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 组合)

模式CPOLCPHA空闲电平采样边沿(第1个/第2个)
Mode 000低上升沿采样,下降沿发送
Mode 101低下降沿采样,上升沿发送
Mode 210高下降沿采样,上升沿发送
Mode 311高上升沿采样,下降沿发送

务必让主机与从机工作在同一模式。 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):
参数取值说明
DirectionFull-Duplex全双工
Data Size8 Bits8 位数据帧
TypeMaster主模式
Frame FormatMotorola标准 SPI 帧
First BitMSB First高位先发
CPOLlow空闲时 SCK 为低电平
CPHA1 Edge第一个跳变沿采样(配合 CPOL=High 即 Mode0)
NSSDisable软件片选(GPIO 手动控制 CS)
Prescaler4SPI 速率 = 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 levelHigh上电默认拉高 = 未选中 Flash(CS 低有效)
Output pull-up/Pull-downPull-up(可选)更稳
Maximum output speedHigh跟随 SPI 速率
User LabelCS生成 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 0x19
    • 0xEF = 制造商华邦(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)的写操作都遵循同一套铁律:

  1. 写使能先行——擦除/编程前必须先发 0x06(Write Enable),否则指令无效;
  2. 写之前必须擦除——Flash 位只能 1→0,要恢复 1 只能整扇区擦除为 0xFF;
  3. 操作后忙等待——用 0x05 轮询状态寄存器 BUSY 位,等到为 0 才算完成,期间不能发起下一次操作。

读操作相对简单:片选拉低 → 发读命令(+地址)→ 接收数据 → 片选拉高,无需擦除与写使能。


九、SPI vs I2C 对比(呼应上一篇 I2C 笔记)

维度SPII2C
信号线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 未使能

SPI通信协议实战指南:从原理到STM32/ESP32驱动W25Q64 Flash SPI(串行外设接口)是一种高速、全双工的同步串行通信协议,广泛应用于嵌入式系统中微控制器与外围设备的短距离通信。其工作原理基于主从架构,通过时钟信号同步数据交换,核心在于理解时钟极性(CPOL)和时钟相位(CPHA)定义的四种工作模式,主从设备必须严格匹配。该协议以其高速度和简单性,在需要快速数据交换的场景中具有重要技术价值,例如驱动SPI Flash、TFT屏幕或读取传感器数据。在工程实践中,开发者常面临时序配置和通信故障排查等挑战。本文以驱动W25Q64 SPI Flash存储器为例,详细解析了硬件连 阅读详情

相关推荐

SPI通信协议深度解析:从模式时序到STM32与W25Q64 Flash实战应用

串行外设接口(SPI)是一种高速、全双工、同步的串行通信总线协议,广泛应用于微控制器与传感器、存储器、显示屏等外设的连接。其核心原理基于主从架构,通过时钟信号同步数据交换,并定义了四种工作模式以适应不同外设的时序要求。在嵌入式开发中,SPI因其协议简单、速率高的技术价值,成为替代I2C和UART的重要板级通信方案,尤其适用于需要高速数据传输的场景,如Flash存储读写和TFT屏幕驱动。本文以STM32微控制器和W25Q64 Flash芯片为例,详细解析了SPI的物理接口、时序配置及驱动开发流程,并针对常见的

chulilang8944的博客 389

SPI原理详解

从图中可以看出, 主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线(发送,接收,时钟,片选),节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。)对传输协议没有重大的影响, 控制的是没有数据传输时时钟的空闲状态电平,对主模式和从模式下的设备都有效。上有其他主设备在通信,

w02091231的博客 2547

江科大STM32学习:15 SPI通信协议

双重SPI和四重SPI:一个时钟发送2位/4位(相当于4位并行)8位一个字节。

weixin_45337658的博客 1108

STM32入门教程(SPI通信篇)

本文是STM32 SPI通信协议的详细教程,主要内容包括: SPI协议基础 4线制全双工通信(SCK、MOSI、MISO、SS) 4种工作模式(CPOL/CPHA组合) 数据交换机制(移位寄存器同步传输) W25Q64 Flash存储器 8MB容量NorFlash特性 擦除/编程注意事项(需先擦除、按页写入) 典型指令时序(读ID、页编程、扇区擦除等) STM32硬件SPI实现 外设框图与寄存器配置 主从模式工作流程 软件模拟与硬件SPI对比实现 教程提供完整代码示例,涵盖SPI初始化、W25Q64驱动开发

Zevalin的博客 2603

STM32F10x硬件SPI接口驱动W25Q16 Flash存储器实战项目

STM32F10x系列集成多个SPI(Serial Peripheral Interface)模块,支持全双工同步通信,工作频率最高可达18MHz(基于72MHz APB2时钟分频),兼容Motorola SPI协议。其硬件支持四种工作模式(由CPOL和CPHA组合决定),内置可编程时钟分频器、数据帧格式(8/16位)、DMA接口以提升传输效率。

weixin_30598047的博客 682

Fast DDS源代码走读

DDS, PDP, EPD, DataWriter, DataReader

weixin_43082062的博客 715

spi的学习

初学spi,做一个小总结

shdksla的博客 1736

车载与安防项目实战:如何用RK3588搭配NVP6158C实现4路AHD摄像头BT1120接入与同步预览

本文详细解析了基于RK3588处理器与NVP6158C解码芯片实现4路AHD摄像头BT1120接入与同步预览的技术方案。针对车载环视和安防监控场景,重点探讨了硬件架构设计、BT1120接口配置、多路同步机制及Android HAL适配等核心问题,提供了从底层驱动到上层应用的完整实现路径,为相关领域的工程实践提供可靠参考。

weixin_30571465的博客 486

STM32-SPI

STM32-SPI

qq_45170479的博客 993

2022(一等奖)B360基于Arcpy与SVM的滑坡易发性评价

此外,在ArcGIS平台下,“易发性图预测(多进程)”工具的运行时间为2h 48m 3s,“易发性图预测(单进程)”工具的运行时间为5h 19m 27s,运行时间缩短了近2h 31m 24s,运行效率提高了47.39%。在ArcGIS Pro平台下,“易发性图预测(多进程)”工具的运行时间为20m 12s,“易发性图预测(单进程)”工具的运行时间为1h 26m 47s,将运行时间缩短了近1h 6m 35s,运行效率提高了76.72%。滑坡的发生受各种影响因素的影响,目前,影响因素的选择没有一个统一的标准。

2301_77310370的博客 1468

MOLDFLOW 2012模流分析基础到实战视频教程

├ 1第一讲MF软件安装与界面定制及模流前期准备工作及分析流程介绍 ├ 2第二讲MF建模工具条命令讲解与图层的运用 ├ 3第三讲MF网格类型与适应范围及网格划分、诊断、修复与统计 ├ 4第四讲MF中CAD doctor 模块对导入模型的修复方法与实例 ├ 5第五讲MF中常见各种流道浇口及水路的建模方法与外部导入方法上 ├ 6第六讲MF产品模型导入CAD DOCTOR与修复 ├ 7第七讲MF分析序列设定与网格的划分与诊断及修复 ├ 8第八讲MF材料选择与成型工艺参数设定 ├ 9第九讲MF创建浇注系统:主流道、

QQ_1987083845的博客 2546

W25Q128 spi-flash HAL库驱动详解

W25Q128是华邦公司推出的一款容量为 128M-bit(1byte == 8bit,相当于 16M-byte)的 SPI 接口的 NOR Flash 芯片(NOR Flash:一种非易失性存储器,它可以在断电或掉电后仍然保持存储的数据,被广泛应用于长期数据存储。有容量大,可重复擦写、按“扇区/块”擦除的特性。Flash的物理特性:只能写0,不能写 1,写1靠擦除。这一特性对后续编程,实现控制非常重要相应的还有其他型号。

weixin_56228133的博客 2786

VC Spyglass:Lint实战指南之TCL脚本自动化解析

本文详细介绍了如何利用TCL脚本实现VC Spyglass Lint检查的自动化流程。通过实战指南,从基础脚本编写到高级参数化技巧,帮助数字IC设计工程师摆脱手动操作,提升验证效率,并实现与CI/CD流程的集成。

h3i4j的博客 799

SPI通信协议(软件SPI读取W25Q64)

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景存储介质:Nor Flash(闪存)时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)存储容量(24位地址):字库存储:如果是少量的可以使用库函数导入使用,如果需要大量的可以添加点阵进行显示。固件程序:使用外挂设备进行时钟频率:160/320MHz,本质上都是80MHz,只是一次性发送/接收一个字节和。

又熟了的博客 1368

YOLO11改进|注意力机制篇|引入ELA注意力机制

YOLO11添加ELA注意力机制

A1983Z的博客 3648

如何使用Python爬取并保存网页上的所有附件

爬虫(Crawler)是一种自动化程序,它通过模拟人工访问网页并下载网页上的数据或文件,常用于数据采集和分析。爬虫通常以“爬取”的形式自动浏览和提取网页中的各种信息。本文详细介绍了如何使用Python爬取并保存网页中的附件文件,涵盖了网页解析、文件下载、多线程与异步优化等内容。希望通过本文的讲解,能够帮助你理解并掌握附件爬取的技巧。在实际应用中,爬虫开发不仅需要解决技术问题,还要遵守网络道德和法律法规,避免不当使用爬虫造成负面影响。

2201_76125261的博客 968

【STM32开发之寄存器版】(十四)-SPI读写外部FLASH

SPI 是一种高速同步串行输入/输出端口,可以将2-16Bit的数据以指定速率移入或移出设备。SPI 通常用于MCU与外设之间的通信。典型应用包括移位寄存器、显示驱动器、SPI EPROMS 和模数转换器等设备。

Daemon.Chen的博客 4092

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

STM32--SPI通信与W25Q64(1)

USART串口链接入口I2C通信链接入口。

m0_74068921的博客 3284

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

【STM32】HAL库 STM32CubeMX教程十四---SPI

前言: 本系列教程将HAL库与STM32CubeMX结合在一起讲解,使您可以更快速的学会各个模块的使用 在我们的HAL库中,对硬件SPI函数做了很好的集成,使得之前SPI几百行代码,在HAL库中,只需要寥寥几行就可以完成 那么这篇文章将带你去感受下它的优异之处,这些优异的函数,也正是HAL库的优点所在 所用工具: 1、芯片: STM32F103ZET6 2、STM32CubeMx软件 3、IDE:...

Z小旋 12万+

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

STM32学习——SPI通信协议(1)协议简介与W25Q64芯片简介

SPI是摩托罗拉公司开发的一种通用数据总线四根通信线:SCL(Serial Clock)(SCLK\CK\CLK)、MOSI(Master Output Slave Input)(DO)(主机输出从机输入)、MISO(Master Input Slave Output)(主机输入从机输出)(DI)、SS(Slave Select)(从机选择)(NSS\CS)同步、全双工同步时序必须有时钟线,数据位的输出和输入,都是在SCK的上升沿或者下降沿来进行的全双工:数据发送和数据接收各占一条线。

qq_46403759的博客 1406

STM32 CubeMx(七)SPI串行同步通信与外部FLASH(W25Q128)的读写和TFT液晶屏

本篇博客简单介绍了SPI通信并对外部FLASH(W25Q128)进行了读写操作并且使用了基于SPI通信的TFT液晶屏。

三木今天学习了嘛の博客 3643

STM32——SPI通信协议以及软件读写

STM32的SPI协议、W25Q64芯片以及软件读写功能实现

hssfscv的博客 2034

学习笔记——SPI通信和W25Q64存储器

闪存Nor Flash(另一种为Nand Flash),非易失性存储器,掉电不丢失,64表示64Mbits,即8M字节(需要24位地址线),可用于数据存储、字库存储(OLED、LCD)、固件程序存储,时钟频率80MHz,双重SPI(Dual SPI)时160M,四重SPI(Quad SPI)时320M双重SPI、四重SPI是什么?上面提到交换时序的时候,MOSI用于发送,MISO用于接收,这是全双工通信。对于双重SPI,那如果我们只发或只收时造成资源浪费怎么办,这时厂商做出了改进,

2301_79532069的博客 1452

W25Q128 SPI闪存驱动实战:从零构建嵌入式存储系统

本文详细介绍了W25Q128 SPI闪存芯片在嵌入式系统中的应用,包括其16MB存储容量、SPI通信协议原理、硬件连接设计与STM32驱动实现。通过实战代码解析擦除、写入和读取操作,并探讨了在固件存储、数据日志等场景的性能优化与常见问题调试技巧,帮助开发者高效构建可靠的嵌入式存储解决方案。

dell8的博客 724

SPI通信+W25Q64笔记

SPI+W25Q64 (软件读写)笔记

as_dfghjklsudk的博客 351
上一篇: 3、Linux 系统管理与 Shell 编程实战指南:从软件安装、进程管理到变量作用域与 Bash 脚本自动化
下一篇: 17.STM32 ADC:从模数转换原理到光敏电阻电压采集实战
硅基造物主
博客等级 码龄4年 17粉丝 42原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值