第四节:数字传感器接口(SPI)——让MPU6050开口说话

SPI这个接口,说实话,是我在嵌入式开发中用得最多的同步串行协议之一。它不像I2C那样需要地址来区分设备,也不像UART那样需要双方约定波特率。SPI的哲学很简单——谁掌握时钟,谁就掌握话语权。

今天我们就来彻底搞懂SPI,然后用ESP32把MPU6050的加速度数据读出来。嗯,这个传感器我当年做平衡小车时调了整整两天,踩过的坑今天一并告诉你。

4.1 SPI协议到底在说什么?

SPI全称是Serial Peripheral Interface,说白了就是「主设备发时钟,从设备回数据」的机制。它用四根线搞定一切:

  • SCLK:时钟线,由主机控制
  • MOSI:主机输出,从机输入
  • MISO:主机输入,从机输出
  • CS:片选线,低电平有效

你想想看,这四根线里,CS决定了「跟谁说话」,SCLK决定了「什么时候说话」,MOSI和MISO则是「说什么」。多简洁的设计。

核心要点:SPI是全双工通信。主机发一个bit的同时,从机也回一个bit。所以一次SPI传输,你发出去8位,也必然收回来8位——哪怕你根本不在乎从机回了什么。

我在项目中遇到过一位同事,他用SPI读传感器时只发了命令字节,没去读MISO上的数据,结果发现传感器一直没响应。后来我告诉他:SPI的收发是绑定的,你发命令的同时,从机可能正在回状态码。你不读,数据就丢了。

4.2 四种模式——别让时钟相位坑了你

SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)组合决定。我刚开始学的时候也觉得这玩意儿绕,后来画了个时序图就全明白了。

模式 CPOL CPHA 时钟空闲电平 数据采样边沿
模式0 0 0 低电平 上升沿
模式1 0 1 低电平 下降沿
模式2 1 0 高电平 下降沿
模式3 1 1 高电平 上升沿

怎么记?我个人习惯这样:CPOL=0,空闲时时钟是低;CPOL=1,空闲时时钟是高。CPHA=0,第一个边沿采样;CPHA=1,第二个边沿采样。

MPU6050默认工作在模式0(CPOL=0, CPHA=0)。为什么?因为大多数SPI从机都默认用模式0,兼容性最好。但注意,不是所有传感器都这样——我曾经被一个温湿度传感器坑过,它非要模式3才能工作,我查了三天手册才发现。

避坑指南:拿到一个新传感器,第一件事就是看数据手册里的SPI时序图。确认CPOL和CPHA,否则数据全是乱的。我曾经因为没看时序,读出来的加速度值一直在跳,还以为是传感器坏了。

4.3 MPU6050的SPI接口——不只是加速度

MPU6050其实是个六轴传感器:三轴加速度 + 三轴陀螺仪。它同时支持I2C和SPI,但SPI模式下速率更高,能达到1MHz以上。我个人做运动追踪项目时,一律用SPI——数据量大,I2C那400kHz的速率真不够用。

它的SPI寄存器地址是8位的,但注意:读操作时,地址最高位要置1;写操作时,地址最高位要置0。这是MPU6050的规矩,很多SPI器件都这么干。

举个例子:你要读加速度X轴的高8位,寄存器地址是0x3B。那么发送的地址字节应该是0x3B | 0x80 = 0xBB。

4.4 实战:ESP32读取MPU6050加速度数据

好,理论说完了,咱们直接上代码。我用的是ESP32的HSPI接口,默认引脚是:

  • SCLK: GPIO14
  • MOSI: GPIO13
  • MISO: GPIO12
  • CS: GPIO15

接线很简单,VCC接3.3V,GND接GND,剩下四根线一一对应。注意MPU6050的AD0引脚——如果接GND,I2C地址是0x68;接VCC是0x69。但我们用SPI,这个引脚不用管。

个人经验:ESP32的SPI引脚可以任意映射,但我建议用默认的HSPI或VSPI引脚组。自己乱映射的话,容易遇到信号完整性问题,尤其是高速率下。

下面是完整的初始化代码:

#include <SPI.h>

#define MPU6050_CS 15
#define MPU6050_WHO_AM_I 0x75
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_PWR_MGMT_1 0x6B

void setup() {
  Serial.begin(115200);
  
  // 初始化SPI,模式0,最高频率1MHz
  SPI.begin();
  SPI.setDataMode(SPI_MODE0);
  SPI.setClockDivider(SPI_CLOCK_DIV8); // 约1MHz
  
  pinMode(MPU6050_CS, OUTPUT);
  digitalWrite(MPU6050_CS, HIGH);
  
  // 检查设备ID
  uint8_t whoami = spiReadRegister(MPU6050_WHO_AM_I);
  Serial.printf("WHO_AM_I = 0x%02X\n", whoami);
  if (whoami != 0x68) {
    Serial.println("MPU6050未找到!");
    while(1);
  }
  
  // 唤醒MPU6050(默认是睡眠状态)
  spiWriteRegister(MPU6050_PWR_MGMT_1, 0x00);
  delay(100);
}

// SPI读一个寄存器
uint8_t spiReadRegister(uint8_t reg) {
  uint8_t data;
  digitalWrite(MPU6050_CS, LOW);
  SPI.transfer(reg | 0x80);  // 读命令,地址最高位置1
  data = SPI.transfer(0x00); // 发送空字节,接收数据
  digitalWrite(MPU6050_CS, HIGH);
  return data;
}

// SPI写一个寄存器
void spiWriteRegister(uint8_t reg, uint8_t value) {
  digitalWrite(MPU6050_CS, LOW);
  SPI.transfer(reg & 0x7F);  // 写命令,地址最高位置0
  SPI.transfer(value);
  digitalWrite(MPU6050_CS, HIGH);
}

读加速度数据时,MPU6050的加速度寄存器是连续排列的:从0x3B到0x40,共6个字节(X、Y、Z各16位)。我们可以一次读6个字节,效率更高:

void readAccelData(int16_t *ax, int16_t *ay, int16_t *az) {
  uint8_t buf[6];
  
  digitalWrite(MPU6050_CS, LOW);
  SPI.transfer(MPU6050_ACCEL_XOUT_H | 0x80); // 从0x3B开始读
  for (int i = 0; i < 6; i++) {
    buf[i] = SPI.transfer(0x00);
  }
  digitalWrite(MPU6050_CS, HIGH);
  
  // 合并高8位和低8位
  *ax = (buf[0] << 8) | buf[1];
  *ay = (buf[2] << 8) | buf[3];
  *az = (buf[4] << 8) | buf[5];
}

void loop() {
  int16_t ax, ay, az;
  readAccelData(&ax, &ay, &az);
  
  Serial.printf("ACCEL: X=%d, Y=%d, Z=%d\n", ax, ay, az);
  delay(100);
}

跑起来之后,你会看到三个轴的数据在串口里刷刷地跳。静止时,Z轴应该接近16384(对应1g重力加速度),X和Y接近0。如果数值不对,先检查接线,再检查SPI模式——我敢打赌,90%的问题出在模式配置上。

4.5 知识体系一览

下面这张图是我自己画的SPI + MPU6050的知识结构,你看一眼就能把今天的内容串起来:

SPI + MPU6050 知识体系 SPI协议基础 SCLK时钟 MOSI数据 MISO数据 CS片选 四种模式 模式0 模式1 模式2 模式3 MPU6050 实战:ESP32读取加速度 初始化SPI 唤醒传感器 连续读6字节 合并高低位 避坑要点 • 先查手册确认模式 • 读地址最高位置1 • 写地址最高位置0 • 收发是绑定的 • 片选必须拉低

这张图把SPI协议、四种模式、MPU6050实战和避坑要点都串起来了。你照着这个结构去学,思路会清晰很多。

最后说一句:SPI其实不难,难的是你拿到一个新器件时,能不能快速从手册里找到CPOL、CPHA和寄存器地址。我个人的习惯是:先读WHO_AM_I寄存器确认通信正常,再干别的。这一步能帮你排除90%的硬件问题。

好了,MPU6050的加速度数据你已经能读到了。陀螺仪的数据读取方式完全一样,只是寄存器地址不同。自己试试看?

可可精品资料库