在用户空间实现 I2C 设备驱动程序

通常,I2C 设备由内核驱动程序控制。但是,也可以通过 /dev 接口从用户空间访问适配器上的所有设备。为此,您需要加载模块 i2c-dev。

每个注册的 I2C 适配器都有一个编号,从 0 开始计数。您可以检查 /sys/class/i2c-dev/ 以查看哪个编号对应于哪个适配器。或者,您可以运行 “i2cdetect -l” 来获取在给定时间您的系统上所有 I2C 适配器的格式化列表。 i2cdetect 是 i2c-tools 包的一部分。

I2C 设备文件是字符设备文件,主设备号为 89,次设备号对应于如上所述分配的编号。它们应被称为 “i2c-%d”(i2c-0、i2c-1、...、i2c-10、...)。所有 256 个次设备号都为 I2C 保留。

C 示例

假设您想从 C 程序访问 I2C 适配器。首先,您需要包含这两个头文件

#include <linux/i2c-dev.h>
#include <i2c/smbus.h>

现在,您必须决定要访问哪个适配器。您应该检查 /sys/class/i2c-dev/ 或运行 “i2cdetect -l” 来决定。适配器编号的分配有些动态,因此您不能对它们做出太多假设。它们甚至可能在两次启动之间发生变化。

接下来,打开设备文件,如下所示

int file;
int adapter_nr = 2; /* probably dynamically determined */
char filename[20];

snprintf(filename, 19, "/dev/i2c-%d", adapter_nr);
file = open(filename, O_RDWR);
if (file < 0) {
  /* ERROR HANDLING; you can check errno to see what went wrong */
  exit(1);
}

打开设备后,必须指定要与之通信的设备地址

int addr = 0x40; /* The I2C address */

if (ioctl(file, I2C_SLAVE, addr) < 0) {
  /* ERROR HANDLING; you can check errno to see what went wrong */
  exit(1);
}

好了,现在一切都设置好了。您现在可以使用 SMBus 命令或普通的 I2C 与您的设备进行通信。如果设备支持,则首选 SMBus 命令。下面说明了两者

__u8 reg = 0x10; /* Device register to access */
__s32 res;
char buf[10];

/* Using SMBus commands */
res = i2c_smbus_read_word_data(file, reg);
if (res < 0) {
  /* ERROR HANDLING: I2C transaction failed */
} else {
  /* res contains the read word */
}

/*
 * Using I2C Write, equivalent of
 * i2c_smbus_write_word_data(file, reg, 0x6543)
 */
buf[0] = reg;
buf[1] = 0x43;
buf[2] = 0x65;
if (write(file, buf, 3) != 3) {
  /* ERROR HANDLING: I2C transaction failed */
}

/* Using I2C Read, equivalent of i2c_smbus_read_byte(file) */
if (read(file, buf, 1) != 1) {
  /* ERROR HANDLING: I2C transaction failed */
} else {
  /* buf[0] contains the read byte */
}

请注意,只有 I2C 和 SMBus 协议的一个子集可以通过 read() 和 write() 调用来实现。特别是,所谓的组合事务(在同一事务中混合读写消息)不受支持。因此,用户空间程序几乎从不使用此接口。

重要提示:由于使用了内联函数,您在编译程序时必须使用“-O”或某些变体!

完整接口描述

定义了以下 IOCTL

ioctl(file, I2C_SLAVE, long addr)

更改从站地址。该地址在参数的低 7 位中传递(除了 10 位地址,在这种情况下在低 10 位中传递)。

ioctl(file, I2C_TENBIT, long select)

如果 select 不等于 0,则选择十位地址,如果 select 等于 0,则选择正常的 7 位地址。默认为 0。只有当适配器具有 I2C_FUNC_10BIT_ADDR 时,此请求才有效。

ioctl(file, I2C_PEC, long select)

如果 select 不等于 0,则选择 SMBus PEC(数据包错误检查)生成和验证,如果 select 等于 0,则禁用。默认为 0。仅用于 SMBus 事务。仅当适配器具有 I2C_FUNC_SMBUS_PEC 时,此请求才有效;即使没有,也是安全的,只是没有任何效果。

ioctl(file, I2C_FUNCS, unsigned long *funcs)

获取适配器功能并将其放入 *funcs

ioctl(file, I2C_RDWR, struct i2c_rdwr_ioctl_data *msgset)

执行组合的读/写事务,中间没有停止。仅当适配器具有 I2C_FUNC_I2C 时才有效。该参数是指向

struct i2c_rdwr_ioctl_data {
  struct i2c_msg *msgs;  /* ptr to array of simple messages */
  int nmsgs;             /* number of messages to exchange */
}

msgs[] 本身包含指向数据缓冲区的进一步指针。该函数将根据是否在特定消息中设置了 I2C_M_RD 标志,将数据写入或读取到这些缓冲区。从站地址以及是否使用十位地址模式必须在每条消息中设置,覆盖通过上述 ioctl 设置的值。

ioctl(file, I2C_SMBUS, struct i2c_smbus_ioctl_data *args)

如果可能,请使用下面提供的 i2c_smbus_* 方法,而不是发出直接 ioctl。

您可以通过使用 read(2) 和 write(2) 调用来执行普通的 I2C 事务。您不需要传递地址字节;而是,在尝试访问设备之前,通过 ioctl I2C_SLAVE 设置它。

您可以通过以下函数执行 SMBus 级别事务(有关详细信息,请参阅文档文件 SMBus 协议

__s32 i2c_smbus_write_quick(int file, __u8 value);
__s32 i2c_smbus_read_byte(int file);
__s32 i2c_smbus_write_byte(int file, __u8 value);
__s32 i2c_smbus_read_byte_data(int file, __u8 command);
__s32 i2c_smbus_write_byte_data(int file, __u8 command, __u8 value);
__s32 i2c_smbus_read_word_data(int file, __u8 command);
__s32 i2c_smbus_write_word_data(int file, __u8 command, __u16 value);
__s32 i2c_smbus_process_call(int file, __u8 command, __u16 value);
__s32 i2c_smbus_block_process_call(int file, __u8 command, __u8 length,
                                   __u8 *values);
__s32 i2c_smbus_read_block_data(int file, __u8 command, __u8 *values);
__s32 i2c_smbus_write_block_data(int file, __u8 command, __u8 length,
                                 __u8 *values);

所有这些事务在失败时返回 -1;您可以读取 errno 以查看发生了什么。 “write”事务在成功时返回 0;“read”事务返回读取的值,除了 read_block,它返回读取的值的数量。 块缓冲区不需要长于 32 个字节。

通过链接 libi2c 库,可以使用上述函数,该库由 i2c-tools 项目提供。 请参阅:https://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git/

实施细节

对于感兴趣的人,这里是当您使用 /dev 接口到 I2C 时,内核内部发生的代码流程

  1. 您的程序打开 /dev/i2c-N 并在其上调用 ioctl(),如上面的“C 示例”部分所述。

  2. 这些 open() 和 ioctl() 调用由 i2c-dev 内核驱动程序处理:请参阅 i2c-dev.c:i2cdev_open() 和 i2c-dev.c:i2cdev_ioctl()。 您可以将 i2c-dev 视为可以从用户空间编程的通用 I2C 芯片驱动程序。

  3. 一些 ioctl() 调用用于管理任务,并由 i2c-dev 直接处理。 示例包括 I2C_SLAVE(设置要访问的设备的地址)和 I2C_PEC(启用或禁用未来事务的 SMBus 错误检查)。

  4. 其他 ioctl() 调用由 i2c-dev 转换为内核函数调用。 示例包括 I2C_FUNCS,它使用 i2c.h:i2c_get_functionality() 查询 I2C 适配器功能,以及 I2C_SMBUS,它使用 i2c-core-smbus.c:i2c_smbus_xfer() 执行 SMBus 事务。

    i2c-dev 驱动程序负责检查来自用户空间的所有参数的有效性。 在此之后,从用户空间通过 i2c-dev 发出的这些调用与内核 I2C 芯片驱动程序直接执行的调用之间没有区别。 这意味着 I2C 总线驱动程序无需实现任何特殊功能即可支持从用户空间进行访问。

  5. 这些 i2c.h 函数是 I2C 总线驱动程序的实际实现的包装器。 每个适配器都必须声明实现这些标准调用的回调函数。 i2c.h:i2c_get_functionality() 调用 i2c_adapter.algo->functionality(),而 i2c-core-smbus.c:i2c_smbus_xfer() 调用 adapter.algo->smbus_xfer()(如果已实现),否则调用 i2c-core-smbus.c:i2c_smbus_xfer_emulated(),后者又调用 i2c_adapter.algo->master_xfer()。

在您的 I2C 总线驱动程序处理完这些请求后,执行沿着调用链向上运行,几乎没有进行任何处理,除了 i2c-dev 将返回的数据(如果有)以适合 ioctl 的格式打包。