【STM32】 SPI阻塞式通信与HAL源码(串口读ID实战)


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 + Flash U 盘
NVMe / SATA / SAS 协议 + Flash SSD 固态硬盘
SD 协议 + Flash SD 卡
SPI + Flash SPI 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 Flash NAND 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 龙头,全球排名第二,产品应用广泛
中国大陆主要制造商
厂商 主要类型 说明
长江存储 YMTC NAND 拥有自主 Xtacking 技术,突破 200 层以上 3D NAND,国产存储核心力量
长鑫存储 CXMT DRAM(注:非 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 最核心的组织结构,四级层次:

层级 大小 包含关系
页 Page 256 字节 最小编程(写)单位
扇区 Sector 4 KB 每个扇区包含 16 个页(256×16 = 4096B)
块 Block 64 KB 每个块包含 16 个扇区(4KB×16 = 64KB)
整片 Chip 8 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 占用地址范围:0x0000000x7FFFFF
  • 每个地址对应 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 Enable 06h --- 写使能(写/擦除前必须先发)
Volatile SR Write Enable 50h --- 易失性状态寄存器写使能
Write Disable 04h --- 写禁止
Release Power-down / ID ABh 3 dummy + 1 ID 释放掉电/读制造商/设备 ID
Manufacturer/Device ID 90h 2 dummy + 2 dummy 读制造商/设备 ID
JEDEC ID 9Fh 读出 MF7-0、ID15-8、ID7-0 读 JEDEC ID(本实验用)
Read Unique ID 4Bh 4 dummy + 8 字节 读唯一 ID
Read Data 03h A23-A0 + 数据 读数据(标准)
Fast Read 0Bh A23-A0 + dummy + 数据 快速读
Page Program 02h A23-A0 + 数据 页编程(写)
Sector Erase (4KB) 20h A23-A0 扇区擦除
Block Erase (32KB) 52h A23-A0 32KB 块擦除
Block Erase (64KB) D8h A23-A0 64KB 块擦除
Chip Erase C7h/60h --- 整片擦除
Read Status Register-1/2/3 05h/35h/15h 读出状态 读状态寄存器(含 BUSY 位)
Write Status Register-1/2/3 01h/31h/11h 写入状态 写状态寄存器
Erase/Program Suspend / Resume 75h / 7Ah --- 擦写挂起 / 恢复
Power-down B9h --- 进入掉电
Enable/Reset Device 66h / 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 /CS FLASH_SS(PE6) 片选,低电平有效
2 DO(IO1) SPI1 MISO(PA6) 从机输出→主机输入
3 /WP(IO2) (上拉/接 VCC) 写保护
4 GND GND
5 DI(IO0) SPI1 MOSI(PA7) 主机输出→从机输入
6 CLK SPI1 SCK(PA5) 时钟
7 /HOLD or /RESET(IO3) (接 VCC) 保持/复位
8 VCC VDD_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 Format Motorola 标准 4 线 SPI(唯一常用选项)
Data Size 8 Bits 8 位数据帧
First Bit MSB First 高位优先
Baud Rate Prescaler 64(分频系数) 72MHz/64 ≈ 1.125 MB/s,先低速保证稳定
Clock Polarity CPOL Low 空闲低电平
Clock Phase CPHA 1 Edge 第一个边沿采样
模式 Mode 0(CPOL=0, CPHA=0) W25Q64 支持 Mode0/Mode3
CRC Calculation Disabled 关闭 CRC
NSS Signal Type Software 软件片选

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)
cpp 复制代码
/* 使能 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(全双工一次交换)
cpp 复制代码
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 已做封装。

cpp 复制代码
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(全双工收发)
cpp 复制代码
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(单工)
cpp 复制代码
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_BUSY SPI 模块正忙(可能上次传输未结束 / 正在 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 的主要区别?

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

二、Flash 与 NOR/NAND

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

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


7. NOR Flash 和 NAND Flash 的区别?

维度 NOR NAND
访问 独立地址线,随机访问 地址/数据复用,串行
单位 读字节,写以页 页读写、块擦除
读速度 较慢
写/擦速度
容量/成本 小、贵 大、便宜
可靠性 会位翻转,需 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 大块数据

相关推荐
吠品1 小时前
Nginx负载均衡配置与线上故障排查的一些经验
java·开发语言·数据库
endeavor121 小时前
BootLoader与OTA学习(三)
c语言·stm32
t13970181 小时前
太阳能板与电池的电压匹配:别用开路电压 Voc,要用 Vmp
嵌入式硬件·物联网·硬件设计·电源管理·太阳能供电
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day44|Vue 前端项目搭建 思维导图复盘
java·开发语言·spring boot·学习·阿里云
zhangzhangkeji1 小时前
stm32 参考文档的目录结构 - 3:STM32F10xxx参考手册(中文).pdf,700多页
stm32·arm
wuyk5551 小时前
Python实战项目01:简易记事本系统
开发语言·python
暖焰核心2 小时前
GPIO ——从端口位电路到八种工作模式
stm32
老k一步步来2 小时前
鸣志驱动器 Alarm_CodeTypeDef 状态报警位详解
嵌入式硬件
君顾12 小时前
本地城市社区家政物业联动系统源码:架构设计与派单联动实战
java·开发语言·健身房