第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 | 读取状态寄存器 |
测量流程是这样的:
- 发送测量指令(如0x2400)
- 等待约15ms(高重复性模式)
- 读取6个字节数据(温度2字节 + CRC1字节 + 湿度2字节 + CRC1字节)
- 将原始数据转换为实际温湿度值
为什么需要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驱动的完整工作流程,从初始化到数据发布:
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