3、SPI通信协议:SPI总线原理、SPI四种模式、SPI与I2C对比、SPI在传感器中的应用

好,咱们今天聊聊SPI。这个协议在传感器世界里太常见了,尤其是那些需要高速数据吞吐的场合。我刚开始做嵌入式那会儿,总觉得SPI比I2C难搞,后来发现,其实就是几条线的事儿,摸透了就很简单。

3.1 SPI总线原理:主从之间的“全双工”对话

SPI的全称是Serial Peripheral Interface,串行外设接口。它由摩托罗拉公司推出,现在已经是嵌入式领域的标配了。

说白了,SPI就是一个主设备和一个或多个从设备之间的高速同步通信。它用四根线:

  • SCLK(串行时钟):由主设备产生,控制数据传输的节奏。
  • MOSI(主出从入):主设备发送数据,从设备接收。
  • MISO(主入从出):从设备发送数据,主设备接收。
  • SS(从设备选择):片选信号,低电平有效。主设备拉低哪个从设备的SS,就跟谁通信。

这里有个关键点:SPI是全双工的。什么意思?就是主设备发数据的同时,从设备也在发数据。你想想看,这比I2C那种半双工(发的时候不能收)要快得多。

核心要点:SPI的通信本质是一个“移位寄存器”的交换过程。主设备每发出一个时钟脉冲,MOSI线上就移出一位数据,同时MISO线上移入一位数据。8个时钟脉冲后,一个字节就交换完了。

我在项目中遇到过一个问题:某款加速度传感器,用I2C读取时总是丢数据,换成SPI后,数据稳稳的。为什么?因为SPI没有I2C那种复杂的应答机制,时钟一给,数据就出来了,干净利落。

3.2 SPI四种模式:CPOL和CPHA的排列组合

SPI有四种模式,由两个参数决定:时钟极性(CPOL)时钟相位(CPHA)

  • CPOL:决定空闲时时钟线的电平。CPOL=0,空闲低电平;CPOL=1,空闲高电平。
  • CPHA:决定数据在哪个时钟沿被采样。CPHA=0,第一个时钟沿采样;CPHA=1,第二个时钟沿采样。

四种模式如下表:

模式 CPOL CPHA 采样沿 数据变化沿
模式0 0 0 上升沿 下降沿
模式1 0 1 下降沿 上升沿
模式2 1 0 下降沿 上升沿
模式3 1 1 上升沿 下降沿

嗯,这里要注意:绝大多数传感器默认使用模式0或模式3。我个人习惯是,拿到一个新传感器,先看数据手册里的时序图,确认它用哪种模式。千万别想当然。

避坑指南:我曾经因为SPI模式没配对,折腾了整整一个下午。传感器读出来的数据全是0xFF,我还以为是芯片坏了。后来用逻辑分析仪一看,时钟沿和数据完全对不上。从那以后,我每次初始化SPI之前,都会先确认CPOL和CPHA。

3.3 SPI与I2C对比:各有千秋

很多初学者会问:SPI和I2C到底选哪个?我的回答是:看需求。

下面这张表,是我自己总结的对比:

特性 SPI I2C
线数 4根(SCLK, MOSI, MISO, SS) 2根(SCL, SDA)
通信方式 全双工 半双工
速度 快(可达几十MHz) 慢(标准模式100kHz,快速模式400kHz)
从设备选择 硬件片选(每设备一根SS线) 软件寻址(7位或10位地址)
从设备数量 受限于主设备GPIO数量 理论上127个(7位地址)
硬件复杂度 简单,无上拉电阻 需要上拉电阻
适用场景 高速、大数据量(如ADC、显示屏) 低速、小数据量、多设备(如温湿度传感器)

你想想看,如果你的电动牙刷里只有一个加速度传感器,而且需要实时采集数据,那SPI是首选。但如果牙刷里要挂好几个传感器(比如加速度、陀螺仪、压力传感器),而且对速度要求不高,I2C可能更省引脚。

我的建议:在电动牙刷这种对功耗和体积敏感的设备里,我倾向于用SPI。因为SPI没有I2C那种上拉电阻的静态功耗,而且速度更快,可以让传感器尽快进入休眠状态。

3.4 SPI在传感器中的应用:以MPU6050为例

咱们拿一个常见的传感器——MPU6050(六轴惯性测量单元)来举例。它既支持I2C,也支持SPI。但用SPI时,数据输出速率可以更高。

下面是一个典型的SPI读取流程:

// 伪代码:SPI读取MPU6050加速度数据
// 假设SPI已初始化为模式0,时钟频率1MHz

uint8_t spi_read_register(uint8_t reg) {
    uint8_t data;
    
    // 拉低片选,开始通信
    CS_LOW();
    
    // 发送寄存器地址(最高位为1表示读操作)
    spi_transfer(reg | 0x80);
    
    // 读取数据
    data = spi_transfer(0x00);
    
    // 拉高片选,结束通信
    CS_HIGH();
    
    return data;
}

// 读取加速度X轴高8位
uint8_t accel_x_high = spi_read_register(0x3B);

这段代码看起来简单,但有几个坑:

  • 片选时序:每次通信前拉低CS,通信结束后拉高。中间不要有其他操作。
  • 寄存器地址:读操作时,地址最高位要置1。写操作时置0。这个细节很容易忘。
  • 连续读取:很多传感器支持连续读取模式,即发一个地址后,连续读多个字节。这时CS要保持低电平。

实战经验:我在做一款运动手环时,用SPI读取MPU6050的数据。一开始用I2C,发现数据更新率只能到100Hz,换成SPI后直接飙到1kHz。但代价是多用了两根线。嗯,这就是取舍。

最后说一句:SPI虽然快,但抗干扰能力不如I2C。如果你的电动牙刷里有电机这种强干扰源,SPI线要尽量短,最好加个屏蔽。我曾经因为SPI线太长,导致数据错位,后来把线剪短了一半,问题就解决了。

好了,SPI就聊到这儿。下一节咱们讲I2C,到时候再对比着看,你会理解得更深。