3. 传感器数据采集基础:I2C/SPI/UART协议回顾与驱动移植

做嵌入式AI,第一步就是跟传感器打交道。数据都拿不到,后面推理、压缩全是空谈。这一章咱们把三种最常用的通信协议——I2C、SPI、UART——再过一遍,然后手把手移植两个经典传感器驱动:MPU6050和BME280。

我个人习惯是,拿到一个新传感器,先不看代码,先看数据手册里的时序图。时序图看懂了,驱动怎么写心里就有底了。

3.1 三种协议,一张表说清楚

先别急着写代码,咱们把这三个协议的核心差异理一理。我在项目里见过不少新手把I2C和SPI搞混,结果调了两天发现是引脚接错了。

特性 I2C SPI UART
信号线数量 2(SCL, SDA) 4(SCK, MOSI, MISO, CS) 2(TX, RX)
通信方式 半双工 全双工 全双工
从设备寻址 7位/10位地址 片选引脚 无(点对点)
速度 100kHz~3.4MHz 最高可达几十MHz 通常115200bps~1Mbps
典型传感器 MPU6050, BME280 SD卡, 显示屏 GPS模块, 蓝牙模块
抗干扰能力 中等(开漏输出) 较强(推挽输出) 较弱(依赖电平标准)
我的经验: 如果传感器离MCU超过10cm,优先选SPI。I2C线长了容易出问题,我曾经被一根30cm的I2C线坑了一整天,波形都变形了。

3.2 I2C协议快速回顾

I2C的核心就两句话:起始条件、停止条件,中间传数据。每个字节后面跟一个ACK应答位。

标准I2C写操作流程:

  1. 主机发送起始条件
  2. 主机发送7位从机地址 + 写位(0)
  3. 从机应答ACK
  4. 主机发送寄存器地址
  5. 从机应答ACK
  6. 主机发送数据字节
  7. 从机应答ACK
  8. 重复第6~7步直到数据发完
  9. 主机发送停止条件

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

  1. 起始条件 → 从机地址+写位 → ACK
  2. 发送寄存器地址 → ACK
  3. 重复起始条件 → 从机地址+读位(1) → ACK
  4. 读取数据字节 → 主机发送NACK → 停止条件
注意: 读操作最后主机要发NACK,告诉从机“别发了”。我见过有人忘了这一步,结果从机一直发数据,总线被锁死。

3.3 SPI协议快速回顾

SPI比I2C简单粗暴。四条线:SCK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、CS片选。

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

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

大部分传感器用模式0或模式3。MPU6050的SPI接口用的是模式0,BME280也支持SPI模式0。

避坑指南: 我曾经在移植一个SPI加速度计驱动时,死活读不到数据。折腾了半天,发现是CS引脚没有在每次传输前拉低、传输后拉高。SPI的CS是低电平有效,这个细节很容易忘。

3.4 UART协议快速回顾

UART最简单,但坑也不少。异步通信,没有时钟线,靠波特率同步。常见波特率:9600、115200、460800。

UART数据帧格式:

  • 起始位:1位(低电平)
  • 数据位:5~9位,通常8位
  • 校验位:可选(奇校验/偶校验/无校验)
  • 停止位:1位或2位(高电平)

传感器用UART的其实不多,大多是GPS、蓝牙这类模块。但咱们课程后面做数据回传和调试,UART是必备技能。

注意: 两个设备通信,波特率、数据位、校验位、停止位必须完全一致。差一点都不行。我遇到过有人把波特率设成115200和1152000,少了个0,调了一下午。

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了。从协议到驱动,再到数据采集,一条线走通。

传感器数据采集知识体系 通信协议层 I2C / SPI / UART 传感器驱动层 MPU6050 / BME280 数据采集层 定时读取 / DMA MPU6050(六轴IMU) BME280(环境传感器) I2C地址:0x68 I2C地址:0x76/0x77 加速度/角速度/温度 温度/湿度/气压 数据融合 → 实时推理

3.6 MPU6050驱动移植实战

MPU6050是六轴惯性测量单元,包含三轴加速度计和三轴陀螺仪。它用I2C通信,地址是0x68(AD0引脚接地时)。

驱动移植的核心就三步:初始化、配置、读取数据。

第一步:初始化I2C外设

在STM32上,我用HAL库的HAL_I2C_Init()。注意时钟频率别超过400kHz,MPU6050不支持高速模式。

// I2C初始化代码片段
void MPU6050_I2C_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);
}

第二步:配置MPU6050

上电后先要唤醒传感器,因为默认是睡眠模式。写寄存器0x6B,把bit6清零。

// 唤醒MPU6050
uint8_t data = 0x00;
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);

// 配置加速度计量程为±2g,写寄存器0x1C
data = 0x00;  // 00: ±2g, 08: ±4g, 10: ±8g, 18: ±16g
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1C, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);

// 配置陀螺仪量程为±250°/s,写寄存器0x1B
data = 0x00;  // 00: ±250, 08: ±500, 10: ±1000, 18: ±2000
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
我的习惯: 每次配置完寄存器,我都会读回来验证一下。比如写0x6B后读一下,确认确实是0x00。这一步能帮你快速定位是写失败了还是读失败了。

第三步:读取加速度和陀螺仪数据

MPU6050的加速度数据在寄存器0x3B~0x40,陀螺仪数据在0x43~0x48。每个轴占2个字节,高字节在前。

// 读取加速度数据
uint8_t accel_buf[6];
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, I2C_MEMADD_SIZE_8BIT, accel_buf, 6, 100);

int16_t accel_x = (accel_buf[0] << 8) | accel_buf[1];
int16_t accel_y = (accel_buf[2] << 8) | accel_buf[3];
int16_t accel_z = (accel_buf[4] << 8) | accel_buf[5];

// 转换为实际物理值(±2g量程下,16384 LSB/g)
float ax = accel_x / 16384.0f;
float ay = accel_y / 16384.0f;
float az = accel_z / 16384.0f;
注意: 原始数据是int16_t,有符号的。直接左移8位再或操作,如果最高位是1,结果会是负数。C语言里int16_t自动处理符号扩展,但如果你用uint16_t存,就得自己处理符号位。

3.7 BME280驱动移植实战

BME280是环境传感器,能测温度、湿度、气压。它也支持I2C和SPI两种接口,咱们用I2C,地址是0x76(SDO接地时)。

BME280有个特点:它出厂时每个芯片的校准参数都不一样。所以第一步必须先读取校准参数,然后才能用公式计算真实值。

第一步:读取校准参数

校准参数存在寄存器0x88~0xA1,一共26个字节。这些参数用于温度、气压、湿度的补偿计算。

// 读取校准参数
uint8_t calib_buf[26];
HAL_I2C_Mem_Read(&hi2c1, 0x76 << 1, 0x88, I2C_MEMADD_SIZE_8BIT, calib_buf, 26, 100);

// 提取温度校准参数
uint16_t dig_T1 = (calib_buf[1] << 8) | calib_buf[0];
int16_t  dig_T2 = (calib_buf[3] << 8) | calib_buf[2];
int16_t  dig_T3 = (calib_buf[5] << 8) | calib_buf[4];

第二步:配置传感器模式

BME280支持三种模式:睡眠模式、强制模式、正常模式。咱们用正常模式,传感器会持续采集。

// 配置为正常模式,过采样次数设为1
uint8_t config_data[3];
config_data[0] = 0x01;  // 湿度过采样×1,写寄存器0xF2
config_data[1] = 0x01;  // 温度过采样×1,写寄存器0xF2
config_data[2] = 0x01;  // 气压过采样×1,写寄存器0xF4

HAL_I2C_Mem_Write(&hi2c1, 0x76 << 1, 0xF2, I2C_MEMADD_SIZE_8BIT, &config_data[0], 1, 100);
HAL_I2C_Mem_Write(&hi2c1, 0x76 << 1, 0xF3, I2C_MEMADD_SIZE_8BIT, &config_data[1], 1, 100);
HAL_I2C_Mem_Write(&hi2c1, 0x76 << 1, 0xF4, I2C_MEMADD_SIZE_8BIT, &config_data[2], 1, 100);

第三步:读取并计算温度

BME280的温度计算有点复杂,需要用到校准参数做补偿。公式在数据手册里有,我直接贴代码:

// 读取原始温度数据(寄存器0xFA~0xFC)
uint8_t temp_buf[3];
HAL_I2C_Mem_Read(&hi2c1, 0x76 << 1, 0xFA, I2C_MEMADD_SIZE_8BIT, temp_buf, 3, 100);

int32_t adc_T = (temp_buf[0] << 12) | (temp_buf[1] << 4) | (temp_buf[2] >> 4);

// 温度补偿计算(BME280数据手册公式)
int32_t var1, var2, T;
var1 = ((((adc_T >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11;
var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
T = (var1 + var2) >> 5;

// 最终温度,单位0.01°C
float temperature = T / 100.0f;
避坑指南: 我曾经在BME280上踩过一个坑——读取数据前没有检查状态寄存器。BME280在测量完成后会把状态寄存器0xF3的bit3置1。如果你在测量还没完成时就读数据,读到的全是0。加个等待循环就好了。

3.8 驱动移植的通用步骤

不管什么传感器,驱动移植的套路都差不多。我总结了一个四步法:

  1. 看数据手册:找到通信协议、寄存器地址、上电时序
  2. 写初始化函数:唤醒传感器、配置量程和模式
  3. 写读取函数:按数据手册的寄存器地址读原始数据
  4. 写转换函数:把原始数据换算成物理值

说白了,传感器驱动就是“读寄存器→算数值”的循环。你把这个循环跑通了,后面做实时推理才有数据可用。