• 周四. 8月 11th, 2022

5G编程聚合网

5G时代下一个聚合的编程学习网

热门标签

实现I2C设备驱动程序

admin

11月 28, 2021

对于那些想要为I2C或SMBus设备编写内核驱动程序,使用Linux作为协议host/master(而不是slave)的人来说,这是一个小指南。

要设置一个驱动程序,您需要做几件事。有些是可选的,有些事情可以做得稍微不同或完全不同。把它作为指导,而不是规则手册!

备注

尽量保持内核名称空间的整洁。最好的方法是为所有全局符号使用唯一的前缀。这对于导出的符号特别重要,但是对于未导出的符号也应该这样做。在本例中,我们将使用前缀foo_。

驱动程序结构

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

static 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,
      /* if device autodetection is needed: */
      .class          = I2C_CLASS_SOMETHING,
      .detect         = foo_detect,
      .address_list   = normal_i2c,

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

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

所有其他字段用于回调函数,下面将对此进行解释。

额外的client数据

每个客户机结构都有一个特殊的 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()失败将数据字段设置为NULL。在这些情况下,i2c-core会自动做到这一点。这也是核心接触这个字段的唯一时间。

访问 client

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

我发现为此定义 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);
}

Probing(探测) and attaching

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

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

设备/驱动程序绑定

系统基础设施(通常是特定于板的初始化代码或引导固件)报告存在什么I2C设备。如,在内核中或从引导加载程序中可能有一个表,它标识I2C设备并将它们连接到关于IRQs和其他布线构件、芯片类型等特定于板的配置信息。可用于为每个I2C设备创建i2c_client对象。

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

static int foo_probe(struct i2c_client *client,
                     const struct i2c_device_id *id);
static int foo_remove(struct i2c_client *client);

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

当 id_table.name 字段中的条目与设备名称匹配时,将调用probe函数。它传递了匹配的条目,这样驱动程序就知道表中哪一个匹配。

设备创建

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

struct i2c_client * i2c_new_client_device(struct i2c_adapter *adap, struct i2c_board_info const *info);

有时,您知道一个设备连接到给定的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总线。这是以PC的SMBus上的硬件监控设备为例。在这种情况下,您可能想让您的驱动程序自动检测支持的设备。这就是遗留模型的工作方式,现在可以作为标准驱动模型的扩展。

你只需要定义一个检测回调来尝试识别支持的设备(支持的返回0,不支持的返回-ENODEV),要探测的地址列表和设备类型(或类),以便只有连接了该类型设备的I2C总线(没有被列举出来)才会被探测。例如,需要自动检测的硬件监控芯片的驱动程序将其类设置为I2C_CLASS_HWMON,并且只有包含I2C_CLASS_HWMON的类的I2C适配器才会被该驱动程序探测。请注意,没有匹配类并不会阻止在给定的I2C适配器上使用该类型的设备。它只能防止自动检测;设备的显式实例化仍然是可能的。

请注意,这种机制纯粹是可选的,并不适用于所有设备。您需要一些可靠的方法来识别所支持的设备(通常使用设备特定的专用标识寄存器),否则,可能会出现错误检测,事情可能很快就会出错。请记住,I2C协议不包括任何检测给定地址上芯片存在的标准方法,更不用说识别设备的标准方法了。更糟糕的是缺乏与总线传输相关的语义,这意味着相同的传输可以被看作是一个芯片的读操作和另一个芯片的写操作。由于这些原因,在可能的情况下,显式设备实例化总是优先于自动检测。

设备删除

使用i2c_new_client_device()或i2c_new_scanned_device()创建的每个I2C设备都可以通过调用i2c_unregister_device()来取消注册。如果您不显式地调用它,它将在底层I2C总线本身被删除之前被自动调用,因为设备不能在设备驱动程序模型中的父设备中存活。

void i2c_unregister_device(struct i2c_client *client);

初始化驱动程序

当内核被引导时,或者当你的foo driver模块被插入时,你必须做一些初始化。幸运的是,通常只需注册驱动程序模块就足够了。

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适配器是活动的,并且IRQs仍然是启用的)。

System Shutdown

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

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

Command功能

支持类似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-protocol 以获得关于实际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-protocol 文件中获取关于实际SMBus协议的更多信息。

通用程序

下面列出了所有以前没有提到的通用程序:

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

本文来自博客园,作者:王楼小子,转载请注明原文链接:https://www.cnblogs.com/wanglouxiaozi/p/15153549.html

发表评论

您的电子邮箱地址不会被公开。