I2C 驱动程序,第一部分

作者:Greg Kroah-Hartman

在 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 总线驱动程序由名为 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 总线驱动程序,已经编写了许多 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 与他联系。

加载 Disqus 评论