4、I2C总线驱动开发:从协议到实战

各位同学,咱们今天聊I2C。说实话,在物联网外设里,I2C和SPI是出场率最高的两种总线。我这些年做过的项目,十有八九都离不开I2C——从温湿度传感器、加速度计,到OLED屏幕、EEPROM,几乎全是I2C接口。

这一章,咱们就把它彻底吃透。我会从I2C的时序讲起,然后带你熟悉鸿蒙的I2C驱动接口,最后手写一个SHT30温湿度传感器的驱动。嗯,跟着走一遍,你就能自己搞定大部分I2C从设备了。

4.1 I2C协议时序:两条线怎么传数据?

I2C只用两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两条线上,通过地址区分谁跟谁说话。说白了,就是一根时钟线带着一根数据线,大家轮流用。

我刚开始学I2C时,总觉得时序图看着复杂。其实拆开来看,就几个关键动作:

  • 起始条件:SCL高电平时,SDA从高变低。这是“我要开始通信了”的信号。
  • 停止条件:SCL高电平时,SDA从低变高。这是“我说完了”的信号。
  • 数据位传输:SCL低电平时改变SDA,SCL高电平时采样SDA。每个时钟传1位。
  • 应答位(ACK):每传完8位数据,接收方在第9个时钟把SDA拉低,表示“收到了”。

你想想看,这其实跟两个人打电话很像:先拨号(起始条件),然后说“我是谁”(设备地址),再说“我要干嘛”(读写位),最后传内容(数据)。挂电话就是停止条件。

I2C通信基本流程:

  1. 主机发送起始条件
  2. 主机发送7位从设备地址 + 1位读写位(0写1读)
  3. 从设备回复ACK
  4. 主机发送/接收数据字节,每字节后跟ACK
  5. 主机发送停止条件

这里有个坑,我曾经踩过:地址左移。很多芯片手册上写的地址是7位,比如SHT30的地址是0x44。但在代码里,你得把它左移1位,变成0x88,再跟读写位组合。为什么?因为I2C协议里,地址占7位,读写位占第8位。所以0x44 << 1 = 0x88,写操作就是0x88,读操作就是0x89。这个细节,新手特别容易搞错。

4.2 鸿蒙I2C驱动接口:怎么用代码操作I2C?

鸿蒙的I2C驱动框架,封装得挺干净的。你不需要关心底层寄存器怎么配,直接调接口就行。我个人习惯把I2C操作封装成几个基础函数,这样上层调用起来特别清爽。

核心接口就这几个:

接口函数 功能说明
I2cOpen() 打开I2C控制器,返回设备句柄
I2cClose() 关闭I2C控制器,释放资源
I2cWrite() 向从设备写入数据
I2cRead() 从从设备读取数据
I2cTransfer() 组合传输(写+读,常用于寄存器操作)

你看,就这么几个。但实际开发中,I2cTransfer() 用得最多。因为它可以一次调用完成“先写寄存器地址,再读数据”的完整流程,中间不释放总线,效率高,也避免被其他设备打断。

我的经验: 写I2C驱动时,一定要先确认I2C控制器的编号。鸿蒙里通常用 I2cOpen(0) 打开第0个I2C控制器。如果你接的传感器在I2C1上,却打开了I2C0,那死活都读不到数据。我曾经因为这个排查了半天,最后发现是总线号搞错了。

4.3 I2C从设备驱动编写:通用套路

写I2C从设备驱动,其实有个通用模板。不管你是驱动SHT30、BMP280还是MPU6050,流程都差不多:

  1. 初始化:打开I2C控制器,配置从设备地址。
  2. 写寄存器:发送寄存器地址 + 要写入的数据。
  3. 读寄存器:先写寄存器地址,再读数据。
  4. 数据处理:把裸数据换算成物理量(温度、湿度等)。

我一般会把驱动拆成两个文件:一个 sht30.h 放宏定义和函数声明,一个 sht30.c 放实现。这样结构清晰,也方便移植。

4.4 实战:驱动温湿度传感器SHT30

好,咱们来真的。SHT30是Sensirion家的数字温湿度传感器,精度高、功耗低,I2C接口。它的默认I2C地址是0x44(7位),支持单次测量和周期测量两种模式。咱们用单次测量模式,简单可靠。

先看SHT30的通信流程:

  • 启动测量:发送命令0x2C 0x06,告诉它“开始一次高精度测量”。
  • 等待测量完成:大约等15ms。
  • 读取数据:读6个字节,分别是温度高8位、温度低8位、CRC校验、湿度高8位、湿度低8位、CRC校验。
  • 数据换算:温度 = -45 + 175 * (原始值 / 65535),湿度 = 100 * (原始值 / 65535)。

嗯,这里要注意CRC校验。SHT30每个数据后面都跟了一个CRC字节,用来保证数据完整性。我建议你一开始先不校验,等基本功能调通了再加。不然CRC算错了,数据读不出来,你都不知道是通信问题还是校验问题。

下面是我写的一个简化版驱动,去掉了一些边界检查,保留核心逻辑:

// sht30.h
#ifndef SHT30_H
#define SHT30_H

#define SHT30_ADDR         0x44  // 7位地址
#define SHT30_CMD_MEASURE  0x2C  // 单次测量命令高字节
#define SHT30_CMD_MEASURE_L 0x06 // 单次测量命令低字节

typedef struct {
    float temperature;
    float humidity;
} sht30_data_t;

int sht30_init(int i2c_bus);
int sht30_read_data(sht30_data_t *data);

#endif
// sht30.c
#include "sht30.h"
#include "i2c_core.h"  // 假设这是鸿蒙I2C接口的封装

static int g_i2c_handle = -1;
static uint8_t g_device_addr;

int sht30_init(int i2c_bus) {
    g_i2c_handle = I2cOpen(i2c_bus);
    if (g_i2c_handle < 0) {
        return -1;
    }
    g_device_addr = SHT30_ADDR << 1;  // 左移1位,变成8位地址
    return 0;
}

int sht30_read_data(sht30_data_t *data) {
    uint8_t cmd[2] = {SHT30_CMD_MEASURE, SHT30_CMD_MEASURE_L};
    uint8_t buf[6] = {0};

    // 发送测量命令
    if (I2cWrite(g_i2c_handle, g_device_addr, cmd, 2) != 0) {
        return -1;
    }

    // 等待15ms
    OsDelay(15);

    // 读取6字节数据
    if (I2cRead(g_i2c_handle, g_device_addr, buf, 6) != 6) {
        return -1;
    }

    // 换算温度
    uint16_t raw_temp = (buf[0] << 8) | buf[1];
    data->temperature = -45.0f + 175.0f * (float)raw_temp / 65535.0f;

    // 换算湿度
    uint16_t raw_humi = (buf[3] << 8) | buf[4];
    data->humidity = 100.0f * (float)raw_humi / 65535.0f;

    return 0;
}

避坑指南: 我曾经在SHT30的驱动里犯过一个低级错误——忘记左移地址。结果I2cWrite一直返回失败,折腾了两个小时才发现。另外,SHT30的测量命令是2字节的,别只发1个字节。还有,等待时间别太短,15ms是底线,我建议保守点设20ms。

你看,整个驱动核心代码不到40行。但这里面包含了I2C通信的全部要素:地址配置、命令发送、数据读取、物理量换算。你把这个模板套到其他I2C传感器上,改改命令字和换算公式,基本就能用了。

4.5 I2C通信流程总览

为了让你更直观地理解整个流程,我画了一张图。它把I2C通信的步骤、鸿蒙接口调用、以及SHT30的读写过程串在了一起。

I2C通信流程与SHT30驱动调用关系 主机(鸿蒙SoC) I2C总线(SCL + SDA) 从设备(SHT30) ① I2cOpen() 打开控制器 ② 起始条件 + 设备地址(0x88) ③ I2cWrite() 发送测量命令 ④ OsDelay(15ms) 等待测量 ⑤ I2cRead() 读取6字节数据 ⑥ 换算温度/湿度值 关键说明 • 地址左移:0x44 << 1 = 0x88 • 写操作地址:0x88,读操作:0x89 • 命令:0x2C 0x06(高精度单次测量) • 数据格式:温度高8位 + 温度低8位 + CRC 湿度高8位 + 湿度低8位 + CRC • 温度换算:-45 + 175 * (raw / 65535) • 湿度换算:100 * (raw / 65535) • 常见问题:地址未左移、等待时间不足、 总线号错误、CRC校验失败 • 建议:先不校验CRC,调通后再加

这张图把整个流程分成了6步,从打开I2C控制器到最终拿到温湿度值。右侧的说明框里,我把关键参数和常见坑都列出来了。你写驱动时,可以对照着这张图一步步来,不容易漏。

好了,I2C总线驱动开发的核心内容就这些。从协议到接口,再到实战,咱们走了一遍完整的流程。你把这个SHT30的驱动跑通了,其他I2C传感器基本就是换汤不换药。记住那几个关键点:地址左移、命令正确、等待时间够、换算公式别搞错。嗯,动手试试吧。