BLOG8
跳转至

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 ├─

传输步骤:

  1. 主机拉低CS:选中从机
  2. 主机产生时钟:SCLK开始翻转
  3. 数据交换:
  4. 每个时钟周期,主机通过MOSI发送1位
  5. 同时,从机通过MISO发送1位
  6. 数据在移位寄存器中移动
  7. 传输完成:8个时钟周期后,完成1字节交换
  8. 主机拉高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布线建议

布线规则:

  1. 短而直:
  2. SPI走线尽量短(<10cm)
  3. 避免长距离走线
  4. 减少寄生电容和电感

  5. 等长布线:

  6. SCLK、MOSI、MISO尽量等长
  7. 减少时序偏差
  8. 高速应用(>20MHz)必须等长

  9. 地平面:

  10. 信号线下方保留完整地平面
  11. 提供低阻抗回流路径
  12. 减少EMI

  13. 远离干扰源:

  14. 远离电源线、高速信号
  15. 避免与其他信号平行走线
  16. 必要时添加地线隔离

  17. 串联电阻(高速应用):

    MCU ─┬─ 22Ω ─┬─ SCLK ─→ 外设
         │       │
         └─ 22Ω ─┴─ MOSI ─→ 外设
    
    作用:
    - 阻尼振铃
    - 改善信号完整性
    - 减少EMI
    

  18. 去耦电容:

    每个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 <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:无法通信

可能原因:

  1. SPI模式不匹配
  2. 检查:CPOL和CPHA配置
  3. 解决:查看从机数据手册,配置正确的模式

  4. 时钟频率过高

  5. 检查:从机最大支持频率
  6. 解决:降低时钟频率

  7. 接线错误

  8. 检查:MOSI/MISO是否接反
  9. 解决:MOSI连接到从机的DI/SDI,MISO连接到从机的DO/SDO

  10. CS控制错误

  11. 检查:CS是否正确拉低
  12. 解决:确保通信期间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:数据错误

可能原因:

  1. 位顺序错误
  2. 检查:MSB/LSB先发送
  3. 解决:配置正确的位顺序(大多数设备使用MSB)

  4. 时序问题

  5. 检查:建立时间和保持时间
  6. 解决:降低时钟频率或调整CPHA

  7. 信号完整性问题

  8. 检查:走线是否过长、有干扰
  9. 解决:缩短走线、添加串联电阻

数据验证代码:

// 发送已知数据,验证接收
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:多从机冲突

可能原因:

  1. CS控制错误
  2. 检查:是否有多个CS同时为低
  3. 解决:确保同一时间只有一个CS为低

  4. MISO总线冲突

  5. 检查:未选中的从机MISO是否为高阻态
  6. 解决:确保从机支持三态输出,或添加缓冲器

多从机测试代码:

// 测试每个从机
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:速度不够快

优化方法:

  1. 使用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();
    // 传输完成
}
  1. 提高时钟频率
// 从4分频改为2分频
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= (0x0 << 3);  // 分频系数2
// 时钟:72MHz / 2 = 36MHz
  1. 批量传输
// 不好:逐字节传输
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传输、功耗管理、可靠性提升

关键要点

  1. SPI四线制:
  2. SCLK:时钟(主机产生)
  3. MOSI:主机输出从机输入
  4. MISO:主机输入从机输出
  5. CS:片选(低电平有效)

  6. 全双工特性:

  7. 同时发送和接收
  8. 即使只想接收,也要发送dummy数据
  9. 即使只想发送,从机也会响应

  10. 模式配置:

  11. 模式0(CPOL=0, CPHA=0)最常用
  12. 主从必须使用相同模式
  13. 查看从机数据手册确定支持的模式

  14. 多从机系统:

  15. 独立CS:每个从机一个CS引脚
  16. 共享SCLK、MOSI、MISO
  17. 同一时间只能选中一个从机

  18. 速度优化:

  19. 使用DMA传输
  20. 提高时钟频率
  21. 减少CS切换次数

实践建议

初学者练习

  1. 基础通信:
  2. 实现MCU与Flash的SPI通信
  3. 读取Flash ID
  4. 读写Flash数据

  5. 多从机系统:

  6. 连接2-3个SPI设备
  7. 实现独立CS控制
  8. 测试不同SPI模式

  9. 显示应用:

  10. 驱动SPI显示屏
  11. 显示文字和图形
  12. 实现简单UI

进阶项目

  1. 数据记录器:
  2. 使用SD卡存储数据
  3. 实现FAT文件系统
  4. 添加时间戳

  5. 无线传感器:

  6. 使用SPI无线模块(nRF24L01)
  7. 实现传感器数据无线传输
  8. 多节点组网

  9. 高速数据采集:

  10. 使用SPI ADC
  11. DMA高速采集
  12. 实时数据处理

延伸阅读

相关文章

外设应用

  • Flash存储器应用
  • SD卡文件系统
  • TFT显示屏驱动

高级主题

参考资料

数据手册

  1. W25Q128 Datasheet - Flash存储器
  2. ILI9341 Datasheet - TFT显示控制器
  3. MCP3008 Datasheet - 8通道ADC
  4. STM32F1 Reference Manual - SPI章节

技术标准

  1. SPI规范:无正式标准,由Motorola定义
  2. SD卡规范:SD Association Physical Layer Specification
  3. SPI Flash规范:JEDEC JESD216(Serial Flash Discoverable Parameters)

在线工具

  1. SPI时序分析器 - Logic分析仪
  2. SPI波形生成器
  3. 在线SPI计算器

开源项目

  1. SPIFlash Library - Arduino Flash库
  2. Adafruit GFX - 图形库
  3. 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 协议,欢迎分享和改编,但请注明出处。