I2C 驱动程序,第一部分
在 2003 年 6 月和 8 月刊的 Linux Journal 中,我的专栏介绍了 Linux 内核驱动程序模型,并以 I2C 子系统为例。本月,我们将讨论 I2C 子系统的作用以及如何为其编写驱动程序。
I2C 是飞利浦公司最初开发的一种双线串行总线协议的名称。它通常用于嵌入式系统中,以便不同的组件可以进行通信;PC 主板使用 I2C 与不同的传感器芯片进行通信。这些传感器通常会报告风扇速度、处理器温度以及大量系统硬件信息。该协议还用于某些 RAM 芯片中,以将有关 DIMM 本身的信息报告回操作系统。
I2C 内核代码在其大部分开发生命周期中都存在于主内核树之外——它最初是在 2.0 时代编写的。2.4 内核包含一些 I2C 支持,主要用于一些视频驱动程序。在 2.6 内核中,大部分 I2C 代码已进入主内核树,这要归功于许多内核开发人员的努力,他们更改了接口,使其更易于内核社区接受。一些驱动程序仍然只存在于外部 CVS 树中,尚未移至主 kernel.org 树中,但这只是时间问题,它们也将被移植。
I2C 内核代码被分解为多个逻辑部分:I2C 核心、I2C 总线驱动程序、I2C 算法驱动程序和 I2C 芯片驱动程序。在本文中,我们忽略 I2C 核心如何运行,而专注于如何编写总线和算法驱动程序。在第二部分中,我们将介绍如何编写 I2C 芯片驱动程序。
I2C 总线驱动程序由名为 i2c_adapter 的结构体描述,该结构体在 include/linux/i2c.h 文件中定义。只有以下字段需要由总线驱动程序设置
struct module *owner; — 设置为值 (THIS_MODULE),以允许正确的模块引用计数。
unsigned int class; — 此驱动程序支持的 I2C 类设备的类型。通常,此值设置为 I2C_ADAP_CLASS_SMBUS。
struct i2c_algorithm *algo; — 指向 struct i2c_algorithm 结构的指针,该结构描述了通过此 I2C 总线控制器传输数据的方式。下面提供了有关此结构的更多信息。
char name[I2C_NAME_SIZE]; — 设置为 I2C 总线驱动程序的描述性名称。此值显示在与此 I2C 适配器关联的 sysfs 文件名中。
以下代码来自名为 tiny_i2c_adap.c 的示例 I2C 适配器驱动程序,可从 Linux Journal FTP 站点 [ftp://ftp.linuxjournal.com/pub/lj/listings/issue116/7136.tgz] 获得,并显示了如何设置 struct i2c_adapter
static struct i2c_adapter tiny_adapter = { .owner = THIS_MODULE, .class = I2C_ADAP_CLASS_SMBUS, .algo = &tiny_algorithm, .name = "tiny adapter", };
为了注册此 I2C 适配器,驱动程序调用函数 i2c_add_adapter,并将指向 struct i2c_adapter 的指针作为参数传递
retval = i2c_add_adapter(&tiny_adapter);
如果 I2C 适配器位于具有关联的 struct device 的设备类型上,例如 PCI 或 USB 设备,则在调用 i2c_add_adapter 之前,应将适配器设备的父指针设置为该设备。此指针配置可以在 drivers/i2c/busses/i2c-piix4.c 驱动程序的以下行中看到
/* set up sysfs linkage to our parent device */ piix4_adapter.dev.parent = &dev->dev;
如果未设置此父指针,则 I2C 适配器将位于传统总线上,并在 /sys/devices/legacy 的 sysfs 树中显示。以下是我们的示例驱动程序注册时发生的情况
$ tree /sys/devices/legacy/ /sys/devices/legacy/ |-- detach_state |-- floppy0 | |-- detach_state | `-- power | `-- state |-- i2c-0 | |-- detach_state | |-- name | `-- power | `-- state `-- power `-- state
正如之前的内核驱动程序模型专栏中所讨论的,I2C 适配器也显示在 /sys/class/i2c-adapter 目录中
$ tree /sys/class/i2c-adapter/ /sys/class/i2c-adapter/ `-- i2c-0 |-- device -> ../../../devices/legacy/i2c-0 `-- driver -> ../../../bus/i2c/drivers/i2c_adapter
要注销 I2C 适配器,驱动程序应调用函数 i2c_del_adapter,并将指向 struct i2c_adapter 的指针作为参数传递,如下所示
i2c_del_adapter(&tiny_adapter);
I2C 总线驱动程序使用 I2C 算法与 I2C 总线进行通信。大多数 I2C 总线驱动程序都定义了自己的 I2C 算法并使用它们,因为它们与总线驱动程序如何与特定类型的硬件通信密切相关。对于某些类型的 I2C 总线驱动程序,已经编写了许多 I2C 算法驱动程序。这些示例包括 drivers/i2c/i2c-algo-ite.c 中找到的 ITE 适配器、drivers/i2c/i2c-algo-ibm_ocp.c 中找到的 IBM PPC 405 适配器以及 drivers/i2c/i2c-algo-bit.c 中找到的通用 I2C 位移算法。所有这些已编写的算法都有自己的函数,I2C 总线驱动程序需要注册才能使用。有关这些算法的更多信息,请参阅内核树中的所有 drivers/i2c/i2c-algo-*.c 文件。
对于我们的示例驱动程序,我们将创建自己的 I2C 算法驱动程序。算法驱动程序由 struct i2c_algorithm 结构定义,并在 include/linux/i2c.h 文件中定义。以下是一些常用字段的描述
char name[32];: 算法的名称。
unsigned int id;: 描述此结构定义的算法类型的描述。这些不同的类型在 include/linux/i2c-id.h 文件中定义,并以字符 I2C_ALGO_ 开头。
int (*master_xfer)(struct i2c_adapter *adap,struct i2c_msg msgs[], int num);: 如果此算法驱动程序可以执行 I2C 直接级别访问,则设置为此函数的函数指针。如果设置了此函数,则每当 I2C 芯片驱动程序想要与芯片设备通信时,都会调用此函数。如果设置为 NULL,则改用 smbus_xfer 函数。
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr, unsigned short flags, char read_write, u8 command, int size, union i2c_smbus_data *data);: 如果此算法驱动程序可以执行 SMB 总线访问,则设置为此函数的函数指针。大多数基于 PCI 的 I2C 总线驱动程序都能够做到这一点,它们应该设置此函数指针。如果设置了此函数,则每当 I2C 芯片驱动程序想要与芯片设备通信时,都会调用此函数。如果设置为 NULL,则改用 master_xfer 函数。
u32 (*functionality) (struct i2c_adapter *);: I2C 核心调用的函数指针,用于确定 I2C 适配器驱动程序可以执行哪些类型的读取和写入操作。
在我们的示例 I2C 适配器驱动程序中,i2c_adapter 结构引用了 tiny_algorithm 变量。该结构定义如下
static struct i2c_algorithm tiny_algorithm = { .name = "tiny algorithm", .id = I2C_ALGO_SMBUS, .smbus_xfer = tiny_access, .functionality = tiny_func, };
tiny_func 函数很小,它告诉 I2C 核心此算法可以支持哪些类型的 I2C 消息。对于此驱动程序,我们希望能够支持几种不同的 I2C 消息类型
static u32 tiny_func(struct i2c_adapter *adapter) { return I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE | I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA | I2C_FUNC_SMBUS_BLOCK_DATA; }
所有不同的 I2C 消息类型都在 include/linux/i2c.h 中定义,并以字符 I2C_FUNC_ 开头。
当 I2C 客户端驱动程序想要与 I2C 总线通信时,将调用 tiny_access 函数。我们的示例函数非常简单;它只是将 I2C 芯片驱动程序向 syslog 发出的所有请求记录下来,并将成功报告回调用方。此日志允许您查看 I2C 芯片驱动程序可能请求的所有不同地址和数据类型。实现方式如下所示
static s32 tiny_access(struct i2c_adapter *adap, u16 addr, unsigned short flags, char read_write, u8 command, int size, union i2c_smbus_data *data) { int i, len; dev_info(&adap->dev, "%s was called with the " "following parameters:\n", __FUNCTION__); dev_info(&adap->dev, "addr = %.4x\n", addr); dev_info(&adap->dev, "flags = %.4x\n", flags); dev_info(&adap->dev, "read_write = %s\n", read_write == I2C_SMBUS_WRITE ? "write" : "read"); dev_info(&adap->dev, "command = %d\n", command); switch (size) { case I2C_SMBUS_PROC_CALL: dev_info(&adap->dev, "size = I2C_SMBUS_PROC_CALL\n"); break; case I2C_SMBUS_QUICK: dev_info(&adap->dev, "size = I2C_SMBUS_QUICK\n"); break; case I2C_SMBUS_BYTE: dev_info(&adap->dev, "size = I2C_SMBUS_BYTE\n"); break; case I2C_SMBUS_BYTE_DATA: dev_info(&adap->dev, "size = I2C_SMBUS_BYTE_DATA\n"); if (read_write == I2C_SMBUS_WRITE) dev_info(&adap->dev, "data = %.2x\n", data->byte); break; case I2C_SMBUS_WORD_DATA: dev_info(&adap->dev, "size = I2C_SMBUS_WORD_DATA\n"); if (read_write == I2C_SMBUS_WRITE) dev_info(&adap->dev, "data = %.4x\n", data->word); break; case I2C_SMBUS_BLOCK_DATA: dev_info(&adap->dev, "size = I2C_SMBUS_BLOCK_DATA\n"); if (read_write == I2C_SMBUS_WRITE) { dev_info(&adap->dev, "data = %.4x\n", data->word); len = data->block[0]; if (len < 0) len = 0; if (len > 32) len = 32; for (i = 1; i <= len; i++) dev_info(&adap->dev, "data->block[%d] = %x\n", i, data->block[i]); } break; } return 0; }
现在 tiny_i2c_adap 驱动程序已构建并加载,它可以做什么?它本身无法做任何事情。I2C 总线驱动程序需要 I2C 客户端驱动程序才能做任何事情,而不是仅仅坐在 sysfs 树中。因此,如果加载了 lm75 I2C 客户端驱动程序,它会尝试使用 tiny_i2c_adap 驱动程序来查找为其编写的芯片
$ modprobe lm75 $ tree /sys/bus/i2c/ /sys/bus/i2c/ |-- devices | |-- 0-0048 -> ../../../devices/legacy/i2c-0/0-0048 | |-- 0-0049 -> ../../../devices/legacy/i2c-0/0-0049 | |-- 0-004a -> ../../../devices/legacy/i2c-0/0-004a | |-- 0-004b -> ../../../devices/legacy/i2c-0/0-004b | |-- 0-004c -> ../../../devices/legacy/i2c-0/0-004c | |-- 0-004d -> ../../../devices/legacy/i2c-0/0-004d | |-- 0-004e -> ../../../devices/legacy/i2c-0/0-004e | `-- 0-004f -> ../../../devices/legacy/i2c-0/0-004f `-- drivers |-- i2c_adapter `-- lm75 |-- 0-0048 -> ../../../../devices/legacy/i2c-0/0-0048 |-- 0-0049 -> ../../../../devices/legacy/i2c-0/0-0049 |-- 0-004a -> ../../../../devices/legacy/i2c-0/0-004a |-- 0-004b -> ../../../../devices/legacy/i2c-0/0-004b |-- 0-004c -> ../../../../devices/legacy/i2c-0/0-004c |-- 0-004d -> ../../../../devices/legacy/i2c-0/0-004d |-- 0-004e -> ../../../../devices/legacy/i2c-0/0-004e `-- 0-004f -> ../../../../devices/legacy/i2c-0/0-004f
由于 tiny_i2c_adap 驱动程序对它被要求完成的每个读取和写入请求都响应成功,因此 lm75 I2C 芯片驱动程序认为它在每个已知的此芯片的可能 I2C 地址都找到了一个 lm75 芯片。地址的丰富性就是创建 I2C 设备 0-0048 到 0-004f 的原因。如果我们查看其中一个设备的目录,则会显示此芯片驱动程序的传感器文件
$ tree /sys/devices/legacy/i2c-0/0-0048/ /sys/devices/legacy/i2c-0/0-0048/ |-- detach_state |-- name |-- power | `-- state |-- temp_input |-- temp_max `-- temp_min
detach_state 文件和 power 目录由内核驱动程序核心创建,用于电源管理。它不是由 lm75 驱动程序创建的。此目录中其他文件的功能如下所述。
如果我们向 lm75 驱动程序询问 temp_max 的当前值,我们将收到以下内容
$ cat /sys/devices/legacy/i2c-0/0-0048/temp_max 1000
为了获得该值,lm75 驱动程序要求 tiny_i2c_adap 驱动程序读取 I2C 总线上的一些地址。此请求显示在 syslog 中
$ dmesg i2c_adapter i2c-0: tiny_access was called with the following parameters: i2c_adapter i2c-0: addr = 0048 i2c_adapter i2c-0: flags = 0000 i2c_adapter i2c-0: read_write = read i2c_adapter i2c-0: command = 0 i2c_adapter i2c-0: size = I2C_SMBUS_WORD_DATA i2c_adapter i2c-0: tiny_access was called with the following parameters: i2c_adapter i2c-0: addr = 0048 i2c_adapter i2c-0: flags = 0000 i2c_adapter i2c-0: read_write = read i2c_adapter i2c-0: command = 3 i2c_adapter i2c-0: size = I2C_SMBUS_WORD_DATA i2c_adapter i2c-0: tiny_access was called with the following parameters: i2c_adapter i2c-0: addr = 0048 i2c_adapter i2c-0: flags = 0000 i2c_adapter i2c-0: read_write = read i2c_adapter i2c-0: command = 2 i2c_adapter i2c-0: size = I2C_SMBUS_WORD_DATA
日志文件显示 tiny_access 函数被调用了三次。第一个命令想要从地址为 0048 的设备中的寄存器 0 读取一个数据字。第二个和第三个读取请求来自同一设备的寄存器 3 和寄存器 2。这些命令与 drivers/i2c/chips/lm75.c 文件中 lm75_update_client 函数中的以下代码匹配
data->temp_input = lm75_read_value(client, LM75_REG_TEMP); data->temp_max = lm75_read_value(client, LM75_REG_TEMP_OS); data->temp_hyst = lm75_read_value(client, LM75_REG_TEMP_HYST);
同一文件中的 lm75_read_value 函数包含以下代码
/* All registers are word-sized, except for the configuration register. LM75 uses a high-byte first convention, which is exactly opposite to the usual practice. */ static int lm75_read_value(struct i2c_client *client, u8 reg) { if (reg == LM75_REG_CONF) return i2c_smbus_read_byte_data(client, reg); else return swap_bytes( i2c_smbus_read_word_data(client, reg)); }
因此,当 lm75 驱动程序想要读取最大温度值时,它会调用带有寄存器号的 lm75_read_value 函数,然后该函数调用 I2C 核心函数 i2c_smbus_read_word_data。该 I2C 核心函数查找客户端设备所在的 I2C 总线,然后调用与该特定 I2C 总线关联的 I2C 算法来执行数据传输。这就是我们的 tiny_i2c_adap 驱动程序被要求完成传输的方法。
如果写入同一个 sysfs 文件,lm75 驱动程序会要求 tiny_i2c_adap 驱动程序以与读取请求相同的方式将一些数据写入 I2C 总线上的特定地址。此请求也显示在 syslog 中
$ echo 300 > /sys/devices/legacy/i2c-0/0-0048/temp_max $ dmesg i2c_adapter i2c-0: tiny_access was called with the following parameters: i2c_adapter i2c-0: addr = 0048 i2c_adapter i2c-0: flags = 0000 i2c_adapter i2c-0: read_write = write i2c_adapter i2c-0: command = 3 i2c_adapter i2c-0: size = I2C_SMBUS_WORD_DATA i2c_adapter i2c-0: data = 8000
本月,我们介绍了 I2C 驱动程序子系统的基础知识,并解释了如何编写一个简单的 I2C 总线和 I2C 算法驱动程序,它们可以与任何现有的 I2C 客户端驱动程序一起工作。完整的驱动程序 dmn-09-i2c-adap.c 可从 Linux Journal FTP 站点 ftp://ftp.linuxjournal.com/pub/lj/listings/issue116/7136.tgz 获取。在第二部分中,我们将介绍如何编写 I2C 芯片驱动程序。
Greg Kroah-Hartman 目前是各种不同驱动程序子系统的 Linux 内核维护者。他在 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。