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的通信过程其实不复杂。我刚开始学的时候,觉得时序图很吓人。后来发现,只要抓住几个关键点就行。
基本时序:
- 起始条件:SCL高电平时,SDA从高变低。
- 从设备地址+读写位:主设备发送7位地址,最后1位是读写标志(0=写,1=读)。
- 应答位(ACK):从设备拉低SDA表示收到。
- 数据字节:每个字节8位,后面跟一个ACK。
- 停止条件: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和SPI的对比、实战要点都画出来了。你保存下来,以后做项目时对照着看。
3.7 一些掏心窝的话
做传感器数据采集,说白了就是跟寄存器打交道。你配置对了,数据就对了;配置错了,后面算法再好也是白搭。
我见过太多人,算法写得天花乱坠,结果传感器初始化就错了。加速度计的量程设错了,陀螺仪的带宽没配好,出来的数据全是噪声。嗯,这种问题查起来最头疼。
所以我的建议是:拿到一个新传感器,先别急着写算法。花半天时间,用逻辑分析仪抓一下I2C或SPI的波形,确认读写操作正确。这一步省不了。
最后一个小技巧:调试I2C/SPI时,我习惯先读传感器的WHO_AM_I寄存器(或者叫Device ID)。这个寄存器是只读的,值固定。如果能正确读出来,说明通信链路没问题。然后再去配置其他寄存器。这个习惯帮我省了不少排查时间。
公众号:蓝海资料掘金营,微信deep3321