1. 通用驱动i2c-dev分析
1.1 字符设备驱动程序
怎么编写字符设备驱动程序?
-
确定主设备号
-
创建file_operations结构体
-
在里面填充drv_open/drv_read/drv_ioctl等函数
-
-
注册file_operations结构体
-
register_chrdev(major, &fops, name)
-
-
谁调用register_chrdev?在入口函数调用
-
有入口自然就有出口
-
在出口函数unregister_chrdev
-
-
辅助函数(帮助系统自动创建设备节点)
-
class_create
-
device_create
-
1.2 i2c-dev.c注册过程分析
1.2.1 register_chrdev的内部实现
1.2.2 i2c-dev驱动的注册过程
1.3 file_operations函数分析
i2c-dev.c的核心:
static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = i2cdev_read,
.write = i2cdev_write,
.unlocked_ioctl = i2cdev_ioctl,
.compat_ioctl = compat_i2cdev_ioctl,
.open = i2cdev_open,
.release = i2cdev_release,
};
这段代码是Linux内核驱动程序中定义文件操作结构体的一个例子,通常用于实现设备驱动的文件接口。以下是对每个成员的解释:
-
.owner: 指定文件操作结构体所属的模块。这里使用
THIS_MODULE
宏来表示当前模块。 -
.llseek: 指向一个函数,用于处理文件的逻辑到物理偏移量转换。这里使用
no_llseek
表示不支持常规的寻址方式,可能是设备特定的寻址方式。 -
.read: 指向一个函数,用于从文件中读取数据。这里使用
i2cdev_read
函数来实现从I2C设备读取数据。 -
.write: 指向一个函数,用于向文件写入数据。这里使用
i2cdev_write
函数来实现向I2C设备写入数据。 -
.unlocked_ioctl: 指向一个函数,用于处理设备控制命令。这个函数不需要文件加锁,通常用于处理设备特定的控制命令。这里使用
i2cdev_ioctl
函数。 -
.compat_ioctl: 用于处理32位系统上的64位ioctl调用。在64位系统上,这个函数通常被忽略。这里使用
compat_i2cdev_ioctl
函数。 -
.open: 指向一个函数,用于在文件被打开时执行初始化操作。这里使用
i2cdev_open
函数。 -
.release: 指向一个函数,用于在文件被释放时执行清理操作。这里使用
i2cdev_release
函数。
函数说明
- no_llseek: 通常是一个空操作的函数,表示不支持标准的文件寻址。
- i2cdev_read: 实现从I2C设备读取数据的逻辑。
- i2cdev_write: 实现向I2C设备写入数据的逻辑。
- i2cdev_ioctl: 实现处理I2C设备控制命令的逻辑。
- compat_i2cdev_ioctl: 实现处理32位系统上的64位ioctl调用的逻辑。
- i2cdev_open: 实现在文件被打开时的初始化逻辑。
- i2cdev_release: 实现在文件被释放时的清理逻辑。
这些函数需要在驱动程序的其他部分实现,具体的实现会根据设备的特性和需求进行编写。通过这种方式,驱动程序可以提供一个标准的文件接口,使得应用程序可以通过标准的文件操作来访问和控制硬件设备。
主要的系统调用:open, ioctl:
要理解这些接口,记住一句话:APP通过I2C Controller与I2C Device传输数据。
1.3.1 i2cdev_open
1.3.2 i2cdev_ioctl: I2C_SLAVE/I2C_SLAVE_FORCE
1.3.3 i2cdev_ioctl: I2C_RDWR
13.4 i2cdev_ioctl: I2C_SMBUS
1.3.5 总结
2. I2C总线-设备-驱动模型
I2C Core就是I2C核心层,它的作用:
-
提供统一的访问函数,比如i2c_transfer、i2c_smbus_xfer等
-
实现
I2C总线-设备-驱动模型
,管理:I2C设备(i2c_client)、I2C设备驱动(i2c_driver)、I2C控制器(i2c_adapter)
在Linux内核中,i2c_client
和i2c_driver
是I2C设备驱动程序中两个密切相关的概念,它们共同工作以实现对I2C设备的管理。以下是它们之间的关系和各自的角色:
-
i2c_driver:
i2c_driver
是一个结构体,定义了驱动程序的属性和操作函数。- 它包含了一个指向
i2c_client
结构体数组的指针,这些数组定义了可以由该驱动程序支持的设备列表。 i2c_driver
结构体中定义的操作函数包括:probe
:当设备被发现并被内核识别时调用,用于初始化设备。remove
:当设备被移除时调用,用于清理资源。shutdown
:在系统关闭时调用,用于执行设备关闭前的清理工作。- 其他可选的回调函数,如
suspend
、resume
等,用于处理设备的电源管理和状态转换。
-
i2c_client:
i2c_client
是一个结构体,代表一个I2C设备客户端,即I2C总线上的一个从设备。- 它包含了设备的地址、设备ID、与设备通信所需的操作函数等信息。
i2c_client
结构体中定义的操作函数包括:command
:用于执行设备特定的命令。read
、write
:用于从设备读取数据和向设备写入数据。ioctl
:用于处理设备控制命令。
-
关系:
- 一个
i2c_driver
可以管理多个i2c_client
。驱动程序通过其probe
函数为每个设备创建一个i2c_client
实例,并将其添加到内核的设备列表中。 i2c_driver
结构体中的id_table
字段是一个i2c_device_id
数组,列出了该驱动程序能够支持的所有设备ID。当内核检测到一个I2C设备时,它会查找所有注册的驱动程序,并尝试匹配设备ID,找到合适的驱动程序来处理该设备。- 一旦驱动程序被匹配并加载,其
probe
函数会被调用,通常在这个函数中,驱动程序会初始化i2c_client
结构体,设置设备的操作函数,并将其注册到内核中。
- 一个
-
设备注册和通信:
- 驱动程序通过调用
i2c_attach_client
或i2c_new_client
函数将i2c_client
注册到内核,这样内核就可以识别并管理这个设备。 - 驱动程序使用
i2c_client
结构体中的操作函数与设备进行通信,如发送和接收数据,执行设备特定的命令。
- 驱动程序通过调用
通过这种设计,Linux内核可以灵活地管理和扩展对各种I2C设备的支持,同时为设备驱动程序提供了一致的接口和框架
2.1 i2c_driver
i2c_driver表明能支持哪些设备:
-
使用of_match_table来判断
-
设备树中,某个I2C控制器节点下可以创建I2C设备的节点
-
如果I2C设备节点的compatible属性跟of_match_table的某项兼容,则匹配成功
-
-
i2c_client.name跟某个of_match_table[i].compatible值相同,则匹配成功
-
-
使用id_table来判断
-
i2c_client.name跟某个id_table[i].name值相同,则匹配成功
-
i2c_driver跟i2c_client匹配成功后,就调用i2c_driver.probe函数。
2.2 i2c_client
i2c_client表示一个I2C设备,创建i2c_client的方法有4种:
-
方法1
-
通过I2C bus number来通过设备树来创建
-
方法2 有时候无法知道该设备挂载哪个I2C bus下,无法知道它对应的I2C bus number。 但是可以通过其他方法知道对应的i2c_adapter结构体。 可以使用下面两个函数来创建i2c_client:
i2c_new_device
i2c_new_probed_device
差别:
-
i2c_new_device:会创建i2c_client,即使该设备并不存在
-
i2c_new_probed_device:
-
它成功的话,会创建i2c_client,并且表示这个设备肯定存在
-
I2C设备的地址可能发生变化,比如AT24C02的引脚A2A1A0电平不一样时,设备地址就不一样
-
可以罗列出可能的地址
-
i2c_new_probed_device使用这些地址判断设备是否存在
-
-
方法3(不推荐):由i2c_driver.detect函数来判断是否有对应的I2C设备并生成i2c_client
-
方法4:通过用户空间(user-space)生成 调试时、或者不方便通过代码明确地生成i2c_client时,可以通过用户空间来生成。
3. 编写设备驱动之i2c_driver
分配、设置、注册一个i2c_driver结构体,类似drivers/eeprom/at24.c
:
在probe_new函数中,分配、设置、注册file_operations结构体。 在file_operations的函数中,使用i2c_transfer等函数发起I2C传输。
3.1 编写i2c_driver框架
以下这段代码分析展示了如何编写一个简单的I2C设备驱动程序。它包括了模块的初始化和退出函数、I2C设备的探测和移除函数,以及相关的设备ID和设备树匹配信息。通过这些函数和结构体,内核能够识别和控制连接到I2C总线上的设备。
驱动程序所依赖的一些头文件
定义设备树匹配表,定义I2C设备ID表,定义I2C设备驱动的探测函数
定义I2C设备驱动的移除函数,定义模块初始化函数
定义模块初始化函数,定义模块退出函数
3.2 编写i2c_client框架
AP3216C是红外、光强、距离三合一的传感器,以读出光强、距离值为例,步骤如下:
-
复位:往寄存器0写入0x4
-
使能:往寄存器0写入0x3
-
读红外:读寄存器0xA、0xB得到2字节的红外数据
-
读光强:读寄存器0xC、0xD得到2字节的光强
-
读距离:读寄存器0xE、0xF得到2字节的距离值
AP3216C的设备地址是0x1E。
这段代码实现了一个简单的I2C设备驱动程序,包括了字符设备的注册和注销,以及I2C通信的基本操作。ap3216c_read
函数实现了从I2C设备读取数据并将其复制到用户空间的功能。ap3216c_open
函数在设备文件被打开时执行一些初始化操作。ap3216c_probe
和ap3216c_remove
函数分别在设备被探测到时和设备被移除时调用。最后,i2c_driver_ap3216c_init
和i2c_driver_ap3216c_exit
函数分别用于模块的初始化和退出。
3.3 多种方法生成i2c_client并测试
在用户态生成
编写代码
-
i2c_new_device
-
i2c_new_probed_device
-
i2c_register_board_info
-
内核没有
EXPORT_SYMBOL(i2c_register_board_info)
-
使用这个函数的驱动必须编进内核里去
-
使用设备树生成
在某个I2C控制器的节点下,添加如下代码:
-
修改
arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts
,添加如下代码:
注意:设备树里i2c1就是I2C BUS0。
确定设备树分区挂载在哪里
由于版本变化,STM32MP157单板上烧录的系统可能有细微差别。 在开发板上执行cat /proc/mounts
后,可以得到两种结果(见下图):
-
mmcblk2p2分区挂载在/boot目录下(下图左边):无需特殊操作,下面把文件复制到/boot目录即可
-
mmcblk2p2挂载在/mnt目录下(下图右边)
-
在视频里、后面文档里,都是更新/boot目录下的文件,所以要先执行以下命令重新挂载:
-
mount /dev/mmcblk2p2 /boot
-
-
上机测试
以下命令在开发板中执行。
4. I2C_Adapter驱动框架讲解与编写
在Linux内核中,特别是在处理I2C(Inter-Integrated Circuit)总线时,i2c_adapter
是一个核心的概念,用于抽象和表示I2C总线的物理适配器。以下是i2c_adapter
的主要特点和作用:
-
总线表示:每个I2C总线都有一个对应的
i2c_adapter
实例,它代表了系统中的一个I2C总线。 -
设备管理:
i2c_adapter
负责管理挂载在该总线上的所有I2C设备(即i2c_client)。它维护了一个设备列表,并负责设备的探测和移除。 -
通信控制:
i2c_adapter
提供了一组标准的操作来控制I2C通信,例如发送和接收数据,生成开始和停止条件等。 -
时钟和地址:它管理I2C总线的时钟速率(如标准模式100kHz,快速模式400kHz等)和设备地址。
-
驱动程序注册:I2C设备驱动程序通过
i2c_adapter
注册自己,以便它们可以被内核识别和加载到对应的I2C设备上。 -
资源管理:
i2c_adapter
还负责一些底层资源的管理,如中断线、DMA通道等。 -
探测机制:Linux内核通过
i2c_adapter
实现自动探测机制,可以在系统启动时自动发现和配置I2C总线上的设备。 -
错误处理:
i2c_adapter
提供了错误报告机制,当I2c总线上的通信出现问题时,相关错误会被上报。 -
电源管理:现代的
i2c_adapter
实现可能包括电源管理功能,允许系统在不使用时关闭I2C总线以节省能源。 -
设备树集成:在支持设备树(Device Tree)的系统中,
i2c_adapter
可以与设备树结合使用,以确定如何初始化和配置I2C总线。
4.1 I2C_Adapter驱动框架
4.1.1 核心的结构体
1. i2c_adapter
2. i2c_algorithm
-
master_xfer:这是最重要的函数,它实现了一般的I2C传输,用来传输一个或多个i2c_msg
-
master_xfer_atomic:
-
可选的函数,功能跟master_xfer一样,在
atomic context
环境下使用 -
比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
-
-
smbus_xfer:实现SMBus传输,如果不提供这个函数,SMBus传输会使用master_xfer来模拟
-
smbus_xfer_atomic:
-
可选的函数,功能跟smbus_xfer一样,在
atomic context
环境下使用 -
比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
-
-
functionality:返回所支持的flags:各类I2C_FUNC_*
-
reg_slave/unreg_slave:
-
有些I2C Adapter也可工作与Slave模式,用来实现或模拟一个I2C设备
-
-
reg_slave就是让把一个i2c_client注册到I2C Adapter,换句话说就是让这个I2C Adapter模拟该i2c_client
-
unreg_slave:反注册
-
4.1.2 驱动程序框架
分配、设置、注册一个i2c_adpater结构体:
-
i2c_adpater的核心是i2c_algorithm
-
i2c_algorithm的核心是master_xfer函数
所涉及的函数:
i2c_algorithm示例
Linux-5.4中使用GPIO模拟I2C
Linux-5.4中STM32F157的I2C驱动
4.2 编写一个框架程序
设备树
在设备树里构造I2C Bus节点:
platform_driver
分配、设置、注册platform_driver结构体。
核心是probe函数,它要做这几件事:
-
根据设备树信息设置硬件(引脚、时钟等)
-
分配、设置、注册i2c_apdater
i2c_apdater
i2c_apdater核心是master_xfer函数,它的实现取决于硬件,大概代码如下:
static int xxx_master_xfer(struct i2c_adapter *adapter,
struct i2c_msg *msgs, int num)
{
for (i = 0; i < num; i++) {
struct i2c_msg *msg = msgs[i];
{
// 1. 发出S信号: 设置寄存器发出S信号
CTLREG = S;
// 2. 根据Flag发出设备地址和R/W位: 把这8位数据写入某个DATAREG即可发出信号
// 判断是否有ACK
if (!ACK)
return ERROR;
else {
// 3. read / write
if (read) {
STATUS = XXX; // 这决定读到一个数据后是否发出ACK给对方
val = DATAREG; // 这会发起I2C读操作
} else if(write) {
DATAREG = val; // 这会发起I2C写操作
val = STATUS; // 判断是否收到ACK
if (!ACK)
return ERROR;
}
}
// 4. 发出P信号
CTLREG = P;
}
}
return i;
}
4.3完善虚拟的I2C_Adapter驱动并模拟EEPROM
4.3.1实现master_xfer函数
在虚拟的I2C_Adapter驱动程序里,只要实现了其中的master_xfer函数,这个I2C Adapter就可以使用了。 在master_xfer函数里,我们模拟一个EEPROM,思路如下:
-
分配一个512自己的buffer,表示EEPROM
-
对于slave address为0x50的i2c_msg,解析并处理
-
对于写:把i2c_msg的数据写入buffer
-
对于读:从buffer中把数据写入i2c_msg
-
-
对于slave address为其他值的i2c_msg,返回错误
4.3.2 具体代码分析
这段代码是一个简单的虚拟I2C总线主设备(也称为软件I2C或模拟I2C)的驱动程序示例,它使用平台设备(platform_device)进行注册。下面是对代码的逐行分析和注释:
这个驱动程序创建了一个虚拟的I2C总线适配器,它不与任何实际的硬件I2C总线交互,而是在内存中模拟一个EEPROM设备的行为。它使用平台驱动程序注册机制来初始化和清理资源。i2c_bus_virtual_algo
结构体定义了这个虚拟适配器的算法,包括主传输函数和功能支持函数。i2c_bus_virtual_probe
函数负责分配和注册I2C适配器,而i2c_bus_virtual_remove
函数负责注销和释放资源。最后,模块的初始化和退出函数使用module_init
和module_exit
宏来注册。
平台设备(Platform Device)是Linux内核中处理特定硬件设备的一种机制,特别是在处理嵌入式系统或SoC(System on Chip)环境中的硬件时非常有用。与传统的设备(如PCI设备)相比,平台设备具有以下优点:
-
简化驱动程序编写:
- 平台设备驱动程序不需要处理硬件发现或资源分配。设备树或ACPI表等机制已经定义了设备的资源和配置,驱动程序只需直接使用这些信息。
-
设备树集成:
- 在使用设备树的系统中,平台设备可以很容易地与设备树结合使用。设备树提供了一种灵活的方式来描述硬件配置,使得驱动程序能够轻松访问设备属性。
-
支持固定硬件:
- 平台设备非常适合固定硬件,如嵌入式系统中的外设。这些设备在系统运行期间不会改变,因此不需要动态探测或热插拔支持。
-
减少资源冲突:
- 由于平台设备在系统启动时就已经配置好了资源,因此减少了运行时资源冲突的可能性。这有助于提高系统的稳定性。
-
电源管理:
- 平台设备可以更容易地实现电源管理,因为设备的电源状态可以由设备树或ACPI表控制,驱动程序可以根据这些信息来管理设备的电源状态。
-
支持多实例设备:
- 平台设备驱动程序可以很容易地支持多个设备实例。每个实例都可以在设备树中单独定义,驱动程序可以遍历这些实例并为每个实例创建设备节点。
-
减少内核空间占用:
- 由于平台设备的资源在编译时就已经确定,因此可以减少内核在运行时需要处理的动态内存分配和资源管理的开销。
-
提高启动速度:
- 平台设备的初始化和配置在系统启动时就已经完成,这有助于减少启动时间,特别是在资源有限的嵌入式系统中。
-
更好的错误处理:
- 平台设备驱动程序可以在系统启动时进行更严格的错误检查,因为设备的资源和配置在编译时就已经确定,这有助于在系统启动前发现潜在的问题。
-
支持自定义硬件:
- 对于自定义硬件或特殊应用,平台设备提供了一种灵活的方式来集成和控制硬件设备,使得开发者可以更精细地控制硬件的行为。
总的来说,平台设备为嵌入式系统和固定硬件提供了一种高效、灵活且易于管理的方式来实现设备驱动程序。这使得Linux内核能够更好地适应各种硬件环境,提高系统的可扩展性和可靠性。
4.3.3 上机实验的步骤:
1.设置交叉编译工具链
2.编译替换设备树
3.开发板上挂载NFS文件系统
4. 编译、安装驱动程序
5. 使用i2c-tools测试
5. 使用GPIO模拟I2C的驱动程序分析
5.1 使用GPIO模拟I2C的要点
-
引脚设为GPIO
-
GPIO设为输出、开极/开漏(open collector/open drain)
-
要有上拉电阻
平台总线设备驱动模型
设备树
5.2
5.2 GPIO 驱动程序分析
I2C-GPIO驱动层次
传输函数分析
5.3. 怎么使用I2C-GPIO
设置设备数,在里面添加一个节点即可,示例代码看上面:
-
compatible = "i2c-gpio";
-
使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极
-
可选
-
-
指定SDA、SCL所用的GPIO
-
指定频率(2种方法):
-
i2c-gpio,delay-us = <5>; /* ~100 kHz */
-
clock-frequency = <400000>;
-
-
#address-cells = <1>;
-
#size-cells = <0>;
-
i2c-gpio,sda-open-drain:
-
它表示其他驱动、其他系统已经把SDA设置为open drain了
-
在驱动里不需要在设置为open drain
-
如果需要驱动代码自己去设置SDA为open drain,就不要提供这个属性
-
-
i2c-gpio,scl-open-drain:
-
它表示其他驱动、其他系统已经把SCL设置为open drain了
-
在驱动里不需要在设置为open drain
-
如果需要驱动代码自己去设置SCL为open drain,就不要提供这个属性
-
5.4 使用GPIO操作I2C设备_IMX6ULL
原理图:
设备树:
i2c_gpio_100ask {
compatible = "i2c-gpio";
gpios = <&gpio4 20 0 /* sda */
&gpio4 21 0 /* scl */
>;
i2c-gpio,delay-us = <5>; /* ~100 kHz */
#address-cells = <1>;
#size-cells = <0>;
};
把上述代码,放入arch/arm/boot/dts/100ask_imx6ull-14x14.dts
的根节点下面。
上机实验步骤:
设置工具链
编译、替换设备树
编译I2C-GPIO驱动
6. 具体芯片的I2C_Adapter驱动分析
6.1 I2C控制器内部结构
-
使能时钟、设置时钟
-
发送数据:
-
把数据写入tx_register,等待中断发生
-
中断发生后,判断状态:是否发生错误、是否得到回应信号(ACK)
-
把下一个数据写入tx_register,等待中断:如此循环
-
-
接收数据:
-
设置controller_register,进入接收模式,启动接收,等待中断发生
-
中断发生后,判断状态,读取rx_register得到数据
-
如此循环
-
6.2 驱动程序分析
读I2C数据时,要先发出设备地址,这是写操作,然后再发起读操作,涉及写、读操作。所以以读I2C数据为例讲解核心代码。
STM32MP157:函数stm32f7_i2c_xfer
分析 这函数完全有驱动程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。
通过中断进行后续传输
标签:分析,i2c,函数,client,驱动程序,I2C,设备 From: https://blog.csdn.net/xace007/article/details/140610999