17. I2C 平台驱动
Linux 内核中的 I2C 平台驱动(Platform Driver)是 I2C 框架中用于支持不同硬件平台的核心部分。它的主要目标是为具体的 I2C 硬件控制器(即 I2C 主机控制器或 I2C 适配器)提供一种通用的驱动模型,使得 I2C 控制器可以被抽象并集成到 Linux I2C 子系统中。
17.1 Linux I2C 平台总线
Linux内核通过使用总线的概念来处理设备,即CPU与这些设备之间的连接。一些总线足够智能,嵌入了可发现性逻辑来枚举其上的设备。在启动阶段早期,Linux内核会请求这些总线提供它们已枚举的设备以及这些设备正常工作所需的资源(如中断线和内存区域)。PCI、USB和SATA总线都属于这种可发现总线的范畴。
然而,现实并不总是如此美好。有许多设备CPU仍然无法检测到。大多数这些不可发现的设备都是片上设备,尽管其中一些位于不支持设备可发现性的慢速或简单总线上。
因此,内核必须提供机制来接收有关硬件的信息,用户必须告知内核这些设备的位置。在Linux内核中,这些不可发现的设备被称为平台设备。因为它们不位于已知的总线上,如I2C、SPI或任何不可发现的总线上,Linux内核实现了平台总线(也称为伪平台总线)的概念,以保持设备始终通过总线连接到CPU的范式。
17.2 I2C Platform Driver 架构
Linux I2C platform driver由以下几部分组成:
1). i2c-core-base.c:这是 I2C 核心的主要文件,包含了 I2C 适配器和设备的注册、探测和数据传输等核心功能.
2). i2c-core-acpi.c:处理与 ACPI 相关的 I2C 设备.
3). i2c-core-of.c:处理与设备树(Device Tree)相关的 I2C 设备.
4). i2c-core-smbus.c:处理 SMBus(系统管理总线)相关的功能.
5.) i2c-dev.c:提供用户空间与 I2C 设备交互的接口.
6.) i2c-algo-bit.c: 实现具体的 I2C 控制器算法.
框架图如下:
+--------------------------------------------------+
| User Space |
|--------------------------------------------------|
| /dev/i2c-x (e.g., sensor, eeprom) |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| i2c-dev character driver |
|--------------------------------------------------| <----- kernel space
| open/read/write/ioctl |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| I2C Core Subsystem | <------ I2C 核心框架
|--------------------------------------------------|
| i2c_transfer(), i2c_smbus_xfer(), etc. |
| 管理适配器和设备之间的交互 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Platform Driver (i2c_algorithm) | <------ Platform Driver 实现了 I2C 控制器算法
| probe(), remove(), master_xfer(), functionality |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| I2C Adapter (i2c_adapter) | <------ 适配器层,抽象了 I2C 控制器
| i2c_add_adapter(), i2c_del_adapter() |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Client Device (Hardware-specific) | <------ 客户端设备定义(设备树、ACPI)
| 设备树 (Device Tree)/ ACPI 定义硬件资源 |
| 包括寄存器、IRQ 中断、时钟等资源 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| I2C Controller (Hardware) | <------ 硬件 I2C 控制器
| I2C 总线的具体硬件实现 |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| I2C Bus (Physical Layer) | <------ I2C 总线
| SDA (数据线), SCL (时钟线) |
+--------------------------------------------------+
17.2.1 Application
用户空间的应用程序通过I2C子系统与I2C设备进行通信。
用户空间调用 open: 应用程序调用 open("/dev/i2c-X", ...),这里的 X 是 I2C 总线的编号。例如,如果要访问 I2C 总线 1,则会调用 open("/dev/i2c-1", ...)。
17.2.3 i2c-dev
提供用户空间访问I2C设备的接口。
i2c-dev是字符设备, 在内核启动时,i2c-dev 模块会被加载并注册字符设备,通常是 /dev/i2c-X 的形式。这个注册过程包括调用 class_create() 和 device_create() 来创建相应的设备文件。 当 open 被调用时,内核会找到相应的字符设备驱动(i2c-dev)并调用其 open 方法。这个方法通常会进行如下操作:
static int i2cdev_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct i2c_client *client;
struct i2c_adapter *adap;
adap = i2c_get_adapter(minor);
if (!adap)
return -ENODEV;
/* This creates an anonymous i2c_client, which may later be
* pointed to some address using I2C_SLAVE or I2C_SLAVE_FORCE.
*
* This client is ** NEVER REGISTERED ** with the driver model
* or I2C core code!! It just holds private copies of addressing
* information and maybe a PEC flag.
*/
client = kzalloc(sizeof(*client), GFP_KERNEL);
if (!client) {
i2c_put_adapter(adap);
return -ENOMEM;
}
snprintf(client->name, I2C_NAME_SIZE, "i2c-dev %d", adap->nr);
client->adapter = adap;
file->private_data = client;
return 0;
}
获取 I2C 适配器: 在 i2c_dev_open 中,内核使用 iminor(inode) 函数获取设备的次设备号,该号对应于 I2C 适配器的编号。接下来创建匿名 I2C 客户端, 这个客户端不会被注册到 I2C 核心,而是用于持有地址信息。在接收到 ioctl 调用后,驱动程序会更新匿名 i2c_client 的设备地址,以便后续的 I2C 操作可以与实际的设备进行通信。
在成功获取到 i2c_client 后,内核将其指针存储在 file->private_data 中。 应用程序接下来的读写操作会调用相应的文件操作函数(如 read, write, ioctl),这些函数会使用 file->private_data 获取绑定的 i2c_client,并通过它与 I2C 总线进行实际的数据交互。
17.2.4 i2c-core
i2c-core 是 Linux 内核中的核心 I2C 框架,负责管理 I2C 适配器和 I2C 设备的注册、匹配和通信。它提供了一个标准化的接口,允许 I2C 适配器和设备驱动程序之间的高效交互。
17.2.4.1 I2C 适配器的管理
注册适配器
当 I2C 适配器被创建时,驱动程序通过调用 i2c_add_adapter() 函数来注册适配器。
int i2c_add_adapter(struct i2c_adapter *adapter);
适配器结构体
i2c_adapter 结构体包含适配器的属性,如适配器名称、速率、操作函数(如 master_xfer),以及指向其上注册的 I2C 设备的链表。
适配器的注销
使用 i2c_del_adapter() 函数注销适配器。
void i2c_del_adapter(struct i2c_adapter *adapter);
注销时,i2c-core 会清理与适配器相关的资源,确保没有剩余的 I2C 设备和引用。
17.2.4.2 I2C设备的管理
注册Client设备
I2C 设备通过调用 i2c_new_device() 或 i2c_register_device() 注册到系统中。
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info *info);
设备结构体
i2c_client 结构体表示一个具体的 I2C 设备,包含设备地址、设备名称、指向适配器的指针等信息。
设备的注销
使用 i2c_unregister_device() 函数注销设备 。
void i2c_unregister_device(struct i2c_client *client);
注销时,i2c-core 会释放与设备相关的资源,并确保设备不再被访问。
设备匹配
I2C 驱动程序通过 MODULE_DEVICE_TABLE(i2c, ...) 宏定义与特定设备的匹配关系。
在适配器注册时,i2c-core 会检查所有已注册的驱动,尝试将它们与当前适配器上的设备匹配。如果匹配成功,驱动的 probe 函数会被调用,驱动程序将对设备进行初始化。
I2C 传输
i2c_transfer() 函数用于执行 I2C 数据传输。它根据 i2c_client 和 i2c_adapter 进行数据发送和接收。
int i2c_transfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num);
可以在一次传输中发送多个消息,i2c-core 会处理这些消息的顺序和传输细节。
事件通知
当设备状态发生变化时,i2c-core 可以通知相应的驱动程序。这通常通过 devm 机制实现,驱动程序可以注册事件回调函数。
17.2.5 i2c-algo-bit
i2c-algo-bit 是 Linux 内核中的一个 I2C 算法驱动模块,主要用于通过软件模拟的方式控制 I2C 总线(即“bit-banging”)。它通过 GPIO(通用输入输出)引脚来手动控制 I2C 时钟线(SCL)和数据线(SDA)的高低电平,从而实现 I2C 数据的传输。
工作流程
1.) 初始化
在使用 i2c-algo-bit 时,驱动程序首先需要初始化 I2C 适配器,并指定 i2c-algo-bit 作为 I2C 控制器的算法。
典型的初始化流程:
2.) 发送起始条件
I2C 通信开始时,i2c-algo-bit 通过设置 SDA 线的电平低并保持 SCL 高电平,生成起始条件 (START condition)。具体步骤:
先确保 SDA 和 SCL 都为高电平。
将 SDA 线拉低,同时保持 SCL 高电平。
3.) 传输数据
数据传输按位进行,通常从高位到低位依次发送。在每一位数据的传输中:
驱动程序将数据位加载到 SDA 线上。
将 SCL 线拉高,表示数据位已经准备好。
将 SCL 线拉低,表示接收方可以读取数据位。
重复上述步骤,直到所有 8 位数据传输完毕
4.) 发送应答信号
在发送完数据字节后,发送方(通常是主机)需要等待从设备的应答信号(ACK)。i2c-algo-bit 会在此时释放 SDA 线,并检查从设备是否拉低 SDA(表示 ACK)。
5.) 发送停止条件
数据传输完成后,i2c-algo-bit 通过将 SCL 保持高电平并将 SDA 线拉高,生成停止条件 (STOP condition),结束 I2C 通信。具体步骤:
先保持 SCL 低电平。
将 SDA 线拉低,然后将 SCL 线拉高。
最后将 SDA 线拉高,表示通信结束
6.) 重复操作
根据 I2C 协议,主机可以发送多个字节数据,重复上述流程(发送起始条件、数据传输、应答和停止条件)来完成复杂的通信任务。
17.2.6 i2c-adapter
表示I2C主设备(控制器),负责与I2C设备进行通信。 查看第18章。
17.2.7 i2c-client
表示连接到I2C总线的设备。查看第19章。
17.2.8 i2c-driver
表示I2C设备驱动程序,负责初始化和管理I2C设备。查看第20章。
17.3 I2C 数据流程
在 Linux 中,用户空间的应用程序通过 I2C 访问硬件设备时,数据从用户空间逐步传递到内核空间,再到硬件设备。这一过程涉及多个层次,从用户空间的系统调用到内核的 I2C 驱动,再到最终的 I2C 硬件控制器。
1.) 应用程序通过标准的文件操作接口(如 open()、read()、write()、ioctl())来与 I2C 设备通信,通常是通过 /dev/i2c-X 这样的字符设备接口来访问。如下:
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, device_address); // 设置 I2C 从设备地址
read(fd, buffer, length); // 读取数据
2.) 接下来会进入内核的 I2C 字符设备驱动 i2c-dev。i2c-dev 负责将用户空间的请求转换为内核中的 I2C 核心 API 调用。
static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.read = i2cdev_read, // 读取操作
.write = i2cdev_write, // 写入操作
.unlocked_ioctl = i2cdev_ioctl, // IOCTL 操作
.open = i2cdev_open,
.release = i2cdev_release,
};
3.) 之后, i2c-dev 调用 I2C 核心子系统中的 API(如 i2c_transfer() 和 i2c_smbus_xfer()),这些 API 是内核中用于管理和协调所有 I2C 适配器(即 I2C 控制器)的接口。I2C 核心子系统是 I2C 框架的核心,它负责调度和管理对 I2C 总线的访问。
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int i2c_smbus_xfer(struct i2c_adapter *adap, u16 addr, unsigned short flags, char read_write, u8 command, int size, union i2c_smbus_data *data);
4.) 在I2C 核心子系统通过 i2c_adapter 结构与具体的 I2C 控制器交互。i2c_adapter 是一个抽象层,封装了底层硬件 I2C 控制器的细节。每个 I2C 控制器都有一个与之对应的 I2C 适配器,内核通过适配器执行 I2C 操作。
i2c_adapter 提供了方法(如 master_xfer())来处理具体的 I2C 数据传输,方法通常由平台驱动实现。
i2c_add_adapter() 用于注册适配器到 I2C 核心子系统中。
struct i2c_adapter {
struct module *owner;
unsigned int class;
struct i2c_algorithm *algo; // 指向 i2c_algorithm,定义控制器算法
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); // 传输函数
...
};
5.) 每个 I2C 控制器(例如 SoC 中的 I2C 控制器)都有其特定的硬件操作,这些操作通过 I2C 平台驱动来实现。i2c_algorithm 结构体定义了控制器使用的 I2C 传输算法(例如 bit-banging、硬件 I2C 控制器等)。
i2c_algorithm 提供了对硬件 I2C 控制器的抽象,包括 master_xfer() 函数和 functionality() 函数,分别用于传输数据和查询硬件功能。
平台驱动中实现的 master_xfer() 函数负责与具体的硬件控制器通信(如读写硬件寄存器,控制 I2C 时钟等)。
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); // 数据传输
u32 (*functionality)(struct i2c_adapter *); // 控制器的功能
};
6.) 平台驱动中的 master_xfer() 最终控制 I2C 硬件控制器,通过 I2C 硬件控制器发出 I2C 总线的信号,控制 SCL 和 SDA 线来完成 I2C 通信。
I2C 控制器硬件:I2C 控制器通过寄存器操作实现对 I2C 总线时序的控制,向从设备发起数据读写请求。
物理层:I2C 控制器操作 I2C 总线上的 SDA(数据线)和 SCL(时钟线),将数据写入或从设备读取。
7.) 最终,I2C 从设备(物理设备)接收到来自主机的 I2C 数据。I2C 驱动会通过 i2c_client 结构体与具体的 I2C 从设备交互,i2c_client 保存了 I2C 设备的地址和相关信息。
- i2c_client 是与 I2C 从设备通信的抽象,它包含从设备的地址、操作标志等信息。
struct i2c_client {
struct i2c_adapter *adapter; // 与之关联的适配器
u16 addr; // 从设备的地址
...
};
总之, 用户空间应用程序通过 read()、write() 或 ioctl() 访问 I2C 设备时,调用进入 I2C 字符设备驱动 i2c-dev,然后通过 I2C 核心子系统转发给相应的 I2C 适配器。适配器通过 I2C 平台驱动与硬件控制器通信,最终在物理 I2C 总线上与从设备完成数据传输。