Linux驱动

一、Linux内核划分

1、进程管理

​ 内核负责创建和销毁进程,并且处理他们与外部世界的联系。不同进程之间的通讯对于整个系统功能来说是基本的,由内核处理。调度器,控制进程如何共享CPU,也是进程管理的一部分。内核的进程管理实现了多个进程在单一CPU或者多CPU上的抽象。

2、内存管理

​ 内核为所有进程在有限的可用资源上建立了一个虚拟地址空间,内核的不同部分与内存管理子系统通过一套函数调用交互,从简单的malloc/free到更多更复杂的功能

3、文件系统

几乎Unix中的任何东西都可以看作是一个文件,内核在非结构化的硬件上建立了一个结构化的文件系统,结果是文件的抽象非常多的在整个系统中应用。

4、设备控制

几乎每个操作系统最终都映射到一个物理设备上,除了处理器,内存和非常少的别的实体之外,全部的设备控制操作都由特定的要寻址的设备相关代码来进行。这些代码就叫做设备驱动。

5、网络

网络必须由操作系统来管理,大部分网络操作不是特定于某一进程,进入系统的报文时异步事件,报文在某一个进程接受之前必须被收集、识别、分发。系统负责在程序和网络接口之间递送数据报文,他必须根据程序的网络活动来控制程序的执行,另外,所有的路由和地址解析问题都在内核中实现。

二、用户空间和内核空间

驱动在内核空间运行,应用程序在用户空间运行。操作系统的角色实际上是给程序提供一个一致的计算机硬件的视角,另外,操作系统必须承担程序的独立操作和保护对于非授权的资源存取。

Unix从用户空间转换执行到内核空间,无论何时一个应用程序发出一个系统调用或者被硬件中断挂起时,执行系统调用的内核代码在进程的上下文中工作–他代表调用进程并且可以存取该进程的地址空间。换句话说,处理中断的代码对进程来说时异步的,不和任何特别的进程有关。

三、字符设备驱动

1、主次编号

字符设备用过文件系统中的名字来存取,那些名字称之为文件系统的特殊文件,或者说设备文件,或者文件系统的简单节点。

传统 上,主编号标识设备相连的驱动,现代Linux内核允许多个驱动共享主编号,但是现在看到的大部分设备任然按照一个主编号一个驱动的原则来驱动。次编号被内核用来决定引用哪个设备。

2、设备编号的内部表示

在内核中,dev_t类型用来定义存放设备编号的参数,包括主次编号。

为了获得一个dev_t的主或者次编号,使用<linux/kdev_t.h> 的宏定义:

MAJOR(dev_t dev);

MINOR(dev_t dev);

相反,如果已经有主次编号,若需要将其转换为dev_t类型的数据,使用:MKDEV(int major,int minor);

3、分配和释放设备编号

建立一个字符驱动时,首先要做的就是获取一个或多个设备编号来使用,需要使用函数

​ *int register_chrdev_region(dev_t first,unsigned int count,char name);

在这个函数中,first是需要分配的起始设备编号。first的次编号部分通常是0,count是请求连续设备编号的总数(如果count过大,所要求的范围可能溢出到下一个次编号),name是应当链接到这个编号范围的设备名。当这个函数成功时,返回值为0,失败时返回一个负数。

​ 当需要内核动态分配一个主编号时,需要使用函数:

​ **int alloc_chrdev_region(dev_t dev,unsigned int firstminor,unsigned int count,char name);

​ 在这个函数中,dev是一个只输出的参数,他在函数成功完成时持有你的分配范围的第一个数;firstminor应当是请求的第一个要用的次编号,他常常是0,count是连接到这个编号范围的设备名,当这个函数成功时,返回值为0,失败是返回一个负数。

​ 当我们在不使用这些设备编号时,应当对他们进行释放,使用函数:

void unregister_chrdev_region(dev_t first,unsigned int count);

​ 上述函数分配设备编号给你的驱动使用,但是他们不会告诉内核我们要对这些编号做什么;在用户空间程序能够存取这些设备号中的其中一个之前,你的驱动需要将他们与他的实现设备操作的内部函数上。

4、一些重要的数据结构

file_operation结构:

​ 大部分的基础性的驱动操作包括3个重要的内核数据结构,称之为file_operations,file,和inode。file_operation结构是一个文件操作的结构体,这个结构定义在<linux/fs.h>,是一个函数指针的集合。每一个打开的文件(用一个file结构来替代)与他自身的函数集合相关联(通过包含一个称为f_op的成员,它指向一个file_operations结构),这些操作大部分负责实现系统调用,因此,命名为open,read等等。

​ 一个file_operation结构或者其一个指针称为fops(或者他的一些变体)。结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作留置为NULL,当指定为NULL指针时,内核的确切的行为是每个函数不同的。

struct file_operations scull_fops = {

.owner = THIS_MODULE,

.llseek = scull_llseek,

.read = scull_read,

.write = scull_write,

.ioctl = scull_ioctl,

.open = scull_open,

.release = scull_release,

};

struct file结构:

​ file与用户空间程序的FILE指针没有任何关系。文件结构代表一个打开的文件(他不特定给设备驱动;系统中每个打开的文件有一个关联的struct file在内核空间)。他由内核在open时创建,并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有实例都关闭后,内核释放这个数据结构。

inode结构

​ inode结构由内核在内部用来表示文件,因此,他和代表打开文件描述符的文件结构是不同的,可能有代表单个文件的多个打开描述符的许多文件结构,但是他们都指向一个单个inode结构。在inode结构中,有两个成员对于编写驱动有用:

dev_t r_rdev;(对于代表设备文件的节点,这个成员包含实际的设备编号)

​ struct cdev *i_cdev;(struct cdev是内核的内部结构,代表字符设备;这个成员包含一个指针, 指向这个结构,当节点指的是一个祖父设备文件时)

3、字符设备注册

​ 内核在内部使用类型struct cdev的结构来代表字符设备,在内核调用设备操作前,我们应该分配并且注册一个或者几个这样的结构。

​ 有两种方法来分配和初始化struct cdev结构:

​ *struct cdev my_cdev = cdev_alloc( );

my_cdev->ops = &my_fops;

​ 但是,当我们想将cdev结构嵌入我们自己的设备的特定结构中去,应当初始化我们已经分配的结构:

​ **void cdev_init(struct cdev cdev ,struct file_operations fops);

​ 上面的两种方法,有一个其他的struct cdev成员需要初始化,在其中有一个结构成员应当设置为THIS_MODULE,在建立cdev结构之后,需要把他告诉内核:

​ *int cdev_add(struct cdv cdev,dev_t num,unsigned int count);

​ 在上面这个函数中,dev时cdev结构,num是这个设备响应的第一个设备号,count是应当关联到设备的设备号的数目。

​ 在使用cdev_add函数时,需要注意:当这个调用失败时,他返回一个负数,而且我们的设备没有添加到系统中。当这个调用成功时,我们添加的设备就已经是可用的状态,此时内核可以调用他的操作。除非我们的驱动完全准备号处理设备上的操作,否则不应当使用cdev_add。

​ 当我们需要将一个字符设备从系统中去除的时候,使用函数:
​ *void cdev_del(struct cdev cdev)

5、open和release

open方法:

​ open方法提供给驱动来做任何的初始化来准备后续的操作。在大部分驱动中,open应当进行下面的工作:

1、检查设备特定的错误(例如设备还没准备好,或者类似的硬件错误)

2、如果他第一次打开,初始化设备

3、如果需要,更新f_op指针*

4、分配并且填充要放进filp->private_data的任何数据结构

​ open方法的函数原型:**int (*open)(struct inode inode ,struct file filp);

​ 在其中,inode参数是我们需要的信息,以他的i_cdev成员的形式,里面包含我们之前建立的cdev结构。但是我们通常需要的是设备本身的cdev结构,而不是我们创立的cdev结构,所以使用下面的宏来获取cdev:

container_of(pointer,container_type,container_field);

​ 这个宏使用一个指向container_field类型的成员的指针,他在一个container_type类型的结构中,并且返回一个指针指向包含结构。(即只需要知道某个结构体内成员的地址,就可以将这个结构体返回)

release方法:

​ release方法是open方法的反面,任一方式,设备方法应当进行下面的任务:

1、释放open分配在filp->private_data中的任何东西

2、在最后的close关闭设备

​ release方法示例:

int scull_release(struct inode *inode,struct file *filp)
{
    return 0;
}

6、scull内存的使用

​ scull使用的内存区,也成为一个设备,长度可变,写的越多,他增长就越多,通过使用一个短文件覆盖设备来进行修整。

​ scull驱动引入2个核心函数来管理Linux内核中的内存,这些函数定义在<linux/slab.h>

void *kmalloc(size_t size, int flags);

void kfree(void *ptr)

​ 对kmalloc的调用试图分配size字节的内存,返回值是指向那个内存的指针或者如果分配失败为NULL。flags参数用来描述内存应当如何分配(GFP_KERNELL)。分配的内存应当用kfree来释放,必须将kmalloc分配的内存给kfree,不能传递一个NULL给kfree。

7、读和写

​ 读和写方法都进行类似的任务,就是从应用程序代码拷贝数据:

ssize_t read(struct file *filp, char __user *buff,size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp)

​ 对于这两个方法,filp是文件指针,count是请求的传输数据大小,buff参数指向持有备写入数据的缓存,或者放入新数据的空缓存,最后,offp是一个指针指向一个“long offset type”对象,他指出用户正在存取的文件位置,返回值是一个“signed size type”;

​ 由于这两个方法的buff’参数是用户空间指针,因此,它不能被内核代码直接解引用,这是因为:

​ 1、依赖于你的驱动运行的体系,以及内核被如何配置,用户空间指针当运行与内核模式可能根本是无效的,可能没有那个地址的映射,或者他可能指向一些其他的随机数据。

​ 2、就算这个指针在内核空间是相同的东西,用户空间内存是分页的,在做系统调用时,这个内存可能没有在RAM中,试图直接引用用户空间的内存可能会产生一个页面错,这是内核代码不允许做的事情, 结果可能是一个“oops”,导致进行系统调用的进程死亡。

​ 3、如果你的驱动盲目的解引用一个用户提供的指针,他提供了一个打开的门路使用用户空间程序存取或覆盖系统任何地方的内存,如果你不想负责你的用户的系统的安全危险,你就不能直接解引用用户空间指针。

​ 显然,你的驱动必须能够存取用户空间缓存以完成他的工作,但是,为了安全起见,这个存取必须使用特殊的、内核提供的函数,他们定义在<arm/uaccess.h>中:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);

​ 这两个函数的角色不限于与用户空间的数据进行交互,他们还检查用户空间指针是否有效,如果指针无效,不进行拷贝,如果在拷贝中遇到一个无效地址,只拷贝数据。

四、platform设备驱动

1、什么是platform设备驱动:

平台设备驱动(Platform Device Driver)是 Linux 内核中一种常见的设备驱动类型,用于管理与硬件平台相关的设备。平台设备通常是与特定硬件平台相关联的设备,如嵌入式系统中的外设、特定类型的传感器等。

以下是平台设备驱动的主要特点和工作原理:

  1. 特点
    • 平台设备驱动通常针对特定硬件平台上的设备进行开发,与硬件紧密相关。
    • 在嵌入式系统中,平台设备通常由设备树(Device Tree)描述,并在设备树中注册。
    • 平台设备驱动不需要考虑总线类型,因为它们直接与硬件平台进行通信。
  2. 工作原理
    • 驱动加载:平台设备驱动在内核启动过程中加载,并根据设备树描述找到与之匹配的硬件设备。
    • 匹配设备:驱动通过设备树匹配机制(of_match_table)寻找匹配的硬件设备。
    • 初始化:驱动通过 probe 函数对匹配的设备进行初始化,包括分配资源、注册字符设备等操作。
    • 操作设备:驱动提供文件操作接口和控制命令,应用程序可以通过这些接口与设备进行通信。
    • 卸载:当驱动不再需要时,会调用 remove 函数进行清理和资源释放。

2、通信方式

平台设备驱动与硬件通信的方式取决于硬件的特性和驱动程序的实现。通常情况下,平台设备驱动通过以下几种方式与硬件进行通信:

  1. 直接寄存器访问:驱动程序通过内存映射 I/O 或端口 I/O 的方式访问设备寄存器,以读取和写入设备状态和控制信息。
  2. GPIO 控制:如果设备具有 GPIO 接口,驱动程序可以使用 GPIO 控制器来控制设备的输入和输出信号。
  3. 中断处理:驱动程序可以注册中断处理函数来处理设备产生的中断事件,以及响应设备状态的变化。
  4. DMA 控制:如果设备支持 DMA(直接内存访问),驱动程序可以使用 DMA 控制器来实现高速数据传输。
  5. 传感器数据读取:对于传感器类设备,驱动程序可以通过相应的接口读取传感器数据,如 I2C、SPI 等。

平台设备的设备树匹配机制(Device Tree Matching Mechanism)是 Linux 内核用于识别和匹配设备树描述的硬件设备的一种机制。在 Linux 内核中,设备树用于描述系统硬件的配置和连接关系,以及与内核驱动程序之间的关联。

3、匹配机制

设备树匹配机制的主要流程如下:

  1. 设备树描述:硬件平台的设备树描述包含了所有硬件设备的配置信息,包括设备类型、资源分配、连接关系等。
  2. 内核初始化:在内核启动时,会解析设备树描述,并构建设备树数据结构。
  3. 设备匹配:内核会遍历设备树中的所有节点,并根据设备树中的 compatible 属性与驱动程序中的 of_match_table 进行匹配。
  4. 驱动加载:当找到匹配的设备节点时,内核会加载相应的驱动程序,并调用其 probe 函数进行初始化。
  5. 设备注册:驱动程序在 probe 函数中会注册设备并分配资源,使其能够被系统正确地识别和访问。

通过设备树匹配机制,Linux 内核能够动态地识别和管理硬件设备,从而实现了硬件和驱动程序的松耦合。

4、常用的数据类型和函数

在Linux platform设备驱动中,常用的数据类型和函数包括但不限于:

  1. 数据类型

    struct platform_device		//表示平台设备的结构体,它包含了设备的相关属性,如设备名、资源等。
    struct platform_driver		//用于描述平台驱动程序的结构体,包含驱动名、probe和remove函数等。
    struct platform_bus_type	//抽象描述系统中总线的结构体,用于实现虚拟的平台总线。
    
  • 函数

  • platform_match(const struct platform_device *dev, const struct platform_driver *drv);	//这是用于匹配设备和驱动的函数。它根据设备或驱动的特性来判断是否匹配,并负责将它们连接起来。
    int platform_driver_register(struct platform_driver *drv);	//用于注册平台驱动,使得内核能够识别并使用这个驱动。
    int platform_device_register(struct platform_device *dev);	//用于注册平台设备,将其与相应的驱动进行绑定。
    int platform_get_resource(struct platform_device *dev, int resource, void *data);	//获取设备资源的函数,包括时钟等资源。
    int platform_get_irq(struct platform_device *dev, unsigned int irq, void *data);	//获取设备的中断请求号(IRQ)。
    struct resource *request_region(unsigned long start, unsigned long n, const char *name);		//申请内存资源的函数,用于为设备驱动分配特定的内存区域。
    
    #include <linux/module.h>
    #include <linux/of.h>
    
    static const struct of_device_id platform_key_of_match[] = {
        { .compatible = "vendor,device", },
        { /* 其他匹配项 */ },
        { /* 最后一个匹配项必须为空 */ }
    };
    
    MODULE_DEVICE_TABLE(of, platform_key_of_match);
    

总的来说,这些数据类型和函数是Linux平台设备驱动开发中的基础组件,它们共同构成了平台设备驱动的核心框架。开发者需要熟悉这些接口以实现设备的正确驱动和运作。

五、中断子系统

下面函数实现中断注册接口:

int request_irq(unsigned int irq,irqreturn_t(*handler)(int,void *,struct pt_regs *),unsigned long flags, const char *dev_name, void *dev_id);

void free_irq(unsigned int irq, void *dev_id);

从request_irq 返回给请求函数的返回值是0表示成功,返回是负数代表失败。

调用 request_irq 的正确位置是当设备第一次打开时, 在硬件被指示来产生中断前. 调用 free_irq 的位 置是设备最后一次被关闭时, 在硬件被告知不要再中断处理器之后.

在Linux内核中,中断处理程序可以在驱动初始化时安装,也可以在设备第一次打开时安装。虽然从模块的初始化函数中安装中断处理听起来是个好主意,但实际上并不推荐,特别是当设备不共享中断时。因为中断线的数量是有限的,浪费它们会导致系统中的设备数多于中断数。如果一个模块在初始化时就请求了一个IRQ(中断请求),它会阻止其他驱动使用这个中断,即使持有它的设备从未被使用过。而在设备打开时请求中断,允许某些共享资源,这样可以避免浪费中断线。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值