往期内容
I2C子系统专栏:
- I2C(IIC)协议讲解-CSDN博客
- SMBus 协议详解-CSDN博客
- I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客
- 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇
- 内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇
- 设备驱动与设备树匹配机制详解
- 编写一个通用的i2c设备驱动框架
- 编写一个通用的i2c控制器驱动框架
总线和设备树专栏:
- 总线和设备树_憧憬一下的博客-CSDN博客
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
前言
在上一章节(编写一个通用的i2c控制器驱动框架)讲了i2c_adapter。这里再继之前的文章( 编写一个通用的i2c设备驱动框架)进行补充,讲一下l2c_cleint的设备形态等
1. 两种设备形态
可以看出是有两种你描述的这两种设备形态反映了设备通过不同的总线进行交互的差异,并且在设备树(DTS)和驱动程序的编写上有不同的表现。根据设备的主要功能和数据传输方式,它们在 Linux 设备模型中的位置不同,从而影响了 DTS 文件中的结构和驱动程序的设计。我们来分析这两种设备形态。
形态 1: I2C-only 设备(图左侧):
- 特点:这类设备和 CPU 之间的所有数据交互都是通过 I2C 总线完成的,没有其他传输方式。
- DTS 描述:设备直接作为 I2C 控制器的子节点存在。在 DTS 中,设备的定义紧随其所在的 I2C 总线节点之后。例如 PMIC,它是 I2C 总线上的一个设备,因此直接在 I2C 节点的子节点中定义。
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
pmic: pf0100@08 {
compatible = "fsl,pfuze100";
reg = <0x08>;
...
};
};
- 驱动模型:I2C 核心负责处理设备的创建和注册。驱动程序是以 I2C slave 设备的方式来实现,I2C 驱动框架通过
i2c_driver
结构和i2c_device_id
来与设备匹配。设备的probe
、remove
等函数由 I2C 核心进行管理。
static const struct i2c_device_id pmic_i2c_ids[] = {
{ "pfuze100", 0 },
{ /* sentinel */ }
};
static struct i2c_driver pmic_driver = {
.driver = {
.name = "pfuze100",
.of_match_table = pmic_of_match,
},
.probe = pmic_probe,
.remove = pmic_remove,
.id_table = pmic_i2c_ids,
};
I2C 是主数据交互总线,设备完全依赖 I2C,所有操作通过 I2C 完成。
驱动注册流程通过 I2C 子系统的 i2c_driver
完成,设备由 I2C 核心管理。
形态 2: 复合设备(I2C 作为辅助接口,图右侧):
- 特点:这类设备通过多种方式与 CPU 交互,例如,主要的数据传输(如音视频数据)通过 TDMS 接口,而配置信息或控制信息通过 I2C(DDC)接口。这类设备的核心功能决定了它们的主要位置在其他总线(如平台总线
platform bus
)上,而 I2C 只是作为一个辅助接口。 - DTS 描述:HDMI 设备作为
platform device
定义,并且通过ddc-i2c-bus
属性引用 I2C 总线,用于 DDC(Display Data Channel)接口的 I2C 通信。
&hdmi {
ddc-i2c-bus = <&i2c2>;
status = "okay";
};
- 驱动模型:在这种情况下,HDMI 控制器是主要设备,注册为
platform_device
。I2C 通信是 HDMI 控制器的一部分,用来处理 EDID 等信息读取。在驱动程序中,I2C 总线是通过 DTS 中的ddc-i2c-bus
属性传递给 HDMI 驱动的,驱动程序会通过of_find_i2c_adapter_by_node()
获取并绑定 I2C 控制器。
struct i2c_adapter *ddc;
struct device_node *ddc_node;
ddc_node = of_parse_phandle(dev->of_node, "ddc-i2c-bus", 0);
if (!ddc_node) {
dev_err(dev, "failed to find ddc-i2c-bus node\n");
return -ENODEV;
}
ddc = of_find_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!ddc)
return -EPROBE_DEFER;
/* Use ddc for I2C transactions */
I2C 是辅助总线,用于少量数据交互(如 EDID、配置信息传输),主要功能通过其他总线(如 TDMS)完成。
驱动以 platform_device
形式注册,I2C 仅作为其中一个通信接口,I2C 控制器通过设备树中的引用绑定。
2. 驱动编写
根据上面所说的形态去编写设备不同的i2c_driver
形态1: 完全依赖I2C的设备(如PMIC)
- 确定I2C adapter
根据设备硬件连接的实际情况,确定该设备所从属的I2C controller,即I2C adapter(在I2C framework中,I2C controller通常被称为I2C adapter)。例如,PMIC设备可能连接在i2c1
总线上。 - 在DTS中添加设备描述
在设备树的I2C adapter节点中,添加I2C从设备的描述。这个从设备的定义格式与普通的platform device一致,例如:
&i2c1 {
pmic: pfuze100@08 {
compatible = "fsl,pfuze100";
reg = <0x08>;
// 其他字段...
};
};
compatible
字段的使用
DTS 中的compatible
字段用于设备驱动的匹配和probe
。I2C驱动会根据compatible
字段匹配到合适的驱动程序,例如"compatible = "fsl,pfuze100";"
。- 编写I2C设备的驱动程序
示例:
static const struct of_device_id pmic_of_match[] = {
{ .compatible = "fsl,pfuze100", },
{ /* sentinel */ }
};
static struct i2c_driver pmic_driver = {
.driver = {
.name = "pfuze100",
.of_match_table = pmic_of_match,
},
.probe = pmic_probe,
.remove = pmic_remove,
};
static int __init i2c_driver_XXX_init(void)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return i2c_add_driver(&i2c_XXX_driver);
}
module_init(i2c_driver_XXX_init);
-
- 定义并注册
i2c_driver
:编写一个struct i2c_driver
结构体,并使用 i2c_add_driver将其注册到I2C core中。 - 匹配设备:在
i2c_driver
结构体中,定义of_match_table
,使其可以通过compatible
字段进行匹配。 - 实现
probe
回调:编写probe
函数,用于设备初始化。
- 定义并注册
- I2C core处理设备注册
当I2C adapter注册时,I2C framework的核心代码会自动为其下的所有I2C从设备创建struct i2c_client
结构体,并根据设备树中的compatible
字段,匹配合适的i2c_driver
,然后调用驱动程序的probe
函数。 - 在
probe
中
当设备被I2C framework匹配到时,调用此函数进行设备初始化。主要负责获取设备资源(如设备地址、I2C Adapter),设置硬件寄存器,或者初始化其他系统资源(如注册字符设备)。
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
// 打印日志确认 probe 触发
printk(KERN_INFO "ap3216c i2c device probe.\n");
// 设置I2C client指针
ap3216c_client = client;
// 执行设备初始化:如设置初始寄存器值
i2c_smbus_write_byte_data(client, 0x00, 0x03); // 设置某些寄存器
// 注册字符设备或其他资源
major = register_chrdev(0, "ap3216c", &ap3216c_ops);
ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");
device_create(ap3216c_class, NULL, MKDEV(major, 0), NULL, "ap3216c");
return 0;
}
在注册的ap3216c_ops中,有关read和write的函数,使用到I2C数据有关的接口就有两类:
- 一类是以i2c client为参数,进行简单的数据收发。该方法只可以通过标准方式,发送或者接收一定数量的数据。
- 另一类是以i2c adapter和i2c msg为参数,可以更为灵活的read或者write数据,包括i2c_transfer。使用该方法可以以struct i2c_msg为参数,一次读取、或者写入、或者读取加写入,一定数量的数据。
- 具体的函数去看\Linux-4.9.88\include\linux\i2c.h
形态2: 复合型设备(如HDMI)
- 确定I2C adapter
根据设备硬件的连接方式,找到设备使用的I2C adapter。例如,HDMI 设备可以使用i2c2
作为其 DDC 通道。 - 将设备作为平台设备描述
将设备作为平台设备(platform device
)进行描述。其 DTS 节点并不是直接位于I2C adapter下,而是位于根目录。例如:
&hdmi {
ddc-i2c-bus = <&i2c2>;
status = "okay";
};
- 获取I2C adapter的引用
在DTS描述中使用ddc-i2c-bus
属性引用I2C adapter节点。该属性通过of_parse_phandle
来解析,并在驱动中获取相应的I2C adapter的指针。 - 在
probe
函数中获取I2C adapter
在平台设备的probe
函数中,通过of_parse_phandle
来获取I2C adapter节点,并通过of_find_i2c_adapter_by_node()
获取对应的struct i2c_adapter
指针。如下代码示例:
struct i2c_adapter *ddc;
struct device_node *ddc_node;
ddc_node = of_parse_phandle(dev->of_node, "ddc-i2c-bus", 0);
if (!ddc_node) {
dev_err(dev, "failed to find ddc-i2c-bus node\n");
return -ENODEV;
}
ddc = of_find_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!ddc)
return -EPROBE_DEFER;
- 使用
i2c_transfer
进行读写操作
获取I2C adapter之后,便可以使用i2c_transfer()
接口来进行I2C读写操作。例如,通过DDC接口读取EDID信息时:
struct i2c_msg msgs[] = {
{
.addr = ddc_addr,
.flags = I2C_M_RD,
.len = 128,
.buf = edid_buf,
},
};
ret = i2c_transfer(ddc, msgs, 1); // 读取EDID数据
3. i2c_client
i2c设备中用i2c_client结构体来指代:
/**
* struct i2c_client - represent an I2C slave device
* @flags: I2C_CLIENT_TEN indicates the device uses a ten bit chip address;
* I2C_CLIENT_PEC indicates it uses SMBus Packet Error Checking
* @addr: Address used on the I2C bus connected to the parent adapter.
* @name: Indicates the type of the device, usually a chip name that's
* generic enough to hide second-sourcing and compatible revisions.
* @adapter: manages the bus segment hosting this I2C device
* @dev: Driver model device node for the slave.
* @irq: indicates the IRQ generated by this device (if any)
* @detected: member of an i2c_driver.clients list or i2c-core's
* userspace_devices list
* @slave_cb: Callback when I2C slave mode of an adapter is used. The adapter
* calls it to pass on slave events to the slave driver.
*
* An i2c_client identifies a single device (i.e. chip) connected to an
* i2c bus. The behaviour exposed to Linux is defined by the driver
* managing the device.
*/
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct device dev; /* the device structure */
int irq; /* irq issued by device */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
flags
:
-
表示I2C设备的标志位。可以包含以下选项:
-
I2C_CLIENT_TEN
:表示设备使用的是十位地址(而不是常见的七位地址)。I2C_CLIENT_PEC
:表示设备使用SMBus包错误校验(Packet Error Checking,PEC),这是一种错误检测机制。
addr
:
- I2C设备的地址。地址位于I2C总线上的低7位或低10位(如果
flags
指示设备使用十位地址的话)。
name
:
- 一个字符数组,表示设备的名称,通常是设备的芯片名称或类型。名称尽可能保持通用,以适应同一设备的不同版本或兼容的设备。
adapter
:
- 指向设备所在的I2C总线适配器(
i2c_adapter
)的指针。每个I2C设备都挂载在一个I2C适配器上,适配器管理与设备的通信。
dev
:
- Linux驱动模型中的设备节点(
struct device
),用于表示I2C从设备。这是Linux设备模型的标准数据结构,提供了与设备类、驱动程序以及电源管理等相关的接口。
irq
:
- 表示设备所使用的中断号(如果该设备可以生成中断)。不使用中断的设备此字段可能为0或无效值。
detected
:
- 用于链表结构中的节点。在内核中,这个字段用于将该设备连接到
i2c_driver.clients
列表或i2c-core
的userspace_devices
列表中。
slave_cb
:
- 仅在启用了 I2C Slave 模式(即
CONFIG_I2C_SLAVE
选项启用时)时有效。 - 这是用于适配器从模式的回调函数指针。当I2C适配器处于从模式时,适配器调用这个回调函数,将从模式的事件传递给从设备的驱动程序。
i2c_client一般是在register adapter的时候,解析adapter的child node自行创建的,这个在对上一章节中对i2c_add_adapte函数进行分析的时候有提到过