SPI阻塞式通信与HAL源码  

1. 预备

1.1 SPI通信必须有主机和从机

要使用 SPI 进行通信,必须存在两类角色:

  • 主机(Master):在之前《SPI》章节中,主机由 MCU 内部的 SPI 片上外设承担,负责产生时钟信号 SCK、发起通信、控制片选。
  • 从机(Slave):从机通常由某些存储设备、显示设备或传感器设备承担,被动响应主机的时钟和指令。

关键原则:

        SPI 总线上 SCK 时钟只能由主机产生,从机无法产生时钟。

        这是后面理解"Dummy 字节为什么必须存在"的根本原因。


1.2 常见 SPI 应用场景(7 例)

编号设备说明
①3.5 寸 LCD 显示屏(IPS 全视角、电容触摸)分辨率 320×480,插接式标准 40PIN,驱动 ST7796/ILI9488,提供 SPI 串口(3线/4线)和 MCU 并口(8/16位),使没有 LCD 控制器的 MCU 也能实现显示
②SPI 接口 Flash(W25Q64 类)外置 Flash 方案,非常常用,引脚 VCC/CS/DO/GND/SLK/D1
③W5500 以太网模块用 SPI 驱动,使没有以太网控制器的 MCU 获得联网能力
④ESP-12F WIFI 模块用 SPI 驱动,使 MCU 实现 WIFI 连接
⑤OLED 显示模块这类模块基本都会提供 SPI 控制方式
⑥ADS1118 ADC 模块(SPI 接口)模数转换,引脚含 DRDY/MISO/MOSI/SCLK/CS
⑦SD 卡模块使用 SPI 模式通信,使没有 SDIO 接口的 MCU 也能操作 SD 存储卡

1.3 本次实验外设:为什么选 W25Q64

本次选择的从机外设是 Flash(型号 W25Q64),原因:

  1. 简单:能让我们学到 SPI 操作外设的基本思路,尽可能减少学习成本;
  2. 场景常见:STM32F103ZET6 内部虽然自带 Flash,但容量有限(512KB),且本身承担特定功能(存程序),对 MCU Flash 容量进行扩展是常见需求。

"主控"与"存储"分离的工程思想(扩展):

  • 在工业、消费电子领域,数据有时比 MCU 芯片本身更重要;
  • 若用 MCU 内部小容量 Flash 临时保存数据,一旦 MCU 损坏,数据无法读取;
  • 把数据保存在外部扩展 Flash 上,MCU 出问题只需更换 MCU,数据依然保留。

1.4 硬件连接关系

        外扩 Flash 不是把 MCU 拆开焊一个更大的 Flash(也做不到),而是通过 MCU + SPI + W25Q64 的方式扩展。芯片选型、硬件走线在公司做板子时已确定,灵犀板已外扩该 Flash。

四者关系:

        Cortex-M3 内核 → MCU(含片上 SPI)→ SPI 总线 → SPI Flash(W25Q64)。

  • Cortex-M3 通过总线配置 SPI 外设寄存器;
  • SPI 外设引出 NSS(片选)、SCK(时钟)、MOSI(主机发从机收)、MISO(主机收从机发)四根信号;
  • W25Q64 内部也有一套对应的 SPI 通信接口硬件电路。

知识迁移:

        SPI Flash 芯片内部的硬件电路虽然和 MCU 完全不同,但必然存在类似的 SPI 通信接口电路,要学会把 MCU 端 SPI 的理解迁移到从机端。


2. Flash 生态介绍

2.1 Flash 基础知识

        Flash 存储器是一种非易失性存储器(Non-Volatile Memory),即使断电也能保持数据。

广泛用于:

        USB 闪存盘(U盘)、固态硬盘(SSD)、SD 卡(Secure Digital Card,安全数字卡)、嵌入式系统的 Flash 存储芯片。

"技术 → 产品"的关系(常识,重点理解):

        能叫上名字的存储设备都是独立硬件模块,底层都用 Flash Memory 作为基本存储单元,但选择的通信协议和控制电路不同:

组合产品
USB + FlashU 盘
NVMe / SATA / SAS 协议 + FlashSSD 固态硬盘
SD 协议 + FlashSD 卡
SPI + FlashSPI Flash(如 W25Q64,属于 SPI NOR Flash)

类比:

        USB 闪存、SSD 是"Flash?",就像面包和面粉的关系——Flash 是面粉(原材料/存储单元),各种设备是面包(成品)。


2.2 Flash 两大常见类型:NOR 与 NAND

NOR Flash
  • 特点:有独立的数据线和地址线,支持随机访问(类似 RAM),读取速度快;                但写入和擦除速度慢,容量较小(通常几 MB 到几十 MB),单元成本较高。
  • 应用:常用于存储程序代码,如 BIOS、嵌入式系统片上 Flash、IoT 设备固件、工控系统核心代码、仪表盘、ADAS、信息娱乐、车身控制模块固件等。
  • 核心优势 XIP(eXecute In Place,片内执行/就地执行):CPU 可以直接从 NOR Flash 上取指令执行,无需先把代码拷贝到 RAM,这依赖于它独立地址线 + 随机访问的能力。
NAND Flash
  • 特点:以页为单位读写、块为单位擦除;没有独立地址线,通过复用的命令/地址/数据线串行访问;容量大(从几 MB 到几 TB)、成本低、写入擦除速度快;但读取速度相对较慢,且存在  位翻转(Bit Flip)问题,需要 ECC(错误校验和纠正)机制。
  • 应用:大容量存储设备,如 SSD、SD 卡、U 盘、eMMC 等。
对比维度NOR FlashNAND Flash
名称来源

存储单元采用"或非"(Not OR)

逻辑门结构

存储单元采用"与非"(Not AND)

逻辑门结构

地址线/数据线独立复用,串行传输
访问方式随机访问页读/写、块擦除
读取速度快较慢
写入/擦除速度慢快
容量小(MB~几十MB)大(MB~TB)
成本高低
可靠性高,几乎无需 ECC会位翻转,需 ECC
XIP 片内执行支持不支持
典型用途存代码/固件存大量数据
典型产品BIOS、SPI Flash(W25Q64)SSD、SD卡、U盘、eMMC

        W25Q64 属于 NOR Flash。


2.3 常见型号和生产厂商

全球 Flash 主要制造商
厂商国家/地区主要类型市场地位
三星 Samsung韩国NAND全球 NAND 市场龙头,市占率领先,基本退出通用 NOR 市场
SK 海力士 SK Hynix韩国NAND全球主要 NAND 供应商,已彻底退出 NOR 业务
美光 Micron美国NAND & NOR全球主要 NAND 供应商之一,在车规/工规等高端 NOR 市场仍具优势
铠侠 Kioxia / 西部数据 WD日本/美国NAND全球 NAND 市场重要参与者,技术和产能合作紧密
Solidigm(原英特尔 NAND 业务)美国NAND专注企业级领域
NOR Flash 领先者
厂商国家/地区市场地位
华邦电子 Winbond中国台湾全球 NOR Flash 市场龙头,市占率排名第一
旺宏电子 Macronix中国台湾全球 NOR Flash 领先厂商,产品用于 FPA 配置、航天等领域
兆易创新 GigaDevice中国大陆国内 NOR Flash 龙头,全球排名第二,产品应用广泛
中国大陆主要制造商
厂商主要类型说明
长江存储 YMTCNAND拥有自主 Xtacking 技术,突破 200 层以上 3D NAND,国产存储核心力量
长鑫存储 CXMTDRAM(注:非 Flash)国内 DRAM 龙头,实现国产替代的关键企业
东芯/普冉/恒烁股份NOR / SLC NAND专注中小容量、高附加值存储芯片设计
江波龙/佰维存储存储模组将 Flash 芯片封装成 SSD、内存卡等成品,拥有品牌和渠道能力
SPI Flash 主要厂商一览(与本实验最相关)
厂商国家/地区相关产品/系列
华邦电子 Winbond中国台湾W25Qxx 系列(如 W25Q64JV)
旺宏电子 Macronix中国台湾MX25L、MX66U 系列
兆易创新 GigaDevice中国大陆GD25Q 系列
普冉半导体 Puya中国大陆SPI NOR 主要厂商
美光 Micron美国工规/车规有优势
微芯 Microchip美国收购 Atmel、SST 等品牌
英飞凌 Infineon德国收购 Cypress(赛普拉斯)
瑞萨 Renesas日本AT45DB 系列
ISSI美国常作第二备选方案
芯讯通/东芯/复旦微/晶宏/武汉新芯(XMC)中国大陆/台湾SPI NAND/NOR 设计或代工

本实验采用的是中国台湾华邦电子 W25Qxx 系列 SPI Flash。


2.4 W25Q64 命名规则

命名字段示例含义
品牌/系列W代表 Winbond 华邦电子,SPI Flash 产品系列前缀
技术类型25代表 SPI 接口系列,华邦对 SPI Flash 的固定编号
容量标识Q64

Q = Quad SPI(支持四线模式);

64 = 容量 64 Mb(兆比特),换算成字节为 8 MB(64 Mb ÷ 8 = 8 MB)

子系列/代际J具体子系列或代际(如"J"代),定义基础架构和性能范围
封装类型V

芯片封装形式,V 通常指 USON(8×6mm)小尺寸封装;

其他如 I 指 SOIC

工作温度(常省略或后缀)

如 I = 工业级(−40℃~85℃),E = 扩展级(−40℃~105℃),

省略通常默认工业级

学习阶段只需关注到 W25Q64 即可,后缀主要与子型号、封装、温度有关。


3. W25Q64 Flash 芯片介绍

        W25Q64 是 Winbond 生产的 SPI NOR Flash 芯片,是嵌入式系统中应用最广泛的存储芯片之一,尤其适合存储启动代码、固件、配置参数等关键数据。

3.1 核心参数

  • 容量:64 Mbits(8 MBytes);
  • 接口:标准 SPI / Dual SPI / Quad SPI(QSPI);最高时钟频率 133 MHz(QSPI);
  • 擦写寿命:10 万次擦写周期(Program/Erase Cycles);
  • 工作电压:2.7V~3.6V(宽压兼容 3.3V 系统);
  • 数据保持:20 年(常温下断电存储);
  • 封装:SOIC-8(208mil)、WSON-8(6×5mm)、USON-8(2×3mm)。

3.2 内部结构

W25Q64 内部主要功能模块:

  • SPI Command & Control Logic(SPI 命令与控制逻辑):接收 CLK、/CS、DI(IO0)、DO(IO1)、/WP(IO2)、/HOLD(IO3) 信号,解析主机发来的指令;
  • Status Register(状态寄存器)+ Write Control Logic(写控制逻辑):管理写使能、忙/就绪状态、保护位;
  • High Voltage Generators(高压发生器):擦写操作需要高电压,由芯片内部升压产生;
  • Page Address Latch/Counter(页地址锁存/计数器)、Byte Address Latch/Counter(字节地址锁存/计数器):锁存地址并自动递增;
  • Column Decode And 256-Byte Page Buffer(列译码与 256 字节页缓存):写入数据先进页缓存(SRAM),再一次性编程进存储阵列;
  • Write Protect Logic and Row Decode(写保护与行译码):行地址译码、块/扇区保护;
  • 存储阵列(W25Q64BV):真正保存数据的 Flash 单元矩阵。

3.3 存储阵列:页 / 扇区 / 块 / 整片

这是 W25Q64 最核心的组织结构,四级层次:

层级大小包含关系
页 Page256 字节最小编程(写)单位
扇区 Sector4 KB每个扇区包含 16 个页(256×16 = 4096B)
块 Block64 KB每个块包含 16 个扇区(4KB×16 = 64KB)
整片 Chip8 MB由 128 个块构成(64KB×128 = 8MB)

层次关系示意:

整片 W25Q64 (8MB)
└── 块 Block ×128 (64KB)
    └── 扇区 Sector ×16 (4KB)
        └── 页 Page ×16 (256B)
            └── 字节 Byte ×256

3.4 容量计算

  • 扇区:256B(Page)  × 16    = 4096B = 4 KB(Sector)
  • 块:    4KB(Sector) × 16   = 64 KB(Block)
  • 整片:64KB(Block) × 128 = 8192 KB = 8 MB(Chip)

3.5 地址空间

  • 使用 24 位地址线(可寻址空间 16MB);
  • W25Q64 占用地址范围:0x000000 ~ 0x7FFFFF;
  • 每个地址对应 8 bits(1 byte),共计 0x800000 = 8388608 = 8×1024×1024 字节。

        W25Q64 给每一个字节都分配一个地址,通过该地址可读取 Flash 中任意一个字节——这正是 NOR Flash 随机访问特性的体现。


3.6 基本操作规则(重点)

3.6.1 读取(Read)
  • 读取基本单元:字节(Byte);
  • 特点:
    • 最灵活,可从任意地址开始,读取任意长度数据;
    • 没有页、扇区、块的限制,可连续读(地址自动递增,跨页/跨扇区也能连续读)。

3.6.2 写 / 编程操作(Page Program)
  • 写基本单元:页(Page),即 256 字节;
  • 注意:基本单元与 Flash 类型有关——NOR Flash 写入可按字节,但单次不能超过一个 Page;NAND Flash 必须整页写入。W25Q64 是 NOR Flash;
  • 特点:
    • 单位限制:每次编程指令最多写 256 字节,超过一页需多次触发编程指令;
    • 对齐要求(回卷/卷绕 Wrap):写入必须在页边界内进行。
    • 若起始地址 + 写入长度超过当前页边界,多出的数据会"回卷"到该页开头,覆盖旧数据,而不会自动写入下一页。

3.6.3 擦除操作(Erase)
  • 擦除基本单元:扇区(Sector);
  • 分级擦除单位:
    • 扇区擦除 Sector Erase:最小单位 4KB;
    • 块擦除 Block Erase:支持 32KB 或 64KB 的块擦除;
    • 整片擦除 Chip Erase:一次性擦除整个 8MB;
  • 特点:不可字节擦除,Flash 不支持像 EEPROM 那样的单字节擦除,必须至少以扇区为单位整体重置。

3.7 为什么写入之前必须先擦除?

NOR Flash 硬件设计限制:

  • 编程(写)只能把数据位从 1 变 0,不能把 0 变 1;
  • 只能通过擦除把扇区/块/整盘整体置为 1,不能按字节或页置 1;
  • 擦除的本质 = 把目标位置全部先设置为 1;
  • 之后编程写入时,只需把对应位由 1 → 0 即可完成写入。

术语:

        写在 Flash 里叫"编程(Program)";

        写之前必须先擦除;

        擦除本质是把目标单位空间全部置 1。


3.8 "写以页为单位、擦除以扇区为单位"的非对称问题与 R-M-W

问题:

        如果只想修改某一页(256B)中的几个字节,无法只擦这一页(擦除最小是 4KB 扇区)。

标准操作流程 —— 读-改-擦-写(R-M-W,Read-Modify-Write):

  1. Read(读):先将目标扇区(整个 4KB)数据全部读到 MCU 的 RAM 缓冲区;
  2. Modify(改):在 RAM 中修改那 256 字节(或几个字节);
  3. Erase(擦):发送擦除指令,清空 Flash 上整个 4KB 扇区;
  4. Write(写/编程):将 RAM 中修改后的 4KB 数据重新分页(16 次页编程)写回 Flash。

注意:

        W25Q64 其实可以写入 256 字节以内,但前提是这 256 字节已被擦除为干净(全 1)状态;否则采用"读-改-写"才是最佳实践。


4. W25Q64 Flash 访问原理

4.1 芯片内部三部分构成

Flash(如 W25Q64)是一个独立芯片,内部主要由三部分构成:

  1. 控制电路:写控制逻辑、状态寄存器、高压发生器、地址锁存/计数器、页缓存等;
  2. 通信模块(SPI):SPI Command & Control Logic,负责通过 SPI 接口收发数据;
  3. 存储单元(存储模块):存储阵列,真正保存数据。

4.2 指令集:厂商约定好的"口令"

        为方便开发者控制 SPI Flash,厂商提供指令集。

        指令集本质是厂商约定好的字节数据,不同数据代表不同含义或操作要求。

生动类比:

        W25Q64 像一个训练有素的士兵,记住大量口令;

        主机(教官)喊出对应口令(指令),士兵就做对应动作(卧倒、前进、集合等)。

        多条指令聚合在一起就是指令集。

W25Q64JV 常用标准 SPI 指令(Instruction Set Table 1,整理扩展):

指令名称操作码后续字节功能
Write Enable06h—写使能(写/擦除前必须先发)
Volatile SR Write Enable50h—易失性状态寄存器写使能
Write Disable04h—写禁止
Release Power-down / IDABh3 dummy + 1 ID释放掉电/读制造商/设备 ID
Manufacturer/Device ID90h2 dummy + 2 dummy读制造商/设备 ID
JEDEC ID9Fh读出 MF7-0、ID15-8、ID7-0读 JEDEC ID(本实验用)
Read Unique ID4Bh4 dummy + 8 字节读唯一 ID
Read Data03hA23-A0 + 数据读数据(标准)
Fast Read0BhA23-A0 + dummy + 数据快速读
Page Program02hA23-A0 + 数据页编程(写)
Sector Erase (4KB)20hA23-A0扇区擦除
Block Erase (32KB)52hA23-A032KB 块擦除
Block Erase (64KB)D8hA23-A064KB 块擦除
Chip EraseC7h/60h—整片擦除
Read Status Register-1/2/305h/35h/15h读出状态读状态寄存器(含 BUSY 位)
Write Status Register-1/2/301h/31h/11h写入状态写状态寄存器
Erase/Program Suspend / Resume75h / 7Ah—擦写挂起 / 恢复
Power-downB9h—进入掉电
Enable/Reset Device66h / 99h—使能/复位设备

此外还有 Instruction Set Table 2(Dual/Quad SPI 指令):

        Fast Read Dual Output(3Bh)、Fast Read Dual I/O(BBh)、Quad Input Page Program(32h)、Fast Read Quad Output(6Bh)、Fast Read Quad I/O(EBh) 等,用于双线/四线高速模式。


4.3 真实走线(灵犀板 W25Q64)

W25Q64 引脚连接信号含义
1 /CSFLASH_SS(PE6)片选,低电平有效
2 DO(IO1)SPI1 MISO(PA6)从机输出→主机输入
3 /WP(IO2)(上拉/接 VCC)写保护
4 GNDGND地
5 DI(IO0)SPI1 MOSI(PA7)主机输出→从机输入
6 CLKSPI1 SCK(PA5)时钟
7 /HOLD or /RESET(IO3)(接 VCC)保持/复位
8 VCCVDD_3.3V电源
  • R41(10kΩ):CS 上拉电阻,未选中时保证 CS 为高电平;
  • C48(100nF):VCC 去耦电容,滤除电源噪声。

        MCU 通过 SPI 接口 + GPIO 引脚向外部 W25Q64 发起通信,核心是发起指令操作。

        MCU SPI 接口是"教官",W25Q64 是"士兵"。


5. 实验 Demo:读取 W25Q64 的 JEDEC ID

5.1 JEDEC ID 是什么

  • JEDEC 全称 Joint Electron Device Engineering Council(电子器件工程联合委员会),是半导体行业负责制定全球统一标准的组织;
  • ID 即 Identification(识别/身份);
  • JEDEC ID 是由 JEDEC 统一分配给各厂家的识别码,确保全球每一款 Flash 芯片都有唯一"身份证号";
  • W25Q64 对应指令:9Fh(Read JEDEC ID)。

9Fh 指令时序(数据手册 8.3.12):

  • 先将 /CS 拉低,移入指令码 9Fh;
  • 之后在 CLK 下降沿、按 MSB(最高位)优先顺序依次移出 3 个字节:
    1. Manufacturer ID(MF7-MF0):华邦 = EFh;
    2. Memory Type(ID15-ID8):= 40h;
    3. Capacity(ID7-ID0):= 17h。

5.2 实验需求

  • 选择串口 UART1,重定向 printf,打印读取到的 ID;
  • 结合原理图,使用 SPI1,用硬件 NSS?不——用软件 GPIO 片选 W25Q64;
  • 不需要多文件项目,为了快速,单文件完成即可。

5.3 STM32CubeMX 配置步骤

5.3.1 开启调试
  • System Core → SYS → Debug 选择 Serial Wire(SWD),否则可能丢失调试接口。
5.3.2 开启 HSE
  • RCC → High Speed Clock (HSE) 选择 Crystal/Ceramic Resonator(外部高速晶振)。
5.3.3 配置时钟树
  • 目标:HCLK = 72MHz;
  • HSE 8MHz → PLL 倍频(×9)→ SYSCLK 72MHz → AHB/APB1/APB2 分频(APB1=36MHz,APB2=72MHz)。
5.3.4 配置 USART1 串口
  • Mode = Asynchronous(异步);
  • 参数:115200 波特率、8 数据位、None 无校验、1 停止位(115200-8-N-1);
  • TX=PA9,RX=PA10。
5.3.5 配置 SPI1
  • Mode = Full-Duplex Master(全双工主机);
  • Hardware NSS Signal = Disable(关闭硬件片选);
  • 关闭硬件片选后,引脚列表里只剩 PA5(SCK)、PA6(MISO)、PA7(MOSI),没有 PA4(NSS)。

为什么关闭硬件片选信号?(重点)

  • 最佳实践:大部分情况采用软件 NSS(GPIO 模拟),不用硬件 NSS;
  • 更灵活:硬件 NSS 在 STM32F103ZET6 上 SPI1 对应固定引脚 PA4,原则上只能接入一个设备(特殊走线不考虑);
  • 片选本质是"拉低某根 GPIO",完全可用任意 GPIO 平替,GPIO 输出引脚多,硬件工程师可自由选择。

多从机片选示意(扩展理解):

                 NSS1 ──────────────► 从机1 /CS
主机 MCU         NSS2 ──────────────► 从机2 /CS
            GPIO NSS3 ──────────────► 从机3 /CS
   SCK ────────┬──────────┬────────── (共享)
  MOSI ────────┼──────────┼────────── (共享)
  MISO ◄───────┴──────────┴────────── (共享)

        哪个 GPIO 给哪个外设片选,由公司硬件/系统工程师决定,随板子不同而不同,不是由 MCU 决定。

灵犀板 SPI1 上挂了两个设备:

  • SPI Flash(W25Q64);
  • 3.2 寸 LCD 触摸屏(后面用)。

查灵犀板原理图确认片选方案(软件片选,PA4 被用作它用):

  • PE6 → SPI Flash(W25Q64)片选(FLASH_SS);
  • PE4 → LCD 触摸屏片选(TC_SS)。

连接示意:

  • SCK=PA5、MOSI=PA7、MISO=PA6(两个外设共享);
  • PE6 → W25Q64 /CS;PE4 → LCD /CS。

特别注意:

        今天只用 PE6 片选 W25Q64;

        但必须同时初始化 PE4 并拉高,否则 PE4 电平不确定可能导致 LCD 被误选中,引发未知错误。PE4、PE6 默认都拉高,片选时只需拉低 PE6。

GPIOE 配置(PE4、PE6)
  • GPIO mode:Output Push-Pull(通用推挽输出);
  • GPIO output level:High(默认高电平);
  • Pull-up/Pull-down:No pull-up and no pull-down;
  • Maximum output speed:Low(或默认)。
SPI1 参数配置(Parameter Settings)
参数配置值说明
Frame FormatMotorola标准 4 线 SPI(唯一常用选项)
Data Size8 Bits8 位数据帧
First BitMSB First高位优先
Baud Rate Prescaler64(分频系数)72MHz/64 ≈ 1.125 MB/s,先低速保证稳定
Clock Polarity CPOLLow空闲低电平
Clock Phase CPHA1 Edge第一个边沿采样
模式Mode 0(CPOL=0, CPHA=0)W25Q64 支持 Mode0/Mode3
CRC CalculationDisabled关闭 CRC
NSS Signal TypeSoftware软件片选

SPI 四种模式:

        由 CPOL(空闲电平)和 CPHA(采样边沿)组合出 Mode0~Mode3;

        W25Q64 支持 Mode0(0,0)和 Mode3(1,1),本实验用 Mode0。


5.3.6 项目管理与生成代码
  • Project:命名(如 SPW25Q64)、选 Toolchain/IDE = MDK-ARM(Keil V5.32);
  • Code Generator:可勾选生成外设初始化成对文件;
  • 点击 GENERATE CODE。
5.3.7 重定位 printf
  • 导入之前写好的 BSP_Drivers(含 fputc 重定向);
  • Keil 中勾选 Use MicroLIB(魔术棒 Target 页);
  • 注意头文件添加路径(Include Paths);
  • 先打印 hello world 验证串口正常(115200,COM 口)。

5.4 代码实现

5.4.1 片选使能 / 禁用(操作 PE6)
/* 使能 W25Qxx 片选:PE6 拉低(低电平有效) */
void W25Qxx_CS_Enable(void)
{
    HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_RESET);
}

/* 禁用 W25Qxx 片选:PE6 拉高 */
void W25Qxx_CS_Disable(void)
{
    HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_SET);
}

5.4.2 方案1:HAL_SPI_TransmitReceive(全双工一次交换)
uint32_t W25Qxx_ReadID(void)
{
    uint8_t cmd = 0x9F;
    uint8_t tx_buffer[4] = {cmd, 0x00, 0x00, 0x00};
    uint8_t rx_buffer[4] = {0x00, 0x00, 0x00, 0x00};
    uint32_t id = 0;
    HAL_StatusTypeDef state;

    W25Qxx_CS_Enable();                       // 拉低片选

    state = HAL_SPI_TransmitReceive(&hspi1, tx_buffer, rx_buffer, 4, HAL_MAX_DELAY);
    if (state != HAL_OK)
    {
        printf("HAL_SPI_TransmitReceive error!\n");
        goto exit;                            // 统一出口,保证片选一定被释放
    }

    id = (rx_buffer[1] << 16) | (rx_buffer[2] << 8) | rx_buffer[3];

exit:
    W25Qxx_CS_Disable();                      // 无论成功失败都执行
    return id;                                // 出错 id=0,成功为读到的值
}

运行结果:串口打印 id : 0xef4017。

JEDEC ID 格式解析(0xEF4017):

  • EF = Winbond 华邦;40 = 存储类型;17 = 容量;
  • 容量计算:末尾 17h = 十进制 23,容量 = 2^23 = 8MB(2^20×8 = 8388608 字节)。

一次读 ID 共发生 4 次数据交换(全双工本质):

  • 主机发 9F(第1字节交换,主机忽略收到的);
  • 从机依次回 EF、40、17(第2、3、4字节交换,从机忽略主机发的 dummy);
  • 全双工下每发一个字节必然同时收一个字节,共 4 字节交换。

5.4.3 方案2:HAL_SPI_Transmit + HAL_SPI_Receive(先发后收)

逻辑:

        主机先只发送 0x9F,再只接收 3 字节。

        只发/只收在功能逻辑上是全双工通信的子集,HAL 已做封装。

uint32_t W25Qxx_ReadID(void)
{
    uint8_t cmd = 0x9F;
    uint8_t id[3] = {0};
    HAL_StatusTypeDef state;

    W25Qxx_CS_Enable();                                   // 使能片选

    state = HAL_SPI_Transmit(&hspi1, &cmd, sizeof(cmd), HAL_MAX_DELAY);  // 只发送
    if (state != HAL_OK) { printf("HAL_SPI_Transmit error\n"); goto END; }

    state = HAL_SPI_Receive(&hspi1, id, 3, HAL_MAX_DELAY);              // 只接收
    if (state != HAL_OK) { printf("HAL_SPI_Receive error\n");  goto END; }

END:
    W25Qxx_CS_Disable();                                  // 关闭片选
    return (id[0] << 16) | (id[1] << 8) | id[2];
}

运行结果:同样打印 0xef4017。


5.5 关键函数详解

5.5.1 HAL_SPI_TransmitReceive(全双工收发)
HAL_StatusTypeDef HAL_SPI_TransmitReceive(
    SPI_HandleTypeDef *hspi,   // SPI 句柄(如 &hspi1),含波特率/相位/极性
    uint8_t *pTxData,          // 发送缓冲区首址(指令 + dummy)
    uint8_t *pRxData,          // 接收缓冲区首址(Flash 返回数据)
    uint16_t Size,             // 传输总字节数,收发长度必须相等,读ID=4
    uint32_t Timeout);         // 超时(ms),未跑完则退出返回超时状态
  • 缓冲区一一对应:pRxData[n] 对应发送 pTxData[n] 时同步接回的数据;
  • 阻塞式特性:在 Size 个字节传输完毕前,CPU 停在这里死等,不执行下一行。
  • 读 4 字节 ID 没问题,但读 4KB 大块数据会导致系统响应变慢;
  • 用户可忽略不关心的数据,只提取需要的(SPI 交换思维)。

5.5.2 HAL_SPI_Transmit / HAL_SPI_Receive(单工)
HAL_StatusTypeDef HAL_SPI_Transmit (SPI_HandleTypeDef *hspi, uint8_t *pData,
                                    uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive  (SPI_HandleTypeDef *hspi, uint8_t *pData,
                                    uint16_t Size, uint32_t Timeout);
  • pData:Transmit 时是发送源地址,Receive 时是接收存放地址;
  • Size:本次传输字节数(16-bit 模式下为字数);
  • Timeout:阻塞等待时间(ms)。

5.5.3 返回值 HAL_StatusTypeDef(枚举)
返回值含义
HAL_OK通信/传输成功
HAL_ERROR配置错误或硬件故障
HAL_BUSYSPI 模块正忙(可能上次传输未结束 / 正在 DMA)
HAL_TIMEOUT规定时间内未完成传输

5.5.4 Dummy 字节(哑元/填充/伪字节)
  • 定义:在 SPI(尤其读 Flash)语境下,为了产生时钟信号 SCLK 而发送的"无意义数据";
  • 根本原因:想从 Flash "读"东西时,必须被迫"写"点东西驱动时钟,写进去的就是 Dummy;
  • 发什么都不影响结果,工程上为整洁统一用 0x00 或 0xFF。

5.5.5 收发的物理本质(重点,回答"为什么接收还要发数据")
  • 时钟由 STM32 内部波特率发生器产生,硬件逻辑规定:只有往发送寄存器(DR/TXDR)写数据,时钟线才会产生对应数量(如8个)脉冲;
  • SPI 从设备无法产生 SCK,从机想发数据给主机,必须先让主机发送 Dummy Byte 主动驱动时钟,从而把从机数据"置换"出来;
  • HAL_SPI_Receive 的本质:底层为产生时钟,Master 必须往 MOSI 发数据(硬件自动发 Dummy),没有这些 Dummy 产生的时钟,Flash 无法把数据推出;
  • HAL_SPI_Transmit 发送时,同样通过 SCK 驱使把数据送出。

5.5.6 收发函数行为对比
  • "只发"和"只收"逻辑上单向,但物理上本质都是全双工(同时双向),只是不关心的数据被 HAL 丢弃或默认发送;
  • Transmit 时:发数据同时 Flash 也会吐回数据,但 HAL 只负责填满发送缓冲区,对 DR/RXDR 里的回传数据直接不读(被下一字节覆盖或退出时清标志忽略);
  • Receive 时:主机发出的 Dummy 对 Flash 是"废弃输入",Flash 在收到读指令后会忽略这些无效输入;
  • 对比结论:
    • TransmitReceive 强制提供两个缓冲区,显式展示每个发送字节对应的接收结果;
    • Transmit / Receive 只是 HAL 库帮你做了"减法"(自动丢弃/填充)。

5.6 三种通信方式对比(扩展,理解"阻塞式"定位)

方式特点CPU 占用适用场景
阻塞式(Polling)

死等传输完成,

代码最简单

高(等待期间 CPU 空转)短小数据(如读 ID、少量配置)
中断式(IT)

传完触发中断,

期间可做别的事

中中等数据、需响应多任务
DMA

硬件直接搬运,

无需 CPU 干预

低大块数据(如读整页/整扇区、刷屏)

 面试题(含答案)

一、SPI 协议基础

1. SPI 通信需要哪些角色?谁产生时钟?

        需要主机(Master)和从机(Slave)。

        时钟 SCK 只能由主机产生,从机无法产生时钟。主机一般是 MCU 片上 SPI 外设,从机多为 Flash、显示屏、传感器等。


2. 标准 SPI 有哪几根线?各自作用?

  • SCK:时钟,主机输出;
  • MOSI(DO):主机发、从机收;
  • MISO(DI):从机发、主机收;
  • NSS/CS:片选,低电平有效,用于选中某个从机。

3. SPI 的四种模式由什么决定?Mode0 是什么?

         由 CPOL(时钟极性,空闲电平)和 CPHA(时钟相位,采样边沿)决定。

        Mode0:CPOL=0(空闲低电平)、CPHA=0(第一个时钟边沿采样)。

        W25Q64 支持 Mode0 和 Mode3。


4. SPI 是同步还是异步?全双工还是半双工?

        同步(靠 SCK 同步)、全双工(MOSI/MISO 同时收发)。


5. SPI 和 I2C 的主要区别?

对比SPII2C
线数4 线(SCK/MOSI/MISO/CS)2 线(SCL/SDA)
片选每个从机一根 CS地址寻址
双工全双工半双工
速率高较低
从机数量受 CS 数量限制受地址限制

二、Flash 与 NOR/NAND

6. Flash 属于易失还是非易失存储器?

        非易失(NVM),断电后数据不丢失。


7. NOR Flash 和 NAND Flash 的区别?

维度NORNAND
访问独立地址线,随机访问地址/数据复用,串行
单位读字节,写以页页读写、块擦除
读速度快较慢
写/擦速度慢快
容量/成本小、贵大、便宜
可靠性高会位翻转,需 ECC
XIP支持不支持
用途存代码/固件存大数据(SSD/SD/U盘)

8. 什么是 XIP?为什么 NOR 支持而 NAND 不支持?

        XIP(eXecute In Place,就地执行)指 CPU 直接在存储器上取指执行,无需先拷贝到 RAM。

        NOR 有独立地址线、支持随机按字节访问,所以可以;

        NAND 串行访问、按页读出且可能出错,无法直接取指。


9. W25Q64 属于 NOR 还是 NAND?

        SPI NOR Flash。


10. "U盘/SSD/SD卡都是 Flash"这句话对吗?

        对,但 Flash 是底层存储单元(面粉),成品是加了不同协议和控制电路的设备(面包):USB+Flash=U盘,NVMe/SATA+Flash=SSD,SD协议+Flash=SD卡,SPI+Flash=SPI Flash。


三、W25Q64 结构与操作

11. W25Q64 的页、扇区、块、整片各是多大?什么关系?

  • 页 Page = 256 字节;
  • 扇区 Sector = 4KB = 16 页;
  • 块 Block = 64KB = 16 扇区;
  • 整片 = 8MB = 128 块。

12. W25Q64 容量是多少?地址线几位?地址范围?

        64 Mbit = 8 MB;24 位地址;

        地址范围 0x000000 ~ 0x7FFFFF,每个字节一个地址。


13. W25Q64 的读、写、擦除基本单位分别是什么?

        读:字节;

        写(编程):页(256B);

        擦除:扇区(4KB,最小)。


14. Flash 为什么"写之前必须先擦除"?擦除的本质是什么?

        NOR Flash 硬件上编程只能把 bit 从 1 变 0,不能把 0 变 1。

        擦除的本质是把目标区域全部置为 1,之后编程只需把对应位 1→0。


15. Flash 能像 EEPROM 那样按字节擦除吗?

        不能,最小擦除单位是扇区(4KB),必须整片/块/扇区整体重置。


16. W25Q64 擦除分哪几个级别?

        扇区擦除(4KB,指令 20h)、块擦除(32KB 52h / 64KB D8h)、

        整片擦除(C7h/60h)。


17. 页编程时如果数据跨页会怎样?

         单次最多写 256 字节,且必须在页边界内;

        超出页边界的数据会"回卷(Wrap)"到本页开头覆盖旧数据,不会自动写到下一页,需分多次编程。


18. 写以页为单位、擦除以扇区为单位,只想改几个字节怎么办?

用读-改-写(R-M-W):

  1. Read:把整个 4KB 扇区读到 RAM;
  2. Modify:在 RAM 中修改目标字节;
  3. Erase:擦除整个扇区;
  4. Write:把 4KB 分页(16 次页编程)写回。

19. W25Q64 的擦写寿命和数据保持时间?

        10 万次擦写周期;常温下数据可保持 20 年;工作电压 2.7~3.6V。


四、HAL 库与阻塞式通信

20. HAL_SPI_TransmitReceive 的参数和作用?

  HAL_SPI_TransmitReceive(hspi, pTxData, pRxData, Size, Timeout),

        全双工同时收发,发送和接收长度必须相等。

        读 JEDEC ID 时发 4 字节(9F + 3 dummy),收 4 字节,Size=4。


21. HAL_SPI_Transmit 和 Receive 与 TransmitReceive 的区别?

        Transmit 只发、Receive 只收,逻辑上是单工;

        但物理底层仍是全双工,只是 HAL 自动丢弃/填充了不关心的数据。

        TransmitReceive 显式提供收发两个缓冲区。


22. 什么是 Dummy 字节?为什么读 Flash 必须发 Dummy?

        Dummy(哑元字节)是为产生时钟而发送的无意义数据(常用 0x00/0xFF)。

        因为从机自己不能产生时钟,主机想读数据必须主动发字节驱动 SCK,从机才能借时钟把数据通过 MISO 置换出来。


23. 阻塞式 SPI 的"阻塞"体现在哪?有什么缺点?

         在指定字节数传输完成前,CPU 一直死等,不执行后面的代码。

        读几个字节没问题,读大块数据(如 4KB)会让系统响应变慢、CPU 空转。


24. HAL_SPI 系列函数的返回值有哪些?

        HAL_OK(成功)、HAL_ERROR(配置/硬件错误)、

        HAL_BUSY(外设忙)、HAL_TIMEOUT(超时)。


25. Timeout 参数的作用?

         超时时间(ms),规定时间内没传完就退出并返回 HAL_TIMEOUT;


五、片选与综合场景

26. 硬件 NSS 和软件片选(GPIO 模拟 CS)的区别?实际用哪个?

        硬件 NSS 由 SPI 外设自动控制,引脚固定(SPI1 是 PA4),一般只能接一个设备;

        软件片选用任意 GPIO 手动拉低/拉高,灵活、可接多个从机。

        实际工程绝大多数用软件片选。


27. 一根 SPI 总线怎么接多个从机?

         SCK/MOSI/MISO 三线并联共享,每个从机单独接一根 GPIO 作为 CS,主机同一时刻只拉低一个从机的 CS。


28. 灵犀板上 SPI1 挂了哪些设备?片选怎么分配?

        W25Q64 Flash(PE6 片选,FLASH_SS)和 3.2 寸 LCD 触摸屏(PE4 片选,TC_SS);SCK=PA5、MOSI=PA7、MISO=PA6 共享。


29. 为什么不用的那根片选(如 PE4)也要初始化并拉高?

         不初始化时引脚电平不确定,可能误选中 LCD,导致片选冲突、通信异常。

        所以所有挂在总线上的设备 CS 都要初始化为高(未选中),用谁再拉低谁。


30. JEDEC ID 是什么?W25Q64 用哪条指令读?读到的值是多少?

        JEDEC(电子器件工程联合委员会)统一分配的厂商/器件识别码。

        用指令 9Fh 读,

        W25Q64 读到 EF 40 17:EF=华邦,40=存储类型,17=容量(2^23=8MB),拼起来 0xEF4017。


高频手撕/场景题(重点中的重点)

手写读 JEDEC ID 流程:CS 拉低 → 发 9Fh → 读 3 字节(EF/40/17)→ CS 拉高。

解释读 Flash 为什么既要写又要读:从机无时钟,靠主机发 Dummy 产生 SCK 才能把数据顶出来(全双工交换)。

为什么擦除最小单位比写入大?改一字节的完整流程:读改擦写 四步。

阻塞 / 中断 / DMA 三种方式对比与选型:

方式CPU 占用适用
阻塞 Polling高,空等少量数据
中断 IT中中等数据/多任务
DMA低大块数据

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐