实现 I2C 设备驱动程序

这是一份小型指南,适用于那些想要使用 Linux 作为协议主机/主设备(而不是从设备)为 I2C 或 SMBus 设备编写内核驱动程序的人员。

要设置驱动程序,您需要执行几项操作。有些是可选的,有些事情可以稍作或完全不同地完成。将其用作指南,而不是规则手册!

一般说明

尽量保持内核命名空间尽可能干净。做到这一点的最佳方法是为所有全局符号使用唯一前缀。这对于导出的符号尤为重要,但对于未导出的符号也这样做是一个好主意。在本教程中,我们将使用前缀 foo_

驱动程序结构

通常,您将实现单个驱动程序结构,并从中实例化所有客户端。请记住,驱动程序结构包含常规访问例程,并且除了您提供数据的字段外,应将其初始化为零。客户端结构保存特定于设备的信息,例如驱动程序模型设备节点及其 I2C 地址。

static const struct i2c_device_id foo_idtable[] = {
      { "foo", my_id_for_foo },
      { "bar", my_id_for_bar },
      { }
};
MODULE_DEVICE_TABLE(i2c, foo_idtable);

static struct i2c_driver foo_driver = {
      .driver = {
              .name   = "foo",
              .pm     = &foo_pm_ops,  /* optional */
      },

      .id_table       = foo_idtable,
      .probe          = foo_probe,
      .remove         = foo_remove,

      .shutdown       = foo_shutdown, /* optional */
      .command        = foo_command,  /* optional, deprecated */
}

name 字段是驱动程序名称,并且不得包含空格。它应与模块名称匹配(如果驱动程序可以编译为模块),尽管您可以使用 MODULE_ALIAS(在本示例中传递 “foo”)为模块添加另一个名称。如果驱动程序名称与模块名称不匹配,则不会自动加载该模块(热插拔/冷插拔)。

所有其他字段用于回调函数,这些函数将在下面解释。

额外的客户端数据

每个客户端结构都有一个特殊的 data 字段,该字段可以指向任何结构。您应该使用它来保存特定于设备的数据。

/* store the value */
void i2c_set_clientdata(struct i2c_client *client, void *data);

/* retrieve the value */
void *i2c_get_clientdata(const struct i2c_client *client);

请注意,从内核 2.6.34 开始,您不必在 remove() 中或如果 probe() 失败时将 data 字段设置为 NULL。i2c-core 会在这些情况下自动执行此操作。这些也是核心将接触此字段的唯一时间。

访问客户端

假设我们有一个有效的客户端结构。在某个时候,我们将需要从客户端收集信息,或将新信息写入客户端。

我发现定义 foo_read 和 foo_write 函数很有用。在某些情况下,直接调用 I2C 函数会更容易,但是许多芯片都有某种可以轻松封装的寄存器值概念。

以下函数是简单的示例,不应逐字复制

int foo_read_value(struct i2c_client *client, u8 reg)
{
      if (reg < 0x10) /* byte-sized register */
              return i2c_smbus_read_byte_data(client, reg);
      else            /* word-sized register */
              return i2c_smbus_read_word_data(client, reg);
}

int foo_write_value(struct i2c_client *client, u8 reg, u16 value)
{
      if (reg == 0x10)        /* Impossible to write - driver error! */
              return -EINVAL;
      else if (reg < 0x10)    /* byte-sized register */
              return i2c_smbus_write_byte_data(client, reg, value);
      else                    /* word-sized register */
              return i2c_smbus_write_word_data(client, reg, value);
}

探测和连接

Linux I2C 堆栈最初是为了支持访问 PC 主板上的硬件监控芯片而编写的,因此过去常常嵌入一些更适合 SMBus(和 PC)而不是 I2C 的假设。其中一个假设是,大多数适配器和设备驱动程序都支持 SMBUS_QUICK 协议来探测设备是否存在。另一个假设是,可以使用此类探测原语充分配置设备及其驱动程序。

随着 Linux 及其 I2C 堆栈在嵌入式系统和 DVB 适配器等复杂组件中得到更广泛的应用,这些假设变得越来越有问题。发出中断的 I2C 设备的驱动程序需要更多(和不同)的配置信息,处理无法通过协议探测区分的芯片变体的驱动程序也是如此,或者需要一些特定于板的信息才能正确运行的驱动程序也是如此。

设备/驱动程序绑定

系统基础设施(通常是特定于板的初始化代码或启动固件)会报告存在哪些 I2C 设备。例如,内核中或来自引导加载程序可能有一个表格,用于识别 I2C 设备并将它们链接到有关 IRQ 和其他接线工件、芯片类型等的特定于板的配置信息。这可用于为每个 I2C 设备创建 i2c_client 对象。

使用此绑定模型的 I2C 设备驱动程序的工作方式与 Linux 中的任何其他类型的驱动程序一样:它们提供一个 probe() 方法来绑定到这些设备,并提供一个 remove() 方法来解除绑定。

static int foo_probe(struct i2c_client *client);
static void foo_remove(struct i2c_client *client);

请记住,i2c_driver 不会创建这些客户端句柄。该句柄可能会在 foo_probe() 期间使用。如果 foo_probe() 报告成功(零而不是负状态代码),则它可以保存该句柄并使用它,直到 foo_remove() 返回。大多数 Linux 驱动程序都使用这种绑定模型。

当 id_table name 字段中的条目与设备的名称匹配时,将调用探测函数。如果探测函数需要该条目,则可以使用以下命令检索它

const struct i2c_device_id *id = i2c_match_id(foo_idtable, client);

设备创建

如果您确定 I2C 设备已连接到给定的 I2C 总线,则可以通过简单地使用设备地址和驱动程序名称填充 i2c_board_info 结构并调用 i2c_new_client_device() 来实例化该设备。这将创建该设备,然后驱动程序核心将负责查找正确的驱动程序并调用其 probe() 方法。如果驱动程序支持不同的设备类型,则可以使用 type 字段指定所需的类型。如果需要,您还可以指定 IRQ 和平台数据。

有时您知道设备已连接到给定的 I2C 总线,但您不知道它使用的确切地址。例如,在电视适配器上会发生这种情况,其中同一个驱动程序支持数十个略有不同的型号,并且 I2C 设备地址因型号而异。在这种情况下,您可以使用 i2c_new_scanned_device() 变体,它类似于 i2c_new_client_device(),不同之处在于它接受要探测的可能 I2C 地址的附加列表。将为列表中第一个响应地址创建一个设备。如果您希望在地址范围内存在多个设备,只需多次调用 i2c_new_scanned_device()。

i2c_new_client_device() 或 i2c_new_scanned_device() 的调用通常发生在 I2C 总线驱动程序中。您可能需要保存返回的 i2c_client 引用以供以后使用。

设备检测

设备检测机制存在许多缺点。您需要一些可靠的方法来识别支持的设备(通常使用特定于设备的专用识别寄存器),否则很可能发生误检,并且事情可能会很快出错。请记住,I2C 协议不包括任何用于检测给定地址处是否存在芯片的标准方法,更不用说识别设备的标准方法。更糟糕的是,缺乏与总线传输相关的语义,这意味着相同的传输可以被一个芯片视为读取操作,而被另一个芯片视为写入操作。由于这些原因,设备检测被认为是遗留机制,不应在新代码中使用。

设备删除

可以使用 i2c_new_client_device() 或 i2c_new_scanned_device() 创建的每个 I2C 设备都可以通过调用 i2c_unregister_device() 来注销。如果您不显式调用它,则在删除底层 I2C 总线本身之前会自动调用它,因为设备在设备驱动程序模型中无法在其父级中生存。

初始化驱动程序

当内核启动时,或者当您的 foo 驱动程序模块插入时,您必须进行一些初始化。幸运的是,通常只需注册驱动程序模块就足够了。

static int __init foo_init(void)
{
      return i2c_add_driver(&foo_driver);
}
module_init(foo_init);

static void __exit foo_cleanup(void)
{
      i2c_del_driver(&foo_driver);
}
module_exit(foo_cleanup);

The module_i2c_driver() macro can be used to reduce above code.

module_i2c_driver(foo_driver);

请注意,某些函数标有 __init。这些函数可以在内核启动(或模块加载)完成后删除。同样,当代码构建到内核中时,编译器会删除标有 __exit 的函数,因为它们永远不会被调用。

驱动程序信息

/* Substitute your own name and email address */
MODULE_AUTHOR("Frodo Looijaard <[email protected]>"
MODULE_DESCRIPTION("Driver for Barf Inc. Foo I2C devices");

/* a few non-GPL license types are also allowed */
MODULE_LICENSE("GPL");

电源管理

如果您的 I2C 设备在进入系统低功耗状态时需要特殊处理(例如将收发器置于低功耗模式或激活系统唤醒机制),请通过为驱动程序的 dev_pm_ops 实现适当的回调(例如暂停和恢复)来执行此操作。

这些是标准的驱动模型调用,它们的工作方式与其他任何驱动堆栈的调用方式相同。这些调用可以休眠,并且可以使用 I2C 消息与被挂起或恢复的设备进行通信(因为它们的父 I2C 适配器在发出这些调用时处于活动状态,并且 IRQ 仍然启用)。

系统关机

如果您的 I2C 设备在系统关闭或重启(包括 kexec)时需要特殊处理,例如关闭某些东西,请使用 shutdown() 方法。

同样,这是一个标准的驱动模型调用,其工作方式与其他任何驱动堆栈的调用方式相同:这些调用可以休眠,并且可以使用 I2C 消息传递。

命令函数

支持通用的类似 ioctl 的函数回调。您很少需要此功能,而且它的使用已被弃用,因此较新的设计不应使用它。

发送和接收

如果您想与您的设备通信,有几个函数可以做到这一点。您可以在 <linux/i2c.h> 中找到所有这些函数。

如果您可以在普通的 I2C 通信和 SMBus 级别通信之间进行选择,请使用后者。所有适配器都理解 SMBus 级别命令,但只有部分适配器理解普通的 I2C!

普通 I2C 通信

int i2c_master_send(struct i2c_client *client, const char *buf,
                    int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);

这些例程从客户端读取/写入一些字节。客户端包含 I2C 地址,因此您不必包含它。第二个参数包含要读取/写入的字节,第三个参数包含要读取/写入的字节数(必须小于缓冲区长度,也应小于 64k,因为 msg.len 是 u16)。返回的是实际读取/写入的字节数。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg,
                 int num);

这会发送一系列消息。每条消息可以是读取或写入,它们可以以任何方式混合。事务是组合的:在事务之间不会发出停止条件。i2c_msg 结构包含每条消息的客户端地址、消息的字节数和消息数据本身。

您可以阅读文件 I2C 协议 以获取有关实际 I2C 协议的更多信息。

SMBus 通信

s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr,
                   unsigned short flags, char read_write, u8 command,
                   int size, union i2c_smbus_data *data);

这是通用的 SMBus 函数。以下所有函数都是根据它实现的。永远不要直接使用此函数!

s32 i2c_smbus_read_byte(struct i2c_client *client);
s32 i2c_smbus_write_byte(struct i2c_client *client, u8 value);
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_byte_data(struct i2c_client *client,
                              u8 command, u8 value);
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_data(struct i2c_client *client,
                              u8 command, u16 value);
s32 i2c_smbus_read_block_data(struct i2c_client *client,
                              u8 command, u8 *values);
s32 i2c_smbus_write_block_data(struct i2c_client *client,
                               u8 command, u8 length, const u8 *values);
s32 i2c_smbus_read_i2c_block_data(struct i2c_client *client,
                                  u8 command, u8 length, u8 *values);
s32 i2c_smbus_write_i2c_block_data(struct i2c_client *client,
                                   u8 command, u8 length,
                                   const u8 *values);

这些函数已从 i2c-core 中删除,因为它们没有用户,但如果需要,以后可以添加回来。

s32 i2c_smbus_write_quick(struct i2c_client *client, u8 value);
s32 i2c_smbus_process_call(struct i2c_client *client,
                           u8 command, u16 value);
s32 i2c_smbus_block_process_call(struct i2c_client *client,
                                 u8 command, u8 length, u8 *values);

所有这些事务在失败时都会返回负的 errno 值。“写”事务在成功时返回 0;“读”事务返回读取的值,但块事务除外,块事务返回读取的值的数量。块缓冲区不需要超过 32 个字节。

您可以阅读文件 SMBus 协议 以获取有关实际 SMBus 协议的更多信息。

通用例程

以下列出了之前没有提到的所有通用例程

/* Return the adapter number for a specific adapter */
int i2c_adapter_id(struct i2c_adapter *adap);