SPI与I2C通信协议

- 次浏览

概述

SPI(Serial Peripheral Interface)和 I2C(Inter-Integrated Circuit)是嵌入式系统中最常用的两种串行通信协议。它们都用于短距离通信,主要连接单片机与各种外设芯片,如传感器、存储器、显示屏等。


一、SPI协议

1.1 简介

SPI是由Motorola公司开发的高速同步串行通信协议,具有以下特点:

特性描述
全双工同时发送和接收
同步需要时钟信号
主从模式一个主机,多个从机
高速可达数十MHz
四线制SCK, MOSI, MISO, SS

1.2 信号线定义

信号名称方向功能
SCKSerial Clock主→从时钟信号
MOSIMaster Out Slave In主→从主机发送数据
MISOMaster In Slave Out从→主从机发送数据
SS/CSSlave Select主→从片选信号(低有效)

1.3 工作模式

SPI有4种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:

模式CPOLCPHA空闲电平采样边沿
Mode 000上升沿
Mode 101下降沿
Mode 210下降沿
Mode 311上升沿
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Mode 0 (CPOL=0, CPHA=0):
    ___     ___     ___     ___
___|   |___|   |___|   |___|   |___  SCK
      ↑       ↑       ↑       ↑
    采样    采样    采样    采样

Mode 1 (CPOL=0, CPHA=1):
    ___     ___     ___     ___
___|   |___|   |___|   |___|   |___  SCK
          ↓       ↓       ↓       ↓
        采样    采样    采样    采样

1.4 数据传输时序

1
2
3
4
5
6
7
8
9
        ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
SCK  ───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───
        
SS   ─────┐                                                       ─────
          └─────────────────────────────────────────────────────────┘
        
MOSI ─────┤ D7  ├───┤ D6  ├───┤ D5  ├───┤ D4  ├───┤ D3  ├───┤ D2  ├───┤ ...
        
MISO ─────┤ D7  ├───┤ D6  ├───┤ D5  ├───┤ D4  ├───┤ D3  ├───┤ D2  ├───┤ ...

1.5 多从机连接

方式一:独立片选

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
        ┌────────┐
        │ Master │
        └───┬────┘
    ┌───────┼───────┬───────┐
   SCK    MOSI    MISO     │
    │       │       │       │
    ├───────┼───────┼───┐   │
    │       │       │   │   │
    │      ┌┴───────┴┐  │   │
    │      │ Slave 1 │  │   │
    │      └─────────┘  │   │
    │       ↑ SS1       │   │
    │                   │   │
    ├───────┼───────┼───┼───┤
    │      ┌┴───────┴┐  │   │
    │      │ Slave 2 │  │   │
    │      └─────────┘  │   │
    │       ↑ SS2       │   │
    └────────────────────┴───┘

方式二:菊花链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        ┌────────┐
        │ Master │
        └───┬────┘
    ┌───────┴───────┐
   SCK    MOSI    MISO
    │       │       │
    │   ┌───┴───┐   │
    │   │Slave 1│   │
    │   └───┬───┘   │
    │       │       │
    │   ┌───┴───┐   │
    │   │Slave 2│   │
    │   └───┬───┘   │
    │       │       │
    └───────┴───────┘

1.6 优缺点

优点:

  • 速度快,可达50MHz以上
  • 全双工通信
  • 协议简单,硬件实现容易
  • 无需从机地址

缺点:

  • 需要更多引脚(至少4根)
  • 没有确认机制
  • 没有标准帧格式
  • 传输距离有限

二、I2C协议

2.1 简介

I2C是由Philips公司开发的两线式串行通信协议:

特性描述
半双工不能同时收发
同步需要时钟信号
多主机支持多主机模式
寻址7位/10位地址
低速标准模式100kHz,快速模式400kHz

2.2 信号线定义

信号名称功能
SDASerial Data数据线(双向)
SCLSerial Clock时钟线(主机控制)

重要特性:开漏输出 + 上拉电阻

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        VCC
        ┌┴┐
        │ │ R_p (上拉电阻)
        └┬┘
    ─────┼───── SDA/SCL
    ┌────┴────┐
    │  开漏   │
    │  输出   │
    └─────────┘

2.3 信号时序

起始条件和停止条件

1
2
3
4
5
6
7
8
9
起始条件(S):SCL高电平时,SDA下降沿
停止条件(P):SCL高电平时,SDA上升沿

        ┌───┐               ┌───┐
SCL  ───┘   └───────────────┘   └───
            │               │
SDA  ───────┐           ┌───┘
            │           │
            S           P

数据传输

1
2
3
4
5
6
7
8
数据在SCL低电平时改变,高电平时稳定

        ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
SCL  ───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───┘   └───
        
SDA  ────┤ D7  ├───────┤ D6  ├───────┤ D5  ├───────┤ D4  ├─── ...
            ↑               ↑               ↑
          采样            采样            采样

2.4 数据帧格式

标准写操作

1
2
3
4
5
6
7
┌───┬─────────┬───┬─────────┬───┬─────────┬───┐
│ S │ 7位地址 │ W │ ACK │ 8位数据 │ ACK │ P │
└───┴─────────┴───┴─────────┴───┴─────────┴───┘
    └────┬────┘
      主机发送
              └────┬────┘
                从机应答

标准读操作

1
2
3
4
5
6
7
┌───┬─────────┬───┬───┬─────────┬───┬───┐
│ S │ 7位地址 │ R │ ACK │ 8位数据 │NACK│ P │
└───┴─────────┴───┴─────────┴───┴───┘
    └────┬────┘          └────┬────┘
      主机发送             从机发送
              └────┬────┘
                主机应答

ACK/NACK规则:

  • ACK:接收方拉低SDA
  • NACK:最后一个字节后,主机不拉低SDA

2.5 多主机仲裁

I2C支持多主机,使用仲裁机制:

1
2
3
4
5
主机A发送: 1 0 1 1 ...
主机B发送: 1 0 0 1 ...
            主机B检测到SDA为高(自己发0,但线为1)
            主机B失去仲裁,释放总线

仲裁原则:发送0的主机优先级高。

2.6 时序参数

参数标准模式快速模式快速模式+
最高频率100 kHz400 kHz1 MHz
SCL低电平时间4.7 μs1.3 μs0.5 μs
SCL高电平时间4.0 μs0.6 μs0.26 μs
起始条件保持时间4.0 μs0.6 μs0.26 μs
停止条件建立时间4.0 μs0.6 μs0.26 μs

2.7 优缺点

优点:

  • 只需2根线
  • 支持多主机
  • 内置应答机制
  • 有标准帧格式

缺点:

  • 速度慢
  • 半双工
  • 需要上拉电阻
  • 地址冲突问题

三、SPI vs I2C 对比

3.1 总体对比

特性SPII2C
信号线数量4+2
全双工/半双工全双工半双工
最大速度50+ MHz1 MHz(高速模式)
多主机支持不支持支持
地址机制片选线地址位
应答机制
传输距离
复杂度简单中等

3.2 选型建议

场景推荐原因
高速数据采集SPI速度快,全双工
多传感器系统I2C引脚少,易扩展
大容量FlashSPI速度快
EEPROM配置存储I2C简单,够用
OLED显示屏SPI/I2C取决于刷新率要求
IMU传感器SPI高速数据流

四、代码实现

4.1 STM32 HAL库 SPI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SPI初始化
SPI_HandleTypeDef hspi1;

void SPI1_Init(void) {
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;    // CPOL=0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;        // CPHA=0
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi1);
}

// SPI收发数据
uint8_t SPI_TransmitReceive(uint8_t data) {
    uint8_t rxData;
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);  // CS拉低
    HAL_SPI_TransmitReceive(&hspi1, &data, &rxData, 1, 100);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);    // CS拉高
    return rxData;
}

// 读取寄存器
uint8_t SPI_ReadReg(uint8_t reg) {
    uint8_t txData[2] = {reg | 0x80, 0xFF};  // 读命令
    uint8_t rxData[2];
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 2, 100);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    return rxData[1];
}

4.2 STM32 HAL库 I2C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// I2C初始化
I2C_HandleTypeDef hi2c1;

void I2C1_Init(void) {
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;  // 100kHz
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

// I2C扫描
void I2C_Scan(void) {
    for (uint8_t addr = 0; addr < 127; addr++) {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 1, 10) == HAL_OK) {
            printf("Found device at 0x%02X\n", addr);
        }
    }
}

// I2C写数据
HAL_StatusTypeDef I2C_WriteBytes(uint8_t devAddr, uint8_t reg, 
                                   uint8_t *data, uint16_t len) {
    uint8_t buf[len + 1];
    buf[0] = reg;
    memcpy(&buf[1], data, len);
    return HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, buf, len + 1, 100);
}

// I2C读数据
HAL_StatusTypeDef I2C_ReadBytes(uint8_t devAddr, uint8_t reg,
                                  uint8_t *data, uint16_t len) {
    HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, &reg, 1, 100);
    return HAL_I2C_Master_Receive(&hi2c1, devAddr << 1, data, len, 100);
}

4.3 Arduino 示例

SPI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <SPI.h>

const int CS_PIN = 10;

void setup() {
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);
    SPI.begin();
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
}

uint8_t spi_transfer(uint8_t data) {
    digitalWrite(CS_PIN, LOW);
    uint8_t rx = SPI.transfer(data);
    digitalWrite(CS_PIN, HIGH);
    return rx;
}

I2C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <Wire.h>

#define DEVICE_ADDR 0x68

void setup() {
    Wire.begin();
    Serial.begin(9600);
}

void i2c_write(uint8_t reg, uint8_t data) {
    Wire.beginTransmission(DEVICE_ADDR);
    Wire.write(reg);
    Wire.write(data);
    Wire.endTransmission();
}

uint8_t i2c_read(uint8_t reg) {
    Wire.beginTransmission(DEVICE_ADDR);
    Wire.write(reg);
    Wire.endTransmission();
    
    Wire.requestFrom(DEVICE_ADDR, 1);
    return Wire.read();
}

五、常见问题与解决

5.1 SPI常见问题

问题原因解决方法
读出全0或全1模式不匹配检查CPOL/CPHA设置
数据错位时序问题检查波特率和边沿
通信不稳定线路干扰缩短线长,加屏蔽
从机不响应CS未拉低检查片选逻辑

5.2 I2C常见问题

问题原因解决方法
找不到设备地址错误I2C扫描确认地址
总线挂死无ACK添加超时和恢复机制
通信失败上拉电阻不当检查上拉电阻值(2.2k-10k)
数据错误时序问题降低时钟频率

5.3 I2C总线恢复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// I2C总线恢复(时钟脉冲法)
void I2C_BusRecovery(void) {
    // 将SDA/SCL设为GPIO输出
    // SCL输出9个时钟脉冲
    for (int i = 0; i < 9; i++) {
        HAL_GPIO_WritePin(GPIOB, SCL_PIN, GPIO_PIN_RESET);
        HAL_Delay(1);
        HAL_GPIO_WritePin(GPIOB, SCL_PIN, GPIO_PIN_SET);
        HAL_Delay(1);
    }
    // 发送停止条件
    HAL_GPIO_WritePin(GPIOB, SDA_PIN, GPIO_PIN_RESET);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, SCL_PIN, GPIO_PIN_SET);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, SDA_PIN, GPIO_PIN_SET);
    // 重新初始化I2C
    HAL_I2C_DeInit(&hi2c1);
    I2C1_Init();
}

六、实际应用示例

6.1 SPI驱动TFT显示屏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 发送命令
void TFT_WriteCommand(uint8_t cmd) {
    DC_LOW();    // 数据/命令选择
    CS_LOW();
    SPI_Transmit(cmd);
    CS_HIGH();
}

// 发送数据
void TFT_WriteData(uint8_t data) {
    DC_HIGH();
    CS_LOW();
    SPI_Transmit(data);
    CS_HIGH();
}

// 设置窗口
void TFT_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
    TFT_WriteCommand(0x2A);  // CASET
    TFT_WriteData(x1 >> 8);
    TFT_WriteData(x1 & 0xFF);
    TFT_WriteData(x2 >> 8);
    TFT_WriteData(x2 & 0xFF);
    
    TFT_WriteCommand(0x2B);  // RASET
    TFT_WriteData(y1 >> 8);
    TFT_WriteData(y1 & 0xFF);
    TFT_WriteData(y2 >> 8);
    TFT_WriteData(y2 & 0xFF);
    
    TFT_WriteCommand(0x2C);  // RAMWR
}

6.2 I2C驱动MPU6050

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define MPU6050_ADDR 0x68

// 初始化
void MPU6050_Init(void) {
    I2C_WriteBytes(MPU6050_ADDR, 0x6B, 0x00);  // 唤醒
    I2C_WriteBytes(MPU6050_ADDR, 0x19, 0x07);  // 采样率分频
    I2C_WriteBytes(MPU6050_ADDR, 0x1A, 0x00);  // 配置
    I2C_WriteBytes(MPU6050_ADDR, 0x1B, 0x00);  // 陀螺仪量程
    I2C_WriteBytes(MPU6050_ADDR, 0x1C, 0x00);  // 加速度量程
}

// 读取加速度
void MPU6050_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az) {
    uint8_t buf[6];
    I2C_ReadBytes(MPU6050_ADDR, 0x3B, buf, 6);
    *ax = (buf[0] << 8) | buf[1];
    *ay = (buf[2] << 8) | buf[3];
    *az = (buf[4] << 8) | buf[5];
}

总结

SPI选择场景:

  • 需要高速传输
  • 数据量大
  • 引脚资源充足
  • 单主机多从机

I2C选择场景:

  • 引脚资源紧张
  • 多传感器系统
  • 低速配置读写
  • 需要多主机

参考资料


🎯 SPI和I2C是嵌入式开发必备技能,理解它们的区别和应用场景非常重要!

评论

留言经审核后显示,欢迎留下想法。