4. 传感器数据采集基础:I2C协议与LSM6DSO驱动
好,咱们进入第四章。这一章要解决一个很实际的问题——怎么让STM32跟传感器“说话”。
我刚开始做嵌入式那会儿,最头疼的就是调I2C。明明照着数据手册写的代码,读回来的数据就是不对。后来才发现,是时序差了那么一点点。嗯,这一章咱们就把I2C和LSM6DSO加速度计彻底讲透。
4.1 I2C协议:两线制通信的底层逻辑
I2C(Inter-Integrated Circuit)是飞利浦公司发明的串行通信协议。它只用两根线:SCL(时钟线)和SDA(数据线)。
说白了,就是一根线负责打拍子,一根线负责传数据。所有设备都挂在这两根线上,靠地址来区分谁跟谁说话。
I2C核心特点:
- 主从架构:STM32做主设备,传感器做从设备
- 7位地址:最多挂127个设备(实际要考虑总线电容)
- 速率模式:标准模式100kHz,快速模式400kHz
- 开漏输出:需要上拉电阻(4.7kΩ典型值)
我在项目里踩过一个坑:板子上I2C上拉电阻用了10kΩ,结果400kHz模式下波形都变形了。后来换成4.7kΩ,问题解决。所以,上拉电阻不是随便选的,跟总线电容和速率都有关系。
4.1.1 I2C通信时序
I2C的通信过程其实不复杂。我习惯把它拆成几个步骤:
- 起始条件:SCL高电平时,SDA从高变低
- 发送地址:7位地址 + 读写位(0写1读)
- 从设备应答:第9个时钟周期,SDA被拉低表示ACK
- 数据传输:每字节8位,后面跟ACK/NACK
- 停止条件:SCL高电平时,SDA从低变高
你想想看,这个协议设计得挺巧妙的。起始和停止条件都是“不合法”的电平变化,这样就不会跟数据传输混淆。
避坑指南:我曾经在写I2C驱动时,忘记在发送停止条件前释放SDA线。结果总线一直卡在忙状态,所有设备都通信不了。排查了整整半天……后来养成习惯:每次通信结束,一定检查总线状态是否恢复空闲。
4.1.2 STM32的I2C外设
STM32的I2C外设支持三种模式:轮询、中断、DMA。咱们做传感器数据采集,用轮询模式就够了。原因很简单:加速度计的数据更新率通常只有几十到几百Hz,CPU完全等得起。
| 模式 | 适用场景 | CPU占用 | 实时性 |
|---|---|---|---|
| 轮询 | 低速传感器、调试阶段 | 高(等待时空转) | 一般 |
| 中断 | 事件触发、低功耗场景 | 低 | 高 |
| DMA | 批量数据、高吞吐 | 极低 | 高 |
我个人习惯:先轮询调通,再考虑优化。上来就搞DMA,万一通信有问题,排查起来更麻烦。
4.2 LSM6DSO加速度计驱动
LSM6DSO是ST自家产的6轴惯性测量单元(IMU),包含3轴加速度计和3轴陀螺仪。咱们这节课先用加速度计部分。
它的I2C地址是0x6A(如果SDO引脚接VDD)或0x6B(接GND)。板子上默认是0x6A。
4.2.1 寄存器概览
LSM6DSO的寄存器很多,但咱们只需要关注几个关键的:
| 寄存器名 | 地址 | 作用 |
|---|---|---|
| WHO_AM_I | 0x0F | 设备ID,固定值0x6C |
| CTRL1_XL | 0x10 | 加速度计配置(ODR、满量程) |
| OUTX_L_A | 0x28 | X轴加速度低字节 |
| OUTX_H_A | 0x29 | X轴加速度高字节 |
| OUTY_L_A | 0x2A | Y轴加速度低字节 |
| OUTY_H_A | 0x2B | Y轴加速度高字节 |
| OUTZ_L_A | 0x2C | Z轴加速度低字节 |
| OUTZ_H_A | 0x2D | Z轴加速度高字节 |
注意:LSM6DSO的加速度数据是16位有符号整数,以补码形式存储。读取时一定要先读低字节,再读高字节。因为芯片内部有数据锁存机制,顺序反了会导致高低字节不匹配。
4.2.2 初始化流程
初始化LSM6DSO加速度计,我一般分三步走:
- 验证设备ID:读WHO_AM_I寄存器,确认是0x6C
- 配置加速度计:设置CTRL1_XL寄存器,选择输出数据率和满量程
- 等待稳定:延时几毫秒,等内部电路稳定
代码实现如下:
// LSM6DSO初始化函数
uint8_t LSM6DSO_Init(void)
{
uint8_t who_am_i;
// 1. 读取WHO_AM_I寄存器
I2C_ReadRegister(LSM6DSO_ADDR, 0x0F, &who_am_i, 1);
if (who_am_i != 0x6C) {
return 1; // 设备ID不匹配,初始化失败
}
// 2. 配置加速度计:ODR=416Hz,满量程=±2g
// CTRL1_XL = 0x60 (0110 0000)
// bit7-4: ODR = 0110 (416Hz)
// bit3-2: FS = 00 (±2g)
uint8_t ctrl1_xl = 0x60;
I2C_WriteRegister(LSM6DSO_ADDR, 0x10, &ctrl1_xl, 1);
// 3. 等待稳定
HAL_Delay(10);
return 0; // 初始化成功
}
这里有个细节:满量程的选择直接影响分辨率。±2g时分辨率最高(约0.061mg/LSB),但量程小。±16g时量程大,但分辨率低。做人体姿态检测,±2g就够了。做剧烈运动检测,建议用±4g或±8g。
4.3 原始数据读取与校验
初始化完成后,就可以读取加速度数据了。LSM6DSO支持批量读取——从起始地址连续读多个字节,芯片会自动递增地址。
我建议一次性读6个字节(X、Y、Z各2字节),效率更高:
// 读取三轴加速度原始数据
void LSM6DSO_ReadAccel(int16_t *accel_x, int16_t *accel_y, int16_t *accel_z)
{
uint8_t data[6];
// 从0x28开始连续读6字节
I2C_ReadRegister(LSM6DSO_ADDR, 0x28, data, 6);
// 组合成16位有符号数(小端模式)
*accel_x = (int16_t)((data[1] << 8) | data[0]);
*accel_y = (int16_t)((data[3] << 8) | data[2]);
*accel_z = (int16_t)((data[5] << 8) | data[4]);
}
经验之谈:我曾经遇到过读回来的数据偶尔跳变。排查后发现是I2C时钟线受到了电源纹波干扰。解决办法是在SCL和SDA线上各加一个10pF的小电容到地,滤掉高频噪声。当然,电容不能太大,否则会影响上升沿时间。
4.3.1 数据校验方法
读回来的数据怎么判断对不对?我总结了三个校验手段:
- 静态校验:把传感器平放,Z轴读数应该接近+1g(约16384 LSB),X和Y轴接近0
- 动态校验:翻转传感器,看数据是否跟着变化
- 连续性校验:连续读100次,看数据是否有突变(排除偶发错误)
举个例子,平放时Z轴的理论值计算:
// ±2g满量程,灵敏度 = 0.061 mg/LSB
// 1g对应的LSB值 = 1000 / 0.061 ≈ 16393
// 实际值应该在16000~17000之间
if (accel_z > 15000 && accel_z < 18000) {
// 数据合理
} else {
// 数据异常,重新初始化或报错
}
4.4 数据可视化方法
原始数据是数字,看起来不直观。我习惯用两种方式可视化:
4.4.1 串口波形图
用串口助手配合简单的字符画,就能看到数据趋势。比如:
// 简易波形显示(串口输出)
void Display_Waveform(int16_t value, int16_t center)
{
int pos = (value - center) / 500; // 缩放因子
if (pos > 20) pos = 20;
if (pos < -20) pos = -20;
// 画波形
for (int i = -20; i < 20; i++) {
if (i == 0) printf("|");
else if (i == pos) printf("*");
else printf(" ");
}
printf("\n");
}
这个方法虽然简陋,但在调试初期非常实用。不用额外工具,一根串口线就能看到数据变化。
4.4.2 上位机实时曲线
更专业的做法是用Python + Matplotlib做实时曲线。我常用的流程是:
- STM32通过串口发送数据(格式:X,Y,Z\n)
- PC端Python脚本读取串口数据
- Matplotlib实时更新曲线
Python脚本核心代码:
import serial
import matplotlib.pyplot as plt
import numpy as np
# 初始化串口
ser = serial.Serial('COM3', 115200)
# 初始化绘图
plt.ion()
fig, ax = plt.subplots()
x_data, y_data, z_data = [], [], []
while True:
line = ser.readline().decode().strip()
if line:
values = line.split(',')
if len(values) == 3:
x_data.append(float(values[0]))
y_data.append(float(values[1]))
z_data.append(float(values[2]))
# 只保留最近100个点
if len(x_data) > 100:
x_data.pop(0)
y_data.pop(0)
z_data.pop(0)
ax.clear()
ax.plot(x_data, label='X')
ax.plot(y_data, label='Y')
ax.plot(z_data, label='Z')
ax.legend()
plt.pause(0.01)
可视化要点:
- 串口波特率建议用115200以上,保证数据不积压
- 数据格式要固定,方便上位机解析
- 采样率不要超过串口传输能力(115200波特率下,每秒最多传约11500字节)
4.5 本章知识体系
下面这张图把本章的核心逻辑串起来了:
从I2C协议到传感器驱动,再到数据校验和可视化,每一步都是嵌入式AI数据采集的关键环节。数据质量决定了后续AI推理的上限——这个道理,我在做第一个AI项目时就深刻体会到了。
最后提醒:如果你发现读回来的数据全是0xFF或0x00,大概率是I2C通信没通。先检查接线,再用逻辑分析仪看波形。不要盲目改代码——硬件问题用硬件手段排查,这是我一直坚持的原则。