星战时空·网游活动中心

SPI硬件接口与应用¶

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 协议,欢迎分享和改编,但请注明出处。

Copyright © 2022 星战时空·网游活动中心 All Rights Reserved.