RK3399—中断

  中断是操作系统最常见的事件之一,无论是系统层的“软中断”还是CPU底层的“硬中断”都是编程时常用的。中断的作用之一是充分利用CPU资源,正常情况下,CPU执行用户任务,当外设触发中断产生时,CPU停止当前任务,转而去处理中断信息。处理完中断再返回任务处继续执行。


  对于硬中断,顾名思义,由硬件产生,CPU定时器、各类总线、GPIO以及外设键盘、磁盘、鼠标等。对于嵌入式来说,触摸屏、传感器等都可以产生中断信号。硬中断处理要实时和高效率,一般由驱动层处理。


  对于软中断,是由操作系统实现,不会直接中断CPU。操作系统在任务管理、调度过程会有软中断过程。对于驱动层面,软中断往往结合硬中断一起使用。


1. linux中断

1.1 中断上半部和下半部

  中断的基本原则是“快速执行完并退出”,但一些设备中往往需要处理大量的耗时事务。不同于裸机编程,操作系统是多个进程和多个线程执行,宏观上达到并行运行的状态,外设中断则会打断内核中任务调度和运行,及屏蔽其外设的中断响应,中断函数耗时过长会使得系统实时性和并发性降低。为了提高系统的实时性和并发性,linux内核将中断处理程序分为上半部(top half)和下半部(bottom half)。上半部分任务比较少,处理一些寄存器操作、时间敏感任务,以及“登记中断”通知内核及时处理下半部的任务。下半部分,则负责处理中断任务中的大部分任务,特别是耗时任务必需放在下半部。

以触摸屏外设为例:

中断上半部:有触摸信号时,产生一个中断通知CPU,驱动负责将中断信息登记到内核,并通知内核处理, 然后退出中断。
中断下半部:内核获取中断信息,读取触摸屏数据返回给系统使用。


中断上下部区别:

  • 上半部由外设中断触发,下半部由上半部触发。
  • 上半部不会被打断,下半部可以被其他中断打断。
  • 上半部分处理时间敏感任务,主要任务、耗时任务放在下半部。

1.2 中断设计

  一个完整的中断程序由上半部和下半部分共同构成,在编写设备驱动程序前,就需考虑好上半部和下半部的分配。很多时候上半部与下半部并没有严格的区分界限,主要由程序员根据实际设计,如某些外设中断可以没有下半部。关于上下半部的划分原则,就是主要事务、耗时事务划分在下半部处理。


可以参考以下原则:

  • 与硬件相关的操作,如寄存器访问,必须放在上半部。
  • 对时间敏感、要求实时性的任务放在上半部。
  • 该任务不能被其他中断或者进程打断的放在上半部。
  • 实时性要求不高的任务、耗时任务放在下半部。

1.3 中断上半部实现

  上半部中断一般包括几个步骤

  • 硬件相关中断配置
  • 中断回调函数
  • 中断号申请
  • 中断注册

1.3.1 中断回调函数

  该部分为真正的中断上半部,处理时间敏感任务和中断状态清除,同时触发内核调度下半部。 中断回调函数类型,是一个函数指针,位于“kernel/include/linux/interrupt.h”中。

typedef irqreturn_t (*irq_handler_t)(int, void *);
  • 参数1,中断号。
  • 参数2,通用void指针,一般指向设备数据结构体,引用前通过强制转换获取设备私有信息。
  • 返回值,irqreturn_t 枚举类型,位于“kernel/include/linux/irqreturn.h”。
/**
 * enum irqreturn
 * @IRQ_NONE		interrupt was not from this device or was not handled
 * @IRQ_HANDLED		interrupt was handled by this device
 * @IRQ_WAKE_THREAD	handler requests to wake the handler thread
 */
enum irqreturn {
	IRQ_NONE		= (0 << 0),	/* 收到的中断信号与注册中断源信号不一致 */
	IRQ_HANDLED		= (1 << 0),	/* 接收到正确中断信号,并且作相应处理 */
	IRQ_WAKE_THREAD		= (1 << 1),
};

typedef enum irqreturn irqreturn_t;


中断函数是不存在返回值的,该回调函数返回值,表示系统响应中断信号的处理状态。


1.3.2 中断号

  系统中断源是多元的,linux内核会给每个中断源分配一个唯一的中断号,用以区分不同设备的中断。终端号为一个int类型的整数。引入设备树后,中断号一般在会设备树中设备节点描述了,驱动程序通过指定函数获取。


  • 从设备树获取中断号
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
参数 含义
引用 #include <linux/of_irq.h>
node 设备节点
index 索引序号,获取指定序号中断信息(如果有多个),只有一个中断信息填0
返回 成功返回中断号,失败返回负数
  • 对于gpio,可以通过gpio序号转为中断号
int gpio_to_irq(unsigned gpio)	/* 把gpio序号转换为中断号 */

1.3.3 中断注册

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)

  request_ir用于中断注册,同时函数内部会使能中断,不需手动再去使能。

参数 含义
引用 #include <linux/interrupt.h>
irq 中断序号
handler 中断处理回调函数,1.3.1节定义
flags 中断类型,详细见下面描述
name 中断名称,可以在"/proc/interrupts"文件查看
dev 数据结构体,一般是设备数据结构,传递给回调函数第二个形参;设备共享中断线时(IRQF_SHARED),可以用来区分不同设备
返回 成功返回0,失败返回负数,中断已存放返回-EBUSY

关于中断类型(flags),“interrupt.h”有相关定义

#define IRQF_TRIGGER_NONE	0x00000000		/* 无触发中断 */
#define IRQF_TRIGGER_RISING	0x00000001		/* 上升沿触发 */
#define IRQF_TRIGGER_FALLING	0x00000002	/* 下降沿触发 */
#define IRQF_TRIGGER_HIGH	0x00000004		/* 高电平触发 */
#define IRQF_TRIGGER_LOW	0x00000008		/* 电平触发 */

#define IRQF_SHARED			0x00000080		/* 多个设备共享中断 */
#define IRQF_PROBE_SHARED	0x00000100	
#define __IRQF_TIMER		0x00000200
#define IRQF_PERCPU			0x00000400
#define IRQF_NOBALANCING	0x00000800
#define IRQF_IRQPOLL		0x00001000
#define IRQF_ONESHOT		0x00002000
#define IRQF_NO_SUSPEND		0x00004000
#define IRQF_FORCE_RESUME	0x00008000
#define IRQF_NO_THREAD		0x00010000
#define IRQF_EARLY_RESUME	0x00020000
#define IRQF_COND_SUSPEND	0x00040000


1.3.4 中断使能和失能

void disable_irq_nosync(unsigned int irq);	/* 失能中断,立即返回 */
bool disable_hardirq(unsigned int irq);
void disable_irq(unsigned int irq);			/* 失能中断,需等待中断执行完才返回 */
void disable_percpu_irq(unsigned int irq);
void enable_irq(unsigned int irq);			/* 使能中断 */

注:
调用“disable_irq”函数前,必须确保不会产生新中断,因为该函数需等待中断执行完才返回,如果有新中断一直产生,会导致阻塞。


1.3.5 中断释放

void free_irq(unsigned int, void *);

  设备退出时,必须释放中断。“free_irq”函数释放设备中断后,并会禁止设备中断,无需手动禁止。如果是共享中断,只有释放完最后一个设备才会禁止中断。


1.4 中断下半部实现

  linux内核提供了3种下半部实现方式,分别是soft tirq(软中断)、tasklet、work queue(工作队列),三种方式应用在不同的场合下。

  • 软中断用于重要场合,对执行时间要求比较高,倾向于提高系统性能
  • tasklet和工作队列用于大多数普通驱动
  • tasklet是在中断上下文执行,工作队列在内核线程执行,可以挂起(sleep)延迟处理。任务需挂起,用工作队列

1.4.1 软中断

  linux内核软中断用结构体“struct softirq_action”描述,位于“kernel/include/linux/interrupt.h”中,从原型看就是一个软中断处理回调函数指针,函数实体就是“下半部”处理的任务,由驱动工程师实现。

/* softirq mask and active fields moved to irq_cpustat_t in
 * asm/hardirq.h to get better cache usage.  KAO
 */

struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

  “interrupt.h”枚举了常用软中断类型。

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

enum
{
	HI_SOFTIRQ=0,			/* 最高优先级软中断 */
	TIMER_SOFTIRQ,			/* 定时器软中断 */
	NET_TX_SOFTIRQ,			/* 网络发送软中断 */
	NET_RX_SOFTIRQ,			/* 网络接收软中断 */
	BLOCK_SOFTIRQ,			/* 块操作软中断 */
	BLOCK_IOPOLL_SOFTIRQ,	/* 块IO轮询软中断 */
	TASKLET_SOFTIRQ,		/* tasklet软中断 */
	SCHED_SOFTIRQ,			/* 调度软中断 */
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */	/* 高精度定时器软中断 */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */	/* RCU软中断 */

	NR_SOFTIRQS		/* 软中断总数 */
};

软中断使用步骤

1)注册软中断

void open_softirq(int nr, void (*action)(struct softirq_action *));
参数 含义
nr 软中断类型,interrupt.h中枚举类型
action 软中断回调处理函数
返回

  软中断注册,必须采用“静态”方式,因为内核起来后会调用“softirq_init”初始化软中断。


2)触发软中断
  在上半部调用“raise_softirq”函数通知内核执行下半部。

void raise_softirq(unsigned int nr);
参数 含义
nr 软中断类型,interrupt.h中枚举类型
返回

1.4.2 tasklet

  tasklet本质是软中断,基于软中断封装实现的一种方式,tasklet的描述“struct tasklet_struct”结构体同样位于“kernel/include/linux/interrupt.h”中。

struct tasklet_struct
{
	struct tasklet_struct *next;	/* 链式存储,表示下一tasklet节点 */
	unsigned long state;			/* tasklet状态,TASKLET_STATE_SCHED表示被调度过程,TASKLET_STATE_RUN表示tasklet正在某个CPU上执行 */
	atomic_t count;					/* tasklet引用数,原子操作 */
	void (*func)(unsigned long);	/* 回调处理函数,下半部处理任务置于此 */
	unsigned long data;				/* 传递给回调函数fun的参数 */
};

count成员与tasklet状态相关,如果count等于0,tasklet处于enable状态,大于0则处于disable状态。

static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
	atomic_inc(&t->count);
	smp_mb__after_atomic();
}

static inline void tasklet_disable(struct tasklet_struct *t)
{
	tasklet_disable_nosync(t);	/* 自减 */
	tasklet_unlock_wait(t);
	smp_mb();
}

static inline void tasklet_enable(struct tasklet_struct *t)
{
	smp_mb__before_atomic();
	atomic_dec(&t->count);		/* 自加 */
}

tasklet使用步骤

1)注册tasklet

  使用tasklet机制,首先需定义一个“struct tasklet_struct”,可以动态和静态定义,然后调用“tasklet_init”函数初始化。

void tasklet_init(struct tasklet_struct *t,
			 void (*func)(unsigned long), unsigned long data);
参数 含义
t 需初始化的tasklet结构体实体地址
func 下半部回调处理函数
data 传递给回调函数fun的参数
返回

  linux内核“interrupt.h”中封装了一个初始化的宏“DECLARE_TASKLET”,也可以直接调用该宏初始化,传入参数与“tasklet_init”一致。

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

2)触发调度
  在上半部调用“tasklet_schedule”函数通知内核执行下半部调度。

void tasklet_schedule(struct tasklet_struct *t)
参数 含义
t 需调度的tasklet结构体实体地址
返回

1.4.3 工作队列

  工作队列与前两种方式最大不同是,下半部任务由内核创建一个线程进行处理。内核线程有可能被其他线程抢占,因此工作队列允许睡眠或者重新调度。如果下半部任务实时性要求不高,允许睡眠,则选择工作队列;否则选择软中断或者tasklet。工作队列是没有优先级的,多个工作队列时,是按照FIFO的方式进行处理。


  工作队列的方式下,把下半部任务封装为工作项(work),linux内核用“struct work_strcut”结构体描述,位于“kernel/include/workqueue.h”中。

struct work_struct {
	atomic_long_t data;		/* 传递给回调函数fun的参数 */
	struct list_head entry;	/* 指针入口 */
	work_func_t func;		/* 回调处理函数,下半部处理任务置于此 */
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

typedef void (*work_func_t)(struct work_struct *work);

  一系列工作项(work)组成工作队列(work queue),“workqueue_struct”结构体描述,原型位于“kernel/kernel/workqueue.c”中。

/*
 * The externally visible workqueue.  It relays the issued work items to
 * the appropriate worker_pool through its pool_workqueues.
 */
struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	struct list_head	list;		/* PR: list of all workqueues */

	struct mutex		mutex;		/* protects this wq */
	int			work_color;	/* WQ: current work color */
	int			flush_color;	/* WQ: current flush color */
	atomic_t		nr_pwqs_to_flush; /* flush in progress */
	struct wq_flusher	*first_flusher;	/* WQ: first flusher */
	struct list_head	flusher_queue;	/* WQ: flush waiters */
	struct list_head	flusher_overflow; /* WQ: flush overflow list */

	struct list_head	maydays;	/* MD: pwqs requesting rescue */
	struct worker		*rescuer;	/* I: rescue worker */

	int			nr_drainers;	/* WQ: drain in progress */
	int			saved_max_active; /* WQ: saved pwq max_active */

	struct workqueue_attrs	*unbound_attrs;	/* PW: only for unbound wqs */
	struct pool_workqueue	*dfl_pwq;	/* PW: only for unbound wqs */

#ifdef CONFIG_SYSFS
	struct wq_device	*wq_dev;	/* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map	lockdep_map;
#endif
	char			name[WQ_NAME_LEN]; /* I: workqueue name */
	
	......
};

工作队列使用步骤

  工作队列可以使用linux系统创建的队列和用户自定义队列,队列可以设定为延迟执行和非延迟执行。


1)注册工作项

  使用工作队列机制,首先需定义一个“struct work_struct”工作项,可以动态和静态定义。


  • 非延工作迟项注册
#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

#define DECLARE_WORK(n, f)						\
	struct work_struct n = __WORK_INITIALIZER(n, f)

  • 延迟工作项注册
#define INIT_DELAYED_WORK(_work, _func)					\
	__INIT_DELAYED_WORK(_work, _func, 0)
	
#define DECLARE_DELAYED_WORK(n, f)					\
	struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
参数 含义
_work 待注册的工作项,静态注册
_func 下半部回调处理函数
n 待注册的工作指针,动态注册
返回

注:
使用系统工作队列时,只需注册工作任务即可。


2)使用自定义队列

  • 创建工作队列

  如果使用用户自定义的工作队列,则首先需创建一个工作队列。创建工作队列,首先是定义一个“struct workqueue_struct”工作队列指针,调用“create_singlethread_workqueue”宏创建工作队列。

#define create_singlethread_workqueue(name)				\
	alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)
参数 含义
name 工作队列名称
返回 工作队列首地址

例子:

struct workqueue_struct *pworkqueue = create_singlethread_workqueue("wq0");

  • 绑定自定义工作队列
/* 非延迟工作队列绑定 */
bool queue_work(struct workqueue_struct *wq,
			      struct work_struct *work)
{
	return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}

/* 延迟工作队列绑定 */
bool queue_delayed_work(struct workqueue_struct *wq,
				      struct delayed_work *dwork,
				      unsigned long delay)
{
	return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}

参数 含义
wq 工作队列
work 工作项
delay 延时的执行的时钟节拍(非时间)
返回 成功返回true

例子:

struct work_struct work;
struct workqueue_struct *pwrokqueue;

void work_handle(struct work_struct *pw)
{
/* todo */
}

INIT_WORK(work, work_handle);
pworkqueue = create_singlethread_workqueue("wq0");
queue_work(pwrokqueue, &work);	/* 绑定自定义工作队列 */

  • 释放工作队列
void destroy_workqueue(struct workqueue_struct *wq)
参数 含义
wq 工作队列
返回

3)触发调度

  • 触发非延迟工作项
bool schedule_work(struct work_struct *work)

  • 触发延迟工作项
bool schedule_delayed_work(struct delayed_work *dwork,
					 unsigned long delay)
参数 含义
dwork 工作项
delay 延迟时钟节拍数
返回 成功返回true
	gq0{
			compatible = "gq0";
			gpios = <&gpio1, 10, GPIO_ACTIVE_LOW>;
			pinctrl-names = "default";
			pinctrl-0 = <&spi1_cs0_gpio>;		/* gpio模式 */
			interrupt-parent = <&gpio1>;	
			interrupts = <10, IRQ_TYPE_EDGE_BOTH>;	/* 上升沿和下降沿触发 */
			status = "okay";
	};
	
	spi1_cs0_gpio: spi1_cs0_gpio {
				rockchip,pins =
					<1 10 RK_FUNC_GPIO &pcfg_pull_none>,
			};

2. 中断驱动编写

  以gpio为例,编写一个gpio触发的中断驱动,并获取gpio状态值。使用的是GPIO1_B2端口。

在这里插入图片描述

2.1 实现方式

  • GPIO上升沿和下降沿触发中断
  • read函数通过等待队列挂起应用进程
  • 中断后触发同步信号唤醒进程读取IO状态

2.2 添加设备树

  GPIO1_B2引脚是复用引脚,可以复用为sp1的片选引脚(SP1_CSn0)。原设备树文件已添加 SPI1_CSn0的pin节点描述,在其他后增加GPIO属性描述。同时增加一个“gpioirq”的驱动节点信息。

  • pin设备树
/* 在rk3399.dtsi 中添加 */
spi1 {
	......
			spi1_cs0: spi1-cs0 {
			rockchip,pins =
					<1 10 RK_FUNC_2 &pcfg_pull_up>;
			};
			spi1_cs0_gpio: spi1_cs0_gpio {						/* 添加GPIO pin描述 */
				rockchip,pins =
					<1 10 RK_FUNC_GPIO &pcfg_pull_none>,
			};
	......
	}
			

  • 驱动节点设备树
/* 在rk3399-firefly-port.dtsi 中添加 */
	gq0{
			compatible = "gq0";
			gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
			pinctrl-names = "default";
			pinctrl-0 = <&spi1_cs0_gpio>;			/* gpio模式 */
			interrupt-parent = <&gpio1>;	
			interrupts = <10 IRQ_TYPE_EDGE_BOTH>;	/* 上升沿和下降沿触发 */
			status = "okay";
	};

注:
设备数下的中断类型描述,位于“irq.h”中,与前面1.3.3节描述的中断类型值是一致的,只是名称不一样。
enum {
IRQ_TYPE_NONE = 0x00000000,
IRQ_TYPE_EDGE_RISING = 0x00000001,
IRQ_TYPE_EDGE_FALLING = 0x00000002,
IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING),
IRQ_TYPE_LEVEL_HIGH = 0x00000004,
IRQ_TYPE_LEVEL_LOW = 0x00000008,
IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW | IRQ_TYPE_LEVEL_HIGH),
IRQ_TYPE_SENSE_MASK = 0x0000000f,
IRQ_TYPE_DEFAULT = IRQ_TYPE_SENSE_MASK,

};


2.3 设备数据结构

struct gpioirq_dev
{
	struct 	cdev 	dev;		/* 字符驱动 */
	dev_t			dev_id;		/* 设备ID */
	struct class 	*dev_class;	/* 设备类 */
	int 			gpio;		/* GPIO序号 */
	int				irq;		/* 中断序号 */
	wait_queue_head_t r_queue;	/* 等待队列 */
	bool			r_en;		/* 可读标识 */
	struct fasync_struct *r_sync;/* 内核通知应用信号 */
};

2.4 中断函数

static irqreturn_t gq0_irq_handle(int irq, void *dev_id)
{
	struct gpioirq_dev *p;

	p = (struct gpioirq_dev *)dev_id;
	p->r_en = true;
    wake_up_interruptible(&(p->r_queue));	/* 唤醒休眠进程 */

	/* 通知应用进程数据可读
	 * SIGIO:信号类型
	 * POLL_IN:普通数据可读
	 */
	kill_fasync(&p->r_sync, SIGIO, POLL_IN);	
	
	return IRQ_HANDLED;
}

2.5 状态读取函数

static ssize_t gq0_read(struct file *pfile, char __user *buf, size_t size, loff_t * offset) 
{ 
	int ret = 0;
	struct gpioirq_dev *p;
	char level = 0;
	
	p = pfile->private_data;

	wait_event_interruptible(p->r_queue, p->r_en);	/* 进程休眠,等待中断 */
	level = gpio_get_value(p->gpio);
	ret = copy_to_user(buf, &level, 1);
	
    return ret; 
}

2.6 中断注册

static int gq0_probe(struct platform_device *pdev)  
{     
    struct device *dev; 
	int ret = -1;
	dev_t	id = 0;
	struct device_node *nd;
	
	nd = pdev->dev.of_node;			/* 设备树节点 */
	if(nd == NULL)
	{
		printk("get node faileed\n");
		return -1;
	}
	gq0.gpio = of_get_named_gpio(nd, "gpios", 0);	/* 获取GPIO */
	if(gq0.gpio < 0)
	{
		printk("get gpio failed\n");
		return -1;
	}
	
	if (!gpio_is_valid(gq0.gpio)) 
	{
		printk("gpio [%d] is invalid\n", gq0.gpio);
		return -1;
	}
	ret = gpio_request(gq0.gpio, "gq0");		/* 申请GPIO */
	if(ret < 0)
	{
		printk("gpio request failed\n");
		return ret;
	}
	ret = gpio_direction_input(gq0.gpio);	
	
	//gq0.irq = gpio_to_irq(gq0.gpio);	/* 中断号映射 */
	gq0.irq = irq_of_parse_and_map(nd, 0);
	ret = request_irq(gq0.irq, gq0_irq_handle, gq0.irq_mode, "gq0", &gq0);/* 注册中断 */
	if(ret<0)
	{
		printk("request gq0 irq failed\n");
		free_irq(gq0.irq, &gq0);
		gpio_free(gq0.gpio);
		return ret;
	}
	......
}

2.7 platform 驱动

static struct of_device_id of_gq0_ids[] = {
   {.compatible = "gpioirq"},	/* 与节点设备树“compatible ”属性一致 */
   { }   
 };
 
static struct platform_driver gq0_driver = { 
	.driver   = { 
	.owner    = THIS_MODULE, 
	.name     = DEV_NAME, 
	.of_match_table = of_gq0_ids,
	}, 
	.probe 	  = gq0_probe, 
	.remove   = gq0_remove, 
};

module_platform_driver(gq0_driver); /* platform 驱动注册和注销 */

3. 源码

【1】https://github.com/Prry/rk3399


4. 参考

【1】https://blog.csdn.net/yhb1047818384/article/details/63687126

【2】http://www.wowotech.net/irq_subsystem/tasklet.html

【3】https://www.ibm.com/developerworks/cn/linux/l-cn-cncrrc-mngd-wkq/

原创文章 128 获赞 147 访问量 36万+

猜你喜欢

转载自blog.csdn.net/qq_20553613/article/details/103970096