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的通信过程其实不复杂。我习惯把它拆成几个步骤:

  1. 起始条件:SCL高电平时,SDA从高变低
  2. 发送地址:7位地址 + 读写位(0写1读)
  3. 从设备应答:第9个时钟周期,SDA被拉低表示ACK
  4. 数据传输:每字节8位,后面跟ACK/NACK
  5. 停止条件: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加速度计,我一般分三步走:

  1. 验证设备ID:读WHO_AM_I寄存器,确认是0x6C
  2. 配置加速度计:设置CTRL1_XL寄存器,选择输出数据率和满量程
  3. 等待稳定:延时几毫秒,等内部电路稳定

代码实现如下:

// 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做实时曲线。我常用的流程是:

  1. STM32通过串口发送数据(格式:X,Y,Z\n)
  2. PC端Python脚本读取串口数据
  3. 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协议基础 物理层:SCL/SDA 通信时序:起始/停止 STM32 I2C外设 LSM6DSO驱动 寄存器配置 初始化流程 批量读取 数据校验与可视化 静态/动态校验 串口/上位机可视化 从I2C协议到传感器驱动,再到数据校验和可视化 每一步都是嵌入式AI数据采集的关键环节 数据质量决定了后续AI推理的上限

从I2C协议到传感器驱动,再到数据校验和可视化,每一步都是嵌入式AI数据采集的关键环节。数据质量决定了后续AI推理的上限——这个道理,我在做第一个AI项目时就深刻体会到了。

最后提醒:如果你发现读回来的数据全是0xFF或0x00,大概率是I2C通信没通。先检查接线,再用逻辑分析仪看波形。不要盲目改代码——硬件问题用硬件手段排查,这是我一直坚持的原则。