第4章:I2C驱动实战——驱动一个I2C温湿度传感器(SHT30)

好,咱们今天来点硬核的。前面几章讲了PX4的驱动框架和uORB通信,很多朋友可能觉得有点虚。那这一章,我们就拿一个真实的传感器——SHT30温湿度传感器,来走一遍完整的I2C驱动开发流程。

SHT30是Sensirion家的经典产品,精度不错,价格也亲民。我最早接触它是在一个农业无人机项目里,用来监测大棚内的温湿度。当时踩了不少坑,今天一并分享给你们。

4.1 硬件连接与I2C协议回顾

先看硬件。SHT30的I2C地址是0x44(ADDR引脚接GND)或0x45(接VCC)。默认是0x44。接线很简单:

  • VCC → 3.3V
  • GND → GND
  • SCL → 飞控的SCL引脚
  • SDA → 飞控的SDA引脚

嗯,这里要注意:SHT30是3.3V供电,千万别接到5V上,否则芯片会冒烟。我有个同事就干过这事,结果传感器直接报废。

I2C协议本身不复杂,但有几个关键点:

  • 起始信号:SCL高电平时,SDA从高变低
  • 停止信号:SCL高电平时,SDA从低变高
  • 每个字节后跟一个ACK位
  • 时钟线SCL由主机控制,从机只能拉低SCL做时钟拉伸

说白了,I2C就是一根时钟线加一根数据线,通过地址来区分设备。你想想看,如果总线上挂了10个传感器,每个地址不同,那就能分别访问。

4.2 SHT30的通信协议

SHT30的指令集很简单。常用的就几个:

指令 命令码(16位) 说明
测量(高重复性) 0x2400 启动一次温湿度测量
读取数据 0xE000 读取上次测量结果
软复位 0x30A2 复位传感器
状态寄存器读 0xF32D 读取状态寄存器

测量流程是这样的:

  1. 发送测量指令(如0x2400)
  2. 等待约15ms(高重复性模式)
  3. 读取6个字节数据(温度2字节 + CRC1字节 + 湿度2字节 + CRC1字节)
  4. 将原始数据转换为实际温湿度值

为什么需要CRC校验?因为I2C总线在长距离或高噪声环境下容易受干扰。我在飞控上测试时,发现如果不做CRC校验,偶尔会读到异常值,比如温度突然跳到85℃。加了CRC后,这种问题就基本杜绝了。

4.3 驱动代码实现

好,咱们直接上代码。我会用PX4的I2C驱动框架来写。先看头文件:

// sht30.hpp
#pragma once

#include <px4_platform_common/i2c.h>
#include <uORB/uORB.h>
#include <uORB/topics/sensor_combined.h>

class SHT30 : public I2C
{
public:
  SHT30(int bus, uint8_t address = 0x44);
  virtual ~SHT30() = default;

  int init() override;
  int measure();
  int collect();

private:
  int _measure_interval_us{20000}; // 20ms测量间隔
  orb_advert_t _sensor_pub{nullptr};
  sensor_combined_s _sensor_data{};

  bool crc8_check(const uint8_t *data, size_t len);
  float raw_to_temperature(uint16_t raw);
  float raw_to_humidity(uint16_t raw);
};

这里我继承了PX4的I2C基类。这个基类封装了底层的I2C读写操作,我们只需要实现init()、measure()和collect()三个方法。

再看实现文件:

// sht30.cpp
#include "sht30.hpp"

SHT30::SHT30(int bus, uint8_t address)
  : I2C(DRV_SHT30_DEVTYPE, MODULE_NAME, bus, address, 400000) // 400kHz
{
  // 设置I2C时钟频率为400kHz(快速模式)
}

int SHT30::init()
{
  int ret = I2C::init();
  if (ret != PX4_OK) {
    PX4_ERR("I2C init failed");
    return ret;
  }

  // 发送软复位指令
  uint8_t reset_cmd[] = {0x30, 0xA2};
  ret = transfer(reset_cmd, sizeof(reset_cmd), nullptr, 0);
  if (ret != PX4_OK) {
    PX4_ERR("Soft reset failed");
    return ret;
  }

  px4_usleep(1000); // 等待复位完成

  // 发布uORB主题
  _sensor_pub = orb_advertise(ORB_ID(sensor_combined), &_sensor_data);

  PX4_INFO("SHT30 initialized on bus %d", _bus);
  return PX4_OK;
}

int SHT30::measure()
{
  // 发送测量指令(高重复性,时钟拉伸关闭)
  uint8_t cmd[] = {0x24, 0x00};
  return transfer(cmd, sizeof(cmd), nullptr, 0);
}

int SHT30::collect()
{
  uint8_t buf[6] = {0};
  int ret = transfer(nullptr, 0, buf, sizeof(buf));
  if (ret != PX4_OK) {
    PX4_ERR("Read failed");
    return ret;
  }

  // CRC校验
  if (!crc8_check(&buf[0], 2) || !crc8_check(&buf[3], 2)) {
    PX4_WARN("CRC error");
    return -EIO;
  }

  // 解析数据
  uint16_t raw_temp = (buf[0] << 8) | buf[1];
  uint16_t raw_hum  = (buf[3] << 8) | buf[4];

  _sensor_data.temperature = raw_to_temperature(raw_temp);
  _sensor_data.humidity = raw_to_humidity(raw_hum);
  _sensor_data.timestamp = hrt_absolute_time();

  // 发布到uORB
  orb_publish(ORB_ID(sensor_combined), _sensor_pub, &_sensor_data);

  return PX4_OK;
}

bool SHT30::crc8_check(const uint8_t *data, size_t len)
{
  uint8_t crc = 0xFF;
  for (size_t i = 0; i < len; i++) {
    crc ^= data[i];
    for (int j = 0; j < 8; j++) {
      if (crc & 0x80) {
        crc = (crc << 1) ^ 0x31;
      } else {
        crc <<= 1;
      }
    }
  }
  return crc == data[len]; // 最后一个字节是CRC
}

float SHT30::raw_to_temperature(uint16_t raw)
{
  return -45.0f + 175.0f * raw / 65535.0f;
}

float SHT30::raw_to_humidity(uint16_t raw)
{
  return 100.0f * raw / 65535.0f;
}

代码看起来不长,但每个细节都有讲究。比如CRC校验的生成多项式是0x31,这是SHT30的固定算法。我曾经试过用标准CRC-8,结果死活对不上,后来查数据手册才发现多项式不一样。

4.4 注册到PX4驱动框架

写完了驱动类,还得把它注册到PX4的驱动管理器中。在启动脚本里加一行:

sht30 start -b 1  # 在I2C总线1上启动

对应的启动代码:

// sht30_main.cpp
#include <px4_platform_common/module.h>
#include "sht30.hpp"

extern "C" __EXPORT int sht30_main(int argc, char *argv[]);

int sht30_main(int argc, char *argv[])
{
  if (argc < 2) {
    PX4_ERR("Usage: sht30 {start|stop|status}");
    return 1;
  }

  if (strcmp(argv[1], "start") == 0) {
    int bus = 1; // 默认总线1
    if (argc > 3 && strcmp(argv[2], "-b") == 0) {
      bus = atoi(argv[3]);
    }

    SHT30 *dev = new SHT30(bus);
    if (dev->init() != PX4_OK) {
      PX4_ERR("Failed to init SHT30");
      delete dev;
      return 1;
    }

    // 启动测量循环
    while (true) {
      dev->measure();
      px4_usleep(15000); // 等待测量完成
      dev->collect();
      px4_usleep(5000);  // 保持20ms周期
    }
  }

  return 0;
}

4.5 避坑指南与调试技巧

⚠️ 常见问题:
  • I2C总线锁死:如果从机没有释放SDA线,总线会一直处于忙状态。解决办法是给SCL发送9个时钟脉冲,强制从机复位。
  • 地址错误:SHT30的7位地址是0x44,但I2C通信时左移一位变成0x88(写)和0x89(读)。很多新手会搞混。
  • 时序问题:测量指令发出后,必须等待足够时间才能读取。我建议至少等15ms,如果时钟拉伸功能开启,可以等SCL被拉低后再读取。
💡 调试小技巧:

我曾经在调试SHT30时,用逻辑分析仪抓I2C波形,发现SCL上有个毛刺导致通信失败。后来在SCL和SDA上各加了一个10kΩ上拉电阻,问题就解决了。如果你遇到通信不稳定,先检查上拉电阻。

4.6 核心逻辑流程图

下面这张图展示了SHT30驱动的完整工作流程,从初始化到数据发布:

SHT30驱动核心流程图 1. I2C初始化 2. 发送软复位指令 3. 发送测量指令 4. 等待15ms 5. 读取6字节数据 6. CRC校验 初始化I2C总线 复位传感器到已知状态 启动一次温湿度测量 等待转换完成 温度2B + CRC1B + 湿度2B + CRC1B 校验通过后发布到uORB

4.7 总结

这一章我们完整走了一遍SHT30的I2C驱动开发流程。从硬件接线、协议分析,到代码实现、调试技巧,每个环节我都结合自己的项目经验做了说明。

我个人觉得,I2C驱动开发的核心就三点:

  • 时序要准:严格按照数据手册的时序要求来操作
  • 校验要全:CRC校验不能省,否则数据可靠性没法保证
  • 调试要细:逻辑分析仪是你的好朋友,波形不对就抓出来看

如果你在开发中遇到问题,不妨先检查I2C总线上拉电阻、确认地址是否正确、看看时序是否满足要求。这三个问题解决了,90%的I2C问题都能搞定。

📌 关键点回顾:

  • SHT30的I2C地址:0x44(ADDR=GND)或0x45(ADDR=VCC)
  • 测量指令:0x2400(高重复性)
  • 数据格式:温度2字节 + CRC1字节 + 湿度2字节 + CRC1字节
  • 转换公式:温度 = -45 + 175 * raw / 65535,湿度 = 100 * raw / 65535
  • 发布主题:sensor_combined(包含temperature和humidity字段)

公众号:蓝海资料掘金营,微信deep3321