06-SPI通信详解.md 26 KB

STM32 SPI 通信

1. SPI 总线协议基础

四线制

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)来产生时钟脉冲。


2. SPI 时序模式

CPOL 和 CPHA

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页)

MSB First 与 LSB First

数据位传输顺序有两种:

  • MSB First:先传最高位(D7→D0),最常用
  • LSB First:先传最低位(D0→D7)

W25Q64 使用 MSB First。


3. STM32 SPI 外设配置

STM32 SPI 模块框图 上图:STM32 SPI 模块框图(来源:STM32入门教程 PPT 第162页)

STM32 SPI 内部结构 上图:STM32 SPI 内部结构(波特率发生器、移位寄存器、数据寄存器)(来源:STM32入门教程 PPT 第163页)

CubeMX 配置

  1. 选择 SPI1,Mode 设为 Full-Duplex Master(全双工主机模式)
  2. Hardware NSS Signal 设为 Disable(手动控制 NSS)
  3. 参数设置:
    • Frame Format: Motorola(标准 SPI 格式)
    • Data Size: 8 Bit
    • First Bit: MSB First
    • Prescaler: 根据波特率选择(如 8 分频得 1MHz)
    • Clock Polarity (CPOL): High(模式 3 时)
    • Clock Phase (CPHA): 2 Edge(模式 3 时)
  4. 引脚分配:
    • PA5 → SCK
    • PA6 → MISO
    • PA7 → MOSI
    • PA4 → NSS(手动 GPIO 控制,配置为推挽输出)

波特率计算

SPI1 挂在 APB2 总线上(最高 72MHz,默认 8MHz):

波特率 = PCLK2 / 分频系数

默认 PCLK2 = 8MHz:

  • 2 分频 → 4MHz
  • 4 分频 → 2MHz
  • 8 分频 → 1MHz(推荐,稳定)
  • 16 分频 → 500kHz

软件 NSS 控制

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 库初始化示例

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 设为 Disable
  • 通信前 HAL_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 内自动调用)

4. 编程接口

HAL 库 API

// 只发送(不关心接收)
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 句柄指针,如 &hspi1
  • Timeout:超时时间(ms),HAL_MAX_DELAY 表示无限等待

标准外设库 API

// 发送一个字节(同时接收一个字节)
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);
}

NSS 手动控制示例

// 向从机 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 接收时主机需要发送数据来产生时钟,如果不赋初值会发送随机值。


5. W25Q64 Flash 实验

W25Q64 简介

W25Q64 是一款 SPI 接口的 Flash 存储芯片,容量 64 Mbit(8 MByte),地址范围 0x000000 ~ 0x7FFFFF。

存储结构:

64 Mbit ─→ 128 块 (Block),每块 64 KB
  每个块 ─→ 16 个扇区 (Sector),每个扇区 4 KB
    每个扇区 ─→ 16 页 (Page),每页 256 字节

关键限制

  • Flash 写入前必须先擦除
  • 擦除的最小单位:扇区(4 KB)
  • 写入的最小单位:(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();
}

读取芯片 ID

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();
}

W25Q64 完整读写示例(使用 HAL_SPI_TransmitReceive)

以下代码使用 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 的 ReadDataReadID 场景通常分步调用即可,只有单字节交换场景推荐 TransmitReceive

完整实验:LED 状态保存

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
            }
        }
    }
}

6. 软件 SPI(GPIO 模拟)

引脚配置

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;

时序模拟(模式 0)

#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;
}

使用软件 SPI 操作 W25Q64

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();
}

软件 SPI 主程序示例

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);
}

7. 硬件 SPI vs 软件 SPI

对比 硬件 SPI 软件 SPI
CPU 占用
引脚 需固定复用引脚 任意 GPIO
速度 可达 18MHz(APB2/2) 受延时精度限制
模式切换 配置寄存器即可 需重写时序
编程复杂度 中等 简单
适用场景 高速、大数据量 低速、引脚受限

8. 常见问题

NSS 未拉低

发送数据前忘记拉低 NSS,从机未被选中,不响应任何指令。务必在传输开始前 NSS_LOW(),结束后 NSS_HIGH()

接收缓冲区未初始化

接收前接收缓冲区元素应赋初值 0xFF。否则在 SPI 收发过程中,硬件会发送缓冲区中的随机值。

模式不匹配

主机和从机的 SPI 模式(CPOL/CPHA)必须一致。W25Q64 支持模式 0 和模式 3,两者时序对称但空闲电平不同。

忘记写使能

对 Flash 进行擦除或编程前,必须先发送 0x06(写使能)。如果忘记,指令会被忽略。

跨页写入

W25Q64 页编程时写入地址不能跨 256 字节边界。如果需要写入超过 256 字节,必须分页写入。

擦除时间长

扇区擦除(4KB)通常需要几十毫秒,在等待过程中可执行其他任务(如使用 DMA 或在主循环中轮询)。


9. 参考代码索引

  • 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 使用
  • 铁头山羊 SPI 系列(5.5~5.8)— 硬件 SPI 模块详解

本笔记综合整理自三套 STM32 教程:

  1. [STM32 HAL库] SPI 总线结构 / 5 个参数 / 外部 flash 实验
  2. 铁头山羊 SPI 系列(5.1~5.8)
  3. 江协科技 SPI 系列(11-1~11-5)