4. I2C总线驱动开发:从协议到实战
I2C总线,说白了就是嵌入式设备里最常用的“短距离串行聊天协议”。两根线——SCL(时钟)和SDA(数据),就能挂一堆传感器、存储器、IO扩展芯片。我在鸿蒙南向开发中,几乎每个传感器项目都离不开它。
这一章,咱们从协议基础讲起,一路走到控制器驱动、设备驱动,最后拿一个温度传感器练练手。嗯,内容有点多,但都是干货。
4.1 I2C协议基础:两根线的默契
I2C是主从结构。主设备(通常是SoC)产生时钟,从设备(传感器)响应。每次通信以“起始条件”开始,以“停止条件”结束。
关键信号时序:
- 起始条件(S):SCL高电平时,SDA从高变低。
- 停止条件(P):SCL高电平时,SDA从低变高。
- 数据采样:SCL低电平时SDA变化,SCL高电平时采样数据。
- 应答(ACK):每字节后接收方拉低SDA表示收到。
地址呢?标准模式是7位地址,后面跟1位读写位。0表示写,1表示读。举个例子:一个温度传感器地址是0x48,你要读它,发送的地址字节就是0x48 << 1 | 0x01 = 0x91。
我的经验:很多新手搞混了“设备地址”和“发送的地址字节”。设备地址是7位,发送时左移一位,最低位填读写标志。我在项目中见过有人直接拿7位地址去发,结果设备死活不应答。
I2C的速率分几种:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。传感器驱动一般用100kHz或400kHz,够用了。速率太快,线长一点就容易出问题。
| 模式 | 最大速率 | 适用场景 |
|---|---|---|
| 标准模式 | 100 kHz | 大多数传感器、EEPROM |
| 快速模式 | 400 kHz | 高速传感器、音频编解码器 |
| 高速模式 | 3.4 MHz | 视频、大容量存储 |
4.2 I2C控制器驱动:鸿蒙的I2C核心
在鸿蒙里,I2C控制器驱动属于HDF(硬件驱动框架)的Platform层。说白了,它负责管理SoC内部的I2C外设——配置时钟、处理中断、收发数据。
控制器驱动的核心接口就几个:
I2cOpen:打开一个I2C控制器,返回句柄。I2cTransfer:执行一次I2C传输(包含起始、数据、停止)。I2cClose:关闭控制器。
鸿蒙把I2C传输抽象成“消息队列”。你准备好一个I2cMsg结构体数组,每个结构体描述一段数据的方向、长度和标志。然后调用I2cTransfer一次性发出去。
struct I2cMsg {
uint16_t addr; // 从设备地址(7位,不包含读写位)
uint16_t len; // 数据长度
uint8_t *buf; // 数据缓冲区
uint16_t flags; // 标志:I2C_FLAG_READ(读)或 0(写)
};
注意:addr字段是7位设备地址,不是左移后的字节。鸿蒙内部会帮你左移并加上读写位。我曾经在移植驱动时,自己手动左移了一次,结果地址变成0x90,设备直接不理我。
控制器驱动的初始化流程,我总结了一下:
- 设备树或HCS配置中获取I2C控制器的基地址、中断号、时钟频率。
- 映射寄存器地址,复位I2C外设。
- 配置时钟分频,设置SCL频率。
- 注册中断处理函数(如果使用中断模式)。
- 调用
PlatformDeviceAdd注册到HDF框架。
4.3 I2C设备驱动:让传感器开口说话
设备驱动是跑在控制器驱动之上的。它不关心寄存器怎么配,只关心怎么发数据、收数据。鸿蒙里,设备驱动通过HDF的I2cCntlr句柄来操作。
典型的I2C设备驱动结构:
- Probe函数:获取I2C控制器句柄,初始化设备(比如配置寄存器、校准)。
- 读写函数:封装
I2cTransfer,实现寄存器读写。 - 上报函数:将传感器数据通过HDF的Input或Sensor模型上报。
读写一个寄存器,通常需要两步:先写寄存器地址,再读数据。鸿蒙里可以用两个I2cMsg组合:
int32_t ReadReg(uint16_t devAddr, uint8_t regAddr, uint8_t *value) {
struct I2cMsg msgs[2];
uint8_t regBuf = regAddr;
msgs[0].addr = devAddr;
msgs[0].len = 1;
msgs[0].buf = ®Buf;
msgs[0].flags = 0; // 写
msgs[1].addr = devAddr;
msgs[1].len = 1;
msgs[1].buf = value;
msgs[1].flags = I2C_FLAG_READ; // 读
return I2cTransfer(g_i2cHandle, msgs, 2);
}
避坑指南:有些传感器支持“重复起始条件”(Restart),即写地址后不发送停止,直接发读地址。鸿蒙的I2cTransfer默认会在消息之间插入停止条件。如果你需要Restart,得把两个消息合并成一个,或者使用支持Restart的控制器驱动。我遇到过一颗气压传感器,必须用Restart才能读到正确数据,折腾了半天。
4.4 实战:读写温度传感器
咱们拿一个常见的温度传感器——LM75A来练手。它的I2C地址是0x48(7位),内部寄存器如下:
| 寄存器地址 | 名称 | 描述 |
|---|---|---|
| 0x00 | 温度寄存器 | 只读,11位温度数据(补码) |
| 0x01 | 配置寄存器 | 读写,控制工作模式 |
| 0x02 | THYST | 读写,迟滞温度 |
| 0x03 | TOS | 读写,过温阈值 |
温度数据格式:16位寄存器,高11位是有效数据,低5位为0。分辨率0.125°C。正温度直接读,负温度用补码。
驱动实现步骤:
- 在HCS配置中声明LM75A设备,绑定I2C控制器。
- 实现Probe函数:打开I2C控制器,读取设备ID(如果有)确认连接。
- 实现温度读取函数:读寄存器0x00,解析11位数据。
- 通过HDF的Sensor模型上报温度值。
核心代码片段:
static int32_t Lm75aReadTemp(int16_t *temp) {
uint8_t regAddr = 0x00;
uint8_t buf[2] = {0};
struct I2cMsg msgs[2];
msgs[0].addr = LM75A_ADDR;
msgs[0].len = 1;
msgs[0].buf = ®Addr;
msgs[0].flags = 0;
msgs[1].addr = LM75A_ADDR;
msgs[1].len = 2;
msgs[1].buf = buf;
msgs[1].flags = I2C_FLAG_READ;
int32_t ret = I2cTransfer(g_i2cHandle, msgs, 2);
if (ret != 0) {
return ret;
}
// 解析11位补码数据
int16_t raw = (int16_t)((buf[0] << 8) | buf[1]);
raw = raw >> 5; // 右移5位,得到11位有效数据
if (raw & 0x0400) { // 符号位判断
raw |= 0xF800; // 扩展符号位
}
*temp = raw; // 单位:0.125°C
return 0;
}
注意:解析补码时,一定要做符号扩展。我见过有人直接取高11位,负数就变成很大的正数。温度显示85°C,其实实际是-10°C。
下面这张图,展示了整个I2C驱动栈的层次关系:
实际项目中,我建议你在Probe函数里加一个“设备检测”步骤——写一个测试寄存器,读回来对比。这样能快速确认I2C通信是否正常。我曾经在一款开发板上,I2C引脚被复用成了GPIO,折腾了两天才发现。