Linux UIO驱动框架
1. 什么是uio驱动框架
uio全称为用户空间IO(Userspace I/O),是一种在用户空间编写设备驱动程序的框架。一般而言,Linux的驱动是运行在内核空间的,即设备驱动本身是作为内核源码的一部分进行编译的,这样的驱动程序能够访问系统的所有资源,但是稍有处理不当就容易引起内核奔溃。而uio驱动是在用户空间就行开发的,其本质就是一个应用开发,因此这类驱动就与内核空间隔离开,即使驱动奔溃也不会影响到整个系统。
对于驱动程序来说,一般有两个重要的功能:
- 读写某一段地址空间
- 响应外部中断
采用统一编址的处理器,实际上就是将处理器能够访问的地址空间划分一部分出来作为外设寄存器的地址,处理器在访问外设寄存器的时候,就可以通过地址来操作。响应外设中断是驱动程序必须的功能,这个就没什么好说的。
uio驱动就可以完成上述两个主要功能,那么这种应用场景在什么地方呢?我目前接触的应用场景就是使用zynq处理器。zynq处理器内部集成了一个arm核和fpag核,叫做ps端和pl端。这种架构增加了设计的灵活性,因为fpga可以作为一个外设挂载在arm的地址空间上的,想要实现什么外设功能fpag开发完成后,然后向arm提供这个外设寄存器的地址和中断就行,使用uio驱动就可以简化这类设备驱动的开发。
2. uio驱动框架使用
下图为uio驱动框架的整体框图,在内核空间uio驱动的内核代码部分会在/dev目录下生成uio设备节点,节点名称为/dev/uiox,其中x从0开始递增。同时,在sysfs文件下还会生成与uio设备相关的属性,读写这些属性文件就可以获得该uio设备的相关信息。
关于具体的uio设备操作方法,本文不会涉及后续文章进行补充。本文主要介绍两个操作函数read
和write
,因为这两个函数可以帮助理解uio内核驱动框架的实现。
操作uio设备的简单流程为:
- 调用open函数打开uio设备节点/dev/uio0,得到文件描述符fd
- 调用write向uio设备写1打开中断,写0会关闭中断
- 调用read函数读uio设备,此时会阻塞等待uio设备中断,若产生中断则会继续执行
上面三个步骤就实现了uio设备的中断响应功能,下面将将介绍内核实现这些过程的原理,然后就能理解uio设备框架到底做了什么。
3. uio驱动框架实现原理
设备驱动模型整体上可分为三层:
- 应用层:应用程序调用设备操作接口
open
、read
、write
等来操作设备 - 核心层:实现字符设备注册那一套操作
- 驱动相关层:定义设备对象、实现设备操作函数、调用设备对象的注册函数进行注册
在应用层,就是调用驱动框架提供的设备标准操作函数。核心层为驱动框架的主要部分,主要是字符设备注册的那一套流程。驱动相关层就是具体设备的的驱动了,开发也较为简单只要实现相关的操作函数,然后调用注册接口进行注册就完成了驱动开发。
要想分析内核驱动框架,首先需要找到uio内核驱动代码文件是哪一个,这个时候可以去设备树中找到uio设备节点,然后根据compatible
的值在驱动代码文件中进行搜索,设备树中uio设备节点列举如下:
/ {
chosen {
bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio";
stdout-path = "serial0:115200n8";
};
uio0@0 {
compatible = "generic-uio";
status = "okay";
interrupt-parent = <&intc>;
interrupts = <0 31 1>;
reg = <0x43c00000 0x140000>;
};
};
uio0
是自己在设备树中添加的节点,本人是在zynq上进行uio驱动开发的,其实采用什么平台都无所谓,原理都一样,你也可以在树莓派的设备树中添加上述节点。compatible
表明这个节点要匹配的驱动,interrupts
表明这个节点使用的中断信息<中断类型 中断号 中断触发方式>,在arm架构中通用中断控制器(gic)有三种类型的中断,分别为SPI、SGI和PPI。reg
属性则表明了这个uio设备使用的地址空间范围<内存起始地址 地址空间长度>。如果直接在内核中搜索"generic-uio"是搜不到任何结果的,这是因为uio驱动的compatible
属性是使用内核参数传递进去的,如节点chosen
。
bootargs = "uio_pdrv_genirq.of_id=generic-uio"
上述的内核参数就是向uio驱动传递的compatible
值,打开驱动源码uio_pdrv_genirq.c
有如下代码:
#ifdef CONFIG_OF
static struct of_device_id uio_of_genirq_match[] = {
{ /* This is filled with module_parm */ },
{ /* Sentinel */ },
};
MODULE_DEVICE_TABLE(of, uio_of_genirq_match);
module_param_string(of_id, uio_of_genirq_match[0].compatible, 128, 0);
MODULE_PARM_DESC(of_id, "Openfirmware id of the device to be handled by uio");
#endif
static struct platform_driver uio_pdrv_genirq = {
.probe = uio_pdrv_genirq_probe,
.remove = uio_pdrv_genirq_remove,
.driver = {
.name = DRIVER_NAME,
.pm = &uio_pdrv_genirq_dev_pm_ops,
.of_match_table = of_match_ptr(uio_of_genirq_match),
},
};
从上述代码可以看出uio驱动采用的是platform
框架,uio_of_genirq_match
就是匹配表,module_param_string(of_id, uio_of_genirq_match[0].compatible, 128, 0)
可以理解从传递的内核参数中填充匹配表,此时uio节点和uio驱动就能匹配成功,函数uio_pdrv_genirq_probe
将会执行。在该函数中主要会进行以下操作:
- 申请uio对象
- 初始化uio对象,即设备操作函数
- 注册uio对象
驱动框架的核心层的逻辑为:提供一个设备抽象数据结构,这个设备抽象数据结构包含一系列设备操作函数,这些函数需要驱动开发者实现,最后调用设备对象的注册函数进行注册。uio驱动框架对uio设备进行了抽象,使用struct uio_info
来描述,主要的数据成员如下:
struct uio_info {
struct uio_device *uio_dev;
const char *name;
irqreturn_t (*handler)(int irq, struct uio_info *dev_info);
int (*mmap)(struct uio_info *info, struct vm_area_struct *vma);
int (*open)(struct uio_info *info, struct inode *inode);
int (*release)(struct uio_info *info, struct inode *inode);
int (*irqcontrol)(struct uio_info *info, s32 irq_on);
};
驱动开发者需要做的就是定义一个uio_info
,实现其中的函数指针,然后注册,正如uio_pdrv_genirq_probe
实现的这样:
uio_pdrv_genirq_probe
/* 1. 申请uioinfo对象 */
uioinfo = devm_kzalloc(&pdev->dev, sizeof(*uioinfo), GFP_KERNEL);
/* 2. 初始化uio设备操作函数 */
uioinfo->handler = uio_pdrv_genirq_handler;
uioinfo->irqcontrol = uio_pdrv_genirq_irqcontrol;
uioinfo->open = uio_pdrv_genirq_open;
uioinfo->release = uio_pdrv_genirq_release;
/* 3. 注册uioinfo */
uio_register_device(&pdev->dev, priv->uioinfo);
可见使用驱动框架开发驱动程序还是比较简单,实现设备的操作函数然后注册就行了。那么对于linux驱动框架来说,框架将简单的部分暴漏给了驱动开发者,那复杂的部分就是由框架的核心层来完成的。
从上面的驱动相关层开发可以引出几个问题:
- 驱动相关层只是用了接口
uio_register_device
,/dev/uioX目录下的设备节点怎么生成的? - uio设备是一个字符设备,应用程序的open、read、write函数对应的是
file_operations
中的函数,我们并没有实现file_operations
,这个在哪儿实现的? file_operations
中的函数是怎么跟uio_info
中的函数联系起来的?
首先看看uio_register_device
这个函数,因为这个是自己编写的驱动与框架核心层的接口,其调用流程:
uio_register_device
1. init_waitqueue_head(&idev->wait);
2. device_create(&uio_class, parent,MKDEV(uio_major, idev->minor), idev,"uio%d", idev->minor);
3. request_irq(info->irq, uio_interrupt, info->irq_flags, info->name, idev);
可以看见uio_register_device
函数里首先初始化了一个等待队列,然后就看到了熟悉的与字符设备相关的函数device_create
,这个函数在字符设备开发中是用于自动生成设备节点的,前面说过uio设备节点名称为/dev/uioX,就是这个函数定义的。最后,就是给这个uio设备申请一个中断,并注册中断处理函数。
从这个函数中又引起了几个问题:
- 自动生成设备节点需要创建类和设备,类是在哪里创建的?
- uio的字符设备是在哪里添加的?
上面这两个步骤是在系统初始化的时候已经做好了,uio框架有如下代码:
static int __init uio_init(void)
{
return init_uio_class();
}
static void __exit uio_exit(void)
{
release_uio_class();
idr_destroy(&uio_idr);
}
module_init(uio_init);
module_exit(uio_exit);
在函数init_uio_class
中就完成了uio设备的字符设备的注册,以及类的创建,调用流程如下:
uio_init
1. init_uio_class
uio_major_init
1. cdev = cdev_alloc
2. cdev->ops = &uio_fops
3. cdev_add
2. class_register(&uio_class)
从上面的流程就可以看出,uio核心层完成了字符设备注册,以及类的创建。字符设备的file_operations
为uio_fops
,当应用程序调用read
、write
函数时就是调用的uio_fops->read
和uio_fops->write
。至此uio框架已经从上至下打通了,后续只要看看uio_fops
这里面的函数做了什么就行了。
static const struct file_operations uio_fops = {
.owner = THIS_MODULE,
.open = uio_open,
.release = uio_release,
.read = uio_read,
.write = uio_write,
.mmap = uio_mmap,
.poll = uio_poll,
.fasync = uio_fasync,
.llseek = noop_llseek,
};
在前文说过,调用write向uio设备写1打开中断,写0会关闭中断。调用read函数读uio设备,此时会阻塞等待uio设备中断。uio_write
的调用流程:
uio_write
idev->info->irqcontrol(idev->info, irq_on);
可见,write
最终会调用在前文驱动相关层定义的uioinfo->irqcontrol
函数,即uio_pdrv_genirq_irqcontrol
。这个函数就是根据写入的值使能或禁止uio设备中断:
uio_pdrv_genirq_irqcontrol
if (irq_on) {
if (__test_and_clear_bit(UIO_IRQ_DISABLED, &priv->flags))
enable_irq(dev_info->irq);
} else {
if (!__test_and_set_bit(UIO_IRQ_DISABLED, &priv->flags))
disable_irq_nosync(dev_info->irq);
}
uio_read
的调用流程:
uio_read
add_wait_queue(&idev->wait, &wait);
调用read
函数会将当前线程加入等待队列,实现线程的阻塞,那么唤醒线程的操作肯定就是在uio设备中断里了,这样就实现了read
阻塞等待中断,中断产生继续执行的功能了。在调用注册接口uio_register_device
已经注册了中断,中断处理函数为uio_interrupt
:
uio_interrupt
uio_event_notify
1. wake_up_interruptible(&idev->wait);
2. kill_fasync(&idev->async_queue, SIGIO, POLL_IN);
可见,跟猜想的一样,在中断中唤醒线程并且发了异步通知。至此,这个uio框架的核心逻辑已经介绍清楚了,uio驱动的其他功能实现逻辑也是相同的,在此不过多介绍。下图为uio框架的整体框图,应该更为清晰。