----------------------------------------------------------------------------------------------------------------------------内核版本:linux 5.2.8根文件系统:busybox 1.25.0u-boot:2016.05----------------------------------------------------------------------------------------------------------------------------
在通信协议-I2C小节,我们已经对I2C协议进行了详细的介绍,并在Mini2440裸机开发之I2C(AT24C08)小节中介绍了通过I2C协议去读写AT24C08 EEPROM。在这一节将会学习I2C总线设备驱动模型。
一、I2C驱动框架
1.1 I2C框架
I2C总线设备驱动模型由I2C核心、I2C总线驱动(或者说I2C适配器驱动、I2C控制器驱动)、I2C设备驱动组成。
对于linux系统来说,支持各式各样的SOC,并且还想要支持各种I2C硬件芯片,就必须将一些公共的部分抽离出来,这样就抽象出了:
- i2c client:描述具体的I2C设备,每个i2c_client对应一个实际的I2C设备,比如AT24C08、MPU6050、SSD1306 OLED等;
- /sys/devices/platform/s3c2440-i2c.0/i2c-0/0-<addr>:I2C设备注册时创建;0为I2C适配器编号,addr为4位16进制数字,比如具有0x50地址的设备连接到0号I2C总线,则设备名为0-0050;
- i2c driver:描述一个I2C设备驱动,每个i2c_driver描述一种I2C设备的驱动;
- i2c dev:I2C字符设备;
- /dev/i2c-0:I2C字符设备文件;0为控制器的编号;
- /sys/class/i2c-dev/i2c-0:I2C字符设备所属类名为i2c-dev;
- i2c adapter:描述SOC的一个I2C控制器;
-
- /sys/devices/platform/s3c2440-i2c.0/i2c-0:I2C适配器设备注册时创建;0为I2C适配器编号,如果有多个I2C控制器注册,/sys/devices/platform/s3c2440-i2c.0目录下将会有多个i2c-%d目录;
注意:/sys/devices/platform/s3c2440-i2c.0的subsystem指向了/sys/bus/platform。而/sys/devices/platform/s3c2440-i2c.0/i2c.0的subsystem指向了/sys/bus/i2c。
-
- /sys/class/i2c-adapter:I2C控制器设备类为i2c-adatper,下面存储的都是I2C控制器相关的符号链接;
![](https://img2023.cnblogs.com/blog/1328274/202302/1328274-20230218200358737-1120992550.png)
-
- /sys/bus/i2c/devices/i2c-0:I2C适配器设备注册时创建;0为I2C适配器编号,如果有多个I2C控制器注册,/sys/bus/devices目录下将会有多个i2c-%链接文件;其指向/sys/devices/platform/s3c2440-i2c.0/i2c-0;
- i2c algorithm:I2C通信算法,用于操作实际的I2C控制器,产生 I2C硬件波形;
在一个SOC上可能有多条I2C总线,一条总线对应一个I2C总线驱动,每一条总线上又可以接多个I2C设备。
假如一个SOC上有3个I2C适配器,每个I2C适配器,外接3个I2C设备,想要3个适配器分别都能驱动3个I2C设备,我们只需写3个适配器代码,3个I2C设备代码即可。
注意:关于/sys目录下各个文件的意义可以参考:/sys目录下各个子目录的具体说明。
1.1.1 I2C核心
I2C核心是linux内核用来维护和管理I2C的核心部分。I2C core提供接口函数,允许一个i2c_adapter、i2c_driver和i2c_client初始化时在I2C core中注册,以及退出时进行卸载,同时还提供了I2C总线读写访问的接口。
1.1.2 I2C总线驱动
I2C总线驱动包含了I2C适配器数据结构i2c_adapter、I2C通信算法i2c_algorithm和控制I2C适配器产生通信信号的函数。
经由I2C总线驱动的代码,我们可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。
1.1.3 I2C设备驱动
I2C设备驱动主要包含了数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数。
1.2 目录结构
linux内核将I2C驱动相关的代码放在drivers/i2c目录下,这下面的文件还是比较多的,我们大概了解一下即可。
![](https://img2023.cnblogs.com/blog/1328274/202302/1328274-20230217200206631-738833722.png)
1.2.1 algos
保存I2C通信方面的算法。
1.2.2 busses
保存SoC厂家提供的I2C控制器驱动相关的文件,比如i2c-stm32.c、i2c-stm32f4.c、i2c-stm32f7.c、 i2c-versatile.c、 i2c-s3c2410.c等。
1.2.3 muxes
实现I2C mux驱动相关的文件,比如i2c-mux-pca954x.c。
mux包含如下功能:
- switch:在配置为 1:N 时,可以为每个子通道选择和设计。换句话说,它可以选择一个或多个;
- muliplexor:在配置为 1:N 或 N:1 时,可以选择和设计一个子通道。选择时,一次只能有一个通道。
1.2.4 i2c-core-base.c
这个文件实现了I2C核心的功能,I2C总线的初始化、注册和适配器添加和注销等相关工作。
1.2.5 i2c-dev.c
实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配为一个设备。通过适配器访问设备的主设备号都为89,次设备号为0~255。
应用程序通过“i2c-%d”(i2c-0、i2c-1、i2c-2...)文件名并使用文件操作结构open()、write()、read()、ioctl()和close()等来访问这个设备。
i2c-dev.c并没有针对特定的设备而设计,只是提供了通用read()等接口,应用程序可以借用这些接口访问适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。
二、I2C总线注册
在linux驱动移植-platform总线设备驱动中我们已经介绍了platfrom总线的注册流程,I2C总线类型注册和platfrom总线注册类似。
2.1 I2C总线类型定义
在linux 设备模型中,总线类型由bus_type 结构表示,我们所用的 I2C、SPI、USB 都是用这个结构体来定义的。该结构体定义在 include/linux/device.h文件中:
/**
* struct bus_type - The bus type of the device
*
* @name: The name of the bus.
* @dev_name: Used for subsystems to enumerate devices like ("foo%u", dev->id).
* @dev_root: Default device to use as the parent.
* @bus_groups: Default attributes of the bus.
* @dev_groups: Default attributes of the devices on the bus.
* @drv_groups: Default attributes of the device drivers on the bus.
* @match: Called, perhaps multiple times, whenever a new device or driver
* is added for this bus. It should return a positive value if the
* given device can be handled by the given driver and zero
* otherwise. It may also return error code if determining that
* the driver supports the device is not possible. In case of
* -EPROBE_DEFER it will queue the device for deferred probing.
* @uevent: Called when a device is added, removed, or a few other things
* that generate uevents to add the environment variables.
* @probe: Called when a new device or driver add to this bus, and callback
* the specific driver's probe to initial the matched device.
* @remove: Called when a device removed from this bus.
* @shutdown: Called at shut-down time to quiesce the device.
*
* @online: Called to put the device back online (after offlining it).
* @offline: Called to put the device offline for hot-removal. May fail.
*
* @suspend: Called when a device on this bus wants to go to sleep mode.
* @resume: Called to bring a device on this bus out of sleep mode.
* @num_vf: Called to find out how many virtual functions a device on this
* bus supports.
* @dma_configure: Called to setup DMA configuration on a device on
* this bus.
* @pm: Power management operations of this bus, callback the specific
* device driver's pm-ops.
* @iommu_ops: IOMMU specific operations for this bus, used to attach IOMMU
* driver implementations to a bus and allow the driver to do
* bus-specific setup
* @p: The private data of the driver core, only the driver core can
* touch this.
* @lock_key: Lock class key for use by the lock validator
* @need_parent_lock: When probing or removing a device on this bus, the
* device core should lock the device's parent.
*
* A bus is a channel between the processor and one or more devices. For the
* purposes of the device model, all devices are connected via a bus, even if
* it is an internal, virtual, "platform" bus. Buses can plug into each other.
* A USB controller is usually a PCI device, for example. The device model
* represents the actual connections between buses and the devices they control.
* A bus is represented by the bus_type structure. It contains the name, the
* default attributes, the bus' methods, PM operations, and the driver core's
* private data.
*/
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*num_vf)(struct device *dev);
int (*dma_configure)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
bool need_parent_lock;
};
其中部分字段的含义如下:
- name:总线名称;
- bus_groups:总线属性;
- dev_groups:该总线上所有设备的默认属性;
- drv_groups:该总线上所有驱动的默认属性;
- match:当有新的设备或驱动添加到总线上时match函数被调用,如果设备和驱动可以匹配,返回0;
- uevent:当一个设备添加、移除或添加环境变量时,函数调用;
- probe:当有新设备或驱动添加时,probe函数调用,并且回调该驱动的probe函数来初始化相关联的设备;
- remove:设备移除时调用remove函数;
- shutdown:设备关机时调用shutdown函数;
- suspend:设备进入睡眠时调用suspend函数;
- resume:设备唤醒时调用resume函数;
- pm:总线的电源管理选项,并回调设备驱动的电源管理模块;
- p: 驱动核心的私有数据,只有驱动核心才可以访问。使用struct subsys_private可以将struct bus_type中的部分细节屏蔽掉,利于外界使用bus_type;struct driver_private和struct device_private都有类似的功能。
i2c_bus_type是 bus_type 类型的全局变量,这个变量已经被 linux 内核赋值好了,其结构体成员对应的函数也已经在内核里面写好,定义在drivers/i2c/i2c-core-base.c:
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
这里我们重点关注I2C匹配函数i2c_device_match即可。
2.2 I2C设备和驱动匹配
i2c_bus_type 中的 i2c_device_match就是我们常说的做驱动和设备匹配的函数,不同的总线对应的match函数肯定不一样,这个我们不用管,内核都会写好。我们所用的 I2C 总线对应的 match 函数是 i2c_device_match函数,该函数定义在drivers/i2c/i2c-core-base.c:
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
/* Attempt an OF style match */
if (i2c_of_match_device(drv->of_match_table, client))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv);
/* Finally an I2C match */
if (i2c_match_id(driver->id_table, client))
return 1;
return 0;
}
该函数有两个参数:设备和设备驱动,该函数主要做了一下事情:
- 将设备转为I2C从设备类型;
- 将驱动转为I2C驱动类型;
- 调用i2c_of_driver_match_device进行设备树OF类型匹配;
- 调用acpi_driver_match_device进行ACPI类型匹配;
- 如果设置值了pdrv->id_table,进行id_table匹配;
通过对上面匹配函数的一个简单分析,我们知道匹配函数做匹配的顺序是先匹配设备树,然后匹配id_table表:
const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id,
const struct i2c_client *client)
{
if (!(id && client))
return NULL;
while (id->name[0]) {
if (strcmp(client->name, id->name) == 0)
return id;
id++;
}
return NULL;
}
对于支持设备树的 Linux 版本,我们一上来做设备树匹配就完事,不支持设备树时,我们就得定义I2C设备,再用id_tabale表匹配。
2.3 I2C总线注册
I2C子系统的初始化是由i2c_init函数完成的。I2C总线以模块的方式注册到内核。i2c_init定义在drivers/i2c/i2c-core-base.c文件中:
static int __init i2c_init(void)
{
int retval;
retval = of_alias_get_highest_id("i2c");
down_write(&__i2c_board_lock);
if (retval >= __i2c_first_dynamic_bus_num)
__i2c_first_dynamic_bus_num = retval + 1;
up_write(&__i2c_board_lock);
retval = bus_register(&i2c_bus_type);
if (retval)
return retval;
is_registered = true;
#ifdef CONFIG_I2C_COMPAT
i2c_adapter_compat_class = class_compat_register("i2c-adapter");
if (!i2c_adapter_compat_class) {
retval = -ENOMEM;
goto bus_err;
}
#endif
retval = i2c_add_driver(&dummy_driver);
if (retval)
goto class_err;
if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier));
if (IS_ENABLED(CONFIG_ACPI))
WARN_ON(acpi_reconfig_notifier_register(&i2c_acpi_notifier));
return 0;
class_err:
#ifdef CONFIG_I2C_COMPAT
class_compat_unregister(i2c_adapter_compat_class);
bus_err:
#endif
is_registered = false;
bus_unregister(&i2c_bus_type);
return retval;
}
我们重点关注bus_register总线注册函数,传入的参数就是我们上面介绍的i2c_bus_type。
bus_register函数调用后,就会在用户空间/sys/bus目录下生成具有总线名称的目录,同时在该目录下创建devices、drivers文件夹,创建uevent、drivers_autoprobe、drivers_probe等文件。
执行如下命令:
![](https://img2023.cnblogs.com/blog/1328274/202302/1328274-20230218232110664-1725172164.png)
/sys/bus/i2c/devices里用来存放的是I2C设备链接,/sys/bus/i2c/drivers里用来存放的是I2C驱动链接。需要注意的是在i2c_bus_type总线链表里面存放的除了I2C从设备,还有I2C适配器。
从其他博文找到一张总线创建的说明图,下图是foo总线创建的过程:
三、I2C核心数据结构
学习I2C驱动,首先要了解驱动框架涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
3.1 struct i2c_adapter
struct i2c_adapter抽象了控制器硬件,在SOC中的指的就是内部I2C控制器,当向I2C核心层注册一个I2C适配器时就需要提供这样的一个结构体变量。它的定义在 include/linux/i2c.h 文件,如下:
/*
* i2c_adapter is the structure used to identify a physical i2c bus along
* with the access algorithms necessary to access it.
*/
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
void *algo_data;
/* data fields that are valid for all devices */
const struct i2c_lock_operations *lock_ops;
struct rt_mutex bus_lock;
struct rt_mutex mux_lock;
int timeout; /* in jiffies */
int retries;
struct device dev; /* the adapter device */
unsigned long locked_flags; /* owned by the I2C core */
#define I2C_ALF_IS_SUSPENDED 0
#define I2C_ALF_SUSPEND_REPORTED 1
int nr;
char name[48];
struct completion dev_released;
struct mutex userspace_clients_lock;
struct list_head userspace_clients;
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
struct irq_domain *host_notify_domain;
};
其中部分参数含义如下:
- algo:适配器与从设备的通信算法,即访问I2C总线的算法;
- class:适配器支持的从设备的类型;
- I2C_CLASS_HWMON:硬件监控类;
- I2C_CLASS_DDC:数字显示通道,通常用于显示设备;
- I2C_CLASS_SPD:内存设备;
- I2C_CLASS_DEPRECATED:已废弃;不在使用的class;如果设置为这个,表示该I2C控制将不支持自动检测I2C从设备,即不再支持i2c_detect(这个后面会介绍);
- ...
- bus_lock:控制并发访问的互斥锁;
- timeout:超时时间;为I2C控制器发送一次消息所需的最长时间,包括重传次数所消耗的总时间,如果在timeout时间内没有完成发送,则表示这次消息发送失败;
- reties:重试次数;比如I2C控制器在传输过程中由于仲裁丢失、总线忙等情况而发送失败,则会根据retries的值来规定最大的重传次数;
- dev:设备驱动模型中的设备,可以把i2c_adapter看做是device的子类;
- bus为i2c_bus_type;
- type为i2c_adapter_type :指定了适配器的类型,该类型中也定义了专有的属性文件;
- kobj.name(设备名称)为i2c-%d:指定在/sys/devices/platform/s3c2440-i2c.0目录下生成的适配器目录名;
- nr:适配器的ID号;即I2C控制器的ID号,用来区分是属于SOC的第几个I2C控制器;
- name:适配器名称;即I2C控制器的名字;
- dev_released:用来实现同步,实现的机制为complete,作用跟信号量一样,只不过该信号解决了信号量在多进程操作中的一些缺点,在I2C控制器销毁过程中执行;
- userspace_client:client链表头;专门存储i2c_client类型的设备,这些设备也就是像EEPROM、MPU6050这类的I2C设备。在设备树中定义了很多 IIC 设备,将来都会被转换成 i2c_client 类型的结构体,但是userspace_clients并不链接这些 I2C 设备。userspace_clients链接的是我们在应用层通过访问 I2C 控制器的属性文件创建的 I2C 设备;
- userspace_clients_lock:就是在向链表添加过程中使用的互斥锁;
3.1.1 struct i2c_algorithm
algo是struct i2c_adatper中的核心成员,它包含了操作SOC I2C 控制器的函数集,也就是直接对接到实际的 SOC 的 I2C 控制器的操作(寄存器配置)。
algo类型为struct i2c_algorithm,定义在 include/linux/i2c.h 文件,如下:
/**
* struct i2c_algorithm - represent I2C transfer method
* @master_xfer: Issue a set of i2c transactions to the given I2C adapter
* defined by the msgs array, with num messages available to transfer via
* the adapter specified by adap.
* @master_xfer_atomic: same as @master_xfer. Yet, only using atomic context
* so e.g. PMICs can be accessed very late before shutdown. Optional.
* @smbus_xfer: Issue smbus transactions to the given I2C adapter. If this
* is not present, then the bus layer will try and convert the SMBus calls
* into I2C transfers instead.
* @smbus_xfer_atomic: same as @smbus_xfer. Yet, only using atomic context
* so e.g. PMICs can be accessed very late before shutdown. Optional.
* @functionality: Return the flags that this algorithm/adapter pair supports
* from the I2C_FUNC_* flags.
* @reg_slave: Register given client to I2C slave mode of this adapter
* @unreg_slave: Unregister given client from I2C slave mode of this adapter
*
* The following structs are for those who like to implement new bus drivers:
* i2c_algorithm is the interface to a class of hardware solutions which can
* be addressed using the same bus algorithms - i.e. bit-banging or the PCF8584
* to name two of the most common.
*
* The return codes from the @master_xfer{_atomic} fields should indicate the
* type of error code that occurred during the transfer, as documented in the
* Kernel Documentation file Documentation/i2c/fault-codes.
*/
struct i2c_algorithm {
/*
* If an adapter algorithm can't do I2C-level access, set master_xfer
* to NULL. If an adapter algorithm can do SMBus access, set
* smbus_xfer. If set to NULL, the SMBus protocol is simulated
* using common I2C messages.
*
* master_xfer should return the number of messages successfully
* processed, or a negative value on error
*/
int (*master_xfer)(struct i2c_adapter *adap, struct