一、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 数据传输规则
-
同步传输:数据在 SCK 时钟的同步下传输,每个时钟周期传输 1 位数据,MOSI 和 MISO 同时传输(全双工)。
-
字节顺序 :SPI 协议本身未强制规定 MSB(高位)先行还是 LSB(低位)先行,但 绝大多数 SPI 设备采用 MSB 先行,STM32 可通过寄存器配置字节顺序。
-
数据长度:支持 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 外设支持以下核心功能:
-
通信模式:3 线全双工、双线单工(带或不带双向数据线)
-
数据帧:8 位或 16 位数据帧,可配置 MSB / LSB 先行
-
主从模式:支持主模式和从模式,支持多主配置
-
波特率:8 级预分频(2 / 4 / 8 / 16 / 32 / 64 / 128 / 256),主模式最高为 PCLK / 2
-
NSS 管理:支持硬件 NSS 和软件 NSS 两种模式
-
可靠性:硬件 CRC 校验,支持 DMA 高速传输
-
扩展功能:大容量和互联型产品支持 I2S 音频协议(默认 SPI 模式)
2.3 硬件架构
SPI 外设的核心由以下部分组成:
发送缓冲区 → 移位寄存器 → MOSI 引脚
MISO 引脚 → 移位寄存器 → 接收缓冲区
↑
波特率发生器 → SCK 引脚
↑
控制寄存器
-
发送 / 接收缓冲区:双缓冲结构,写入 SPI_DR 寄存器的数据进入发送缓冲区,接收到的数据存放在接收缓冲区
-
移位寄存器:负责将并行数据转换为串行数据输出,或将串行输入数据转换为并行数据
-
波特率发生器:根据控制寄存器的配置,从 APB 总线时钟分频产生 SCK 时钟信号
-
控制电路:管理整个 SPI 的工作模式、时序和中断
三、SPI 初始化配置(标准库)
3.1 初始化步骤
-
开启 GPIO 和 SPI 外设时钟
-
配置 GPIO 引脚模式(SPI 功能复用)
-
配置 SPI 初始化结构体(波特率、CPOL、CPHA、数据帧等)
-
使能 SPI 外设
-
(可选)配置中断或 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 代码关键解释
-
GPIO 配置:
-
SCK 和 MOSI 作为主机输出,配置为 复用推挽输出 (
GPIO_Mode_AF_PP) -
MISO 作为主机输入,配置为 浮空输入 (
GPIO_Mode_IN_FLOATING) -
NSS 采用软件控制,配置为 通用推挽输出 (
GPIO_Mode_Out_PP)
-
-
SPI 读写函数原理:
SPI 是全双工通信,发送和接收同时进行。因此:
-
写操作:主机发送数据,同时会收到从机返回的无效数据,忽略即可
-
读操作 :主机需要发送一个 "dummy 字节"(如
0xFF),同时接收从机返回的有效数据
-
-
波特率计算:
SPI1 挂载在 APB2 总线(72 MHz),预分频系数为 16 时,波特率 = 72 MHz / 16 = 4.5 MHz。
预分频系数可在
SPI_BaudRatePrescaler_2到SPI_BaudRatePrescaler_256之间选择。
四、注意事项(避坑指南)
-
引脚复用冲突:
SPI3 的 NSS (PB4) 和 SCK (PB3) 引脚与 JTAG 引脚共享,使用 SPI3 时必须:
// 关闭 JTAG,保留 SWD RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); -
主从模式一致性:
主设备和从设备的 CPOL、CPHA、数据帧长度、字节顺序 必须完全一致,否则会出现数据错乱。
-
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); -
软件 NSS 管理:
推荐使用软件控制 NSS 引脚,灵活性更高。硬件 NSS 模式适用于单主单从场景,多从场景下必须使用软件 NSS。
-
溢出错误处理:
只发送模式下,由于不会读取接收缓冲区的数据,SPI_SR 寄存器的 OVR(溢出)标志会被置 1,这是正常现象,无需处理。
-
时钟频率限制:
SPI 通信速率受限于 低速设备,即使 STM32 支持 36 MHz,也不能超过 SPI Flash 的最高工作频率(如 W25Q64 最高支持 80 MHz)。
五、参考出处
-
《STM32F10xxx 参考手册》第 23 章 串行外设接口 (SPI)
-
《零死角玩转 STM32》SPI - 读写串行 FLASH 章节
-
STMicroelectronics. RM0008 Reference Manual, Rev 10, 2009