飞利浦(现恩智浦)发明的 I2C 总线是双线:串行数据(SDA)串行时钟 ( SCL )异步串行总线。这是一个多主总线,虽然多主模式没有广泛使用。SDA 和 SCL 都是开漏/集电极开路,这意味着它们都可以将其输出驱动为低电平,但如果没有上拉电阻,它们都不能将其输出驱动为高电平。SCL 由主机生成,以便同步总线上的数据(由 SDA 承载)传输。从机和主机都可以发送数据(当然不是同时发送),从而使 SDA 成为双向线路。也就是说,SCL 信号也是双向的,因为从机可以通过保持 SCL 线为低电平来延长时钟。总线由主控器控制,在我们的例子中,主控器是 SoC 的一部分。该总线常用于嵌入式系统中连接串行 EEPROM、RTC 芯片、GPIO 扩展器、温度传感器等:
I2C bus and devices
时钟速度从 10 千赫到 100 千赫不等,从 400 千赫到 2 兆赫不等。在这本书里,我们将不涉及总线规范或总线驱动。然而,管理总线和处理规范是由总线驱动决定的。在内核源代码中的drivers/i2C/busses/i2c-imx.c
处可以找到 i.MX6 芯片的总线驱动示例,在http://www.nxp.com/documents/user_manual/UM10204.pdf处可以找到 I2C 规范。
在本章中,我们对客户端驱动感兴趣,以便处理总线上的从属设备。本章将涵盖以下主题:
- I2C 客户端驱动架构
- 访问设备,从而从设备读取数据/向设备写入数据
- 从 DT 声明客户端
当您为其编写驱动的设备在名为总线控制器的物理总线上就座时,它必须依赖名为控制器驱动的总线驱动,该驱动负责在设备之间共享总线访问。控制器驱动在设备和总线之间提供了一个抽象层。例如,每当您在 I2C 或通用串行总线上执行事务(读或写)时,I2C/通用串行总线控制器都会在后台透明地处理该事务。每个总线控制器驱动都会导出一组函数,以简化总线上设备驱动的开发。这适用于所有物理总线(I2C、SPI、USB、PCI、SDIO 等)。
I2C 驱动在内核中被表示为struct i2c_driver
的一个实例。I2C 客户端(代表设备本身)由struct i2c_client
结构表示。
I2C 驱动在内核中被声明为struct i2c_driver,
的一个实例,如下所示:
struct i2c_driver {
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
struct i2c_driver
结构包含并描述了一般的访问例程,这些例程是处理需要驱动的设备所需要的,而struct i2c_client
包含设备特定的信息,比如它的地址。一个struct i2c_client
结构代表并描述了一个 I2C 装置。在本章的后面,我们将看到如何填充这些结构。
probe()
功能是struct i2c_driver
结构的一部分,一旦 I2C 设备被实例化,该功能就会随时执行。它负责以下任务:
- 检查设备是否是您期望的设备
- 使用
i2c_check_functionality
功能检查 SoC 的 I2C 总线控制器是否支持设备所需的功能 - 初始化设备
- 设置设备特定数据
- 注册适当的内核框架
probe
功能的原型如下:
static int foo_probe(struct i2c_client *client, const struct
i2c_device_id *id)
如您所见,它的参数是:
struct i2c_client
指针:这代表 I2C 设备本身。这个结构继承自结构设备,由内核提供给你的probe
函数。客户结构在include/linux/i2c.h
中定义。其定义如下:
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct device dev; /* the device structure */
intirq; /* irq issued by device */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
- 所有字段都由内核根据您为注册客户端提供的参数来填充。稍后我们将看到如何向内核注册设备。
struct i2c_device_id
指针:指向与被探测设备匹配的 I2C 设备标识条目。
I2C 内核为您提供了将指向您选择的任何数据结构的指针存储为设备特定数据的可能性。要存储或检索数据,请使用 I2C 核心提供的以下功能:
/* set the data */
void i2c_set_clientdata(struct i2c_client *client, void *data);
/* get the data */
void *i2c_get_clientdata(const struct i2c_client *client);
这些函数在内部调用dev_set_drvdata
和dev_get_drvdata
来更新或获取struct i2c_client
结构中struct device
子结构的void *driver_data
字段的值。
这是一个如何使用额外客户端数据的示例;drivers/gpio/gpio-mc9s08dz60.c:
节选
/* This is the device specific data structure */
struct mc9s08dz60 {
struct i2c_client *client;
struct gpio_chip chip;
};
static int mc9s08dz60_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mc9s08dz60 *mc9s;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
mc9s = devm_kzalloc(&client->dev, sizeof(*mc9s), GFP_KERNEL);
if (!mc9s)
return -ENOMEM;
[...]
mc9s->client = client;
i2c_set_clientdata(client, mc9s);
return gpiochip_add(&mc9s->chip);
}
实际上,这些功能并不真正针对 I2C。他们除了获取/设置struct device
成员的void *driver_data
指针,本身就是struct i2c_client
的成员之外什么都不做。事实上,我们可以直接使用dev_get_drvdata
和dev_set_drvdata
。在linux/include/linux/i2c.h
中可以看到它们的定义。
remove
功能的原型如下:
static int foo_remove(struct i2c_client *client)
remove()
功能也提供了与probe()
功能相同的struct i2c_client*
,所以你可以检索你的私人数据。例如,根据您在probe
功能中设置的私人数据,您可能需要进行一些清洁或任何其他工作:
static int mc9s08dz60_remove(struct i2c_client *client)
{
struct mc9s08dz60 *mc9s;
/* We retrieve our private data */
mc9s = i2c_get_clientdata(client);
/* Wich hold gpiochip we want to work on */
return gpiochip_remove(&mc9s->chip);
}
remove
功能负责从我们在probe()
功能中注册的子系统中注销我们。在前面的例子中,我们只是从内核中移除gpiochip
。
当一个模块被加载时,可能需要进行一些初始化。大多数时候,只需向 I2C 核心注册驱动就足够了。与此同时,当模块卸载时,我们通常只需要从 I2C 堆芯中出来。在第 5 章、平台设备驱动中,我们看到用 init/exit 函数来打扰自己是不值得的,而应该用module_*_driver
函数来代替。在这种情况下,要使用的函数是:
module_i2c_driver(foo_driver);
正如我们在匹配机制中看到的,我们需要提供一个device_id
数组,以便公开我们的驱动可以管理的设备。既然我们谈论的是 I2C 设备,结构应该是i2c_device_id
。该阵列将通知内核我们感兴趣的设备,即驱动。
现在回到我们的 I2C 设备驱动;在include/linux/mod_devicetable.h
中看一看,你会看到struct i2c_device_id
是如何定义的:
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_tdriver_data; /* Data private to the driver */
};
也就是说,struct i2c_device_id
必须嵌入一个struct i2c_driver
。为了让 I2C 核心(用于模块自动加载)知道我们需要处理的设备,我们必须使用MODULE_DEVICE_TABLE
宏。内核必须知道每当匹配发生时调用哪个probe
或remove
函数,这就是为什么我们的probe
和remove
函数也必须嵌入到相同的i2c_driver
结构中:
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",
},
.id_table = foo_idtable,
.probe = foo_probe,
.remove = foo_remove,
}
串行总线事务只是访问寄存器以设置/获取其内容的问题。I2C 尊重这一原则。I2C 核心提供了两种应用编程接口,一种用于普通 I2C 通信,另一种用于 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 通信功能都以一个struct i2c_client
作为第一参数。第二个参数包含要读取或写入的字节,第三个参数表示要读取或写入的字节数。像任何读/写函数一样,返回值是正在读/写的字节数。还可以通过以下方式处理消息传输:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg,
int num);
i2c_transfer
发送一组消息,每个消息可以是读操作或写操作,并且可以以任何方式混合。请记住,每个事务之间没有停止位。查看include/uapi/linux/i2c.h
,消息结构如下:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags; /* Message flags */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
i2c_msg
结构描述并表征了 I2C 信息。对于每条消息,它必须包含客户端地址、消息的字节数和消息有效负载。
msg.len
is a u16
. It means you must always be less than 216 (64k) with your read/write buffers.
让我们来看看微芯片 I2C 24ls 512 EEPROM 字符驱动的read
功能;我们应该理解事情是如何运作的。这本书的源代码提供了完整的代码。
ssize_t
eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
[...]
int _reg_addr = dev->current_pointer;
u8 reg_addr[2];
reg_addr[0] = (u8)(_reg_addr>> 8);
reg_addr[1] = (u8)(_reg_addr& 0xFF);
struct i2c_msg msg[2];
msg[0].addr = dev->client->addr;
msg[0].flags = 0; /* Write */
msg[0].len = 2; /* Address is 2bytes coded */
msg[0].buf = reg_addr;
msg[1].addr = dev->client->addr;
msg[1].flags = I2C_M_RD; /* We need to read */
msg[1].len = count;
msg[1].buf = dev->data;
if (i2c_transfer(dev->client->adapter, msg, 2) < 0)
pr_err("ee24lc512: i2c_transfer failed\n");
if (copy_to_user(buf, dev->data, count) != 0) {
retval = -EIO;
goto end_read;
}
[...]
}
对于读事务,应该是I2C_M_RD
,对于写事务,应该是0
。有时候,你可能不想创建struct i2c_msg
而只是处理简单的读写。
SMBus 是英特尔开发的双线总线,与 I2C 非常相似。I2C 设备与 SMBus 兼容,但不是相反。因此,如果对正在为其编写驱动的芯片有疑问,最好使用 SMBus 功能。
下面显示了一些 SMBus 应用编程接口:
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);
查看内核源码中的include/linux/i2c.h
和drivers/i2c/i2c-core.c
获得更多解释。
以下示例显示了 I2C gpio 扩展器中的简单读/写操作:
struct mcp23016 {
struct i2c_client *client;
structgpio_chip chip;
structmutex lock;
};
[...]
/* This function is called when one needs to change a gpio state */
static int mcp23016_set(struct mcp23016 *mcp,
unsigned offset, intval)
{
s32 value;
unsigned bank = offset / 8 ;
u8 reg_gpio = (bank == 0) ? GP0 : GP1;
unsigned bit = offset % 8 ;
value = i2c_smbus_read_byte_data(mcp->client, reg_gpio);
if (value >= 0) {
if (val)
value |= 1 << bit;
else
value &= ~(1 << bit);
return i2c_smbus_write_byte_data(mcp->client,
reg_gpio, value);
} else
return value;
}
[...]
我们必须通知内核系统上物理存在哪些设备。有两种方法可以实现这一点。在 DT 中,正如我们将在本章后面看到的,或者通过板配置文件(这是旧的和折旧的方式)。让我们看看如何在电路板配置文件中做到这一点:
struct i2c_board_info
是用于表示我们板上的 I2C 设备的结构。结构定义如下:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short addr;
void *platform_data;
int irq;
};
同样,与我们无关的元素已经从结构中移除。
在前面的结构中,type
应该包含与设备驱动在i2c_driver.driver.name
字段中定义的值相同的值。然后,您需要填充一个i2c_board_info
数组,并将其作为参数传递给电路板初始化例程中的i2c_register_board_info
函数:
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len)
这里,busnum
是设备所在的总线号。这是一种古老的折旧方法,所以我在这本书里不再赘述。请随意查看内核源代码中的文档/I2C/实例化设备,看看事情是如何进行的。
正如我们在前面几节中看到的,为了配置 I2C 设备,基本上有两个步骤:
- 定义并注册 I2C 驱动
- 定义和注册 I2C 设备
I2C 设备属于 DT 中的非内存映射设备家族,而 I2C 总线是可寻址总线(通过可寻址,我的意思是您可以寻址总线上的特定设备)。在这种情况下,设备节点中的reg
属性表示总线上的设备地址。
I2C 设备节点都是它们所在的总线节点的子节点。每个设备只分配一个地址。不涉及长度或范围。需要为 I2C 设备声明的标准属性是reg
,表示设备在总线上的地址,以及compatible
字符串,用于将设备与驱动匹配。关于寻址的更多信息,可以参考第六章、设备树概念。
&i2c2 { /* Phandle of the bus node */
pcf8523: rtc@68 {
compatible = "nxp,pcf8523";
reg = <0x68>;
};
eeprom: ee24lc512@55 { /* eeprom device */
compatible = "packt,ee24lc512";
reg = <0x55>;
};
};
前面的示例声明了位于 SoC 的 I2C 2 号总线上的地址 0x50 处的 HDMI EDID 芯片,以及位于同一总线上的地址 0x68 处的实时时钟 ( RTC )。
到目前为止,我们所看到的并没有改变。我们额外需要的是定义一个struct of_device_id
。Struct of_device_id
定义为匹配.dts
文件中的相应节点:
/* no extra data for this device */
static const struct of_device_id foobar_of_match[] = {
{ .compatible = "packtpub,foobar-device" },
{}
};
MODULE_DEVICE_TABLE(of, foobar_of_match);
现在我们定义i2c_driver
如下:
static struct i2c_driver foo_driver = {
.driver = {
.name = "foo",
.of_match_table = of_match_ptr(foobar_of_match), /* Only this line is added */
},
.probe = foo_probe,
.id_table = foo_id,
};
然后可以这样改进probe
功能:
static int my_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
const struct of_device_id *match;
match = of_match_device(mcp23s08_i2c_of_match, &client->dev);
if (match) {
/* Device tree code goes here */
} else {
/*
* Platform data code comes here.
* One can use
* pdata = dev_get_platdata(&client->dev);
*
* or *id*, which is a pointer on the *i2c_device_id* entry that originated
* the match, in order to use *id->driver_data* to extract the device
* specific data, as described in platform driver chapter.
*/
}
[...]
}
对于早于 4.10 的内核版本,如果查看drivers/i2c/i2c-core.c
,在i2c_device_probe()
函数中(有关信息,它是内核每次将 I2C 设备注册到 I2C 内核时调用的函数),您会看到类似以下内容:
if (!driver->probe || !driver->id_table)
return -ENODEV;
这意味着即使不需要使用.id_table
,在驾驶员中也是强制性的。事实上,一个人只能使用 OF 匹配风格,但无法摆脱.id_table
。内核开发人员试图消除对.id_table
的需求,并专门使用.of_match_table
进行设备匹配。该补丁可在以下网址获得:https://git . kernel . org/cgit/Linux/kernel/git/Torvalds/Linux . git/commit/?id = c 80 f 52847 c 50109 ca 248 c 22 efbf 71 ff 10553 DCA 4。
然而,已经发现了回归,并且提交被恢复。详情请看这里:https://git . kernel . org/cgit/Linux/kernel/git/Torvalds/Linux . git/commit/?id = 661 F6 C1 CD 926 c6c 973 e 03 C6 b 5151d 161 F3 a 666 ed。这个问题在内核版本> = 4.10 后已经被修复。修复方法如下:
/*
* An I2C ID table is not mandatory, if and only if, a suitable Device
* Tree match table entry is supplied for the probing device.
*/
if (!driver->id_table &&
!i2c_of_match_device(dev->driver->of_match_table, client))
return -ENODEV;
换句话说,您必须为 I2C 驱动定义.id_table
和.of_match_table
,否则您的设备将不会被探测到内核版本 4.10 或更早版本。
struct i2c_client
是用来描述 I2C 装置的结构。然而,使用 OF 风格,这种结构不能在板文件中定义了。我们唯一需要做的就是在 DT 中提供设备的信息,内核将从中构建一个。
下面的代码展示了如何在一个dts
文件中声明我们的 I2C foobar
设备节点:
&i2c3 {
status = "okay";
foo-bar: foo@55 {
compatible = "packtpub,foobar-device";
reg = <55>;
};
};
要总结编写 I2C 客户端驱动所需的步骤,您需要:
-
声明驱动支持的设备 id。你可以使用
i2c_device_id
来完成。如果支持 DT,也可以使用of_device_id
。 -
拨打
MODULE_DEVICE_TABLE(i2c, my_id_table
向 I2C 核注册您的设备列表。如果支持设备树,您必须调用MODULE_DEVICE_TABLE(of, your_of_match_table)
向 OF 核心注册您的设备列表。 -
根据各自的原型写出
probe
和remove
函数。如果需要,也编写电源管理功能。probe
功能必须识别您的设备,对其进行配置,定义每设备(私有)数据,并向适当的内核框架注册。驾驶员的行为取决于您在probe
功能中做了什么。remove
功能必须撤销您在probe
功能中所做的一切(释放内存并从任何框架中注销)。 -
声明并填充一个
struct i2c_driver
结构,并用您创建的 id 数组设置id_table
字段。用上面写的相应函数的名称设置.probe
和.remove
字段。在。driver
子结构,设置.owner
字段为THIS_MODULE
,设置驱动名称,最后设置.of_match_table
字段,数组为of_device_id
如果支持 DT。 -
用你刚刚在上面填充的
i2c_driver
结构调用module_i2c_driver
函数:module_i2c_driver(serial_eeprom_i2c_driver)
,以便向内核注册你的驱动。
我们刚刚处理了 I2C 设备驱动。是时候让你挑选市场上的任何 I2C 设备,并编写相应的驱动了,支持 DT。本章讨论了内核 I2C 核心和相关的应用编程接口,包括设备树支持,为您提供与 I2C 设备对话的必要技能。您应该能够编写高效的probe
函数,并向内核 I2C 核注册。在下一章中,我们将使用我们在这里学到的技能来开发 SPI 设备驱动。