3. 传感器数据采集:I2C/SPI总线协议详解、寄存器配置与数据读取实战

好,咱们进入第三章。这一章我打算跟你好好聊聊传感器数据采集的核心——I2C和SPI总线。说实话,这两个协议是嵌入式世界里最常用的“对话方式”。你想想看,一个传感器芯片,不管它多智能,总得跟主控芯片说话吧?怎么说话?就是靠这两条总线。

我个人习惯把I2C比作“邮局系统”,把SPI比作“专线电话”。为什么这么比?咱们往下看。

3.1 I2C总线:两根线的艺术

I2C,全称Inter-Integrated Circuit,是飞利浦公司(现在的NXP)在80年代搞出来的。它只用两根线:SCL(时钟线)和SDA(数据线)。

核心特点:

  • 主从架构:一个主设备(通常是MCU),多个从设备(传感器们)。
  • 7位或10位地址:每个从设备有唯一地址。7位地址理论上可以挂127个设备(0x00保留)。
  • 半双工:同一时刻只能一个方向传输数据。
  • 速率:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。

避坑指南:我曾经在一个项目里同时挂了5个I2C传感器,结果通信老是出错。查了半天,发现是总线电容太大,上拉电阻没选对。记住:I2C总线的上拉电阻值跟总线电容和通信速率直接相关。100kHz下,4.7kΩ通常没问题;400kHz下,可能得降到2.2kΩ甚至1kΩ。

3.2 I2C通信时序:看懂波形图

I2C的通信过程其实不复杂。我刚开始学的时候,觉得时序图很吓人。后来发现,只要抓住几个关键点就行。

基本时序:

  1. 起始条件:SCL高电平时,SDA从高变低。
  2. 从设备地址+读写位:主设备发送7位地址,最后1位是读写标志(0=写,1=读)。
  3. 应答位(ACK):从设备拉低SDA表示收到。
  4. 数据字节:每个字节8位,后面跟一个ACK。
  5. 停止条件:SCL高电平时,SDA从低变高。

嗯,这里要注意:读操作和写操作的流程不太一样。写操作简单,发地址、发寄存器地址、发数据。读操作呢?得先“伪写”一次,告诉传感器你要读哪个寄存器,然后再重新发起始条件,发读命令。

// I2C写寄存器示例(伪代码)
uint8_t i2c_write_register(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) {
    i2c_start();                    // 起始条件
    i2c_send_byte(dev_addr << 1);  // 设备地址 + 写位(0)
    if (!i2c_wait_ack()) return -1; // 等待应答
    i2c_send_byte(reg_addr);        // 寄存器地址
    if (!i2c_wait_ack()) return -1;
    i2c_send_byte(data);            // 要写入的数据
    if (!i2c_wait_ack()) return -1;
    i2c_stop();                     // 停止条件
    return 0;
}

// I2C读寄存器示例
uint8_t i2c_read_register(uint8_t dev_addr, uint8_t reg_addr) {
    uint8_t data;
    i2c_start();
    i2c_send_byte(dev_addr << 1);  // 写操作
    i2c_wait_ack();
    i2c_send_byte(reg_addr);        // 指定寄存器
    i2c_wait_ack();
    i2c_start();                    // 重复起始条件
    i2c_send_byte((dev_addr << 1) | 0x01); // 读操作
    i2c_wait_ack();
    data = i2c_read_byte();         // 读取数据
    i2c_send_nack();                // 发送非应答
    i2c_stop();
    return data;
}

我的经验:实际项目中,我很少用软件模拟I2C。除非是低成本的MCU没有硬件I2C外设。硬件I2C有FIFO、有中断、有DMA支持,效率高得多。但调试的时候,用逻辑分析仪抓波形,软件模拟反而更容易理解。

3.3 SPI总线:速度至上

SPI,Serial Peripheral Interface,摩托罗拉发明的。它比I2C快得多,但用的线也多。

四根线:

  • SCLK:时钟线,由主设备产生。
  • MOSI:主出从入(Master Out Slave In)。
  • MISO:主入从出(Master In Slave Out)。
  • CS/SS:片选线,低电平有效。每个从设备一根。

说白了,SPI就是全双工的移位寄存器。主设备每发一个时钟脉冲,MOSI上送出一位数据,同时MISO上接收一位数据。所以SPI的收发是同步进行的。

四种模式:

模式 CPOL(时钟极性) CPHA(时钟相位) 说明
模式0 0 0 空闲低电平,第一个边沿采样
模式1 0 1 空闲低电平,第二个边沿采样
模式2 1 0 空闲高电平,第一个边沿采样
模式3 1 1 空闲高电平,第二个边沿采样

重要提醒:SPI模式不匹配是新手最容易犯的错误。我曾经调试一个加速度传感器,读出来的数据全是0xFF。折腾了两天,最后发现是CPHA设反了。传感器手册里写的是“Data captured on rising edge”,我设成了模式3,应该是模式0。记住:一定要仔细看数据手册里的时序图!

3.4 寄存器配置实战:以MPU6050为例

光讲理论没意思,咱们来点实战。以经典的MPU6050六轴传感器为例,看看怎么配置它的寄存器。

MPU6050的I2C地址是0x68(AD0引脚接地时)。它内部有上百个寄存器,我们常用的就几个。

关键寄存器:

寄存器地址 名称 功能
0x6B PWR_MGMT_1 电源管理,复位后需要唤醒
0x1B GYRO_CONFIG 陀螺仪量程配置
0x1C ACCEL_CONFIG 加速度计量程配置
0x3B ACCEL_XOUT_H 加速度计X轴数据高字节(起始地址)
// MPU6050初始化代码(基于STM32 HAL库)
void MPU6050_Init(void) {
    uint8_t data;
    
    // 1. 唤醒传感器:清除SLEEP位
    data = 0x00;
    HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
    
    // 2. 配置陀螺仪量程:±2000°/s
    data = 0x18;  // 0x18 = ±2000°/s
    HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
    
    // 3. 配置加速度计量程:±2g
    data = 0x00;  // 0x00 = ±2g
    HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
    
    // 4. 配置数字低通滤波器(DLPF):带宽44Hz
    data = 0x03;  // 0x03 = 44Hz
    HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1A, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
}

// 读取加速度计数据
void MPU6050_Read_Accel(int16_t *ax, int16_t *ay, int16_t *az) {
    uint8_t buf[6];
    HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 6, 100);
    
    *ax = (buf[0] << 8) | buf[1];
    *ay = (buf[2] << 8) | buf[3];
    *az = (buf[4] << 8) | buf[5];
}

关键点:注意MPU6050的数据是16位有符号整数,高字节在前(Big Endian)。读取时先读高字节,再读低字节。另外,I2C读操作支持“连续读取”,你只需要指定起始寄存器地址,传感器会自动递增地址。这比一个一个读快多了。

3.5 I2C vs SPI:怎么选?

这个问题我经常被问到。我的回答是:看场景。

选I2C的情况:

  • 引脚紧张,只能省出两根线。
  • 需要挂多个从设备,且速率要求不高。
  • 传感器距离主控较远(I2C抗干扰能力相对好一些)。

选SPI的情况:

  • 需要高速数据采集(比如摄像头、高刷新率IMU)。
  • 数据量大的传感器(比如24位ADC)。
  • 从设备数量少,引脚够用。

我个人习惯:温度、湿度、气压这类慢变信号用I2C;加速度计、陀螺仪、磁力计这类需要高速采样的用SPI。当然,很多传感器两种接口都支持,那就看你的MCU资源了。

3.6 知识体系总览

下面这张图是我画的,把本章的核心内容串起来了。你一看就明白。

传感器数据采集:总线协议与实战 I2C总线 2线:SCL + SDA 半双工 速率:100k~3.4MHz 7位地址,多从机 SPI总线 4线:SCLK/MOSI/MISO/CS 全双工 速率可达数十MHz 4种模式(CPOL/CPHA) 实战:MPU6050传感器 寄存器配置 数据读取 避坑指南 核心:理解时序 → 配置寄存器 → 读取数据 → 融合算法

这张图把I2C和SPI的对比、实战要点都画出来了。你保存下来,以后做项目时对照着看。

3.7 一些掏心窝的话

做传感器数据采集,说白了就是跟寄存器打交道。你配置对了,数据就对了;配置错了,后面算法再好也是白搭。

我见过太多人,算法写得天花乱坠,结果传感器初始化就错了。加速度计的量程设错了,陀螺仪的带宽没配好,出来的数据全是噪声。嗯,这种问题查起来最头疼。

所以我的建议是:拿到一个新传感器,先别急着写算法。花半天时间,用逻辑分析仪抓一下I2C或SPI的波形,确认读写操作正确。这一步省不了。

最后一个小技巧:调试I2C/SPI时,我习惯先读传感器的WHO_AM_I寄存器(或者叫Device ID)。这个寄存器是只读的,值固定。如果能正确读出来,说明通信链路没问题。然后再去配置其他寄存器。这个习惯帮我省了不少排查时间。


公众号:蓝海资料掘金营,微信deep3321