SPI(Serial Peripheral Interface)是一种同步、全双工的串行通信总线,使用 4 根线:
| 信号 | 全称 | 方向(主机→从机) |
|---|---|---|
| SCLK | Serial Clock | 主机→所有从机 |
| MOSI | Master Out Slave In | 主机→从机 |
| MISO | Master In Slave Out | 从机→主机 |
| NSS | Negative Slave Select | 主机→指定从机(低电平有效) |
注意不同芯片的引脚命名可能不同。例如 W25Q64 Flash 模块的标注:
| Flash 模块引脚 | 对应 SPI 信号 |
|---|---|
| DI (Data Input) | MOSI |
| DO (Data Output) | MISO |
| CLK (Clock) | SCLK |
| CS (Chip Select) | NSS |
主机通过多个 NSS 引脚分别控制多个从机:
主机 从机1 从机2 从机3
SCLK ──────────────→ SCLK SCLK SCLK
MOSI ──────────────→ MOSI MOSI MOSI
MISO ←────────────── MISO MISO MISO
NSS1 ──────────────→ NSS
NSS2 ──────────────→ NSS
NSS3 ──────────────→ NSS
同一时刻只能选中一个从机(NSS 为低电平),未被选中的从机 MISO 呈高阻态,不干扰总线。
上图:SPI 一主多从硬件连接图(来源:STM32入门教程 PPT 第147页)
SPI 的全双工特性:每发送一个字节的同时也会收到一个字节。主机的移位寄存器和从机的移位寄存器构成一个16 位的环形移位寄存器:
主机移位寄存器 ←→ MOSI → 从机移位寄存器
主机移位寄存器 ←→ MISO ← 从机移位寄存器
每个 SCLK 周期,主机和从机各移出一位(MOSI/MISO 方向),同时各移入一位。
因此如果主机只需要接收从机数据,仍需要发送一个虚拟字节(通常是 0xFF 或 0x00)来产生时钟脉冲。
SPI 有 4 种模式,由 CPOL(Clock Polarity,时钟极性)和 CPHA(Clock Phase,时钟相位)决定:
| 模式 | CPOL | CPHA | 空闲时 SCLK | 数据采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿(第一边沿) |
| 模式 1 | 0 | 1 | 低电平 | 下降沿(第二边沿) |
| 模式 2 | 1 | 0 | 高电平 | 下降沿(第一边沿) |
| 模式 3 | 1 | 1 | 高电平 | 上升沿(第二边沿) |
CPOL = 0:空闲时 SCLK 为低电平 CPOL = 1:空闲时 SCLK 为高电平
CPHA = 0:在 SCLK 第一个边沿采样数据 CPHA = 1:在 SCLK 第二个边沿采样数据
最常用的是模式 0 和模式 3。W25Q64 Flash 芯片支持模式 0 和模式 3。
上图:SPI 模式 0 时序(空闲时 SCK 低电平,上升沿采样)(来源:STM32入门教程 PPT 第150页)
数据位传输顺序有两种:
W25Q64 使用 MSB First。
上图:STM32 SPI 模块框图(来源:STM32入门教程 PPT 第162页)
上图:STM32 SPI 内部结构(波特率发生器、移位寄存器、数据寄存器)(来源:STM32入门教程 PPT 第163页)
SPI1 挂在 APB2 总线上(最高 72MHz,默认 8MHz):
波特率 = PCLK2 / 分频系数
默认 PCLK2 = 8MHz:
CubeMX 中 NSS 设置为 Disable 后,需要手动用 GPIO 控制:
#define NSS_PIN GPIO_Pin_4
#define NSS_PORT GPIOA
#define NSS_LOW() GPIO_ResetBits(NSS_PORT, NSS_PIN)
#define NSS_HIGH() GPIO_SetBits(NSS_PORT, NSS_PIN)
通信前拉低 NSS,通信后拉高:
NSS_LOW(); // 选中从机
// ... 传输数据 ...
NSS_HIGH(); // 释放从机
void SPI_Init(void) {
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 初始化 MOSI (PA7)、SCK (PA5) 为复用推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 初始化 MISO (PA6) 为上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4. 初始化 NSS (PA4) 为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
NSS_HIGH(); // 初始为高电平(未选中)
// 5. SPI 参数配置
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 模式 3
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件 NSS
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 1MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
// 6. 使能 SPI
SPI_Cmd(SPI1, ENABLE);
}
HAL 库使用 SPI_HandleTypeDef 句柄 + HAL_SPI_Init() 完成配置,底层 GPIO/RCC/NVIC 初始化在 HAL_SPI_MspInit() 回调中实现。
// === SPI_HandleTypeDef 定义(全局)===
SPI_HandleTypeDef hspi1;
// === SPI 参数配置 ===
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式
hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8 位数据
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL = 1(模式 3)
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1(模式 3)
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件 NSS 管理
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 1MHz(APB2=8MHz/8)
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // MSB 先行
hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 标准 SPI(非 TI 模式)
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 0;
HAL_SPI_Init(&hspi1);
}
// === MSP 初始化回调:GPIO + RCC + NVIC ===
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hspi->Instance == SPI1)
{
__HAL_RCC_SPI1_CLK_ENABLE(); // 开启 SPI1 时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 GPIOA 时钟
// PA5 (SCK)、PA7 (MOSI) → 复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA6 (MISO) → 浮空输入
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA4 (NSS/CS) → 推挽输出,软件手动控制
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 默认高电平
}
}
NSS 软件管理(SPI_NSS_SOFT)说明:
SPI_NSS_SOFT 告诉硬件:NSS 信号由软件通过寄存器(SSI 位)控制,而非硬件 NSS 引脚。此时 NSS 引脚(如 PA4)释放为普通 GPIO,由用户手动拉高/拉低来选择从机。
SPI_NSS_SOFT 后,CubeMX 中对应 Hardware NSS Signal 设为 DisableHAL_GPIO_WritePin(GPIOx, NSS_PIN, GPIO_PIN_RESET)HAL_GPIO_WritePin(GPIOx, NSS_PIN, GPIO_PIN_SET)SPI_NSS_Soft 行为完全一致标准库 vs HAL 库 SPI 配置对照:
| 参数 | 标准库 | HAL 库 |
|---|---|---|
| 模式 | SPI_Mode_Master |
SPI_MODE_MASTER |
| 方向 | SPI_Direction_2Lines_FullDuplex |
SPI_DIRECTION_2LINES |
| 数据宽度 | SPI_DataSize_8b |
SPI_DATASIZE_8BIT |
| 时钟极性 | SPI_CPOL_High |
SPI_POLARITY_HIGH |
| 时钟相位 | SPI_CPHA_2Edge |
SPI_PHASE_2EDGE |
| NSS 管理 | SPI_NSS_Soft |
SPI_NSS_SOFT |
| 波特率分频 | SPI_BaudRatePrescaler_8 |
SPI_BAUDRATEPRESCALER_8 |
| 先行位 | SPI_FirstBit_MSB |
SPI_FIRSTBIT_MSB |
| 初始化函数 | SPI_Init(SPIx, &init) |
HAL_SPI_Init(&hspi) |
| 使能 | SPI_Cmd(SPIx, ENABLE) |
__HAL_SPI_ENABLE(&hspi)(Init 内自动调用) |
// 只发送(不关心接收)
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
// 只接收(发送虚拟字节 0xFF)
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
// 同时收发
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
uint8_t *pTxData,
uint8_t *pRxData,
uint16_t Size,
uint32_t Timeout);
hspi:SPI 句柄指针,如 &hspi1Timeout:超时时间(ms),HAL_MAX_DELAY 表示无限等待// 发送一个字节(同时接收一个字节)
uint8_t SPI_SwapByte(uint8_t Byte) {
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待 TX 缓冲区空
SPI_I2S_SendData(SPI1, Byte);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 等待 RX 缓冲区非空
return SPI_I2S_ReceiveData(SPI1);
}
// 向从机 1 发送 2 个字节
uint8_t TxData[] = {0x5A, 0x33};
NSS1_LOW(); // 选中从机 1
HAL_SPI_Transmit(&hspi1, TxData, 2, HAL_MAX_DELAY); // 发送
NSS1_HIGH(); // 释放从机 1
// 从从机 1 接收 2 个字节
uint8_t RxData[2] = {0xFF, 0xFF}; // 必须赋初值 0xFF
NSS1_LOW();
HAL_SPI_Receive(&hspi1, RxData, 2, HAL_MAX_DELAY);
NSS1_HIGH();
接收缓冲区赋初值 0xFF 非常重要:因为 SPI 接收时主机需要发送数据来产生时钟,如果不赋初值会发送随机值。
W25Q64 是一款 SPI 接口的 Flash 存储芯片,容量 64 Mbit(8 MByte),地址范围 0x000000 ~ 0x7FFFFF。
存储结构:
64 Mbit ─→ 128 块 (Block),每块 64 KB
每个块 ─→ 16 个扇区 (Sector),每个扇区 4 KB
每个扇区 ─→ 16 页 (Page),每页 256 字节
关键限制:
| 指令 | 码值 | 说明 |
|---|---|---|
| WRITE_ENABLE | 0x06 | 写使能(每次擦除/写前必须) |
| SECTOR_ERASE_4KB | 0x20 | 扇区擦除(后跟 24 位地址) |
| PAGE_PROGRAM | 0x02 | 页编程(后跟 24 位地址 + 数据) |
| READ_DATA | 0x03 | 读取数据(后跟 24 位地址) |
| READ_STATUS_REGISTER_1 | 0x05 | 读状态寄存器 1(查询 BUSY 位) |
| JEDEC_ID | 0x9F | 读取芯片 ID(返回 3 字节:制造商 ID + 设备 ID) |
每次擦除和编程之前,必须先发送写使能指令:
void W25Q64_WriteEnable(void) {
NSS_LOW();
HAL_SPI_Transmit(&hspi1, (uint8_t*)"\x06", 1, HAL_MAX_DELAY);
NSS_HIGH();
}
擦除后扇区所有位变为 1(0xFF):
void W25Q64_SectorErase(uint32_t Address) {
W25Q64_WriteEnable(); // 先写使能
uint8_t Cmd[4] = {0x20, Address >> 16, Address >> 8, Address};
NSS_LOW();
HAL_SPI_Transmit(&hspi1, Cmd, 4, HAL_MAX_DELAY);
NSS_HIGH();
W25Q64_WaitBusy(); // 等待擦除完成
}
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) {
W25Q64_WriteEnable();
uint8_t Cmd[4] = {0x02, Address >> 16, Address >> 8, Address};
NSS_LOW();
HAL_SPI_Transmit(&hspi1, Cmd, 4, HAL_MAX_DELAY); // 发送指令 + 地址
HAL_SPI_Transmit(&hspi1, DataArray, Count, HAL_MAX_DELAY); // 发送数据
NSS_HIGH();
W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) {
uint8_t Cmd[4] = {0x03, Address >> 16, Address >> 8, Address};
NSS_LOW();
HAL_SPI_Transmit(&hspi1, Cmd, 4, HAL_MAX_DELAY); // 发送指令 + 地址
HAL_SPI_Receive(&hspi1, DataArray, Count, HAL_MAX_DELAY); // 接收数据
NSS_HIGH();
}
Flash 在执行擦除或编程时需要时间,通过查询状态寄存器的 BUSY 位(bit 0)判断:
void W25Q64_WaitBusy(void) {
uint32_t Timeout = 100000;
uint8_t Status;
NSS_LOW();
HAL_SPI_Transmit(&hspi1, (uint8_t*)"\x05", 1, HAL_MAX_DELAY); // 读状态寄存器命令
do {
HAL_SPI_Receive(&hspi1, &Status, 1, HAL_MAX_DELAY);
if (--Timeout == 0) break; // 超时保护
} while (Status & 0x01);
NSS_HIGH();
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) {
NSS_LOW();
uint8_t Cmd = 0x9F;
HAL_SPI_Transmit(&hspi1, &Cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, MID, 1, HAL_MAX_DELAY); // 制造商 ID
uint8_t DID_H, DID_L;
HAL_SPI_Receive(&hspi1, &DID_H, 1, HAL_MAX_DELAY); // 设备 ID 高字节
HAL_SPI_Receive(&hspi1, &DID_L, 1, HAL_MAX_DELAY); // 设备 ID 低字节
*DID = (DID_H << 8) | DID_L;
NSS_HIGH();
}
以下代码使用 HAL_SPI_TransmitReceive 同时收发,适用于 W25Q64 的读写操作。与分离调用 HAL_SPI_Transmit + HAL_SPI_Receive 不同,TransmitReceive 在发送指令字节的同时接收从机返回的数据,逻辑更紧凑。
#define NSS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define NSS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
// 单字节交换(发送一个字节,接收一个字节)
uint8_t W25Q64_SwapByte(uint8_t byte)
{
uint8_t rx;
HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, HAL_MAX_DELAY);
return rx;
}
// 写使能
void W25Q64_WriteEnable(void)
{
uint8_t cmd = 0x06;
NSS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
NSS_HIGH();
}
// 等待 BUSY 释放
void W25Q64_WaitBusy(void)
{
uint8_t cmd = 0x05, status;
NSS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
do {
HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY);
} while (status & 0x01);
NSS_HIGH();
}
// 扇区擦除(4KB)
void W25Q64_SectorErase(uint32_t addr)
{
uint8_t cmd[4] = {0x20, addr >> 16, addr >> 8, addr};
W25Q64_WriteEnable();
NSS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
NSS_HIGH();
W25Q64_WaitBusy();
}
// 页编程(最多 256 字节)
void W25Q64_PageProgram(uint32_t addr, const uint8_t *data, uint16_t len)
{
uint8_t cmd[4] = {0x02, addr >> 16, addr >> 8, addr};
W25Q64_WriteEnable();
NSS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, (uint8_t *)data, len, HAL_MAX_DELAY);
NSS_HIGH();
W25Q64_WaitBusy();
}
// 读取数据(使用 TransmitReceive 实现)
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint8_t cmd[4] = {0x03, addr >> 16, addr >> 8, addr};
NSS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
NSS_HIGH();
}
// 读取芯片 ID
void W25Q64_ReadID(uint8_t *mid, uint16_t *did)
{
uint8_t cmd = 0x9F, buf[3];
NSS_LOW();
HAL_SPI_TransmitReceive(&hspi1, &cmd, buf, 1, HAL_MAX_DELAY); // 发指令,收制造商 ID
HAL_SPI_Receive(&hspi1, &buf[1], 2, HAL_MAX_DELAY); // 收设备 ID 高 + 低
NSS_HIGH();
*mid = buf[0];
*did = ((uint16_t)buf[1] << 8) | buf[2];
}
对比单独调用 Transmit + Receive 与一次性 TransmitReceive:前者分两步执行(底层仍是全双工),后者将收发捆绑在一个 API 调用中,代码更紧凑、中断/DMA 模式下效率更高。W25Q64 的 ReadData 和 ReadID 场景通常分步调用即可,只有单字节交换场景推荐 TransmitReceive。
uint8_t LED_State = 0; // 0=熄灭, 1=点亮
// 保存 LED 状态到 Flash
void SaveLEDState(uint8_t State) {
W25Q64_WriteEnable();
W25Q64_SectorErase(0x000000); // 擦除 0 号扇区
Delay_ms(100);
W25Q64_WriteEnable();
W25Q64_PageProgram(0x000000, &State, 1); // 写入 1 字节
Delay_ms(10);
}
// 从 Flash 加载 LED 状态
uint8_t LoadLEDState(void) {
uint8_t State;
W25Q64_ReadData(0x000000, &State, 1);
return State;
}
int main(void) {
// 初始化
OLED_Init();
W25Q64_Init();
// 显示 ID
uint8_t MID;
uint16_t DID;
W25Q64_ReadID(&MID, &DID);
OLED_ShowHexNum(1, 1, MID, 2);
OLED_ShowHexNum(1, 8, DID, 4);
// 上电恢复 LED 状态
LED_State = LoadLEDState();
if (LED_State == 1) LED_ON();
else LED_OFF();
// 按钮控制
uint8_t Previous = 1, Current = 1;
while (1) {
Previous = Current;
Current = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
if (Previous != Current) { // 检测到变化
Delay_ms(10); // 消抖
if (Current == 1) { // 按钮抬起
LED_State = !LED_State; // 切换 LED
if (LED_State) LED_ON();
else LED_OFF();
SaveLEDState(LED_State); // 保存到 Flash
}
}
}
}
PA4 (NSS)、PA5 (SCK)、PA7 (MOSI) 为推挽输出,PA6 (MISO) 为浮空输入(或上拉输入):
// NSS, SCK, MOSI: 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
// MISO: 浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
#define SPI_SCK_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_5)
#define SPI_SCK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5)
#define SPI_MOSI_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_7)
#define SPI_MOSI_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_7)
#define SPI_MISO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)
#define SPI_NSS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define SPI_NSS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4)
void MySPI_Start(void) {
SPI_NSS_LOW(); // 选中从机
}
void MySPI_Stop(void) {
SPI_NSS_HIGH(); // 释放从机
}
uint8_t MySPI_SwapByte(uint8_t Byte) {
for (uint8_t i = 0; i < 8; i++) {
// MOSI 输出:MSB first
if (Byte & 0x80) SPI_MOSI_HIGH();
else SPI_MOSI_LOW();
Byte <<= 1;
// SCK 上升沿:发送数据被从机采样
SPI_SCK_HIGH();
Delay_us(1);
// MISO 采样:在 SCK 上升沿读取
if (SPI_MISO_READ()) Byte |= 0x01;
// SCK 下降沿
SPI_SCK_LOW();
Delay_us(1);
}
return Byte;
}
void W25Q64_Init(void) {
MySPI_Init(); // 初始化 GPIO
}
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) {
MySPI_Start();
MySPI_SwapByte(0x9F); // JEDEC ID 指令
*MID = MySPI_SwapByte(0xFF); // 读制造商 ID
*DID = MySPI_SwapByte(0xFF) << 8; // 读设备 ID 高字节
*DID |= MySPI_SwapByte(0xFF); // 读设备 ID 低字节
MySPI_Stop();
}
void W25Q64_WriteEnable(void) {
MySPI_Start();
MySPI_SwapByte(0x06);
MySPI_Stop();
}
void W25Q64_WaitBusy(void) {
uint32_t Timeout = 100000;
MySPI_Start();
MySPI_SwapByte(0x05); // 读状态寄存器
while ((MySPI_SwapByte(0xFF) & 0x01) == 0x01) {
if (--Timeout == 0) break;
}
MySPI_Stop();
}
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) {
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(0x02); // 页编程指令
MySPI_SwapByte(Address >> 16); // 地址 23~16
MySPI_SwapByte(Address >> 8); // 地址 15~8
MySPI_SwapByte(Address); // 地址 7~0
for (uint16_t i = 0; i < Count; i++)
MySPI_SwapByte(DataArray[i]); // 写入数据
MySPI_Stop();
W25Q64_WaitBusy();
}
void W25Q64_SectorErase(uint32_t Address) {
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(0x20); // 扇区擦除指令
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) {
MySPI_Start();
MySPI_SwapByte(0x03); // 读数据指令
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (uint32_t i = 0; i < Count; i++)
DataArray[i] = MySPI_SwapByte(0xFF); // 发送虚拟字节接收数据
MySPI_Stop();
}
int main(void) {
OLED_Init();
W25Q64_Init();
uint8_t MID;
uint16_t DID;
W25Q64_ReadID(&MID, &DID);
OLED_ShowHexNum(1, 1, MID, 2);
OLED_ShowHexNum(1, 8, DID, 4);
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];
W25Q64_SectorErase(0x000000);
W25Q64_PageProgram(0x000000, ArrayWrite, 4);
W25Q64_ReadData(0x000000, ArrayRead, 4);
// 显示写入和读出的数据(应一致)
OLED_ShowHexNum(2, 1, ArrayWrite[0], 2);
OLED_ShowHexNum(2, 4, ArrayWrite[1], 2);
OLED_ShowHexNum(3, 1, ArrayRead[0], 2);
OLED_ShowHexNum(3, 4, ArrayRead[1], 2);
while (1);
}
| 对比 | 硬件 SPI | 软件 SPI |
|---|---|---|
| CPU 占用 | 低 | 高 |
| 引脚 | 需固定复用引脚 | 任意 GPIO |
| 速度 | 可达 18MHz(APB2/2) | 受延时精度限制 |
| 模式切换 | 配置寄存器即可 | 需重写时序 |
| 编程复杂度 | 中等 | 简单 |
| 适用场景 | 高速、大数据量 | 低速、引脚受限 |
发送数据前忘记拉低 NSS,从机未被选中,不响应任何指令。务必在传输开始前 NSS_LOW(),结束后 NSS_HIGH()。
接收前接收缓冲区元素应赋初值 0xFF。否则在 SPI 收发过程中,硬件会发送缓冲区中的随机值。
主机和从机的 SPI 模式(CPOL/CPHA)必须一致。W25Q64 支持模式 0 和模式 3,两者时序对称但空闲电平不同。
对 Flash 进行擦除或编程前,必须先发送 0x06(写使能)。如果忘记,指令会被忽略。
W25Q64 页编程时写入地址不能跨 256 字节边界。如果需要写入超过 256 字节,必须分页写入。
扇区擦除(4KB)通常需要几十毫秒,在等待过程中可执行其他任务(如使用 DMA 或在主循环中轮询)。
11-1 软件SPI读写W25Q64\Hardware\MySPI.c — 软件 SPI GPIO 模拟实现11-1 软件SPI读写W25Q64\Hardware\W25Q64.c — W25Q64 驱动(标准外设库)11-1 软件SPI读写W25Q64\User\main.c — 主程序示例21-[STM32 HAL库][SPI]外部flash实验 — CubeMX + HAL 库配置22-[STM32 HAL库][SPI]flash数据存取 — HAL 库 API 使用本笔记综合整理自三套 STM32 教程:
- [STM32 HAL库] SPI 总线结构 / 5 个参数 / 外部 flash 实验
- 铁头山羊 SPI 系列(5.1~5.8)
- 江协科技 SPI 系列(11-1~11-5)