[Linux]学习笔记系列 -- [drivers][I2C]I2C


title: i2c

categories:

  • linux
  • drivers
  • I2C
    tags:
  • linux
  • drivers
    abbrlink: 818c4d2e
    date: 2025-10-03 09:01:49

https://github.com/wdfk-prog/linux-study

文章目录

drivers/i2c I2C总线子系统(I2C Bus Subsystem) 管理I2C总线控制器与设备

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/i2c 目录中的代码实现了Linux内核的I2C(Inter-Integrated Circuit)总线子系统。这项技术的诞生是为了解决在Linux系统中管理和访问大量通过I2C总线连接的外设时面临的代码冗余、缺乏抽象和可移植性差的问题。

I2C是一种简单、低速的两线(SDA -串行数据线, SCL -串行时钟线)串行总线,广泛用于连接微控制器和各种外围芯片(如传感器、EEPROM、RTC、PMIC等)。在没有统一子系统的情况下:

  • 驱动与硬件耦合:每个需要访问I2C设备的驱动程序,都必须包含直接操作特定I2C控制器硬件(即"总线主控器")的代码。这意味着为一个I2C传感器编写的驱动,在更换了主控芯片(如从NXP的SoC换到Broadcom的SoC)后就无法工作,必须重写。
  • 代码重复:访问I2C总线的基本协议(起始/停止信号、发送地址、读写数据、ACK/NACK)是标准的,但每个驱动都在重复实现这些逻辑。
  • 缺乏标准化:没有统一的API来枚举总线上的设备、注册驱动程序或进行数据传输。

I2C子系统的核心目标就是建立一个抽象层 ,将I2C总线控制器(Adapter)的驱动I2C外设(Client)的驱动彻底分离开来,使得一个外设驱动可以不经修改地运行在任何被Linux支持的I2C控制器上。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux I2C子系统的发展与整个设备模型的演进息息相关。

  • 核心抽象的建立 :引入了三个核心概念:
    1. i2c_adapter:代表一个物理的I2C总线控制器。它提供了实际产生SDA/SCL信号的能力。
    2. i2c_client:代表一个挂载在I2C总线上的物理外设。它有唯一的设备地址。
    3. i2c_driver :代表一个可以驱动一类i2c_client的驱动程序。
  • 从板级文件到设备树 :早期的内核通过在板级配置文件(board file)中硬编码 i2c_board_info 结构体来声明I2C总线上有哪些设备。这是一个巨大的进步,但仍然需要在C代码中描述硬件。随着设备树(Device Tree)在嵌入式领域的普及,I2C子系统完全转向使用设备树来描述硬件拓扑。现在,I2C控制器节点下会包含一系列子节点,每个子节点描述一个I2C外设及其地址和属性。
  • SMBus(系统管理总线)的集成 :SMBus是I2C的一个功能子集,具有更严格的协议。I2C核心无缝地集成了SMBus协议的支持,提供了一套更高级的、面向事务的API(如i2c_smbus_read_byte_data)。
  • regmap的集成 :为了进一步简化设备驱动,I2C子系统与regmap API紧密集成。驱动程序现在可以使用devm_regmap_init_i2c()来初始化一个regmap实例,从而使用统一的、带缓存的接口来访问设备寄存器,而无需关心底层的I2C传输细节。
目前该技术的社区活跃度和主流应用情况如何?

I2C子系统是Linux内核中最核心、最稳定、应用最广泛的子系统之一。它是所有现代嵌入式Linux系统(包括Android、路由器、物联网设备等)的基石。几乎所有与低速外设通信的场景都会用到它,例如:

  • 读取环境传感器(温度、湿度、压力)
  • 访问实时时钟(RTC)和EEPROM
  • 控制电源管理芯片(PMIC)
  • 与触摸屏控制器和音频编解码器通信
  • 读取显示器的EDID信息(DDC/I2C)

核心原理与设计

它的核心工作原理是什么?

I2C子系统通过一个分层的模型来工作,核心是**i2c-core.c**,它扮演着"胶水层"的角色。

  1. Adapter(总线控制器)驱动层

    • 硬件厂商(如NXP, TI, Intel)为其SoC中的I2C控制器硬件编写驱动,例如drivers/i2c/busses/i2c-imx.c
    • 这个驱动的核心任务是实现一个struct i2c_algorithm,其中包含一个master_xfer函数指针。这个函数知道如何操作硬件寄存器来产生I2C协议的起始、停止、读写等信号。
    • 驱动通过i2c_add_adapter()将一个i2c_adapter实例注册到I2C核心。
  2. Client(外设)驱动层

    • 设备厂商(如Bosch, NXP)为其I2C外设芯片(如BME280传感器)编写驱动,例如drivers/iio/pressure/bmp280-i2c.c
    • 这个驱动通过i2c_driver_register()注册一个struct i2c_driver。该结构体中包含了.probe.remove回调,以及一个id_table,用于声明它支持哪些设备(通常通过设备树的compatible字符串匹配)。
  3. I2C核心的匹配与绑定

    • 当系统启动时,设备树被解析。I2C控制器节点会被其adapter驱动识别并注册。
    • I2C核心接着会解析该控制器节点下的所有子节点,为每个子节点创建一个struct i2c_client实例(包含了设备地址等信息),并将其挂载到对应的adapter上。
    • 每当一个新的i2c_client被创建,或一个新的i2c_driver被注册,I2C核心就会尝试进行匹配。如果一个clientcompatible字符串出现在一个driverid_table中,匹配成功。
    • 匹配成功后,I2C核心会调用该driver.probe函数,并将对应的i2c_client作为参数传入。
  4. 数据传输

    • client驱动中,当需要与设备通信时,它会填充一个或多个struct i2c_msg结构体(描述了要发送/接收的数据、目标地址、读/写标志等),然后调用统一的API i2c_transfer(adapter, msgs, num)
    • I2C核心接收到这个请求后,会通过adapter找到其底层的algorithm,并最终调用master_xfer函数,由adapter驱动来完成实际的硬件操作。
它的主要优势体现在哪些方面?
  • 完全的驱动分离(解耦)client驱动完全不知道也不关心底层adapter的硬件细节,实现了极高的可移植性。
  • 代码重用 :一个client驱动可以用于所有支持的Linux平台。
  • 标准化:提供了统一的设备枚举、驱动注册和数据传输API。
  • 数据驱动的硬件描述:通过设备树,硬件连接关系被清晰地描述在数据文件中,而不是硬编码在C代码里。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 协议固有的局限性:I2C总线本身速度较慢,不适合高速数据传输。总线上的设备数量和物理长度也有限制。这些是I2C协议的限制,而非Linux子系统的限制。
  • 错误处理复杂性 :I2C总线上的错误(如设备无应答NACK、总线仲裁丢失)需要adapterclient驱动协同处理,有时会比较复杂。
  • 阻塞式API :核心的i2c_transfer API是阻塞的,它会一直等待传输完成。在需要高性能异步操作的场景中可能不是最优选择(尽管这种情况在I2C的典型应用中很少见)。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请例举说明。

Linux I2C子系统是与任何I2C外设 在内核空间进行通信的唯一且标准的解决方案。

  • 场景一:传感器数据采集
    一个气象站设备,主控CPU通过I2C总线连接了温度、湿度、气压传感器。IIO(Industrial I/O)子系统中的传感器驱动就是一个i2c_driver,它通过i2c_transfer(或更上层的regmap)周期性地从传感器(i2c_client)读取测量数据。
  • 场景二:系统配置存储
    许多系统使用I2C接口的EEPROM芯片来存储配置信息,如MAC地址、序列号等。一个简单的i2c_driver会在内核启动时读取这些信息,并通过sysfs暴露给用户空间。
  • 场景三:电源管理
    复杂的SoC通常使用一个PMIC(电源管理芯片)来管理多个电压轨。主CPU通过I2C向PMIC发送命令来动态调整电压或开关电源。Regulator子系统中的PMIC驱动就是一个i2c_driver
是否有不推荐使用该技术的场景?为什么?

这里的"不推荐"主要指不推荐使用I2C总线本身,而非Linux的I2C子系统。

  • 高速数据流:如摄像头图像数据、高速ADC采样数据。这些场景应选择MIPI CSI-2、SPI、并行总线或USB。
  • 长距离通信:I2C总线的物理长度通常限制在几米以内。长距离通信应选择CAN、RS-485等差分总线。
  • 需要全双工通信:I2C是半双工的。如果需要同时发送和接收数据,应选择SPI或UART。

对比分析

请将其 与 其他相似技术 进行详细对比。

在嵌入式系统中,与I2C最常进行对比的是SPI(Serial Peripheral Interface)总线。

特性 I2C (Inter-Integrated Circuit) SPI (Serial Peripheral Interface)
物理连线 2根线(SDA, SCL),外加电源和地。 至少4根线(MISO, MOSI, SCLK, CS),每个从设备需要一个额外的CS线。
通信协议 半双工 ,同步,多主控,设备地址在协议内传输。 全双工 ,同步,单主控,通过片选(CS)线选择从设备。
数据速率 较慢。标准模式100kbps,快速模式400kbps,高速模式可达3.4Mbps。 非常快。速率可轻松达到数十Mbps甚至更高。
硬件复杂度 简单。从设备无需复杂的片选逻辑,但需要能检测地址。 简单。协议本身比I2C更简单,但布线更复杂。
Linux子系统 drivers/i2c drivers/spi
核心抽象 i2c_adapter (控制器), i2c_client (外设) spi_controller (控制器), spi_device (外设)
核心API i2c_transfer(),使用struct i2c_msg数组。 spi_sync() / spi_async(),使用struct spi_messagestruct spi_transfer链表。
共同点 两者都与设备树深度集成,都与regmap API完美配合,都遵循了控制器驱动和外设驱动分离的设计哲学。 两者都与设备树深度集成,都与regmap API完美配合,都遵循了控制器驱动和外设驱动分离的设计哲学。
适用场景 适用于大量、低速、配置型外设的连接,布线简单。 适用于需要高速、流式数据传输的场景,如Flash存储、ADC/DAC、显示屏。

include/linux/i2c.h

I2C总线访问仲裁:原子与非原子上下文的锁定机制

本代码片段展示了Linux I2C核心层中至关重要的总线锁定机制。其核心功能是提供一个上下文感知的(context-aware)锁定辅助函数 __i2c_lock_bus_helper,它能够根据当前的系统状态和执行上下文(是否为原子上下文)来智能地选择阻塞式锁定或非阻塞式锁定,从而确保对I2C适配器总线的独占访问,防止数据传输过程中的并发冲突。此外,它还定义了i2c_lock_busi2c_trylock_busi2c_unlock_bus等一组标准的API,用于抽象和封装底层适配器驱动所提供的具体锁定操作。

实现原理分析

该机制的实现原理是分离"锁定策略"和"锁定实现"。I2C核心层负责制定策略,而具体的实现则委托给底层的I2C适配器驱动。

  1. 上下文检测 (i2c_in_atomic_xfer_mode) : 这是策略制定的关键。此函数用于判断当前代码是否在一个"原子传输模式"下运行。这种情况被严格限制在系统关闭或重启(system_state > SYSTEM_RUNNING)等非常晚期的阶段,并且当前上下文不允许睡眠(通过!preemptible()irqs_disabled()判断)。在这些场景下(例如,在系统断电前通过PMIC保存最后的状态),任何可能导致睡眠的阻塞式锁定都是非法的。

  2. 条件锁定 (__i2c_lock_bus_helper) : 此函数根据i2c_in_atomic_xfer_mode的返回值来执行不同的锁定策略:

    • 原子路径 : 如果处于原子模式,它必须使用非阻塞的方式获取锁。因此,它调用i2c_trylock_bus。如果立即成功获取锁,则返回0。如果锁已被其他任务持有,i2c_trylock_bus会立即失败,函数返回-EAGAIN,告知上层调用者(如i2c_smbus_xfer)总线正忙,无法执行传输。同时,它会发出一个警告,如果底层适配器驱动没有提供专门的原子传输函数,这表明驱动实现不完整。
    • 常规路径 : 在正常的、可睡眠的上下文中,它调用i2c_lock_bus。这是一个阻塞式调用。如果总线被占用,当前任务将被置于睡眠状态,直到锁被释放。这是绝大多数I2C传输所采用的路径。
  3. 锁定操作抽象 : i2c_lock_busi2c_trylock_busi2c_unlock_bus这三个static inline函数本身不包含锁定逻辑。它们通过adapter->lock_ops函数指针表,将调用分派到底层I2C适配器驱动所提供的具体实现函数上。这种设计将I2C核心与具体的锁定实现(例如,可以是基于mutex的互斥锁,或是基于spinlock的自旋锁,或是更复杂的仲裁逻辑)完全解耦,提供了极大的灵活性。

代码分析

c 复制代码
/*
 * 我们只允许在非常晚期的通信中使用原子传输,例如在关机时访问电源管理芯片(PMIC)。
 * 原子传输是一种特殊情况,不应用于通用目的!
 */
// i2c_in_atomic_xfer_mode: 判断当前是否处于原子传输模式。
static inline bool i2c_in_atomic_xfer_mode(void)
{
	// 必须是系统正常运行之后的状态(如关机、重启),并且
	// 当前上下文不可抢占 (如果CONFIG_PREEMPT_COUNT启用) 或中断被禁用。
	return system_state > SYSTEM_RUNNING &&
	       (IS_ENABLED(CONFIG_PREEMPT_COUNT) ? !preemptible() : irqs_disabled());
}

// __i2c_lock_bus_helper: 一个根据上下文选择锁定方式的辅助函数。
static inline int __i2c_lock_bus_helper(struct i2c_adapter *adap)
{
	int ret = 0;

	// 判断是否处于原子传输模式。
	if (i2c_in_atomic_xfer_mode()) {
		// 如果是,但适配器驱动没有提供原子传输函数,则发出警告。
		WARN(!adap->algo->master_xfer_atomic && !adap->algo->smbus_xfer_atomic,
		     "No atomic I2C transfer handler for '%s'\n", dev_name(&adap->dev));
		// 尝试以非阻塞方式获取总线锁。成功返回0,失败返回-EAGAIN。
		ret = i2c_trylock_bus(adap, I2C_LOCK_SEGMENT) ? 0 : -EAGAIN;
	} else {
		// 在正常情况下,以阻塞方式获取总线锁。
		i2c_lock_bus(adap, I2C_LOCK_SEGMENT);
	}

	return ret;
}

/* 适配器锁定函数,为共享引脚等情况导出 */
#define I2C_LOCK_ROOT_ADAPTER BIT(0) // 锁定根适配器
#define I2C_LOCK_SEGMENT      BIT(1) // 仅锁定总线段

// i2c_lock_bus: 获取对I2C总线段的独占访问权 (阻塞)。
// @adapter: 目标I2C总线段。
// @flags: 锁定标志。
static inline void
i2c_lock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	// 调用底层适配器驱动提供的lock_bus函数实现。
	adapter->lock_ops->lock_bus(adapter, flags);
}

// i2c_trylock_bus: 尝试获取对I2C总线段的独占访问权 (非阻塞)。
// @adapter: 目标I2C总线段。
// @flags: 锁定标志。
// 返回值: 如果成功锁定则返回true,否则返回false。
static inline int
i2c_trylock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	// 调用底层适配器驱动提供的trylock_bus函数实现。
	return adapter->lock_ops->trylock_bus(adapter, flags);
}

// i2c_unlock_bus: 释放对I2C总线段的独占访问权。
// @adapter: 目标I2C总线段。
// @flags: 解锁标志。
static inline void
i2c_unlock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	// 调用底层适配器驱动提供的unlock_bus函数实现。
	adapter->lock_ops->unlock_bus(adapter, flags);
}

drivers/i2c/i2c-core-base.c

i2c_device_match: 决定一个I2C驱动是否支持一个I2C设备

此函数是I2C总线类型的核心匹配函数, 在 i2c_bus_type 结构体中被注册。当一个新的I2C设备被发现, 或者一个新的I2C驱动被加载时, 内核的驱动核心会调用此函数, 来判断这个驱动程序是否能够管理这个设备。它扮演着I2C子系统中的"红娘"角色, 是实现设备与驱动自动绑定的关键。

此函数的匹配逻辑遵循一个严格的、有优先级的顺序:

  1. 设备树 (Device Tree) 匹配 : 首先尝试基于设备树的 compatible 属性进行匹配。
  2. ACPI 匹配: 如果设备树匹配失败, 尝试基于ACPI的硬件ID进行匹配。
  3. I2C ID 表匹配: 如果以上两者都失败, 最后尝试基于传统的I2C设备名称和驱动ID表进行匹配。

只要其中一种方式匹配成功, 函数就会立即返回1 (表示成功), 后续的匹配方式将不再尝试。

c 复制代码
/*
 * 这是一个静态函数, 作为 i2c_bus_type 的 .match 回调.
 *
 * @dev:  指向一个通用的 struct device 的指针, 代表需要为其寻找驱动的设备.
 * @drv:  指向一个 const struct device_driver 的指针, 代表一个潜在的驱动程序.
 * @return: 1 表示设备和驱动匹配, 0 表示不匹配.
 */
static int i2c_device_match(struct device *dev, const struct device_driver *drv)
{
	/*
	 * 定义一个指向 i2c_client 结构体的指针 client.
	 * i2c_verify_client 是一个安全的宏, 用于将通用的 device 指针转换为 I2C 子系统专用的 i2c_client 指针.
	 * i2c_client 代表了I2C总线上的一个具体设备(例如一个特定地址的传感器).
	 */
	struct i2c_client	*client = i2c_verify_client(dev);
	/*
	 * 定义一个指向 const i2c_driver 结构体的指针 driver.
	 */
	const struct i2c_driver	*driver;


	/*
	 * 优先级1: 尝试基于设备树(Open Firmware, OF)风格的匹配.
	 * 在STM32H750这类嵌入式系统上, 这是最主要和最常用的匹配方式.
	 */
	if (i2c_of_match_device(drv->of_match_table, client))
		/*
		 * i2c_of_match_device 会比较驱动的 of_match_table (一个包含多个 compatible 字符串的数组)
		 * 和设备的设备树节点中的 "compatible" 属性.
		 * 如果在表中找到了与设备兼容的条目, 函数返回 true.
		 * 匹配成功, 立即返回1, 后续的匹配方式不再执行.
		 */
		return 1;

	/*
	 * 优先级2: 尝试ACPI风格的匹配.
	 * ACPI是PC架构中用于硬件发现和配置的技术, 在STM32H750上不使用.
	 */
	if (acpi_driver_match_device(dev, drv))
		/*
		 * 如果acpi_driver_match_device返回true (在PC上可能发生), 则匹配成功.
		 * 在STM32上, 这个条件永远为false.
		 */
		return 1;

	/*
	 * 将通用的 device_driver 指针转换为I2C专用的 i2c_driver 指针, 以便访问其特有的 id_table 成员.
	 */
	driver = to_i2c_driver(drv);

	/*
	 * 优先级3: 尝试传统的I2C ID表匹配.
	 * 这是在没有设备树或ACPI时的传统匹配方法.
	 */
	if (i2c_match_id(driver->id_table, client))
		/*
		 * i2c_match_id 会遍历驱动的 id_table (一个包含多个设备名称字符串的数组),
		 * 并将其与 client->name 进行比较.
		 * 如果找到匹配的名称, 函数返回 true.
		 * 这对于通过用户空间手动实例化的设备很有用.
		 */
		return 1;

	/*
	 * 如果以上所有三种方式都未能找到匹配, 则返回0, 表示此驱动不适用于此设备.
	 */
	return 0;
}

i2c_device_probe: 探测并初始化I2C设备

此函数是I2C总线类型的总线级别 probe 函数。它并不直接驱动任何具体的I2C设备, 而是扮演一个"分发者"和"资源协调者"的角色。当内核的驱动核心将一个I2C设备(i2c_client)和一个I2C驱动(i2c_driver)成功匹配后, 就会调用此函数。它的核心职责是:在调用具体驱动自己的probe函数之前, 为设备准备好所有必需的通用资源, 如中断号(IRQ)、时钟和电源域;在具体驱动探测失败后, 负责按相反的顺序清理这些已分配的资源。

在单核无MMU的STM32H750平台上的原理与作用

在STM32H750平台上, 此函数是连接设备树硬件描述和具体I2C从设备驱动(如EEPROM、传感器驱动)的关键环节。

  • 中断解析 : STM32H750上的I2C从设备(如一个带中断引脚的传感器)会在设备树中通过interruptsirq属性来描述其中断连接。此函数中的fwnode_irq_get调用会负责解析这些设备树属性, 并将它们翻译成Linux内核可以使用的IRQ号码, 然后存入client->irq
  • 平台特定路径 : 由于STM32H750使用设备树(Device Tree)而非ACPI, 函数中所有is_acpi_device_node的路径都不会被执行。is_of_node路径则是其主要工作路径。I2C_CLIENT_HOST_NOTIFY是SMBus的一个特性, 在典型的STM32应用中也不常用。
  • 电源和时钟 : of_clk_set_defaultsdev_pm_domain_attach调用对于STM32平台至关重要。它们确保了在驱动probe函数被调用之前, 该I2C从设备所依赖的时钟已被正确配置, 并且其所属的电源域(Power Domain)已经被使能。
  • 资源管理 (devres) : devres_open_group创建了一个资源管理的"组"。这是一个通用的内核机制, 在STM32平台上同样有效。它确保了所有在此之后由驱动分配的资源(内存、IRQ等)都被自动追踪。如果驱动probe失败或将来设备被移除, 内核会自动释放这个组内的所有资源, 极大地简化了驱动的错误处理和清理逻辑, 避免了资源泄漏。

c 复制代码
/*
 * 这是一个静态函数, 作为 i2c_bus_type 的 .probe 回调.
 *
 * @dev: 指向一个通用的 struct device 的指针, 代表正在被探测的I2C设备.
 * @return: 0 表示成功, 负值错误码表示失败.
 */
static int i2c_device_probe(struct device *dev)
{
	/*
	 * 获取设备的固件节点句柄(fwnode). 在STM32H750上, 这通常是一个指向设备树节点的指针.
	 */
	struct fwnode_handle	*fwnode = dev_fwnode(dev);
	/*
	 * 将通用的 device 指针安全地转换为 I2C 专用的 i2c_client 指针.
	 */
	struct i2c_client	*client = i2c_verify_client(dev);
	/*
	 * 定义一个指向 i2c_driver 结构体的指针.
	 */
	struct i2c_driver	*driver;
	/*
	 * 一个布尔标志, 用于决定是否需要在附加电源域时就立即打开电源.
	 */
	bool do_power_on;
	/*
	 * status 用于存储每一步操作的返回值.
	 */
	int status;

	/*
	 * 如果 dev 不是一个有效的 i2c_client (例如, 它是一个I2C适配器本身), 那么无需探测, 直接返回成功.
	 */
	if (!client)
		return 0;

	/*
	 * 将在实例化client时可能传入的初始IRQ值(init_irq)赋给client->irq.
	 */
	client->irq = client->init_irq;

	/*
	 * 如果 client->irq 仍然为0, 表示没有通过实例化时传入IRQ, 我们需要尝试从固件(设备树)中解析.
	 */
	if (!client->irq) {
		int irq = -ENOENT; // 初始化irq为一个表示"未找到"的错误码.

		/*
		 * 检查是否使用了 SMBus Host Notify 协议. 在STM32上不常用.
		 */
		if (client->flags & I2C_CLIENT_HOST_NOTIFY) {
			dev_dbg(dev, "Using Host Notify IRQ\n");
			pm_runtime_get_sync(&client->adapter->dev);
			irq = i2c_smbus_host_notify_to_irq(client);
		} else if (is_of_node(fwnode)) {
			/*
			 * 在STM32平台上, fwnode 是一个设备树节点.
			 * 首先尝试按名称 "irq" 获取中断.
			 */
			irq = fwnode_irq_get_byname(fwnode, "irq");
			/* 如果按名称查找失败, 则尝试按索引0获取中断. 这是更通用的方法. */
			if (irq == -EINVAL || irq == -ENODATA)
				irq = fwnode_irq_get(fwnode, 0);
		} else if (is_acpi_device_node(fwnode)) {
			/* ACPI路径, 在STM32上不执行. */
			bool wake_capable;
			irq = i2c_acpi_get_irq(client, &wake_capable);
			if (irq > 0 && wake_capable)
				client->flags |= I2C_CLIENT_WAKE;
		}
		/*
		 * 如果获取IRQ返回 -EPROBE_DEFER, 意味着中断控制器尚未就绪, 必须延迟探测.
		 */
		if (irq == -EPROBE_DEFER) {
			status = dev_err_probe(dev, irq, "can't get irq\n");
			goto put_sync_adapter;
		}

		/* 如果最终未能获取到一个有效的IRQ (irq < 0), 就将其置为0 (表示无中断). */
		if (irq < 0)
			irq = 0;

		/* 将解析出的IRQ号存入client结构体. */
		client->irq = irq;
	}

	/* 获取与此设备匹配的驱动程序. */
	driver = to_i2c_driver(dev->driver);

	/*
	 * 一个I2C驱动的ID表不是强制性的, 当且仅当, 为被探测的设备提供了一个合适的OF(设备树)或ACPI的ID表时.
	 * 这个检查确保了驱动至少有一种方法(OF, ACPI, 或I2C ID表)来声明它支持这个设备.
	 */
	if (!driver->id_table &&
	    !acpi_driver_match_device(dev, dev->driver) &&
	    !i2c_of_match_device(dev->driver->of_match_table, client)) {
		status = -ENODEV; // 没有设备
		goto put_sync_adapter;
	}

	/* 处理WAKE(唤醒)中断的逻辑. */
	if (client->flags & I2C_CLIENT_WAKE) {
		int wakeirq;

		wakeirq = fwnode_irq_get_byname(fwnode, "wakeup");
		if (wakeirq == -EPROBE_DEFER) {
			status = dev_err_probe(dev, wakeirq, "can't get wakeirq\n");
			goto put_sync_adapter;
		}

		device_init_wakeup(&client->dev, true);

		if (wakeirq > 0 && wakeirq != client->irq)
			status = dev_pm_set_dedicated_wake_irq(dev, wakeirq);
		else if (client->irq > 0)
			status = dev_pm_set_wake_irq(dev, client->irq);
		else
			status = 0;

		if (status)
			dev_warn(&client->dev, "failed to set up wakeup irq\n");
	}

	dev_dbg(dev, "probe\n"); // 打印一条调试信息, 表示探测开始.

	/* 根据设备树的 "clocks" 属性, 设置和准备时钟. */
	status = of_clk_set_defaults(to_of_node(fwnode), false);
	if (status < 0)
		goto err_clear_wakeup_irq;

	/* ACPI相关逻辑, 在STM32上 do_power_on 将为true. */
	do_power_on = !i2c_acpi_waive_d0_probe(dev);
	/* 将设备附加到其电源域, 并根据标志决定是否立即上电. */
	status = dev_pm_domain_attach(&client->dev, do_power_on ? PD_FLAG_ATTACH_POWER_ON : 0);
	if (status)
		goto err_clear_wakeup_irq;

	/*
	 * 打开一个新的devres资源管理组. 从此之后, 所有通过devres分配的资源都会被关联到这个组.
	 */
	client->devres_group_id = devres_open_group(&client->dev, NULL,
						    GFP_KERNEL);
	if (!client->devres_group_id) {
		status = -ENOMEM;
		goto err_detach_pm_domain;
	}

	/* 在I2C适配器的debugfs目录下, 为此设备创建一个以设备名命名的子目录. */
	client->debugfs = debugfs_create_dir(dev_name(&client->dev),
					     client->adapter->debugfs);

	/*
	 * 调用具体设备驱动自己的 probe 函数. 这是执行设备特定初始化的核心步骤.
	 */
	if (driver->probe)
		status = driver->probe(client);
	else
		status = -EINVAL; // 如果驱动没有提供probe函数, 这是一个错误.

	/*
	 * 注意我们没有关闭上面打开的devres组. 这样, 即使在probe函数运行后
	 * 驱动又分配了其他资源, 这些资源在 i2c_device_remove() 执行时也会被释放.
	 * 这对于固件更新等场景是必要的.
	 */
	if (status) // 如果具体驱动的probe失败了
		goto err_release_driver_resources; // 跳转到错误处理流程

	return 0; // 所有步骤成功, 返回0.

/*
 * 标准的错误处理流程, 使用 goto 按相反的顺序清理已分配的资源.
 */
err_release_driver_resources:
	debugfs_remove_recursive(client->debugfs);
	devres_release_group(&client->dev, client->devres_group_id);
err_detach_pm_domain:
	dev_pm_domain_detach(&client->dev, do_power_on);
err_clear_wakeup_irq:
	dev_pm_clear_wake_irq(&client->dev);
	device_init_wakeup(&client->dev, false);
put_sync_adapter:
	if (client->flags & I2C_CLIENT_HOST_NOTIFY)
		pm_runtime_put_sync(&client->adapter->dev);

	return status; // 返回最终的错误码.
}

i2c_client_type: 定义I2C从设备的核心属性与操作

此结构体 i2c_client_type 是一个 struct device_type 的实例, 它为所有I2C从设备 (在内核中由 struct i2c_client 表示) 定义了一套通用的、与类型相关的操作。它像一个模板, 规定了所有I2C从设备在设备模型中的核心行为, 例如它们在 sysfs 中应该有哪些默认的文件、它们的 uevent 事件应该如何生成, 以及它们的内存最后应该如何被释放。

c 复制代码
/*
 * 定义一个全局的、常量 struct device_type 实例, 名为 i2c_client_type.
 * 这个结构体为所有 i2c_client 设备提供了一组通用的操作.
 */
const struct device_type i2c_client_type = {
	/*
	 * .groups: 指向一个属性组数组. 它定义了所有属于此类型的设备在sysfs中
	 *          都会自动创建的默认文件(属性)组. i2c_dev_groups 包含了像 "name" 这样的标准文件.
	 */
	.groups		= i2c_dev_groups,
	/*
	 * .uevent: 一个函数指针, 指向uevent事件处理函数. 当此类型的设备被添加或移除时,
	 *          内核会调用 i2c_device_uevent 函数来填充uevent的环境变量.
	 */
	.uevent		= i2c_device_uevent,
	/*
	 * .release: 一个函数指针, 指向一个释放函数. 当一个设备的最后一个引用被释放
	 *           (即引用计数降为0)时, 内核会调用 i2c_client_dev_release 函数来
	 *           执行最终的清理工作, 释放设备结构体本身占用的内存.
	 */
	.release	= i2c_client_dev_release,
};
/*
 * EXPORT_SYMBOL_GPL 将 i2c_client_type 这个符号导出到内核符号表中, 并标记为GPL许可证.
 * 这使得其他内核模块(主要是I2C适配器驱动)在创建 i2c_client 时可以引用这个结构体.
 */
EXPORT_SYMBOL_GPL(i2c_client_type);

i2c_device_uevent: 为I2C从设备生成uevent事件以实现驱动自动加载

此函数是一个回调函数, 其核心职责是为一个I2C设备生成 uevent (内核事件) 所需的环境变量, 其中最关键的是 MODALIAS。这个 MODALIAS 字符串是一个标准化的设备标识符, 用户空间的 udevmdev 守护进程会捕获这个事件, 并根据 MODALIAS 的值在系统中查找能够处理此设备的驱动程序, 然后自动加载它。这是Linux实现设备即插即用和驱动模块化加载的基础。

在单核无MMU的STM32H750平台上的原理与作用

在STM32H750这样的嵌入式系统上, 硬件拓扑结构由设备树(Device Tree)静态定义。当内核解析设备树并发现一个I2C设备(例如, 一个连接在I2C1总线上的温度传感器)时, 会为其创建一个i2c_client设备实例。

  1. 关联设备类型 : 新创建的 i2c_client 设备的 dev.type 字段会被设置为指向 i2c_client_type
  2. 触发uevent: 设备的创建会触发一个 "add" uevent 的生成。
  3. 调用回调 : 内核会调用 i2c_client_type 中注册的 .uevent 回调, 也就是 i2c_device_uevent 函数。
  4. 生成MODALIAS : 在i2c_device_uevent函数中:
    • 它首先会尝试 of_device_uevent_modalias。这一步在STM32平台上至关重要, 它会读取设备树中该设备的 compatible 属性(例如, compatible = "bosch,bmp280";), 并生成一个格式如 of:Nbmp280T<NULL>Cbosch,bmp280MODALIAS
    • ACPI路径在STM32上会被跳过。
    • 用户空间的udev服务会匹配这个 MODALIAS, 找到声明支持"bosch,bmp280"的驱动模块, 并自动加载它。

这个流程使得为STM32平台添加新的I2C外设驱动变得非常模块化和自动化。


c 复制代码
/*
 * 这是一个静态函数, 作为 i2c_client_type 的 .uevent 回调.
 *
 * @dev:  指向一个 const struct device 的指针, 代表触发事件的I2C设备.
 * @env:  指向 struct kobj_uevent_env 的指针, 这是一个环境块, 此函数需要向其中添加变量.
 * @return: 0 表示成功, 负值错误码表示失败.
 */
static int i2c_device_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
	/*
	 * 将通用的 device 指针安全地转换为 I2C 专用的 i2c_client 指针.
	 */
	const struct i2c_client *client = to_i2c_client(dev);
	/*
	 * rc 用于存储函数调用的返回值.
	 */
	int rc;

	/*
	 * 优先级1: 尝试从设备的设备树节点(OF node)生成一个 MODALIAS.
	 * of_device_uevent_modalias 会读取设备树的 "compatible" 属性并生成一个标准格式的别名.
	 * 在STM32H750这样的嵌入式系统上, 这是最主要的、最重要的一种方式.
	 */
	rc = of_device_uevent_modalias(dev, env);
	/*
	 * 检查返回值. 如果不等于 -ENODEV, 就直接返回.
	 * -ENODEV 在这里是一个特殊的返回值, 它表示"此设备没有设备树节点", 这并不是一个致命错误,
	 * 意味着我们应该继续尝试下一种匹配方式.
	 * 任何其他的返回值 (0表示成功, 其他负值表示真实错误) 都应立即返回.
	 */
	if (rc != -ENODEV)
		return rc;

	/*
	 * 优先级2: 尝试从设备的ACPI节点生成一个 MODALIAS.
	 * ACPI是PC架构的技术, 在STM32H750上不使用. 因此, 这一步总会返回 -ENODEV.
	 */
	rc = acpi_device_uevent_modalias(dev, env);
	/*
	 * 同样, 检查返回值, 如果不是-ENODEV, 就返回.
	 */
	if (rc != -ENODEV)
		return rc;

	/*
	 * 优先级3: 作为最后的备用方案, 使用传统的I2C设备名称生成 MODALIAS.
	 * 这种方式适用于那些没有设备树或ACPI描述, 而是通过其他方式(如代码中硬编码或用户空间实例化)创建的设备.
	 * add_uevent_var 是向uevent环境块中添加一个变量的核心函数.
	 * @ env: 目标环境块.
	 * @ "MODALIAS=%s%s": 格式化字符串.
	 * @ I2C_MODULE_PREFIX: 这是一个宏, 通常定义为 "i2c:".
	 * @ client->name: I2C设备的名称字符串.
	 * 最终会生成一个类似 "MODALIAS=i2c:mpu6050" 的变量.
	 */
	return add_uevent_var(env, "MODALIAS=%s%s", I2C_MODULE_PREFIX, client->name);
}

i2c_bus_type: 定义I2C总线类型的核心行为

此结构体是Linux内核设备模型的核心部分, 它定义了"i2c"这种总线类型的所有基本操作和属性。它不代表任何一个具体的硬件I2C控制器, 而是为所有I2C总线提供了一个统一的行为框架。当内核需要管理I2C设备和驱动时(例如, 决定哪个驱动可以控制哪个设备, 或者在探测、移除设备时应该执行哪些操作), 它就会查询并使用这个结构体中定义的函数指针。

c 复制代码
/*
 * 定义一个全局的、常量 struct bus_type 实例, 名为 i2c_bus_type.
 * 这个结构体是Linux内核驱动模型的核心, 用于描述一种总线类型的所有行为.
 */
const struct bus_type i2c_bus_type = {
	/*
	 * .name: 总线的名称. 这个字符串 "i2c" 将会出现在sysfs中,
	 *        例如, 内核会创建一个 /sys/bus/i2c/ 目录.
	 */
	.name		= "i2c",
	/*
	 * .match: 一个函数指针, 指向设备与驱动的匹配函数.
	 *         当一个新的I2C设备或驱动被注册时, 内核会调用 i2c_device_match 函数,
	 *         来判断这个驱动是否支持这个设备. 这是驱动绑定的第一步.
	 */
	.match		= i2c_device_match,
	/*
	 * .probe: 一个函数指针, 指向总线级别的探测函数.
	 *         在 .match 成功返回后, 内核会调用 i2c_device_probe.
	 *         这个函数负责执行一些通用的探测前准备工作, 然后再调用具体驱动自己的 probe 函数.
	 */
	.probe		= i2c_device_probe,
	/*
	 * .remove: 一个函数指针, 指向总线级别的移除函数.
	 *          当一个设备被移除时, 内核调用 i2c_device_remove 来执行通用的清理工作,
	 *          并调用具体驱动自己的 remove 函数.
	 */
	.remove		= i2c_device_remove,
	/*
	 * .shutdown: 一个函数指针, 指向总线级别的关机处理函数.
	 *            当系统关机时, 内核会为总线上的每个设备调用 i2c_device_shutdown,
	 *            以确保设备被置于一个安全的状态.
	 */
	.shutdown	= i2c_device_shutdown,
};
/*
 * EXPORT_SYMBOL_GPL 将 i2c_bus_type 这个符号导出到内核符号表中, 并且标记为GPL许可证.
 * 这使得其他遵循GPL许可证的可加载内核模块(例如一个I2C传感器的驱动)
 * 可以在运行时链接到这个核心的总线类型定义.
 */
EXPORT_SYMBOL_GPL(i2c_bus_type);

dummy_driver: 定义一个用于占位的虚拟I2C驱动程序

这个结构体和它的probe函数一起, 定义了一个完整的、但功能极简的I2C驱动。它不控制任何实际的硬件。它的核心作用是作为一个"占位符", 可以被用户或脚本从用户空间强制绑定到任何一个I2C设备地址上。这在以下场景中非常有用:

  • 屏蔽设备 : 如果总线上有一个设备行为异常或其驱动不稳定, 可以将其绑定到dummy_driver。这会阻止任何真实的驱动程序去尝试探测该设备, 从而避免了潜在的系统错误或崩溃。
  • 用户空间管理 : 对于某些完全由用户空间程序通过i2c-dev接口管理的设备, 可以将其绑定到dummy_driver以告知内核该设备已被认领, 避免内核日志中出现"未找到驱动"的警告。

在STM32H750平台上, 这两个结构体共同构成了I2C子系统的基础。i2c_bus_type为连接在STM32硬件I2C控制器上的所有设备(如传感器、EEPROM等)提供了统一的管理规则, 而dummy_driver则提供了一个重要的调试和系统集成工具。


c 复制代码
/*
 * 这是一个静态函数, 是 dummy_driver 的 probe 实现.
 *
 * @client: 指向 struct i2c_client 的指针, 代表被探测的I2C设备.
 * @return: 总是返回0, 表示探测"成功".
 */
static int dummy_probe(struct i2c_client *client)
{
	/*
	 * 这个函数体是空的, 它不执行任何硬件操作.
	 * 它的唯一作用就是成功返回, 告诉I2C核心, 它已经成功地"绑定"到了这个设备.
	 */
	return 0;
}

/*
 * 定义一个静态的 struct i2c_driver 实例, 名为 dummy_driver.
 * 这是一个功能性的I2C驱动, 尽管它什么也不做.
 */
static struct i2c_driver dummy_driver = {
	/*
	 * .driver.name: 驱动的名称. 这个名字会出现在 sysfs 中, 并且可以被用于从用户空间手动绑定设备.
	 *               例如: echo dummy 0x50 > /sys/bus/i2c/devices/i2c-1/new_device
	 */
	.driver.name	= "dummy",
	/*
	 * .probe: 将此驱动的 probe 操作指向上面定义的 dummy_probe 函数.
	 */
	.probe		= dummy_probe,
	/*
	 * .id_table: 指向一个 i2c_device_id 数组.
	 *            这个表定义了此驱动能自动匹配哪些设备.
	 *            对于 dummy_driver, 这个表 (dummy_id) 可能是空的, 或者包含一个通配符条目,
	 *            因为它通常是通过手动绑定来使用的, 而不是自动匹配.
	 */
	.id_table	= dummy_id,
};

i2c_init: 初始化I2C核心子系统

此函数是Linux内核I2C子系统的核心初始化函数。它的主要职责是在内核启动时, 准备好I2C子系统所需的所有基础框架, 包括注册I2C总线类型、设定总线编号的起始范围、创建调试接口以及注册一个"虚拟"驱动。这确保了当真正的I2C适配器驱动(例如STM32的I2C控制器驱动)开始加载时, 它们所需的所有基础设施都已准备就绪。

在单核无MMU的STM32H750平台上的原理与作用

在STM32H750这样的嵌入式系统上, I2C总线由一个或多个I2C外设硬件控制器(I2C Adapter)提供。此i2c_init函数的作用与这些具体的硬件控制器无关, 而是为它们建立一个统一的管理层。

  1. 总线编号 : STM32的设备树(.dts)通常会为每个I2C控制器定义一个别名, 如i2c0, i2c1等。of_alias_get_highest_id("i2c")会解析这些别名, 找到最大的编号。然后, i2c_init会设置__i2c_first_dynamic_bus_num, 确保将来由用户空间动态实例化的I2C总线号不会与设备树中静态定义的总线号冲突。
  2. 并发保护 : down_write(&__i2c_board_lock)获取一个读写信号量。即使在单核系统上, 如果内核是抢占式的, 这个锁也是必需的。它能防止一个任务在修改全局变量__i2c_first_dynamic_bus_num时, 被另一个可能也访问此变量的任务或中断抢占, 从而保证了操作的原子性和数据一致性。
  3. 总线注册 : bus_register(&i2c_bus_type)是核心步骤。它向内核的设备驱动模型注册了"i2c"这种总线类型。这使得内核能够理解设备(如I2C传感器)和驱动(如传感器驱动)之间的匹配规则, 是整个I2C驱动框架能够工作的基础。
  4. 虚拟驱动 : i2c_add_driver(&dummy_driver)注册一个虚拟的I2C驱动。这个驱动并不操作任何真实硬件, 它的一个重要作用是"占位"。用户可以通过sysfs接口将一个I2C地址绑定到这个dummy驱动上, 从而防止任何真实的驱动去探测或绑定该地址。这在调试或需要阻止某个设备被自动加载驱动时非常有用。
  5. 平台特定部分 : CONFIG_OF_DYNAMICCONFIG_ACPI相关的代码在STM32平台上通常不适用, 因为其硬件配置是静态的, 并且不使用ACPI。

c 复制代码
/*
 * 这是一个静态的初始化函数, 用于初始化I2C核心子系统.
 * __init 宏表示此函数仅在内核启动期间执行.
 */
static int __init i2c_init(void)
{
	/*
	 * retval 用于存储函数调用的返回值(错误码).
	 */
	int retval;

	/*
	 * 调用 of_alias_get_highest_id, 在设备树的 /aliases 节点中,
	 * 查找所有以 "i2c" 为前缀的别名 (如 "i2c0", "i2c1" 等), 并返回其中最大的编号.
	 * 例如, 如果设备树中有 i2c0, i2c1, i2c2, 此函数会返回 2.
	 * 这用于确保动态分配的总线号不会与设备树中静态定义的总线号冲突.
	 */
	retval = of_alias_get_highest_id("i2c");

	/*
	 * 获取 __i2c_board_lock 这个读写信号量的写锁.
	 * 这是一个全局锁, 用于保护对I2C板级信息(board info)和总线编号等全局数据的访问.
	 * 在单核抢占式内核中, 写锁可以防止其他任务并发地读或写这些数据.
	 */
	down_write(&__i2c_board_lock);
	/*
	 * 检查从设备树中找到的最高总线号是否大于等于当前记录的第一个动态总线号.
	 * __i2c_first_dynamic_bus_num 是一个全局变量, 记录了可以被动态分配的I2C总线号的起始值.
	 */
	if (retval >= __i2c_first_dynamic_bus_num)
		/*
		 * 如果是, 就更新动态总线号的起始值, 使其比设备树中定义的最大号还要大1.
		 * 这样就避免了未来的动态分配与静态定义发生冲突.
		 */
		__i2c_first_dynamic_bus_num = retval + 1;
	/*
	 * 释放写锁, 允许其他任务访问.
	 */
	up_write(&__i2c_board_lock);

	/*
	 * 调用 bus_register(), 向内核的驱动模型核心注册 i2c_bus_type.
	 * i2c_bus_type 是一个全局的 struct bus_type 实例, 它定义了I2C总线的名称("i2c")、
	 * 设备与驱动的匹配规则(match函数)等核心行为. 这是I2C子系统能够工作的最关键一步.
	 */
	retval = bus_register(&i2c_bus_type);
	/*
	 * 如果总线注册失败, 这是一个致命错误, 立即返回错误码.
	 */
	if (retval)
		return retval;

	/*
	 * 设置一个全局标志位, 表示I2C核心总线已经成功注册.
	 */
	is_registered = true;

	/*
	 * 在 debugfs 文件系统中创建一个名为 "i2c" 的目录 (/sys/kernel/debug/i2c).
	 * I2C适配器和设备驱动可以在此目录下创建自己的调试文件.
	 * 如果内核没有开启debugfs, debugfs_create_dir 会返回NULL, 但不会影响程序执行.
	 */
	i2c_debugfs_root = debugfs_create_dir("i2c", NULL);

	/*
	 * 调用 i2c_add_driver() 注册一个名为 dummy_driver 的虚拟I2C驱动.
	 * 这个驱动不与任何真实硬件交互, 它的作用是作为一个占位符, 用户可以从用户空间
	 * 将某个I2C地址绑定到这个驱动, 以阻止其他真实驱动去探测或使用该地址.
	 */
	retval = i2c_add_driver(&dummy_driver);
	/*
	 * 如果注册dummy驱动失败, 跳转到 class_err 标签去执行清理操作.
	 */
	if (retval)
		goto class_err;

	/*
	 * 如果内核配置了动态设备树(CONFIG_OF_DYNAMIC), 则注册一个通知器, 以便在设备树发生变化时得到通知.
	 * 这在STM32这类硬件配置静态的系统上通常不启用. WARN_ON确保如果注册失败会打印警告.
	 */
	if (IS_ENABLED(CONFIG_OF_DYNAMIC))
		WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier));
	/*
	 * 如果内核配置了ACPI(CONFIG_ACPI), 则注册一个ACPI通知器.
	 * ACPI是PC架构的技术, 在STM32上不使用.
	 */
	if (IS_ENABLED(CONFIG_ACPI))
		WARN_ON(acpi_reconfig_notifier_register(&i2c_acpi_notifier));

	/*
	 * 所有初始化步骤成功完成, 返回0.
	 */
	return 0;

/*
 * 错误处理标签. 如果在注册dummy驱动后发生错误, 跳转到这里.
 */
class_err:
	/*
	 * 重置标志位.
	 */
	is_registered = false;
	/*
	 * 注销之前成功注册的I2C总线类型, 保持系统状态的一致性.
	 */
	bus_unregister(&i2c_bus_type);
	/*
	 * 返回导致错误的错误码.
	 */
	return retval;
}
/*
 * 我们必须尽早初始化, 因为某些子系统在 subsys_initcall() 代码中注册i2c驱动,
 * 但它们在链接(和初始化)时排在i2c之前.
 */
/*
 * postcore_initcall 是一个宏, 用于将 i2c_init 函数注册为一个内核初始化回调.
 * "postcore" 阶段确保了I2C核心框架在大多数设备驱动(如在 subsys_initcall 阶段初始化的驱动)
 * 尝试注册它们的I2C客户端驱动之前就已经准备就绪.
 */
postcore_initcall(i2c_init);

I2C地址有效性检查:标准与严格模式

本代码片段提供了两个用于验证I2C从设备地址有效性的辅助函数:i2c_check_addr_validityi2c_check_7bit_addr_validity_strict。它们的核心功能是在I2C通信或设备探测之前,对给定的地址进行检查,以确保其符合I2C协议规范。这两个函数分别代表了两种不同的检查策略:一种是相对宽松的常规检查,另一种是用于自动探测场景下的严格检查。

实现原理分析

这两个函数通过简单的整数比较来实现其功能,但它们检查的范围和依据的规则有所不同,以适应不同的使用场景。

  1. 宽松检查 (i2c_check_addr_validity):

    • 此函数是"宽容的"(permissive),设计用于常规的设备实例化,即当开发者明确知道要与哪个地址的设备通信时使用。
    • 它能够处理7位和10位两种地址模式,通过flags参数中的I2C_CLIENT_TEN标志来区分。
    • 对于10位地址,它只检查地址是否超出了0x3ff(1023)的范围。
    • 对于7位地址,它只排除了两个绝对无效的情况:通用调用地址(General Call Address)0x00和超出7位范围(> 0x7f)的地址。它故意不检查I2C规范中定义的其他保留地址(如0x01-0x07,0x78-0x7f),因为在某些特殊或非标准的硬件实现中,这些地址可能被实际使用。
  2. 严格检查 (i2c_check_7bit_addr_validity_strict):

    • 此函数是"严格的"(strict),专门用于总线探测(probing)场景。
    • 它假定只处理7位地址,因为10位地址的设备相对少见,并且其探测机制不同,通常需要被显式地枚举而不是自动扫描。
    • 它严格遵守I2C规范,将所有为特殊功能保留的地址范围都视为无效。根据I2C标准,有效的普通从设备地址范围是从0x080x77。任何在此范围之外的地址都会被拒绝。
    • 这种严格性在自动扫描时至关重要,可以避免意外地向广播地址、Hs-mode主控代码或10位地址的引导序列发送数据,从而防止总线状态混乱或不可预期的硬件行为。

代码分析

c 复制代码
// i2c_check_addr_validity: 宽容的I2C地址有效性检查函数。
// 描述: 此函数故意不强制执行I2C地址映射的所有约束,只排除最基本的无效地址。
// @addr: 要检查的地址。
// @flags: 客户端标志,用于判断是7位还是10位地址。
// 返回值: 地址有效则返回0,无效则返回-EINVAL。
static int i2c_check_addr_validity(unsigned int addr, unsigned short flags)
{
	// 检查是否为10位地址模式。
	if (flags & I2C_CLIENT_TEN) {
		/* 10位地址,所有0x000到0x3ff之间的值都是有效的。*/
		if (addr > 0x3ff)
			return -EINVAL;
	} else {
		/* 7位地址,拒绝通用调用地址(0x00)和超出7位范围的地址。*/
		if (addr == 0x00 || addr > 0x7f)
			return -EINVAL;
	}
	// 地址有效。
	return 0;
}

// i2c_check_7bit_addr_validity_strict: 严格的7位I2C地址有效性检查函数。
// 描述: 用于探测(probing)场景。如果一个设备使用了I2C规范中的保留地址,
//       那么它不应该被探测到。
// @addr: 要检查的7位地址。
// 返回值: 地址在有效范围内则返回0,在保留范围内则返回-EINVAL。
int i2c_check_7bit_addr_validity_strict(unsigned short addr)
{
	/*
	 * 根据I2C规范的保留地址:
	 *  0x00       通用调用地址 / START字节
	 *  0x01       CBUS 地址
	 *  0x02       为其他总线格式保留
	 *  0x03       为未来目的保留
	 *  0x04-0x07  Hs-mode (高速模式) 主控代码
	 *  0x78-0x7b  用于10位从设备寻址的头部序列
	 *  0x7c-0x7f  为未来目的保留
	 */

	// 因此,任何小于0x08或大于0x77的地址都被认为是无效的普通设备地址。
	if (addr < 0x08 || addr > 0x77)
		return -EINVAL;
	
	// 地址有效。
	return 0;
}

I2C地址占用检查:处理I2C多路复用器(MUX)树

本代码片段实现了一个关键的I2C核心功能:i2c_check_addr_busy。其核心作用是检查一个给定的I2C地址在特定的I2C总线(适配器)上是否已经被某个设备占用。这个功能不仅仅是检查当前总线,其复杂性和精妙之处在于它能够正确处理由I2C多路复用器(MUX)或切换器(switch)构成的复杂的树状总线拓扑。

实现原理分析

在现代嵌入式系统中,一个物理I2C控制器后面可能会连接一个I2C MUX芯片,该芯片又能分出多个独立的I2C总线段。在Linux内核中,这被抽象为一个父i2c_adapter下有多个子i2c_adapter的树形结构。由于在物理上,所有这些总线段共享相同的SDA/SCL线路(只是由MUX进行切换),因此一个I2C地址在整个MUX树的任何一个分支上被占用,就意味着在所有其他分支上该地址也无法使用。本代码正是为了解决这个问题而设计的。

其实现原理是一个分两步的、递归的树遍历算法:

  1. 向上及同级检查 (i2c_check_mux_parents):

    • 当要检查一个适配器上的地址时,代码首先会沿着MUX树向上回溯。
    • 对于每一级的父适配器,它会遍历该父适配器下的所有直接子设备。这相当于检查了当前适配器的所有"兄弟"适配器(sibling buses)上的设备。
    • 这个过程会一直递归到最顶层的根适配器(物理I2C控制器),从而确保了在整个上游路径及所有旁系分支中没有地址冲突。
  2. 向下检查 (i2c_check_mux_children):

    • 在确认上游和同级没有冲突后,代码会从当前适配器开始,向下递归遍历。
    • 它会遍历当前适配器下的所有子设备。如果子设备本身又是一个适配器(即下一级MUX),则递归地对这个子适配器进行向下检查。如果子设备是一个普通的i2c_client,则直接检查其地址。
    • 这个过程确保了在当前总线段及其所有下游分支中没有地址冲突。
  3. 核心比较 (__i2c_check_addr_busy):

    • 所有遍历的最终落脚点都是__i2c_check_addr_busy这个回调函数。它负责对单个设备进行检查,判断其是否是一个i2c_client以及其地址是否与目标地址匹配。如果匹配,则返回-EBUSY,这个错误码会中断遍历并逐层向上传递,表示"地址已被占用"。

i2c_check_addr_busy作为入口函数,巧妙地编排了这两个搜索过程,从而实现了对整个相关总线树的完整性检查。

代码分析

c 复制代码
// __i2c_check_addr_busy: 检查单个设备是否占用目标地址的回调函数。
// @dev:   指向被检查的设备。
// @addrp: 指向目标地址整数的void指针。
// 返回值: 如果设备占用了该地址,返回-EBUSY;否则返回0。
static int __i2c_check_addr_busy(struct device *dev, void *addrp)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	int			addr = *(int *)addrp;

	// 检查该设备是否是一个有效的i2c_client,并且其地址与目标地址匹配。
	// i2c_encode_flags_to_addr会将客户端的7/10位地址和标志转换为一个统一的整数。
	if (client && i2c_encode_flags_to_addr(client) == addr)
		return -EBUSY; // 地址被占用
	return 0; // 地址未被此设备占用
}

// i2c_check_mux_parents: 递归地向上遍历MUX树,检查父节点和兄弟节点。
// @adapter: 当前检查的适配器。
// @addr:    要检查的目标地址。
static int i2c_check_mux_parents(struct i2c_adapter *adapter, int addr)
{
	// 查找当前适配器的父适配器。
	struct i2c_adapter *parent = i2c_parent_is_i2c_adapter(adapter);
	int result;

	// 首先,遍历并检查当前适配器下的所有直接子设备。
	result = device_for_each_child(&adapter->dev, &addr,
					__i2c_check_addr_busy);

	// 如果没有发现冲突,并且存在父适配器,则递归地对父适配器进行检查。
	if (!result && parent)
		result = i2c_check_mux_parents(parent, addr);

	return result;
}

// i2c_check_mux_children: 递归地向下遍历MUX树,检查子节点。
// @dev:   当前检查的设备。
// @addrp: 指向目标地址。
static int i2c_check_mux_children(struct device *dev, void *addrp)
{
	int result;

	// 如果当前设备本身就是一个适配器(即一个MUX的下游总线)。
	if (dev->type == &i2c_adapter_type)
		// 则递归地遍历这个子适配器下的所有子设备。
		result = device_for_each_child(dev, addrp,
						i2c_check_mux_children);
	else
		// 如果是普通设备(i2c_client),则直接进行地址比较。
		result = __i2c_check_addr_busy(dev, addrp);

	return result;
}

// i2c_check_addr_busy: 检查I2C地址是否被占用的主入口函数。
// @adapter: 开始检查的适配器。
// @addr:    要检查的目标地址。
static int i2c_check_addr_busy(struct i2c_adapter *adapter, int addr)
{
	struct i2c_adapter *parent = i2c_parent_is_i2c_adapter(adapter);
	int result = 0;

	// 如果存在父适配器,首先向上回溯检查所有父节点和兄弟节点。
	if (parent)
		result = i2c_check_mux_parents(parent, addr);

	// 如果向上检查没有发现冲突。
	if (!result)
		// 则从当前适配器开始,向下递归检查所有子节点。
		result = device_for_each_child(&adapter->dev, &addr,
						i2c_check_mux_children);

	return result;
}

I2C设备实例化与注销:创建I2C从设备的核心接口

本代码片段展示了Linux I2C子系统的核心功能之一:i2c_new_client_device函数,它负责在内核中实例化(创建)一个代表物理I2C从设备的i2c_client对象。同时,也提供了与之配对的i2c_unregister_device函数用于销毁该对象。这是I2C子系统将板级信息(来自设备树、ACPI或board file)转化为一个具体的、可与驱动绑定的内核对象的关键枢纽。

实现原理分析

i2c_new_client_device的实现是一个严谨的多步骤过程,深度集成在Linux的统一设备模型之中。

  1. 内存分配与基础信息填充 : 函数首先为i2c_client结构体分配内存。然后,它从调用者传入的i2c_board_info结构体中拷贝最基础的信息,包括它所属的适配器(adapter)、设备地址(addr)、标志(flags)和平台数据(platform_data)。

  2. 中断资源处理 : 它通过i2c_dev_irq_from_resources函数,提供了一种灵活的方式来处理中断。驱动可以从一个标准的resource数组中解析出IRQ号并设置其中断触发类型。这使得中断信息可以和内存、I/O端口等其他硬件资源一样被标准化地描述。

  3. 地址有效性与并发控制: 这是实例化过程中至关重要的一步。

    • 验证 : 首先调用i2c_check_addr_validity对地址进行初步的(宽容的)检查。
    • 锁定 : 接着调用i2c_lock_addr。这是一个轻量级的锁机制,它使用适配器内部的一个位图(addrs_in_instantiation)来标记某个7位地址正在被实例化。这可以防止两个不同的执行路径(例如,一个来自设备树,一个来自用户空间)并发地在同一地址上创建设备,从而避免了竞争条件。
    • 占用检查 : 在成功获取锁之后,再调用i2c_check_addr_busy。这个函数会执行复杂的、能够感知I2C MUX拓扑的检查,以确保该地址在整个相关的总线树上都未被占用。
  4. 设备模型注册:

    • 在所有检查通过后,函数会填充client->dev这个内嵌的device结构体。它设置了设备的父节点(parent)、总线类型(bus)和设备类型(type),将其正式地链入设备模型的层级结构中。
    • 最关键的一步是调用device_register(&client->dev)。这个函数会将设备注册到内核,使其在sysfs中可见。更重要的是,它会触发i2c_bus_type去寻找一个能够匹配此新设备的驱动。如果找到了匹配的驱动,总线核心会立即调用该驱动的.probe()函数,从而完成设备与驱动的绑定。
  5. 注销过程 : i2c_unregister_device执行相反的操作。其核心是调用device_unregister(),这会触发总线核心调用已绑定驱动的.remove()函数,并最终将设备从内核和sysfs中移除。

代码分析

c 复制代码
struct i2c_adapter *i2c_get_adapter(int nr)
{
	struct i2c_adapter *adapter;

	mutex_lock(&core_lock);
	adapter = idr_find(&i2c_adapter_idr, nr);
	if (!adapter)
		goto exit;

	if (try_module_get(adapter->owner))
		get_device(&adapter->dev);
	else
		adapter = NULL;

 exit:
	mutex_unlock(&core_lock);
	return adapter;
}
EXPORT_SYMBOL(i2c_get_adapter);

void i2c_put_adapter(struct i2c_adapter *adap)
{
	if (!adap)
		return;

	module_put(adap->owner);
	/* Should be last, otherwise we risk use-after-free with 'adap' */
	put_device(&adap->dev);
}
EXPORT_SYMBOL(i2c_put_adapter);

// i2c_dev_irq_from_resources: 从标准资源数组中解析IRQ信息。
// @resources: 指向资源描述符数组的指针。
// @num_resources: 数组中的资源数量。
// 返回值: 成功则返回IRQ号,失败或未找到则返回0。
int i2c_dev_irq_from_resources(const struct resource *resources,
			       unsigned int num_resources)
{
	struct irq_data *irqd;
	int i;

	// 遍历资源数组。
	for (i = 0; i < num_resources; i++) {
		const struct resource *r = &resources[i];

		// 如果当前资源不是IRQ类型,则跳过。
		if (resource_type(r) != IORESOURCE_IRQ)
			continue;

		// 如果资源标志位中包含了中断触发类型信息。
		if (r->flags & IORESOURCE_BITS) {
			irqd = irq_get_irq_data(r->start);
			if (!irqd)
				break;

			// 设置中断的触发类型(如上升沿、下降沿等)。
			irqd_set_trigger_type(irqd, r->flags & IORESOURCE_BITS);
		}

		// 返回找到的第一个IRQ号。
		return r->start;
	}

	return 0;
}

/* 这是一个用于防止并发实例化的轻量级锁机制 */

// i2c_lock_addr: 锁定一个I2C地址,防止并发实例化。
// @adap:  I2C适配器。
// @addr:  要锁定的7位地址。
// @flags: 设备标志。
static int i2c_lock_addr(struct i2c_adapter *adap, unsigned short addr,
			 unsigned short flags)
{
	// 此锁机制仅对7位地址有效。
	// test_and_set_bit是原子操作,如果该位原先是0,则将其置1并返回0;
	// 如果原先是1,则不改变并返回1。
	if (!(flags & I2C_CLIENT_TEN) &&
	    test_and_set_bit(addr, adap->addrs_in_instantiation))
		return -EBUSY; // 如果位已设置,说明正在实例化,返回忙。

	return 0;
}

// i2c_unlock_addr: 解锁一个I2C地址。
static void i2c_unlock_addr(struct i2c_adapter *adap, unsigned short addr,
			    unsigned short flags)
{
	if (!(flags & I2C_CLIENT_TEN))
		clear_bit(addr, adap->addrs_in_instantiation);
}

// i2c_new_client_device: 实例化一个新的I2C设备(客户端)。
// @adap: 管理此设备的适配器。
// @info: 描述I2C设备信息的结构体。
// 返回值: 成功则返回新创建的i2c_client指针,失败则返回ERR_PTR。
struct i2c_client *
i2c_new_client_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
	struct i2c_client *client;
	int status;

	// 为i2c_client结构体分配内存。
	client = kzalloc(sizeof *client, GFP_KERNEL);
	if (!client)
		return ERR_PTR(-ENOMEM);

	// 填充基础信息。
	client->adapter = adap;
	client->dev.platform_data = info->platform_data;
	client->flags = info->flags;
	client->addr = info->addr;

	// 获取IRQ号,优先使用直接指定的irq,否则从资源列表中解析。
	client->init_irq = info->irq;
	if (!client->init_irq)
		client->init_irq = i2c_dev_irq_from_resources(info->resources,
							 info->num_resources);

	// 复制设备类型字符串作为设备名。
	strscpy(client->name, info->type, sizeof(client->name));

	// 检查地址的有效性。
	status = i2c_check_addr_validity(client->addr, client->flags);
	if (status)
		goto out_err_silent;

	// 锁定地址以防并发实例化。
	status = i2c_lock_addr(adap, client->addr, client->flags);
	if (status)
		goto out_err_silent;

	// 检查地址是否已被其他设备占用(MUX-aware)。
	status = i2c_check_addr_busy(adap, i2c_encode_flags_to_addr(client));
	if (status)
		goto out_err;

	// 填充device结构体,将其接入设备模型。
	client->dev.parent = &client->adapter->dev;
	client->dev.bus = &i2c_bus_type;
	client->dev.type = &i2c_client_type;
	device_set_node(&client->dev, info->fwnode); // 设置固件节点(如DT节点)

	// 设置设备名称。
	i2c_dev_set_name(adap, client, info);
	// 注册设备到内核,此步骤将触发驱动探测和绑定。
	status = device_register(&client->dev);
	if (status)
		goto out_err;

	// 成功后解锁地址。
	i2c_unlock_addr(adap, client->addr, client->flags);

	return client;

out_err:
	dev_err(&adap->dev,
		"Failed to register i2c client %s at 0x%02x (%d)\n",
		client->name, client->addr, status);
	// 错误处理路径:解锁地址。
	i2c_unlock_addr(adap, client->addr, client->flags);
out_err_silent:
	// 错误处理路径:释放已分配的内存。
	kfree(client);
	return ERR_PTR(status);
}
EXPORT_SYMBOL_GPL(i2c_new_client_device);

// i2c_unregister_device: 注销一个I2C设备。
// @client: 指向要注销的i2c_client的指针。
void i2c_unregister_device(struct i2c_client *client)
{
	if (IS_ERR_OR_NULL(client))
		return;

	// 从内核中注销设备,这将触发驱动的remove函数。
	device_unregister(&client->dev);
}
EXPORT_SYMBOL_GPL(i2c_unregister_device);

I2C设备探测与实例化:在I2C总线上动态发现并创建设备

本代码片段展示了Linux I2C核心中的一个重要(但目前已不推荐使用)的机制:基于类的设备探测和动态实例化。其核心功能是,当一个I2C驱动程序被加载时,I2C核心会主动地、根据该驱动提供的一个地址列表,去扫描I2C总线,尝试发现并创建(实例化)该驱动所支持的硬件设备。这套机制允许在没有设备树(Device Tree)或ACPI描述的情况下,自动发现和配置I2C从设备。

实现原理分析

该机制的实现是一个多阶段、层层深入的探测过程,由I2C核心与I2C设备驱动协同完成。

  1. 触发点 : 过程的入口是i2c_do_add_adapter函数,它在I2C驱动注册并与一个已存在的I2C适配器(总线控制器)关联时被调用。它随即调用i2c_detect来启动探测流程。

  2. 地址列表遍历 (i2c_detect):

    • i2c_detect函数首先检查驱动是否支持此探测机制(即driver->detect函数指针和driver->address_list都必须存在)。
    • 它会进行"类"匹配,只有当适配器(adapter->class)和驱动(driver->class)的类掩码有交集时,探测才会继续。这是一种粗粒度的过滤,例如,只在标记为硬件监控(HWMON)的总线上探测硬件监控芯片。
    • 该函数会遍历驱动提供的address_list,这是一个以I2C_CLIENT_END结尾的7位I2C地址数组。
  3. 单地址探测 (i2c_detect_address) : 对于地址列表中的每一个地址,i2c_detect_address会执行以下步骤:

    • 合法性与忙碌检查: 确认地址是有效的7位地址,并检查该地址是否已经被其他设备占用。
    • 物理存在性探测 (i2c_default_probe) : 这是关键的第一步物理探测。i2c_default_probe尝试通过发送一个简单的I2C事务(如 SMBus QUICK WRITE 或 READ BYTE)来判断该地址上是否有设备应答(ACK)。它包含一些特殊的规避逻辑,例如对于某些可能被写操作损坏的EEPROM地址范围,会强制使用读操作进行探测。只有物理上存在应答的设备,才会进入下一步。
    • 驱动专属识别 : 如果物理探测成功,核心会调用驱动自己实现的driver->detect(temp_client, &info)函数。在这一步,驱动可以执行更复杂的I2C事务(例如读取芯片ID寄存器)来精确识别设备是否是它所支持的型号。
    • 设备实例化 : 如果driver->detect()返回成功,并填充了设备信息(尤其是设备类型info.type),I2C核心就会调用i2c_new_client_device()来创建一个新的i2c_client实例。这个过程相当于在内核中动态地"插入"了一个设备。一旦设备被创建,设备模型就会自动尝试将它与当前这个驱动进行绑定,最终调用驱动的.probe()函数。

代码分析

c 复制代码
// struct i2c_adapter: I2C适配器结构体
// 用于标识一个物理I2C总线,并包含了访问该总线所需的算法和数据。
struct i2c_adapter {
	struct module *owner;             // 指向拥有此适配器的模块
	unsigned int class;		  // 适配器的类别,用于设备探测过滤
	const struct i2c_algorithm *algo; // 访问总线的算法函数集
	void *algo_data;                  // 算法私有数据

	/* 对所有设备都有效的数据字段 */
	const struct i2c_lock_operations *lock_ops; // 总线锁定操作
	struct rt_mutex bus_lock;         // 保护总线访问的实时互斥锁
	struct rt_mutex mux_lock;         // 用于I2C MUX的互斥锁

	int timeout;			  // 事务超时时间 (单位:jiffies)
	int retries;			  // 重试次数
	struct device dev;		  // 适配器对应的设备结构体
	unsigned long locked_flags;	  // I2C核心拥有的锁定标志

	int nr;                           // 适配器的全局唯一编号
	char name[48];                    // 适配器的名称
	struct completion dev_released;   // 设备释放完成量

	struct mutex userspace_clients_lock; // 保护用户空间客户端列表的互斥锁
	struct list_head userspace_clients; // 用户空间客户端列表

	// ... 其他字段 ...
};
// to_i2c_adapter: 一个宏,通过内嵌的device结构体指针找到其宿主i2c_adapter结构体的指针。
#define to_i2c_adapter(d) container_of(d, struct i2c_adapter, dev)

// i2c_do_add_adapter: 当驱动和适配器关联时,执行设备探测。
// @driver: 新关联的I2C驱动。
// @adap: 驱动所关联的I2C适配器。
static int i2c_do_add_adapter(struct i2c_driver *driver,
			      struct i2c_adapter *adap)
{
	/* 在该总线上探测驱动所支持的设备,并实例化它们。*/
	i2c_detect(adap, driver);

	return 0;
}


// i2c_default_probe: 默认的物理存在性探测函数。
// 描述: 尝试通过一次简单的I2C/SMBus写或读操作来检查指定地址是否有设备应答。
//       包含特殊逻辑以避免损坏某些EEPROM。
// @adap: 进行探测的I2C适配器。
// @addr: 要探测的7位I2C地址。
// 返回值: 如果探测到设备应答则返回1,否则返回0。
static int i2c_default_probe(struct i2c_adapter *adap, unsigned short addr)
{
	int err;
	union i2c_smbus_data dummy;

	/*
	 * 对于非EEPROM地址范围 (0x30-0x37, 0x50-0x5f),并且适配器支持
	 * SMBUS_QUICK功能,优先使用快速写命令探测。
	 */
	if (!((addr & ~0x07) == 0x30 || (addr & ~0x0f) == 0x50)
	 && i2c_check_functionality(adap, I2C_FUNC_SMBUS_QUICK))
		err = i2c_smbus_xfer(adap, addr, 0, I2C_SMBUS_WRITE, 0,
				     I2C_SMBUS_QUICK, NULL);
	/* 否则,如果适配器支持读字节功能,则使用读字节命令探测。*/
	else if (i2c_check_functionality(adap, I2C_FUNC_SMBUS_READ_BYTE))
		err = i2c_smbus_xfer(adap, addr, 0, I2C_SMBUS_READ, 0,
				     I2C_SMBUS_BYTE, &dummy);
	/* 如果两种方法都不支持,则无法探测。*/
	else {
		dev_warn(&adap->dev, "No suitable probing method supported for address 0x%02X\n",
			 addr);
		err = -EOPNOTSUPP;
	}

	// 如果i2c_smbus_xfer返回非负值(成功),则表示探测成功。
	return err >= 0;
}

// i2c_detect_address: 在单个地址上执行完整的探测和实例化流程。
// @temp_client: 一个临时的i2c_client结构体,用于传递adapter和addr。
// @driver: 正在进行探测的I2C驱动。
static int i2c_detect_address(struct i2c_client *temp_client,
			      struct i2c_driver *driver)
{
	struct i2c_board_info info;
	struct i2c_adapter *adapter = temp_client->adapter;
	int addr = temp_client->addr;
	int err;

	/* 确保地址是有效的7位地址。*/
	err = i2c_check_7bit_addr_validity_strict(addr);
	if (err) {
		dev_warn(&adapter->dev, "Invalid probe address 0x%02x\n",
			 addr);
		return err;
	}

	/* 如果地址已被占用,则跳过。*/
	if (i2c_check_addr_busy(adapter, addr))
		return 0;

	/* 确保该地址上物理存在设备(有ACK应答)。*/
	if (!i2c_default_probe(adapter, addr))
		return 0;

	/* 最后,调用驱动自定义的探测函数进行精确识别。*/
	memset(&info, 0, sizeof(struct i2c_board_info));
	info.addr = addr;
	err = driver->detect(temp_client, &info);
	if (err) {
		/* 如果驱动返回-ENODEV,表示"不是我支持的设备",这不是一个错误,正常返回。*/
		return err == -ENODEV ? 0 : err;
	}

	/* 检查驱动的detect函数是否提供了设备类型名称。*/
	if (info.type[0] == '\0') {
		dev_err(&adapter->dev,
			"%s detection function provided no name for 0x%x\n",
			driver->driver.name, addr);
	} else {
		struct i2c_client *client;

		/* 探测成功,实例化该设备。*/
		dev_dbg(&adapter->dev, "Creating %s at 0x%02x\n",
			info.type, info.addr);
		// 创建一个新的i2c_client设备实例。
		client = i2c_new_client_device(adapter, &info);
		if (!IS_ERR(client))
			// 将新创建的client添加到驱动的已探测设备列表中。
			list_add_tail(&client->detected, &driver->clients);
		else
			dev_err(&adapter->dev, "Failed creating %s at 0x%02x\n",
				info.type, info.addr);
	}
	return 0;
}

// i2c_detect: I2C设备探测的主函数。
// @adapter: 进行探测的I2C适配器。
// @driver: 正在进行探测的I2C驱动。
static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
{
	const unsigned short *address_list;
	struct i2c_client *temp_client;
	int i, err = 0;

	address_list = driver->address_list;
	// 如果驱动不支持探测(没有detect函数或地址列表),则直接返回。
	if (!driver->detect || !address_list)
		return 0;

	/* 如果适配器和驱动的class不匹配,则停止探测。*/
	if (!(adapter->class & driver->class))
		return 0;

	/* 分配一个临时的i2c_client结构体,用于在回调中传递参数。*/
	temp_client = kzalloc(sizeof(*temp_client), GFP_KERNEL);
	if (!temp_client)
		return -ENOMEM;

	temp_client->adapter = adapter;

	// 遍历驱动提供的地址列表。
	for (i = 0; address_list[i] != I2C_CLIENT_END; i += 1) {
		dev_dbg(&adapter->dev,
			"found normal entry for adapter %d, addr 0x%02x\n",
			i2c_adapter_id(adapter), address_list[i]);
		temp_client->addr = address_list[i];
		// 对每个地址执行探测。
		err = i2c_detect_address(temp_client, driver);
		if (unlikely(err))
			break;
	}

	// 释放临时客户端。
	kfree(temp_client);

	return err;
}

I2C驱动注册与管理:驱动程序与总线核心的接口

本代码片段摘自I2C核心层,提供了I2C客户端驱动程序向内核注册和注销自身的标准接口。其核心功能是管理I2C驱动程序的生命周期,并将它们与I2C总线上的物理设备进行绑定(binding)或解绑。这是Linux设备模型中总线、驱动和设备三者之间交互的具体实现,是所有I2C设备驱动能够工作的基础。

宏定义与辅助函数

c 复制代码
/* 使用一个宏来避免为了获取THIS_MODULE而产生的头文件包含链 */
#define i2c_add_driver(driver) \
	i2c_register_driver(THIS_MODULE, driver)

/* ------------------------------------------------------------------------- */

// i2c_for_each_dev: 遍历I2C总线上的每一个设备并执行一个回调函数。
// @data: 传递给回调函数的私有数据。
// @fn: 回调函数指针,原型为 int (*fn)(struct device *dev, void *data)。
// 返回值: 如果回调函数提前返回一个非零值,则该值被作为此函数的返回值;否则返回0。
int i2c_for_each_dev(void *data, int (*fn)(struct device *dev, void *data))
{
	int res;

	// 加锁以保护I2C核心的数据结构,防止并发访问。
	mutex_lock(&core_lock);
	// 调用通用的总线设备遍历函数来遍历i2c_bus_type上的所有设备。
	res = bus_for_each_dev(&i2c_bus_type, NULL, data, fn);
	// 解锁。
	mutex_unlock(&core_lock);

	return res;
}
// 导出符号,使得其他内核模块可以调用此函数。
EXPORT_SYMBOL_GPL(i2c_for_each_dev);

// __process_new_driver: 作为回调函数,用于处理新驱动注册时与已存在适配器的关系。
// @dev: 当前遍历到的设备。
// @data: 传入的私有数据,此处为新注册的 i2c_driver 指针。
// 返回值: 恒为0。
static int __process_new_driver(struct device *dev, void *data)
{
	// 检查设备是否为一个I2C适配器。我们只关心适配器。
	if (dev->type != &i2c_adapter_type)
		return 0;
	// 对于每一个已存在的适配器,调用i2c_do_add_adapter来触发总线扫描和设备探测。
	return i2c_do_add_adapter(data, to_i2c_adapter(dev));
}

I2C驱动注册与注销接口

c 复制代码
// i2c_register_driver: 注册一个I2C驱动程序。
// @owner: 指向驱动程序所属模块的指针,通常是THIS_MODULE。
// @driver: 指向要注册的i2c_driver结构体的指针。
// 返回值: 成功则为0,失败则为负数错误码。
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
	int res;

	/* 必须在驱动模型初始化完成后才能注册 */
	if (WARN_ON(!is_registered))
		return -EAGAIN;

	/* 将驱动添加到驱动模型核心的i2c驱动列表中 */
	driver->driver.owner = owner;       // 设置驱动所属的模块。
	driver->driver.bus = &i2c_bus_type; // 明确指定此驱动属于I2C总线。
	INIT_LIST_HEAD(&driver->clients);   // 初始化该驱动绑定的客户端设备链表。

	/* 当此函数返回时,驱动模型核心已经为所有匹配但未绑定的设备调用了probe()函数。 */
	res = driver_register(&driver->driver);
	if (res)
		return res;

	pr_debug("driver [%s] registered\n", driver->driver.name);

	/* 遍历系统中所有已经存在的适配器,确保新驱动能在这些适配器上探测设备。 */
	i2c_for_each_dev(driver, __process_new_driver);

	return 0;
}
// 导出符号,供i2c_add_driver宏使用以及其他模块直接调用。
EXPORT_SYMBOL(i2c_register_driver);

// __process_removed_driver: 作为回调函数,用于处理驱动被移除时与适配器的关系。
// @dev: 当前遍历到的设备。
// @data: 传入的私有数据,此处为被移除的 i2c_driver 指针。
// 返回值: 恒为0。
static int __process_removed_driver(struct device *dev, void *data)
{
	if (dev->type == &i2c_adapter_type)
		// 通知每一个适配器,此驱动已被移除,以便进行清理。
		i2c_do_del_adapter(data, to_i2c_adapter(dev));
	return 0;
}

// i2c_del_driver: 注销一个I2C驱动程序。
// @driver: 指向要注销的i2c_driver结构体的指针。
// 上下文: 该函数可能会导致睡眠。
void i2c_del_driver(struct i2c_driver *driver)
{
	// 遍历所有适配器,通知它们此驱动将被移除。
	i2c_for_each_dev(driver, __process_removed_driver);

	// 调用驱动模型核心的注销函数。
	// 这会为所有绑定到此驱动的设备调用remove()函数,并最终将驱动从总线上移除。
	driver_unregister(&driver->driver);
	pr_debug("driver [%s] unregistered\n", driver->driver.name);
}
// 导出符号,使得内核其他部分可以调用此函数。
EXPORT_SYMBOL(i2c_del_driver);

基础I2C消息传输:I2C核心与硬件驱动的桥梁

本代码片段展示了__i2c_transfer函数,它是Linux I2C核心层中负责执行基础数据传输的最低级通用接口。其核心功能是接收一个由一个或多个i2c_msg结构体组成的消息数组,并将这个序列转发给底层的、与具体硬件相关的I2C适配器驱动去执行。这个函数是所有更高级I2C和SMBus协议函数(如i2c_smbus_xfer_emulated)的最终执行端点,构成了I2C子系统软件栈与硬件驱动之间的关键桥梁。

实现原理分析

__i2c_transfer的实现原理是作为I2C适配器驱动master_xfer方法的一个通用调度器和封装器。它在直接调用硬件驱动之前,添加了若干重要的健壮性与调试功能。

  1. 能力检查 : 函数首先验证适配器驱动是否实现了核心的master_xfer方法。如果这个函数指针为空,意味着该适配器驱动不支持基础的I2C协议传输,函数将返回错误。
  2. 上下文感知 : 它会检查当前是否处于原子上下文(通过i2c_in_atomic_xfer_mode()),并据此选择调用适配器驱动提供的普通master_xfer函数还是特殊的、非阻塞的master_xfer_atomic函数。这确保了在中断等不允许睡眠的环境下,I2C传输也能以正确的方式进行。
  3. 自动重试机制 : 函数实现了一个循环,用于在发生总线仲裁丢失(底层驱动返回-EAGAIN)时自动重试传输。重试的次数和超时时间由适配器自身的属性(adap->retries, adap->timeout)决定。这个机制对上层驱动透明,极大地增强了在多主控总线环境下的通信可靠性。
  4. 硬件怪癖(Quirks)处理 : 在传输前,它会调用i2c_check_for_quirks。这允许适配器驱动注册一些针对特定从设备的硬件限制或变通方法(例如,不支持0长度的读操作),I2C核心可以在此进行检查并提前中止不支持的事务。
  5. 调试与追踪 : 在传输前后,函数使用静态分支(static_branch_unlikely)来高效地调用内核的tracepoints。这使得在需要时可以动态开启详细的I2C传输日志,用于调试和性能分析,而在关闭时几乎没有性能开销。
  6. 调用分派 : 所有检查和准备工作完成后,它最终会通过函数指针adap->algo->master_xfer(...)adap->algo->master_xfer_atomic(...),将i2c_msg数组和消息数量直接传递给适配器驱动,由驱动程序负责将其转换为对物理硬件的实际操作。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互

当此函数被调用,且adap参数指向代表STM32H750片上I2C外设的适配器时,整个流程将直接与硬件交互:

  1. 调用adap->algo->master_xfer实际上会调用i2c-stm32.c驱动中实现的i2c_stm32_xfer(或类似)函数。
  2. i2c-stm32驱动会接收msgs数组和num。它会遍历这个数组,对每一个i2c_msg结构体进行解析。
  3. 对于数组中的第一个消息,驱动会配置STM32 I2C外设的控制寄存器(例如I2C_CR2)来设置从机地址、传输方向(读/写)、要传输的字节数,然后置位START位来启动传输。
  4. 对于写消息,驱动会循环地将msg->buf中的数据写入数据寄存器(I2C_TXDR)。对于读消息,它会循环地从数据寄存器(I2C_RXDR)中读取数据并存入msg->buf
  5. 如果num大于1,对于消息之间的转换(例如从写转向读),驱动会在第一个消息传输完成后,生成一个REPEATED START信号,而不是STOP信号,然后配置下一个消息的传输参数。
  6. 在最后一个消息传输完成后,驱动会生成一个STOP信号来释放总线。
    整个过程通过操作STM32 I2C外设的寄存器,精确地将i2c_msg数组描述的协议序列在物理总线上实现。
单核环境影响

此函数明确要求调用者必须已经持有适配器锁。因此,它自身不处理并发问题,而是依赖于外部的同步机制。在单核系统中,这个锁确保了__i2c_transfer的执行不会被另一个试图使用同一I2C总线的任务抢占。重试机制在单核系统中虽然不处理多核竞争,但对于处理由外部设备(另一个I2C主控)引起的仲裁丢失仍然有效。

无MMU影响

此函数的逻辑与MMU无关。它操作的i2c_msg结构中的缓冲区指针(buf)是内核空间的地址。在无MMU的STM32H750上,这些地址直接对应物理内存。如果底层i2c-stm32驱动使用DMA进行传输,那么上层(如i2c_smbus_xfer_emulated)在分配这个buf时必须确保它是物理连续的。在标准的无MMU内核中,kmalloc分配的内存天然就是物理连续的,因此与DMA的配合是自然的。函数本身的代码逻辑不受MMU缺失的影响。

代码分析

c 复制代码
// __i2c_transfer: i2c_transfer的无锁版本。
// @adap: 指向I2C总线适配器的句柄。
// @msgs: 在发出STOP信号终止操作前要执行的一个或多个消息。
// @num: 要执行的消息数量。
// 描述:
//   返回负的errno,否则返回已执行的消息数量。
//   调用此函数时必须已持有适配器锁。不进行调试日志记录。
int __i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	unsigned long orig_jiffies;
	int ret, try;

	// 检查底层适配器驱动是否实现了 master_xfer 算法。
	if (!adap->algo->master_xfer) {
		dev_dbg(&adap->dev, "I2C level transfers not supported\n");
		return -EOPNOTSUPP;
	}

	// 警告无效的输入参数。
	if (WARN_ON(!msgs || num < 1))
		return -EINVAL;

	// 检查适配器是否处于挂起状态。
	ret = __i2c_check_suspended(adap);
	if (ret)
		return ret;

	// 如果适配器有怪癖(quirks),检查当前传输是否与怪癖冲突。
	if (adap->quirks && i2c_check_for_quirks(adap, msgs, num))
		return -EOPNOTSUPP;

	/*
	 * i2c_trace_msg_key 在 i2c_transfer 跟踪点被启用时才为真。
	 * 这种静态分支优化可以高效地避免在不需要时执行下面的for循环。
	 */
	if (static_branch_unlikely(&i2c_trace_msg_key)) {
		int i;
		for (i = 0; i < num; i++)
			if (msgs[i].flags & I2C_M_RD)
				trace_i2c_read(adap, &msgs[i], i); // 记录读消息
			else
				trace_i2c_write(adap, &msgs[i], i); // 记录写消息
	}

	// 在仲裁丢失时自动重试
	orig_jiffies = jiffies; // 记录起始时间以用于超时计算
	for (ret = 0, try = 0; try <= adap->retries; try++) {
		// 如果在原子上下文且驱动支持原子传输,则使用原子传输函数。
		if (i2c_in_atomic_xfer_mode() && adap->algo->master_xfer_atomic)
			ret = adap->algo->master_xfer_atomic(adap, msgs, num);
		else
			// 否则,调用标准的传输函数。这是与硬件驱动交互的关键点。
			ret = adap->algo->master_xfer(adap, msgs, num);

		// 如果返回值不是 -EAGAIN (意为"请重试"),则退出循环。
		if (ret != -EAGAIN)
			break;
		// 如果重试时间超过了适配器定义的超时时间,也退出循环。
		if (time_after(jiffies, orig_jiffies + adap->timeout))
			break;
	}

	// 再次使用静态分支来高效地记录追踪信息。
	if (static_branch_unlikely(&i2c_trace_msg_key)) {
		int i;
		// 记录成功传输的读消息的回复。
		for (i = 0; i < ret; i++)
			if (msgs[i].flags & I2C_M_RD)
				trace_i2c_reply(adap, &msgs[i], i);
		// 记录整个传输操作的结果。
		trace_i2c_result(adap, num, ret);
	}

	return ret;
}
// 导出符号,使得内核其他部分(如I2C复用器驱动)可以调用此函数。
EXPORT_SYMBOL(__i2c_transfer);

/**
 * i2c_transfer - execute a single or combined I2C message
 * @adap: Handle to I2C bus
 * @msgs: One or more messages to execute before STOP is issued to
 *	terminate the operation; each message begins with a START.
 * @num: Number of messages to be executed.
 *
 * Returns negative errno, else the number of messages executed.
 *
 * Note that there is no requirement that each message be sent to
 * the same slave address, although that is the most common model.
 */
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	int ret;

	/* REVISIT the fault reporting model here is weak:
	 *
	 *  - When we get an error after receiving N bytes from a slave,
	 *    there is no way to report "N".
	 *
	 *  - When we get a NAK after transmitting N bytes to a slave,
	 *    there is no way to report "N" ... or to let the master
	 *    continue executing the rest of this combined message, if
	 *    that's the appropriate response.
	 *
	 *  - When for example "num" is two and we successfully complete
	 *    the first message but get an error part way through the
	 *    second, it's unclear whether that should be reported as
	 *    one (discarding status on the second message) or errno
	 *    (discarding status on the first one).
	 */
	ret = __i2c_lock_bus_helper(adap);
	if (ret)
		return ret;

	ret = __i2c_transfer(adap, msgs, num);
	i2c_unlock_bus(adap, I2C_LOCK_SEGMENT);

	return ret;
}
EXPORT_SYMBOL(i2c_transfer);

/**
 * i2c_master_recv - issue a single I2C message in master receive mode
 * @client: Handle to slave device
 * @buf: Where to store data read from slave
 * @count: How many bytes to read, must be less than 64k since msg.len is u16
 *
 * Returns negative errno, or else the number of bytes read.
 */
static inline int i2c_master_recv(const struct i2c_client *client,
				  char *buf, int count)
{
	return i2c_transfer_buffer_flags(client, buf, count, I2C_M_RD);
};

/**
 * i2c_master_recv_dmasafe - issue a single I2C message in master receive mode
 *			     using a DMA safe buffer
 * @client: Handle to slave device
 * @buf: Where to store data read from slave, must be safe to use with DMA
 * @count: How many bytes to read, must be less than 64k since msg.len is u16
 *
 * Returns negative errno, or else the number of bytes read.
 */
static inline int i2c_master_recv_dmasafe(const struct i2c_client *client,
					  char *buf, int count)
{
	return i2c_transfer_buffer_flags(client, buf, count,
					 I2C_M_RD | I2C_M_DMA_SAFE);
};

/**
 * i2c_master_send - issue a single I2C message in master transmit mode
 * @client: Handle to slave device
 * @buf: Data that will be written to the slave
 * @count: How many bytes to write, must be less than 64k since msg.len is u16
 *
 * Returns negative errno, or else the number of bytes written.
 */
static inline int i2c_master_send(const struct i2c_client *client,
				  const char *buf, int count)
{
	return i2c_transfer_buffer_flags(client, (char *)buf, count, 0);
};

/**
 * i2c_transfer_buffer_flags - issue a single I2C message transferring data
 *			       to/from a buffer
 * @client: Handle to slave device
 * @buf: Where the data is stored
 * @count: How many bytes to transfer, must be less than 64k since msg.len is u16
 * @flags: The flags to be used for the message, e.g. I2C_M_RD for reads
 *
 * Returns negative errno, or else the number of bytes transferred.
 */
int i2c_transfer_buffer_flags(const struct i2c_client *client, char *buf,
			      int count, u16 flags)
{
	int ret;
	struct i2c_msg msg = {
		.addr = client->addr,
		.flags = flags | (client->flags & I2C_M_TEN),
		.len = count,
		.buf = buf,
	};

	ret = i2c_transfer(client->adapter, &msg, 1);

	/*
	 * If everything went ok (i.e. 1 msg transferred), return #bytes
	 * transferred, else error code.
	 */
	return (ret == 1) ? count : ret;
}
EXPORT_SYMBOL(i2c_transfer_buffer_flags);
相关推荐
执笔论英雄10 分钟前
【大模型学习cuda】入们第一个例子-向量和
学习
wdfk_prog22 分钟前
[Linux]学习笔记系列 -- [drivers][input]input
linux·笔记·学习
ouliten36 分钟前
cuda编程笔记(36)-- 应用Tensor Core加速矩阵乘法
笔记·cuda
盟接之桥1 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
忆~遂愿1 小时前
ops-cv 算子库深度解析:面向视觉任务的硬件优化与数据布局(NCHW/NHWC)策略
java·大数据·linux·人工智能
湘-枫叶情缘1 小时前
1990:种下那棵不落叶的树-第6集 圆明园的对话
linux·系统架构
孞㐑¥1 小时前
算法——BFS
开发语言·c++·经验分享·笔记·算法
Fcy6482 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满2 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠3 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法