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

I2C总线,说白了就是嵌入式世界里最常用的"两线制"通信方式。一根时钟线SCL,一根数据线SDA,就能挂载一堆外设。我在项目中遇到过不少开发者,上来就调SHT30,结果连I2C核心数据结构都没搞明白,折腾半天读回来的全是0xFF。嗯,咱们先把地基打牢。

4.1 I2C核心数据结构:驱动开发的"骨架"

鸿蒙的I2C驱动框架,核心就几个结构体。你把这些搞懂了,写任何I2C从设备驱动都顺手。

结构体 作用 关键字段
I2cCntlr I2C控制器实例,代表一个硬件I2C总线 busId、ops、resource
I2cMsg 一次I2C消息传输的载体 addr、flags、len、buf
I2cMethod 控制器操作函数集,即底层硬件接口 transfer、setConfig、getConfig

我个人习惯,拿到一个新平台,第一件事就是打印I2cCntlr里的busId和resource信息。为什么?因为很多坑都出在总线号映射错误上。

核心要点:鸿蒙的I2C驱动采用"控制器-消息-方法"三层架构。控制器管理硬件资源,消息描述一次传输,方法实现具体操作。三者缺一不可。

4.2 I2C消息传输:数据是怎么"走"起来的

I2C消息传输,说白了就是组装I2cMsg结构体数组,然后调用控制器的transfer函数。你想想看,一次完整的读写操作,往往需要多条消息组合完成。

// 典型的I2C消息传输流程
struct I2cMsg msgs[2];
uint8_t reg_addr = 0x10;
uint8_t recv_buf[2];

// 第一条消息:写寄存器地址
msgs[0].addr = SHT30_ADDR;
msgs[0].flags = 0;          // 写标志
msgs[0].len = 1;
msgs[0].buf = &reg_addr;

// 第二条消息:读数据
msgs[1].addr = SHT30_ADDR;
msgs[1].flags = I2C_FLAG_READ;  // 读标志
msgs[1].len = 2;
msgs[1].buf = recv_buf;

// 执行传输
int ret = I2cTransfer(cntlr, msgs, 2);
if (ret != 2) {
    // 处理错误
}

我曾经在一个项目里,因为flags字段没设置I2C_FLAG_READ,结果读回来的数据全是0。排查了半天,最后发现是消息标志位搞反了。这种低级错误,新手最容易犯。

小技巧:I2C消息传输支持"重复起始条件"(Restart)。如果你需要连续读写,记得在消息之间不要发停止信号。鸿蒙的I2cTransfer默认会帮你处理好,但前提是消息数组要连续。

4.3 I2C从设备驱动编写:模板化开发

写I2C从设备驱动,其实有个固定套路。我总结了四步法,你照着来,基本不会出错。

  1. 设备注册:在HDF框架中注册设备,绑定控制器和从设备地址。
  2. 初始化序列:发送初始化命令,配置从设备的工作模式。
  3. 读写接口封装:封装I2cReadReg/I2cWriteReg等底层操作函数。
  4. 业务逻辑实现:基于读写接口,实现具体的功能(如读取温湿度)。
// 从设备驱动模板框架
struct Sht30Device {
    struct I2cCntlr *cntlr;   // I2C控制器
    uint16_t addr;            // 从设备地址
    uint8_t status;           // 设备状态
};

// 初始化函数
int32_t Sht30Init(struct Sht30Device *dev)
{
    // 1. 获取控制器
    dev->cntlr = I2cCntlrGet(dev->busId);
    if (dev->cntlr == NULL) {
        return HDF_FAILURE;
    }

    // 2. 发送初始化命令
    uint8_t init_cmd[] = {0x2C, 0x06};  // 高重复性测量
    return I2cWrite(dev->cntlr, dev->addr, init_cmd, sizeof(init_cmd));
}

注意:I2C从设备地址通常是7位,但有些芯片手册给的是8位地址(左移了一位)。写驱动时一定要确认清楚。我见过有人把0x44写成了0x88,结果设备死活不响应。

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

SHT30是Sensirion家的经典温湿度传感器,精度高、功耗低。咱们一步步把它驱动起来。

4.4.1 SHT30关键参数

参数 值 说明
I2C地址 0x44 (ADDR引脚接GND) 7位地址,左移1位后为0x88
测量命令 0x2C 0x06 高重复性,时钟拉伸使能
数据长度 6字节 温度2字节 + CRC1字节 + 湿度2字节 + CRC1字节
转换时间 约15ms 高重复性模式下

4.4.2 驱动实现步骤

我个人习惯,先把底层读写封装好,再写业务逻辑。这样调试起来方便。

// SHT30温湿度读取函数
int32_t Sht30ReadTH(struct Sht30Device *dev, float *temp, float *humi)
{
    uint8_t cmd[] = {0x2C, 0x06};
    uint8_t data[6] = {0};
    struct I2cMsg msgs[2];

    // 发送测量命令
    msgs[0].addr = dev->addr;
    msgs[0].flags = 0;
    msgs[0].len = 2;
    msgs[0].buf = cmd;

    // 等待转换完成(实际项目中建议用延时或查询)
    OsalMSleep(20);

    // 读取数据
    msgs[1].addr = dev->addr;
    msgs[1].flags = I2C_FLAG_READ;
    msgs[1].len = 6;
    msgs[1].buf = data;

    int32_t ret = I2cTransfer(dev->cntlr, msgs, 2);
    if (ret != 2) {
        return HDF_FAILURE;
    }

    // 数据转换(SHT30原始数据转物理值)
    uint16_t raw_temp = (data[0] << 8) | data[1];
    uint16_t raw_humi = (data[3] << 8) | data[4];

    *temp = -45.0f + 175.0f * raw_temp / 65535.0f;
    *humi = 100.0f * raw_humi / 65535.0f;

    return HDF_SUCCESS;
}

避坑指南:SHT30的CRC校验很多人会忽略。虽然大部分时候数据是对的,但一旦遇到干扰,没有CRC校验你根本不知道数据是错的。我曾经在一个工业现场项目里,就是因为没做CRC校验,导致温湿度数据偶尔跳变,排查了整整两天。

4.4.3 驱动注册与HDF适配

在鸿蒙HDF框架中,你需要实现Bind、Init、Release三个接口。这里我给出一个简化版本:

// HDF驱动入口
struct HdfDriverEntry g_sht30DriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_I2C_SHT30",
    .Bind = Sht30Bind,
    .Init = Sht30Init,
    .Release = Sht30Release,
};

HDF_INIT(g_sht30DriverEntry);

嗯,这里要注意:moduleName必须和device_info.hcs配置文件中的名字一致。否则驱动加载时会报"找不到设备"的错误。

4.5 知识体系总览

下面这张图,把I2C驱动开发的整体脉络梳理清楚了。你写驱动时,可以对照着看,不容易漏掉关键环节。

I2C总线驱动开发知识体系 I2C核心数据结构 I2cCntlr(控制器) I2cMsg(消息) I2cMethod(方法) I2C消息传输机制 I2C从设备驱动编写 实战:SHT30温湿度传感器 设备注册 初始化序列 读写封装 业务逻辑 核心思路:从数据结构到消息传输,再到设备驱动,最后落地到具体传感器

好了,I2C总线驱动开发的核心内容就这些。从数据结构到消息传输,再到从设备驱动编写,最后用SHT30实战收尾。你写驱动时,只要按照这个脉络走,基本不会跑偏。记住,调试I2C设备时,先用逻辑分析仪抓波形,确认地址和命令对不对,再查代码逻辑。这是我多年总结的经验,能省你不少时间。