STM32 零基础可移植教程 22:SPI 入门,先读一个外部 Flash
前面几篇我们讲完了 I2C:
bash
第 18 篇:I2C Scanner,先确认总线上有没有设备
第 19 篇:I2C 读写寄存器,先读一个设备 ID
这一篇开始讲 SPI。但在动手之前,我们先搞清楚 SPI 到底是什么。
SPI 是什么?
SPI 全称是 Serial Peripheral Interface(串行外设接口)。
它是一种让单片机(主机)和外部芯片(从机)之间互相通信的协议。听起来很抽象?拆开看就简单了。
串行(Serial):数据是一位一位发的,不是 8 位一起发。好处是引脚少。
同步(Synchronous):通信双方共用一根时钟线(SCK)。时钟就像节拍器------主机敲一下,双方同时收发一位数据。
这和 UART 不一样。UART 没有独立的时钟线,双方要靠提前约定好的波特率来猜对方什么时候开始发。SPI 不用猜------时钟线会明确告诉双方"现在收/发一位"。
全双工(Full-Duplex):SPI 可以同时收发。主机通过 MOSI 发数据的同时,从机也在通过 MISO 回数据。就像两条独立车道,一进一出互不影响。
这是 SPI 很关键的一个特点------主机每发一个字节,必然同时收到一个字节。后面写代码时会反复体会到这一点。
SPI 的四根线
SPI 最少需要 4 根线:
|
信号
|
全称
|
方向
|
干什么的
|
| --- | --- | --- | --- |
|
SCK
|
Serial Clock
|
主机→从机
|
时钟线,主机控制通信节奏
|
|
MOSI
|
Master Out Slave In
|
主机→从机
|
主机发给从机的数据线
|
|
MISO
|
Master In Slave Out
|
从机→主机
|
从机回给主机的数据线
|
|
CS
|
Chip Select
|
主机→从机
|
片选,告诉哪个从机"轮到你了"
|
两个容易记混的名字:
bash
MOSI = Master Output, Slave Input → 主机出,从机进
MISO = Master Input, Slave Output → 主机进,从机出
接线的时候别接反,这是 SPI 调试中最常见的错误。
一次 SPI 通信长什么样
假设你想读一个 Flash 芯片的 ID:
-
主机把 Flash 的 CS 拉低 → "Flash,我要跟你说话了"
-
主机通过 MOSI 发送命令字节(比如
0x9F,意思是"报上你的 ID") -
主机继续发几个哑字节 (dummy byte,通常是
0xFF)------目的是让时钟继续跳。因为时钟每跳一次,从机才能通过 MISO 回传一位数据 -
从机通过 MISO 把 ID 数据一位一位传回来
-
主机把 CS 拉高 → "我说完了"
流程图:
bash
CS: \___________________/
拉低 拉高
SCK: |‾|_|‾|_|‾|_|‾|_| (时钟一直在跳)
MOSI: [0x9F][0xFF][0xFF][0xFF] (主机发送)
MISO: [?? ][ID1 ][ID2 ][ID3 ] (从机回复)
注意:主机发的后 3 个字节(0xFF)本身对 Flash 没有意义,但它们的作用很大------制造时钟。因为 SPI 的时钟是主机控制的,只有主机发数据时钟才会跳,时钟跳了从机才能回数据。

为什么 SPI 没有地址?
这是 SPI 和 I2C 最核心的区别。
I2C 是地址寻址:总线上挂多个设备,每个有一个 7 位地址。主机先发地址,地址匹配的从机回应。
SPI 是硬件片选:
bash
SCK、MOSI、MISO 三根线 → 所有从机共用
CS 线 → 每个从机独占一根,接到主机的一个 GPIO
主机想跟谁说话,就把谁的 CS 拉低。其他从机的 CS 保持高电平,它们会自动忽略总线上的数据。
bash
SCK / MOSI / MISO(三根线所有设备共用)
|
+--------+--------+
| | |
CS1 CS2 CS3
| | |
从机1 从机2 从机3
所以 SPI 没有 I2C Scanner 那种"扫一遍总线看谁在"的功能。你必须事先知道:接的是哪个芯片、它的 CS 是哪个 GPIO、它支持什么命令、它的 SPI Mode 是什么。
SPI Mode(CPOL 和 CPHA)
SPI 有一个让新手容易困惑的配置:SPI Mode。它由两个参数决定:
-
CPOL(Clock Polarity,时钟极性):时钟空闲时是高电平还是低电平
-
CPHA(Clock Phase,时钟相位):在时钟的第几个边沿采样数据
组合出 4 种模式:
|
Mode
|
CPOL
|
CPHA
|
空闲时 SCK
|
在哪个边沿采样
|
| --- | --- | --- | --- | --- |
|
Mode 0
|
0
|
0
|
低电平
|
第 1 个边沿
|
|
Mode 1
|
0
|
1
|
低电平
|
第 2 个边沿
|
|
Mode 2
|
1
|
0
|
高电平
|
第 1 个边沿
|
|
Mode 3
|
1
|
1
|
高电平
|
第 2 个边沿
|
不要被这 4 种模式吓到。 对于 W25Qxx 这类 SPI Flash,入门阶段记住一句话:
先用 Mode 0(CPOL = Low,CPHA = 1 Edge)。绝大多数 Flash 都支持。等调通了再研究其他模式。
模式配错的典型现象:读出来全是 0xFF、全是 0x00、或者每次读的值不一样。
SPI、I2C、UART 快速对比
| |
SPI
|
I2C
|
UART
|
| --- | --- | --- | --- |
|
最少线数
|
4 根
|
2 根
|
2 根(TX/RX)
|
|
通信方式
|
同步
|
同步
|
异步
|
|
双工
|
全双工
|
半双工
|
全双工
|
|
速度
|
快(MHz 级)
|
较慢
|
中等
|
|
区分设备
|
CS 硬件片选
|
地址寻址
|
点对点,无寻址
|
|
总线扫描
|
无
|
有(I2C Scanner)
|
无
|
|
典型场景
|
Flash、屏幕、传感器
|
传感器、RTC、EEPROM
|
串口调试、GPS、蓝牙
|
有了这些基础概念,再来看具体怎么用 STM32 的 SPI 外设。
所以这一篇只做一个明确目标:
bash
用 STM32 SPI 读取一个外部 SPI Flash 的 JEDEC ID
这类 SPI Flash 常见型号是 W25Qxx。
它们通常支持一个标准命令:
bash
0x9F:Read JEDEC ID
主机发 0x9F,Flash 会返回 3 个字节:
bash
Manufacturer ID
Memory Type
Capacity
这非常适合做 SPI 入门验证。
本篇目标
最终现象:
串口打印类似:
bash
SPI Flash ID test
JEDEC ID: manufacturer=0xEF, type=0x40, capacity=0x17
Device ID: manufacturer=0xEF, device=0x16
不同 Flash 型号读出来的值可能不一样。
比如 W25Q32、W25Q64、W25Q128 的容量字节就会不同。
本篇用到的外设:
bash
SPI
GPIO Output
USART printf
本篇跑通标准:
-
CubeMX 能正确配置 SPI Master;
-
CS 引脚能作为普通 GPIO 输出;
-
串口能打印 JEDEC ID;
-
知道
SCK/MISO/MOSI/CS分别是什么; -
知道 SPI 没有 I2C Scanner 那种地址扫描;
-
换 SPI Flash 或换板子时,知道要改 SPI 实例、引脚、CS 和 SPI Mode。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意带 SPI 的 STM32 都可以
|
|
SPI Flash 模块
|
W25Q32/W25Q64/W25Q128 等都可以
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
串口工具
|
用来看 ID 输出
|
|
杜邦线
|
外接 SPI Flash 模块时使用
|
|
原理图/模块资料
|
确认引脚和供电电压
|
如果你的开发板板载了 SPI Flash,也可以直接用板载 Flash。
但一定要看原理图,确认它接到哪个 SPI:
bash
SPI1 还是 SPI2
SCK/MISO/MOSI 是哪些引脚
CS 接到哪个 GPIO
如果你暂时没有 SPI Flash,就不太适合完整验证这一篇。
SPI 没有 I2C Scanner 那种"空总线也能扫一遍"的验证方式。
没有从机时,MISO 可能读到:
bash
0xFF
0x00
随机值
这些都不能证明 SPI 真的通了。

硬件连接
SPI Flash 常见引脚:
|
SPI Flash
|
STM32
|
| --- | --- |
|
VCC
|
3.3V
|
|
GND
|
GND
|
|
SCK / CLK
|
SPI SCK
|
|
DO / SO / MISO
|
SPI MISO
|
|
DI / SI / MOSI
|
SPI MOSI
|
|
CS / NSS
|
任意普通 GPIO 输出
|
注意几个命名:
bash
MOSI:Master Out Slave In,主机输出,从机输入
MISO:Master In Slave Out,主机输入,从机输出
所以接线是:
bash
STM32 MOSI -> Flash DI/SI
STM32 MISO -> Flash DO/SO
STM32 SCK -> Flash CLK/SCK
STM32 GPIO -> Flash CS
别把 MOSI 和 MISO 接反。
很多模块丝印写的是:
bash
DO
DI
CLK
CS
其中:
bash
DO -> 接 STM32 MISO
DI -> 接 STM32 MOSI
这点很容易接错。
CS 为什么单独用 GPIO 控制
SPI 的 CS 很关键。
它的作用是:
bash
选中当前要通信的从机
通常:
bash
CS 拉低 -> 选中 Flash
CS 拉高 -> 结束本次通信
本篇不依赖硬件 NSS。
我们把 CS 当成普通 GPIO 输出,自己控制:
bash
CS 拉低
发送命令并接收数据
CS 拉高
这样做的好处是:
-
新手更容易理解一帧 SPI 通信的开始和结束;
-
后面一个 SPI 总线上挂多个设备时,每个设备都可以有自己的 CS;
-
不同开发板移植时,只要改 CS 引脚即可。
本篇要求在 CubeMX 里给 CS 引脚设置 User Label:
bash
SPI_FLASH_CS
这样代码里会使用:
bash
SPI_FLASH_CS_GPIO_Port
SPI_FLASH_CS_Pin
如果没设置标签,编译时会直接报错提醒。

CubeMX 配置步骤
1. 新建或复制工程
建议从第 07 篇 USART printf 工程复制一份,改名为:
bash
22_spi_flash_id
因为这篇需要串口打印 ID。
如果你重新建工程,也按前面流程:
-
选择芯片型号;
-
SYS -> Debug设置为Serial Wire; -
配置 USART printf;
-
配置 SPI;
-
配置 CS GPIO;
-
生成 Keil 工程。

2. 配置 SPI
选择一个 SPI,比如:
bash
SPI1
模式选择:
bash
Full-Duplex Master
常见 SPI1 引脚示例:
|
SPI1 信号
|
常见引脚
|
| --- | --- |
|
SCK
|
PA5
|
|
MISO
|
PA6
|
|
MOSI
|
PA7
|
具体以你的芯片和开发板原理图为准。

3. 设置 SPI 参数
推荐入门配置:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
Mode
|
Full-Duplex Master
|
STM32 当主机
|
|
Hardware NSS Signal
|
Disable / Software
|
CS 用普通 GPIO 控制
|
|
Data Size
|
8 Bits
|
Flash 命令按字节收发
|
|
First Bit
|
MSB First
|
常见 SPI Flash 要求
|
|
Prescaler
|
64 或 128
|
先慢一点,稳定优先
|
|
Clock Polarity
|
Low
|
Mode 0
|
|
Clock Phase
|
1 Edge
|
Mode 0
|
|
CRC Calculation
|
Disable
|
入门先不用
|
为什么分频先设大一点?
因为刚开始排查接线和模式,速度越慢越好排。
等 ID 读通了,再逐步提高 SPI 速度。

4. 配置 CS 引脚
找一个普通 GPIO 作为 Flash 的 CS。
配置为:
bash
GPIO_Output
初始电平建议:
bash
High
因为 CS 通常低电平有效。
User Label 设置为:
bash
SPI_FLASH_CS
CubeMX 生成后,main.h 里会有:
bash
#define SPI_FLASH_CS_Pin ...
#define SPI_FLASH_CS_GPIO_Port ...
本篇代码就靠这两个宏控制片选。

5. 配置 USART printf
继续使用前面的串口打印配置:
bash
115200
8 数据位
无校验
1 停止位

6. 生成 Keil 工程
点击:
bash
GENERATE CODE

打开 Keil,先编译 CubeMX 原始工程。
确认:
bash
0 Error(s)
再加本篇代码。
Keil 工程生成和编译
本篇新增两个文件:
bash
Core/Inc/app_spi_flash.h
Core/Src/app_spi_flash.c
如果你手动新建 .c 文件,记得在 Keil 工程树里添加:
bash
Core/Src/app_spi_flash.c
否则会报:
bash
undefined symbol App_SPIFlash_ReadJedecID
这不是函数写错,而是 .c 文件没有参与编译。

完整代码
1. 新建 Core/Inc/app_spi_flash.h
bash
#ifndef APP_SPI_FLASH_H
#define APP_SPI_FLASH_H
#include "main.h"
#include <stdint.h>
/*
* JEDEC ID 结构体
* 通过 0x9F 命令从 SPI Flash 读取,包含制造商、存储类型、容量三个字节
*/
typedef struct
{
uint8_t manufacturer_id; /* 制造商 ID,例如 0xEF = Winbond */
uint8_t memory_type; /* 存储类型,不同型号返回不同值 */
uint8_t capacity; /* 容量标识,W25Q32/W25Q64/W25Q128 不同 */
} App_SPIFlash_JedecID;
/* 初始化:将 CS 引脚拉高,确保 Flash 处于未选中状态 */
void App_SPIFlash_Init(void);
/* 读取 JEDEC ID(命令 0x9F),结果存入 id 指向的结构体 */
HAL_StatusTypeDef App_SPIFlash_ReadJedecID(App_SPIFlash_JedecID *id);
/* 读取 Device ID(命令 0x90),返回制造商 ID 和设备 ID */
HAL_StatusTypeDef App_SPIFlash_ReadDeviceID(uint8_t *manufacturer_id, uint8_t *device_id);
#endif
2. 新建 Core/Src/app_spi_flash.c
bash
#include "app_spi_flash.h"
/*
* 为什么 SPI 和 CS 要分开控制?
* SPI 外设只负责 SCK/MOSI/MISO 三根线的时序。
* CS(片选)由应用层通过普通 GPIO 手动拉低/拉高。
* 好处:一帧通信的开始和结束明确;一个 SPI 总线上挂多个设备时,
* 每个设备各用一个 GPIO 做 CS;换板子移植时只改 CS 引脚。
*/
/* 默认使用 SPI1。如果 Flash 接到 SPI2,改为 hspi2 即可 */
#ifndef APP_SPI_FLASH_HANDLE
#define APP_SPI_FLASH_HANDLE hspi1
#endif
/* SPI 通信超时时间(毫秒),读 ID 这种几个字节的操作 100ms 足够 */
#ifndef APP_SPI_FLASH_TIMEOUT_MS
#define APP_SPI_FLASH_TIMEOUT_MS 100u
#endif
/* 编译期检查:CS 引脚必须在 CubeMX 中设置 User Label = "SPI_FLASH_CS" */
#ifndef SPI_FLASH_CS_GPIO_Port
#error "SPI_FLASH_CS_GPIO_Port is not defined. Set CS pin User Label to SPI_FLASH_CS in CubeMX."
#endif
#ifndef SPI_FLASH_CS_Pin
#error "SPI_FLASH_CS_Pin is not defined. Set CS pin User Label to SPI_FLASH_CS in CubeMX."
#endif
/*
* SPI Flash 常用命令
* 0x9F = JEDEC ID:读取制造商 + 存储类型 + 容量(3 字节)
* 0x90 = Device ID:需要先发 3 字节地址,返回制造商 + 设备 ID
*/
#define APP_SPI_FLASH_CMD_JEDEC_ID 0x9Fu
#define APP_SPI_FLASH_CMD_DEVICE_ID 0x90u
/*
* 哑字节(Dummy Byte)
* SPI 是全双工的:主机每发一个字节,必然同时收到一个字节。
* 当主机只想收数据时,必须发一些无意义的字节来维持时钟------这就是哑字节。
* 为什么是 0xFF?Flash 的 MISO 空闲时通常被上拉,发 0xFF 干扰最小。
*/
#define APP_SPI_FLASH_DUMMY_BYTE 0xFFu
/* 引用 CubeMX 在 spi.c 中生成的 SPI 句柄 */
extern SPI_HandleTypeDef APP_SPI_FLASH_HANDLE;
/* CS 拉低:选中 Flash。CS 低电平有效------Flash 看到 CS 变低才开始响应 */
static void App_SPIFlash_CS_Low(void)
{
HAL_GPIO_WritePin(SPI_FLASH_CS_GPIO_Port, SPI_FLASH_CS_Pin, GPIO_PIN_RESET);
}
/* CS 拉高:取消选中 Flash,通信结束 */
static void App_SPIFlash_CS_High(void)
{
HAL_GPIO_WritePin(SPI_FLASH_CS_GPIO_Port, SPI_FLASH_CS_Pin, GPIO_PIN_SET);
}
/*
* 封装 HAL 的发送+接收函数。
* 为什么用 HAL_SPI_TransmitReceive 而不是分开写?
* 因为 SPI 是全双工的------收发在同一个时钟周期完成,
* 分开调用会导致中间时钟停顿,Flash 可能误判。
*/
static HAL_StatusTypeDef App_SPIFlash_TransmitReceive(const uint8_t *tx_data,
uint8_t *rx_data,
uint16_t length)
{
return HAL_SPI_TransmitReceive(&APP_SPI_FLASH_HANDLE,
(uint8_t *)tx_data,
rx_data,
length,
APP_SPI_FLASH_TIMEOUT_MS);
}
/* 初始化:上电后调用一次,确保 CS 为高(Flash 未选中) */
void App_SPIFlash_Init(void)
{
App_SPIFlash_CS_High();
}
/*
* 读取 JEDEC ID(命令 0x9F)
*
* 通信时序(参考前面"一次 SPI 通信长什么样"的图):
* CS 拉低
* MOSI 发送: [0x9F] [0xFF] [0xFF] [0xFF]
* MISO 收到: [?? ] [ID1 ] [ID2 ] [ID3 ]
* CS 拉高
*
* rx_buffer 下标说明:
* [0] = 发送 0x9F 时收到的(无效,Flash 还没反应过来)
* [1] = 第 1 个哑字节换回 → Manufacturer ID
* [2] = 第 2 个哑字节换回 → Memory Type
* [3] = 第 3 个哑字节换回 → Capacity
*/
HAL_StatusTypeDef App_SPIFlash_ReadJedecID(App_SPIFlash_JedecID *id)
{
uint8_t tx_buffer[4] =
{
APP_SPI_FLASH_CMD_JEDEC_ID, /* [0] 命令 */
APP_SPI_FLASH_DUMMY_BYTE, /* [1] 哑字节 → 换 Manufacturer ID */
APP_SPI_FLASH_DUMMY_BYTE, /* [2] 哑字节 → 换 Memory Type */
APP_SPI_FLASH_DUMMY_BYTE /* [3] 哑字节 → 换 Capacity */
};
uint8_t rx_buffer[4] = {0u};
HAL_StatusTypeDef status;
if (id == 0) /* 空指针保护 */
{
return HAL_ERROR;
}
/* 标准 SPI 通信帧:CS 拉低 → 收发数据 → CS 拉高 */
App_SPIFlash_CS_Low();
status = App_SPIFlash_TransmitReceive(tx_buffer, rx_buffer, 4u);
App_SPIFlash_CS_High();
if (status != HAL_OK)
{
return status; /* SPI 通信失败,直接返回错误码 */
}
/* 从接收缓冲区提取 3 字节 ID(跳过 [0],那是命令阶段的无效数据) */
id->manufacturer_id = rx_buffer[1];
id->memory_type = rx_buffer[2];
id->capacity = rx_buffer[3];
return HAL_OK;
}
/*
* 读取 Device ID(命令 0x90)
*
* 通信时序:
* MOSI: [0x90] [0x00] [0x00] [0x00] [0xFF] [0xFF]
* \命令/ \---- 3 字节地址 ----/ \-- 哑字节 --/
* MISO: [?? ] [?? ] [?? ] [?? ] [ID1 ] [ID2 ]
*
* 和 0x9F 的区别:0x9F 不需要地址直接返回,0x90 需要先发 3 字节地址
* 两个命令返回的 manufacturer ID 应该一致,可以互相印证
*
* rx_buffer 下标说明:
* [0..3] = 命令+地址阶段的无效数据
* [4] = Manufacturer ID
* [5] = Device ID
*/
HAL_StatusTypeDef App_SPIFlash_ReadDeviceID(uint8_t *manufacturer_id, uint8_t *device_id)
{
uint8_t tx_buffer[6] =
{
APP_SPI_FLASH_CMD_DEVICE_ID, /* [0] 命令 0x90 */
0x00u, /* [1] 地址字节 1(通常填 0) */
0x00u, /* [2] 地址字节 2(通常填 0) */
0x00u, /* [3] 地址字节 3(通常填 0) */
APP_SPI_FLASH_DUMMY_BYTE, /* [4] 哑字节 → 换 Manufacturer ID */
APP_SPI_FLASH_DUMMY_BYTE /* [5] 哑字节 → 换 Device ID */
};
uint8_t rx_buffer[6] = {0u};
HAL_StatusTypeDef status;
if ((manufacturer_id == 0) || (device_id == 0)) /* 空指针保护 */
{
return HAL_ERROR;
}
App_SPIFlash_CS_Low();
status = App_SPIFlash_TransmitReceive(tx_buffer, rx_buffer, 6u);
App_SPIFlash_CS_High();
if (status != HAL_OK)
{
return status;
}
/* 提取后 2 字节:跳过前面命令+地址阶段的无效数据 */
*manufacturer_id = rx_buffer[4];
*device_id = rx_buffer[5];
return HAL_OK;
}
这里最关键的是 CS 拉低→收发→CS 拉高这个顺序:
bash
App_SPIFlash_CS_Low(); // 1. 选中 Flash
status = App_SPIFlash_TransmitReceive(tx_buffer, rx_buffer, 4u); // 2. 发送命令+收数据
App_SPIFlash_CS_High(); // 3. 释放 Flash
SPI Flash 看到 CS 拉低后,才认为一帧通信开始。
通信过程中 CS 要保持低。
读完以后 CS 拉高,Flash 才认为这一帧结束。
main.c 调用方式
1. 添加头文件
在 main.c 顶部添加:
bash
/* USER CODE BEGIN Includes */
#include "app_spi_flash.h" /* SPI Flash 驱动 */
#include <stdio.h> /* printf 需要 */
/* USER CODE END Includes */
2. 初始化后读取 ID
确认 CubeMX 已生成:
bash
MX_GPIO_Init();
MX_SPI1_Init();
MX_USART1_UART_Init();
然后在 USER CODE BEGIN 2 中添加:
bash
/* USER CODE BEGIN 2 */
App_SPIFlash_JedecID jedec_id; /* 存放 JEDEC ID(3 字节) */
uint8_t manufacturer_id = 0u; /* 存放 Device ID 的制造商字节 */
uint8_t device_id = 0u; /* 存放 Device ID 的设备字节 */
App_SPIFlash_Init(); /* 初始化 CS 为高电平 */
printf("\r\nSPI Flash ID test\r\n");
/* 方式一:用 0x9F 命令读 JEDEC ID */
if (App_SPIFlash_ReadJedecID(&jedec_id) == HAL_OK)
{
printf("JEDEC ID: manufacturer=0x%02X, type=0x%02X, capacity=0x%02X\r\n",
jedec_id.manufacturer_id,
jedec_id.memory_type,
jedec_id.capacity);
}
else
{
printf("Read JEDEC ID failed.\r\n");
}
/* 方式二:用 0x90 命令读 Device ID,两个命令返回的 manufacturer 可以互相印证 */
if (App_SPIFlash_ReadDeviceID(&manufacturer_id, &device_id) == HAL_OK)
{
printf("Device ID: manufacturer=0x%02X, device=0x%02X\r\n",
manufacturer_id,
device_id);
}
else
{
printf("Read Device ID failed.\r\n");
}
/* USER CODE END 2 */
3. while 循环
本篇只在上电后读一次 ID,while 里先不用写。
如果你想每 2 秒读一次,也可以放到 USER CODE BEGIN 3。
但入门调试时,建议先读一次,串口输出更干净。
编译、下载和验证
代码加完后:
-
Keil 编译;
-
下载程序;
-
打开串口助手;
-
复位开发板;
-
查看输出。
正常情况下可能看到:

bash
SPI Flash ID test
JEDEC ID: manufacturer=0xEF, type=0x40, capacity=0x17
Device ID: manufacturer=0xEF, device=0x16
这里 0xEF 常见于 Winbond。
不同 Flash 返回值可能不同。
如果你看到:
bash
JEDEC ID: manufacturer=0xFF, type=0xFF, capacity=0xFF
或者:
bash
JEDEC ID: manufacturer=0x00, type=0x00, capacity=0x00
通常说明 SPI 还没真正通。
优先查:
bash
CS
MISO/MOSI 是否接反
SPI Mode
Flash 供电
SPI 速度
移植到其他板子的修改点
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
SPI 实例
|
可能用 SPI1、SPI2、SPI3
|
CubeMX,APP_SPI_FLASH_HANDLE
|
|
SCK/MISO/MOSI 引脚
|
不同板子 SPI 引脚不同
|
CubeMX Pinout
|
|
CS 引脚
|
CS 通常是任意 GPIO
|
CubeMX GPIO,User Label = SPI_FLASH_CS
|
|
SPI Mode
|
不同设备要求不同
|
CubeMX CPOL/CPHA
|
|
SPI 分频
|
线长和模块影响速度
|
CubeMX Prescaler
|
|
Flash 命令
|
不同 SPI 设备命令不同
| APP_SPI_FLASH_CMD_* |
|
ID 含义
|
不同芯片返回 ID 不同
|
查芯片手册
|
如果你用 SPI2,把代码里的默认句柄改成:
bash
#define APP_SPI_FLASH_HANDLE hspi2
如果你 CS 引脚标签不是 SPI_FLASH_CS,建议回 CubeMX 改成统一标签。
这样应用代码不用到处换名字。
常见问题排查
1. 编译报 SPI_FLASH_CS_GPIO_Port is not defined
说明 CubeMX 没有生成:
bash
SPI_FLASH_CS_GPIO_Port
SPI_FLASH_CS_Pin
解决方法:
-
回 CubeMX;
-
找到 CS 引脚;
-
设置为
GPIO_Output; -
User Label 填
SPI_FLASH_CS; -
重新 Generate Code。
2. 编译报 hspi1 未定义
说明你的工程没有开启 SPI1,或者实际用的是 SPI2。
打开 spi.c 看句柄:
bash
SPI_HandleTypeDef hspi1;
还是:
bash
SPI_HandleTypeDef hspi2;
如果实际是 SPI2,就改:
bash
#define APP_SPI_FLASH_HANDLE hspi2
3. 读出来全是 0xFF
常见原因:
-
MISO 没接好;
-
Flash 没有被 CS 选中;
-
CS 一直高;
-
Flash 没供电;
-
MISO 被上拉;
-
SPI Mode 不对。
先量一下 CS:
bash
读 ID 时 CS 应该从高变低,再回到高
如果 CS 从来不变,先查 CS GPIO。
4. 读出来全是 0x00
常见原因:
-
MISO 被拉低;
-
Flash 供电异常;
-
MISO/MOSI 接错;
-
SPI Mode 不对;
-
设备不是 SPI Flash,命令不支持
0x9F。
5. ID 每次都不一样
优先考虑:
-
杜邦线接触不良;
-
SPI 速度太快;
-
GND 没接好;
-
线太长;
-
CPOL/CPHA 不匹配;
-
Flash 电源不稳。
先把 Prescaler 调大,让 SPI 慢下来。
6. MISO 和 MOSI 到底怎么接
记住:
bash
STM32 MOSI -> Flash DI/SI
STM32 MISO -> Flash DO/SO
有些模块不会写 MOSI/MISO,而是写:
bash
DI
DO
从 Flash 角度看:
bash
DI = Data In = Flash 输入 = 接 STM32 MOSI
DO = Data Out = Flash 输出 = 接 STM32 MISO
7. SPI 没有扫描,那怎么知道设备在不在
SPI 没有 I2C 地址扫描。
你只能通过具体设备支持的命令验证。
对 SPI Flash 来说,常用:
bash
0x9F 读 JEDEC ID
如果是其他 SPI 设备,就要查它的数据手册。
比如某些传感器有:
bash
WHO_AM_I
ID
Product ID
但命令格式可能和 Flash 完全不同。
不要拿 0x9F 去读所有 SPI 设备。
本篇小结
这一篇我们完成了 SPI 入门的第一步:读取外部 SPI Flash ID。
你现在应该知道:
-
SPI 常见信号是 SCK、MOSI、MISO、CS;
-
SPI 没有 I2C 那种地址扫描,靠 CS 选择设备;
-
CS 通常用普通 GPIO 手动控制;
-
读 Flash JEDEC ID 常用命令是
0x9F; -
SPI Mode 由 CPOL/CPHA 决定,入门先用 Mode 0;
-
读到全
0xFF或全0x00时,先查 CS、MISO/MOSI、供电和 SPI Mode; -
换板子时重点改 SPI 实例、SCK/MISO/MOSI、CS、分频和命令。
下一篇继续 SPI:
STM32 SPI 排坑:读出来全是 0xFF、0x00 或 ID 乱跳,先查什么。
SPI 的坑和 I2C 不一样,下一篇会重点讲 CS 时序、MISO/MOSI、Mode 0/3、速度、接线和逻辑分析仪怎么看。