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通信基本流程:
- 主机发送起始条件
- 主机发送7位从设备地址 + 1位读写位(0写1读)
- 从设备回复ACK
- 主机发送/接收数据字节,每字节后跟ACK
- 主机发送停止条件
这里有个坑,我曾经踩过:地址左移。很多芯片手册上写的地址是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,流程都差不多:
- 初始化:打开I2C控制器,配置从设备地址。
- 写寄存器:发送寄存器地址 + 要写入的数据。
- 读寄存器:先写寄存器地址,再读数据。
- 数据处理:把裸数据换算成物理量(温度、湿度等)。
我一般会把驱动拆成两个文件:一个 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的读写过程串在了一起。
这张图把整个流程分成了6步,从打开I2C控制器到最终拿到温湿度值。右侧的说明框里,我把关键参数和常见坑都列出来了。你写驱动时,可以对照着这张图一步步来,不容易漏。
好了,I2C总线驱动开发的核心内容就这些。从协议到接口,再到实战,咱们走了一遍完整的流程。你把这个SHT30的驱动跑通了,其他I2C传感器基本就是换汤不换药。记住那几个关键点:地址左移、命令正确、等待时间够、换算公式别搞错。嗯,动手试试吧。