在嵌入式系统开发中,I2C (Inter-Integrated Circuit) 总线作为一种常用的串行通信协议,被广泛应用于各种传感器、EEPROM 等外设的连接。本文将深入探讨嵌入式第六十六天(I2C子系统架构),从底层原理到驱动实现,再到实际调试,帮助开发者全面掌握 I2C 总线的使用。
I2C 总线底层原理剖析
I2C 总线是一种双线制串行总线,只需要两根信号线即可实现设备之间的通信:SDA (Serial Data) 用于传输数据,SCL (Serial Clock) 用于提供时钟信号。I2C 总线采用主从模式,由主设备控制总线的通信。主设备可以发起读写操作,从设备则响应主设备的请求。I2C 的物理层实现比较简单,但是协议层却比较复杂,需要理解起始位、停止位、地址帧、数据帧、ACK/NACK 等概念。
I2C 通信时序
I2C 通信的时序非常重要,任何错误的时序都可能导致通信失败。以下是 I2C 常见的通信时序:
- 起始位 (Start Condition):SCL 保持高电平,SDA 从高电平切换到低电平。
- 停止位 (Stop Condition):SCL 保持高电平,SDA 从低电平切换到高电平。
- 地址帧 (Address Frame):主设备发送从设备地址,以及读写位(R/W)。
- 数据帧 (Data Frame):主设备或从设备发送 8 位数据。
- ACK/NACK 位 (Acknowledge/Not Acknowledge Bit):接收方在接收到每个字节后,发送一个 ACK 位表示成功接收,或者发送一个 NACK 位表示接收失败。
I2C 地址
每个 I2C 设备都有一个唯一的地址,主设备通过地址来选择要通信的从设备。I2C 地址可以是 7 位或 10 位,常见的 I2C 设备通常使用 7 位地址。地址的最高位用于标识设备类型,低位用于区分同一类型的设备。需要仔细查阅 I2C 设备的数据手册,才能正确设置 I2C 地址。
Linux I2C 子系统架构
Linux 内核提供了一个完善的 I2C 子系统,方便驱动开发者使用 I2C 总线。I2C 子系统主要由以下几部分组成:
- I2C 核心层 (I2C Core):提供 I2C 总线的基本功能,例如注册和注销 I2C 设备、分配 I2C 总线号等。
- I2C 总线驱动 (I2C Bus Driver):负责控制 I2C 控制器硬件,实现 I2C 总线的通信时序。常见的 I2C 控制器包括 GPIO 模拟 I2C 和硬件 I2C 控制器。
- I2C 设备驱动 (I2C Device Driver):负责与 I2C 设备进行通信,例如读取传感器数据、写入 EEPROM 数据等。
- I2C 适配器 (I2C Adapter):I2C 适配器连接 I2C 总线和 I2C 设备,是 I2C 子系统中的重要组成部分。
I2C 设备树配置
在设备树中,需要配置 I2C 控制器和 I2C 设备的信息。以下是一个示例的 I2C 设备树配置:
&i2c1 {
status = "okay";
scl-pin = <&gpio1 1>;
sda-pin = <&gpio1 2>;
sensor@50 {
compatible = "invensense,mpu6050";
reg = <0x50>; // I2C 设备地址
};
};
这段代码定义了 I2C1 控制器,并配置了 SCL 和 SDA 引脚。同时,还定义了一个 I2C 设备 sensor@50,指定了设备的兼容性和 I2C 地址。
I2C 驱动代码示例
以下是一个简单的 I2C 设备驱动代码示例:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, mpu6050_id);
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("mpu6050_probe\n");
return 0;
}
static void mpu6050_remove(struct i2c_client *client)
{
printk("mpu6050_remove\n");
}
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,
.id_table = mpu6050_id,
};
module_i2c_driver(mpu6050_driver);
MODULE_AUTHOR("linuxer_zhao");
MODULE_LICENSE("GPL");
这段代码定义了一个 I2C 驱动,用于控制 MPU6050 传感器。驱动程序中包含了 probe 函数和 remove 函数,分别在设备连接和断开时被调用。
I2C 调试经验总结
在实际开发中,I2C 调试可能会遇到各种问题。以下是一些常见的调试经验:
- 检查 I2C 地址是否正确:使用 I2C 工具(例如
i2cdetect)扫描 I2C 总线,确认设备地址是否正确。 - 检查 I2C 时序是否符合规范:使用示波器观察 SCL 和 SDA 信号,确认时序是否符合 I2C 规范。
- 检查设备树配置是否正确:确认设备树中 I2C 控制器和设备的配置是否正确。
- 检查驱动代码是否存在错误:仔细检查驱动代码,确认是否存在逻辑错误。
- 注意上下拉电阻:很多 I2C 设备都需要外部上下拉电阻,否则可能无法正常工作。
掌握了以上 I2C 子系统架构的知识,并积累了丰富的调试经验,相信你一定能够轻松应对 I2C 相关的问题。
冠军资讯
linuxer_zhao