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模块, 蓝牙模块 |
| 抗干扰能力 | 中等(开漏输出) | 较强(推挽输出) | 较弱(依赖电平标准) |
3.2 I2C协议快速回顾
I2C的核心就两句话:起始条件、停止条件,中间传数据。每个字节后面跟一个ACK应答位。
标准I2C写操作流程:
- 主机发送起始条件
- 主机发送7位从机地址 + 写位(0)
- 从机应答ACK
- 主机发送寄存器地址
- 从机应答ACK
- 主机发送数据字节
- 从机应答ACK
- 重复第6~7步直到数据发完
- 主机发送停止条件
读操作稍微绕一点,要先写寄存器地址,再重新发起始条件,然后读数据:
- 起始条件 → 从机地址+写位 → ACK
- 发送寄存器地址 → ACK
- 重复起始条件 → 从机地址+读位(1) → ACK
- 读取数据字节 → 主机发送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。
3.4 UART协议快速回顾
UART最简单,但坑也不少。异步通信,没有时钟线,靠波特率同步。常见波特率:9600、115200、460800。
UART数据帧格式:
- 起始位:1位(低电平)
- 数据位:5~9位,通常8位
- 校验位:可选(奇校验/偶校验/无校验)
- 停止位:1位或2位(高电平)
传感器用UART的其实不多,大多是GPS、蓝牙这类模块。但咱们课程后面做数据回传和调试,UART是必备技能。
3.5 知识体系总览
下面这张图把本章的核心知识点串起来了。从协议到驱动,再到数据采集,一条线走通。
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);
第三步:读取加速度和陀螺仪数据
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;
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;
3.8 驱动移植的通用步骤
不管什么传感器,驱动移植的套路都差不多。我总结了一个四步法:
- 看数据手册:找到通信协议、寄存器地址、上电时序
- 写初始化函数:唤醒传感器、配置量程和模式
- 写读取函数:按数据手册的寄存器地址读原始数据
- 写转换函数:把原始数据换算成物理值
说白了,传感器驱动就是“读寄存器→算数值”的循环。你把这个循环跑通了,后面做实时推理才有数据可用。