SPI硬件接口与应用¶
概述¶
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信接口,由Motorola公司开发。SPI广泛应用于MCU与外设(如Flash存储器、SD卡、显示屏、传感器)之间的通信,以其简单、高速的特点成为嵌入式系统中最常用的通信接口之一。
完成本教程学习后,你将能够:
理解SPI的工作原理和硬件特性
掌握主从模式和多从设备连接方式
理解时钟极性(CPOL)和时钟相位(CPHA)的配置
学会SPI接口的硬件设计和PCB布线
实现STM32和Arduino的SPI通信
掌握常见SPI外设的应用
背景知识¶
SPI vs UART vs I2C¶
特性
SPI
UART
I2C
通信方式
同步
异步
同步
数据线
4根(MOSI/MISO/SCK/CS)
2根(TX/RX)
2根(SDA/SCL)
速度
很快(MHz级)
较慢(kbps级)
中等(kHz-MHz)
全双工
是
是
否
多主机
复杂
不支持
支持
多从机
简单(独立CS)
不支持
简单(地址)
硬件复杂度
中等
简单
简单
传输距离
短(<1米)
中(数米)
短(<1米)
SPI的优势¶
速度快:
- 时钟频率可达数十MHz
- 适合大数据量传输
- 无需波特率配置
全双工:
- 同时发送和接收数据
- 效率高
硬件简单:
- 无需复杂的协议
- 无需地址配置
- 无需应答机制
灵活性高:
- 数据位宽可配置(8/16位)
- 时钟极性和相位可配置
- 支持多种工作模式
SPI工作原理¶
信号线定义¶
SPI使用4根信号线进行通信:
1. SCLK(Serial Clock)- 串行时钟
- 由主机产生
- 同步数据传输
- 频率可配置(通常1-50MHz)
- 别名:SCK、CLK
2. MOSI(Master Out Slave In)- 主机输出从机输入
- 主机发送数据到从机
- 数据方向:主机 → 从机
- 别名:SDO(Serial Data Out)、DO、DOUT、SI(Slave In)
3. MISO(Master In Slave Out)- 主机输入从机输出
- 从机发送数据到主机
- 数据方向:从机 → 主机
- 别名:SDI(Serial Data In)、DI、DIN、SO(Slave Out)
4. CS/SS(Chip Select/Slave Select)- 片选信号
- 选择要通信的从机
- 低电平有效(通常)
- 每个从机需要独立的CS线
- 别名:NSS(Not Slave Select)、CE(Chip Enable)
基本连接方式¶
单主机单从机:
主机(MCU) 从机(外设)
SCLK ──────────────────→ SCLK
MOSI ──────────────────→ MOSI
MISO ←────────────────── MISO
CS ──────────────────→ CS
GND ←─────────────────→ GND
关键要点:
- SCLK、MOSI、MISO直接连接
- CS由主机控制,选择从机
- 必须共地
数据传输过程¶
SPI数据传输特点:
- 全双工:同时发送和接收
- 同步:由时钟信号同步
- 移位寄存器:主从机各有一个8位移位寄存器
传输过程示意:
主机 从机
┌─────────┐ ┌─────────┐
│ 移位寄存器 │ │ 移位寄存器 │
│ 7 6 5 4 3 2 1 0 │ │ 7 6 5 4 3 2 1 0 │
└─────────┘ └─────────┘
│ │
└──── MOSI ────────────────────→ │
│ │
←──── MISO ────────────────────┘
│ │
└──── SCLK ────────────────────→
│ │
└──── CS ──────────────────────→
时钟周期:
SCLK: ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─
MOSI: ─┤ D7 ├─┤ D6 ├─┤ D5 ├─┤ D4 ├─┤ D3 ├─┤ D2 ├─┤ D1 ├─┤ D0 ├─
MISO: ─┤ D7 ├─┤ D6 ├─┤ D5 ├─┤ D4 ├─┤ D3 ├─┤ D2 ├─┤ D1 ├─┤ D0 ├─
传输步骤:
主机拉低CS:选中从机
主机产生时钟:SCLK开始翻转
数据交换:
每个时钟周期,主机通过MOSI发送1位
同时,从机通过MISO发送1位
数据在移位寄存器中移动
传输完成:8个时钟周期后,完成1字节交换
主机拉高CS:释放从机
重要特性:
- 即使主机只想接收数据,也必须发送时钟和数据(可以发送dummy数据0x00或0xFF)
- 即使主机只想发送数据,从机也会同时发送数据(主机可以忽略)
- 这是SPI"全双工"的本质
时钟极性和相位¶
CPOL和CPHA详解¶
SPI有两个重要的配置参数:
CPOL(Clock Polarity)- 时钟极性:
- CPOL=0:时钟空闲时为低电平
- CPOL=1:时钟空闲时为高电平
CPHA(Clock Phase)- 时钟相位:
- CPHA=0:在时钟的第一个边沿采样数据
- CPHA=1:在时钟的第二个边沿采样数据
四种SPI模式¶
CPOL和CPHA组合形成4种SPI模式:
模式
CPOL
CPHA
空闲电平
采样边沿
输出边沿
0
0
0
低
上升沿
下降沿
1
0
1
低
下降沿
上升沿
2
1
0
高
下降沿
上升沿
3
1
1
高
上升沿
下降沿
模式0(CPOL=0, CPHA=0)- 最常用:
CS: ────┐ ┌────
└────────────────────────────────────┘
SCLK: ────┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌────
└───┘ └───┘ └───┘ └───┘ └───┘
空闲低 ↑采样 ↑采样 ↑采样 ↑采样
MOSI: ────────┤ D7 ├───┤ D6 ├───┤ D5 ├───┤ D4 ├────
└───┘输出 └───┘ └───┘ └───┘
说明:
- 空闲时SCLK为低电平
- 数据在下降沿输出(准备)
- 数据在上升沿采样(读取)
模式1(CPOL=0, CPHA=1):
CS: ────┐ ┌────
└────────────────────────────────────┘
SCLK: ────┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌────
└───┘ └───┘ └───┘ └───┘ └───┘
空闲低 ↓采样 ↓采样 ↓采样 ↓采样
MOSI: ────┤ D7 ├───┤ D6 ├───┤ D5 ├───┤ D4 ├────────
└───┘ └───┘ └───┘ └───┘
说明:
- 空闲时SCLK为低电平
- 数据在上升沿输出(准备)
- 数据在下降沿采样(读取)
模式2(CPOL=1, CPHA=0):
CS: ────┐ ┌────
└────────────────────────────────────┘
SCLK: ────┘ └───┐ └───┐ └───┐ └───┐ └────
┌───┘ ┌───┘ ┌───┘ ┌───┘
空闲高 ↓采样 ↓采样 ↓采样 ↓采样
MOSI: ────────┤ D7 ├───┤ D6 ├───┤ D5 ├───┤ D4 ├────
└───┘ └───┘ └───┘ └───┘
说明:
- 空闲时SCLK为高电平
- 数据在上升沿输出(准备)
- 数据在下降沿采样(读取)
模式3(CPOL=1, CPHA=1):
CS: ────┐ ┌────
└────────────────────────────────────┘
SCLK: ────┘ └───┐ └───┐ └───┐ └───┐ └────
┌───┘ ┌───┘ ┌───┘ ┌───┘
空闲高 ↑采样 ↑采样 ↑采样 ↑采样
MOSI: ────┤ D7 ├───┤ D6 ├───┤ D5 ├───┤ D4 ├────────
└───┘ └───┘ └───┘ └───┘
说明:
- 空闲时SCLK为高电平
- 数据在下降沿输出(准备)
- 数据在上升沿采样(读取)
模式选择建议¶
如何选择SPI模式:
1. 查看从机数据手册:从机通常只支持特定模式
2. 模式0最常用:大多数设备支持模式0
3. 主从必须匹配:主机和从机必须使用相同模式
4. 多从机系统:所有从机最好使用相同模式
常见设备的SPI模式:
设备类型
常用模式
示例
Flash存储器
模式0或3
W25Q128, AT25SF128
SD卡
模式0
microSD
显示屏
模式0或3
ILI9341, ST7735
ADC
模式0或1
MCP3008, ADS1256
加速度计
模式0或3
ADXL345, MPU6050
无线模块
模式0
nRF24L01, CC2500
多从设备连接¶
独立片选方式(最常用)¶
每个从机有独立的CS线:
主机 从机1 从机2 从机3
SCLK ──────┬────────→ SCLK
├────────→ SCLK
└────────→ SCLK
MOSI ──────┬────────→ MOSI
├────────→ MOSI
└────────→ MOSI
MISO ←─────┴──────── MISO
←───┴──────── MISO
←───┴──────── MISO
CS1 ──────────────→ CS
CS2 ──────────────────────→ CS
CS3 ──────────────────────────────→ CS
优点:
- 简单可靠
- 从机之间完全独立
- 可以使用不同的SPI模式
- 可以使用不同的时钟频率
缺点:
- 每个从机需要一个GPIO作为CS
- GPIO资源消耗大
使用场景:
- 从机数量较少(<4个)
- 从机需要不同配置
- 对可靠性要求高
菊花链方式(Daisy Chain)¶
从机串联连接:
主机 从机1 从机2 从机3
SCLK ────────→ SCLK ────────→ SCLK ────────→ SCLK
MOSI ────────→ MOSI ────────→ MOSI ────────→ MOSI
MISO ←──────── MISO ←──────── MISO ←──────── MISO
CS ────────→ CS ────────→ CS ────────→ CS
数据流:
主机 → 从机1 → 从机2 → 从机3 → 主机
工作原理:
- 所有从机共享CS
- 数据依次通过每个从机
- 发送N个字节,第1个字节到达从机N
优点:
- 只需一个CS引脚
- 节省GPIO资源
缺点:
- 传输速度慢(数据需要经过所有从机)
- 所有从机必须支持菊花链模式
- 配置复杂
使用场景:
- GPIO资源紧张
- 从机支持菊花链
- 对速度要求不高
多主机模式(不常用)¶
多个主机共享SPI总线:
主机1 ──┬── SCLK
主机2 ──┤
│
主机1 ──┬── MOSI
主机2 ──┤
│
主机1 ──┬── MISO
主机2 ──┤
│
└──→ 从机
问题:
- 需要总线仲裁机制
- 硬件复杂(需要三态缓冲器)
- 软件复杂(需要冲突检测)
建议:
- 尽量避免多主机SPI
- 考虑使用I2C(原生支持多主机)
- 或使用主从切换机制
SPI硬件设计要点¶
电平匹配¶
3.3V ↔ 5V电平转换:
方案1:使用电平转换芯片
3.3V MCU TXS0108E 5V设备
SCLK ──→ A1 B1 ──→ SCLK
MOSI ──→ A2 B2 ──→ MOSI
MISO ←── A3 B3 ←── MISO
CS ──→ A4 B4 ──→ CS
方案2:使用分压电阻(5V→3.3V,单向)
5V设备 3.3V MCU
MISO ──┬─ 1kΩ ─┬─────────→ MISO
└─ 2kΩ ─┴─ GND
输出电压 = 5V × 2kΩ/(1kΩ+2kΩ) ≈ 3.33V ✓
方案3:5V容忍输入
- 许多3.3V MCU的GPIO支持5V输入
- 查看数据手册确认
- STM32大部分GPIO支持5V容忍
上拉/下拉电阻¶
CS线上拉:
VCC
│
├─ 10kΩ ─┬─ CS1
│ │
├─ 10kΩ ─┼─ CS2
│ │
└─ 10kΩ ─┴─ CS3
作用:
- 确保CS默认为高电平(未选中)
- 防止MCU复位时误选中从机
MISO线上拉/下拉(可选):
VCC
│
└─ 47kΩ ─┬─ MISO
作用:
- 防止MISO浮空
- 多从机时,未选中的从机MISO为高阻态
典型值:
- CS上拉:10kΩ
- MISO上拉/下拉:47kΩ(可选)
- 高速应用(>10MHz):减小到4.7kΩ或去掉
PCB布线建议¶
布线规则:
短而直:
SPI走线尽量短(<10cm)
避免长距离走线
减少寄生电容和电感
等长布线:
SCLK、MOSI、MISO尽量等长
减少时序偏差
高速应用(>20MHz)必须等长
地平面:
信号线下方保留完整地平面
提供低阻抗回流路径
减少EMI
远离干扰源:
远离电源线、高速信号
避免与其他信号平行走线
必要时添加地线隔离
串联电阻(高速应用):
MCU ─┬─ 22Ω ─┬─ SCLK ─→ 外设
│ │
└─ 22Ω ─┴─ MOSI ─→ 外设
作用:
- 阻尼振铃
- 改善信号完整性
- 减少EMI
去耦电容:
每个IC的VCC引脚附近:
VCC ─┬─ 0.1μF ─┬─ GND
└─ 10μF ──┘
放置位置:
- 尽量靠近IC电源引脚
- 0.1μF高频去耦
- 10μF低频去耦
ESD保护¶
保护电路:
SCLK ──┬─────────→ 外部接口
│
├─ TVS二极管 ─┬─ GND
│ │
└─ 33Ω ───────┘
推荐TVS型号:
- PESD5V0S1BA(单通道)
- PESD5V0L4UG(4通道)
保护要点:
- 外部接口必须添加ESD保护
- 板内连接可以不加
- 高速信号注意TVS的电容
实践示例¶
示例1:STM32 SPI硬件配置¶
硬件连接:
STM32F103C8T6 W25Q128 Flash
PA5 (SPI1_SCK) ────→ CLK
PA7 (SPI1_MOSI) ────→ DI
PA6 (SPI1_MISO) ←──── DO
PA4 (GPIO) ────→ CS
3.3V ───────────────→ VCC
GND ────────────────→ GND
寄存器配置(模式0,8位,18MHz):
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA时钟
// 2. 配置GPIO
// PA5 (SCK) - 复用推挽输出,50MHz
GPIOA->CRL &= ~(0xF << 20);
GPIOA->CRL |= (0xB << 20);
// PA7 (MOSI) - 复用推挽输出,50MHz
GPIOA->CRL &= ~(0xF << 28);
GPIOA->CRL |= (0xB << 28);
// PA6 (MISO) - 浮空输入
GPIOA->CRL &= ~(0xF << 24);
GPIOA->CRL |= (0x4 << 24);
// PA4 (CS) - 通用推挽输出,50MHz
GPIOA->CRL &= ~(0xF << 16);
GPIOA->CRL |= (0x3 << 16);
GPIOA->ODR |= (1 << 4); // CS默认高电平
// 3. 配置SPI
// 模式0:CPOL=0, CPHA=0
SPI1->CR1 &= ~SPI_CR1_CPOL; // 时钟空闲为低
SPI1->CR1 &= ~SPI_CR1_CPHA; // 第一个边沿采样
// 8位数据帧
SPI1->CR1 &= ~SPI_CR1_DFF;
// MSB先发送
SPI1->CR1 &= ~SPI_CR1_LSBFIRST;
// 软件管理CS
SPI1->CR1 |= SPI_CR1_SSM;
SPI1->CR1 |= SPI_CR1_SSI;
// 主机模式
SPI1->CR1 |= SPI_CR1_MSTR;
// 波特率:72MHz / 4 = 18MHz
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x1 << 3); // 分频系数4
// 使能SPI
SPI1->CR1 |= SPI_CR1_SPE;
发送/接收函数:
// CS控制
void SPI_CS_Low(void) {
GPIOA->ODR &= ~(1 << 4);
}
void SPI_CS_High(void) {
GPIOA->ODR |= (1 << 4);
}
// 发送并接收一个字节
uint8_t SPI_TransferByte(uint8_t data) {
// 等待发送缓冲区空
while (!(SPI1->SR & SPI_SR_TXE));
// 发送数据
SPI1->DR = data;
// 等待接收完成
while (!(SPI1->SR & SPI_SR_RXNE));
// 读取接收到的数据
return SPI1->DR;
}
// 发送多个字节
void SPI_TransmitBytes(uint8_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
SPI_TransferByte(data[i]);
}
}
// 接收多个字节
void SPI_ReceiveBytes(uint8_t *buffer, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
buffer[i] = SPI_TransferByte(0xFF); // 发送dummy数据
}
}
W25Q128 Flash读取示例:
// 读取Flash ID
void W25Q_ReadID(uint8_t *id) {
SPI_CS_Low();
SPI_TransferByte(0x9F); // 读ID命令
id[0] = SPI_TransferByte(0xFF); // 厂商ID
id[1] = SPI_TransferByte(0xFF); // 设备ID高字节
id[2] = SPI_TransferByte(0xFF); // 设备ID低字节
SPI_CS_High();
}
// 读取Flash数据
void W25Q_ReadData(uint32_t addr, uint8_t *buffer, uint16_t len) {
SPI_CS_Low();
SPI_TransferByte(0x03); // 读数据命令
SPI_TransferByte((addr >> 16) & 0xFF); // 地址高字节
SPI_TransferByte((addr >> 8) & 0xFF); // 地址中字节
SPI_TransferByte(addr & 0xFF); // 地址低字节
SPI_ReceiveBytes(buffer, len);
SPI_CS_High();
}
// 使用示例
void main(void) {
uint8_t id[3];
uint8_t data[256];
// 读取Flash ID
W25Q_ReadID(id);
printf("Flash ID: 0x%02X%02X%02X\n", id[0], id[1], id[2]);
// 预期输出:Flash ID: 0xEF4018 (W25Q128)
// 读取数据
W25Q_ReadData(0x000000, data, 256);
}
示例2:Arduino SPI配置¶
硬件连接:
Arduino Uno 外设
D13 (SCK) ────────→ SCLK
D11 (MOSI) ────────→ MOSI
D12 (MISO) ←──────── MISO
D10 (SS) ────────→ CS
5V ────────────────→ VCC
GND ───────────────→ GND
基本配置:
#include
void setup() {
Serial.begin(115200);
// 初始化SPI
SPI.begin();
// 配置CS引脚
pinMode(10, OUTPUT);
digitalWrite(10, HIGH); // CS默认高电平
// 配置SPI参数
// 参数:时钟频率、位顺序、模式
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
Serial.println("SPI Ready!");
}
void loop() {
// 选中从机
digitalWrite(10, LOW);
// 发送数据
uint8_t response = SPI.transfer(0x55);
// 释放从机
digitalWrite(10, HIGH);
Serial.print("Response: 0x");
Serial.println(response, HEX);
delay(1000);
}
多从机示例:
#include
// 定义CS引脚
#define CS_FLASH 10
#define CS_SENSOR 9
#define CS_DISPLAY 8
void setup() {
SPI.begin();
// 配置CS引脚
pinMode(CS_FLASH, OUTPUT);
pinMode(CS_SENSOR, OUTPUT);
pinMode(CS_DISPLAY, OUTPUT);
// 默认全部未选中
digitalWrite(CS_FLASH, HIGH);
digitalWrite(CS_SENSOR, HIGH);
digitalWrite(CS_DISPLAY, HIGH);
}
// Flash通信(模式0,10MHz)
void Flash_Write(uint8_t data) {
SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_FLASH, LOW);
SPI.transfer(data);
digitalWrite(CS_FLASH, HIGH);
SPI.endTransaction();
}
// 传感器通信(模式3,1MHz)
uint8_t Sensor_Read(void) {
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE3));
digitalWrite(CS_SENSOR, LOW);
uint8_t data = SPI.transfer(0x00);
digitalWrite(CS_SENSOR, HIGH);
SPI.endTransaction();
return data;
}
// 显示屏通信(模式0,8MHz)
void Display_SendCommand(uint8_t cmd) {
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_DISPLAY, LOW);
SPI.transfer(cmd);
digitalWrite(CS_DISPLAY, HIGH);
SPI.endTransaction();
}
SD卡读写示例:
#include
#include
#define CS_SD 10
void setup() {
Serial.begin(115200);
// 初始化SD卡
if (!SD.begin(CS_SD)) {
Serial.println("SD Card initialization failed!");
return;
}
Serial.println("SD Card initialized.");
// 写入文件
File myFile = SD.open("test.txt", FILE_WRITE);
if (myFile) {
myFile.println("Hello, SPI!");
myFile.close();
Serial.println("Write complete.");
}
// 读取文件
myFile = SD.open("test.txt");
if (myFile) {
while (myFile.available()) {
Serial.write(myFile.read());
}
myFile.close();
}
}
void loop() {
// 空
}
示例3:软件模拟SPI(BitBang)¶
当硬件SPI不可用或引脚冲突时,可以用GPIO模拟:
// 定义引脚
#define SPI_SCK_PIN GPIO_PIN_0
#define SPI_MOSI_PIN GPIO_PIN_1
#define SPI_MISO_PIN GPIO_PIN_2
#define SPI_CS_PIN GPIO_PIN_3
#define SPI_PORT GPIOA
// 延时函数(调整以匹配所需速度)
void SPI_Delay(void) {
for (volatile int i = 0; i < 10; i++);
}
// 初始化GPIO
void SPI_GPIO_Init(void) {
// 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// SCK, MOSI, CS - 推挽输出
GPIOA->CRL &= ~(0xFFF << 0);
GPIOA->CRL |= (0x333 << 0);
// MISO - 浮空输入
GPIOA->CRL &= ~(0xF << 8);
GPIOA->CRL |= (0x4 << 8);
// 初始状态
GPIOA->ODR &= ~SPI_SCK_PIN; // SCK低
GPIOA->ODR &= ~SPI_MOSI_PIN; // MOSI低
GPIOA->ODR |= SPI_CS_PIN; // CS高
}
// 发送并接收一个字节(模式0)
uint8_t SPI_TransferByte_SW(uint8_t data) {
uint8_t received = 0;
for (int i = 7; i >= 0; i--) {
// 输出数据位(MOSI)
if (data & (1 << i)) {
GPIOA->ODR |= SPI_MOSI_PIN;
} else {
GPIOA->ODR &= ~SPI_MOSI_PIN;
}
SPI_Delay();
// 上升沿(采样)
GPIOA->ODR |= SPI_SCK_PIN;
SPI_Delay();
// 读取数据位(MISO)
if (GPIOA->IDR & SPI_MISO_PIN) {
received |= (1 << i);
}
// 下降沿
GPIOA->ODR &= ~SPI_SCK_PIN;
}
return received;
}
// CS控制
void SPI_CS_Low_SW(void) {
GPIOA->ODR &= ~SPI_CS_PIN;
}
void SPI_CS_High_SW(void) {
GPIOA->ODR |= SPI_CS_PIN;
}
// 使用示例
void main(void) {
SPI_GPIO_Init();
SPI_CS_Low_SW();
uint8_t response = SPI_TransferByte_SW(0x55);
SPI_CS_High_SW();
printf("Response: 0x%02X\n", response);
}
软件SPI的优缺点:
优点:
- 任意GPIO都可以使用
- 不占用硬件SPI资源
- 灵活性高
缺点:
- 速度慢(通常<1MHz)
- 占用CPU时间
- 不支持DMA
- 代码量大
使用场景:
- 硬件SPI已被占用
- 需要多个SPI接口
- 速度要求不高(<1MHz)
- 引脚位置特殊
常见问题与调试¶
问题1:无法通信¶
可能原因:
SPI模式不匹配
检查:CPOL和CPHA配置
解决:查看从机数据手册,配置正确的模式
时钟频率过高
检查:从机最大支持频率
解决:降低时钟频率
接线错误
检查:MOSI/MISO是否接反
解决:MOSI连接到从机的DI/SDI,MISO连接到从机的DO/SDO
CS控制错误
检查:CS是否正确拉低
解决:确保通信期间CS保持低电平
调试步骤:
// 1. 回环测试(短接MOSI和MISO)
SPI_CS_Low();
uint8_t sent = 0x55;
uint8_t received = SPI_TransferByte(sent);
SPI_CS_High();
if (sent == received) {
printf("SPI hardware OK\n");
} else {
printf("SPI hardware error\n");
}
// 2. 示波器检查
// - CS:应该在通信期间为低电平
// - SCLK:应该有时钟信号
// - MOSI:应该有数据变化
// - MISO:检查从机是否有响应
// 3. 逻辑分析仪抓包
// - 查看完整的时序
// - 验证CPOL/CPHA
// - 检查数据内容
问题2:数据错误¶
可能原因:
位顺序错误
检查:MSB/LSB先发送
解决:配置正确的位顺序(大多数设备使用MSB)
时序问题
检查:建立时间和保持时间
解决:降低时钟频率或调整CPHA
信号完整性问题
检查:走线是否过长、有干扰
解决:缩短走线、添加串联电阻
数据验证代码:
// 发送已知数据,验证接收
uint8_t test_pattern[] = {0x00, 0xFF, 0x55, 0xAA, 0x01, 0x02, 0x04, 0x08};
uint8_t received[8];
SPI_CS_Low();
for (int i = 0; i < 8; i++) {
received[i] = SPI_TransferByte(test_pattern[i]);
}
SPI_CS_High();
// 检查数据
for (int i = 0; i < 8; i++) {
if (received[i] != expected[i]) {
printf("Data error at index %d: sent=0x%02X, received=0x%02X\n",
i, test_pattern[i], received[i]);
}
}
问题3:多从机冲突¶
可能原因:
CS控制错误
检查:是否有多个CS同时为低
解决:确保同一时间只有一个CS为低
MISO总线冲突
检查:未选中的从机MISO是否为高阻态
解决:确保从机支持三态输出,或添加缓冲器
多从机测试代码:
// 测试每个从机
void Test_MultiSlave(void) {
uint8_t id1, id2, id3;
// 测试从机1
SPI_CS1_Low();
id1 = SPI_TransferByte(0x9F); // 读ID命令
SPI_CS1_High();
// 测试从机2
SPI_CS2_Low();
id2 = SPI_TransferByte(0x9F);
SPI_CS2_High();
// 测试从机3
SPI_CS3_Low();
id3 = SPI_TransferByte(0x9F);
SPI_CS3_High();
printf("Slave1 ID: 0x%02X\n", id1);
printf("Slave2 ID: 0x%02X\n", id2);
printf("Slave3 ID: 0x%02X\n", id3);
}
问题4:速度不够快¶
优化方法:
使用DMA传输
// STM32 HAL库DMA配置
void SPI_DMA_Init(void) {
// 配置DMA
hdma_spi1_tx.Instance = DMA1_Channel3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_spi1_tx);
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
}
// DMA发送
void SPI_DMA_Transmit(uint8_t *data, uint16_t len) {
SPI_CS_Low();
HAL_SPI_Transmit_DMA(&hspi1, data, len);
}
// DMA完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
SPI_CS_High();
// 传输完成
}
提高时钟频率
// 从4分频改为2分频
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x0 << 3); // 分频系数2
// 时钟:72MHz / 2 = 36MHz
批量传输
// 不好:逐字节传输
for (int i = 0; i < 1000; i++) {
SPI_CS_Low();
SPI_TransferByte(data[i]);
SPI_CS_High();
}
// 好:批量传输
SPI_CS_Low();
for (int i = 0; i < 1000; i++) {
SPI_TransferByte(data[i]);
}
SPI_CS_High();
常见SPI外设应用¶
1. Flash存储器(W25Q系列)¶
特点:
- 容量:1MB - 128MB
- 速度:最高104MHz
- 模式:模式0或模式3
- 应用:程序存储、数据记录
基本操作:
// 读取制造商ID
uint8_t W25Q_ReadManufacturerID(void) {
uint8_t id;
SPI_CS_Low();
SPI_TransferByte(0x90); // 命令
SPI_TransferByte(0x00); // 地址
SPI_TransferByte(0x00);
SPI_TransferByte(0x00);
id = SPI_TransferByte(0xFF); // 读取ID
SPI_CS_High();
return id;
}
// 写使能
void W25Q_WriteEnable(void) {
SPI_CS_Low();
SPI_TransferByte(0x06);
SPI_CS_High();
}
// 扇区擦除(4KB)
void W25Q_SectorErase(uint32_t addr) {
W25Q_WriteEnable();
SPI_CS_Low();
SPI_TransferByte(0x20); // 扇区擦除命令
SPI_TransferByte((addr >> 16) & 0xFF);
SPI_TransferByte((addr >> 8) & 0xFF);
SPI_TransferByte(addr & 0xFF);
SPI_CS_High();
// 等待擦除完成
while (W25Q_IsBusy());
}
// 页编程(256字节)
void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) {
W25Q_WriteEnable();
SPI_CS_Low();
SPI_TransferByte(0x02); // 页编程命令
SPI_TransferByte((addr >> 16) & 0xFF);
SPI_TransferByte((addr >> 8) & 0xFF);
SPI_TransferByte(addr & 0xFF);
for (uint16_t i = 0; i < len; i++) {
SPI_TransferByte(data[i]);
}
SPI_CS_High();
// 等待编程完成
while (W25Q_IsBusy());
}
2. SD卡¶
特点:
- 容量:GB级
- 速度:最高25MHz(标准模式)
- 模式:模式0
- 应用:数据存储、日志记录
初始化流程:
// SD卡初始化
bool SD_Init(void) {
uint8_t response;
// 1. 发送至少74个时钟脉冲(CS为高)
SPI_CS_High();
for (int i = 0; i < 10; i++) {
SPI_TransferByte(0xFF);
}
// 2. 发送CMD0(复位)
SPI_CS_Low();
response = SD_SendCommand(0, 0);
SPI_CS_High();
if (response != 0x01) {
return false; // 初始化失败
}
// 3. 发送CMD8(检查电压范围)
SPI_CS_Low();
response = SD_SendCommand(8, 0x1AA);
SPI_CS_High();
// 4. 发送ACMD41(初始化)
int timeout = 1000;
do {
SPI_CS_Low();
SD_SendCommand(55, 0); // CMD55
response = SD_SendCommand(41, 0x40000000); // ACMD41
SPI_CS_High();
HAL_Delay(1);
timeout--;
} while (response != 0x00 && timeout > 0);
if (timeout == 0) {
return false;
}
return true;
}
// 发送命令
uint8_t SD_SendCommand(uint8_t cmd, uint32_t arg) {
uint8_t response;
// 发送命令帧
SPI_TransferByte(0x40 | cmd); // 起始位+命令
SPI_TransferByte((arg >> 24) & 0xFF); // 参数
SPI_TransferByte((arg >> 16) & 0xFF);
SPI_TransferByte((arg >> 8) & 0xFF);
SPI_TransferByte(arg & 0xFF);
SPI_TransferByte(0x95); // CRC(CMD0需要有效CRC)
// 等待响应
for (int i = 0; i < 10; i++) {
response = SPI_TransferByte(0xFF);
if (!(response & 0x80)) {
return response;
}
}
return 0xFF; // 超时
}
3. TFT显示屏(ILI9341)¶
特点:
- 分辨率:240x320
- 速度:最高10MHz
- 模式:模式0或模式3
- 应用:图形显示、用户界面
基本操作:
// 定义引脚
#define LCD_DC_PIN GPIO_PIN_0 // 数据/命令选择
#define LCD_RST_PIN GPIO_PIN_1 // 复位
// 发送命令
void LCD_WriteCommand(uint8_t cmd) {
GPIOA->ODR &= ~LCD_DC_PIN; // DC=0(命令)
SPI_CS_Low();
SPI_TransferByte(cmd);
SPI_CS_High();
}
// 发送数据
void LCD_WriteData(uint8_t data) {
GPIOA->ODR |= LCD_DC_PIN; // DC=1(数据)
SPI_CS_Low();
SPI_TransferByte(data);
SPI_CS_High();
}
// 初始化
void LCD_Init(void) {
// 硬件复位
GPIOA->ODR &= ~LCD_RST_PIN;
HAL_Delay(10);
GPIOA->ODR |= LCD_RST_PIN;
HAL_Delay(120);
// 软件初始化
LCD_WriteCommand(0x01); // 软件复位
HAL_Delay(120);
LCD_WriteCommand(0x11); // 退出睡眠模式
HAL_Delay(120);
LCD_WriteCommand(0x29); // 显示开启
}
// 画点
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
// 设置窗口
LCD_WriteCommand(0x2A); // 列地址设置
LCD_WriteData(x >> 8);
LCD_WriteData(x & 0xFF);
LCD_WriteData(x >> 8);
LCD_WriteData(x & 0xFF);
LCD_WriteCommand(0x2B); // 行地址设置
LCD_WriteData(y >> 8);
LCD_WriteData(y & 0xFF);
LCD_WriteData(y >> 8);
LCD_WriteData(y & 0xFF);
// 写入颜色
LCD_WriteCommand(0x2C); // 内存写入
LCD_WriteData(color >> 8);
LCD_WriteData(color & 0xFF);
}
4. ADC(MCP3008)¶
特点:
- 分辨率:10位
- 通道:8个单端或4个差分
- 速度:最高3.6MHz
- 模式:模式0或模式3
读取ADC:
// 读取ADC通道
uint16_t MCP3008_Read(uint8_t channel) {
uint8_t cmd_high = 0x01; // 起始位
uint8_t cmd_low = (channel << 4) | 0x80; // 单端模式
uint16_t result;
SPI_CS_Low();
SPI_TransferByte(cmd_high);
uint8_t high = SPI_TransferByte(cmd_low);
uint8_t low = SPI_TransferByte(0x00);
SPI_CS_High();
// 组合结果(10位)
result = ((high & 0x03) << 8) | low;
return result;
}
// 使用示例
void main(void) {
uint16_t adc_value;
float voltage;
adc_value = MCP3008_Read(0); // 读取通道0
voltage = (adc_value * 3.3) / 1024.0; // 转换为电压
printf("ADC: %d, Voltage: %.2fV\n", adc_value, voltage);
}
性能优化¶
提高传输速度¶
1. 使用最高时钟频率
// 检查从机支持的最大频率
// 例如:W25Q128支持104MHz
// STM32配置最高速度
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x0 << 3); // 不分频
// 时钟:72MHz / 2 = 36MHz(APB2最大分频)
2. 使用DMA
// 配置DMA
void SPI_DMA_Config(void) {
// 使能DMA时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 配置DMA通道3(SPI1_TX)
DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR; // 外设地址
DMA1_Channel3->CCR = DMA_CCR_MINC | // 内存地址递增
DMA_CCR_DIR | // 内存到外设
DMA_CCR_TCIE; // 传输完成中断
// 使能SPI的DMA请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;
}
// DMA发送
void SPI_DMA_Send(uint8_t *data, uint16_t len) {
// 设置内存地址和数据长度
DMA1_Channel3->CMAR = (uint32_t)data;
DMA1_Channel3->CNDTR = len;
// 使能DMA
DMA1_Channel3->CCR |= DMA_CCR_EN;
// CS拉低
SPI_CS_Low();
}
// DMA中断处理
void DMA1_Channel3_IRQHandler(void) {
if (DMA1->ISR & DMA_ISR_TCIF3) {
// 清除中断标志
DMA1->IFCR = DMA_IFCR_CTCIF3;
// 等待SPI传输完成
while (SPI1->SR & SPI_SR_BSY);
// CS拉高
SPI_CS_High();
// 关闭DMA
DMA1_Channel3->CCR &= ~DMA_CCR_EN;
}
}
3. 减少CS切换
// 不好:频繁切换CS
for (int i = 0; i < 100; i++) {
SPI_CS_Low();
SPI_TransferByte(data[i]);
SPI_CS_High();
}
// 好:保持CS低电平
SPI_CS_Low();
for (int i = 0; i < 100; i++) {
SPI_TransferByte(data[i]);
}
SPI_CS_High();
降低功耗¶
1. 动态时钟控制
// 不使用时关闭SPI时钟
void SPI_Sleep(void) {
SPI1->CR1 &= ~SPI_CR1_SPE; // 禁用SPI
RCC->APB2ENR &= ~RCC_APB2ENR_SPI1EN; // 关闭时钟
}
// 使用时重新使能
void SPI_Wakeup(void) {
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // 使能时钟
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
}
2. 降低时钟频率
// 低功耗模式:降低时钟到1MHz
void SPI_LowPowerMode(void) {
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x5 << 3); // 分频64
// 时钟:72MHz / 64 = 1.125MHz
}
// 正常模式:恢复到18MHz
void SPI_NormalMode(void) {
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x1 << 3); // 分频4
// 时钟:72MHz / 4 = 18MHz
}
提高可靠性¶
1. 添加CRC校验
// 使能硬件CRC
void SPI_CRC_Enable(void) {
SPI1->CR1 |= SPI_CR1_CRCEN;
}
// 发送数据并计算CRC
void SPI_SendWithCRC(uint8_t *data, uint16_t len) {
SPI_CS_Low();
// 复位CRC
SPI1->CR1 |= SPI_CR1_CRCNEXT;
// 发送数据
for (uint16_t i = 0; i < len; i++) {
SPI_TransferByte(data[i]);
}
// 发送CRC
SPI1->CR1 |= SPI_CR1_CRCNEXT;
SPI_TransferByte(0x00); // CRC自动发送
SPI_CS_High();
}
2. 添加重试机制
#define MAX_RETRY 3
bool SPI_TransferWithRetry(uint8_t *tx_data, uint8_t *rx_data, uint16_t len) {
for (int retry = 0; retry < MAX_RETRY; retry++) {
SPI_CS_Low();
// 传输数据
for (uint16_t i = 0; i < len; i++) {
rx_data[i] = SPI_TransferByte(tx_data[i]);
}
SPI_CS_High();
// 验证数据
if (VerifyData(rx_data, len)) {
return true; // 成功
}
// 重试前延时
HAL_Delay(10);
}
return false; // 失败
}
3. 超时保护
// 带超时的传输
bool SPI_TransferByte_Timeout(uint8_t data, uint8_t *result, uint32_t timeout) {
uint32_t start = HAL_GetTick();
// 等待发送缓冲区空
while (!(SPI1->SR & SPI_SR_TXE)) {
if ((HAL_GetTick() - start) > timeout) {
return false; // 超时
}
}
// 发送数据
SPI1->DR = data;
// 等待接收完成
start = HAL_GetTick();
while (!(SPI1->SR & SPI_SR_RXNE)) {
if ((HAL_GetTick() - start) > timeout) {
return false; // 超时
}
}
// 读取数据
*result = SPI1->DR;
return true; // 成功
}
总结¶
通过本教程学习,你已经掌握了:
✅ SPI基础:工作原理、信号线定义、全双工通信
✅ 时钟配置:CPOL和CPHA的4种模式组合
✅ 多从机连接:独立片选和菊花链方式
✅ 硬件设计:电平匹配、PCB布线、ESD保护
✅ 实践应用:STM32/Arduino配置、常见外设使用
✅ 故障排查:常见问题诊断和解决方法
✅ 性能优化:DMA传输、功耗管理、可靠性提升
关键要点¶
SPI四线制:
SCLK:时钟(主机产生)
MOSI:主机输出从机输入
MISO:主机输入从机输出
CS:片选(低电平有效)
全双工特性:
同时发送和接收
即使只想接收,也要发送dummy数据
即使只想发送,从机也会响应
模式配置:
模式0(CPOL=0, CPHA=0)最常用
主从必须使用相同模式
查看从机数据手册确定支持的模式
多从机系统:
独立CS:每个从机一个CS引脚
共享SCLK、MOSI、MISO
同一时间只能选中一个从机
速度优化:
使用DMA传输
提高时钟频率
减少CS切换次数
实践建议¶
初学者练习¶
基础通信:
实现MCU与Flash的SPI通信
读取Flash ID
读写Flash数据
多从机系统:
连接2-3个SPI设备
实现独立CS控制
测试不同SPI模式
显示应用:
驱动SPI显示屏
显示文字和图形
实现简单UI
进阶项目¶
数据记录器:
使用SD卡存储数据
实现FAT文件系统
添加时间戳
无线传感器:
使用SPI无线模块(nRF24L01)
实现传感器数据无线传输
多节点组网
高速数据采集:
使用SPI ADC
DMA高速采集
实时数据处理
延伸阅读¶
相关文章¶
UART/USART硬件接口详解 - 学习异步串行通信
I2C硬件接口实战 - 学习两线总线通信
信号完整性分析 - 高速信号设计
外设应用¶
Flash存储器应用
SD卡文件系统
TFT显示屏驱动
高级主题¶
DMA传输技术
高速PCB设计
参考资料¶
数据手册¶
W25Q128 Datasheet - Flash存储器
ILI9341 Datasheet - TFT显示控制器
MCP3008 Datasheet - 8通道ADC
STM32F1 Reference Manual - SPI章节
技术标准¶
SPI规范:无正式标准,由Motorola定义
SD卡规范:SD Association Physical Layer Specification
SPI Flash规范:JEDEC JESD216(Serial Flash Discoverable Parameters)
在线工具¶
SPI时序分析器 - Logic分析仪
SPI波形生成器
在线SPI计算器
开源项目¶
SPIFlash Library - Arduino Flash库
Adafruit GFX - 图形库
FatFs - FAT文件系统
常见应用场景¶
1. 存储应用¶
Flash存储器:
- 程序存储(Bootloader)
- 配置参数保存
- 数据日志记录
SD卡:
- 大容量数据存储
- 音频/视频文件
- 固件升级
2. 显示应用¶
TFT显示屏:
- 用户界面
- 数据可视化
- 图形显示
OLED显示屏:
- 低功耗显示
- 小尺寸应用
- 高对比度
3. 传感器应用¶
ADC:
- 模拟信号采集
- 多通道测量
- 高精度采样
IMU(惯性测量单元):
- 姿态检测
- 运动追踪
- 导航系统
4. 通信应用¶
无线模块:
- nRF24L01(2.4GHz)
- LoRa模块
- 蓝牙模块
以太网:
- W5500以太网芯片
- 网络通信
- IoT应用
5. 控制应用¶
DAC:
- 模拟信号输出
- 波形生成
- 电压控制
GPIO扩展:
- MCP23S17(16位I/O)
- 引脚扩展
- 多路控制
附录¶
A. SPI模式速查表¶
模式
CPOL
CPHA
空闲
采样边沿
输出边沿
常见设备
0
0
0
低
上升沿
下降沿
SD卡, W25Q, ILI9341
1
0
1
低
下降沿
上升沿
MCP3008
2
1
0
高
下降沿
上升沿
-
3
1
1
高
上升沿
下降沿
W25Q, ADXL345
B. 常见SPI设备引脚对照表¶
设备
SCLK
MOSI
MISO
CS
其他
W25Q Flash
CLK
DI
DO
CS
-
SD卡
CLK
CMD
DAT0
DAT3
-
ILI9341
SCK
MOSI
MISO
CS
DC, RST
MCP3008
CLK
DIN
DOUT
CS
-
nRF24L01
SCK
MOSI
MISO
CSN
CE, IRQ
C. 故障排查检查清单¶
硬件检查:
- [ ] 电源电压正常(3.3V或5V)
- [ ] SCLK、MOSI、MISO正确连接
- [ ] CS正确连接且默认为高
- [ ] GND已连接
- [ ] 电平匹配(3.3V ↔ 3.3V 或 5V ↔ 5V)
- [ ] 走线长度合理(<10cm)
软件检查:
- [ ] SPI模式配置正确(CPOL/CPHA)
- [ ] 时钟频率在从机支持范围内
- [ ] 位顺序正确(MSB/LSB)
- [ ] GPIO复用功能正确
- [ ] SPI时钟已使能
- [ ] CS控制逻辑正确
测试方法:
- [ ] 回环测试(短接MOSI和MISO)
- [ ] 示波器观察波形
- [ ] 逻辑分析仪抓包
- [ ] 万用表测量电压
- [ ] 检查从机数据手册
D. 性能对比¶
参数
SPI
UART
I2C
最大速度
50MHz+
921.6kbps
3.4MHz
典型速度
10-20MHz
115.2kbps
400kHz
线数
4+
2
2
全双工
是
是
否
多主机
困难
不支持
支持
多从机
简单(独立CS)
不支持
简单(地址)
传输距离
短(<1m)
中(数米)
短(<1m)
功耗
中
低
低
复杂度
中
低
中
反馈与支持:
- 如果你在学习过程中遇到问题,欢迎在评论区留言
- 分享你的SPI项目经验,与其他学习者交流
- 发现文档错误?请提交Issue帮助我们改进
版权声明:本文采用 CC BY-SA 4.0 协议,欢迎分享和改编,但请注明出处。