STM32F103 学习笔记-25-SPI-读写串行FLASH(第1节)-SPI协议介绍

一、SPI 协议原理

1.1 协议概述

SPI(Serial Peripheral Interface,串行外设接口)是摩托罗拉公司提出的 高速全双工同步通信总线,广泛应用于 ADC、LCD、Flash 等高速外设与 MCU 之间的数据传输。

  • 对比 I2C:SPI 传输速率更高(STM32F1 最高支持 36 MHz),无设备地址概念,通过独立片选线选中从机;I2C 是半双工,最高速率 400 kHz,通过地址寻址。

  • 存储特性:SPI Flash 是采用 SPI 协议的非易失性存储器,掉电后数据不丢失,常见容量从几兆字节到几十兆字节,远大于 EEPROM(通常几百字节到几千字节)。

1.2 物理层(4 线制)

SPI 总线由 4 根核心信号线组成,支持 一主多从 ​ 架构,所有从机共享 SCK、MOSI、MISO 三根线,每个从机拥有独立的 NSS 片选线。

信号线 英文全称 功能
NSS / CS Slave Select 从机选择(片选)线,主机通过拉低对应从机的 NSS 线选中该设备
SCK Serial Clock 时钟信号线,由主机产生,用于同步数据传输
MOSI Master Output, Slave Input 主设备输出 / 从设备输入,数据从主机流向从机
MISO Master Input, Slave Output 主设备输入 / 从设备输出,数据从从机流向主机

生活类比

SPI 通信就像老师和学生的课堂问答:

  • NSS:老师点名,只有被点到的学生(从机)才能参与对话

  • SCK:上课铃,规定什么时候说话、什么时候听讲

  • MOSI:老师讲课(主机发数据)

  • MISO:学生回答问题(从机发数据)

全双工意味着老师讲课的同时,学生也可以举手提问(数据双向同时传输)。

1.3 协议层

1.3.1 起始与停止信号
  • 起始信号 :NSS 线由 高电平变为低电平,表示通信开始,对应从机被选中。

  • 停止信号 :NSS 线由 低电平变为高电平,表示本次通信结束,从机释放总线。

1.3.2 数据传输规则
  1. 同步传输:数据在 SCK 时钟的同步下传输,每个时钟周期传输 1 位数据,MOSI 和 MISO 同时传输(全双工)。

  2. 字节顺序 :SPI 协议本身未强制规定 MSB(高位)先行还是 LSB(低位)先行,但 绝大多数 SPI 设备采用 MSB 先行,STM32 可通过寄存器配置字节顺序。

  3. 数据长度:支持 8 位或 16 位数据帧,通信过程中只要 NSS 保持低电平,SCK 持续输出,就可以连续传输任意长度的数据。

1.3.3 时钟极性 (CPOL) 与时钟相位 (CPHA)

这两个参数决定了数据的采样时刻和时钟空闲状态,主从设备必须配置为相同的 CPOL 和 CPHA 才能正常通信

  • CPOL(时钟极性):定义 SPI 空闲时 SCK 的电平状态

    • CPOL = 0:空闲时 SCK 为低电平

    • CPOL = 1:空闲时 SCK 为高电平

  • CPHA(时钟相位):定义数据的采样时刻

    • CPHA = 0:在 SCK 的 奇数边沿​ 采样数据(第一个边沿)

    • CPHA = 1:在 SCK 的 偶数边沿​ 采样数据(第二个边沿)

1.3.4 SPI 四种通信模式

CPOL 和 CPHA 的组合产生了 4 种 SPI 模式,其中 模式 0 和模式 3​ 是实际应用中最常用的两种。

SPI 模式 CPOL CPHA 空闲时 SCK 电平 采样时刻
0 0 0 低电平 上升沿
1 0 1 低电平 下降沿
2 1 0 高电平 下降沿
3 1 1 高电平 上升沿

二、STM32F103 SPI 外设特性与架构

2.1 外设资源与总线挂载

STM32F103 系列包含 3 个 SPI 外设,挂载在不同的 APB 总线上,时钟频率不同:

  • SPI1 :挂载在 APB2 总线,最高时钟 72 MHz,SPI 波特率最高为 72 MHz / 2 = 36 MHz

  • SPI2 :挂载在 APB1 总线,最高时钟 36 MHz,SPI 波特率最高为 36 MHz / 2 = 18 MHz

  • SPI3 :仅大容量产品支持,挂载在 APB1 总线,最高波特率 18 MHz;注意:SPI3 的 NSS 和 SCK 引脚与 JTAG 引脚共享,使用时需关闭 JTAG 并切换至 SWD 模式。

2.2 核心特性

根据 STM32F10x 参考手册,SPI 外设支持以下核心功能:

  1. 通信模式:3 线全双工、双线单工(带或不带双向数据线)

  2. 数据帧:8 位或 16 位数据帧,可配置 MSB / LSB 先行

  3. 主从模式:支持主模式和从模式,支持多主配置

  4. 波特率:8 级预分频(2 / 4 / 8 / 16 / 32 / 64 / 128 / 256),主模式最高为 PCLK / 2

  5. NSS 管理:支持硬件 NSS 和软件 NSS 两种模式

  6. 可靠性:硬件 CRC 校验,支持 DMA 高速传输

  7. 扩展功能:大容量和互联型产品支持 I2S 音频协议(默认 SPI 模式)

2.3 硬件架构

SPI 外设的核心由以下部分组成:

复制代码
发送缓冲区 → 移位寄存器 → MOSI 引脚
MISO 引脚 → 移位寄存器 → 接收缓冲区
                  ↑
            波特率发生器 → SCK 引脚
                  ↑
              控制寄存器
  • 发送 / 接收缓冲区:双缓冲结构,写入 SPI_DR 寄存器的数据进入发送缓冲区,接收到的数据存放在接收缓冲区

  • 移位寄存器:负责将并行数据转换为串行数据输出,或将串行输入数据转换为并行数据

  • 波特率发生器:根据控制寄存器的配置,从 APB 总线时钟分频产生 SCK 时钟信号

  • 控制电路:管理整个 SPI 的工作模式、时序和中断

三、SPI 初始化配置(标准库)

3.1 初始化步骤

  1. 开启 GPIO 和 SPI 外设时钟

  2. 配置 GPIO 引脚模式(SPI 功能复用)

  3. 配置 SPI 初始化结构体(波特率、CPOL、CPHA、数据帧等)

  4. 使能 SPI 外设

  5. (可选)配置中断或 DMA

3.2 最小可运行代码示例

bsp_spi.h
复制代码
#ifndef __BSP_SPI_H
#define __BSP_SPI_H

#include "stm32f10x.h"

// SPI1 引脚定义
#define SPI1_SCK_PIN     GPIO_Pin_5
#define SPI1_MOSI_PIN    GPIO_Pin_7
#define SPI1_MISO_PIN    GPIO_Pin_6
#define SPI1_NSS_PIN     GPIO_Pin_4  // 软件控制的片选引脚
#define SPI1_GPIO_PORT   GPIOA
#define SPI1_GPIO_CLK    RCC_APB2Periph_GPIOA
#define SPI1_CLK         RCC_APB2Periph_SPI1

// 片选控制宏
#define SPI1_NSS_LOW()   GPIO_ResetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN)
#define SPI1_NSS_HIGH()  GPIO_SetBits(SPI1_GPIO_PORT, SPI1_NSS_PIN)

void SPI1_Init(void);
uint8_t SPI1_ReadWriteByte(uint8_t data);

#endif
bsp_spi.c
复制代码
#include "bsp_spi.h"

/**
 * @brief  SPI1 初始化函数
 * @note   配置为模式 0 (CPOL=0, CPHA=0),8 位数据帧,MSB 先行,主模式
 *         波特率 = 72 MHz / 16 = 4.5 MHz,适用于大多数 SPI Flash
 */
void SPI1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    // 1. 开启 GPIO 和 SPI 时钟
    RCC_APB2PeriphClockCmd(SPI1_GPIO_CLK | SPI1_CLK, ENABLE);

    // 2. 配置 SPI 引脚
    // SCK 和 MOSI 配置为复用推挽输出(主机主动输出时钟和数据)
    GPIO_InitStructure.GPIO_Pin = SPI1_SCK_PIN | SPI1_MOSI_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStructure);

    // MISO 配置为浮空输入(主机接收从机数据)
    GPIO_InitStructure.GPIO_Pin = SPI1_MISO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStructure);

    // NSS 配置为通用推挽输出(软件控制片选)
    GPIO_InitStructure.GPIO_Pin = SPI1_NSS_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SPI1_GPIO_PORT, &GPIO_InitStructure);
    SPI1_NSS_HIGH();  // 默认拉高,未选中任何从机

    // 3. 配置 SPI 工作模式
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 双线全双工
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                       // 主模式
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                   // 8 位数据帧
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                          // CPOL=0,空闲时 SCK 低电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                        // CPHA=0,第一个边沿采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                           // 软件控制 NSS
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 波特率预分频 16
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                  // MSB 先行
    SPI_InitStructure.SPI_CRCPolynomial = 7;                            // CRC 多项式(不使用 CRC 时默认 7 即可)
    SPI_Init(SPI1, &SPI_InitStructure);

    // 4. 使能 SPI 外设
    SPI_Cmd(SPI1, ENABLE);
}

/**
 * @brief  SPI1 读写一个字节(全双工)
 * @param  data: 要发送的字节
 * @retval 接收到的字节
 * @note   SPI 全双工特性:发送一个字节的同时会接收一个字节
 *         写操作时,返回的字节无意义;读操作时,需要发送一个 dummy 字节
 */
uint8_t SPI1_ReadWriteByte(uint8_t data)
{
    // 等待发送缓冲区为空(TXE=1)
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    
    // 写入要发送的数据到数据寄存器
    SPI_I2S_SendData(SPI1, data);
    
    // 等待接收缓冲区非空(RXNE=1)
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    
    // 读取接收到的数据
    return SPI_I2S_ReceiveData(SPI1);
}
main.c(简单测试)
复制代码
#include "stm32f10x.h"
#include "bsp_spi.h"

int main(void)
{
    uint8_t tx_data = 0xAA;
    uint8_t rx_data;

    SPI1_Init();

    while (1)
    {
        SPI1_NSS_LOW();          // 选中从机
        rx_data = SPI1_ReadWriteByte(tx_data);  // 发送 0xAA,同时接收数据
        SPI1_NSS_HIGH();         // 取消选中从机
    }
}

3.3 代码关键解释

  1. GPIO 配置

    • SCK 和 MOSI 作为主机输出,配置为 复用推挽输出GPIO_Mode_AF_PP

    • MISO 作为主机输入,配置为 浮空输入GPIO_Mode_IN_FLOATING

    • NSS 采用软件控制,配置为 通用推挽输出GPIO_Mode_Out_PP

  2. SPI 读写函数原理

    SPI 是全双工通信,发送和接收同时进行。因此:

    • 写操作:主机发送数据,同时会收到从机返回的无效数据,忽略即可

    • 读操作 :主机需要发送一个 "dummy 字节"(如 0xFF),同时接收从机返回的有效数据

  3. 波特率计算

    SPI1 挂载在 APB2 总线(72 MHz),预分频系数为 16 时,波特率 = 72 MHz / 16 = 4.5 MHz。

    预分频系数可在 SPI_BaudRatePrescaler_2SPI_BaudRatePrescaler_256之间选择。

四、注意事项(避坑指南)

  1. 引脚复用冲突

    SPI3 的 NSS (PB4) 和 SCK (PB3) 引脚与 JTAG 引脚共享,使用 SPI3 时必须:

    复制代码
    // 关闭 JTAG,保留 SWD
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
  2. 主从模式一致性

    主设备和从设备的 CPOL、CPHA、数据帧长度、字节顺序​ 必须完全一致,否则会出现数据错乱。

  3. SPI 关闭时机

    关闭 SPI 前必须等待传输完成,否则会丢失最后一个字节:

    复制代码
    // 等待发送缓冲区为空
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    // 等待 SPI 总线空闲
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
    // 关闭 SPI
    SPI_Cmd(SPI1, DISABLE);
  4. 软件 NSS 管理

    推荐使用软件控制 NSS 引脚,灵活性更高。硬件 NSS 模式适用于单主单从场景,多从场景下必须使用软件 NSS。

  5. 溢出错误处理

    只发送模式下,由于不会读取接收缓冲区的数据,SPI_SR 寄存器的 OVR(溢出)标志会被置 1,这是正常现象,无需处理。

  6. 时钟频率限制

    SPI 通信速率受限于 低速设备,即使 STM32 支持 36 MHz,也不能超过 SPI Flash 的最高工作频率(如 W25Q64 最高支持 80 MHz)。

五、参考出处

  1. 《STM32F10xxx 参考手册》第 23 章 串行外设接口 (SPI)

  2. 《零死角玩转 STM32》SPI - 读写串行 FLASH 章节

  3. STMicroelectronics. RM0008 Reference Manual, Rev 10, 2009