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的核心结构:环形移位寄存器
SPI发送和接收是同时进行的,本质上是一个环形移位寄存器:
主机 从机
┌────────────┐ ┌────────────┐
│ 移位寄存器 │──MOSI──→│ 移位寄存器 │
│ │←──MISO──│ │
└────────────┘ └────────────┘
│ ↑ │ ↑
│ │ SCK(时钟) │ │
└──┴────────────────────┴──┘
工作机制:
所以SPI的收发函数只有一个:SPI_SwapByte(byte) — 发送一个字节的同时接收一个字节
CPOL/CPHA四种模式详解:
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低 | 上升沿采样,下降沿移位 |
| 模式1 | 0 | 1 | 低 | 下降沿采样,上升沿移位 |
| 模式2 | 1 | 0 | 高 | 下降沿采样,上升沿移位 |
| 模式3 | 1 | 1 | 高 | 上升沿采样,下降沿移位 |
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) // →BSRR写入: 对应位写1→PA5(SCK)输出高电平
#define SPI_SCK_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_5) // →BRR写入: 对应位写1→PA5(SCK)输出低电平
#define SPI_MOSI_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_7) // →BSRR写入: 对应位写1→PA7(MOSI)输出高电平
#define SPI_MOSI_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_7) // →BRR写入: 对应位写1→PA7(MOSI)输出低电平
#define SPI_MISO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) // →IDR读取: 读取IDR的bit6→返回PA6(MISO)当前电平(0或1)
#define SPI_NSS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) // →BRR写入: 对应位写1→PA4(NSS)输出低电平(选中从机)
#define SPI_NSS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) // →BSRR写入: 对应位写1→PA4(NSS)输出高电平(释放从机)
void MySPI_Start(void) {
SPI_NSS_LOW(); // →BRR写入: PA4清零→选中从机(NSS低电平有效)
}
void MySPI_Stop(void) {
SPI_NSS_HIGH(); // →BSRR写入: PA4置1→释放从机(NSS恢复高电平)
}
uint8_t MySPI_SwapByte(uint8_t Byte) {
for (uint8_t i = 0; i < 8; i++) {
// MOSI 输出:MSB first
if (Byte & 0x80) SPI_MOSI_HIGH(); // & 按位与: 测试最高位(bit7)是否为1;若为1→MOSI输出高电平
else SPI_MOSI_LOW(); // 否则→MOSI输出低电平
Byte <<= 1; // <<= 左移1位: 将次高位移到最高位,准备下一轮测试
// SCK 上升沿:发送数据被从机采样
SPI_SCK_HIGH(); // →BSRR写入: PA5置1→SCK上升沿,从机在此时采样MOSI
Delay_us(1);
// MISO 采样:在 SCK 上升沿读取
if (SPI_MISO_READ()) Byte |= 0x01; // →IDR读取&|=: 读MISO电平,若为1则|=按位或将Byte最低位置1
// SCK 下降沿
SPI_SCK_LOW(); // →BRR写入: PA5清零→SCK下降沿,准备下一位
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) { // &按位与: 测试bit0(BUSY位); ==比较: 若=1→Flash忙,继续等待
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);
}
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13) // &= ~ 按位与+取反: ODR13清零→PC13(CS)输出低电平,其他位不变
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13) // |= 按位或: ODR13置1→PC13(CS)输出高电平,其他位不变
#define SCK_HIGH (GPIOA->ODR |= GPIO_ODR_ODR5) // |= 按位或: ODR5置1→PA5(SCK)输出高电平,其他位不变
#define SCK_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR5) // &= ~ 按位与+取反: ODR5清零→PA5(SCK)输出低电平,其他位不变
#define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7) // |= 按位或: ODR7置1→PA7(MOSI)输出高电平,其他位不变
#define MOSI_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR7) // &= ~ 按位与+取反: ODR7清零→PA7(MOSI)输出低电平,其他位不变
#define MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6) // & 按位与: 读取IDR提取bit6(其余位被屏蔽)→返回PA6(MISO)当前电平(0或1)
// ============================================================
// 【关键函数】软件SPI初始化
// 配置CS(PC13)、SCK(PA5)、MOSI(PA7)为推挽输出
// 配置MISO(PA6)为上拉输入
// ============================================================
void SPI_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN; // |= 按位或: IOPAEN+IOPCEN置1→开启GPIOA/C时钟
GPIOC->CRH |= GPIO_CRH_MODE13; GPIOC->CRH &= ~GPIO_CRH_CNF13; // |=: MODE13置1→50MHz; &=~: CNF13清零→推挽输出
GPIOA->CRL |= GPIO_CRL_MODE5; GPIOA->CRL &= ~GPIO_CRL_CNF5; // PA5(SCK): MODE5置1→50MHz; CNF5清零→推挽输出
GPIOA->CRL |= GPIO_CRL_MODE7; GPIOA->CRL &= ~GPIO_CRL_CNF7; // PA7(MOSI): MODE7置1→50MHz; CNF7清零→推挽输出
GPIOA->CRL &= ~GPIO_CRL_MODE6; // &= ~: MODE6清零→PA6(MISO)配置为输入
GPIOA->CRL &= ~GPIO_CRL_CNF6_1; GPIOA->CRL |= GPIO_CRL_CNF6_0; // &=~: CNF6_1清零; |=: CNF6_0置1→CNF[1:0]=10(上拉/下拉输入)
SCK_LOW; CS_HIGH; // 宏展开→ODR&=~ODR5(SCK低); ODR|=ODR13(CS高): 空闲态
}
// 核心函数:交换一个字节(同时收发)
// 模式0(CPOL=0, CPHA=0):上升沿采样,下降沿移位
// 每次循环:MOSI输出当前位 → SCK上升沿(从机采样)→ MISO读取 → SCK下降沿
uint8_t SPI_SwapByte(uint8_t byte) {
uint8_t rByte = 0;
for (uint8_t i = 0; i < 8; i++) {
if (byte & 0x80) MOSI_HIGH; else MOSI_LOW; // &按位与: 测试最高位; MOSI_HIGH=ODR|=ODR7(置1), MOSI_LOW=ODR&=~ODR7(清零)
byte <<= 1; // <<= 左移1位: 将次高位移到最高位,准备下一轮测试
SCK_HIGH; SPI_DELAY; // ODR|=ODR5: SCK上升沿→从机采样MOSI
rByte <<= 1; // <<= 左移1位: 腾出最低位,准备填入新读取的1位
if (MISO_READ) rByte |= 0x01; // &按位与(IDR)+|=按位或: 读MISO电平,若为1则将rByte最低位置1
SCK_LOW; SPI_DELAY; // ODR&=~ODR5: SCK下降沿→准备下一位
}
return rByte;
}
// ============================================================
// 【关键函数】硬件SPI初始化
// 使用SPI1(PA5=SCK, PA6=MISO, PA7=MOSI)
// 配置为:主机模式、模式0(CPOL=0, CPHA=0)、8位、MSB first
// ============================================================
void SPI_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // |= 按位或: SPI1EN置1→开启SPI1时钟(APB2总线)
// PA5(SCK), PA7(MOSI): 复用推挽输出(CNF=10)
GPIOA->CRL |= GPIO_CRL_MODE5 | GPIO_CRL_CNF5_1; GPIOA->CRL &= ~GPIO_CRL_CNF5_0; // |=: MODE5/CNF5_1置1; &=~: CNF5_0清零→CNF[1:0]=10(复用推挽)
GPIOA->CRL |= GPIO_CRL_MODE7 | GPIO_CRL_CNF7_1; GPIOA->CRL &= ~GPIO_CRL_CNF7_0; // PA7同理: MODE7+CNF7_1置1, CNF7_0清零→复用推挽
// PA6(MISO): 浮空输入(硬件自动配置,此处用默认值)
// 配置SPI1控制寄存器
SPI1->CR1 |= SPI_CR1_MSTR; // |= 按位或: MSTR位置1→主机模式,其他控制位不变
SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // |= 按位或: SSM+SSI位置1→软件NSS管理(SSM=1使能软件从机, SSI=1设NSS高)
SPI1->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA); // &= ~ 按位与+取反: CPOL+CPHA清零→CPOL=0, CPHA=0(模式0)
SPI1->CR1 &= ~SPI_CR1_BR; SPI1->CR1 |= SPI_CR1_BR_0; // &=~: BR[2:0]全清零; |=: BR0置1→BR[2:0]=001(分频系数=4)
SPI1->CR1 &= ~(SPI_CR1_DFF | SPI_CR1_LSBFIRST); // &= ~: DFF+LSBFIRST清零→8位数据, MSB first
SPI1->CR1 |= SPI_CR1_SPE; // |= 按位或: SPE位(bit6)置1→使能SPI外设(总开关)
}
// 核心函数:交换一个字节(同时收发)
// 硬件自动完成移位过程,CPU只需读写DR寄存器
uint8_t SPI_SwapByte(uint8_t byte) {
while ((SPI1->SR & SPI_SR_TXE) == 0) {} // &按位与: 读SR提取TXE位(bit1),==0判断→发送缓冲区未空,继续等待
SPI1->DR = byte; // 写入DR: 将待发字节写入数据寄存器,硬件自动移出并接收
while ((SPI1->SR & SPI_SR_RXNE) == 0) {} // &按位与: 读SR提取RXNE位(bit0),==0判断→接收缓冲区为空,继续等待
return (uint8_t)(SPI1->DR & 0xff); // &按位与: 读取DR并屏蔽高24位→返回低8位接收数据
}
| 对比 | 硬件 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)
软件SPI(寄存器):
SPI_SwapByte(b): MOSI移出→SCK高→MISO移入→SCK低 // 一位一位交换硬件SPI(寄存器):
SPI_SwapByte(b): 等待TXE→写入DR→等待RXNE→读取DR // 硬件自动交换标准库:
SPI_I2S_SendData() / SPI_I2S_ReceiveData()HAL库:
HAL_SPI_TransmitReceive(&hspi, tx, rx, len, timeout) // 同时收发