跳转至

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