2、硬件接口与通信协议:I2C协议详解、SPI协议详解、传感器寄存器读写实战

好,咱们进入正题。做可穿戴设备,传感器是核心,而传感器怎么跟主控芯片“说话”?靠的就是通信协议。说白了,就是定好规矩,谁先讲、讲多快、讲完怎么收尾。

我这些年调过的传感器,少说也有几十种。最常用的就是I2C和SPI。这两个协议,你玩透了,市面上九成的传感器都能搞定。今天我就把压箱底的经验掏出来,跟你聊聊。

2.1 I2C协议详解

I2C,全称是Inter-Integrated Circuit。我习惯叫它“I方C”。它最大的特点就是省引脚——只需要两根线:SCL(时钟线)和SDA(数据线)。

你想想看,一个主控芯片要挂好几个传感器,如果用SPI,每个传感器得多占一根片选线。但I2C呢?两根线搞定所有。这就是为什么心率血氧传感器几乎清一色用I2C——省引脚啊!

2.1.1 物理层与电气特性

  • SCL:时钟线,由主设备控制。我一般用400kHz,这是快速模式。老一点的传感器可能只支持100kHz标准模式。
  • SDA:数据线,双向传输。主设备发地址、写数据,从设备回数据。
  • 上拉电阻:这个很重要。两根线必须接上拉电阻到VCC。我常用的阻值是4.7kΩ。如果总线电容大,可以换成2.2kΩ。
注意: 我曾经在一个项目里,因为上拉电阻选太大(10kΩ),导致信号上升沿太慢,通信时好时坏。查了两天才找到原因。嗯,这种坑踩过一次就记住了。

2.1.2 通信时序

I2C的通信流程,我总结成四步走:

  1. 起始条件:SCL高电平时,SDA从高变低。这是“我要开始说话了”的信号。
  2. 发送从设备地址:7位地址 + 1位读写位。比如MAX30102心率传感器的地址是0x57(7位),左移一位变成0xAE(写)或0xAF(读)。
  3. 数据传输:每8位数据后跟一个应答位(ACK)。从设备拉低SDA表示“收到了”。
  4. 停止条件:SCL高电平时,SDA从低变高。表示“我说完了”。

这里有个细节:地址发送后,如果从设备没应答(SDA没被拉低),说明设备没在线或者地址错了。我调试时经常用逻辑分析仪抓这个信号,一眼就能看出问题。

2.1.3 读写操作示例

以MAX30102为例,写一个寄存器:

// 写寄存器:先发设备地址(写),再发寄存器地址,最后发数据
uint8_t write_reg(uint8_t reg_addr, uint8_t data) {
    i2c_start();                    // 起始条件
    i2c_send_byte(0xAE);           // 设备地址 + 写位
    if (!i2c_wait_ack()) return 1; // 等待应答
    i2c_send_byte(reg_addr);       // 寄存器地址
    i2c_wait_ack();
    i2c_send_byte(data);           // 要写入的数据
    i2c_wait_ack();
    i2c_stop();                    // 停止条件
    return 0;
}

读寄存器稍微复杂一点:要先写寄存器地址,再重新发起始条件,然后读数据。

// 读寄存器:先写寄存器地址,再重新开始读
uint8_t read_reg(uint8_t reg_addr) {
    uint8_t data;
    i2c_start();
    i2c_send_byte(0xAE);           // 写地址
    i2c_wait_ack();
    i2c_send_byte(reg_addr);       // 指定寄存器
    i2c_wait_ack();
    i2c_start();                   // 重新起始条件
    i2c_send_byte(0xAF);           // 读地址
    i2c_wait_ack();
    data = i2c_read_byte();        // 读取数据
    i2c_send_nack();               // 发送非应答
    i2c_stop();
    return data;
}
实战技巧: 我习惯在读操作前,先写一次寄存器地址。这样能确保指针指向正确的位置。有些传感器支持“自动递增”,连续读多个寄存器时特别方便。

2.2 SPI协议详解

SPI,全称Serial Peripheral Interface。它比I2C快得多,但多占引脚。SPI需要四根线:

  • SCLK:串行时钟,由主设备产生
  • MOSI:主设备输出,从设备输入
  • MISO:主设备输入,从设备输出
  • CS:片选信号,低电平有效

为什么可穿戴设备里SPI用得少?因为心率血氧传感器数据量不大,I2C的400kHz完全够用。但如果你做的是高采样率的加速度计或陀螺仪,SPI就派上用场了。

2.2.1 四种模式

SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我刚开始做的时候,被这个搞晕过。

模式 CPOL CPHA 说明
模式0 0 0 空闲时SCLK为低,第一个边沿采样
模式1 0 1 空闲时SCLK为低,第二个边沿采样
模式2 1 0 空闲时SCLK为高,第一个边沿采样
模式3 1 1 空闲时SCLK为高,第二个边沿采样

我个人习惯:先看传感器数据手册,找到“SPI Timing Diagram”那张图。图上会标清楚数据是在时钟上升沿还是下降沿采样。照着配就行,别自己猜。

2.2.2 SPI读写示例

SPI的读写是同步的。你发一个字节的同时,会收到一个字节。所以读操作其实是“假读真写”——你得发一个空字节来产生时钟。

// SPI读写一个字节
uint8_t spi_transfer(uint8_t data) {
    // 发送数据的同时接收数据
    SPI_DR = data;                  // 写入数据寄存器
    while(!(SPI_SR & SPI_SR_TXE)); // 等待发送完成
    while(!(SPI_SR & SPI_SR_RXNE));// 等待接收完成
    return SPI_DR;                  // 返回接收到的数据
}

// 读寄存器:先发寄存器地址(带读标志),再读数据
uint8_t spi_read_reg(uint8_t reg_addr) {
    uint8_t data;
    CS_LOW();                       // 拉低片选
    spi_transfer(reg_addr | 0x80); // 地址 + 读标志(最高位置1)
    data = spi_transfer(0x00);     // 发空字节,收数据
    CS_HIGH();                      // 拉高片选
    return data;
}
关键点: SPI的片选信号一定要控制好。每次通信开始前拉低,结束后拉高。我曾经遇到过片选没拉高,导致多个SPI设备冲突的问题。那叫一个头疼。

2.3 传感器寄存器读写实战

理论说完了,咱们来点实战。以MAX30102心率血氧传感器为例,我带你走一遍完整的寄存器读写流程。

2.3.1 初始化流程

传感器上电后,第一步是复位。然后配置工作模式、采样率、LED电流等参数。

// MAX30102初始化
void max30102_init(void) {
    // 1. 复位传感器
    write_reg(0x09, 0x40);  // 写0x40到模式配置寄存器,触发复位
    delay_ms(100);          // 等待复位完成
    
    // 2. 设置中断使能
    write_reg(0x02, 0x00);  // 关闭所有中断,先清干净
    
    // 3. 配置FIFO
    write_reg(0x08, 0x0F);  // FIFO配置:采样平均=1,FIFO满时触发中断
    
    // 4. 设置模式
    write_reg(0x09, 0x03);  // 模式:心率+血氧(SpO2模式)
    
    // 5. 配置采样率与LED电流
    write_reg(0x0A, 0x43);  // SpO2配置:采样率400Hz,LED脉宽411μs
    write_reg(0x0C, 0x24);  // LED1(红光)电流:6.4mA
    write_reg(0x0D, 0x24);  // LED2(红外)电流:6.4mA
    
    // 6. 使能传感器
    write_reg(0x09, 0x03);  // 再次确认模式,开始工作
}

2.3.2 读取FIFO数据

传感器采集到的数据存在FIFO里。我们需要连续读取多个字节。

// 读取FIFO数据(一次读6个字节:2字节红光 + 2字节红外 + 2字节备用)
void max30102_read_fifo(uint32_t *red, uint32_t *ir) {
    uint8_t buffer[6];
    
    // 从FIFO数据寄存器(0x07)开始连续读
    for(int i = 0; i < 6; i++) {
        buffer[i] = read_reg(0x07);
    }
    
    // 组装数据(注意:传感器数据是大端格式)
    *red = ((uint32_t)buffer[0] << 16) | 
           ((uint32_t)buffer[1] << 8) | 
           (uint32_t)buffer[2];
    
    *ir = ((uint32_t)buffer[3] << 16) | 
          ((uint32_t)buffer[4] << 8) | 
          (uint32_t)buffer[5];
}
避坑指南: 我曾经犯过一个低级错误——读FIFO时没有检查数据是否就绪。结果读到的全是0xFF。正确的做法是:先读中断状态寄存器,确认有数据了再读FIFO。或者直接读FIFO写指针,跟读指针比较,有差值就说明有新数据。

2.3.3 调试技巧

调试传感器通信,我推荐两个工具:

  • 逻辑分析仪:几十块钱的USB逻辑分析仪就够用。抓取SCL和SDA波形,看地址、数据、应答位对不对。
  • 串口打印:把读到的寄存器值通过串口打印出来。我习惯在初始化后,把关键寄存器的值读回来打印,确认写入成功。

举个例子,初始化后打印寄存器0x09的值:

uint8_t mode = read_reg(0x09);
printf("Mode register: 0x%02X\n", mode);
// 如果打印出来是0x03,说明模式配置成功

如果打印出来是0x40,说明传感器还在复位状态。这时候就要检查复位时序了。

我的习惯: 每个传感器都写一个“寄存器dump”函数,把关键寄存器的值全部打印出来。这样一眼就能看出配置有没有生效。省得后面数据不对了再回头查。

好了,I2C和SPI的要点就这些。说白了,通信协议就是定好规矩,你按规矩来,传感器就乖乖听话。下一章咱们聊聊数据处理的那些坑——嗯,那才是真正让人头大的地方。