Linux driver development | Linux interrupt

Keywords: Linux imx6ull

Linux interrupt

Using interrupts in IM6ULL bare metal requires a lot of work, such as configuring registers, enabling IRQ and so on. The Linux kernel provides a perfect interrupt framework. We only need to apply for interrupts and register interrupt processing functions. It is very convenient to use and does not need a series of complex register configurations

1. Introduction to interruption

1.1 interrupt API function

Each interrupt has an interrupt number, which can distinguish different interrupts. In the Linux kernel, an int variable is used to represent the interrupt number

  • request_irq function: to apply for an interrupt, this function will activate the interrupt, so it is not necessary to enable the interrupt manually
int request_irq(unsigned int 	irq,
				irq_handler_t	handler,
				unsigned long	flags,
				const char		*name,
				void			*dev)
//irq: interrupt number to apply for interrupt
//handler: interrupt handling function. This function will be executed after an interrupt occurs
//flags: interrupt flag, defined in include/linux/interrupt.h
//Name: interrupt name. After setting, you can see the corresponding interrupt name in / proc/interrupts
//Dev: if flags is set to irqf_ If shared, dev is used to distinguish different interrupts
//Return value: 0 indicates that the interrupt application is successful, other negative values indicate that the interrupt application fails. If - EBUSY is returned, it indicates that the interrupt has been applied
  • free_irq function: release the interrupt, which will delete the interrupt handling function and prohibit the interrupt
void free_irq(unsigned int irq,
			  void *dev)
//irq: interrupt to release
//Dev: if flags is set to irqf_ If shared, dev is used to distinguish different interrupts
//     Shared interrupts are disabled only when the last interrupt handler is released
//Return value: None
  • Interrupt processing function: the interrupt processing function needs to be set when applying for an interrupt, and its format is as follows
irqreturn_t (*irq_handler_t) (int, void *)
//Return value irqreturn_t is an enumeration type with three return values:
enum irqreturn {
	IRQ_NONE = (0 << 0),
	IRQ_HANDLED = (1 << 0),
	IRQ_WAKE_THREAD = (1 << 1),
typedef enum irqreturn irqreturn_t;
//The return value of general interrupt service function is in the following form:
  • Interrupt enable and disable functions
void enable_irq(unsigned int irq);	//Enables the specified interrupt
void disable_irq(unsigned int irq);	//Interrupt is prohibited. It will not return until the current processing function is executed
void disable_irq_nosync(unsigned int irq);	//Interrupt is prohibited. Return immediately after execution
local_irq_enable();		//Enables the current processor to interrupt the system
local_irq_disable();	//Disable the current processor from interrupting the system
local_irq_save(flags);	//Interrupt is prohibited and the interrupt status is saved in flags
local_irq_restore(flags);  //Recover the interrupt and save the interrupt status in flags
1.2 upper and lower half

Linux kernel divides interrupts into upper and lower parts, and its main purpose is to realize fast in and fast out of interrupt processing functions.

  • Upper part: the upper part is the interrupt processing function. The processing process is relatively fast and will not take a long time
    To finish on the top half
  • The second half: if the interrupt processing process is time-consuming, put forward these time-consuming codes and give them to the second half for execution, so that the interrupt processing function will be fast in and fast out

Generally, if the content to be processed does not want to be interrupted by other interrupts, the task to be processed is time sensitive, or the task to be processed is related to hardware, it is usually placed in the top half. In other cases, priority should be given to the lower half.

The upper half can be directly processed by writing interrupt processing functions. The Linux kernel also provides a variety of lower half mechanisms:

  • Soft interrupt: the kernel uses the structure softirq_action represents soft interrupt, which is defined in the file include/linux/interrupt.h
struct softirq_action{
	void (*action)(struct softirq_action *);
//A total of 10 soft interrupts are defined in the file kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS];
//NR_SOFTIRQS is an enumeration type defined in the file include/linux/interrupt.h
	HI_SOFTIRQ=0, 		/* High priority soft interrupt */
	TIMER_SOFTIRQ, 		/* Timer soft interrupt */
	NET_TX_SOFTIRQ, 	/* Network data sending soft interrupt */
	NET_RX_SOFTIRQ, 	/* Network data receiving soft interrupt */
	TASKLET_SOFTIRQ, 	/* tasklet Soft interrupt */
	SCHED_SOFTIRQ, 		/* Scheduling soft interrupt */
	HRTIMER_SOFTIRQ, 	/* High precision timer soft interrupt */
	RCU_SOFTIRQ, 		/* RCU Soft interrupt */

To use soft interrupts, you must first use open_softirq function registers the corresponding soft interrupt processing function. The prototype is as follows:

void open_softirq(int nr, void (*action)(struct softirq_action *));
//nr: soft interrupt to be turned on
//action: processing function corresponding to soft interrupt
//Return value: None

After registering the soft interrupt, you need to pass raise_softirq function is triggered, and its prototype is as follows:

void raise_softirq(unsigned int nr);
//nr: soft interrupt to trigger
//Return value: None

Soft interrupts must be statically registered at compile time. The Linux kernel uses softirq_init function initializes the soft interrupt, which is defined in the kernel/softirq.c file, as follows:

void __init softirq_init(void)
	int cpu;

	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;

	open_softirq(TASKLET_SOFTIRQ, tasklet_action);//Tasklet opens by default_ SOFTIRQ
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);//Hi is on by default_ SOFTIRQ
  • Tasklet: tasklet is another lower half mechanism implemented by soft interrupt. It is recommended to use tasklet between soft interrupt and tasklet. The kernel uses tasklets_ Struct structure to represent the tasklet
struct tasklet_struct
	struct tasklet_struct *next; 	/* Next tasklet */
	unsigned long state; 			/* tasklet state */
	atomic_t count; 				/* Counter to record the number of references to the tasklet */
	void (*func)(unsigned long); 	/* tasklet Functions executed */
	unsigned long data; 			/* Arguments to func function */

To use a tasklet, you must first define a tasklet and then use the tasklet_init function initializes tasklet, and its function prototype is as follows:

void tasklet_init(struct tasklet_struct *t,
				  void (*func)(unsigned long),
				  unsigned long data);
//t: tasklet to initialize
//func: handling function of tasklet
//data: parameter to pass to func function

You can also use the macro DECLARE_TASKLET to complete the definition and initialization of tasklet at one time_ Tasklet is defined in the include/linux/interrupt.h file as follows:

DECLARE_TASKLET(name, func, data)
//Name: name of the tasklet to be defined
//func: handling function of tasklet
//data: parameters passed to func function

In the upper part, that is, calling tasklet_ in interrupt handling function. The schedule function can make the tasklet run at the right time_ The prototype of schedule function is as follows:

void tasklet_schedule(struct tasklet_struct *t)
//t: The tasklet to schedule, namely declare_ name in tasklet macro

The reference usage example of tasklet is as follows:

/* Define taselet */
struct tasklet_struct testtasklet;
/* tasklet Processing function */
void testtasklet_func(unsigned long data)
	/* tasklet Specific processing content */
/* Interrupt handling function */
irqreturn_t test_handler(int irq, void *dev_id)
	/* Scheduling tasklet s */
/* Drive entry function */
static int __init xxxx_init(void)
	/* Initialize tasklet */
	tasklet_init(&testtasklet, testtasklet_func, data);
	/* Register interrupt handler */
	request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
  • Work queue: work queue is another lower half execution method. Work queue executes in the process context. Work queue hands over the work to be postponed to a kernel thread for execution. Because work queue works in the process context, work queue allows sleep or rescheduling. Therefore, if you want to put off work to sleep, you can choose work queue, otherwise you can only choose soft interrupt or tasklet
//The kernel uses work_ The struct structure represents a work
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func; 	/* Work queue processing function */
//These jobs are organized into work queues, which use workqueue_struct structure representation
struct workqueue_struct {
	struct list_head pwqs;
	struct list_head list;
	struct mutex mutex;
	int work_color;
	int flush_color;
	atomic_t nr_pwqs_to_flush;
	struct wq_flusher *first_flusher;
	struct list_head flusher_queue;
	struct list_head flusher_overflow;
	struct list_head maydays;
	struct worker *rescuer;
	int nr_drainers;
	int saved_max_active;
	struct workqueue_attrs *unbound_attrs;
	struct pool_workqueue *dfl_pwq;
	char name[WQ_NAME_LEN];
	struct rcu_head rcu;
	unsigned int flags ____cacheline_aligned;
	struct pool_workqueue __percpu *cpu_pwqs;
	struct pool_workqueue __rcu *numa_pwq_tbl[];
//The kernel uses worker threads to process various jobs in the work queue. The worker structure is as follows:
struct worker {
	union {
		struct list_head entry;
		struct hlist_node hentry;
	struct work_struct *current_work;
	work_func_t current_func;
	struct pool_workqueue *current_pwq;
	bool desc_valid;
	struct list_head scheduled;
	struct task_struct *task;
	struct worker_pool *pool;
	struct list_head node;
	unsigned long last_active;
	unsigned int flags;
	int id;
	char desc[WORKER_DESC_LEN];
	struct workqueue_struct *rescue_wq;

Each worker has a work queue, and the worker thread processes all the work in its own work queue. In actual driver development, you only need to define work_struct. Create a work_ Struct structure
Variable, and then use INIT_WORK macro to initialize work, init_ The work macro is defined as follows:

#define INIT_WORK(_work, _func)
//_ Work indicates the work to be initialized
//_ func is the processing function corresponding to the work

You can also use declare_ The work macro completes the creation and initialization of work at one time. The macro definition is as follows:

#define DECLARE_WORK(n, f)
//n represents the defined work_struct
//f represents the processing function corresponding to the work

Work also needs scheduling to run. The scheduling function of work is schedule_work, the prototype is as follows:

bool schedule_work(struct work_struct *work)
//Work: the work to be scheduled
//Return value: 0 succeeded, other values failed

An example of the reference use of a work queue is as follows:

/* Define work */
struct work_struct testwork;
/* work Processing function */
void testwork_func_t(struct work_struct *work);
	/* work Specific processing content */
/* Interrupt handling function */
irqreturn_t test_handler(int irq, void *dev_id)
	/* Scheduling work */
/* Drive entry function */
static int __init xxxx_init(void)
	/* Initialize work */
	INIT_WORK(&testwork, testwork_func_t);
	/* Register interrupt handler */
	request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
1.3 equipment tree interrupt information node

If you use the device tree, you need to set the interrupt attribute information in the device tree. The Linux kernel configures the interrupt by reading the interrupt attribute information in the device tree. For the interrupt controller, refer to the document Documentation/devicetree/bindings/arm/gic.txt for the device tree binding information, and open the imx6ull.dtsi file. The intc node is the interrupt controller node of I.MX6ULL. The node content is as follows:

intc: interrupt-controller@00a01000 {
	compatible = "arm,cortex-a7-gic";
	#interrupt-cells = <3>; 	// Cells size is 3
	interrupt-controller;	//If the node is empty, it indicates that the current node is an interrupt controller
	reg = <0x00a01000 0x1000>,
		<0x00a02000 0x100>;

For GPIO, the GPIO node can also be used as an interrupt controller, such as the gpio5 node in the imx6ull.dtsi file, as shown below

gpio5: gpio@020ac000 {
	compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
	reg = <0x020ac000 0x4000>;
	//interrupts describes the interrupt information source (74 and 75). The interrupt type is SPI and triggered at high level
	interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
	#gpio-cells = <2>;
	interrupt-controller; //Indicates that the gpio5 node is an interrupt controller
	#interrupt-cells = <2>;

Open the imx6ull-alientek-emmc.dts file with the following contents:

//fxls8471 is a magnetometer chip on the official 6ULL development board of NXP, which has an interrupt pin connected to the
//Snvs of IMX6ULL_ On the tamper0 pin, this pin can be multiplexed to GPIO5_IO00
fxls8471@1e {
	compatible = "fsl,fxls8471";
	reg = <0x1e>;
	position = <0>;
	interrupt-parent = <&gpio5>; //Set interrupt controller, gpio5 here
	interrupts = <0 8>; //Set interrupt information. 0 means GPIO5_IO00, 8 indicates low level trigger

To sum up, the device tree attribute information related to interrupt includes:

  • #Interrupt cells: Specifies the number of interrupt source information cells
  • Interrupt controller, indicating that the current node is an interrupt controller
  • interrupts, specifying interrupt number, trigger method, etc
  • Interrupt parent specifies the parent interrupt, that is, the interrupt controller
1.4 get interrupt number

After the interrupt information is written into the device tree, if you want to use the interrupt number when writing the driver, you can use IRQ_ of_ parse_ and_ The map function extracts the corresponding device number from the interupts attribute. The function prototype is as follows:

unsigned int irq_of_parse_and_map(struct device_node *dev,
								  int index)
//dev: device node
//Index: index number. The interrupts attribute can contain multiple pieces of interrupt information. The index number is used to specify the information to be obtained
//Return value: interrupt number

If GPIO interrupt is used, GPIO can be used_ to_ IRQ function to obtain the interrupt number corresponding to GPIO. The function prototype is as follows:

int gpio_to_irq(unsigned int gpio)
//GPIO: GPIO number to get
//Return value: interrupt number corresponding to GPIO

2. Hardware principle

3. Preparation of experimental program

In this experiment, interrupt is used to drive the KEY0 key on the IMX6ULL development board, and the key jitter is eliminated through the timer. The application program reads the key value and prints it through the terminal

3.1 modify equipment tree file

Pressing KEY0 uses the interrupt mode, so you need to add interrupt related attributes under the key node

key {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "atkalpha-key";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_key>;
	key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
	interrupt-parent = <&gpio1>; //Set gpio1 as interrupt controller
	interrupts = <18 IRQ_TYPE_EDGE_BOTH>; //The rising and falling edges of IO 18 of GPIO1 group are triggered
	status = "okay";

After the device tree is written, use the "make dtbs" command to recompile the device tree and start the linux system with the new device tree file

3.2 key interrupt driver programming

After the device tree is ready, you can write the driver. Create a new "imx6uirq" folder, create a vscode project in the folder, create a new imx6uirq.c file, and write the program

#define IMX6UIRQ_CNT 1  				/*  Number of equipment numbers*/
#define IMX6UIRQ_NAME "imx6uirq"  	/*  Name*/
#define KEY0VALUE 0X01  				/*  KEY0 key value*/
#define INVAKEY 0XFF  				/*  Invalid key value*/
#define KEY_NUM 1  					/*  Number of keys*/

/* Interrupt IO description structure */
struct irq_keydesc {
	int gpio; 						/* gpio */
	int irqnum; 					/* interrupt number */
	unsigned char value; 			/* Key value corresponding to key */
	char name[10]; 					/* name */
	irqreturn_t (*handler)(int, void *); /* Interrupt service function */

/* imx6uirq Equipment structure */
struct imx6uirq_dev{
	dev_t devid; 					/* Equipment number */
	struct cdev cdev; 				/* cdev */
	struct class *class; 			/* class */
	struct device *device; 			/* equipment */
	int major; 						/* Main equipment No */
	int minor; 						/* Secondary equipment No */
	struct device_node *nd; 		/* Device node */
	atomic_t keyvalue; 				/* Valid key values */
	atomic_t releasekey; 			/* Key to mark whether to complete one-time completion*/
	struct timer_list timer; 		/* Define a timer*/
	struct irq_keydesc irqkeydesc[KEY_NUM]; /* Key description array */
	unsigned char curkeynum; 		/* Current key number */

struct imx6uirq_dev imx6uirq; /* irq equipment */

/* Interrupt the service function and start the timer with a delay of 10ms */
static irqreturn_t key0_handler(int irq, void *dev_id){
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
	dev->curkeynum = 0;
	dev-> = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));

/* The timer service function is used for key chattering elimination. After the timer arrives, the key value is read again,
   If the key is still pressed, it means that the key is valid */
void timer_function(unsigned long arg){
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];
	value = gpio_get_value(keydesc->gpio); /* Read IO value */
	if(value == 0){ 	/* Press the key */
		atomic_set(&dev->keyvalue, keydesc->value);
	else{ 				/* Key release */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1); /* Mark release key */

/* Key IO initialization */
static int keyio_init(void){
	unsigned char i = 0;
	int ret = 0;
	imx6uirq.nd = of_find_node_by_path("/key");
	if (imx6uirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;

	/* Extract GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,
													"key-gpio", i);
		if (imx6uirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);

	/* Initialize the IO used by the key and set it to interrupt mode */
	for (i = 0; i < KEY_NUM; i++) {
		memset(imx6uirq.irqkeydesc[i].name, 0,
		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);
		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(
		printk("key%d:gpio=%d, irqnum=%d\r\n",i,

	/* Application interruption */
	imx6uirq.irqkeydesc[0].handler = key0_handler;
	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum,
		imx6uirq.irqkeydesc[i].name, &imx6uirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n",imx6uirq.irqkeydesc[i].irqnum);
			return -EFAULT;

	/* Create timer */
	imx6uirq.timer.function = timer_function;
	return 0;

/* open device */
static int imx6uirq_open(struct inode *inode, struct file *filp){
	filp->private_data = &imx6uirq; /* Set private data */
	return 0;

/* Read data from device */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt){
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char releasekey = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* A key is pressed */
		if (keyvalue & 0x80) {
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		} else {
			goto data_error;
		atomic_set(&dev->releasekey, 0); /* Press the flag to clear */
	} else {
		goto data_error;
	return 0;
	return -EINVAL;

/* Device operation function */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,

/* Drive entry function */
static int __init imx6uirq_init(void){
	/* 1,Build equipment number */
	if (imx6uirq.major) {
		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT,IMX6UIRQ_NAME);
	} else {
		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT,IMX6UIRQ_NAME);
		imx6uirq.major = MAJOR(imx6uirq.devid);
		imx6uirq.minor = MINOR(imx6uirq.devid);

	/* 2,Register character device */
	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
 	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

	/* 3,Create class */
	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.class)) {
		return PTR_ERR(imx6uirq.class);

	/* 4,Create device */
	imx6uirq.device = device_create(imx6uirq.class, NULL,
					imx6uirq.devid, NULL, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.device)) {
		return PTR_ERR(imx6uirq.device);

	/* 5, Initialization key */
	atomic_set(&imx6uirq.keyvalue, INVAKEY);
 	atomic_set(&imx6uirq.releasekey, 0);
	return 0;

/* Drive exit function */
static void __exit imx6uirq_exit(void){
	unsigned int i = 0;
	/* Delete timer */

	/* Release interrupt */
 	for (i = 0; i < KEY_NUM; i++) {
 		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
 	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
	device_destroy(imx6uirq.class, imx6uirq.devid);

3.3 test program preparation

The test APP obtains the key value by constantly reading the / dev/imx6uirq file. When the key is pressed, the obtained key value will be output to the terminal. Create a new file named imx6uirqApp.c and write test code

int main(int argc, char *argv[]){
	int fd;
	int ret = 0;
	char *filename;
	unsigned char data;
	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;

	while (1) {
		ret = read(fd, &data, sizeof(data));
		if (ret < 0) { /* Data read error or invalid */

		} else { /* Data read correctly */
			if (data) /* Read data */
			printf("key value = %#X\r\n", data);
	return ret;

4. Operation test

4.1 program compilation
  • Modify Makefile compilation target variable
obj-m := imx6uirq.o
  • Use "make -j32" to compile the driver module file
make -j32
  • Use the "arm linux gnueabihf GCC" command to compile the test APP
arm-linux-gnueabihf-gcc imx6uirqApp.c -o imx6uirqApp
4.2 operation test
  • Copy the driver file and APP executable file to "rootfs/lib/modules/4.1.15"
  • When loading the driver for the first time, use the "depmod" command
  • Use the "modprobe" command to load the driver
modprobe imx6uirq.ko
  • After the driver is loaded successfully, you can check whether the corresponding interrupt is registered successfully by viewing the / proc/interrupts file
cat /proc/interrupts
  • Use the ". / imx6uirqApp /dev/imx6uirq" command to test the interrupt

  • Press the KEY0 key on the development board, and the terminal will output the key value

  • Use the "rmmod" command to unload the driver

rmmod imx6uirq.ko

Posted by Huntress on Thu, 18 Nov 2021 21:18:52 -0800