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总线:
问题: - 需要总线仲裁机制 - 硬件复杂(需要三态缓冲器) - 软件复杂(需要冲突检测)
建议: - 尽量避免多主机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,单向)
方案3:5V容忍输入 - 许多3.3V MCU的GPIO支持5V输入 - 查看数据手册确认 - STM32大部分GPIO支持5V容忍
上拉/下拉电阻¶
CS线上拉:
MISO线上拉/下拉(可选):
典型值: - CS上拉:10kΩ - MISO上拉/下拉:47kΩ(可选) - 高速应用(>10MHz):减小到4.7kΩ或去掉
PCB布线建议¶
布线规则:
- 短而直:
- SPI走线尽量短(<10cm)
- 避免长距离走线
-
减少寄生电容和电感
-
等长布线:
- SCLK、MOSI、MISO尽量等长
- 减少时序偏差
-
高速应用(>20MHz)必须等长
-
地平面:
- 信号线下方保留完整地平面
- 提供低阻抗回流路径
-
减少EMI
-
远离干扰源:
- 远离电源线、高速信号
- 避免与其他信号平行走线
-
必要时添加地线隔离
-
串联电阻(高速应用):
-
去耦电容:
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 <SPI.h>
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 <SPI.h>
// 定义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 <SPI.h>
#include <SD.h>
#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();
// 传输完成
}
- 提高时钟频率
- 批量传输
// 不好:逐字节传输
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显示屏驱动
高级主题¶
参考资料¶
数据手册¶
- 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)
在线工具¶
开源项目¶
- 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 协议,欢迎分享和改编,但请注明出处。