裸机开发之驱动开发

发布时间 2023-06-26 10:40:26作者: 阿风小子
一、驱动开发的基础理解   
        在计算中,设备驱动程序是一种计算机程序,用于操作或控制连接到计算机的特定类型的设备。驱动程序提供了与硬件设备的软件接口,使操作系统和其他计算机程序可以访问硬件功能,而无需了解有关所使用硬件的精确细节。
 
驱动程序通过硬件连接到的计算机总线或通信子系统与设备进行通信。当调用程序调用驱动程序中的例程时,驱动程序向设备发出命令。设备将数据发送回驱动程序后,驱动程序可以调用原始调用程序中的例程。驱动程序依赖于硬件且特定于操作系统。它们通常为那些有必要的时间异步的硬件接口提供终端处理。
 
        驱动开发就是在操作系统的基础上实现驱动程序。也可以理解为驱动程序就是中间件,让应用层去控制底层寄存器进行工作。
 
        其实编写的驱动文件也是.c为结尾的文件,但是其在编译阶段是调用的内核来进行编译,所生成的也是满足该内核的驱动文件.ko结尾文件,所以在内核使用的时候也就是没问题的。
 
1、驱动框架
一个基本的驱动框架如下所示:        4部分组成
 
1、头文件
        #include <linux/init.h>
        #include <linux/module.h>
 
2、驱动入口函数的声明,在内核加载驱动时,执行哪个函数;在内核卸载驱动时,执行哪个函数
        module_init(hello_init);           //声明:加载时的入口声明
        module_exit(hello_exit);         //声明:卸载时的入口声明
 
3、加载函数、卸载函数的实现
        //加载函数的实现:当内核加载驱动(内核执行这个驱动时,就会调用的函数)
        static int __init hello_init(void)
        {
 
            return 0;
        }
 
        //卸载函数的实现:当内核卸载驱动(内核删除这个驱动时,就会调用的函数)
        static void __exit hello_exit(void)
        {
 
        }
 
4、协议选择GPL
        MODULE_LICENSE("GPL");
 
例如下面一个LED灯的示例程序
 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
 
unsigned int * gpx1con;
unsigned int * gpx1dat;
 
//驱动与应用程序函数关联
int led_open (struct inode * inode, struct file * file)
{
printk("led_open\n");
return 0;
}
 
int led_close (struct inode * inode, struct file * file)
{
printk("led_close\n");
return 0;
}
 
ssize_t led_read (struct file * file, char __user * data, size_t size, loff_t * ops)
{
//读取数据
printk("led_read");
copy_to_user(data,"nihao",6);
return 0;
}
ssize_t led_write (struct file * file, const char __user * data, size_t size, loff_t * ops)
{
int num = 0;
copy_from_user(&num,data,size);
printk("num = %d\n",num);
if(num == 1){
*gpx1dat |= 1;
}
else{
*gpx1dat &= ~1;
}
return 0;
}
 
const struct file_operations fops = {
.open = led_open,//当应用程序调用open时,驱动则执行结构体成员open对应的赋值函数
.release = led_close,
.read = led_read,
.write = led_write,
};
//入口实现
//加载 insmod
static int __init led_init(void)
{
//字符设备框架
//1、申请设备号,驱动必须有一个设备号来区别其他驱动
int ret = -1;
ret = register_chrdev(250,"led",&fops);
if(ret == 0){
printk("register ok\n");
}
//2、创建设备节点---设备文件
//创建设备节点信息结构体
struct class * cls_led = class_create(THIS_MODULE,"led cls");
//创建设备文件
dev_t devt = 250<<20 | 0;//设备号
struct device * led_dev = device_create(cls_led,NULL,devt,NULL,"led%d",3);
if(led_dev != NULL){
printk("device create ok\n");
}
//初始化硬件
//地址映射
gpx1con = ioremap(0x11000c20,4);//指针变量就是映射寄存器地址
*gpx1con = *gpx1con & ~(0xf) | 0x1;
gpx1dat = ioremap(0x11000c24,4);
*gpx1dat |= 1;
return 0;
}
//卸载
static void __exit led_exit(void)
{
与初始化逆序过程进行卸载
//1、映射释放(中断释放)
iounmap(映射的虚拟内存地址);----释放映射地址
//2、释放设备文件
void device_destroy(struct class * class,dev_t devt);
//3、释放设备文件结构体
void class_destroy(struct class * cls)
    //4、释放设备号
void unregister_chrdev(unsigned int major,const char * name)
}
//入口声明
module_init(led_init);
module_exit(led_exit);
//GPL声明
MODULE_LICENSE("GPL");
#include <sys/types.h>//进行应用的例程实现点灯
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
 
int main()
{
int a = 1;
int fd = open("/dev/led3",O_RDWR);
while(1)
{
    a = 1;
    write(fd,&a,4);
    sleep(1);
    a = 0;
    write(fd,&a,4);
    sleep(1);
}
close(fd);
return 0;
}
虽然有很多知识没学过,后面再进行介绍,这个时候看着或许有一些是不懂的
 
        驱动程序也就是让其通过文件方式进行控制底层的硬件寄存器,在驱动中实现文件io接口功能(与应用关联),应用程序调用文件io时,驱动程序也调用对应的文件io接口函数
    在结构体 struct file_operations 每一个成员变量都代表绑定一个系统调用(文件io)函数,只要对结构体中的成员赋值,就代表值绑定上一个文件io函数  
 
  struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        int (*iterate) (struct file *, struct dir_context *);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*aio_fsync) (struct kiocb *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
        int (*show_fdinfo)(struct seq_file *m, struct file *f);
    };//函数指针的集合,
        每个函数指针赋值为函数地址,就代表当应用程序调用对应的文件io函数时,驱动就执行函数指针赋值的对应函数,例如下面的LED驱动例程中的
 
const struct file_operations fops = {
.open = led_open,//当应用程序调用open时,驱动则执行结构体成员open对应的赋值函数
.release = led_close,
.read = led_read,
.write = led_write,
};
        当然,这些对应的函数则需要我们自己去按照格式进行操作,如open对应的led_open就是一个函数,看上面的例程就可以看到对应的操作。
 
在上例中的函数实现是将外面传输的数据是拷贝到另外一个地方的,保证不改变数据的内容
 
copy_to_user()
copy_from_user()
既将数据进行copy到那里去
 
        在基础的驱动框架中,有一个加载和卸载函数,这个函数是安装或者卸载这个驱动的将会去处理的函数。也就是这个里面写的这两个,一个对应初始化,而另外一个则对于着卸载的时候将会触发的。里面的led_init才是执行函数。
 
module_init(led_init);        //对应初始化
module_exit(led_exit);      //对应卸载
 
        最后还需要加入一个协议(通用公共许可协议GPL)才能进行在最后内核编译时通过,不然将发生内核污染的编译错误。
 
MODULE_LICENSE("GPL");        //GPL声明
 
2、驱动开发之字符设备驱动模型
I/O设备大致可以分为两类:块设备(block device)和字符设备(character device)。
 
        字符设备发送或接收的是字符流,而不考虑任何块结构。字符设备无法编址,也不存在任何寻址操作。打印机、网络接口、鼠标(用做指点设备),大多数与磁盘不同的设备均可被视为字符设备。
 
        块设备将信息存储在固定大小的块中,每个块都有自己的地址。数据块的大小通常在512字节到32768字节之间。块设备的基本特征是每个块都能独立与其他块而读写。磁盘是最常见的块设备。
 
我总结的是:只要不是存储在固定大小的地址中的设备都为字符设备。
 
   1、必须要有一个设备号,用于在内核中的众多设备驱动进行区分
 
    2、必须要有一个设备文件,用户必须知道设备驱动对应的设备节点(设备文件)
 
    3、驱动对设备的操作,与应用程序中的系统调用关联,其实就是文件操作
 
上面在led_init()函数里面就有了符号设备的操作了
 
//字符设备框架
//1、申请设备号,驱动必须有一个设备号来区别其他驱动
int ret = -1;
ret = register_chrdev(250,"led",&fops);
if(ret == 0){
printk("register ok\n");
}
//2、创建设备节点---设备文件
//创建设备节点信息结构体
struct class * cls_led = class_create(THIS_MODULE,"led cls");
//创建设备文件
dev_t devt = 250<<20 | 0;//设备号
struct device * led_dev = device_create(cls_led,NULL,devt,NULL,"led%d",3);
if(led_dev != NULL){
printk("device create ok\n");
}
 
其中使用到了一些不认识的函数,如下
 
register_chrdev()        //用于创建一个设备号
 
int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)
参数1:unsigned int major:主设备号,次设备号自动分配
设备号:32bit = 主设备号(12bit) + 次设备号(20bit)
主设备号:表示同一类设备;次设备号:表示同一类设备中的不同设备
参数2:const char *name:描述一个设备驱动信息,自定义
参数3:const struct file_operations *fops:文件操作对象,函数关联(使用结构体来存储驱动与应用程序的关联)
class_create()             //创建设备节点信息结构体
 
struct class * class_create(owner,name);
参数1:owner:拥有者,一般THIS_MODULE
参数2:name:字符串,描述信息
返回值:struct class *  信息结构体
device_create()      //创建设备文件
 
struct device *device_create( struct class *class,struct device *parent,dev_t devt,void *drvdata, const char *fmt, ...)
参数1:struct class *class:class结构体,创建的设备文件的信息内容。通过 class_create()函数创建
参数2:struct device *parent:表示父类对象,一般直接写NULL
参数3:dev_t devt:设备号
参数4:void *drvdata:私有数据,一般填NULL
参数5:const char *fmt, ...:设备文件名
返回值:struct device *---------设备节点对象(设备文件描述)
成功返回地址,失败返回NULL
        驱动控制硬件,控制外设,其实就是控制地址,通过地址往寄存器写入、读出控制内核驱动是通过虚拟地址操作,则就需要用到另外的函数,地址映射函数  ioremap
 
void * ioremap(cookie,size);
参数1:cookie:物理地址
参数2:size:映射内容大小,字节
返回值:返回映射成功后的虚拟内存地址,操作虚拟内存地址中的内容就是操作对应的物理地址空间内容
示例就如下:
 
//地址映射和硬件初始化
gpx1con = ioremap(0x11000c20,4);//指针变量就是映射寄存器地址
*gpx1con = *gpx1con & ~(0xf) | 0x1;
gpx1dat = ioremap(0x11000c24,4);
*gpx1dat |= 1;
到这里就已经将上面示例所用到的所有实例已经解释完了,看到这里再去看例程就可以看懂了。
 
注意:在卸载驱动的函数里,再写的时候要用创建时候的倒序进行释放。如下:
 
static void __exit led_exit(void)
{
与初始化逆序过程进行卸载
//1、映射释放(中断释放)
iounmap(映射的虚拟内存地址);----释放映射地址
//2、释放设备文件
void device_destroy(struct class * class,dev_t devt);
//3、释放设备文件结构体
void class_destroy(struct class * cls)
    //4、释放设备号
void unregister_chrdev(unsigned int major,const char * name)
}
二、文件IO模型
http://t.csdn.cn/e5u8z     这就是如何实现中断的文章,可以去了解哈,因为后面的文件IO模型就是根据中断来进行演示的。
 
文件io模型一共分为四种
 
1、阻塞        2、非阻塞        3、IO多路复用        4、异步信号
 
1、阻塞io模型------休眠等待
阻塞:当进程读取外部资源(数据),但是外部资源没有准备好,进程就进行休眠等待资源可用
 
在使用这个时候必须要知道那些能阻塞那些不能阻塞不然容易出现问题的。
 
在应用中:read、wirte、accept、scanf 默认都是阻塞的
 
那么如何在驱动中实现阻塞:
 
1.1 创建一个等待队列的头,用于判断是否接收到数据的
 
wait_queue_head_t head;
init_waitqueue_head(&head);
1.2 在需要等待的位置(没有数据),就阻塞等待
 
wait_event_interruptible(wq,condition)-----根据参数是否进行阻塞等待,完成阻塞等待
参数1:wq:等待队列头,把当前进程加入到哪个等待队列中
参数2:condition:是否执行阻塞等待的条件
        condition:真---不进行阻塞
        condition:假---进行阻塞
1.3 2、合适位置进行阻塞唤醒
 
wake_up_interruptible(&head);
实现例程:
 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>
 
struct key_node
{
unsigned int major;
struct class * cls;
struct device * dev;
dev_t devno;
wait_queue_head_t head;
};
char data;//获取硬件数据存储的变量
int condition;
struct key_node key;
 
ssize_t key_read (struct file * file, char __user * buf, size_t size, loff_t * ops)
{
wait_event_interruptible(key.head,condition);//阻塞等待
int ret;//拷贝数据
printk("key read\n");
ret = copy_to_user(buf,&data,1);
condition = 0;//下一次能够阻塞
return 0;
}
int key_open (struct inode * inode, struct file * file)
{
printk("key open\n");
return 0;
}
int key_release (struct inode * inode , struct file * file)
{
printk("key close\n");
return 0;
}
irqreturn_t key_irq_handler(int irqno, void * dev)//实现中断处理函数
{
//获取到数据
data = 'q';
condition = 1;
printk("irqno is %d\n",irqno);
printk("input char '%c'\n",data);//可以认为是一个字符‘q’,也可以从数据寄存器中获取真实的值
//唤醒,把进程从等待队列中拿出来继续执行
wake_up_interruptible(&(key.head));
return IRQ_HANDLED;
}
const struct file_operations fops = {
.open = key_open,
.release = key_release,
.read = key_read,
 
};
static int __init key_drv_init(void)
{
//1、申请设备号
key.major = 230;
register_chrdev(key.major,"key drv",&fops);
//2、创建设备节点
key.cls = class_create(THIS_MODULE,"cls");
key.devno = MKDEV(key.major,0);
key.dev = device_create(key.cls,NULL,key.devno,NULL,"key3");
//4、硬件初始化
//a、获取中断号
struct device_node * node = of_find_node_by_path("/key3_node");
int irqno = irq_of_parse_and_map(node,0);
//b、申请中断
request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);
//irqreturn_t (*handler)(int, void *)函数指着
//handler = key_irq_handler
//创建等待队列头
init_waitqueue_head(&(key.head));
return 0;
}
static void __exit key_exit(void)
{}
module_init(key_drv_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
 
如果按键没有按下,则一直卡在那个地方。知道按键按下(中断发生)
 
2、非阻塞--------快速通过
非阻塞:在进行读写操作时,如果没有数据,就立即返回,如果有数据读取数据然后立即返回
 
和上面所用到的函数都是一样的,只是差距在判断condition是否为真,为真就不阻塞。
 
3、IO多路复用
4、异步信号
        异步通知(异步信号):当有数据的时候,驱动就会发送信号(SIGIO)给应用,应用就可以异步接收数据,而不用主动接收(可以去完成其他工作)。
 
        异步信号需要 分为两步,一步是触发这个异步信号,另一个则是收到这个异步信号进行相应的处理操作,一般触发信号是在驱动中去完成什么去触发这个信号,而处理操作则是在应用程序里进行处理。
 
应用程序的流程(处理信号):
 
signal(SIGIO,catch_signal);        //1、设置信号处理方式,catch_signal是处理函数
 
fcntl(fd,F_SETOWN,getpid());     //2、设置当前进程为SIGIO信号的属主进程  
//3、将io模式设置为异步模式        ----异步信号设置
        int flags = fcntl(fd,F_GETFL);
        flags |= FASYNC;//添加异步属性
        fcntl(fd,F_SETFL,flags);
 
驱动程序的流程(发送信号):
 
1、需要和应用进程进行关联-------信号要发送给谁
      实现fasync接口,在接口中进行关联
        int key_fasync (int fd , struct file * file, int no)
         {
 
                 //记录信号发送给哪个应用
 
                return fasync_helper(fd,file,no,&(key.fasync));       
          }
2、在合适位置发送信号
                kill_fasync(&(key.fasync),SIGIO,POLLIN);
 
下面看一下修改后的驱动程序是怎么样的
 
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/poll.h>
 
struct key_node
{
unsigned int major;
struct class * cls;
struct device * dev;
dev_t devno;
wait_queue_head_t head;
struct fasync_struct * fasync;
};
 
char data;
int condition = 0;
struct key_node key;
 
ssize_t key_read (struct file * file, char __user * buf, size_t size, loff_t * ops)
{
 
if(((file->f_flags & O_NONBLOCK) != 0) && (condition == 0))//设置非阻塞
{    //不管是阻塞还是非阻塞,只要有数据都要读取数据继续执行
return -1;
}
wait_event_interruptible(key.head,condition);//阻塞等待
int ret;    //拷贝数据
printk("key read\n");
ret = copy_to_user(buf,&data,1);
condition = 0;    //下一次能够阻塞
data = '\0';    //把存储数据的缓冲区清空
return 1;
}
 
int key_open (struct inode * inode, struct file * file)
{
printk("key open\n");
return 0;
}
int key_release (struct inode * inode , struct file * file)
{
printk("key close\n");
return 0;
}
 
irqreturn_t key_irq_handler(int irqno, void * dev)    //实现中断处理函数
{
//获取到数据
data = 'q';
condition = 1;//变成非阻塞模式
printk("input char '%c'\n",data);//可以认为是一个字符‘q’,也可以从数据寄存器中获取真实的值
wake_up_interruptible(&(key.head));    //唤醒,把进程从等待队列中拿出来继续执行
kill_fasync(&(key.fasync),SIGIO,POLLIN);//发送信号
return IRQ_HANDLED;
}
 
int key_fasync (int fd , struct file * file, int no)
{
printk("key fasync \n");
return fasync_helper(fd,file,no,&(key.fasync));//记录信号发送给哪个应用
}
 
const struct file_operations fops = {
.open = key_open,
.release = key_release,
.read = key_read,
.fasync = key_fasync,
 
};
 
static int __init key_drv_init(void)
{
//1、申请设备号
key.major = 230;
register_chrdev(key.major,"key drv",&fops);
//2、创建设备节点
key.cls = class_create(THIS_MODULE,"cls");
key.devno = MKDEV(key.major,0);
key.dev = device_create(key.cls,NULL,key.devno,NULL,"key3");
//4、硬件初始化
//a、获取中断号
struct device_node * node = of_find_node_by_path("/key3_node");//获取设备树中的要使用的硬件节点
int irqno = irq_of_parse_and_map(node,0);    //获取节点中的中断号
//b、申请中断
init_waitqueue_head(&(key.head));    //创建等待队列头
request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);
return 0;
}
 
static void __exit key_exit(void)
{
}
 
module_init(key_drv_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
 
应用程序则是:
 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
 
int fd;
 
void catch_signal(int signo)
{
char num;
if( read(fd,&num,1) < 0)
{
printf("no data;");
return ;
}
printf("data is %c\n",num);
}
 
int main()
{
fd = open("/dev/key3",O_RDONLY);//阻塞
if(fd < 0)
{
perror("open error");
return -1;
}
signal(SIGIO,catch_signal);//1、设置信号处理方式
fcntl(fd,F_SETOWN,getpid());//2、设置当前进程为SIGIO信号的属主进程
int flags = fcntl(fd,F_GETFL);//3、将io模式设置为异步模式
flags |= FASYNC;//添加异步属性
fcntl(fd,F_SETFL,flags);
//完成其他功能操作(没有按下时打印)
while(1)
{
printf("hello world\n");
sleep(1);
}
close(fd);
return 0;
}
 
  5、分段处理中断数据(数据量耗时时用)
        中断实际上在处理上,实现的原则是“快进快出”,所以在遇见信息量太大,而无法完成快进快出的时候,则需要将信息进行分段处理。一部分先处理,剩下的在进行处理,也就是让处理器先处理一部分,然后处理器回到中断那里,而剩下的部分在进行处理。
 
则一般有下面三种方式(其实第二种方式已经包含了第一种方式)
 
        1、softirq:软中断,处理级别比较高,在内核机制中,需要修改内核源码功能
        2、tasklet:实际上就是内部调用了softirq
        3、workqueue:工作队列
 
tasklet:    
 
 初始化任务队列
void tasklet_init(struct tasklet_struct * t,void(* func)(unsigned long),unsigned long data)
参数1:struct tasklet_struct * t :任务队列头节点
参数2:void(* func)(unsigned long):下半部分的实现逻辑
参数3:unsigned long data:参数2函数的参数
在中断上半部分中,启动下半部分(放入内核线程中)
tasklet_schedule(struct tasklet_struct * t)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/poll.h>
 
struct key_drv
{
unsigned int major;
struct class * cls;
struct device * dev;
struct tasklet_struct t;//任务队列头
int irqno;
};
 
struct key_drv key;
irqreturn_t key_irq_handler(int irqno, void * dev)//中断处理--------中断上半部分
{
 
printk("%s\n",(char *)dev);
printk("key data\n");
tasklet_schedule(&(key.t));
return IRQ_HANDLED;
}
 
void key_func(unsigned long data)
{
printk("data is %d\n",data);
}
const struct file_operations fops;
static int __init key_drv_init(void)
{
//1、申请设备号
key.major = 250;
register_chrdev(key.major,"key int",&fops);
//2、创建设备节点
key.cls = class_create(THIS_MODULE,"cls");
key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key");
//获取到设备号
//获取设备树中的要使用的硬件节点
struct device_node * node = of_find_node_by_path("/key3_node");
//获取节点中的中断号
key.irqno = irq_of_parse_and_map(node,0);
//实现添加中断下半部分-------初始化任务队列
tasklet_init(&(key.t),key_func,45);
request_irq(key.irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt","key down");//申请中断
return 0;
}
 
static void __exit key_drv_exit(void)
{
free_irq(key.irqno,"key down");    //中断释放
device_destroy(key.cls,MKDEV(key.major,0));    //释放设备节点
class_destroy(key.cls);    //释放结构体
unregister_chrdev(key.major,"key int");    //释放设备号
}
 
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
workqueue:
 
三、系统总线
四、平台总线