Linux中断

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

使用Linux内核提供的API函数,完成中断的驱动开发。

一、中断的相关概念

1.中断号

中断号的作用是用来区分不同的中断类型,数据结构是int类型。

编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因
此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。

2.中断的申请和释放

Linux中使用中断要向内核提交申请,使用完成后要释放掉中断。

申请API函数如下:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
 const char *name, void *dev)
函数参数和返回值含义如下:
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将
dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经
被申请了

下图提供常用的中断标志
在这里插入图片描述
注意:request_irq函数可能会导致休眠,因此不能在中断上下文或者其他禁止休眠的代码段中使用 request_irq 函数。request_irq 函数会自动激活(使能)中断,所以不需要我们手动去使能中断。

释放API函数如下:

void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。
共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。

注意:如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断

中断处理函数如下:

irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就
是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,
dev 也可以指向设备数据结构。
中断处理函数的返回值为 irqreturn_t 类型。其定义如下:
10 enum irqreturn {
11 IRQ_NONE = (0 << 0),
12 IRQ_HANDLED = (1 << 0),
13 IRQ_WAKE_THREAD = (1 << 1),
14 };
15
16 typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值
一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)

使能和禁止中断

void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。那如何处理上述情况的使用如下函数:

void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

上述三个函数是使能或者禁止某一中断,有时候需要使能和关闭整个系统的中断,使用如下函数:

local_irq_enable()
local_irq_disable()

在使用全局中断的时候有个问题,比如当程序A执行关闭10S中断,当2秒后程序B也执行了关闭全局中断,但过了3秒后,B程序又启用了全局中断,但这时候程序A关闭10S还没有结束,这种情况严重会导致系统崩溃。那该怎么解决呢,Linux中提供如下函数解决整个问题

local_irq_save(flags)
local_irq_restore(flags)

这两个函数是成对出现的,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。

二、上半部分和下本部分

当发生中断的时候,会进入中断处理函数,在函数中,我们希望时间越小越好,但往往事与愿违,在时间驱动编写的时候,在中断处理函数中,需要花费的时间不小,比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅响应中断,然后清除中断标志位即可,这种方式就是Linux处理中断的过程:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可
以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部
去执行,这样中断处理函数就会快进快出。
比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,当然了也是有些经验可以参考的:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
那如何将上半部分和下半部分连接起来呢,Linux提供许多方法,可以百度查看一下,下面提供两种常用的方式

1.tasklet

Linux 内核使用 tasklet_struct 结构体来表示 tasklet:

484 struct tasklet_struct
485 {
486 struct tasklet_struct *next; /* 下一个 tasklet */
487 unsigned long state; /* tasklet 状态 */
488 atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
489 void (*func)(unsigned long); /* tasklet 执行的函数 */
490 unsigned long data; /* 函数 func 的参数 */
491 };

第 489 行的 func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理
函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:

void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), 
unsigned long data);
函数参数和返回值含义如下:
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
返回值:没有返回值


也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化,
DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:
DECLARE_TASKLET(name, func, data)
其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func
就是 tasklet 的处理函数,data 是传递给 func 函数的参数。

在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:

void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值:没有返回值

使用tasklet的格式如下:

/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
 /* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
 ......
 /* 调度 tasklet */
 tasklet_schedule(&testtasklet);
 ......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
 ......
 /* 初始化 tasklet */
  tasklet_init(&testtasklet, testtasklet_func, data);
 /* 注册中断处理函数 */
 request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
 ......
}

2.工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的
工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:

#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原
型如下所示:
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。

格式如下:

/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
 /* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
 ......
 /* 调度 work */
 schedule_work(&testwork);
 ......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
 ......
 /* 初始化 work */
 INIT_WORK(&testwork, testwork_func_t);
 /* 注册中断处理函数 */
 request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
 ......
}

三、设备树中对于中断的描述

如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。
在imx6ull中描述中断控制器的节点在imx6ull.dtsi文件中

intc: interrupt-controller@00a01000 {
		compatible = "arm,cortex-a7-gic";
		#interrupt-cells = <3>;
		interrupt-controller;
		reg = <0x00a01000 0x1000>,
		      <0x00a02000 0x100>;
	};

compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到中断控制器驱动文件。
#interrupt-cells属性,描述了cells大小,可以在绑定文档中查看其含义:
在这里插入图片描述
第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断
号的范围为 0~15。
第三个 cells:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候
表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。bit[15:8]为 PPI 中断的 CPU 掩码。
interrupt-controller 节点为空,表示当前节点是中断控制器

对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内
容如下所示:

	gpio5: gpio@020ac000 {
				compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
				reg = <0x020ac000 0x4000>;
				interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
					     <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
				gpio-controller;
				#gpio-cells = <2>;
				interrupt-controller;
				#interrupt-cells = <2>;
			};

interrupts描述中断源信息,属性值大小按照intc中设置填写,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1:
在这里插入图片描述
从上图中可以看出,GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应 GPIO5_IO00-GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。
将#interrupt-cells 修改为 2,为什么是2呢,查看绑定信息
在这里插入图片描述
此绑定信息在目录\Documentation\devicetree\bindings\gpio
在这里插入图片描述
上边是NXP官方写的设备树,如果用户要使用该如何使用呢,下面举个例子:

1 fxls8471@1e {
2 compatible = "fsl,fxls8471";
3 reg = <0x1e>;
4 position = <0>;
5 interrupt-parent = <&gpio5>;
6 interrupts = <0 8>;
7 };
fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片,fxls8471 有一个中断引脚链接
到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00。
第 5 行,interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
第 6 行,interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。
简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器

三、获取中断号

编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因
此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:

unsigned int irq_of_parse_and_map(struct device_node *dev,
 int index)
函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如
下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。

四、实例如下

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/export.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/sched.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: imx6uirq.c
作者	  	: 车文超
版本	   	: V1.0
描述	   	: Linux中断驱动实验
其他	   	: 无
论坛 	   	: 
日志	   	: 
***************************************************************/
#define IMX6ULL_CNT		1			    /* 设备号个数 	*/
#define IMX6ULL_NAME		"imx6ull"	/* 名字 		*/

#define KEY0VALUE 0X01 /* KEY0 按键值 */
#define INVAKEY 0XFF /* 无效的按键值 */
#define KEY_NUM 1 /* 按键数量 */
/*按键设备*/
struct key_dev{
	int gpioid;		            /*gpio编号*/
	unsigned char value;		/*按键保存值*/
	char name[10];				/*按键名字*/
	int irqnum;				/*中断号*/
	irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};

/* imx6uirq设备结构体 */
struct imx6ull_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	struct key_dev key0dev;	/*按键设备*/
	struct timer_list timerdev;/*定时器设备*/
	atomic_t keyvalue;/*按键值*/
	atomic_t release;/*按键释放值*/

};

struct imx6ull_dev imx6ulldev;	/* imx6ulldev */
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6ull_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6ulldev;	/* 设置私有数据 */
	return 0;
}

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t imx6ull_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	
	struct imx6ull_dev *dev = (struct imx6ull_dev *)filp->private_data;

	int ret = 0;
	int value = 0;
	int release = 0;

	value = atomic_read(&dev->keyvalue);
	release = atomic_read(&dev->release);
	if(release ==1){
		if(value & 0x80){
			value &= ~0x80;
			ret = copy_to_user(buf,&value,sizeof(value));
		}else{
			goto data_error;
		}
		atomic_set(&dev->release,0);
	}else{
		return -EINVAL;
	}
	return ret;
data_error:
	return -EINVAL;
}

/* 设备操作函数 */
static const struct file_operations imx6ull_fops = {
	.owner = THIS_MODULE,
	.open = imx6ull_open,
	.read = imx6ull_read,
};
/*
 * @description	: 中断处理函数
 * @param 		: 无
 * @return 		: 无
 */
static irqreturn_t key0_irq(int irq, void *dev_id){

		
		struct imx6ull_dev *dev = (struct imx6ull_dev *)dev_id;
#if 0
		value = gpio_get_value(dev->key0dev.gpioid);
		printk("value=%d\n\r", value);
		if(value == 0){
			printk("key0按下");
		}
		if(value ==1){
			printk("key0抬起");
		}
#endif
     dev->timerdev.data = (unsigned long)dev_id;
	 mod_timer(&dev->timerdev, jiffies + msecs_to_jiffies(10));
		return 0;
}
/*
 * @description	: 定时器中断函数
 * @param 		: 无
 * @return 		: 无
 */
static void timer_func(unsigned long data)
{
	int value = 0;
    struct imx6ull_dev *dev = (struct imx6ull_dev *)data;
    value = gpio_get_value(dev->key0dev.gpioid);
		printk("value=%d\n\r", value);
		if(value == 0){
			printk("key0按下");
			atomic_set(&dev->keyvalue, dev->key0dev.value);
		}
		if(value ==1){
			printk("key0抬起");
			atomic_set(&dev->keyvalue, (dev->key0dev.value)|0x80);
			atomic_set(&dev->release,1);
		}
   
}


/*
 * @description	: 按键初始化函数
 * @param 		: 无
 * @return 		: 无
 */
static int key_myinit(struct imx6ull_dev *dev_id){
	int ret = 0	;
	
	struct imx6ull_dev *dev = (struct imx6ull_dev *)dev_id;
	/*1.获取节点*/
	dev->nd = of_find_node_by_path("/key");
	if(dev->nd == NULL){
		printk("of_find_node_by_path_erro\n\r");
        return -1;
	}
	/*2.获取GPIO编号*/
	dev->key0dev.gpioid = of_get_named_gpio(dev->nd, "key-gpios", 0);
    if(dev->key0dev.gpioid < 0){
            printk("of_get_named_gpio_erro\n\r");
            return -1;
    }
	/*3.申请IO*/
	
		memset(dev->key0dev.name, 0, sizeof(dev->key0dev.name));
		//sprintf(dev->keydev[i].name,"KEY%d",i);
		sprintf(dev->key0dev.name,"key%d",0);
		ret = gpio_request(dev->key0dev.gpioid,dev->key0dev.name);
		if(ret < 0){
            printk("gpio_request_erro\n\r");
            return -1;
        }
		gpio_direction_input(dev->key0dev.gpioid);
		/*4中断号获取*/
		//dev->keydev[i].irqnum = gpio_to_irq(dev->keydev[i].gpioid);
		dev->key0dev.irqnum = irq_of_parse_and_map(dev->nd,0);
		printk("dev->key0dev.irqnum=%d\n\r",dev->key0dev.irqnum);
	
	  dev->key0dev.handler = key0_irq;
	  dev->key0dev.value = KEY0VALUE;
	/*5.注册中断*/
		 ret = request_irq(dev->key0dev.irqnum, dev->key0dev.handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,dev->key0dev.name, &imx6ulldev);
		printk("ret=%d\n\r",ret);
        if(ret){
            printk("request_irq_erro\n\r");
			//goto file_irq;
            return -1;
        }
	
   /*初始化定时器*/
    init_timer(&imx6ulldev.timerdev);
	imx6ulldev.timerdev.function = timer_func;
   
  
//file_irq:
       // free_irq(dev->key0dev.irqnum, &imx6ulldev);
return ret;
}
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init imx6ull_init(void)
{
	/* 1、构建设备号 */
	if (imx6ulldev.major) {
		imx6ulldev.devid = MKDEV(imx6ulldev.major, 0);
		register_chrdev_region(imx6ulldev.devid, IMX6ULL_CNT, IMX6ULL_NAME);
	} else {
		alloc_chrdev_region(&imx6ulldev.devid, 0, IMX6ULL_CNT, IMX6ULL_NAME);
		imx6ulldev.major = MAJOR(imx6ulldev.devid);
		imx6ulldev.minor = MINOR(imx6ulldev.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&imx6ulldev.cdev, &imx6ull_fops);
	cdev_add(&imx6ulldev.cdev, imx6ulldev.devid, IMX6ULL_CNT);

	/* 3、创建类 */
	imx6ulldev.class = class_create(THIS_MODULE, IMX6ULL_NAME);
	if (IS_ERR(imx6ulldev.class)) {
		return PTR_ERR(imx6ulldev.class);
	}

	/* 4、创建设备 */
	imx6ulldev.device = device_create(imx6ulldev.class, NULL, imx6ulldev.devid, NULL, IMX6ULL_NAME);
	if (IS_ERR(imx6ulldev.device)) {
		return PTR_ERR(imx6ulldev.device);
	}
	/*key初始化*/
	key_myinit(&imx6ulldev);

	atomic_set(&imx6ulldev.keyvalue, INVAKEY);
	atomic_set(&imx6ulldev.release, 0);
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6ull_exit(void)
{
   /*释放中断号,释放IO*/
	 free_irq(imx6ulldev.key0dev.irqnum, &imx6ulldev);
	 gpio_free(imx6ulldev.key0dev.gpioid);
   /*删除定时器*/
    del_timer_sync(&imx6ulldev.timerdev);
   /*删除设备 */
    /*删除字符设备 */
	cdev_del(&imx6ulldev.cdev);
    /*删除设备号 */
	unregister_chrdev_region(imx6ulldev.devid, IMX6ULL_CNT);
    /*删除设备 */
	device_destroy(imx6ulldev.class, imx6ulldev.devid);
    /*删除类 */
	class_destroy(imx6ulldev.class);
}

module_init(imx6ull_init);
module_exit(imx6ull_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chewenchao");

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值