久等了各位!
本篇开始讲解 IO 虚拟化中的 virtio,我会以 Linux 的 IIC 驱动为例,从 IIC 驱动的非虚拟化实现,到 IIC 驱动的半虚拟化实现,再到最后 X-Hyper 中如何通过 virtio 来实现前后端联系,一步步把 virtio 讲清楚。所以我一共会分为4个子篇幅来介绍virtio,内容有点多,需要一点点消化。
本篇我们以 RK 板的 IIC 控制器为例,先讲解 Linux 下的 IIC 驱动框架。
设备树创建 i2c 设备的节点,在设备树遍历时会创建一个 i2c 的 platform 设备出来:
i2c@fdd40000 {
compatible = "rockchip,rk3399-i2c";
reg = <0x00 0xfdd40000 0x00 0x1000>;
clocks = <0x32 0x07 0x32 0x2d>;
clock-names = "i2c\0pclk";
interrupts = <0x00 0x2e 0x04>;
pinctrl-names = "default";
pinctrl-0 = <0x35>;
#address-cells = <0x01>;
#size-cells = <0x00>;
status = "okay";
phandle = <0x17a>;
}
设备包含如下信息:
- compatible:兼容性,用于匹配可以用于该设备的驱动;
- reg:该设备的寄存器基地址和范围;
- interrupts:i2c 中断控制器使用的中断配置;
这个 platform device 在被放入平台总线时,会匹配对应的 platform driver,那么先来看一下 platform bus 是什么:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.dma_configure = platform_dma_configure,
.pm = &platform_dev_pm_ops,
};
我们只关注其中的 match 函数,它是当一个新的设备或者一个新的驱动被添加到该总线时会被调用的匹配函数,当一个新的设备被加入时,使用 match 来匹配对应的驱动,当一个新的驱动被添加到该总线时,也会使用这个 match 来匹配设备。
我们以 rk 板的 i2c 平台总线驱动为例,首先我们找到 rk 板的 i2c 平台总线驱动:
static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe,
.remove = rk3x_i2c_remove,
.driver = {
.name = "rk3x-i2c",
.of_match_table = rk3x_i2c_match,
.pm = &rk3x_i2c_pm_ops,
},
};
使用of_match_table 来匹配对应的的设备,使用probe 来初始化设备。
整体流程如下:
在深入分析 IIC 的驱动代码前,先简单看一下 IIC 的整个数据发送和接受流程,这里不会涉及底层硬件的时序,需要读者自己去学习。
IIC 主机向从机写数据:
IIC 主机向从机读数据:
当然IIC的发送接收不止上述两种模式,这里只讨论常用的两种发送和接收数据的方法。
然后我们开始分析 IIC platform driver 的代码了:
我们以 RK board 为例,其总线驱动代码在:drivers\i2c\busses\i2c-rk3x.c 中,具体实现可以参考我之前的文章,这里只给出一个整体框图。最终 i2c 的控制器也会以字符设备节点暴露给用户态,我们可以通过 i2c 控制器的字符设备给相应的 i2c 外设通信。
然后看一下用户层打开一个 i2c 控制器对应的字符设备节点的整体流程图:
整个流程如下:
当应用程序打开一个设备文件时,通过系统调用 sys_open 进入内核,在内核空间中由 do_sys_open 负责发起整个设备文件的打开操作,首先获得该设备文件所对应的 inode,然后调用其中的 i_fop 函数,对字符设备而言,i_fop 函数就是 chrdev_open,后者通过 inode 中的 i_rdev 成员在 cdev_map 中查找该设备所对应的设备对象 cdev,在成功找到了该设备对象后,将 inode 的 cdev 成员指向该字符设备对象,这样下次再对该设备文件节点进行打开操作时,就可以直接通过 i_cdev 成员得到设备节点所对应的字符设备对象了。内核在每次打开一个设备文件时,会产生一个整形的文件描述符 fd 和一个新的 struct file 对象 filp 来跟踪对该文件的这一次操作,在打开设备文件时,内核会将 filp 和 fd 关联起来,同时会将 cdev 中的 ops 赋值给 filp->f_op,同时创建 i2c_client,关联 i2c_adapter,并将 filp 的 private_data 和 i2_client 关联起来。最后 sys_open 系统调用将设备文件描述符 fd 返回到用户空间。
接下来用一个实际的例子去理解一下 IIC 的完成一次 Combined R/W 的流程:
用户侧示例代码如下:
int main(void)
{
int fd = 0;
int ret = 0;
const char *path_name ="/dev/i2c-0";
uint8_t buf[8] = {0};
uint8_t start_reg = 0x0;
struct i2c_msg read_msg[2] = {
{
0x20, /* slave addr */
0, /* operate flags */
1, /* data len */
&start_reg /* data buf */
},
{
0x20, /* slave addr */
I2C_M_RD, /* operate flags */
8, /* data len */
&buf[0] /* data buf */
},
};
struct i2c_rdwr_ioctl_data rdwr = {
.msgs = read_msg,
.nmsgs = 2
};
fd = open(path_name, O_RDWR);
ret = ioctl(fd, I2C_SLAVE_FORCE, 0x20);
ret = ioctl(fd, I2C_RDWR, (unsigned long)&rdwr);
return 0;
}
这是一次 Combined 的从机数据读取操作,由两部分i2c_msg 组成,第一个 msg 向从机写设备寄存器地址,表示要读取的设备寄存器,然后再发送读数据请求,向从机请求 8 个字节的 data。
对应整个 IIC 的协议段如下:
然后我们通过 ioctl 进入内核,并最终调用 i2cdev_fops->i2cdev_ioctl。
static const struct file_operations i2cdev_fops = {
...
.compat_ioctl = compat_i2cdev_ioctl,
...
};
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct i2c_client *client = file->private_data;
...
switch (cmd) {
...
case I2C_RDWR: {
...
return i2cdev_ioctl_rdwr(client, rdwr_arg.nmsgs, rdwr_pa);
}
...
return 0;
}
static noinline int i2cdev_ioctl_rdwr(struct i2c_client *client,
unsigned nmsgs, struct i2c_msg *msgs)
{
...
res = i2c_transfer(client->adapter, msgs, nmsgs);
...
return res;
}
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
...
ret = __i2c_transfer(adap, msgs, num);
return ret;
}
int __i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
...
ret = adap->algo->master_xfer(adap, msgs, num);
...
return ret;
}
可以看到最后就是调用了i2c_adapter 中的master_xfer。
static int rk3x_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
struct rk3x_i2c *i2c = (struct rk3x_i2c *)adap->algo_data;
...
for (i = 0; i < num; i += ret) {
ret = rk3x_i2c_setup(i2c, msgs + i, num - i);
...
rk3x_i2c_start(i2c);
...
}
...
}
rk3x_i2c_xfer 中初始化MRXADDR 和 MRXRADDR 寄存器并初始化 i2c 的初始化状态机状态。然后通过rk3x_i2c_start 发送 start 信号开始 i2c 的整个流程,并在rk3x_i2c_irq 中维护整个 i2c 数据发送和接受的状态机:
static irqreturn_t rk3x_i2c_irq(int irqno, void *dev_id)
{
...
switch (i2c->state) {
case STATE_START:
rk3x_i2c_handle_start(i2c, ipd);
break;
case STATE_WRITE:
rk3x_i2c_handle_write(i2c, ipd);
break;
case STATE_READ:
rk3x_i2c_handle_read(i2c, ipd);
break;
case STATE_STOP:
rk3x_i2c_handle_stop(i2c, ipd);
break;
case STATE_IDLE:
break;
}
...
}
在 Combined W/R 模式下的流程图和状态机如下所示:
流程图:
IIC 驱动状态机:
到这里我们简单的介绍了Linux下的IIC驱动框架和其工作流程,如果要深入理解还需自己阅读驱动代码,下一篇幅将介绍IIC半虚拟化的Virtio前端驱动。