Linux 内核发现 I2C 设备的过程主要涉及 I2C 总线适配器驱动、I2C 设备描述以及 I2C 设备驱动之间的匹配。以下是其工作原理的详细解释:
1. I2C 总线适配器(Master)驱动的注册
首先,系统中的 I2C 控制器(硬件,通常集成在 SoC 中)需要有对应的 Linux 驱动程序来管理。这个驱动被称为 I2C 总线适配器驱动(I2C Bus Adapter Driver) 或 I2C Master Driver。
功能:它负责与 I2C 硬件交互,提供 I2C 协议的底层实现,例如发送启动信号、停止信号、读写字节、处理 ACK/NACK 等。
注册:当系统启动时,I2C 总线适配器驱动会被加载并注册到内核的 I2C 子系统中。它会创建一个 i2c_adapter 结构体实例,代表一个 I2C 总线。
2. I2C 设备的描述和注册
一旦 I2C 总线适配器就绪,内核就需要知道有哪些 I2C 设备连接到这条总线上。I2C 设备本身是"哑"的,它们不会主动向总线报告自己的存在和类型。因此,设备的描述信息必须由系统固件(如 Device Tree 或 ACPI)或板级文件(较旧的方法)提供给内核。
a. Device Tree (设备树 - 现代 Linux 系统的主流方式)
描述:在 Device Tree (.dts 或 .dtsi 文件) 中,I2C 总线控制器下会以节点的形式描述连接在其上的 I2C 设备。
信息:每个设备节点会包含:
设备地址:I2C 设备的 7 位或 10 位从地址。
兼容字符串 (compatible string):这是最重要的信息,它是一个字符串列表,用于标识设备的类型和型号,例如 "vendor,chip-model"。这是驱动匹配的关键。
其他属性:如中断号、时钟频率、GPIO 引脚等设备特有的配置信息。
注册:当内核解析 Device Tree 时,它会根据这些描述信息在内存中创建 i2c_client 结构体实例,并将其注册到对应的 i2c_adapter 下。这些 i2c_client 实例代表了内核中已知的 I2C 设备。
b. ACPI (Advanced Configuration and Power Interface - 主要用于 x86 平台)
描述:在 ACPI 表(特别是 DSDT - Differentiated System Description Table)中,可以描述 I2C 控制器及其连接的设备。
信息:类似于 Device Tree,ACPI 也提供了设备地址和设备 ID(_HID 或 _CID)等信息。
注册:ACPI 子系统会解析这些表,并为 I2C 设备创建相应的 i2c_client 实例。
c. 板级文件 (Board Files - 较旧的方式)
描述:在 Device Tree 普及之前,开发者需要在 C 语言的板级文件中(例如 arch/arm/mach-xxx/board-yyy.c)硬编码 I2C 设备的信息。
信息:通过 i2c_board_info 结构体数组来定义设备名称、地址等。
注册:在板级初始化代码中,通过 i2c_register_board_info() 函数将这些设备信息注册到内核。
3. I2C 设备驱动的注册
同时,针对特定 I2C 设备的驱动程序(例如 acm8625p_i2c_driver)也需要注册到内核。
结构:I2C 设备驱动通常是一个 struct i2c_driver 类型的实例。
关键字段:
driver.name:驱动程序的名称。
id_table:一个 struct i2c_device_id 数组,包含驱动程序能支持的设备名称(name 字段)。
of_match_table:一个 struct of_device_id 数组,包含驱动程序能支持的 Device Tree 兼容字符串。
probe 函数:当驱动与设备匹配成功时,内核会调用这个函数来初始化设备。
remove 函数:当设备被移除或驱动被卸载时调用。
注册:通过 module_i2c_driver() 宏(它内部会调用 i2c_add_driver())将 struct i2c_driver 实例注册到 I2C 子系统。
4. 驱动匹配 (Driver Matching)
这是设备发现的核心步骤。当一个 I2C 设备(i2c_client 实例)被注册到内核后,或者一个 I2C 驱动(i2c_driver 实例)被注册到内核后,I2C 子系统会尝试进行匹配。
匹配机制:
Device Tree 匹配:如果 i2c_client 是由 Device Tree 创建的,内核会尝试将其 Device Tree 节点的 compatible 字符串与所有已注册 i2c_driver 的 of_match_table 中的 compatible 字符串进行匹配。这是最常用的匹配方式。
i2c_device_id 匹配:如果 i2c_client 是通过板级文件或运行时添加的,内核会尝试将其 name 字段(通常是设备型号的短名称)与所有已注册 i2c_driver 的 id_table 中的 name 字段进行匹配。
匹配成功:一旦找到一个匹配的 i2c_driver,内核就会认为这个设备找到了对应的驱动。
5. Probe 函数的调用
当一个 I2C 设备和其对应的 I2C 驱动成功匹配后,内核会调用该驱动的 probe 函数。
作用:probe 函数是驱动程序的入口点,它负责:
硬件初始化:通过 I2C 总线向设备发送命令,配置设备寄存器。
资源分配:为设备分配内存、中断等资源。
注册接口:向内核的其他子系统(如 ALSA 音频子系统、输入子系统等)注册相应的设备接口,使其可供用户空间应用程序使用。
创建设备文件:在 /dev 目录下创建设备节点(如果需要)。
设备树(Device Tree)的作用
内核在启动时,会解析设备树,从而得知(或者说"被告知")在某个I2C总线上预期会有哪些I2C设备,以及它们的从机地址、中断线、兼容字符串等属性。
设备树为内核提供了一个静态的硬件拓扑图和设备配置信息。它告诉内核"应该"去哪里找什么设备,以及找到后应该用哪个驱动程序来管理它。
内核的初始化和驱动绑定
根据设备树的信息,内核会为每个描述的I2C设备创建一个i2c_client结构体。
然后,内核会尝试将与该i2c_client匹配的I2C设备驱动程序(i2c_driver)绑定到它上面。
在驱动程序绑定(或者说"探测",probe函数执行)的过程中,驱动程序通常会通过I2C控制器主动地与设备进行通信,例如读取设备的ID寄存器,或者执行一个简单的写操作。
当驱动程序(通过内核的I2C子系统)指示I2C控制器向某个地址发送数据(例如,尝试读取设备ID)时,I2C控制器会执行I2C协议的寻址步骤。
此时,I2C控制器会等待并检测来自物理I2C设备的硬件应答信号(ACK/NACK)。
如果收到ACK,表示该地址的设备确实存在且响应正常,驱动程序就可以继续与它通信。
如果收到NACK,表示该地址没有设备响应,或者设备有问题,驱动程序就会报告错误,通常会导致设备探测失败。
设备树是告知内核"应该"有哪些I2C设备及其地址的配置信息。它不是一个实时监测的机制。(类似于audio hal的xml配置文件)
**内核(通过驱动程序)**会根据设备树的指示,主动发起与I2C设备的通信请求。(类似于audio hal的so)
I2C控制器在执行这些通信请求时,通过检测硬件信号(ACK/NACK)来实时判断某个特定地址上是否有设备响应。
我理解这样设计的好处是:同样的I2C设备可以接到不同的I2C接口,但是不需要修改驱动。改设备树文件就行了。
驱动中经常见到of词缀,of 是 Open Firmware 的缩写。更准确地说,它指的是 Device Tree (设备树)。
Open Firmware (OF) 是一种固件接口标准,最初用于 PowerPC 架构,用于描述硬件配置。
Device Tree (DT) 是 Linux 内核借鉴 Open Firmware 概念发展出来的一种数据结构,用于描述非可发现(non-discoverable)硬件。在 ARM、PowerPC 等嵌入式系统中,很多硬件设备(如 I2C 控制器、GPIO、SPI 控制器等)是静态连接的,无法像 PCI 设备那样通过总线枚举发现。
在 Linux 设备驱动模型中,特别是对于使用 Device Tree 的系统,驱动程序需要一种机制来识别它所支持的硬件设备。
of_match_table 就是一个 const struct of_device_id 类型的数组,它定义了驱动程序能够匹配的设备树节点。
每个 of_device_id 结构体通常包含 compatible 字符串,这个字符串与设备树中设备的 compatible 属性进行匹配。当内核遍历设备树时,如果找到一个设备的 compatible 属性与某个驱动的 of_match_table 中的条目匹配,那么该驱动就会被绑定到这个设备上。