在用户空间实现 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 */
}
请注意,通过 read() 和 write() 调用只能实现 I2C 和 SMBus 协议的一个子集。特别是,不支持所谓的组合事务(在同一事务中混合读取和写入消息)。因此,用户空间程序几乎从不使用此接口。
重要提示:由于使用了内联函数,您在编译程序时*必须*使用“-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 时内核内部发生的代码流程
您的程序打开 /dev/i2c-N 并对其调用 ioctl(),如上面的“C 示例”部分所述。
这些 open() 和 ioctl() 调用由 i2c-dev 内核驱动程序处理:分别参见 i2c-dev.c:i2cdev_open() 和 i2c-dev.c:i2cdev_ioctl()。您可以将 i2c-dev 视为可以从用户空间编程的通用 I2C 芯片驱动程序。
一些 ioctl() 调用用于管理任务,并由 i2c-dev 直接处理。示例包括 I2C_SLAVE(设置要访问的设备的地址)和 I2C_PEC(启用或禁用未来事务的 SMBus 错误检查。)
其他 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 总线驱动程序不需要实现任何特殊功能来支持从用户空间进行访问。
这些 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 的格式。