I2C 驱动程序,第二部分

作者:Greg Kroah-Hartman

在我上个月的专栏 [LJ,2003 年 12 月] 中,我们讨论了 I2C 总线驱动程序和 I2C 算法驱动程序的工作原理。我们还描述了如何制作一个微型的虚拟 I2C 总线驱动程序。本月,我们将讨论 I2C 芯片驱动程序的工作原理,并提供一个实际应用的例子。

I2C 芯片驱动程序控制与位于 I2C 总线上的单个 I2C 设备通信的过程。I2C 芯片设备通常监控主板上许多不同的物理设备,例如不同的风扇速度、温度值和电压。

struct i2c_driver 结构体描述了一个 I2C 芯片驱动程序。此结构体在 include/linux/i2c.h 文件中定义。只有以下字段是创建可工作的芯片驱动程序所必需的

  • struct module *owner; — 设置为 THIS_MODULE 值,以允许正确的模块引用计数。

  • char name[I2C_NAME_SIZE]; — 设置为 I2C 芯片驱动程序的描述性名称。此值会显示在为每个 I2C 芯片设备创建的 sysfs 文件名中。

  • unsigned int flags; — 设置为 I2C_DF_NOTIFY 值,以便在此驱动程序加载后加载任何新的 I2C 设备时通知芯片驱动程序。此字段可能很快就会消失,因为几乎所有驱动程序都设置了此字段。

  • int (*attach_adapter)(struct i2c_adapter *); — 每当在系统中加载新的 I2C 总线驱动程序时调用。此函数在下面更详细地描述。

  • int (*detach_client)(struct i2c_client *); — 当 i2c_client 设备要从系统中移除时调用。有关此函数的更多信息在下面提供。

以下代码来自一个名为 tiny_i2c_chip.c 的示例 I2C 芯片驱动程序,该驱动程序可从 Linux Journal FTP 站点 [ftp.linuxjournal.com/pub/lj/listings/issue118/7252.tgz] 获取。它展示了 struct i2c_driver 结构体是如何设置的

static struct i2c_driver chip_driver = {
    .owner          = THIS_MODULE,
    .name           = "tiny_chip",
    .flags          = I2C_DF_NOTIFY,
    .attach_adapter = chip_attach_adapter,
    .detach_client  = chip_detach_client,
};

注册芯片驱动程序

要注册此 I2C 芯片驱动程序,应使用指向 struct i2c_driver 的指针调用函数 i2c_add_driver

static int __init tiny_init(void)
{
    return i2c_add_driver(&chip_driver);
}

要注销 I2C 芯片驱动程序,应使用相同的指向 struct i2c_driver 的指针调用函数 i2c_del_driver

static void __exit tiny_exit(void)
{
    i2c_del_driver(&chip_driver);
}

在 I2C 芯片驱动程序注册后,当加载 I2C 总线驱动程序时,将调用 attach_adapter 函数回调。此函数检查此 I2C 总线上是否有任何客户端驱动程序想要连接的 I2C 设备。几乎所有的 I2C 芯片驱动程序都调用核心 I2C 函数 i2c_detect 来确定这一点。例如,tiny_i2c_chip.c 驱动程序就是这样做的

static int
chip_attach_adapter(struct i2c_adapter *adapter)
{
    return i2c_detect(adapter, &addr_data,
                      chip_detect);
}

i2c_detect 函数探测 I2C 适配器,寻找 addr_data 结构体中指定的不同地址。如果找到设备,则调用 chip_detect 函数。

如果您仔细查看源代码,您会发现任何地方都找不到 addr_data 结构体。原因是它是由 SENSORS_INSMOD_1 宏创建的。此宏在 include/linux/i2c-sensor.h 文件中定义,并且非常复杂。它根据此驱动程序支持的不同类型芯片的数量以及这些芯片通常存在的地址,设置一个名为 addr_data 的静态变量。然后,它提供了通过使用模块参数来覆盖这些值的能力。I2C 芯片驱动程序必须提供变量 normal_i2c、normal_i2c_range、normal_isa 和 normal_isa_range。这些变量定义了此芯片驱动程序支持的 i2c smbus 和 i2c isa 地址。它们是一个地址数组,都以特殊值 I2C_CLIENT_END 或 I2C_CLIENT_ISA_END 结尾。通常,特定类型的 I2C 芯片仅出现在有限的地址范围内。tiny_i2c_client.c 驱动程序将这些变量定义为

static unsigned short normal_i2c[] =
  { I2C_CLIENT_END };
static unsigned short normal_i2c_range[] =
  { 0x00, 0xff, I2C_CLIENT_END };
static unsigned int normal_isa[] =
  { I2C_CLIENT_ISA_END };
static unsigned int normal_isa_range[] =
  { I2C_CLIENT_ISA_END };

normal_i2c_range 变量指定我们可以在任何 I2C smbus 地址找到此芯片设备。这使我们能够在几乎任何 I2C 总线驱动程序上测试此驱动程序。

当找到芯片时该怎么做

在 tiny_i2c_chip.c 驱动程序中,当找到 I2C 芯片设备时,I2C 核心会调用 chip_detect 函数。此函数使用以下参数声明

static int
chip_detect(struct i2c_adapter *adapter,
            int address, int kind);

adapter 变量是此芯片所在的 I2C 适配器结构体。address 变量包含找到芯片的地址,而 kind 变量指示找到的芯片类型。kind 变量通常被忽略,但是一些 I2C 芯片驱动程序支持不同类型的 I2C 芯片,因此可以使用此变量来确定存在的芯片类型。

此函数负责创建 struct i2c_client 结构体,然后将其注册到 I2C 核心。I2C 核心将该结构体用作单个 I2C 芯片设备。要创建此结构体,chip_detect 函数执行以下操作

struct i2c_client *new_client = NULL;
struct chip_data *data = NULL;
int err = 0;

new_client = kmalloc(sizeof(*new_client),
                     GFP_KERNEL);
if (!new_client) {
    err = -ENOMEM;
    goto error;
}
memset(new_client, 0x00, sizeof(*new_client));

data = kmalloc(sizeof(*data), GFP_KERNEL);
if (!data) {
    err = -ENOMEM;
    goto error;
}
memset(data, 0x00, sizeof(*data));

i2c_set_clientdata(new_client, data);
new_client->addr = address;
new_client->adapter = adapter;
new_client->driver = &chip_driver;
new_client->flags = 0;
strncpy(new_client->name, "tiny_chip",
        I2C_NAME_SIZE);


首先,创建 struct i2c_client 结构体和一个单独的本地数据结构体(称为 struct chip_data)并将其初始化为零。重要的是 i2c_client 结构体要初始化为零,因为内核驱动程序核心的较低级别需要这样做才能正常工作。成功分配内存后,struct i2c_client 中的一些字段被设置为指向此特定设备和此特定驱动程序。值得注意的是,addr、adapter 和 driver 变量必须初始化。如果要在 sysfs 树中正确显示此 I2C 设备,还必须设置 struct i2c_client 的名称。

在初始化 struct i2c_client 结构体后,必须将其注册到 I2C 核心。这是通过调用 i2c_attach_client 函数来完成的

/* Tell the I2C layer a new client has arrived */
err = i2c_attach_client(new_client);
if (err)
    goto error;

当此函数返回且没有报告错误时,I2C 芯片设备在内核中已正确设置。

I2C 和 sysfs

在 2.0、2.2 和 2.4 内核中,I2C 代码会将 I2C 芯片设备放置在 /proc/bus/i2c 目录中。在 2.6 内核中,所有 I2C 芯片设备和适配器都显示在 sysfs 文件系统中。I2C 芯片设备可以在 /sys/bus/i2c/devices 中找到,按其适配器地址和芯片地址列出。例如,加载到机器上的 tiny_i2c_chip 驱动程序可能会产生以下 sysfs 树结构

$ tree /sys/bus/i2c/
/sys/bus/i2c/
|-- devices
|   |-- 0-0009 -> ../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-0009
|   |-- 0-000a -> ../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-000a
|   |-- 0-000b -> ../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-000b
|   `-- 0-0019 -> ../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-0019
`-- drivers
    |-- i2c_adapter
    `-- tiny_chip
        |-- 0-0009 -> ../../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-0009
        |-- 0-000a -> ../../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-000a
        |-- 0-000b -> ../../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-000b
        `-- 0-0019 -> ../../../../devices/pci0000:00/0000:00:06.0/i2c-0/0-0019

这显示了四个不同的 I2C 芯片设备,它们都由同一个 tiny_chip 驱动程序控制。可以通过查看 /sys/bus/i2c/drivers 目录中的设备,或者查看芯片设备本身的目录并读取 name 文件来找到控制驱动程序

$ cat /sys/devices/pci0000\:00/0000\:00\:06.0/i2c-0/0-0009/name
tiny_chip

所有 I2C 芯片驱动程序都通过 I2C 芯片设备目录中的 sysfs 文件导出不同的传感器值。这些文件名是标准化的,以及值所表达的单位,并在内核树中的 Documentation/i2c/sysfs-interface 文件(表 1)中记录。

表 1. 通过 sysfs 文件导出的传感器值

temp_max[1-3]最高温度值。定点值,格式为 XXXXX,应除以 1,000 以获得摄氏度。读/写值。
temp_min[1-3]最低温度或滞后值。定点值,格式为 XXXXX,应除以 1,000 以获得摄氏度。这最好是一个滞后值,报告为绝对温度,不是与最大值的差值。读/写值。
temp_input[1-3]温度输入值。只读值。

如表 1 中的信息所示,每个文件只有一个值。所有文件都是可读的,一些文件可以由具有适当权限的用户写入。

tiny_i2c_chip.c 驱动程序模拟一个可以报告温度值的 I2C 芯片设备。它在 sysfs 中创建文件 temp_max1、temp_min1 和 temp_input1。每次读取文件时,它返回的值都会递增,以显示如何访问不同的唯一芯片值。

为了在 sysfs 中创建一个文件,使用了 DEVICE_ATTR 宏

static DEVICE_ATTR(temp_max, S_IWUSR | S_IRUGO,
                   show_temp_max, set_temp_max);
static DEVICE_ATTR(temp_min, S_IWUSR | S_IRUGO,
                   show_temp_hyst, set_temp_hyst);
static DEVICE_ATTR(temp_input, S_IRUGO,
                   show_temp_input, NULL);

此宏创建一个结构体,然后在 chip_detect 函数的末尾将其传递给函数 device_create_file

/* Register sysfs files */
device_create_file(&new_client->dev,
                   &dev_attr_temp_max);
device_create_file(&new_client->dev,
                   &dev_attr_temp_min);
device_create_file(&new_client->dev,
                   &dev_attr_temp_input);

该调用为设备创建 sysfs 文件

/sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009
|-- detach_state
|-- name
|-- power
|   `-- state
|-- temp_input
|-- temp_max
`-- temp_min

文件名由 I2C 核心创建,文件 detach_state 和 power/state 由驱动程序核心创建。

但是,让我们回到 DEVICE_ATTR 宏。该宏想知道要创建的文件名、要创建的文件模式、从文件读取时要调用的函数名称以及向文件写入时要调用的函数名称。对于文件 temp_max,此声明是

static DEVICE_ATTR(temp_max, S_IWUSR | S_IRUGO,
                   show_temp_max, set_temp_max);

从文件读取时调用的函数是 show_temp_max。这与许多 sysfs 文件一样,使用另一个创建函数的宏定义

#define show(value) \
static ssize_t \
show_##value(struct device *dev, char *buf)        \
{                                                  \
    struct i2c_client *client = to_i2c_client(dev);\
    struct chip_data *data =                       \
        i2c_get_clientdata(client);                \
                                                   \
    chip_update_client(client);                    \
    return sprintf(buf, "%d\n", data->value);      \
}
show(temp_max);
show(temp_hyst);
show(temp_input);

使用宏创建此函数的原因是,创建其他 sysfs 文件非常简单,这些文件执行几乎相同的操作,但名称不同,并且从不同的变量读取,而无需重复代码。这个宏创建了三个不同的函数,用于从 struct chip_data 结构体中的三个不同变量读取。

在此函数中,struct device * 被转换为 struct i2c_client *。然后从 struct i2c_client * 获取私有 struct chip_data *。之后,通过调用 chip_update_client 更新芯片数据。从那里,请求的变量被打印到缓冲区中并返回给驱动程序核心,然后驱动程序核心将其返回给用户

$ cat /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_input
1

chip_update_client 每次调用时都会将所有值递增 1

static void
chip_update_client(struct i2c_client *client)
{
    struct chip_data *data =
        i2c_get_clientdata(client);

    down(&data->update_lock);
    dev_dbg(&client->dev, "%s\n", __FUNCTION__);
    ++data->temp_input;
    ++data->temp_max;
    ++data->temp_hyst;
    data->last_updated = jiffies;
    data->valid = 1;
    up(&data->update_lock);
}

因此,所有后续对此值的请求都不同

$ cat /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_input
2
$ cat /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_input
3

set_temp_max 函数也是从宏创建的,以允许写入变量

#define set(value, reg)	\
static ssize_t                                     \
set_##value(struct device *dev,                    \
            const char *buf, size_t count)         \
{                                                  \
    struct i2c_client *client = to_i2c_client(dev);\
    struct chip_data *data =                       \
        i2c_get_clientdata(client);                \
    int temp = simple_strtoul(buf, NULL, 10);      \
                                                   \
    down(&data->update_lock);                      \
    data->value = temp;                            \
    up(&data->update_lock);                        \
    return count;                                  \
}
set(temp_max, REG_TEMP_OS);
set(temp_hyst, REG_TEMP_HYST);

与 show 函数一样,此函数将 struct device * 转换为 struct i2c_client *,然后找到私有 struct chip_data *。然后,用户提供的数据通过调用 simple_strtoul 转换为数字,并保存到正确的变量中

$ cat /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_max
1
$ echo 41 > /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_max
$ cat /sys/devices/pci0000:00/0000:00:06.0/i2c-0/0-0009/temp_max
42
清理

当 I2C 芯片设备从系统中移除时,无论是通过卸载 I2C 总线驱动程序还是卸载 I2C 芯片驱动程序,I2C 核心都会调用 struct i2c_driver 结构体中指定的 detatch_client 函数。这通常是一个简单的函数,正如在示例驱动程序的实现中可以看到的那样

static int chip_detach_client(struct i2c_client *client)
{
    struct chip_data *data = i2c_get_clientdata(client);
    int err;

    err = i2c_detach_client(client);
    if (err) {
        dev_err(&client->dev,
                "Client deregistration failed, "
                "client not detached.\n");
        return err;
    }
    kfree(client);
    kfree(data);
    return 0;
}

由于调用了 i2c_attach_client 函数以将 struct i2c_client 结构体注册到 I2C 核心,因此必须调用 i2c_detach_client 函数来注销它。如果该函数成功,则需要在从函数返回之前释放驱动程序为 I2C 设备分配的内存。

此示例驱动程序没有专门从 sysfs 核心删除 sysfs 文件。此步骤在驱动程序核心中的 i2c_detach_client 函数中自动完成。但是,如果作者愿意,可以通过调用 device_remove_file 手动删除该文件。

结论

这篇由两部分组成的文章系列解释了如何编写内核 I2C 总线驱动程序、I2C 算法驱动程序和 I2C 芯片驱动程序的基础知识。有关如何编写 I2C 驱动程序的许多有用信息可以在内核树中的 Documentation/i2c 目录和 Lm_sensors 网站 (secure.netroedge.com/~lm78) 上找到。

Greg Kroah-Hartman 目前是 Linux 内核中各种不同驱动程序子系统的维护者。他在 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。

加载 Disqus 评论