Linux驱动开发 框架

设备驱动开发分类

  1. 字符设备驱动

      Linux 驱动中最基本的一类设备驱动,字符设备是按照字节流进行逐个字节读写操作的设备,读写数据是分先后顺序的。比如常见点灯、IIC、SPI,LCD 等都是字符设备,它们的驱动就是字符设备驱动。

字符设备的注册与注销

旧方法
//注册函数,一般在驱动模块的加载函数xxx_init进行
static inline int register_chrdev(unsigned int major, //要注册设备的主设备号,Linux 下每个设备都有一个设备号 ,设备号分为主设备号和次设备号两部分                                  
                                  const char   *name, //要注册设备的名字,指向一串字符串
                                  const struct file_operations *fops)//指向设备的操作函数集合变量
//注销函数,一般在驱动模块的卸载函数xxx_exit进行                    
static inline void unregister_chrdev(unsigned int major,  //要注销的设备对应的主设备号
                                     const char *name) //要注销的设备对应的设备名    
使用示例:
......
static struct file_operations test_fops;
/* 驱动入口函数 */ 
static int __init xxx_init(void) 
{ 
    ....
    /* 注册字符设备驱动 */  
    retvalue = register_chrdev(200, "chrtest", &test_fops); 
    .....
} 

/* 驱动出口函数 */ 
static void __exit xxx_exit(void) 
{ 
    /* 注销字符设备驱动 */ 
    unregister_chrdev(200, "chrtest"); 
} 
 .....    
             
新方法

  新方法自动创建节点文件,且不需要确定被占用的设备号,也不会将一个主设备下的所有次设备都使用。

  分配和释放设备号

//如果没有设备号,调用该函数申请设备号,而不再是自定义设备号,避免了占用已有的设备号的情况
int alloc_chrdev_region(dev_t *dev, 
                        unsigned baseminor, 
                        unsigned count,         //要申请的数量
                        const char *name)       //设备名字 


//注册设备函数,设备号参数是由主设备号+旧设备号,而不是旧方法仅是主设备号
int register_chrdev_region(dev_t from,         //是要申请的起始设备号,也就是给定的设备号
                           unsigned count,     
                           const char *name)  

//释放设备号函数                           
void unregister_chrdev_region(dev_t from, 
                              unsigned count) 
                                                       
获取设备号示例:
int major;     /* 主设备号 */ 
int minor;     /* 次设备号*/ 
dev_t devid;   /* 设备号*/ 
if (major) {                  /* 定义了主设备号*/ 
    devid = MKDEV(major, 0);  /* 大部分驱动次设备号都选择 0 */ 
    register_chrdev_region(devid, 1, "test"); 
} else {     /* 没有定义设备号*/ 
    alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */ 
    major = MAJOR(devid);     /* 获取分配号的主设备号*/ 
    minor = MINOR(devid);     /* 获取分配号的次设备号 */ 
}        

自动创建设备节点解释

  自动创建的具体原理udev与mdev不多缀叙。具体步骤如下:

  • 创建和删除类

      自动创建设备节点是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件include/linux/device.h 里面。class_create 是类创建函数,class_create 是个宏定义,内容如下

struct class *class_create (struct module *owner,  //一般为 THIS_MODULE
                            const char *name)  //类名字。返回值是个指向结构体 class 的指针,也就是创建的类

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy
void class_destroy(struct class *cls);       //参数 cls 就是要删除的类
  • 建好类后还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备

struct device *device_create(struct class *cls,    //设备要创建哪个类下面
                             struct device *parent, //是父设备,一般为 NULL,也就是没有父设备
                             dev_t devt,            //设备号
                             void *drvdata,         //设备可能会使用的一些数据,一般为 NULL
                             const char *fmt, ...)  //设备名字,如果设置 fmt=xxx 的话,会生成/dev/xxx 这个设备文件。返回值就是创建好的设备

卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy
void device_destroy(struct class *cls, dev_t devt) //参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号

示例

struct class  *class; /* 类 */  
struct device *device; /* 设备*/ 
dev_t devid; /* 设备号 */  
/* 驱动入口函数 */ 
static int __init xxx_init(void) 
{ 
    /* 创建类 */ 
    class = class_create(THIS_MODULE, "xxx"); 
    /* 创建设备 */ 
    device = device_create(class, NULL, devid, NULL, "xxx"); 
    return 0; 
} 

/* 驱动出口函数 */ 
static void __exit led_exit(void) 
{ 
    /* 删除设备 */ 
    device_destroy(newchrled.class, newchrled.devid); 
    /* 删除类 */ 
    class_destroy(newchrled.class); 
} 

module_init(led_init); 
module_exit(led_exit);
MISC 驱动

MISC 驱动也叫做杂项驱动,当板子上的某些外设无法进行分类的时候就可以使用 MISC 驱动,它其实就是最简单的字符设备驱动,用于解决主设备号日益紧张的问题。

所有 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。MISC 设备会自动创建 cdev,不需要像以前那样手动创建,因此采用 MISC 设备驱动可以简化字符设备驱动的编写。只需要向 Linux 注册一个 miscdevice 设备,miscdevice 是一个结构体,定义在文件 include/linux/miscdevice.h 中,内容如下

struct miscdevice { 
    int minor; /* 子设备号,除了自定义外还可以使用内核已预定义的子设备号(1~255) */ 
    const char *name; /* 设备名字设备注册成功以后就会在/dev 目录下生成一个名为 name 的设备文件 */  
    const struct file_operations *fops; /* 设备操作集 */ 
    struct list_head list; 
    struct device *parent; 
    struct device *this_device; 
    const struct attribute_group **groups; 
    const char *nodename; 
    umode_t mode; 
}; 

设备注册:

外部设备驱动开发

任何外设驱动,最终都是要配置相应的硬件寄存器,即需要配置IO端口。因为Linux内核会初始化MMU设置好虚拟内存映射,因此要获得端口引脚寄存器的物理地址在linux系统里的虚拟地址。在加载驱动时获得虚拟地址,并在卸载驱动时释放虚拟地址。

//用于获取指定物理地址空间对应的虚拟地址空间
void __iomem *ioremap(resource_size_t res_cookie, //要映射的物理起始地址
                      size_t size)                //要映射的内存空间大小。
{ 
    return arch_ioremap_caller(res_cookie, size, MT_DEVICE, 
    __builtin_return_address(0)); 
} 
//用于释放指定物理地址空间对应的虚拟地址空间
void iounmap (volatile void __iomem *addr)        //addr 要取消映射的虚拟地址空间首地址

实现字符设备的具体操作函数

file_operations 结构体内部有open、release、read 和 write 等具体的字符设备操作函数。之后确定需求后对操作函数进行初始化再对fil_operations初始化即可:

//chrtest_open等函数的初始化
static int chrdevbase_open(struct inode *inode, struct file *filp)  
{ 
    return 0; 
} 
...

static struct file_operations test_fops = { 
    .owner = THIS_MODULE,  
    .open = chrtest_open, 
    .read = chrtest_read,     
    .write = chrtest_write,     
    .release = chrtest_release, 
}; 
...
 

设置文件私有数据(推荐编程形式)

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state) 等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,但对于一个设备的所有属性信息最好将其做成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取 private_data 即可得到设备结构体。

/* 设备结构体 */ 
    struct test_dev{ 
    dev_t devid; /* 设备号*/ 
    struct cdev cdev; /* cdev*/ 
    struct class *class; /* 类*/ 
    struct device *device; /* 设备*/ 
    int major;     /* 主设备号 */ 
    int minor;     /* 次设备号 */ 
};  
struct test_dev testdev; 
/* open 函数 */ 
static int test_open(struct inode *inode, struct file *filp) 
{ 
    filp->private_data = &testdev; /* 设置私有数据 */ 
    return 0; 
}  

字符设备驱动通信框架

整体的通信过程

首先在应用层通过fd=open("/dev/hello",O_RDWR);

1.sys_open:open函数之后会有一个系统调用sys_open(const char __user *filename,int flags)。在文件描述符表中分配一个新的文件描述符。调用内核的 do_sys_open() 函数,根据文件名和标志打开文件,并将 struct file 结构体与文件描述符关联起来。函数成功执行后,应用程序将获得一个文件描述符。

2.chrdev_open()之后chrdev_open() 函数将 inodecdev 结构体和 struct file 结构体关联起来。inode 结构体包含设备的主设备号和次设备号,用于确定与设备文件关联的驱动程序;cdev 结构体将设备与驱动程序的操作函数集关联起来,从而实现设备和驱动程序之间的通信;struct file 结构体将驱动程序的操作函数集与应用层联系起来,使得应用程序可以通过系统调用与设备进行通信。chrdev_open() 函数连接了这些部分,从而使得应用层能够操作具体的设备。

3.struct file:通过open,在进程中找到相应的进程的struct task结构体,这个结构体中是文件描述符fd的集合,每次应用层使用open打开一个新文件,操作系统将会在进程的这个集合中找到最下面的位置添加一个struct file这个结构体,同时这个数组的下标就是返回给应用层的文件描述符fd,这个struct file结构体中有一个struct file_operation *f_ops;指针这个指针可以访问具体的驱动的操作函数集,还有一个private_date 指针 这个指针可以用来访问驱动程序中的设备文件结构体中的数据,一般会在驱动程序的open函数中定义将date指针和设备文件联系在一起。struct file是将应用层和驱动程序的操作函数集联系在了一起。当设备数量增加或者设备数量不固定时,通过 private_data 字段动态获取设备信息将变得非常有用。

4.inode结构体:通过设备编号找到了inode结构体,这个结构体中的两个参数,一个是dev_t rdev,这里面包含了真正的设备号,通过这个设备号找到驱动程序中描述设备的结构体,另一个是struct cdev *i_cdev ,这个参数指向了cdev结构的指针。

5.设备结构体:在设备的结构体中,通常包含了设备的一些具体信息,这个结构体中会有一个struct cdev指针,inode结构体中的i_node指向这个指针。cdev是设备的抽象,是内核中字符设备的实例化。

6.init_cdev:通过void init_cdev(struct cdev *cdev,const file_operation *fops)将cdev和驱动的操作函数集联系在了一起,这样就实现了操作函数和具体的设备的通信。

7.add_cdev:在linux内核中有一个链表管理着设备。add_cdev用于将一个字符设备(cdev 结构体)添加到 Linux 内核中的字符设备管理系统中。这个函数将 cdev 结构体注册到内核中,使得内核能够识别和管理该设备。

1.什么是inode?

linux操作系统中,每一个文件都有inode编号,文件系统将“inode”与文件名分开存储。使得硬链接成为可能。即多个文件名可以指向同一个“inode”。“inode”与文件名的映射关系存储在目录中。

  1. 文件类型:文件、目录、符号链接、设备文件等。r代表字符设备 b代表块设备。

  2. 权限和所有权:文件的读、写、执行权限以及文件的所有者和所属组。

  3. 时间戳:文件的创建时间、最后访问时间、最后修改时间等。

  4. 文件大小:文件所占用的字节数。

  5. 数据块指针:指向存储文件数据的磁盘块。

设备文件的inode,多了两个参数,分别是主设备号和次设备号。这些属性用于标识设备,并指向与设备关联的驱动程序。所以,inode可以看作是设备的静态属性,用于描述设备文件及其关联设备的基本信息。

2.什么是cdev?

cdev(character device)指的是字符设备的抽象,并不是设备本身,它是一个内核数据的结构,用于表示字符设备驱动在内核空间中的实例。

cdev结构体的作用是将设备文件与实际的操作函数集struct file_operation联系在一起,从而允许用户空间应用程序通过设备文件和设备进行通信。

3.为什么有了cdev还需要struct dev具体的设备文件结构体?

因为cdev并不包含设备文件的详细信息,它是内核中字符设备驱动的实例化,主要用于将设备文件与驱动程序的实际操作函数关联起来。但是没有设备文件诸如硬件资源等信息。因此还需要一个设备文件结构体将struct cdev与其他的信息封装在这个结构体当中。这个结构体通常包含一下内容:

  1. 设备的硬件资源:例如,内存地址、中断号、GPIO引脚等。

  2. 设备状态:例如,设备是否已经初始化、是否处于工作状态等。

  3. 设备的配置参数:例如,通信速率、缓冲区大小等。

  4. 同步和互斥原语:例如,用于保护设备的信号量、互斥锁等。

  5. 驱动程序私有数据:例如,用于存储与特定设备相关的数据结构、算法等。

  6. 指向cdev结构体的指针:将描述设备的结构体与cdev结构体关联。

这个描述设备的结构体可以使驱动程序针对特定设备进行定制化处理,更好地管理设备的资源和状态。在多设备驱动程序中,这种结构体尤为重要,因为它可以帮助驱动程序管理多个设备实例,确保它们之间的正常运作和相互隔离。

其实可以用面向对象的思想来理解。cdev结构体是所有字符设备的一个抽象,是一个基类。而一个具体类型的设备应该是由基类派生出来的一个子类,子类包含了特定设备所持有的属性,这样子类能更好的 刻画好一类具体的设备。

4.为什么需要用add_cdev将设备添加到内核中?

add_cdev() 函数用于将一个字符设备(cdev 结构体)添加到 Linux 内核中的字符设备管理系统中。这个函数将 cdev 结构体注册到内核中,使得内核能够识别和管理该设备。

驱动程序通过 add_cdev() 函数将设备添加到内核的字符设备管理系统中有以下目的:

  1. 使内核能够识别设备:内核需要知道设备的主设备号(major)和次设备号(minor),以便在应用程序尝试访问设备时找到正确的驱动程序。通过将 cdev 结构体添加到内核的管理系统中,内核能够建立设备号与驱动程序之间的映射关系。

  2. 与设备文件关联:在 /dev 目录下创建的设备文件与 cdev 结构体关联。当用户空间的应用程序打开设备文件时,内核会根据设备文件的主设备号和次设备号找到对应的 cdev 结构体。cdev 结构体中的 file_operations 结构体指针使得内核能够找到并调用驱动程序的操作函数。

  3. 管理设备生命周期:内核需要跟踪设备的生命周期,以便在设备被移除或驱动程序被卸载时执行适当的清理操作。将 cdev 结构体添加到内核的管理系统中有助于实现设备的生命周期管理。

通过 add_cdev() 函数将驱动程序添加到内核中的字符设备管理系统是为了使内核能够识别和管理设备,以及实现应用程序与设备的通信。这是驱动程序开发过程中的一个重要步骤。

5.mknod和cdev_alloc注册设备号的区别?

mknod是一个linux命令,mknod命令是在文件系统中创建的设备文件,这个设备文件是用户空间的一部分,文件包含主设备号和次设备号,通过这些信息使得内核能够找到与设备文件关联的驱动程序。

cdev_alloc(),cdev_init()cdev_add() 函数用于在内核空间创建和注册字符设备。这些函数将设备与驱动程序的操作函数集关联起来,并告知内核如何处理设备文件。

如果在应用层通过mknod创建了一个设备文件,而在内核的驱动中,没有通过cdev的函数,将这个设备文件和struct file_operation操作函数集关联起来,并注册到内核当中让内核管理的话。mknod即使创建了设备文件,这个文件也只是一个包含了主设备号和次设备号的普通文件,当应用程序尝试通过这个文件和设备进行通信的时候,会由于内核找不到与之关联的驱动程序,操作将会失效。

驱动中的高级文件I/O操作

阻塞I/O

在资源不可用时,进程阻塞,阻塞发生在驱动中,资源可用后进程被唤醒,在阻塞期间不占用CPU,是最常见的一种方式。一般在驱动中用wait_head_t 等待队列实现

非阻塞I/O

调用立即返回,即便是在资源不可用的情况下,通过返回值来确定I/O时候成功,如果不成功,程序将会在之后继续尝试(轮询)。对于大多数时间内资源都不可用的设备(如鼠标、键盘),这种尝试会白白消耗掉CPU大量的时间,如果尝试的间隔时间增加,又可能产生不能及时处理设备数据。

I/O多路复用

可以同时监听多个设备的状态,如果被监听的所有设备都没有关心的事情发生,那么系统调用将会被阻塞。当被监听的任何一个设备有对应关心的事件发生,将会唤醒系统调用,系统调用将会再次遍历所监听的设备,获取其时间信息,然后系统调用返回。之后可以对设备发起非阻塞的读或写操作。

异步I/O

异步I/O(AIO)是一种允许应用程序在内核执行I/O操作时继续执行其他任务的技术。在异步I/O模型中,应用程序向内核发送一个I/O操作请求,而内核负责在后台处理这个请求。具体的I/O操作在驱动中完成,驱动中可能会被阻塞也可能不会被阻塞。当I/O操作完成时,内核会通过信号或回调函数通知应用程序。这样,应用程序在等待I/O操作完成的过程中可以继续执行其他任务,提高了程序的执行效率和吞吐量。

  1. 基本工作流程,应用程序首先初始化一个异步I/O请求,通常包括指定I/O操作的类型(如读、写等)、目标文件或设备、缓冲区指针、操作字节数等。

  2. 应用程序设置一个回调函数或信号处理函数,用于在I/O操作完成时接收通知。这些通知机制是异步I/O与同步I/O的区别。

  3. 应用程序将异步I/O请求提交给内核。此时,应用程序还不会被阻塞,而是继续执行其他任务。内核负责在后台处理异步I/O请求。

  4. 当I/O操作完成时,内核会根据应用程序设置的通知机制(如回调或信号)通知应用程序。应用程序在收到通知后可以处理I/O操作的结果,如读数据或检查错误状态等。

      异步I/O模型允许应用程序在等待I/O操作完成的过程中执行其他任务,提高了程序的并发性。这种模型在处理大量的I/O操作或需要同时执行多个任务场景中非常有用,如数据库、文件系统、网络服务器等应用。

异步通知

异步通知类似于异步IO,只是当设备资源可用时,它是向应用层发信号,而不是直接调用应用层注册的回调函数,并且发信号的操作也是驱动程序自身来完成的。上述的IO模型中,应用程序都是主 动的获取设备的资源信息,即便是异步IO也是需要先发起一个IO的操作请求。而异步通知则是当设备资源可获得的时候,由驱动主动通知应用程序,再由应用程序发起访问。这种机制和中断非常相似,可以借用中断的思想来理解。

  1. 注册信号处理函数,相当于注册中断处理函数。

  2. 打开设备文件,设置文件属性。目的是使驱动根据打开文件的file结构,找到对应的进程,从而向该进程发送信号。

  3. 设置设备资源可用时驱动向进程发送的信号,这一过程并不是必须的,但是若要使用sigaction的高级特性,该步骤是必不可少的。

  4. 设置文件的FASYNC标志,使能异步通知机制,这相当于打开中断使能位。

mmap地址映射

和mmap地址映射对应的驱动函数,也是struct file_operation fops结构体中的一个成员,里面的实现逻辑主要是通过

 

static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long pfn;
    unsigned long size = vma->vm_end - vma->vm_start;

    /* 获取物理地址对应的页帧号 */
    pfn = virt_to_phys(my_device_buffer) >> PAGE_SHIFT;

    /* 将设备内存映射到用户空间 */
    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
        return -EAGAIN;
    }

    return 0;
}
主要是remap_pfn_range(struct vm_area_struct *vma vma,unsigned long addr, unsigned long pfn, 
unsigned long size, pgprot_t prot))
1.vma描述一片映射区域的结构体指针,一个进程有很多篇营社区与,每个区域都有这样对应的结构,这些结构通过
链表和红黑树组织在一起实现。
2.第二个参数addr是用户指定的映射之后的虚拟起始地址,若用户没有指定,将由内核指定。
3.第三个参数是物理内存所对应的页框号,就是将物理地址处以页大小的得到的值。
4.第四个参数是想要映射的空间的大小。
5.最后一个prot是该内存的访问权限
该函数之后,一篇物理内存区域将会被映射到相应的起始位置的地址。

 

在驱动程序中实现mmap操作是指提供一个mmap方法,使得用户空间程序可以将某些内核空间资源(例如内存映射的硬件寄存器或者内核分配的内存缓冲区)映射到用户空间的虚拟地址。

当在驱动中实现mmap操作时,用户空间程序可以使用mmap系统调用在其虚拟地址空间中创建一个内存映射。这样一来,用户空间程序就能够直接访问内核空间的资源,而无需通过系统调用,从而提高性能。

具体来说,mmap操作是将内核空间的某个内存区域映射到用户空间的虚拟地址。这个内存区域可以是内核分配的缓冲区,也可以是内存映射的硬件资源,如硬件寄存器。通过在驱动程序中实现mmap方法,用户空间程序就可以直接访问这些内核空间资源,而无需频繁地使用系统调用。

定位操作

和llseek对应的驱动接口是file_operations结构中llseek函数指针指向的函数。

static loff_t vfb_llseek(struct file *filp,loff_t off,int whence)
{
    //loff_t 表示文件中的位移偏移量。通常在内核中表示64位有符号整数。
    loff_t newpos;

    //off表示要的偏移量
    //filp->f_pos 记录了上次到了那些   off是这次的偏移量
    switch (whence)
    {
    case SEEK_SET://从头开始计算 新位置等于off
        newpos=off;
        break;
    case SEEK_CUR://从当前位置开始偏移
        newpos=filp->f_ops+off;
        break;
    case SEEK_END://从文件末尾开始偏移
        newpos=PAGE_SIZE+off;
        break;
    
    default:
        return -EINVAL;
        break;
    }
    if(newpos<0||newpos>PAGE_SIZE)
    {
        return -EINVAL;//文件偏移量越界了
    }
    filp->f_pos=newpos;//记录当前位置
    return newpos;
}

驱动的中断和时间管理

一个硬件中断到内核处理的过程

当一个硬件中断发生的时候,会在中断控制器上触发一个信号,然后中断控制器将这个信号传递给CPU。接着,CPU会根据中断向量表找到对应的中断服务例程(ISR)并执行。在linux内核的arm中,中断的处理过程涉及到这些过程。

  1. 当中断到达CPU的时候,硬件将自动保存当前的程序计数器(PC)和CPSR,然后跳转到中断向量表中的对应向量的地址。arm中通常,中断向量表的位置在 0xFFFF0000(高端向量) 或者 0x00000000(低端向量)

  2. 在向量表中,有一个特定于体系结构的汇编代码段,例如‘__irq_svc’,它负责保存寄存器状态并调用C语言级别的中断处理函数‘handle_IRQ()’.handle_IRQ()将会从中断控制器中获取硬件IRQ号。

  3. handle_IRQ()函数会从中断控制器(例如GIC)获取硬件IRQ号,并调用handle_domain_irq()

  4. handle_domain_irq()函数负责将硬件IRQ号映射到内核IRQ号,并调用generic_handle_irq()

  5. ‘generic_handle_irq()’函数根据IRQ查找‘irqaction’结构体,并遍历这个结构体中的所有处理函数(handler)。每个处理函数都会被调用以处理中断。

由于中断控制器的管脚有限,所以在某系体系结构上会出现两个或两个以上的设备被接到了同一根中断线上,这个时候任何一个设备产生了中断都会造成中断。这就是共享中断。由于Linux内核不能够判断新产生的中断属于那个设备,所以用一根中断线的中断处理函数通过不同的struct irqaciton结构包装起来,然后用链表链接起来。因此使用dev_id参数:当设备驱动注册中断处理程序时,它会向request_irq()函数传递一个dev_id参数。这个参数是一个指针,通常指向与中断关联的设备实例。在中断处理程序中,驱动可以使用这个dev_id指针来访问设备的数据结构,并检查设备的状态。如果设备产生了中断,处理程序会继续处理;否则,它会立即返回。

驱动的中断处理

如果要驱动支持中断,那么需要构造一个struct irqaction的结构对象,根据IRQ号加入到对应的链表中。这里直接用内核设置好的API就好。

struct irqaction {
    irq_handler_t handler;   // 中断处理函数
    unsigned long flags;     // 描述中断处理程序行为的标志
    const char *name;        // 与中断关联的设备名或描述符
    void *dev_id;            // 指向与中断关联的设备实例的指针
    struct irqaction *next;  // 指向同一中断下一个处理程序的指针,用于形成链表结构
    int irq;                 // 中断号
};


int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,\
const char *name,void *dev)   __init中注册
1.第一个参数irq是IRQ号,不是硬件手册上的IRQ号,而是被handler_domain_irq映射到了内核的IRQ号。这个
号将决定构造的struct irqaction对象插入到那个链表中,并用于初始化struct irqaction对象的irq成员
2.第二个参数对应 指向中断处理函数的指针 类型定义为 irqreturn_t (irq_handler_t)(int,void*) 中段发生后
这个中断处理函数将会被自动调用,第一个int是IRQ号,第二个参数是设备ID。这个中断返回的参数有三个类型
irqreturn_t IRQ_NONE:不是驱动所管理的设备产生的中断,适用于共享中断。
            IRQ_HANDLED:中断被正常处理
            IRQ_WAKE_THREAD:需要唤醒一个内核线程。
3.flags:使用中断相关的标志 初始化struct irqaction中的成员 来标志什么方式出发中断
4.name:该中断在/proc中的名字,用于初始化struct irqaction对象中的name成员
5.dev:区别共享中断中的不同设备所对应的struct irqaction对象,dev用于初始化struct irqaciton中的
dev_t成员。共享终端必须穿第一个非NULL的实参,非共享中断可以穿NULL。
request_irq函数成功返回0,失败返回负数。根据传入的参数创建struct action对象,并加入到相应的链表中
,还能将对应的中断使能了。
void free_irq(unsigned int,void *);在exit中销毁注册的中断。

中断处理函数应该迅速完成,不能消耗太长时间,因此ARM处理器进入中断后,相应的中断被屏蔽(IRQ中断禁止IRQ中断,FIQ中断禁止IRQ中断和FIQ中断,内核没有使用FIQ中断)。在之后的代码中有没有重新开启中断,所以整个中断的处理过程中中断是禁止的。如果中断处理函数执行时间过长,那么其他的中断将会被挂起,从而对其他中断的相应造成严重的影响。同时在中断处理函数中一定不能使用进程调度器,即一定不能用可能会引起进程切换的函数(比如copy_to_user,copy_from_user)因为一旦进程发生了切换,将不能够再次调度中断。这是内核对中断控制器的一个严格限制。

中断下半部

中断处理函数应该尽快完成,但是有时候这些耗时的操作又无法彻底避免,为了防止这样的情况产生。Linux中将中断分为了两个部分:上半部和下半部。在上半部完成紧急但能很快完成的事情,下半部完成不紧急但比较耗时的操作。 上半部一般指:static irqreturn_t vser_handler

下半部:

软中断:在static irqreturn_t vser_handler 这个上半部的处理函数中 的irq_exit()中调用到了下半部的机制 而且这里将中断重新使能了。软中断的结构是struct softirq_action 它的定义就是内嵌了一个函数指针。内核定义好了10个软中断对象,内核有一个全局变量记录是否有相应的中断需要执行,比如有相应的编号为1的软中断。就会用这个比特位去索引softirq_vec这个数组,然后调用softorq_action对象中的action指针所指向的函数。

虽然软中断可以实现中断的下半部,但是软中断基本上是内核开发者预定义好的,通常应用在对性能要求特别高的场合,而且需要一定的内核编程技巧,不太适用于驱动开发者。

tasklet:虽然软中断通常是内核开发者设计,但是专门保留了一个软中断给驱动开发者,这就是TASKLET_SOFTIRQ。相应的软中断处理函数是taasklet_action 。要实现tasklet的下半部,需要构造一个struct tasklet_struct 结构对象,初始化里面的成员。然后放入对应CPU的tasklet链表中,然后设置TASKLET_SOFTIRQ对应的比特位。

1.DECLARE_TASKLET(name,func,data);静态定义一个struct tasklet_struct 结构对象,名字为name下半部函数
为func,传递的参数为data,该tasklet可以被执行。
2.DECLARE_TASKLET_DISABLED(name,func,data);这个和上面的相似但是不能被执行,需要调用tasklet_enablee来使能
3.void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data)
这个函数主要用于初始化一个动态分配的struct stasklet_struct结构地向
4.void tasklet_schedule(struct tasklet_struct *t);
这函数将tasklet结构对象加入到CPU对应的tasklet的链表中。

1.步骤在开始DECLARE_TASKLET();静态定义一个结构对象。
2.在中断上半部的函数static irqreturn_t handler中将下半部函数添加到内核中 tasklet_schedule。
工作队列

下半部机制中不管是软中断还是tasklet都有一个限制,就是中断上下文中不能直接或简介地调用调度器。为了解决这个问题,内核新加入了一种下半部机制 工作队列。实现思想就是在内核启动的时候创建一个或者多个内核工作线程,工作线程取出工作队列中的每一个任务,然后执行,当队列没有工作的时候,工作线程休眠。驱动想要延迟执行某一个工作的时候,构造一个工作队列结点对象,然后加入到对应的工作队列,并唤醒工作线程,工作线程又取出工作队列上的结点来完成工作。所有工作完成后再休眠。因为是在进程上下文中工作,所以工作可以调用调度器。工作队列体用了一种延迟机制,因此这种机制适用于中断上下文中。

DECLARE_WORK(n,f);  静态定义一个工作队列结点,n为节点名字,f是工作函数
DECLARE_WORK(n,f);  静态定义一个延时的工作结点
INIT_WORK: 常用于动态分配的工作队列节点初始化
bool schedule_work(struct work_struct *work);将工作队列节点加入到内核定义的全局工作队列中。


1. DECLARE_WORK定义一个工作结点
2.在在中断上半部的函数static irqreturn_t handler中将下半部函数添加到内核中 schedule_work
3.具体函数实现。

一致性DMA映射和流式DMA映射

一致性DMA映射和流式DMA映射通常在不同的使用场景下被选用:

  1. 一致性DMA映射:适用于需要频繁、实时或低延迟数据交换的场景,例如网络数据包处理或音视频流处理。在这些场景中,保证CPU和设备之间数据的一致性是至关重要的。一致性DMA映射通常在内核初始化时分配并保留足够的内存空间,以供设备和CPU在整个会话期间使用。

  2. 流式DMA映射:适用于数据传输量较大、传输时间较长或不需要实时数据交换的场景,例如文件传输、存储设备访问等。流式DMA映射通常需要在数据传输前后进行映射和同步操作,以确保设备和CPU之间的数据一致性

流式缓存:

  1. 读方向(设备从内存读取数据):在这种情况下,驱动程序会在设备开始读取数据之前使相应的缓存行无效。这样,当设备读取数据时,它将直接从内存中读取,而不是从缓存中读取。这确保了设备读取到的数据是最新的,因为缓存中可能存在过时的数据。

  2. 写方向(设备向内存写入数据):在流式DMA映射的写方向时,设备将数据直接写入内存。然后,驱动程序需要确保CPU缓存与内存中的数据保持一致。这可以通过刷新或使缓存行无效来实现。具体来说,驱动程序会执行一个写回(write-back)操作,将CPU缓存中对应的缓存行的数据写回到内存,然后使该缓存行无效。这样,当CPU下次访问该内存地址时,它将从内存中读取最新的数据,而不是从缓存中读取过时的数据。

/sys伪文件系统

在/sys目录下有很多子目录,例如block下基本上都是块设备,bus下都是系统中所有的总线(i2c,spi等),class目录下是一些设备类(input输入设备类、tty终端设备等)、devices目录下是系统中所有的设备。

总线目录bus下有很多具体的总线,而具体的总线下有注册的驱动和挂接的设备。注册的驱动程序驱动对应总线目录下的某些具体设备,总线目录下的某些设备被对应总线下的某个驱动程序所驱动。

伪文件系统在系统运行时才会有内容,也就是说伪文件系统的目录、文件和软链接都是动态生成的,而这些内容都是反映内核的相关信息。声称这些信息的重要内核数据结构是struct kobject。

  1. /sys 文件系统中的文件和目录并不是真正的代码实现,而是以文本形式呈现的内核数据结构的抽象表示。

  2. /sys 文件系统中的文件和目录通常是通过软链接来表示设备和驱动之间的关系。这些链接并不指向驱动代码的实际实现,而是展示了设备和驱动在内核对象模型(Kobject)中的关联关系。通过这种方式,用户可以更容易地了解内核中设备和驱动的层次结构以及它们之间的连接。

当成功向内核添加一个kobject对象后,底层的代码会自动在/sys目录下生成一个子目录。另外,kobject可以附加一些属性,并绑定操作这些属性的方法。其附加的属性会被底层的代码自动实现为对象对应目录下的文件。用户访问这些文件最终就变成了调用操作属性的方法来访问属性。最后通过sys的API接口可以将两个kobject对象关联起来,形成软链接。

字符设备驱动代码框架

1.一个驱动支持多个设备
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>

#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/kfifo.h>//虚拟串口设备

#define VSER_MAJOR  256
#define VSER_MINOR  0
#define VSER_DEV_CNT    2
#define VSER_DEV_NAME   "vser"

//创建两个虚拟串口设备 这里是静态创建比较繁琐,后续将学习到动态创建
static DEFINE_KFIFO(vsfifo0,char,32);
static DEFINE_KFIFO(vsfifo1,char,32);
//此处可以理解为面向对象的思想 将cdev作为基类 而这里创建了具体设备
//的派生类对象。子类包含了派生对象所特有的属性。
struct vser_dev{
    struct kfifo *fifo;
    struct cdev cdev;
};
//这里通过这个派生类 实例化出来了两个具体的对象,但是和C++不同的是
//这里实例化的对象 只分配内存 并没有通过构造函数初始化。在init函数中
//初始化
static struct vser_dev vsdev[2];

//应用层open("/dev/**",O_RDWR)
static int vser_open(struct inode *inode,struct file *filp)
{
    //多个cdev 构体的情况下每个cdev结构体对应一个设备实例。这里,设备实例和 cdev 结构体之间存在一对一的关系,
    //因此用container_of方式获取结构体更为直接。也可以用次设备号方式获取。
    filp->private_data=container_of(inode->i_cdev,struct vser_dev,cdev);
    //container_of 是一个宏,这是在Linux内核中设计巧妙经常使用的一个宏,这个宏
    //可以根据结构成员的地址,来反向获取到整个结构的起始地址。这里,我们因为inode
    //->i_cdev给出了struct vser_dev结构类型中成员cdev的地址。
    return 0;
}

static int vser_release(struct inode *inode,struct file *filp)
{
    return;
}
//对应read函数
static ssize_t vser_read(struct file *filp,char __user *buf,size_t count,loff_t *pos)
{
    unsigned int coied=0;
    struct vser_dev *dev=filp->private_data;//接到fil中的数据

    kfifo_to_user(dev->fifo,buf,count,&copied);

    return copied;
}

static ssize_t vser_write(struct file *filp,char __user *buf,size_t count,loff_t *pos)
{
    unsigned int coied=0;
    struct vser_dev *dev=filp->private_data;//接到fil中的数据

    kfifo_from_user(dev->fifo,buf,count,&copied);

    return copied;

}

static struct file_operaitons vser_ops={
    .ower=THIS_MODULE;
    .open=vser_open;
    .release=vser_release;
    .read=vser_read;
    .write=vser_write;
}

//static:C语言中没有C++中命名空间的概念,为了避免因重复命名而带来的重复定义
//问题,函数可以添加static关键字修饰,通过static关键字修饰后,函数的链接属性
//将成为内部属性,从而解决了该问题。
static int __init vser_init(void)
{
    int i;
    int ret;
    dev_t dev;

    dev=MKDEV(VSER_MAJOR,VSER_MINOR);
    ret=register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);//这里注册了两个设备
    if(ret)
    {goto reg_err;}//在内核编程中经常用到goto 处理错误信息,虽然这和应用层编程比有点违反常识。

    for(i=0;i<VSER_DEV_CNT;i++)//完成对实例化对象的初始化
    {
        cdev_init(&vsdev[i].cdev,&vser_ops);
        vsdev[i].cdev.owner=THIS_MODULE;
        vsdev[i].fifo=i==0?(struct kfifo*)&vsfifo0:(struct kfifo*)&vsfifo1;
        ret=cdev_add(&vsdev[i].cdev,dev+i,1);
        if(ret)
            goto add_err;
    }
    return 0;
    add_err:
        for(--i;i>0;--i)
        {
            cdev_del(&vsdev[i].cdev);
        }
        unregister_chrdev_region(dev,VSER_DEV_CNT);
    reg_err:
    return ret;
}

static void __exit vser_exit(void)
{
    int i;
    dev_t dev;

    dev=MKDEV(VSER_MAJOR,VSER_MINOR);

    for(i=0;i<VSER_DEV_CNT;i++)//注册多少进内核就要删除多少
    {
        cdev_del(&vsdev[i].cdev);
    }
    unregister_chrdev_region(dev,VSER_DEV_CNT);

}

module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
  1. 首先,在驱动的初始化函数 vser_init 中,每个设备结构体(vsdev[0]vsdev[1])都会调用 cdev_init 函数关联 vser_ops 文件操作结构体,然后调用 cdev_add 函数将每个设备的 cdev 结构体注册到内核。

  2. 当应用程序打开设备文件时,内核会根据设备文件的主设备号和次设备号找到对应的 cdev 结构体,并将其放入 inode->i_cdev 中。接下来,内核会调用驱动的 vser_open 函数。此时inode已经根据主设备号和次设备号,获取到了cdev结构体的地址。

  3. vser_open 函数中,通过调用 container_of(inode->i_cdev, struct vser_dev, cdev),可以根据 inode->i_cdev(即对应设备的 cdev 结构体)获取到设备结构体的地址。这个地址就是初始化vser_init函数中创建的 struct vser_dev 结构体实例(vsdev[0]vsdev[1])。

  4. read和write的实现:在 vser_open 函数中,当应用程序打开设备文件时,驱动会通过 container_of(inode->i_cdev, struct vser_dev, cdev) 获取当前设备的结构体地址,然后将这个地址存储到 filp->private_data。当应用程序调用 readwrite 时,内核会将包含 private_datafilp 结构传递给驱动的 vser_readvser_write 函数。这样,驱动就可以通过 filp->private_data 获取到当前操作的设备结构体指针,从而知道它正在操作哪个设备。

这是一个驱动支持多个设备中的 多个cdev支持多个设备的方法,此外还有一个cdev结构体支持多个设备的方法。1. 一个 cdev 对应多个设备的情况下,可以使用次设备号来区分不同的设备。在这种情况下,cdev 代表了一个设备类型,而次设备号用于表示该设备类型下的具体设备实例。在 open 函数中,您可以使用 iminor(inode) 函数获取当前操作设备的次设备号,然后根据次设备号查找和操作相应的设备实例。

2.在多个 cdev 结构体的情况下,每个 cdev 结构体对应一个设备实例。这里,设备实例和 cdev 结构体之间存在一对一的关系,因此您可以使用 container_of 宏根据当前 cdev 结构体获取到对应的设备结构体。当然,也可以使用次设备号来区分不同的设备,但由于 cdev 结构体已经与设备实例一一对应,所以使用 container_of 宏更为直接。

简而言之,无论是使用一个 cdev 对应多个设备,还是使用多个 cdev 结构体,次设备号都可以用来区分不同的设备实例。然而,在多个 cdev 结构体的情况下,使用 container_of 宏根据当前 cdev 结构体获取到对应的设备结构体更为方便。在一个 cdev 对应多个设备的情况下,您需要根据次设备号在驱动程序中查找和操作相应的设备实例。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值