Linux UIO驱动框架(一)

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设备操作方法,本文不会涉及后续文章进行补充。本文主要介绍两个操作函数readwrite,因为这两个函数可以帮助理解uio内核驱动框架的实现。

操作uio设备的简单流程为:

  • 调用open函数打开uio设备节点/dev/uio0,得到文件描述符fd
  • 调用write向uio设备写1打开中断,写0会关闭中断
  • 调用read函数读uio设备,此时会阻塞等待uio设备中断,若产生中断则会继续执行

上面三个步骤就实现了uio设备的中断响应功能,下面将将介绍内核实现这些过程的原理,然后就能理解uio设备框架到底做了什么。

3. uio驱动框架实现原理

设备驱动模型整体上可分为三层:

  • 应用层:应用程序调用设备操作接口openreadwrite等来操作设备
  • 核心层:实现字符设备注册那一套操作
  • 驱动相关层:定义设备对象、实现设备操作函数、调用设备对象的注册函数进行注册

在应用层,就是调用驱动框架提供的设备标准操作函数。核心层为驱动框架的主要部分,主要是字符设备注册的那一套流程。驱动相关层就是具体设备的的驱动了,开发也较为简单只要实现相关的操作函数,然后调用注册接口进行注册就完成了驱动开发。

要想分析内核驱动框架,首先需要找到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将会执行。在该函数中主要会进行以下操作:

  1. 申请uio对象
  2. 初始化uio对象,即设备操作函数
  3. 注册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_operationsuio_fops,当应用程序调用readwrite函数时就是调用的uio_fops->readuio_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框架的整体框图,应该更为清晰。
在这里插入图片描述

  • 10
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值