18 Linux I2C Adapter Driver
在 Linux 系统中,I2C(Inter-Integrated Circuit)是一种广泛使用的通信协议,允许多个设备在同一条数据线上进行通信。I2C 适配器驱动是与 I2C 总线相关的核心组件,它连接了内核与 I2C 设备,负责数据的读写操作。
由17章中所描述的inux kernel I2C框架, 我们知道 I2C 适配器驱动、I2C 设备驱动和用户空间应用程序之间有一个清晰的层次结构:
I2C 适配器驱动:负责与底层硬件交互,管理 I2C 总线的通信。
I2C 设备驱动:注册在适配器上的设备,每个设备都有一个对应的驱动。
用户空间应用程序:通过系统调用与设备驱动进行交互,进行数据读取和写入。
18.1 I2C 适配器驱动的作用
桥梁:I2C 适配器驱动就像一座桥,将用户空间的请求转化为底层硬件可以理解的信号。
管理总线:适配器负责管理 I2C 总线的访问和控制,从而确保数据的准确性和稳定性。
执行操作:它实现了读写操作的具体细节,使得上层驱动和用户应用不需要关心底层硬件的复杂性。
数据传递过程
1.) 用户空间到内核空间
2.) 内核空间的处理
I2C 适配器驱动
收到请求后,会调用适配器的 i2c_transfer() 函数, 定义在 drivers/i2c/i2c-core.c 文件中。该函数负责将 I2C 消息传递给注册的 I2C 适配器,并执行实际的数据传输。
int i2c_transfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num) {
// 实现 I2C 消息的传输
}
在 i2c_transfer() 中,i2c_msg 结构体传递了具体的 I2C 消息信息(包括设备地址和要发送/接收的数据)。
I2C 设备驱动
3.) 从内核空间到用户空间
- 返回数据:读取完成后,适配器驱动将数据返回到用户空间,用户应用程序接收到结果。
18.2 创建 I2C Adapter驱动
在Linux kernel中, 已经有一个基于GPIO来模拟I2C Adapter驱动 (也可以说是I2C bus驱动), 这里我们来配置及启用此驱动。
18.2.1 添加DTS
在DTS里设置具体的GPIO, 如下:
demoi2c: i2c@20 {
compatible = "i2c-gpio";
sda-gpios = <&gpio1 RK_PA2 0 >;
scl-gpios = <&gpio1 RK_PA4 0 >;
i2c-gpio,scl-open-drain;
i2c-gpio,delay-us = <1>; /* ~100 kHz */
#address-cells = <1>;
#size-cells = <0>;
pinctrl-names = "default";
pinctrl-0 = <&demo_i2c_pin>;
};
aliases {
i2c20 = &demoi2c;
};
&pinctrl {
demo_i2c {
demo_i2c_pin: demo_i2c_pin {
rockchip,pins = <1 RK_PA2 RK_FUNC_GPIO &pcfg_pull_none>,
<1 RK_PA4 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
18.2.2 驱动代码 i2c-gpio.c
驱动代码在kernel/driver/i2c/buses/i2c-gpio.c, 编译的时候, 需要把CONFIG_I2C_GPIO选项勾选, 如下:
18.2.3 数据结构
struct i2c_gpio_private_data数据结构, 定义如下:
struct i2c_gpio_private_data {
struct gpio_desc *sda;
struct gpio_desc *scl;
struct i2c_adapter adap;
struct i2c_algo_bit_data bit_data;
struct i2c_gpio_platform_data pdata;
#ifdef CONFIG_I2C_GPIO_FAULT_INJECTOR
struct dentry *debug_dir;
/* these must be protected by bus lock */
struct completion scl_irq_completion;
u64 scl_irq_data;
#endif
};
sda:指向 SDA(数据线)的 GPIO 描述符,表示 I2C 数据线的控制。
scl: 指向 SCL(时钟线)的 GPIO 描述符,表示 I2C 时钟线的控制。
adap: 通过适配器,驱动可以向 I2C 核心注册自己,并提供 I2C 设备的访问接口。
bit_data: 该结构体用于实现通过 GPIO 控制 I2C 总线的具体方法,如设置和获取 SDA 和 SCL 的电平。
pdata:存储与平台相关的配置数据,如 SDA 和 SCL 的特性(例如是否为开漏)。
CONFIG_I2C_GPIO_FAULT_INJECTOR, 用于调试的目录项,方便调试信息的输出。用于在故障注入调试时跟踪和管理中断,增强驱动的可靠性和调试能力。
通过定义 i2c_gpio_private_data 结构体,驱动可以集中管理与 I2C GPIO 操作相关的所有状态和配置信息。
struct i2c_adapter数据结构, 定义在 Linux 内核的 linux/i2c.h 头文件中, 如下:
struct i2c_adapter {
struct device dev; // 设备结构体
char name[I2C_NAME_SIZE]; // 适配器名称
unsigned int id; // 适配器 ID
struct i2c_algo_driver *algo; // 算法驱动
void *algo_data; // 算法数据
unsigned int retries; // 重试次数
unsigned long timeout; // 超时时间
// 其他字段...
};
i2c_adapter 结构体通过包含与设备管理、传输算法、重试和超时相关的字段,使得 I2C 驱动能够有效地与 I2C 核心进行交互,并灵活地适应各种硬件平台和通信需求。
struct i2c_algo_bit_data, 定义在 Linux 内核的头文件 include/linux/i2c-algo-bit.h 中, 如下:
struct i2c_algo_bit_data {
void *data; /* 私有数据,供驱动使用 */
void (*setsda)(void *data, int state); /* 设置 SDA 线状态 */
void (*setscl)(void *data, int state); /* 设置 SCL 线状态 */
int (*getsda)(void *data); /* 获取 SDA 线状态 */
int (*getscl)(void *data); /* 获取 SCL 线状态 */
unsigned long udelay; /* 延迟时间(微秒) */
unsigned long timeout; /* 超时时间 */
bool can_do_atomic; /* 是否可以原子操作 */
};
这个结构体用于存储与位操作相关的算法数据。它包含了一些指向操作函数的指针(如 setsda 和 setscl),以及一些配置参数(如 udelay 和 timeout)。 在 I2C 驱动中,位操作算法允许通过 GPIO 引脚模拟 I2C 的时钟和数据线。这意味着它使得驱动能够通过软件控制 GPIO 来实现 I2C 协议。这种灵活性对于没有硬件 I2C 控制器的系统尤其重要。
18.2.4 重要函数
初始化
subsys_initcall(i2c_gpio_init);
module_exit(i2c_gpio_exit);
其中, 使用 subsys_initcall 进行初始化的方式主要是为了在 Linux 内核启动时,以特定的顺序初始化子系统。这样可以确保在系统启动过程中,相关的子系统按正确顺序初始化。
i2c_gpio_init
static int __init i2c_gpio_init(void)
{
int ret;
ret = platform_driver_register(&i2c_gpio_driver);
if (ret)
printk(KERN_ERR "i2c-gpio: probe failed: %d\n", ret);
return ret;
}
驱动注册: platform_driver_register(&i2c_gpio_driver); 这一行代码将 i2c-gpio 驱动注册到平台驱动模型中,使得内核能够识别并管理该驱动。在成功调用 platform_driver_register 后,内核将能够识别和管理该驱动,设备的实例化(通过 platform_device)会触发相应的 probe 函数进行设备的初始化。
i2c_gpio_probe
i2c_gpio_probe 是 I2C GPIO 驱动的 probe 函数,负责在设备被发现时初始化与该设备相关的资源。这是 I2C 驱动与硬件交互的关键步骤,确保 GPIO 引脚正确配置以支持 I2C 通信。
(1). 内存分配, 如下:
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
分配 i2c_gpio_private_data 结构体的内存,用于存储设备的私有数据。
(2). 获取平台数据, 如下:
if (np) {
of_i2c_gpio_get_props(np, pdata);
} else {
if (dev_get_platdata(dev))
memcpy(pdata, dev_get_platdata(dev), sizeof(*pdata));
}
从设备树或平台数据中提取 GPIO 配置参数,确保必要的配置被正确获取。
(3). 获取 GPIO 描述符, 如下:
priv->sda = i2c_gpio_get_desc(dev, "sda", 0, gflags);
priv->scl = i2c_gpio_get_desc(dev, "scl", 1, gflags);
根据配置获取 SDA 和 SCL 引脚的 GPIO 描述符,并检查返回值以确保没有错误。
(4). 设置算法数据, 如下:
bit_data->setsda = i2c_gpio_setsda_val;
bit_data->setscl = i2c_gpio_setscl_val;
指定设置 SDA 和 SCL 引脚值的函数,确保 I2C 数据传输操作的实现。
(5). 添加 I2C 总线, 如下:
ret = i2c_bit_add_numbered_bus(adap);
if (ret)
return ret;
将适配器注册到 I2C 核心,创建与 I2C 总线的连接。
(6). 最后, 保存驱动数据, 如下:
platform_set_drvdata(pdev, priv);
将私有数据与平台设备绑定,以便后续访问。
18.2.5 加载驱动
首先使用下面命令编译kernel:
rk3588-linux$ ./build.sh kernel
编译成功后, 在设备上更新boot.img。 查看gpio管脚是否配置正确, 如下:
root@LPA3588:~# cat /sys/kernel/debug/gpio
gpiochip1: GPIOs 32-63, parent: platform/fec20000.gpio, gpio1:
gpio-34 ( |sda ) out hi
gpio-36 ( |scl ) out hi
gpio-40 ( |gpio_export ) out lo
gpio-41 ( |vcc-mipidphy1-regula) out lo
gpio-42 ( |vcc-mipidphy0-regula) out lo
gpio-44 ( |reset ) out hi
gpio-52 ( |hp-det ) in hi ACTIVE LOW
gpio-54 ( |work1 ) out hi ACTIVE LOW
可以看见有gpio-34, gpio-36分别被设置为sda及scl。
查看i2c节点是否创建成功, 如下:
neardi@LPA3588:/sys/bus/i2c/devices/i2c-20$ ls /dev/i2c-20 -l
crw-rw---- 1 root i2c 89, 20 Sep 20 18:25 /dev/i2c-20
neardi@LPA3588:/sys/bus/i2c/devices/i2c-20$ cat name
i2c@20
这里成功地创建了i2c节点, 由于我们在DTS里设置了别名, 故有/dev/i2c-20。