linux Pci字符驱动基本加载流程

今天有朋友问我linux系统Pci字符驱动加载流程,简单整理了一下,顺便做个记录。

首先说下需要包含的头文件:
一个完整的字符驱动一般包含下面这些头文件:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/fcntl.h>
#include <linux/cdev.h>
#include <linux/version.h>
#include <linux/vmalloc.h>
#include <linux/ctype.h>
#include <linux/pagemap.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/io.h>
#include <linux/dma-mapping.h>
#include <linux/pci.h>
#include <linux/device.h>

当然,可以根据实际使用情况增加头文件。

首先必须要有的函数:
module_init(Pci_init_module);
初始化模块函数,挂载驱动时调用这个函数;

static int Pci_init_module(void);
这个需要开发者自己实现的函数,具体实现方式需要结合业务需求。这个函数由初始化模块函数调用;

module_exit(Pci_cleanupmodule);
卸载驱动函数,输入卸载指令时调用这个函数;

static void Pci_cleanupmodule(void);
这个需要开发者自己实现的函数,用来释放驱动执行过程中分配的内存。这个函数由卸载模块函数调用;

MODULE_AUTHOR();
开发者名称注册函数(可以是驱动名称也可以是开发者名称);

MODULE_LICENSE(“GPL”);驱动采用的开放协议,一般默认采用"GPL"方便动态挂载驱动。

下面这些函数在真实驱动中会用到:
static int __init pci_probe(struct pci_dev *dev, const struct pci_device_id *id);
驱动探测函数,用来探测当前保存的设备信息是否有匹配的设备,匹配成功后就可以访问设备了;

static int Pci_open(struct inode *inode,struct file *filp);
这个函数由用户层软件调用,当使用open函数打开驱动时,将会调用这个函数。这个函数的作用是从设备树中拿出设备描述符保存入自己的设备描述符中,后续操作中会使用到;

static int Pci_release(struct inode *inode,struct file *filp);
这个函数由用户层软件调用,当使用close函数关闭驱动时,将会调用这个函数。一般这个函数不需要实现具体内容,直接“return 0;”即可;

static ssize_t Pci_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos);
这个函数由用户层软件调用,当使用read函数读取设备数据时,将会调用这个函数;

static ssize_t Pci_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos);
这个函数由用户层软件调用,当使用write函数向设备写入数据时,将会调用这个函数;

static int Pci_ioctl(/*struct inode inode,/struct file *filp,unsigned int cmd,unsigned long arg);
这个函数由用户层软件调用,当使用ioctl函数时用来操作设备虚拟内存空间(由设备物理地址映射到系统虚拟地址的空间,从bar0到bar5六个空间,实际设备中一般实现两到三个空间,其中bar0可以作为io空间或内存空间,bar1到bar5一般为内存空间);
特别说明:这个函数完整体是在2.6.11内核及以上版本的实现,在3.2.5内核(具体记不太清除了…)及以上版本需要注释掉inode参数,实际使用过程中用不到这个参数(在Pci_open函数已经实现了设备描述符的保存);

static loff_t Pci_llseek(struct file *filp,loff_t off,int whence);
这个函数由用户层软件调用,当使用seek函数对驱动中的数据进行跳转定位;

static int scan_bars(struct pcie_dma_bookkeep *bk_ptr, struct pci_dev *dev);
一般用作打印bar0到bar5的相关描述信息,比如bar0的起始地址与结束地址等;

static int __init map_bars(struct pcie_dma_bookkeep *bk_ptr, struct pci_dev *dev);
设备物理地址映射到内核虚拟内存的实现和相关描述信息,比如映射后的bar0的起始地址与结束地址等;

static void unmap_bars(struct pcie_dma_bookkeep *bk_ptr, struct pci_dev *dev);
回收映射的bar内存,并初始化bar结构。

下面介绍一下真实驱动用到的相关结构:
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = Pci_open,
.release = Pci_release,
};
设备函数回调结构,其中这三个必须填写,当驱动挂载成功后,用户空间调用相关函数时(比如open函数),将会通过这个结构体对象找到绑定的Pci_open函数进行执行,其中的操作流程是:由文件句柄在设备树中进行遍历并找到匹配的文件句柄,然后获取到当前驱动的设备描述符,通过设备描述符进行相关的操作(其中用到的是抽象概念,系统提供具体函数格式,这些函数实现就是上面讲的由自己来实现,系统只是负责调用这些函数);

static struct pci_device_id pci_ids[] = {
{ PCI_DEVICE(MANF_ID,MODEL_CODE) },
{ 0 }
};
设备标识结构,MANF_ID是设备厂商ID,MODEL_CODE产品ID,这些都是由设备厂商进行提供;

static struct pci_driver driver_ops = {
.name = AMC4401ADEVNAME,
.id_table = pci_ids,
.probe = pci_probe,
.remove = pci_remove,
};
设备探测结构,如果打算把驱动注册到设备树中需要进行实现,探测到设备后将会把设备描述符加入设备树中。

驱动在计算机中的意义:
计算机系统的实现的初衷是为了便于管理化、高效率访问设备。驱动的实现是为了让系统避免一些繁琐、无意义重复的工作。
计算机系统提供了统一的驱动编写结构,不仅省去了重复的驱动框架编写(比如字符驱动只需编写一套完整的代码便可重复利用),而且得到了较高的执行效率,也不需要为了一款驱动改写内核文件;
设备树就像我们的执行程序,驱动可以看做是执行程序其中的一个库文件(比喻成库文件是因为系统不需要知道驱动如何实现,它只需要知道相关信息即可,当然驱动设计不合理也会造成系统崩溃等问题);

前面啰嗦这么多是为了下面做基础,下面描述字符驱动执行基本流程:

首先进入初始化函数:
1.分配一个设备对象(设备结构一般根据需求进行编写);
2.注册pci设备,传入设备探测结构对象;

注册pci设备成功后,进入探测函数:
1.设置设备私有数据;
2.使用自定义设备名称注册设备号(设备号由系统生成,也可以手动设置设备号进行注册);
3.创建class(设备驱动管理使用,拷贝设备信息到/dev路径下);
4.设备描述符初始化并绑定设备函数回调结构对象;
5.设备描述符绑定设备号;
6.检测设备是否有效(如果没有检测到匹配的设备,不再继续执行);
7.检测设备请求信息(比较当前自定义设备名称与设备描述符中的设备名称是否相同);
8.设置设备工作在总线主设备模式;
9.判断msi是否有效(有效表示有硬中断能力);
10.获取bar信息(bar0到bar5的地址);
11.映射bar内存到虚拟内存;
12.设置dma相关配置(如果存在dma);

一般执行这些步骤,驱动基本加载完成了,剩下就是应用层的操作了。

发布了87 篇原创文章 · 获赞 195 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/a29562268/article/details/84778768