1. I2C总线技术概述
I2C(Inter-Integrated Circuit)总线是飞利浦半导体(现恩智浦)在1980年代开发的一种同步串行通信协议。作为嵌入式系统中最常用的通信接口之一,I2C以其简洁的两线制结构(SDA数据线和SCL时钟线)和多主多从的拓扑能力,在传感器、EEPROM、RTC等低速外设连接中占据主导地位。
在实际工程中,I2C系统通常包含三个核心组件:适配器驱动(Adapter Driver)负责硬件控制器操作,客户端驱动(Client Driver)实现设备功能,以及连接二者的I2C核心层。这种分层架构使得硬件差异对上层透明,开发者可以专注于设备功能实现。以常见的BMP280气压传感器为例,其典型工作流程是:客户端驱动通过I2C核心向适配器发送读写请求,适配器将请求转换为具体的时序信号,最终通过物理总线与传感器交互。
2. I2C适配器驱动开发详解
2.1 硬件控制器初始化
现代SoC通常集成多个I2C控制器,如树莓派4B的Broadcom BCM2711就包含7个独立I2C接口。适配器驱动的首要任务是初始化这些硬件控制器。以Linux内核为例,开发者需要实现struct i2c_adapter结构体,其中关键操作包括:
c复制static struct i2c_algorithm bcm2835_i2c_algo = {
.master_xfer = bcm2835_i2c_xfer,
.functionality = bcm2835_i2c_func,
};
static int bcm2835_i2c_probe(struct platform_device *pdev) {
struct bcm2835_i2c_dev *i2c_dev;
// 1. 映射寄存器地址
i2c_dev->regs = devm_platform_ioremap_resource(pdev, 0);
// 2. 配置时钟分频(计算示例)
div = (clk_rate / bus_clk_rate) / 2;
writel(div, i2c_dev->regs + BCM2835_I2C_DIV);
// 3. 注册中断处理
ret = devm_request_irq(&pdev->dev, irq, bcm2835_i2c_isr, 0, ...);
// 4. 注册适配器
i2c_dev->adapter.algo = &bcm2835_i2c_algo;
i2c_set_adapdata(&i2c_dev->adapter, i2c_dev);
return i2c_add_adapter(&i2c_dev->adapter);
}
关键提示:时钟配置需特别注意。例如当主频为150MHz,目标总线速率100kHz时,分频系数应为(150000000/100000)/2=750。实际值需写入DIV寄存器前减去1(749)。
2.2 数据传输实现
master_xfer函数是适配器核心,负责将I2C消息转换为具体时序。以下是典型实现框架:
c复制static int bcm2835_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num) {
// 1. 启动条件
writel(BCM2835_I2C_C_ST | BCM2835_I2C_C_I2CEN,
i2c_dev->regs + BCM2835_I2C_C);
for (i = 0; i < num; i++) {
// 2. 设置从机地址和方向
addr = (msgs[i].addr << 1) | !!(msgs[i].flags & I2C_M_RD);
writel(addr, i2c_dev->regs + BCM2835_I2C_A);
// 3. 数据传输
if (msgs[i].flags & I2C_M_RD) {
// 读模式处理
for (j = 0; j < msgs[i].len; j++) {
val = readb(i2c_dev->regs + BCM2835_I2C_FIFO);
msgs[i].buf[j] = val;
}
} else {
// 写模式处理
for (j = 0; j < msgs[i].len; j++) {
writeb(msgs[i].buf[j],
i2c_dev->regs + BCM2835_I2C_FIFO);
}
}
}
// 4. 停止条件
writel(BCM2835_I2C_C_STP | BCM2835_I2C_C_I2CEN,
i2c_dev->regs + BCM2835_I2C_C);
return num;
}
常见问题排查:
- 总线锁死:通常因异常中断导致,可通过拉低SCL线产生9个时钟脉冲恢复
- ACK丢失:检查设备地址是否正确、供电是否稳定
- 时钟拉伸超时:适当调整适配器的timeout参数
3. I2C客户端驱动开发实战
3.1 设备树绑定与匹配
现代Linux驱动优先采用设备树描述硬件连接。以MPU6050陀螺仪为例,其设备树节点如下:
dts复制&i2c1 {
mpu6050: imu@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio>;
interrupts = <23 IRQ_TYPE_EDGE_RISING>;
i2c-gate {
#address-cells = <1>;
#size-cells = <0>;
ax8975: compass@c {
compatible = "ak,ak8975";
reg = <0x0c>;
};
};
};
};
驱动中通过of_match_table实现匹配:
c复制static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "invensense,mpu6050" },
{}
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match);
static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
.of_match_table = mpu6050_of_match,
},
.probe = mpu6050_probe,
.remove = mpu6050_remove,
};
3.2 传感器数据采集实现
典型的传感器驱动包含以下核心功能:
c复制static int mpu6050_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask) {
struct mpu6050 *mpu = iio_priv(indio_dev);
switch (mask) {
case IIO_CHAN_INFO_RAW:
// 1. 启动测量(写入配置寄存器)
ret = i2c_smbus_write_byte_data(mpu->client,
MPU6050_REG_ACCEL_CONFIG,
0x01);
// 2. 读取6字节数据(X/Y/Z各2字节)
ret = i2c_smbus_read_i2c_block_data(mpu->client,
MPU6050_REG_ACCEL_XOUT_H,
6, data);
// 3. 数据转换(16位有符号数)
*val = sign_extend32(be16_to_cpu(data[chan->scan_index*2]), 15);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
// 量程处理
*val = 0;
*val2 = mpu->scale;
return IIO_VAL_INT_PLUS_MICRO;
}
return -EINVAL;
}
性能优化技巧:对于高频采样场景,建议:
- 使用i2c_smbus_read_i2c_block_data批量读取
- 启用传感器的FIFO模式减少中断次数
- 在probe阶段预加载常用配置
4. I2C子系统高级应用
4.1 多设备管理策略
当单条总线上挂载多个设备时,需特别注意:
- 地址冲突检测:通过i2c-detect工具扫描(需加载i2c-dev模块):
bash复制# 扫描I2C-1总线上的设备
i2cdetect -y 1
- 速率协商:低速设备(如EEPROM)可能限制整条总线速率。可通过设备树单独配置:
dts复制&i2c1 {
clock-frequency = <100000>; // 默认100kHz
eeprom: eeprom@50 {
reg = <0x50>;
compatible = "atmel,24c02";
// 此设备仅支持400kHz
maximum-frequency = <400000>;
};
};
4.2 调试与性能分析
内核提供多种调试手段:
- 动态日志等级控制:
bash复制# 启用I2C核心调试信息
echo 8 > /sys/module/i2c_core/parameters/debug
- 时序分析(需逻辑分析仪配合):
c复制// 在驱动中添加调试点
dev_dbg(&client->dev, "Transfer %d messages\n", num);
for (i = 0; i < num; i++) {
dev_dbg(&client->dev, "msg[%d] %s len=%d\n",
i, (msgs[i].flags & I2C_M_RD) ? "read" : "write",
msgs[i].len);
}
- 性能统计:
bash复制cat /sys/kernel/debug/i2c/1/status
# 输出示例:
I2C Adapter: bcm2835 (i2c@7e804000)
Transfers: 1245
Errors: 3
Timing:
Avg write: 1.2ms
Max write: 8.5ms
5. 典型传感器应用案例
5.1 环境监测系统搭建
组合使用BME280(温湿度气压)和VEML6075(UV)传感器的示例:
c复制struct sensor_data {
struct i2c_client *bme;
struct i2c_client *veml;
};
static int env_probe(struct i2c_client *client) {
// 1. 初始化BME280
bme = devm_i2c_new_dummy_device(&client->dev, client->adapter, 0x76);
ret = i2c_smbus_write_byte_data(bme, BME280_REG_CTRL_HUM, 0x01);
// 2. 初始化VEML6075
veml = devm_i2c_new_dummy_device(&client->dev, client->adapter, 0x10);
ret = i2c_smbus_write_word_data(veml, VEML6075_REG_CONF, 0x02);
// 3. 创建sysfs接口
sysfs_create_group(&client->dev.kobj, &env_attr_group);
}
5.2 工业传感器网络
在工业现场常需长距离传输,此时需注意:
- 总线电容控制:每增加1米线缆约增加50-100pF电容,总电容应小于400pF
- 上拉电阻计算:根据总线电压和速率选择,5V/100kHz通常用4.7kΩ
- 隔离方案:采用ADUM1250等隔离芯片,防止地环路干扰
实际部署参数示例:
- 线缆长度:15米(双绞线)
- 计算上拉电阻:
code复制Rmin = (Vdd - 0.4) / 3mA ≈ 1.53kΩ (for 5V) Rmax = 1000ns / (Cbus * 0.8473) = 1000 / (1500pF * 0.8473) ≈ 787Ω // 取中间值2.2kΩ
