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 = ®_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从设备驱动,其实有个固定套路。我总结了四步法,你照着来,基本不会出错。
- 设备注册:在HDF框架中注册设备,绑定控制器和从设备地址。
- 初始化序列:发送初始化命令,配置从设备的工作模式。
- 读写接口封装:封装
I2cReadReg/I2cWriteReg等底层操作函数。 - 业务逻辑实现:基于读写接口,实现具体的功能(如读取温湿度)。
// 从设备驱动模板框架
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总线驱动开发的核心内容就这些。从数据结构到消息传输,再到从设备驱动编写,最后用SHT30实战收尾。你写驱动时,只要按照这个脉络走,基本不会跑偏。记住,调试I2C设备时,先用逻辑分析仪抓波形,确认地址和命令对不对,再查代码逻辑。这是我多年总结的经验,能省你不少时间。